test(rollback): documenta ensaio isolado de recuperacao
This commit is contained in:
1 parent
bd0ecabf41
commit
4779d6b2a3
4 files changed
+447
No files matched your search
@@ -0,0 +1,199 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import ExcelJS from "exceljs";
|
||||
|
||||
/**
|
||||
* Jornada fictícia contra o ensaio de rollback, só em 127.0.0.1:3310.
|
||||
* Recusa a porta 3000 do compose principal. Não abre banco nem Docker.
|
||||
*/
|
||||
|
||||
const ALPHA = "11111111-1111-4111-8111-111111111111";
|
||||
const ANA = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
|
||||
const base = process.env.ROLLBACK_PROBE_URL ?? "http://127.0.0.1:3310";
|
||||
const mode = process.argv[2];
|
||||
const expectedRaw = process.argv[3];
|
||||
|
||||
function assertTarget(): void {
|
||||
const url = new URL(base);
|
||||
if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || url.port !== "3310") {
|
||||
throw new Error("alvo recusado: o ensaio só fala com http://127.0.0.1:3310");
|
||||
}
|
||||
}
|
||||
|
||||
function sha256(body: Buffer): string {
|
||||
return createHash("sha256").update(body).digest("hex");
|
||||
}
|
||||
|
||||
async function xlsx(rows: string[][]): Promise<Buffer> {
|
||||
const book = new ExcelJS.Workbook();
|
||||
const sheet = book.addWorksheet("Dados");
|
||||
for (const row of rows) sheet.addRow(row);
|
||||
return Buffer.from(await book.xlsx.writeBuffer()) as Buffer;
|
||||
}
|
||||
|
||||
async function login(): Promise<string> {
|
||||
const response = await fetch(`${base}/api/dev/session`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", accept: "application/json" },
|
||||
body: JSON.stringify({ userId: ANA, workspaceId: ALPHA }),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
if (response.status !== 200) throw new Error(`sessão local HTTP ${response.status}`);
|
||||
const lines = typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : [];
|
||||
const pair = lines.map((line) => line.split(";")[0]).find((line) => line.startsWith("adapter_dev_session="));
|
||||
if (!pair) throw new Error("cookie de sessão ausente");
|
||||
return pair;
|
||||
}
|
||||
|
||||
async function request(path: string, cookie: string, init?: RequestInit): Promise<{ status: number; body: unknown; bytes: Buffer; headers: Headers }> {
|
||||
const headers = new Headers(init?.headers);
|
||||
headers.set("cookie", cookie);
|
||||
headers.set("accept", headers.get("accept") ?? "application/json");
|
||||
const response = await fetch(`${base}${path}`, { ...init, headers, signal: AbortSignal.timeout(60_000) });
|
||||
const bytes = Buffer.from(await response.arrayBuffer());
|
||||
let body: unknown = bytes.toString("utf8");
|
||||
try {
|
||||
body = JSON.parse(bytes.toString("utf8"));
|
||||
} catch {
|
||||
/* arquivo binário */
|
||||
}
|
||||
return { status: response.status, body, bytes, headers: response.headers };
|
||||
}
|
||||
|
||||
async function waitJob(cookie: string, jobId: string): Promise<string> {
|
||||
let status = "";
|
||||
for (let attempt = 0; attempt < 45; attempt += 1) {
|
||||
const polled = await request(`/api/spreadsheet-adapter/jobs/${jobId}`, cookie);
|
||||
status = (polled.body as { job?: { status?: string } }).job?.status ?? "";
|
||||
if (status === "completed" || status === "failed") return status;
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
return status || "timeout";
|
||||
}
|
||||
|
||||
async function downloads(cookie: string, jobId: string, sourceId: string) {
|
||||
const files: Record<string, { status: number; sha256: string; size: number }> = {};
|
||||
for (const format of ["xlsx", "csv", "pdf"]) {
|
||||
const file = await request(`/api/spreadsheet-adapter/jobs/${jobId}/download?format=${format}&sourceId=${sourceId}`, cookie, {
|
||||
headers: { accept: "*/*" },
|
||||
});
|
||||
files[format] = { status: file.status, sha256: sha256(file.bytes), size: file.bytes.length };
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
async function seed(): Promise<void> {
|
||||
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
||||
const suffix = Math.random().toString(36).slice(2, 8).padEnd(6, "0");
|
||||
const modelName = `Rollback local ${stamp}-${suffix}`;
|
||||
const sentinel = `ROLLBACKLOCAL${suffix.toUpperCase()}`;
|
||||
const cookie = await login();
|
||||
const modelBytes = await xlsx([["Nome", "Codigo"]]);
|
||||
const sourceBytes = await xlsx([
|
||||
["Nome", "Codigo"],
|
||||
["Registro ficticio", sentinel],
|
||||
]);
|
||||
const modelForm = new FormData();
|
||||
modelForm.append("name", modelName);
|
||||
modelForm.append("requiredColumns", JSON.stringify(["Nome", "Codigo"]));
|
||||
modelForm.append("file", new File([new Uint8Array(modelBytes)], "modelo-ficticio.xlsx", { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }));
|
||||
const created = await request("/api/spreadsheet-adapter/models", cookie, { method: "POST", body: modelForm, headers: { accept: "application/json" } });
|
||||
const modelId = (created.body as { model?: { id?: string } }).model?.id ?? "";
|
||||
if (created.status !== 201 || !modelId) throw new Error(`modelo HTTP ${created.status}`);
|
||||
const job = await request("/api/spreadsheet-adapter/jobs", cookie, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ modelId, idempotencyKey: `rollback-job-${suffix}` }),
|
||||
});
|
||||
const jobId = (job.body as { job?: { id?: string } }).job?.id ?? "";
|
||||
if (job.status !== 201 || !jobId) throw new Error(`operação HTTP ${job.status}`);
|
||||
const sourceForm = new FormData();
|
||||
sourceForm.append("jobId", jobId);
|
||||
sourceForm.append("file", new File([new Uint8Array(sourceBytes)], "fonte-ficticia.xlsx", { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }));
|
||||
const uploaded = await request("/api/spreadsheet-adapter/sources/validate", cookie, { method: "POST", body: sourceForm, headers: { accept: "application/json" } });
|
||||
const sourceId = (uploaded.body as { job?: { sources?: { id?: string }[] } }).job?.sources?.[0]?.id ?? "";
|
||||
if (uploaded.status !== 200 || !sourceId) throw new Error(`fonte HTTP ${uploaded.status}`);
|
||||
const mapped = await request(`/api/spreadsheet-adapter/jobs/${jobId}/mappings`, cookie, {
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sourceId,
|
||||
mappings: [
|
||||
{ sourceColumn: "Nome", modelColumn: "Nome" },
|
||||
{ sourceColumn: "Codigo", modelColumn: "Codigo" },
|
||||
],
|
||||
}),
|
||||
});
|
||||
if (mapped.status !== 200) throw new Error(`mapeamento HTTP ${mapped.status}`);
|
||||
const preview = await request(`/api/spreadsheet-adapter/jobs/${jobId}/preview`, cookie, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ sourceId }),
|
||||
});
|
||||
const previewRows = (preview.body as { rows?: string[][] }).rows ?? [];
|
||||
if (preview.status !== 200 || !previewRows.some((row) => row.includes(sentinel))) throw new Error(`prévia HTTP ${preview.status}`);
|
||||
const queued = await request(`/api/spreadsheet-adapter/jobs/${jobId}/process`, cookie, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ sourceId, idempotencyKey: `rollback-proc-${suffix}` }),
|
||||
});
|
||||
const immediate = (queued.body as { job?: { status?: string } }).job?.status ?? "";
|
||||
if (queued.status !== 200 || (immediate !== "queued" && immediate !== "processing")) throw new Error(`processo HTTP ${queued.status} status=${immediate}`);
|
||||
const finalStatus = await waitJob(cookie, jobId);
|
||||
if (finalStatus !== "completed") throw new Error(`worker status=${finalStatus}`);
|
||||
const files = await downloads(cookie, jobId, sourceId);
|
||||
if (Object.values(files).some((file) => file.status !== 200 || file.size < 1)) throw new Error("download incompleto");
|
||||
console.log(JSON.stringify({ modelName, modelId, jobId, sourceId, modelSha256: sha256(modelBytes), sentinel, files }));
|
||||
}
|
||||
|
||||
async function verify(): Promise<void> {
|
||||
if (!expectedRaw) throw new Error("informe o JSON gravado na subida");
|
||||
const expected = JSON.parse(expectedRaw.startsWith("{") ? expectedRaw : readFileSync(expectedRaw, "utf8")) as {
|
||||
modelName: string;
|
||||
modelId: string;
|
||||
jobId: string;
|
||||
sourceId: string;
|
||||
modelSha256: string;
|
||||
files: Record<string, { sha256: string }>;
|
||||
};
|
||||
const cookie = await login();
|
||||
const ready = await request("/api/health/ready", cookie);
|
||||
const models = await request("/api/spreadsheet-adapter/models", cookie);
|
||||
const list = (models.body as { models?: { id?: string; name?: string }[] }).models ?? [];
|
||||
const found = list.some((model) => model.id === expected.modelId && model.name === expected.modelName);
|
||||
const job = await request(`/api/spreadsheet-adapter/jobs/${expected.jobId}`, cookie);
|
||||
const status = (job.body as { job?: { status?: string } }).job?.status ?? "";
|
||||
const files = await downloads(cookie, expected.jobId, expected.sourceId);
|
||||
const stored = (["xlsx", "csv"] as const).every((format) => files[format].sha256 === expected.files[format].sha256 && files[format].status === 200);
|
||||
const pdf = files.pdf.status === 200 && files.pdf.size > 4;
|
||||
const report = {
|
||||
ready: (ready.body as { status?: string }).status ?? ready.status,
|
||||
modelFound: found,
|
||||
jobStatus: status,
|
||||
storedFilesMatch: stored,
|
||||
pdfRegenerated: pdf,
|
||||
sha256: {
|
||||
xlsx: files.xlsx.sha256 === expected.files.xlsx.sha256,
|
||||
csv: files.csv.sha256 === expected.files.csv.sha256,
|
||||
pdf: files.pdf.sha256 === expected.files.pdf.sha256,
|
||||
},
|
||||
};
|
||||
console.log(JSON.stringify(report));
|
||||
if (report.ready !== "ready" || !found || status !== "completed" || !stored || !pdf) process.exitCode = 1;
|
||||
}
|
||||
|
||||
assertTarget();
|
||||
if (mode === "seed") {
|
||||
seed().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.message : "erro");
|
||||
process.exit(1);
|
||||
});
|
||||
} else if (mode === "verify") {
|
||||
verify().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.message : "erro");
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
console.error("uso: rollback-local-probe.ts seed|verify [json]");
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in new issue
Block a user