Files
0pt/tests/p0/document-capture-mailbox.test.ts
T
admin 1b6140a16a
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 10s
masterplan-lock / check (pull_request) Successful in 9s
Security Scan (SIAX Cloud) / security (push) Successful in 24s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 24s
CI (SIAX Cloud) / security (pull_request) Successful in 48s
CI (SIAX Cloud) / security (push) Successful in 49s
CI (SIAX Cloud) / integration (pull_request) Successful in 49s
CI (SIAX Cloud) / quality (push) Failing after 56s
CI (SIAX Cloud) / quality (pull_request) Failing after 1m1s
fix(lint): rotfelsa ärvd lint-skuld — graph-adapter + p0-tester (blockerade #128)
Parallellsessions-merge lämnade 12 eslint-errors på main (no-explicit-any i
graph-mailbox-adapter + 2 p0-tester). Graph-JSON:n är genuint ostrukturerad
— medveten, avgränsad eslint-undantag med skäl (DoD §13); p0-testerna
typade med Record/cast. Lint 0 errors; tsc grön; 1141/1141.
2026-09-29 12:31:34 +02:00

171 lines
7.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import os from "os";
import path from "path";
import fs from "fs";
import { DocumentCaptureService } from "@/src/services/document-capture-service";
import { LocalBlobStore } from "@/src/adapters/local-blob-store";
function jsonResponse(body: unknown, opts: { status?: number; headers?: Record<string, string> } = {}) {
return {
ok: (opts.status ?? 200) < 400,
status: opts.status ?? 200,
headers: { get: (k: string) => (opts.headers ?? {})[k] ?? null },
json: async () => body,
text: async () => JSON.stringify(body),
} as unknown as Response;
}
describe("DocumentCaptureService.captureFromMailbox", () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "0pt-blob-"));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
vi.restoreAllMocks();
});
it("downloads PDF attachments, persists bytes, registers a ST0RE artifact, enqueues an N0D extraction job, and emits AUD0 events", async () => {
const pdfBytes = Buffer.from("%PDF-1.4 real bytes for test");
const pdfBase64 = pdfBytes.toString("base64");
const aud0Calls: { url: string; body: Record<string, unknown> }[] = [];
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
if (url.includes("login.microsoftonline.com")) return jsonResponse({ access_token: "tok", expires_in: 3600 });
if (url.includes("/messages?")) {
return jsonResponse({
value: [
{
id: "msg-1",
receivedDateTime: "2026-07-01T09:00:00Z",
from: { emailAddress: { name: "Sender", address: "sender@example.com" } },
toRecipients: [],
subject: "A document",
bodyPreview: "preview",
hasAttachments: true,
webLink: "https://outlook.office.com/mail/1",
},
],
});
}
if (url.includes("/attachments")) {
return jsonResponse({
value: [
{
"@odata.type": "#microsoft.graph.fileAttachment",
id: "att-1",
name: "doc.pdf",
contentType: "application/pdf",
size: pdfBytes.length,
contentBytes: pdfBase64,
},
],
});
}
if (url.includes("/v1/aud0/events") || url.includes("/v1/aud0/openlineage")) {
aud0Calls.push({ url, body: init?.body ? JSON.parse(init.body as string) : null });
return jsonResponse({});
}
throw new Error(`unexpected fetch: ${url}`);
});
vi.stubGlobal("fetch", fetchMock);
const service = new DocumentCaptureService({
blobStore: new LocalBlobStore(tmpDir),
aud0BaseUrl: "https://aud0.example.com",
});
const result = await service.captureFromMailbox(
{ mailbox: { tenantId: "t", clientId: "c", clientSecret: "s", mailboxAddress: "docs@customer.com" } },
{ tenant_id: "tenant1", project_id: "proj1", actor_id: "actor1", correlation_id: "corr1" },
);
expect(result.scanned_messages).toBe(1);
expect(result.captured_documents).toHaveLength(1);
const doc = result.captured_documents[0];
expect(doc.status).toBe("extraction_requested");
expect(doc.filename).toBe("doc.pdf");
expect(doc.mailbox_address).toBe("docs@customer.com");
expect(doc.st0re_artifact_id).toContain("mock_artifact_");
expect(doc.n0d_job_id).toContain("mock_job_");
// Bytes were actually persisted (INTERIM local store) and are readable back byte-for-byte.
const storedBytes = await service.getDocumentBytes(doc.document_id);
expect(storedBytes.equals(pdfBytes)).toBe(true);
const captured = aud0Calls.find((c) => c.body?.event_type === "0pt.document_captured") as { url: string; body: { event_type: string; capability_id: string; payload: { filename: string } } } | undefined;
expect(captured).toBeTruthy();
expect(captured!.body.payload.filename).toBe("doc.pdf");
expect(captured!.body.capability_id).toBe("0pt.capture_document");
// Lineage now goes to AUD0's REAL ingest contract: an OpenLineage
// RunEvent on /v1/aud0/openlineage.
const lineage = aud0Calls.find((c) => c.url.includes("openlineage")) as { url: string; body: { outputs: { name: string }[] } } | undefined;
expect(lineage).toBeTruthy();
expect(lineage!.body.outputs[0].name).toContain(doc.document_id);
});
it("skips non-PDF attachments and records per-message errors without aborting the whole run", async () => {
const fetchMock = vi.fn(async (url: string) => {
if (url.includes("login.microsoftonline.com")) return jsonResponse({ access_token: "tok", expires_in: 3600 });
if (url.includes("/messages?")) {
return jsonResponse({
value: [
{
id: "msg-ok",
receivedDateTime: "2026-07-01T09:00:00Z",
hasAttachments: true,
from: { emailAddress: {} },
toRecipients: [],
},
{
id: "msg-bad",
receivedDateTime: "2026-07-01T09:05:00Z",
hasAttachments: true,
from: { emailAddress: {} },
toRecipients: [],
},
],
});
}
if (url.includes("msg-ok/attachments")) {
return jsonResponse({
value: [
{
"@odata.type": "#microsoft.graph.fileAttachment",
id: "a1",
name: "x.png",
contentType: "image/png",
contentBytes: Buffer.from("x").toString("base64"),
},
],
});
}
if (url.includes("msg-bad/attachments")) {
return { ok: false, status: 500, headers: { get: () => null }, json: async () => ({ error: { code: "Fail", message: "boom" } }) } as unknown as Response;
}
throw new Error(`unexpected fetch: ${url}`);
});
vi.stubGlobal("fetch", fetchMock);
const service = new DocumentCaptureService({ blobStore: new LocalBlobStore(tmpDir) });
const result = await service.captureFromMailbox(
{ mailbox: { tenantId: "t", clientId: "c", clientSecret: "s", mailboxAddress: "docs@customer.com" } },
{ tenant_id: "tenant1", project_id: "proj1", actor_id: "actor1", correlation_id: "corr1" },
);
expect(result.scanned_messages).toBe(2);
expect(result.captured_documents).toHaveLength(0); // png skipped, msg-bad errored
expect(result.errors).toHaveLength(1);
expect(result.errors[0]).toContain("msg-bad");
});
it("rejects when mailbox credentials are incomplete", async () => {
const service = new DocumentCaptureService({ blobStore: new LocalBlobStore(tmpDir) });
await expect(
service.captureFromMailbox(
{ mailbox: { tenantId: "t", clientId: "", clientSecret: "s", mailboxAddress: "docs@customer.com" } },
{ tenant_id: "tenant1", project_id: "proj1", actor_id: "actor1", correlation_id: "corr1" },
),
).rejects.toThrow(/missing mailbox/i);
});
});