188 lines
No EOL
7 KiB
TypeScript
188 lines
No EOL
7 KiB
TypeScript
import { describe, it, expect, beforeAll } from 'vitest'
|
|
import * as db from '../src/database.js'
|
|
|
|
// Regression test for the "two rows created when turning email alerts back on" bug.
|
|
//
|
|
// Decoded production sequence (Sep 18 2026):
|
|
// 13:42:05 register → row 1 (unconfirmed), confirm email #1
|
|
// 13:44:35 DELETE → row 1 tombstoned (unsubscribed_at set)
|
|
// 13:44:36 register → OLD BUG: a brand-new row 2 was inserted, confirm email #2
|
|
// 14:02:32 confirm → row 2 finally confirmed
|
|
//
|
|
// Fix: when the same pubkey re-subscribes to the same email after being
|
|
// unsubscribed, reactivate the tombstoned row instead of inserting a new one,
|
|
// preserving the existing confirmed state and key.
|
|
|
|
const unique = (label: string) => `${label}-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
|
|
describe('Re-subscribe after DELETE reactivates the same subscription (off/on cycle)', () => {
|
|
const pubkey = unique('resub')
|
|
const email = `${unique('resub')}@example.com`
|
|
|
|
beforeAll(async () => {
|
|
await db.migrate()
|
|
})
|
|
|
|
it('creates an unconfirmed subscription', async () => {
|
|
const sub = await db.insertSubscription(pubkey, email, 'daily')
|
|
expect(sub).toBeTruthy()
|
|
expect(sub.confirmed_at).toBeFalsy()
|
|
expect(sub.unsubscribed_at).toBeFalsy()
|
|
})
|
|
|
|
it('confirms it (user clicks the confirm link)', async () => {
|
|
const active = await db.getSubscriptionByPubkey(pubkey)
|
|
const result = await db.confirmSubscription(active!.key)
|
|
expect(result).toBeTruthy()
|
|
expect(result!.alreadyConfirmed).toBe(false)
|
|
expect(result!.sub.confirmed_at).toBeTruthy()
|
|
})
|
|
|
|
it('unsubscribes it (the flotilla DELETE path)', async () => {
|
|
const active = await db.getSubscriptionByPubkey(pubkey)
|
|
const gone = await db.unsubscribeSubscription(active!.key)
|
|
expect(gone).toBeTruthy()
|
|
expect(gone!.unsubscribed_at).toBeTruthy()
|
|
expect(await db.getSubscriptionByPubkey(pubkey)).toBeFalsy()
|
|
})
|
|
|
|
it('re-registers the SAME email — must reactivate row, NOT create a second row', async () => {
|
|
const resigned = await db.insertSubscription(pubkey, email, 'daily')
|
|
|
|
// Restores the very same row
|
|
const active = await db.getSubscriptionByPubkey(pubkey)
|
|
expect(active).toBeTruthy()
|
|
expect(active!.key).toBe(resigned.key)
|
|
expect(active!.unsubscribed_at).toBeFalsy()
|
|
|
|
// Confirmed state is preserved — no second confirmation email needed
|
|
expect(active!.confirmed_at).toBeTruthy()
|
|
|
|
// Exhaustive: there exists exactly one row total for this pubkey
|
|
const rows = await db.getAllSubscriptionsByPubkey(pubkey)
|
|
expect(rows).toHaveLength(1)
|
|
expect(rows[0].id).toBe(active!.id)
|
|
})
|
|
})
|
|
|
|
describe('Re-subscribe after DELETE before ever confirming', () => {
|
|
const pubkey = unique('resub-unconfirmed')
|
|
const email = `${unique('resub-unconfirmed')}@example.com`
|
|
|
|
beforeAll(async () => {
|
|
await db.migrate()
|
|
})
|
|
|
|
it('registers then unsubscribes without confirming', async () => {
|
|
const sub = await db.insertSubscription(pubkey, email, 'daily')
|
|
await db.unsubscribeSubscription(sub.key)
|
|
expect(await db.getSubscriptionByPubkey(pubkey)).toBeFalsy()
|
|
})
|
|
|
|
it('re-registers the same email — reactivates same row, still unconfirmed', async () => {
|
|
const resigned = await db.insertSubscription(pubkey, email, 'daily')
|
|
const active = await db.getSubscriptionByPubkey(pubkey)
|
|
|
|
expect(active!.key).toBe(resigned.key)
|
|
expect(active!.id).toBe(resigned.id)
|
|
expect(active!.unsubscribed_at).toBeFalsy()
|
|
// Was never confirmed, and re-subscribing does not skip confirmation
|
|
expect(active!.confirmed_at).toBeFalsy()
|
|
|
|
const rows = await db.getAllSubscriptionsByPubkey(pubkey)
|
|
expect(rows).toHaveLength(1)
|
|
})
|
|
})
|
|
|
|
describe('Re-subscribe with a DIFFERENT email after DELETE', () => {
|
|
const pubkey = unique('resub-2')
|
|
const email = `${unique('resub-2')}@example.com`
|
|
|
|
beforeAll(async () => {
|
|
await db.migrate()
|
|
})
|
|
|
|
it('registers, confirms, and unsubscribes', async () => {
|
|
const sub = await db.insertSubscription(pubkey, email, 'daily')
|
|
await db.confirmSubscription(sub.key)
|
|
await db.unsubscribeSubscription(sub.key)
|
|
expect(await db.getSubscriptionByPubkey(pubkey)).toBeFalsy()
|
|
})
|
|
|
|
it('a new email creates a new row, leaving the old one tombstoned', async () => {
|
|
const newEmail = `${unique('resub-2-new')}@example.com`
|
|
const resigned = await db.insertSubscription(pubkey, newEmail, 'daily')
|
|
|
|
expect(resigned.email).toBe(newEmail)
|
|
expect(resigned.confirmed_at).toBeFalsy() // new address must re-confirm
|
|
|
|
const rows = await db.getAllSubscriptionsByPubkey(pubkey)
|
|
expect(rows).toHaveLength(2)
|
|
expect(rows[0].email).toBe(email) // old, tombstoned
|
|
expect(rows[0].unsubscribed_at).toBeTruthy()
|
|
expect(rows[1].email).toBe(newEmail) // new, active
|
|
expect(rows[1].unsubscribed_at).toBeFalsy()
|
|
})
|
|
})
|
|
|
|
describe('Re-subscribe with a changed frequency reactivates and updates cadence', () => {
|
|
const pubkey = unique('resub-freq')
|
|
const email = `${unique('resub-freq')}@example.com`
|
|
|
|
beforeAll(async () => {
|
|
await db.migrate()
|
|
})
|
|
|
|
it('registers confirm-free, unsubscribes', async () => {
|
|
const sub = await db.insertSubscription(pubkey, email, 'daily')
|
|
await db.unsubscribeSubscription(sub.key)
|
|
})
|
|
|
|
it('re-registers with weekly — reactivates the same row at the new cadence', async () => {
|
|
const resigned = await db.insertSubscription(pubkey, email, 'weekly')
|
|
const active = await db.getSubscriptionByPubkey(pubkey)
|
|
|
|
expect(active!.key).toBe(resigned.key)
|
|
expect(active!.frequency).toBe('weekly')
|
|
expect(active!.unsubscribed_at).toBeFalsy()
|
|
|
|
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)
|
|
})
|
|
}) |