Files
0pt/tests/p0/graph-mailbox-adapter.test.ts
T
adminandClaude Sonnet 5 c89731115c feat: add 0pt.query_source, 0pt.document_ocr, 0pt.write_external_record capabilities
Three new, tenant-agnostic P0 capabilities:

- 0pt.query_source: bounded, timeout/row-capped ad-hoc SQL query execution
  against registered database sources, via a Fabric/OneLake adapter
  (Azure AD credential-based mssql connection), with AUD0 audit events
  and an app/api/0pt/sources/[sourceId]/query endpoint.

- 0pt.document_ocr (document capture + extraction): mailbox-based document
  intake via Microsoft Graph, OCR/field extraction via Azure Document
  Intelligence, with capture/extraction API endpoints and services that
  never fabricate extracted values.

- 0pt.write_external_record (external writeback): a generic writeback-adapter
  contract plus a concrete Odoo bills adapter that matches partners via
  Fabric, backing an external-writeback-service for pushing records to
  downstream systems of record.

Known interim gaps:
- src/adapters/local-blob-store.ts is an interim ST0RE substitute pending
  the real ST0RE artifact-store integration.
- mssql is used by the Fabric/OneLake adapter but deliberately not yet
  added to package.json (pending `pnpm add mssql` once the concurrent
  session's package.json/dependency changes settle).

A separate, concurrently-active session has unrelated in-flight changes
in this same working tree (data-platform catalog/lineage/feature-store,
dataset quality, lakehouse/Iceberg docs, deploy scaffolding, package.json/
pnpm-lock.yaml updates, etc.) — none of that is touched or included here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 19:32:43 +02:00

150 lines
5.5 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { GraphMailboxAdapter } from "@/src/adapters/graph-mailbox-adapter";
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("GraphMailboxAdapter", () => {
const config = {
tenantId: "tenant-1",
clientId: "client-1",
clientSecret: "secret-1",
mailboxAddress: "docs@customer.com",
folder: "inbox",
};
beforeEach(() => {
vi.restoreAllMocks();
});
it("isConfigured requires tenantId/clientId/clientSecret/mailboxAddress", () => {
expect(GraphMailboxAdapter.isConfigured(config)).toBe(true);
expect(GraphMailboxAdapter.isConfigured({ ...config, clientSecret: "" })).toBe(false);
expect(GraphMailboxAdapter.isConfigured({ ...config, mailboxAddress: "" })).toBe(false);
});
it("polls messages across pages, following @odata.nextLink up to maxPages", async () => {
const calls: string[] = [];
const fetchMock = vi.fn(async (url: string) => {
calls.push(url);
if (url.includes("login.microsoftonline.com")) {
return jsonResponse({ access_token: "tok123", expires_in: 3600 });
}
if (url.includes("/messages?")) {
return jsonResponse({
value: [
{
id: "msg-1",
internetMessageId: "<msg-1@customer.com>",
conversationId: "conv-1",
receivedDateTime: "2026-07-01T10:00:00Z",
from: { emailAddress: { name: "Vendor AB", address: "vendor@example.com" } },
toRecipients: [{ emailAddress: { address: "docs@customer.com" } }],
subject: "Document 1",
bodyPreview: "preview",
body: { content: "<p>Hello <b>World</b></p>" },
hasAttachments: true,
webLink: "https://outlook.office.com/mail/1",
},
],
"@odata.nextLink": "https://graph.microsoft.com/v1.0/next-page",
});
}
if (url.includes("next-page")) {
return jsonResponse({
value: [
{
id: "msg-2",
receivedDateTime: "2026-07-01T11:00:00Z",
from: { emailAddress: { name: "Vendor 2", address: "vendor2@example.com" } },
toRecipients: [],
subject: "Document 2",
hasAttachments: false,
},
],
});
}
throw new Error(`unexpected fetch: ${url}`);
});
vi.stubGlobal("fetch", fetchMock);
const adapter = new GraphMailboxAdapter(config);
const messages = await adapter.pollMessages({ top: 50, maxPages: 2 });
expect(messages).toHaveLength(2);
expect(messages[0].id).toBe("msg-1");
expect(messages[0].senderEmail).toBe("vendor@example.com");
expect(messages[0].bodyText).toBe("Hello World");
expect(messages[1].id).toBe("msg-2");
expect(messages[1].hasAttachments).toBe(false);
expect(calls.some((u) => u.includes("mailFolders/inbox/messages"))).toBe(true);
});
it("lists only file attachments and flags PDFs by content-type or filename", async () => {
const pdfBase64 = Buffer.from("%PDF-1.4 fake pdf bytes").toString("base64");
const fetchMock = vi.fn(async (url: string) => {
if (url.includes("login.microsoftonline.com")) {
return jsonResponse({ access_token: "tok123", expires_in: 3600 });
}
if (url.includes("/attachments")) {
return jsonResponse({
value: [
{
"@odata.type": "#microsoft.graph.fileAttachment",
id: "att-1",
name: "invoice.pdf",
contentType: "application/pdf",
size: 123,
contentBytes: pdfBase64,
},
{
"@odata.type": "#microsoft.graph.fileAttachment",
id: "att-2",
name: "logo.png",
contentType: "image/png",
size: 50,
contentBytes: Buffer.from("not-a-pdf").toString("base64"),
},
{
// No mime type but a .pdf filename should still be detected.
"@odata.type": "#microsoft.graph.fileAttachment",
id: "att-3",
name: "scan.PDF",
contentType: "application/octet-stream",
size: 999,
contentBytes: pdfBase64,
},
{
// Non-file attachment (e.g. embedded email) must be skipped entirely.
"@odata.type": "#microsoft.graph.itemAttachment",
id: "att-4",
name: "embedded-email",
},
],
});
}
throw new Error(`unexpected fetch: ${url}`);
});
vi.stubGlobal("fetch", fetchMock);
const adapter = new GraphMailboxAdapter(config);
const attachments = await adapter.listAttachments("msg-1");
expect(attachments).toHaveLength(3); // itemAttachment skipped
const pdf = attachments.find((a) => a.name === "invoice.pdf")!;
expect(pdf.isPdf).toBe(true);
expect(pdf.contentBytes.toString()).toContain("%PDF-1.4");
const png = attachments.find((a) => a.name === "logo.png")!;
expect(png.isPdf).toBe(false);
const scanByExt = attachments.find((a) => a.name === "scan.PDF")!;
expect(scanByExt.isPdf).toBe(true);
});
});