Files
0pt/tests/document-workbench/upload-coordinator.test.ts
T
admin ce41d601f7
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / security (push) Successful in 17s
Security Scan (SIAX Cloud) / security (push) Successful in 18s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 17s
CI (SIAX Cloud) / quality (push) Successful in 50s
CI (SIAX Cloud) / integration (pull_request) Successful in 33s
CI (SIAX Cloud) / quality (pull_request) Successful in 39s
feat: coordinate journaled document upload attempts
2026-11-02 07:17:32 +01:00

69 lines
5.2 KiB
TypeScript

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<PostgresUploadAttemptStore['prepare']>(async () => ({ state: 'prepared' as const, replay: false })), startDispatch: vi.fn<PostgresUploadAttemptStore['startDispatch']>(async () => true) };
const doc0 = { upload: vi.fn<Doc0UploadClient['upload']>(async () => receipt), inspect: vi.fn<Doc0UploadClient['inspect']>(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');
});
});