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
69 lines
5.2 KiB
TypeScript
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');
|
|
});
|
|
});
|