diff --git a/src/database.ts b/src/database.ts index 0fbc0c8..f80148c 100644 --- a/src/database.ts +++ b/src/database.ts @@ -152,13 +152,13 @@ export const updateSubscription = instrument( } ) -const getMostRecentTombstonedSubscription = async (pubkey: string) => +const getMostRecentTombstonedSubscription = async (pubkey: string, email: string) => parseSubscription( await get( `SELECT * FROM subscriptions - WHERE pubkey = ? AND unsubscribed_at IS NOT NULL - ORDER BY created_at DESC, id DESC LIMIT 1`, - [pubkey] + WHERE pubkey = ? AND email = ? AND unsubscribed_at IS NOT NULL + ORDER BY rowid DESC LIMIT 1`, + [pubkey, email] ) ) @@ -180,13 +180,13 @@ export const insertSubscription = instrument( return assertResult(await updateSubscription(existing, email, frequency)) } - // No active row. If this account previously subscribed to the same email - // and was unsubscribed, reactivate that row instead of inserting a fresh - // one. Otherwise every turn-on → turn-off → turn-on cycle would create a - // new row, abandoning the confirmed state (and the already-known key). - const tombstoned = await getMostRecentTombstonedSubscription(pubkey) + // No active row. If this account previously subscribed to this email and + // was unsubscribed, reactivate the most recent such row instead of inserting + // a fresh one. Otherwise every turn-on → turn-off → turn-on cycle would + // create a new row, abandoning the confirmed state (and the already-known key). + const tombstoned = await getMostRecentTombstonedSubscription(pubkey, email) - if (tombstoned?.email === email) { + if (tombstoned) { try { return assertResult(await reactivateSubscription(tombstoned, frequency)) } catch (err: any) { diff --git a/test/resubscribe-reactivates.test.ts b/test/resubscribe-reactivates.test.ts index dac8ed3..f779bf3 100644 --- a/test/resubscribe-reactivates.test.ts +++ b/test/resubscribe-reactivates.test.ts @@ -149,4 +149,40 @@ describe('Re-subscribe with a changed frequency reactivates and updates cadence' const rows = await db.getAllSubscriptionsByPubkey(pubkey) expect(rows).toHaveLength(1) }) +}) + +describe('Re-subscribe with mixed emails for one pubkey picks the right row', () => { + const pubkey = unique('resub-mixed') + const emailA = `${unique('resub-mixed-a')}@example.com` + const emailB = `${unique('resub-mixed-b')}@example.com` + + beforeAll(async () => { + await db.migrate() + }) + + it('creates and tombstones two different emails, then re-subscribes email B', async () => { + // Email A cycle + const subA = await db.insertSubscription(pubkey, emailA, 'daily') + await db.unsubscribeSubscription(subA.key) + + // Email B cycle + const subB = await db.insertSubscription(pubkey, emailB, 'daily') + const bId = subB!.id + await db.unsubscribeSubscription(subB.key) + + // Re-subscribe with email B — must reactivate B's own row, not A's, + // and must not resurrect the wrong email. + const resigned = await db.insertSubscription(pubkey, emailB, 'daily') + const active = await db.getSubscriptionByPubkey(pubkey) + + expect(active!.id).toBe(bId) + expect(active!.id).not.toBe(subA!.id) + expect(active!.email).toBe(emailB) + expect(active!.unsubscribed_at).toBeFalsy() + + const rows = await db.getAllSubscriptionsByPubkey(pubkey) + expect(rows).toHaveLength(2) + expect(rows.filter(r => r.email === emailA)).toHaveLength(1) + expect(rows.filter(r => r.email === emailB)).toHaveLength(1) + }) }) \ No newline at end of file