Assessor_2.0/tests/server/backend-artifact-store.test.ts

106 lines
5.9 KiB
TypeScript

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<string> {
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' }] });
});
});