91 lines
4.7 KiB
TypeScript
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([]);
|
|
});
|
|
});
|