79 lines
4.3 KiB
TypeScript
79 lines
4.3 KiB
TypeScript
import { Pool } from 'pg';
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
import type { AssessmentProject } from '../../src/domain/canonical-model';
|
|
import type { ImportArtifact, IngestionJob } from '../../src/domain/ingestion-persistence';
|
|
import type { ArtifactDeletionRequest } from '../../src/persistence/assessor-repository';
|
|
import { runDatabaseMigrations } from '../../server/postgres/migration-runner';
|
|
import { PostgresAssessorRepository } from '../../server/postgres/postgres-assessor-repository';
|
|
|
|
const connectionString = process.env.ASSESSOR_TEST_DATABASE_URL;
|
|
const describePostgres = connectionString ? describe : describe.skip;
|
|
const timestamp = '2026-07-21T12:00:00.000Z';
|
|
|
|
describePostgres('PostgreSQL failed GPO backup management', () => {
|
|
let pool: Pool;
|
|
let repository: PostgresAssessorRepository;
|
|
|
|
beforeAll(async () => {
|
|
pool = new Pool({ connectionString, max: 2, connectionTimeoutMillis: 5_000 });
|
|
await runDatabaseMigrations(pool);
|
|
repository = await PostgresAssessorRepository.create(pool);
|
|
}, 30_000);
|
|
|
|
afterAll(async () => {
|
|
await pool?.end();
|
|
});
|
|
|
|
it('requeues dead-letter work and deletes another failed artifact through the durable outbox', async () => {
|
|
const project: AssessmentProject = {
|
|
id: 'pg-gpo-management-project', name: 'Synthetic GPO management', createdAt: timestamp, updatedAt: timestamp, attributes: {}
|
|
};
|
|
await pool.query('DELETE FROM assessor_projects WHERE id = $1', [project.id]);
|
|
await repository.saveProject(project);
|
|
|
|
const artifact = (suffix: string): ImportArtifact => ({
|
|
version: 1, id: `pg-gpo-management-artifact-${suffix}`, projectId: project.id, kind: 'source-file',
|
|
relativePath: `gpo-backups/synthetic-${suffix}.zip`, sha256: suffix.repeat(64).slice(0, 64), sizeBytes: 10,
|
|
sensitivity: 'secret', parseStatus: 'failed',
|
|
storage: { kind: 'backend-controlled', storageKey: `synthetic-gpo-management-${suffix}` }, createdAt: timestamp
|
|
});
|
|
const job = (source: ImportArtifact, suffix: string): IngestionJob => ({
|
|
version: 1, id: `pg-gpo-management-job-${suffix}`, projectId: project.id, artifactId: source.id,
|
|
kind: 'analyze-gpo-backup', idempotencyKey: `synthetic-gpo-management-${suffix}`, status: 'queued',
|
|
attempts: 0, maxAttempts: 3, availableAt: timestamp, createdAt: timestamp, updatedAt: timestamp
|
|
});
|
|
|
|
const exhaust = async (jobId: string) => {
|
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
const now = `2026-07-21T12:00:0${attempt}.000Z`;
|
|
await repository.claimNextIngestionJob({
|
|
workerId: 'pg-gpo-management-worker', now, leaseDurationMs: 1_000, kinds: ['analyze-gpo-backup']
|
|
});
|
|
await repository.failIngestionJob(jobId, 'pg-gpo-management-worker', now, 'gpo-inventory-memory-limit', now);
|
|
}
|
|
};
|
|
|
|
const retryArtifact = artifact('a');
|
|
await repository.saveImportArtifactAndEnqueueJob(retryArtifact, job(retryArtifact, 'a'));
|
|
await exhaust('pg-gpo-management-job-a');
|
|
await expect(repository.retryIngestionJob(project.id, 'pg-gpo-management-job-a', '2026-07-21T12:01:00.000Z'))
|
|
.resolves.toMatchObject({ status: 'queued', attempts: 0, availableAt: '2026-07-21T12:01:00.000Z' });
|
|
|
|
const deleteArtifact = artifact('b');
|
|
await repository.saveImportArtifactAndEnqueueJob(deleteArtifact, job(deleteArtifact, 'b'));
|
|
await exhaust('pg-gpo-management-job-b');
|
|
const request: ArtifactDeletionRequest = {
|
|
version: 1, id: 'pg-gpo-management-delete-b', projectId: project.id, artifactId: deleteArtifact.id,
|
|
storageKey: 'synthetic-gpo-management-b', attempts: 0, nextAttemptAt: timestamp, createdAt: timestamp
|
|
};
|
|
await expect(repository.deleteFailedGpoArtifact(project.id, deleteArtifact.id, request))
|
|
.resolves.toMatchObject({ artifact: { id: deleteArtifact.id }, request: { id: request.id } });
|
|
await expect(repository.listImportArtifacts(project.id)).resolves.toEqual([retryArtifact]);
|
|
await expect(repository.listArtifactDeletionRequests(timestamp, 10)).resolves.toEqual([request]);
|
|
await repository.completeArtifactDeletionRequest(request.id);
|
|
await expect(repository.listArtifactDeletionRequests('2026-07-21T12:02:00.000Z', 10)).resolves.toEqual([]);
|
|
|
|
await repository.deleteProject(project.id, { projectId: project.id, cascade: true });
|
|
}, 30_000);
|
|
});
|