import { createHash } from 'node:crypto'; import { mkdir, 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('checks live availability without reading contents and safely restores the original opaque identity', async () => { const root = await temporaryRoot(); const store = new FileSystemBackendArtifactStore(root); const bytes = new TextEncoder().encode('synthetic original'); const receipt = await store.put({ bytes, maximumBytes: 1024 }); const request = { storageKey: receipt.storageKey, expectedSha256: receipt.sha256, maximumBytes: 1024, bytes }; await expect(store.availability(receipt.storageKey)).resolves.toBe('available'); await store.remove(receipt.storageKey); await expect(store.availability(receipt.storageKey)).resolves.toBe('missing'); await expect(store.restore({ ...request, bytes: new TextEncoder().encode('wrong') })) .rejects.toMatchObject({ diagnostics: [{ code: 'artifact-store-hash-mismatch' }] }); await expect(store.availability(receipt.storageKey)).resolves.toBe('missing'); await Promise.all([store.restore(request), store.restore(request)]); await expect(store.read(request)).resolves.toEqual(bytes); await expect(readdir(join(root, 'objects'))).resolves.toEqual([`${receipt.storageKey}.bin`]); await expect(readdir(join(root, 'staging'))).resolves.toEqual([]); // A cached initialization must not prevent recovery of an absent store tree. await rm(root, { recursive: true }); await expect(store.availability(receipt.storageKey)).resolves.toBe('missing'); await store.restore(request); await expect(store.read(request)).resolves.toEqual(bytes); }); it('rejects occupied or unsafe source locations and never overwrites existing bytes', async () => { const root = await temporaryRoot(); const store = new FileSystemBackendArtifactStore(root); const bytes = new TextEncoder().encode('original'); const receipt = await store.put({ bytes, maximumBytes: 1024 }); const path = join(root, 'objects', `${receipt.storageKey}.bin`); await writeFile(path, 'different'); await expect(store.restore({ storageKey: receipt.storageKey, expectedSha256: receipt.sha256, maximumBytes: 1024, bytes })) .rejects.toMatchObject({ diagnostics: [{ code: 'artifact-store-integrity-mismatch' }] }); await store.remove(receipt.storageKey); await mkdir(path); await expect(store.availability(receipt.storageKey)).resolves.toBe('unavailable'); await expect(store.availability('../escape')).resolves.toBe('unavailable'); await expect(store.read({ storageKey: receipt.storageKey, expectedSha256: receipt.sha256, maximumBytes: 1024 })) .rejects.toMatchObject({ diagnostics: [{ code: 'artifact-store-object-rejected' }] }); }); 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' }] }); }); });