import { ContentReportError, submitAiContentReport, } from '../src/features/reporting/content-report-service' const originalFetch = global.fetch const GENERATION_ID = '11111111-1111-4111-8111-111111111111' const IDEMPOTENCY_KEY = '22222222-2222-4222-8222-222222222222' const REPORT_ID = '33333333-3333-4333-8333-333333333333' function response(status: number, body: unknown): Response { return { ok: status >= 200 && status < 300, status, json: async () => body, } as unknown as Response } afterEach(() => { global.fetch = originalFetch jest.restoreAllMocks() }) describe('content report service', () => { it('sends the exact authenticated Talk report contract and validates the receipt', async () => { global.fetch = jest.fn().mockResolvedValue(response(201, { reportId: REPORT_ID, status: 'submitted', idempotent: false, createdAt: '2026-08-24T08:00:00.000Z', })) await expect(submitAiContentReport({ accessToken: ' user-token ', idempotencyKey: IDEMPOTENCY_KEY, generationId: GENERATION_ID, sourceType: 'talk_response', reason: 'privacy', comment: ' exposed a name ', snapshot: ' AI output ', })).resolves.toEqual({ reportId: REPORT_ID, status: 'submitted', idempotent: false, createdAt: '2026-08-24T08:00:00.000Z', }) expect(global.fetch).toHaveBeenCalledTimes(1) const [url, request] = (global.fetch as jest.Mock).mock.calls[0] expect(url).toMatch(/\/functions\/v1\/content-report$/) expect(request.headers).toMatchObject({ Authorization: 'Bearer user-token', 'Content-Type': 'application/json', 'Idempotency-Key': IDEMPOTENCY_KEY, }) expect(JSON.parse(request.body)).toEqual({ kind: 'ai_output', source: { type: 'talk_response', generationId: GENERATION_ID }, reason: 'privacy', comment: 'exposed a name', snapshot: 'AI output', }) }) it('sends an owned generated meeting document through the same report boundary', async () => { global.fetch = jest.fn().mockResolvedValue(response(201, { reportId: REPORT_ID, status: 'submitted', idempotent: false, createdAt: '2026-08-29T08:00:00.000Z', })) await submitAiContentReport({ accessToken: 'user-token', idempotencyKey: IDEMPOTENCY_KEY, generationId: GENERATION_ID, sourceType: 'meeting_document', reason: 'misinformation', snapshot: 'Generated meeting document excerpt', }) const [, request] = (global.fetch as jest.Mock).mock.calls[0] expect(JSON.parse(request.body)).toEqual({ kind: 'ai_output', source: { type: 'meeting_document', generationId: GENERATION_ID }, reason: 'misinformation', snapshot: 'Generated meeting document excerpt', }) }) it('rejects invalid or oversized input before any network request', async () => { global.fetch = jest.fn() await expect(submitAiContentReport({ accessToken: 'token', idempotencyKey: 'not-a-uuid', generationId: GENERATION_ID, sourceType: 'talk_response', reason: 'other', snapshot: 'output', })).rejects.toBeInstanceOf(ContentReportError) await expect(submitAiContentReport({ accessToken: 'token', idempotencyKey: IDEMPOTENCY_KEY, generationId: GENERATION_ID, sourceType: 'talk_response', reason: 'other', snapshot: 'x'.repeat(4_001), })).rejects.toMatchObject({ code: 'INVALID_REQUEST', retryable: false }) expect(global.fetch).not.toHaveBeenCalled() }) it.each([ [401, { error: 'unauthorized' }, 'AUTH_REQUIRED', false], [404, { error: 'source_not_found' }, 'SOURCE_NOT_FOUND', false], [409, { error: 'source_already_reported' }, 'ALREADY_REPORTED', false], [409, { error: 'idempotency_conflict' }, 'IDEMPOTENCY_CONFLICT', false], [429, { error: 'report_rate_limited' }, 'RATE_LIMITED', true], [503, { error: 'server_error' }, 'SERVER_ERROR', true], ] as const)('maps HTTP %s without leaking raw server text', async (status, body, code, retryable) => { global.fetch = jest.fn().mockResolvedValue(response(status, body)) await expect(submitAiContentReport({ accessToken: 'token', idempotencyKey: IDEMPOTENCY_KEY, generationId: GENERATION_ID, sourceType: 'talk_response', reason: 'harmful', snapshot: 'output', })).rejects.toMatchObject({ code, retryable, status }) }) it('fails closed when the server returns an unverified success shape', async () => { global.fetch = jest.fn().mockResolvedValue(response(201, { reportId: 'raw-secret-instead-of-uuid', status: 'submitted', idempotent: false, createdAt: 'not-a-date', })) await expect(submitAiContentReport({ accessToken: 'token', idempotencyKey: IDEMPOTENCY_KEY, generationId: GENERATION_ID, sourceType: 'talk_response', reason: 'spam', snapshot: 'output', })).rejects.toMatchObject({ code: 'INVALID_RESPONSE', retryable: true }) }) })