import { createHash } from 'node:crypto'; import { mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { FileSystemBackendArtifactStore } from '../../server/artifacts/backend-artifact-store'; const temporaryRoots: string[] = []; async function temporaryRoot(): Promise { const root = await mkdtemp(join(tmpdir(), 'assessor-artifact-store-')); temporaryRoots.push(resolve(root)); return root; } afterEach(async () => { for (const root of temporaryRoots.splice(0)) { if (!root.startsWith(resolve(tmpdir()))) throw new Error('Unsicheres Testlöschziel.'); await rm(root, { recursive: true, force: true }); } }); describe('backend-controlled artifact store', () => { it('stages, verifies and publishes immutable opaque objects', async () => { const root = await temporaryRoot(); const store = new FileSystemBackendArtifactStore(root); const bytes = new TextEncoder().encode('synthetic-gpo-container'); const expectedSha256 = createHash('sha256').update(bytes).digest('hex'); const receipt = await store.put({ bytes, expectedSha256, maximumBytes: 1024 }); expect(receipt).toMatchObject({ version: 1, sha256: expectedSha256, sizeBytes: bytes.byteLength }); expect(receipt.storageKey).toMatch(/^[a-f0-9-]{36}$/); expect(receipt.storageKey).not.toContain('synthetic'); await expect(store.read({ storageKey: receipt.storageKey, expectedSha256, maximumBytes: 1024 })) .resolves.toEqual(bytes); await expect(readdir(join(root, 'staging'))).resolves.toEqual([]); await expect(readdir(join(root, 'objects'))).resolves.toHaveLength(1); }); it('rejects size/hash violations and removes failed staging data', async () => { const root = await temporaryRoot(); const store = new FileSystemBackendArtifactStore(root); const bytes = new TextEncoder().encode('synthetic'); await expect(store.put({ bytes, maximumBytes: 2 })) .rejects.toMatchObject({ diagnostics: [{ code: 'artifact-store-size-limit' }] }); await expect(store.put({ bytes, expectedSha256: '0'.repeat(64), maximumBytes: 1024 })) .rejects.toMatchObject({ diagnostics: [{ code: 'artifact-store-hash-mismatch' }] }); await expect(readdir(join(root, 'staging'))).resolves.toEqual([]); await expect(readdir(join(root, 'objects'))).resolves.toEqual([]); }); it('detects stored-object tampering before returning bytes', async () => { const root = await temporaryRoot(); const store = new FileSystemBackendArtifactStore(root); const original = new TextEncoder().encode('original'); const receipt = await store.put({ bytes: original, maximumBytes: 1024 }); const [objectName] = await readdir(join(root, 'objects')); await writeFile(join(root, 'objects', objectName), new TextEncoder().encode('tampered')); await expect(store.read({ storageKey: receipt.storageKey, expectedSha256: receipt.sha256, maximumBytes: 1024 })) .rejects.toMatchObject({ diagnostics: [{ code: 'artifact-store-integrity-mismatch' }] }); await expect(store.read({ storageKey: '../escape', expectedSha256: receipt.sha256, maximumBytes: 1024 })) .rejects.toMatchObject({ diagnostics: [{ code: 'artifact-store-key-invalid' }] }); }); });