import { describe, it, expect, beforeAll, vi, afterEach, beforeEach } from 'vitest' import * as db from '../src/database.js' import { registerSubscription, CONFIRM_EMAIL_COOLDOWN_SECONDS } from '../src/actions.js' import * as mailer from '../src/mailer.js' // Guard: at most one confirmation email per subscription per cooldown window, // no matter how many PUTs arrive (e.g. a page-refresh storm while unconfirmed). vi.mock('../src/mailer.js', async (importOriginal) => { const actual: any = await importOriginal() return { ...actual, sendConfirm: vi.fn(async () => {}) } }) const unique = (label: string) => `${label}-${Date.now()}-${Math.random().toString(36).slice(2)}` const pubkey = unique('cooldown') const email = `${unique('cooldown')}@example.com` const confirmCalls = () => vi.mocked(mailer.sendConfirm).mock.calls.length describe('Confirmation email cooldown', () => { beforeAll(async () => { await db.migrate() }) beforeEach(() => { vi.mocked(mailer.sendConfirm).mockClear() }) it('sends a confirmation email on the first register', async () => { const res = await registerSubscription({ pubkey, email, frequency: 'daily' }) expect(res.key).toBeTruthy() expect(confirmCalls()).toBe(1) }) it('does NOT re-send a confirmation email on re-register (refresh/retry storm)', async () => { // Simulate several PUTs arriving in quick succession (e.g. page reloads). for (let i = 0; i < 5; i++) { await registerSubscription({ pubkey, email, frequency: 'daily' }) } expect(confirmCalls()).toBe(0) }) it('a new frequency (setting change) does not re-send', async () => { await registerSubscription({ pubkey, email, frequency: 'weekly' }) expect(confirmCalls()).toBe(0) }) it('changing the email address sends immediately (cooldown reset)', async () => { const newEmail = `${unique('cooldown')}@example.com` await registerSubscription({ pubkey, email: newEmail, frequency: 'daily' }) expect(confirmCalls()).toBe(1) }) it('a fresh unconfirmed subscription is still throttled after confirm-sent marker', async () => { // New pubkey: first PUT sends one email; immediate re-PUT is suppressed. const pk2 = unique('cooldown2') const em2 = `${unique('cooldown2')}@example.com` await registerSubscription({ pubkey: pk2, email: em2, frequency: 'daily' }) await registerSubscription({ pubkey: pk2, email: em2, frequency: 'daily' }) expect(confirmCalls()).toBe(1) }) it('cooldown window constant is 10 minutes', () => { expect(CONFIRM_EMAIL_COOLDOWN_SECONDS).toBe(600) }) }) describe('Cooldown reset on subscription reactivation (same email, off/on cycle)', () => { const k = unique('cooldown-reactivate') const e = `${unique('cooldown-reactivate')}@example.com` beforeAll(async () => { await db.migrate() }) beforeEach(() => { vi.mocked(mailer.sendConfirm).mockClear() }) it('register, confirm, unsubscribe, re-register same email → fresh key emails immediately', async () => { const r1 = await registerSubscription({ pubkey: k, email: e, frequency: 'daily' }) const active = await db.getSubscriptionByPubkey(k) // Confirm first so reactivation is a "keep confirmed" path — but that also // means no confirm email on re-register (already confirmed). Verify the row // is reactivated with a fresh key, not a duplicate. const confirmed = await db.confirmSubscription(active!.key) expect(confirmed).toBeTruthy() await db.unsubscribeSubscription(active!.key) vi.mocked(mailer.sendConfirm).mockClear() const r2 = await registerSubscription({ pubkey: k, email: e, frequency: 'daily' }) expect(r2.key).not.toBe(r1.key) // fresh key issued expect(confirmCalls()).toBe(0) // still confirmed → no new email const rows = await db.getAllSubscriptionsByPubkey(k) expect(rows).toHaveLength(1) }) })