133 lines
7.1 KiB
TypeScript
133 lines
7.1 KiB
TypeScript
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<void>((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<void>((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', '<?xml version="1.0"?><BackupInst><ID>{22222222-2222-4222-8222-222222222222}</ID><GPOGuid>{11111111-1111-4111-8111-111111111111}</GPOGuid><GPODomain>synthetic.example</GPODomain><BackupTime>2026-07-21T09:00:00Z</BackupTime></BackupInst>'],
|
|
['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 });
|
|
});});
|