Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -1319,7 +1322,7 @@ export async function upsertDigestSubscription(

export async function listDigestSubscriptionsForLogin(env: Env, login: string): Promise<DigestSubscriptionRecord[]> {
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);
}

Expand Down
15 changes: 15 additions & 0 deletions test/unit/product-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
Loading