import { describe, expect, it, vi } from 'vitest'; import { Doc0UploadOutcomeUnknownError, prepareDoc0UploadIdentity, type Doc0UploadClient } from '../../src/document-workbench/doc0-upload-client'; import { DocumentUploadCoordinator } from '../../src/document-workbench/upload-coordinator'; import type { PostgresUploadAttemptStore } from '../../src/document-workbench/upload-attempt-store'; const input = { tenantId: 't1', projectId: 'p1', actorId: 'a1', requestId: 'r1', idempotencyKey: 'i1', filename: 'rapport åäö.txt', mimeType: 'text/plain', classification: 'confidential' as const, bytes: new TextEncoder().encode('original') }; const receipt = { documentId: 'd1', versionId: 'v1', checksumSha256: 'test', pipelineStage: 'accepted' }; function fixture() { const attempts = { prepare: vi.fn(async () => ({ state: 'prepared' as const, replay: false })), startDispatch: vi.fn(async () => true) }; const doc0 = { upload: vi.fn(async () => receipt), inspect: vi.fn(async () => ({ status: 'not_found' as const })) }; const authorize = vi.fn(async () => true); const coordinator = new DocumentUploadCoordinator({ attempts, doc0, authorize, maxBytes: 100 }); return { attempts, doc0, authorize, coordinator }; } describe('document upload coordination', () => { it('authorizes and persists the exact fingerprint before a single write', async () => { const f = fixture(); expect(await f.coordinator.execute(input)).toEqual({ status: 'receipt_verified', receipt }); expect(f.authorize).toHaveBeenCalledWith({ tenantId: 't1', projectId: 'p1', actorId: 'a1' }); const attempt = f.attempts.prepare.mock.calls[0][0]; expect(attempt).toEqual({ tenantId: 't1', projectId: 'p1', actorId: 'a1', attemptId: 'i1', sourceSha256: prepareDoc0UploadIdentity(input).sourceSha256, sourceSizeBytes: input.bytes.length, requestFingerprint: prepareDoc0UploadIdentity(input).requestFingerprint }); expect(f.attempts.startDispatch).toHaveBeenCalledWith(attempt); expect(f.attempts.prepare.mock.invocationCallOrder[0]).toBeLessThan(f.attempts.startDispatch.mock.invocationCallOrder[0]); expect(f.attempts.startDispatch.mock.invocationCallOrder[0]).toBeLessThan(f.doc0.upload.mock.invocationCallOrder[0]); }); it('denies before persistence or network when authorization fails', async () => { const f = fixture(); f.authorize.mockResolvedValueOnce(false); await expect(f.coordinator.execute(input)).rejects.toThrow(/not authorized/); expect(f.attempts.prepare).not.toHaveBeenCalled(); expect(f.doc0.upload).not.toHaveBeenCalled(); }); it('rejects truthy but non-boolean authorization results', async () => { const f = fixture(); f.authorize.mockResolvedValueOnce('allow' as unknown as boolean); await expect(f.coordinator.execute(input)).rejects.toThrow(/not authorized/); expect(f.attempts.prepare).not.toHaveBeenCalled(); }); it('does not dispatch after a failed or ambiguous database transition', async () => { const f = fixture(); f.attempts.startDispatch.mockRejectedValueOnce(new Error('connection lost')); await expect(f.coordinator.execute(input)).rejects.toThrow(/connection lost/); expect(f.doc0.upload).not.toHaveBeenCalled(); expect(f.doc0.inspect).not.toHaveBeenCalled(); }); it('only observes a previously started attempt and never sends a second POST', async () => { const f = fixture(); f.attempts.startDispatch.mockResolvedValueOnce(false); expect(await f.coordinator.execute(input)).toEqual({ status: 'outcome_unknown', observation: { status: 'not_found' } }); expect(f.doc0.upload).not.toHaveBeenCalled(); expect(f.doc0.inspect).toHaveBeenCalledOnce(); }); it('keeps a journaled completion distinct from a verified receipt', async () => { const f = fixture(); f.attempts.startDispatch.mockResolvedValueOnce(false); f.doc0.inspect.mockResolvedValueOnce({ status: 'completed_recorded', documentId: 'doc', versionId: 'version' }); expect(await f.coordinator.execute(input)).toEqual({ status: 'outcome_unknown', observation: { status: 'completed_recorded', documentId: 'doc', versionId: 'version' } }); expect(f.doc0.upload).not.toHaveBeenCalled(); }); it('keeps a possibly committed upload unknown even when inspection is unavailable', async () => { const f = fixture(); f.doc0.upload.mockRejectedValueOnce(new Doc0UploadOutcomeUnknownError()); f.doc0.inspect.mockRejectedValueOnce(new Error('unavailable')); expect(await f.coordinator.execute(input)).toEqual({ status: 'outcome_unknown' }); expect(f.doc0.upload).toHaveBeenCalledOnce(); expect(f.doc0.inspect).toHaveBeenCalledOnce(); }); it('snapshots bytes and metadata before asynchronous authorization', async () => { const f = fixture(); const mutable = { ...input, bytes: new Uint8Array(input.bytes) }; f.authorize.mockImplementationOnce(async () => { mutable.bytes.fill(0); mutable.filename = 'changed.txt'; return true; }); await f.coordinator.execute(mutable); expect(f.doc0.upload.mock.calls[0][0].filename).toBe(input.filename); expect(new TextDecoder().decode(f.doc0.upload.mock.calls[0][0].bytes)).toBe('original'); }); });