CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (push) Successful in 5s
Security Scan (SIAX Cloud) / security (push) Successful in 15s
CI (SIAX Cloud) / security (push) Successful in 16s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 7s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 21s
CI (SIAX Cloud) / security (pull_request) Successful in 25s
CI (SIAX Cloud) / quality (push) Successful in 1m17s
CI (SIAX Cloud) / integration (pull_request) Successful in 54s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m7s
173 lines
7.1 KiB
TypeScript
173 lines
7.1 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 artifactCalls = vi.spyOn(service['st0re'], 'createArtifact');
|
|
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(artifactCalls).toHaveBeenCalledWith(expect.objectContaining({ pii_level: 'high' }));
|
|
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);
|
|
});
|
|
});
|