189 lines
7.0 KiB
TypeScript
189 lines
7.0 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { POST, OPTIONS } = await import("../../src/app/api/v1/ocr/route.ts");
|
|
const { handleOcr } = await import("../../open-sse/handlers/ocr.ts");
|
|
const { OCR_PROVIDERS, getOcrProvider, parseOcrModel, getAllOcrModels } =
|
|
await import("../../open-sse/config/ocrRegistry.ts");
|
|
const { v1OcrSchema } = await import("../../src/shared/validation/schemas/apiV1.ts");
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
test.afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
|
|
function ocrRequest(body: string) {
|
|
return new Request("http://localhost/v1/ocr", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body,
|
|
});
|
|
}
|
|
|
|
// ── Registry ───────────────────────────────────────────────────────────────
|
|
|
|
test("OCR_PROVIDERS registers mistral with the Mistral OCR base URL", () => {
|
|
assert.equal(OCR_PROVIDERS.mistral.baseUrl, "https://api.mistral.ai/v1/ocr");
|
|
const provider = getOcrProvider("mistral");
|
|
assert.ok(provider);
|
|
assert.equal(provider.authHeader, "bearer");
|
|
assert.ok(provider.models.some((m: { id: string }) => m.id === "mistral-ocr-latest"));
|
|
});
|
|
|
|
test("parseOcrModel routes bare and prefixed mistral models to the mistral provider", () => {
|
|
assert.deepEqual(parseOcrModel("mistral-ocr-latest"), {
|
|
provider: "mistral",
|
|
model: "mistral-ocr-latest",
|
|
});
|
|
assert.deepEqual(parseOcrModel("mistral/mistral-ocr-latest"), {
|
|
provider: "mistral",
|
|
model: "mistral-ocr-latest",
|
|
});
|
|
// Unknown model → no provider resolved
|
|
assert.deepEqual(parseOcrModel("mystery-model"), {
|
|
provider: null,
|
|
model: "mystery-model",
|
|
});
|
|
});
|
|
|
|
test("getAllOcrModels exposes the mistral OCR model with a provider prefix", () => {
|
|
const models = getAllOcrModels();
|
|
assert.ok(models.some((m: { id: string }) => m.id === "mistral/mistral-ocr-latest"));
|
|
});
|
|
|
|
// ── Schema (Zod, Rule #7) ────────────────────────────────────────────────────
|
|
|
|
test("v1OcrSchema rejects a body without a document", () => {
|
|
const result = v1OcrSchema.safeParse({ model: "mistral-ocr-latest" });
|
|
assert.equal(result.success, false);
|
|
});
|
|
|
|
test("v1OcrSchema accepts a document_url document object", () => {
|
|
const result = v1OcrSchema.safeParse({
|
|
model: "mistral-ocr-latest",
|
|
document: { type: "document_url", document_url: "https://example.com/a.pdf" },
|
|
});
|
|
assert.equal(result.success, true);
|
|
});
|
|
|
|
test("v1OcrSchema accepts an image_url document object", () => {
|
|
const result = v1OcrSchema.safeParse({
|
|
document: { type: "image_url", image_url: "https://example.com/a.png" },
|
|
});
|
|
assert.equal(result.success, true);
|
|
});
|
|
|
|
// ── Route (public /v1/ocr entry point) ───────────────────────────────────────
|
|
|
|
test("POST /v1/ocr returns 400 for invalid JSON without leaking a stack trace", async () => {
|
|
const response = await POST(ocrRequest("not json at all"));
|
|
const body = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 400);
|
|
assert.equal(body.error.message, "Invalid JSON body");
|
|
// Rule #12 — error responses must never leak stack traces.
|
|
assert.ok(!body.error.message.includes("at /"));
|
|
});
|
|
|
|
test("OPTIONS /v1/ocr answers the CORS preflight", async () => {
|
|
const response = await OPTIONS();
|
|
assert.equal(response.status, 200);
|
|
assert.match(response.headers.get("access-control-allow-methods") || "", /OPTIONS/);
|
|
});
|
|
|
|
// ── Handler ──────────────────────────────────────────────────────────────────
|
|
|
|
test("handleOcr requires a document", async () => {
|
|
const response = await handleOcr({
|
|
body: { model: "mistral-ocr-latest" },
|
|
credentials: { apiKey: "sk-test" },
|
|
});
|
|
const payload = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 400);
|
|
assert.equal(payload.error.message, "document is required");
|
|
});
|
|
|
|
test("handleOcr rejects unknown OCR models", async () => {
|
|
const response = await handleOcr({
|
|
body: { model: "mystery/ocr", document: { document_url: "x" } },
|
|
credentials: { apiKey: "sk-test" },
|
|
});
|
|
const payload = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 400);
|
|
assert.match(payload.error.message, /No OCR provider found/);
|
|
});
|
|
|
|
test("handleOcr requires credentials for the resolved provider", async () => {
|
|
const response = await handleOcr({
|
|
body: { document: { document_url: "x" } },
|
|
credentials: null,
|
|
});
|
|
const payload = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 401);
|
|
assert.equal(payload.error.message, "No credentials for OCR provider: mistral");
|
|
});
|
|
|
|
test("handleOcr proxies a successful request to the mistral OCR endpoint", async () => {
|
|
let captured: any;
|
|
globalThis.fetch = async (url: any, options: any = {}) => {
|
|
captured = {
|
|
url: String(url),
|
|
headers: options.headers,
|
|
body: JSON.parse(String(options.body || "{}")),
|
|
};
|
|
return Response.json({ pages: [{ index: 0, markdown: "hello" }] });
|
|
};
|
|
|
|
const response = await handleOcr({
|
|
body: { document: { type: "document_url", document_url: "https://example.com/a.pdf" } },
|
|
credentials: { apiKey: "sk-mistral" },
|
|
});
|
|
|
|
assert.equal(captured.url, "https://api.mistral.ai/v1/ocr");
|
|
assert.equal(captured.headers.Authorization, "Bearer sk-mistral");
|
|
// model defaults to mistral-ocr-latest and the document is forwarded upstream.
|
|
assert.equal(captured.body.model, "mistral-ocr-latest");
|
|
assert.deepEqual(captured.body.document, {
|
|
type: "document_url",
|
|
document_url: "https://example.com/a.pdf",
|
|
});
|
|
assert.equal(response.status, 200);
|
|
assert.deepEqual(await response.json(), { pages: [{ index: 0, markdown: "hello" }] });
|
|
});
|
|
|
|
test("handleOcr passes upstream error payloads through with the upstream status", async () => {
|
|
globalThis.fetch = async () =>
|
|
new Response('{"error":"bad request"}', {
|
|
status: 422,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
|
|
const response = await handleOcr({
|
|
body: { model: "mistral/mistral-ocr-latest", document: { document_url: "x" } },
|
|
credentials: { apiKey: "sk-test" },
|
|
});
|
|
|
|
assert.equal(response.status, 422);
|
|
assert.equal(await response.text(), '{"error":"bad request"}');
|
|
});
|
|
|
|
test("handleOcr returns a sanitized 500 when the upstream request throws", async () => {
|
|
globalThis.fetch = async () => {
|
|
throw new Error("socket closed");
|
|
};
|
|
|
|
const response = await handleOcr({
|
|
body: { model: "mistral-ocr-latest", document: { document_url: "x" } },
|
|
credentials: { apiKey: "sk-test" },
|
|
});
|
|
const payload = (await response.json()) as any;
|
|
|
|
assert.equal(response.status, 500);
|
|
assert.match(payload.error.message, /OCR request failed: socket closed/);
|
|
assert.ok(!payload.error.message.includes("at /"));
|
|
});
|