import { INestApplication } from '@nestjs/common'; import * as bcrypt from 'bcryptjs'; import request from 'supertest'; import { App } from 'supertest/types'; import { DomainEventDispatcher } from '../src/domain-events/domain-event-dispatcher'; import { PrismaService } from '../src/prisma/prisma.service'; import { createTestApp } from './utils/create-test-app'; import { login } from './utils/api'; interface FailedEventBody { id: string; failedHandlers: unknown[]; [key: string]: unknown; } interface FailedDeliveryBody { id: string; targetHost: string; [key: string]: unknown; } interface OverviewBody { jobs: unknown[]; domainEvents: { failed: number; oldestPendingAgeSeconds: number | null }; webhookDeliveries: { failed: number }; } describe('System API (e2e)', () => { let app: INestApplication; let prisma: PrismaService; const suffix = `${Date.now()}-${Math.round(Math.random() * 1e6)}`; const password = 'SuperSecret123!'; let adminToken: string; let staffToken: string; const server = () => app.getHttpServer(); const auth = (token: string) => ({ Authorization: `Bearer ${token}` }); beforeAll(async () => { app = await createTestApp(); prisma = app.get(PrismaService); const passwordHash = await bcrypt.hash(password, 12); for (const [key, role] of [ ['admin', 'ADMIN'], ['staff', 'STAFF'], ] as const) { await prisma.user.create({ data: { email: `system-${key}-${suffix}@example.com`, passwordHash, name: key, role: { connect: { key: role } }, }, }); } adminToken = ( await login(app, `system-admin-${suffix}@example.com`, password) ).accessToken; staffToken = ( await login(app, `system-staff-${suffix}@example.com`, password) ).accessToken; }); afterAll(async () => { await app.close(); }); it('staff without system:read gets 403; API keys never reach /system', async () => { await request(server()) .get('/api/v1/system/overview') .set(auth(staffToken)) .expect(403); await request(server()) .post('/api/v1/system/domain-events/x/retry') .set(auth(staffToken)) .expect(403); const created = await request(server()) .post('/api/v1/api-keys') .set(auth(adminToken)) .send({ name: `System ${suffix}`, scopes: ['shop-sync:read'] }) .expect(201); const { token } = created.body as { token: string }; await request(server()) .get('/api/v1/system/overview') .set('Authorization', `Bearer ${token}`) .expect(403); await request(server()) .get('/api/v1/system/job-runs') .set('Authorization', `Bearer ${token}`) .expect(403); }); it('shows the overview and validates the list limit', async () => { const response = await request(server()) .get('/api/v1/system/overview') .set(auth(adminToken)) .expect(200); const body = response.body as OverviewBody; expect(body.jobs).toHaveLength(5); expect(typeof body.domainEvents.failed).toBe('number'); await request(server()) .get('/api/v1/system/job-runs?limit=0') .set(auth(adminToken)) .expect(400); await request(server()) .get('/api/v1/system/job-runs?limit=101') .set(auth(adminToken)) .expect(400); await request(server()) .get('/api/v1/system/job-runs?job=nope') .set(auth(adminToken)) .expect(400); await request(server()) .get('/api/v1/system/job-runs?job=cleanup&limit=5') .set(auth(adminToken)) .expect(200); }); it('admin retries a FAILED event and the next drain completes it', async () => { const correlationId = `system-${suffix}`; const event = await prisma.domainEvent.create({ data: { type: 'test.system-retry', aggregateType: 'SystemTest', aggregateId: `agg-${suffix}`, payload: { secret: 'must-not-leak' }, correlationId, status: 'FAILED', attempts: 8, lastError: 'boom', availableAt: new Date(Date.now() + 86_400_000), }, }); const failed = await request(server()) .get('/api/v1/system/domain-events/failed?limit=100') .set(auth(adminToken)) .expect(200); const listed = (failed.body as FailedEventBody[]).find( (e) => e.id === event.id, ); expect(listed).toBeDefined(); expect(JSON.stringify(failed.body)).not.toContain('must-not-leak'); expect(listed).not.toHaveProperty('payload'); await request(server()) .post(`/api/v1/system/domain-events/${event.id}/retry`) .set(auth(adminToken)) .expect(204); const reset = await prisma.domainEvent.findUniqueOrThrow({ where: { id: event.id }, }); expect(reset).toMatchObject({ status: 'PENDING', attempts: 0, lastError: null, }); expect(reset.availableAt.getTime()).toBeLessThanOrEqual(Date.now() + 5_000); await expect( prisma.auditLog.count({ where: { entityType: 'DomainEvent', entityId: event.id }, }), ).resolves.toBe(1); await app .get(DomainEventDispatcher) .drain({ correlationIds: [correlationId] }); const done = await prisma.domainEvent.findUniqueOrThrow({ where: { id: event.id }, }); expect(done.status).toBe('DONE'); // DONE is not FAILED any more: a second retry is a conflict. await request(server()) .post(`/api/v1/system/domain-events/${event.id}/retry`) .set(auth(adminToken)) .expect(409); await request(server()) .post('/api/v1/system/domain-events/does-not-exist/retry') .set(auth(adminToken)) .expect(404); }); it('lists a failed webhook delivery with host only and retries it', async () => { const subscription = await prisma.webhookSubscription.create({ data: { url: 'https://hooks.example.com/very/secret/path?token=abc', events: ['harvest.created'], secret: 'enc-secret', }, }); const delivery = await prisma.webhookDelivery.create({ data: { subscriptionId: subscription.id, event: 'harvest.created', body: '{"private":"record"}', status: 'FAILED', attempts: 8, lastStatusCode: 500, lastError: 'HTTP 500', }, }); const response = await request(server()) .get('/api/v1/system/webhook-deliveries/failed?limit=100') .set(auth(adminToken)) .expect(200); const listed = (response.body as FailedDeliveryBody[]).find( (d) => d.id === delivery.id, ); expect(listed?.targetHost).toBe('hooks.example.com'); const raw = JSON.stringify(listed); expect(raw).not.toMatch(/secret|token|private/); await request(server()) .post(`/api/v1/system/webhook-deliveries/${delivery.id}/retry`) .set(auth(adminToken)) .expect(204); const reset = await prisma.webhookDelivery.findUniqueOrThrow({ where: { id: delivery.id }, }); expect(reset).toMatchObject({ status: 'PENDING', attempts: 0, lastError: null, lastStatusCode: null, }); await request(server()) .post(`/api/v1/system/webhook-deliveries/${delivery.id}/retry`) .set(auth(adminToken)) .expect(409); await prisma.webhookSubscription.delete({ where: { id: subscription.id } }); }); });