import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest' import * as db from '../src/database.js' import { registerSubscription } from '../src/actions.js' import * as mailer from '../src/mailer.js' // Regression guards for the row-reactivation fix (src/database.ts): // * same-email re-subscribe → reactivates the SAME row and must NOT email a // stale confirmation code to an unrelated address // * new-email re-subscribe → MUST create a fresh row (fresh key) so the // confirmation email carries a code bound to the new address // // We mock the mailer so these run hermetically (the unit env's SMTP is a dummy). vi.mock('../src/mailer.js', async (importOriginal) => { const actual: any = await importOriginal() return { ...actual, sendConfirm: vi.fn(async () => {}) } }) const pubkey = 'new-email-' + Date.now() + '-' + Math.random().toString(36).slice(2) const oldEmail = `${pubkey}-old@example.com` const newEmail = `${pubkey}-new@example.com` let subscribedIds: string[] = [] const subscribeIdsFor = async () => { const rows = await db.getAllSubscriptionsByPubkey(pubkey) subscribedIds = rows.map((r: any) => r.id) return rows } describe('Re-subscribe with a NEW email dispatches the correct confirmation code', () => { beforeAll(async () => { await db.migrate() vi.mocked(mailer.sendConfirm).mockClear() }) afterAll(async () => { const key = (await db.getAllSubscriptionsByPubkey(pubkey))[0]?.key if (key) await db.unsubscribeSubscription(key) }) it('registers + confirms the original email', async () => { const result = await registerSubscription({ pubkey, email: oldEmail, frequency: 'daily' }) expect(result.key).toBeTruthy() await db.confirmSubscription(result.key) expect(vi.mocked(mailer.sendConfirm)).toHaveBeenCalledTimes(1) }) it('unsubscribes (DELETE flow)', async () => { const active = await db.getSubscriptionByPubkey(pubkey) await db.unsubscribeSubscription(active!.key) expect(await db.getSubscriptionByPubkey(pubkey)).toBeFalsy() }) it('re-registering with the NEW email creates a fresh row, not a reactivation', async () => { vi.mocked(mailer.sendConfirm).mockClear() const result = await registerSubscription({ pubkey, email: newEmail, frequency: 'daily' }) // A NEW confirmation email was sent — to the NEW address, with the key // returned to the caller (which the caller uses to confirm). const sent = vi.mocked(mailer.sendConfirm).mock.calls[0][0] expect(sent.email).toBe(newEmail) expect(sent.key).toBe(result.key) // And that key actually confirms the new-email row (not the old one). const confirmed = await db.confirmSubscription(result.key) expect(confirmed!.sub.email).toBe(newEmail) expect(confirmed!.alreadyConfirmed).toBe(false) }) it('leaves the old row and new row as distinct rows', async () => { const rows = await subscribeIdsFor() expect(rows).toHaveLength(2) expect(new Set(subscribedIds).size).toBe(2) }) }) describe('Reactivating the SAME email never emails an unrelated address', () => { const k = 'same-email-' + Date.now() + '-' + Math.random().toString(36).slice(2) const email = `${k}@example.com` beforeAll(async () => { vi.mocked(mailer.sendConfirm).mockClear() }) afterAll(async () => { const key = (await db.getAllSubscriptionsByPubkey(k))[0]?.key if (key) await db.unsubscribeSubscription(key) }) it('register + confirm + unsubscribe, then re-register the same email', async () => { const r1 = await registerSubscription({ pubkey: k, email, frequency: 'daily' }) await db.confirmSubscription(r1.key) const row1 = (await db.getAllSubscriptionsByPubkey(k))[0] await db.unsubscribeSubscription(r1.key) vi.mocked(mailer.sendConfirm).mockClear() const r2 = await registerSubscription({ pubkey: k, email, frequency: 'daily' }) // Same row, still confirmed, but a FRESH key is issued — no new // confirmation email, and old tokens for this row are invalidated. const rows = await db.getAllSubscriptionsByPubkey(k) expect(rows).toHaveLength(1) expect(r2.key).not.toBe(r1.key) expect(rows[0].id).toBe(row1.id) expect(rows[0].confirmed_at).toBeTruthy() expect(vi.mocked(mailer.sendConfirm)).not.toHaveBeenCalled() }) })