import { identityApp, type IdentityApp } from './helpers/identity-app'; import { checkoutFixture } from './helpers/checkout'; import { secondActor } from './helpers/commerce'; import { DeliveryService } from '../src/events/delivery.service'; import { DeliveryStore } from '../src/events/delivery.store'; import { DisabledDelivery } from '../src/events/disabled-delivery'; import * as audit from '../src/identity/audit'; describe('transactional commerce event delivery', () => { let ctx: IdentityApp; beforeAll(async () => { ctx = await identityApp(); }, 60000); afterAll(async () => { await ctx.close(); }); beforeEach(async () => { await ctx.clearLimits(); await ctx.db.eventDelivery.updateMany({ data: { deliveredAt: new Date(), leaseToken: null, leaseExpiresAt: null }, }); }); afterEach(() => jest.restoreAllMocks()); async function event() { const f = await checkoutFixture(ctx); const order = await ctx .api() .post('/api/v1/checkout') .auth(f.actor.token, { type: 'bearer' }) .send(f.input) .expect(201); return { ...f, order: order.body, event: await ctx.db.commerceEvent.findFirstOrThrow({ where: { orderId: order.body.id }, }), }; } it('emits one event per committed transition and none after rollback', async () => { const f = await event(); await ctx .api() .post('/api/v1/checkout') .auth(f.actor.token, { type: 'bearer' }) .send(f.input) .expect(201); await ctx .api() .post('/api/v1/orders/' + f.order.id + '/cancel') .auth(f.actor.token, { type: 'bearer' }) .expect(201); await ctx .api() .post('/api/v1/orders/' + f.order.id + '/cancel') .auth(f.actor.token, { type: 'bearer' }) .expect(201); expect( await ctx.db.commerceEvent.count({ where: { orderId: f.order.id } }), ).toBe(2); const next = await checkoutFixture(ctx); jest .spyOn(audit, 'recordAudit') .mockRejectedValueOnce(new Error('Synthetic final write failure')); await ctx .api() .post('/api/v1/checkout') .auth(next.actor.token, { type: 'bearer' }) .send(next.input) .expect(500); expect( await ctx.db.commerceEvent.count({ where: { order: { userId: next.actor.userId } }, }), ).toBe(0); await expect( ctx.executeSql(`DELETE FROM commerce_events WHERE id = '${f.event.id}'`), ).rejects.toThrow(); }); it('keeps delivery disabled by default and uses a test adapter with stable event IDs', async () => { const f = await event(); await expect( ctx.app.get(DeliveryService).dispatchOne(), ).rejects.toMatchObject({ code: 'DELIVERY_UNAVAILABLE' }); await expect(new DisabledDelivery().deliver()).rejects.toThrow(); const adapter = { assertConfigured: jest.fn(), deliver: jest.fn().mockResolvedValue(undefined), }; const service = new DeliveryService(adapter, ctx.app.get(DeliveryStore)); expect(await service.dispatchOne()).toBe(true); expect(adapter.deliver).toHaveBeenCalledWith({ id: f.event.id, orderId: f.order.id, organizationId: f.actor.organizationId, kind: 'order.created', }); expect(await service.dispatchOne()).toBe(false); expect( ( await ctx.db.eventDelivery.findUniqueOrThrow({ where: { eventId: f.event.id }, }) ).deliveredAt, ).not.toBeNull(); }); it('backs off failures, bounds attempts and permits authorized manual retries', async () => { const f = await event(); const store = ctx.app.get(DeliveryStore); const service = new DeliveryService( { assertConfigured() {}, deliver: jest .fn() .mockRejectedValue(new Error('Private transport error')), }, store, ); await service.dispatchOne(); const failed = await ctx.db.eventDelivery.findUniqueOrThrow({ where: { eventId: f.event.id }, }); expect(failed).toMatchObject({ attempts: 1, lastErrorCode: 'DELIVERY_FAILED', leaseToken: null, }); expect(failed.availableAt.getTime()).toBeGreaterThan(Date.now()); expect(await store.claim()).toBeNull(); await ctx.db.eventDelivery.update({ where: { eventId: f.event.id }, data: { attempts: 5, availableAt: new Date(0) }, }); expect(await store.claim()).toBeNull(); const summary = await ctx .api() .get('/api/v1/admin/operations/summary') .auth(ctx.token, { type: 'bearer' }) .expect(200); expect(summary.body.failedDeliveries).toBe(1); await ctx .api() .post('/api/v1/admin/operations/events/' + f.event.id + '/retry') .auth(f.actor.token, { type: 'bearer' }) .expect(403); await ctx .api() .post('/api/v1/admin/operations/events/' + f.event.id + '/retry') .auth(ctx.token, { type: 'bearer' }) .expect(201); expect((await store.claim())?.attempts).toBe(1); }); it('rejects stale lease acknowledgements and protects cross-organization event access', async () => { const f = await event(); const store = ctx.app.get(DeliveryStore); const first = (await store.claim())!; await ctx .api() .post('/api/v1/admin/operations/events/' + f.event.id + '/retry') .auth(ctx.token, { type: 'bearer' }) .expect(409); await ctx.db.eventDelivery.update({ where: { eventId: f.event.id }, data: { leaseExpiresAt: new Date(0) }, }); const next = (await store.claim())!; expect(next.leaseToken).not.toBe(first.leaseToken); expect((await store.acknowledge(f.event.id, first.leaseToken!)).count).toBe( 0, ); expect((await store.fail(f.event.id, first.leaseToken!, 1)).count).toBe(0); await store.acknowledge(f.event.id, next.leaseToken!); await ctx .api() .post('/api/v1/admin/operations/events/' + f.event.id + '/retry') .auth(ctx.token, { type: 'bearer' }) .expect(409); const other = await secondActor(ctx, false, [ 'operations.read', 'notifications.retry', ]); const list = await ctx .api() .get('/api/v1/admin/operations/events') .auth(other.token, { type: 'bearer' }) .expect(200); expect(list.body).toEqual([]); await ctx .api() .post('/api/v1/admin/operations/events/' + f.event.id + '/retry') .auth(other.token, { type: 'bearer' }) .expect(404); const own = await ctx .api() .get('/api/v1/admin/operations/events?limit=1') .auth(ctx.token, { type: 'bearer' }) .expect(200); expect(own.body[0].delivery.leaseToken).toBeUndefined(); }); });