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(); }); });