106 lines
5.9 KiB
TypeScript
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' }] });
|
|
});
|
|
});
|