Port cron-minutely fixes: reactivate tombstoned subs + confirm-email rate limit #29

Merged
matt merged 3 commits from mailship-fc-merge-cron-fixes-df6 into main 2026-09-23 17:56:12 +00:00
2 changed files with 46 additions and 10 deletions
Showing only changes of commit 0ff8984a94 - Show all commits

View file

@ -181,13 +181,13 @@ export const updateSubscription = instrument(
}
)
const getMostRecentTombstonedSubscription = async (pubkey: string) =>
const getMostRecentTombstonedSubscription = async (pubkey: string, email: string) =>
parseSubscription(
await get(
`SELECT * FROM subscriptions
WHERE pubkey = ? AND unsubscribed_at IS NOT NULL
ORDER BY created_at DESC, id DESC LIMIT 1`,
[pubkey]
WHERE pubkey = ? AND email = ? AND unsubscribed_at IS NOT NULL
ORDER BY rowid DESC LIMIT 1`,
[pubkey, email]
)
)
@ -209,13 +209,13 @@ export const insertSubscription = instrument(
return assertResult(await updateSubscription(existing, email, frequency, hour, minute, dayOfWeek, timezone))
}
// No active row. If this account previously subscribed to the same email
// and was unsubscribed, reactivate that row instead of inserting a fresh
// one. Otherwise every turn-on → turn-off → turn-on cycle would create a
// new row, abandoning the confirmed state (and the already-known key).
const tombstoned = await getMostRecentTombstonedSubscription(pubkey)
// No active row. If this account previously subscribed to this email and
// was unsubscribed, reactivate the most recent such row instead of inserting
// a fresh one. Otherwise every turn-on → turn-off → turn-on cycle would
// create a new row, abandoning the confirmed state (and the already-known key).
const tombstoned = await getMostRecentTombstonedSubscription(pubkey, email)
if (tombstoned?.email === email) {
if (tombstoned) {
try {
return assertResult(await reactivateSubscription(tombstoned, frequency))
} catch (err: any) {

View file

@ -149,4 +149,40 @@ describe('Re-subscribe with a changed frequency reactivates and updates cadence'
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)
})
})