Skip to content

src/idempotency.test.ts

179 lines · 4.4 KB · TypeScript
Raw
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import {
	IDEMPOTENCY_INFLIGHT_MS,
	IDEMPOTENCY_RETENTION_MS,
	idempotencyStorageKey,
	normalizeIdempotencyKey,
	readFreshFingerprint,
	withSendIdempotency,
	type IdempotencyStorage,
} from './idempotency.ts'

function memoryStorage(initial: Record<string, unknown> = {}): IdempotencyStorage & { data: Record<string, unknown> } {
	const data = { ...initial }
	return {
		data,
		async get(key) {
			return data[key]
		},
		async set(key, value) {
			data[key] = value
		},
		async delete(key) {
			delete data[key]
		},
	}
}

describe('normalizeIdempotencyKey', () => {
	it('trims and accepts non-empty keys', () => {
		assert.equal(normalizeIdempotencyKey('  batch:1  '), 'batch:1')
	})

	it('treats blank as absent', () => {
		assert.equal(normalizeIdempotencyKey(''), null)
		assert.equal(normalizeIdempotencyKey('   '), null)
		assert.equal(normalizeIdempotencyKey(undefined), null)
	})

	it('rejects oversized keys', () => {
		assert.throws(() => normalizeIdempotencyKey('x'.repeat(257)), /at most 256/)
	})
})

describe('idempotencyStorageKey', () => {
	it('scopes by action, account, and key', () => {
		assert.equal(
			idempotencyStorageKey('sendMessage', 'business', 'welcome:42'),
			'idempotency:sendMessage:business:welcome:42',
		)
	})
})

describe('readFreshFingerprint', () => {
	it('returns success within retention', () => {
		const now = Date.parse('2026-09-11T20:00:00.000Z')
		const fresh = readFreshFingerprint(
			{
				status: 'success',
				result: { id: 'msg-1' },
				completedAt: new Date(now - 60_000).toISOString(),
			},
			now,
		)
		assert.deepEqual(fresh, { kind: 'success', result: { id: 'msg-1' } })
	})

	it('expires success after retention', () => {
		const now = Date.parse('2026-09-11T20:00:00.000Z')
		const stale = readFreshFingerprint(
			{
				status: 'success',
				result: { id: 'msg-1' },
				completedAt: new Date(now - IDEMPOTENCY_RETENTION_MS - 1).toISOString(),
			},
			now,
		)
		assert.equal(stale, null)
	})

	it('detects fresh inflight', () => {
		const now = Date.parse('2026-09-11T20:00:00.000Z')
		const inflight = readFreshFingerprint(
			{
				status: 'inflight',
				startedAt: new Date(now - IDEMPOTENCY_INFLIGHT_MS + 1_000).toISOString(),
			},
			now,
		)
		assert.deepEqual(inflight, { kind: 'inflight' })
	})
})

describe('withSendIdempotency', () => {
	it('passes through when key omitted', async () => {
		const storage = memoryStorage()
		const result = await withSendIdempotency({
			action: 'sendMessage',
			account: 'business',
			storage,
			run: async () => ({ id: 'fresh' }),
		})
		assert.deepEqual(result, { id: 'fresh' })
		assert.deepEqual(storage.data, {})
	})

	it('stores and replays successful sends', async () => {
		const storage = memoryStorage()
		let calls = 0
		const first = await withSendIdempotency({
			action: 'sendMessage',
			account: 'business',
			idempotencyKey: 'welcome:42',
			storage,
			run: async () => {
				calls += 1
				return { id: 'msg-1' }
			},
		})
		const second = await withSendIdempotency({
			action: 'sendMessage',
			account: 'business',
			idempotencyKey: 'welcome:42',
			storage,
			run: async () => {
				calls += 1
				return { id: 'msg-2' }
			},
		})
		assert.deepEqual(first, { id: 'msg-1' })
		assert.deepEqual(second, { id: 'msg-1' })
		assert.equal(calls, 1)
	})

	it('treats different keys as different sends', async () => {
		const storage = memoryStorage()
		const a = await withSendIdempotency({
			action: 'sendMessage',
			account: 'business',
			idempotencyKey: 'a',
			storage,
			run: async () => ({ id: 'a' }),
		})
		const b = await withSendIdempotency({
			action: 'sendMessage',
			account: 'business',
			idempotencyKey: 'b',
			storage,
			run: async () => ({ id: 'b' }),
		})
		assert.deepEqual(a, { id: 'a' })
		assert.deepEqual(b, { id: 'b' })
	})

	it('clears inflight on failure so retries can send', async () => {
		const storage = memoryStorage()
		await assert.rejects(
			withSendIdempotency({
				action: 'sendMessage',
				account: 'business',
				idempotencyKey: 'fail-once',
				storage,
				run: async () => {
					throw new Error('boom')
				},
			}),
			/boom/,
		)
		assert.equal(Object.keys(storage.data).length, 0)
		const recovered = await withSendIdempotency({
			action: 'sendMessage',
			account: 'business',
			idempotencyKey: 'fail-once',
			storage,
			run: async () => ({ id: 'ok' }),
		})
		assert.deepEqual(recovered, { id: 'ok' })
	})
})