All checks were successful
CI / checks (pull_request) Successful in 41s
- Add last_confirm_sent_at to subscriptions, with a migration for
existing databases. registerSubscription now sends at most one
confirmation email per 10 minutes per subscription, so a page-refresh
or client retry storm can't spam an inbox. The cooldown resets when
the address changes or a tombstoned row is reactivated (fresh key),
so genuinely new addresses always get an email immediately.
- Restore getCronExpression to the documented daily (17:00 UTC) and
weekly (Monday 17:00 UTC) schedules with the 6-field cron syntax the
cron package expects, fixing the pre-existing reschedule tests that the
'run every minute' testing hack had broken.
- Add confirm-email-cooldown.test.ts covering first send, refresh storms,
frequency changes, email changes, reactivation, and the 10-minute constant.
(cherry picked from commit 051c1e88fb)
101 lines
No EOL
3.8 KiB
TypeScript
101 lines
No EOL
3.8 KiB
TypeScript
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)
|
|
})
|
|
}) |