diff --git a/src/db/repositories.ts b/src/db/repositories.ts index bee97c2fd..d461f16dd 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -1283,7 +1283,10 @@ export async function upsertDigestSubscription( const now = nowIso(); const record: DigestSubscriptionRecord = { id: crypto.randomUUID(), - login: input.login, + // GitHub logins are case-insensitive, so normalize like every sibling subscription path + // (notification subscriptions, issue-watch) — otherwise a subscriber stored as "Foo" is missed on a + // "foo" lookup and the [login, email] conflict target accumulates case-variant duplicate rows. + login: input.login.toLowerCase(), email: input.email.toLowerCase(), status: input.status ?? "active", source: input.source ?? "app", @@ -1319,7 +1322,7 @@ export async function upsertDigestSubscription( export async function listDigestSubscriptionsForLogin(env: Env, login: string): Promise { const db = getDb(env.DB); - const rows = await db.select().from(digestSubscriptions).where(eq(digestSubscriptions.login, login)).orderBy(desc(digestSubscriptions.updatedAt)).limit(20); + const rows = await db.select().from(digestSubscriptions).where(eq(digestSubscriptions.login, login.toLowerCase())).orderBy(desc(digestSubscriptions.updatedAt)).limit(20); return rows.map(toDigestSubscriptionRecord); } diff --git a/test/unit/product-usage.test.ts b/test/unit/product-usage.test.ts index c16df6d7c..77d0a8ed7 100644 --- a/test/unit/product-usage.test.ts +++ b/test/unit/product-usage.test.ts @@ -326,6 +326,21 @@ describe("product usage events", () => { ).resolves.toBeUndefined(); }); + it("matches digest subscriptions case-insensitively by login and dedupes case-variant logins", async () => { + const env = createTestEnv(); + // Subscribe under a mixed-case login, then look up under a different casing — GitHub logins are + // case-insensitive, so it must still resolve (mirrors the notification/issue-watch subscription paths). + await upsertDigestSubscription(env, { login: "OktoFeesh1", email: "digest@example.com" }); + await expect(listDigestSubscriptionsForLogin(env, "oktofeesh1")).resolves.toEqual([ + expect.objectContaining({ login: "oktofeesh1", email: "digest@example.com", status: "active" }), + ]); + // Re-subscribing under another casing with the same email updates the one row instead of duplicating it. + await upsertDigestSubscription(env, { login: "OKTOFEESH1", email: "digest@example.com", status: "paused" }); + await expect(listDigestSubscriptionsForLogin(env, "Oktofeesh1")).resolves.toEqual([ + expect.objectContaining({ login: "oktofeesh1", email: "digest@example.com", status: "paused" }), + ]); + }); + it("summarizes recent events without counting stale records", async () => { const env = createTestEnv({ PRODUCT_USAGE_HASH_SALT: "fixed-test-salt" }); await recordProductUsageEvent(env, {