import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import type { Server } from 'node:http'; import type { AddressInfo } from 'node:net'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { FileSystemBackendArtifactStore } from '../../server/artifacts/backend-artifact-store'; import { createAssessorHttpServer } from '../../server/assessor-http-server'; 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 { createGpoFolderTransport } from '../../src/gpo/gpo-folder-transport'; import { syntheticGpoBackupZip } from './gpo-synthetic-fixture'; const now = new Date('2026-07-21T09:00:00.000Z'); const project = { id: 'http-gpo-project', name: 'Synthetic HTTP project', createdAt: now.toISOString(), updatedAt: now.toISOString(), attributes: {} }; describe('GPO backup submission HTTP boundary', () => { let directory: string; let server: Server; let baseUrl: string; beforeEach(async () => { directory = await mkdtemp(join(tmpdir(), 'assessor-gpo-http-')); const staticRoot = join(directory, 'dist'); await writeFile(join(directory, 'placeholder'), 'synthetic'); const repository = new LocalAssessorRepository(); await repository.saveProject(project); const service = new GpoBackupSubmissionService( repository, new FileSystemBackendArtifactStore(join(directory, 'artifacts')), directGpoInventoryExecutor, 1024 * 1024 ); server = createAssessorHttpServer({ repository, staticRoot, now: () => now, gpoBackupSubmissionService: service }); await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); }); baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; }); afterEach(async () => { await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); await rm(directory, { recursive: true, force: true }); }); it('accepts a same-origin project upload and returns only opaque IDs and technical receipt data', async () => { const bytes = syntheticGpoBackupZip(); const response = await fetch(`${baseUrl}/api/v1/projects/${project.id}/gpo-backups`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: baseUrl }, body: JSON.stringify({ fileName: 'backup.zip', sizeBytes: bytes.byteLength, bytesBase64: Buffer.from(bytes).toString('base64') }) }); expect(response.status).toBe(202); const body = await response.json(); expect(body).toMatchObject({ status: 'queued', deduplicated: false, sizeBytes: bytes.byteLength }); expect(JSON.stringify(body)).not.toContain('storageKey'); expect(JSON.stringify(body)).not.toContain('synthetic.example'); }); it('returns the persisted project GPO status after leaving and reopening the tenant view', async () => { const bytes = syntheticGpoBackupZip(); const submitted = await fetch(`${baseUrl}/api/v1/projects/${project.id}/gpo-backups`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: baseUrl }, body: JSON.stringify({ fileName: 'persistent-status.zip', sizeBytes: bytes.byteLength, bytesBase64: Buffer.from(bytes).toString('base64') }) }); expect(submitted.status).toBe(202); const response = await fetch(`${baseUrl}/api/v1/projects/${project.id}/gpo-backups/status`); expect(response.status).toBe(200); await expect(response.json()).resolves.toMatchObject({ version: 1, projectId: project.id, records: [{ fileName: 'persistent-status.zip', state: 'queued', attempts: 0, gpoCount: 0, backupInstanceCount: 0 }] }); }); it('binds the uploaded GPO snapshot to the selected assessment timeslot', async () => { const timeslot = { id: 'gpo-timeslot', projectId: project.id, name: '22.07.2026', effectiveAt: '2026-07-22T12:00:00.000Z', createdAt: now.toISOString(), updatedAt: now.toISOString() }; const saved = await fetch(`${baseUrl}/api/v1/assessments`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1' }, body: JSON.stringify(timeslot) }); expect(saved.status).toBe(200); const bytes = syntheticGpoBackupZip(); const submitted = await fetch(`${baseUrl}/api/v1/projects/${project.id}/gpo-backups`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: baseUrl }, body: JSON.stringify({ fileName: 'timeslot-gpo.zip', sizeBytes: bytes.byteLength, bytesBase64: Buffer.from(bytes).toString('base64'), assessmentId: timeslot.id }) }); expect(submitted.status).toBe(202); await expect((await fetch(`${baseUrl}/api/v1/projects/${project.id}/gpo-backups/status`)).json()).resolves.toMatchObject({ records: [expect.objectContaining({ fileName: 'timeslot-gpo.zip', assessmentId: timeslot.id })] }); }); it('rejects size mismatches without exposing or storing content', async () => { const bytes = syntheticGpoBackupZip(); const response = await fetch(`${baseUrl}/api/v1/projects/${project.id}/gpo-backups`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: baseUrl }, body: JSON.stringify({ fileName: 'backup.zip', sizeBytes: bytes.byteLength + 1, bytesBase64: Buffer.from(bytes).toString('base64') }) }); expect(response.status).toBe(400); await expect(response.json()).resolves.toMatchObject({ error: { code: 'file_size_mismatch' } }); }); it('accepts a complete unzipped browser folder through the deterministic transport envelope', async () => { const encoder = new TextEncoder(); const folderEntries = [ ['SyntheticFolder/instance/Backup.xml', '{22222222-2222-4222-8222-222222222222}{11111111-1111-4111-8111-111111111111}synthetic.example2026-07-21T09:00:00Z'], ['SyntheticFolder/instance/DomainSysvol/GPO/gpt.ini', '[General]'] ].map(([relativePath, content]) => { const bytes = encoder.encode(content); return { relativePath, sizeBytes: bytes.byteLength, readBytes: async () => bytes }; }); const transport = await createGpoFolderTransport(folderEntries); const response = await fetch(`${baseUrl}/api/v1/projects/${project.id}/gpo-backups`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: baseUrl }, body: JSON.stringify({ fileName: transport.fileName, sizeBytes: transport.bytes.byteLength, bytesBase64: Buffer.from(transport.bytes).toString('base64') }) }); expect(response.status).toBe(202); await expect(response.json()).resolves.toMatchObject({ status: 'queued', deduplicated: false }); });});