75 lines
4.4 KiB
TypeScript
75 lines
4.4 KiB
TypeScript
import { mkdtemp, readdir, rm } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join, resolve } from 'node:path';
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { assertArtifactStoreOutsidePublicRoot, FileSystemBackendArtifactStore } from '../../server/artifacts/backend-artifact-store';
|
|
import { GpoBackupSubmissionService } from '../../server/gpo/gpo-backup-submission-service';
|
|
import { directGpoInventoryExecutor } from '../../server/gpo/gpo-inventory-executor';
|
|
import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository';
|
|
import { RepositoryNotFoundError, RepositoryValidationError } from '../../src/persistence/repository-errors';
|
|
import { syntheticGpoBackupZip } from './gpo-synthetic-fixture';
|
|
|
|
const receivedAt = '2026-07-21T08:00:00.000Z';
|
|
const project = { id: 'gpo-project', name: 'Synthetic GPO project', createdAt: receivedAt, updatedAt: receivedAt, attributes: {} };
|
|
|
|
describe('project-bound GPO backup submission', () => {
|
|
let directory: string;
|
|
let repository: LocalAssessorRepository;
|
|
let service: GpoBackupSubmissionService;
|
|
|
|
beforeEach(async () => {
|
|
directory = await mkdtemp(join(tmpdir(), 'assessor-gpo-submit-'));
|
|
repository = new LocalAssessorRepository();
|
|
await repository.saveProject(project);
|
|
service = new GpoBackupSubmissionService(repository, new FileSystemBackendArtifactStore(directory), directGpoInventoryExecutor, 1024 * 1024);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(directory, { recursive: true, force: true });
|
|
});
|
|
|
|
it('preflights, stores and queues a confidential-by-default GPO original within its project', async () => {
|
|
const bytes = syntheticGpoBackupZip();
|
|
const result = await service.submit({ projectId: project.id, fileName: 'backup.zip', bytes, receivedAt });
|
|
expect(result).toMatchObject({ version: 1, status: 'queued', deduplicated: false, sizeBytes: bytes.byteLength });
|
|
await expect(repository.listImportArtifacts(project.id)).resolves.toMatchObject([{
|
|
id: result.artifactId, projectId: project.id, sensitivity: 'secret', parseStatus: 'pending',
|
|
storage: { kind: 'backend-controlled' }
|
|
}]);
|
|
await expect(repository.listIngestionJobs(project.id)).resolves.toMatchObject([{
|
|
id: result.jobId, artifactId: result.artifactId, kind: 'analyze-gpo-backup', status: 'queued'
|
|
}]);
|
|
expect(await readdir(join(directory, 'objects'))).toHaveLength(1);
|
|
});
|
|
|
|
it('reuses the project artifact and idempotent job for the same bytes', async () => {
|
|
const bytes = syntheticGpoBackupZip();
|
|
const first = await service.submit({ projectId: project.id, fileName: 'first.zip', bytes, receivedAt });
|
|
const second = await service.submit({ projectId: project.id, fileName: 'first.zip', bytes, receivedAt: '2026-07-21T08:01:00.000Z' });
|
|
expect(second).toMatchObject({ artifactId: first.artifactId, jobId: first.jobId, deduplicated: true });
|
|
await expect(repository.listImportArtifacts(project.id)).resolves.toHaveLength(1);
|
|
await expect(repository.listIngestionJobs(project.id)).resolves.toHaveLength(1);
|
|
expect(await readdir(join(directory, 'objects'))).toHaveLength(1);
|
|
});
|
|
|
|
it('rejects non-GPO bytes during worker preflight before creating repository state', async () => {
|
|
await expect(service.submit({
|
|
projectId: project.id, fileName: 'not-a-backup.zip', bytes: new Uint8Array([1, 2, 3, 4]), receivedAt
|
|
})).rejects.toThrow();
|
|
await expect(repository.listImportArtifacts(project.id)).resolves.toEqual([]);
|
|
await expect(repository.listIngestionJobs(project.id)).resolves.toEqual([]);
|
|
});
|
|
it('rejects unknown projects and unsafe names before retaining bytes', async () => {
|
|
const bytes = syntheticGpoBackupZip();
|
|
await expect(service.submit({ projectId: 'missing', fileName: 'backup.zip', bytes, receivedAt })).rejects.toBeInstanceOf(RepositoryNotFoundError);
|
|
await expect(service.submit({ projectId: project.id, fileName: '../backup.zip', bytes, receivedAt })).rejects.toBeInstanceOf(RepositoryValidationError);
|
|
await expect(repository.listImportArtifacts(project.id)).resolves.toEqual([]);
|
|
});
|
|
|
|
it('requires the artifact root to be outside the published webroot', () => {
|
|
const publicRoot = resolve(directory, 'dist');
|
|
expect(() => assertArtifactStoreOutsidePublicRoot(resolve(publicRoot, 'uploads'), publicRoot)).toThrow();
|
|
expect(() => assertArtifactStoreOutsidePublicRoot(resolve(directory, 'artifacts'), publicRoot)).not.toThrow();
|
|
});
|
|
});
|