Assessor_2.0/tests/server/gpo-backup-management-http....

91 lines
4.7 KiB
TypeScript

import { mkdtemp, readdir, rm } 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 { GpoBackupManagementService } from '../../server/gpo/gpo-backup-management-service';
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 { syntheticGpoBackupZip } from './gpo-synthetic-fixture';
const initial = Date.parse('2026-07-21T17:00:00.000Z');
const project = { id: 'gpo-management-http', name: 'Synthetic management HTTP', createdAt: new Date(initial).toISOString(), updatedAt: new Date(initial).toISOString(), attributes: {} };
describe('GPO dead-letter management HTTP boundary', () => {
let directory: string;
let server: Server;
let baseUrl: string;
let repository: LocalAssessorRepository;
let store: FileSystemBackendArtifactStore;
let artifactId: string;
let jobId: string;
let currentTime: number;
async function deadLetter(): Promise<void> {
for (let attempt = 0; attempt < 3; attempt += 1) {
const claimedAt = new Date(currentTime).toISOString();
await repository.claimNextIngestionJob({ workerId: 'http-worker', now: claimedAt, leaseDurationMs: 60_000, kinds: ['analyze-gpo-backup'] });
currentTime += 1_000;
const failedAt = new Date(currentTime).toISOString();
await repository.failIngestionJob(jobId, 'http-worker', failedAt, 'synthetic-memory-limit', failedAt);
}
}
beforeEach(async () => {
directory = await mkdtemp(join(tmpdir(), 'assessor-gpo-management-http-'));
repository = new LocalAssessorRepository();
store = new FileSystemBackendArtifactStore(join(directory, 'artifacts'));
currentTime = initial;
await repository.saveProject(project);
const submission = new GpoBackupSubmissionService(repository, store, directGpoInventoryExecutor, 1024 * 1024);
const submitted = await submission.submit({ projectId: project.id, fileName: 'failed.zip', bytes: syntheticGpoBackupZip(), receivedAt: new Date(currentTime).toISOString() });
artifactId = submitted.artifactId;
jobId = submitted.jobId;
await deadLetter();
server = createAssessorHttpServer({
repository,
staticRoot: join(directory, 'dist'),
now: () => new Date(currentTime),
gpoBackupSubmissionService: submission,
gpoBackupManagementService: new GpoBackupManagementService(repository, store, () => new Date(currentTime))
});
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('retries and deletes only through exact project/artifact mutation routes with confirmation', async () => {
const retry = await fetch(`${baseUrl}/api/v1/projects/${project.id}/gpo-backups/${artifactId}/retry`, {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: baseUrl }, body: '{}'
});
expect(retry.status).toBe(200);
await expect(retry.json()).resolves.toMatchObject({ artifactId, jobId, status: 'queued', attempts: 0 });
await deadLetter();
const unconfirmed = await fetch(`${baseUrl}/api/v1/projects/${project.id}/gpo-backups/${artifactId}`, {
method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: baseUrl },
body: JSON.stringify({ artifactId, confirmDelete: false })
});
expect(unconfirmed.status).toBe(400);
const deleted = await fetch(`${baseUrl}/api/v1/projects/${project.id}/gpo-backups/${artifactId}`, {
method: 'DELETE', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: baseUrl },
body: JSON.stringify({ artifactId, confirmDelete: true })
});
expect(deleted.status).toBe(200);
await expect(deleted.json()).resolves.toMatchObject({ artifactId, deleted: true, storageRemoval: 'complete' });
await expect(readdir(join(directory, 'artifacts', 'objects'))).resolves.toEqual([]);
});
});