109 lines
No EOL
4.2 KiB
TypeScript
109 lines
No EOL
4.2 KiB
TypeScript
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()
|
|
})
|
|
}) |