mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
717 lines
22 KiB
TypeScript
717 lines
22 KiB
TypeScript
/**
|
|
* `scripts/import-documents.mjs` against recorded-shape fixtures of the two
|
|
* source APIs and a stand-in HealthLog, all on local HTTP servers.
|
|
*
|
|
* The fixtures are the shapes the script depends on (Paperless-ngx API v10
|
|
* pagination, tag and document-type lookups, `?original=true` downloads;
|
|
* Papra's organization-scoped document list, tags and `/file`), so a change
|
|
* on either side shows up here as a fixture to refresh rather than as a user
|
|
* report. The HealthLog stand-in answers the upload contract the real route
|
|
* speaks: 201, 200 duplicate, 200 deleted, 413, 429 with Retry-After, 403
|
|
* `module.disabled`.
|
|
*/
|
|
import { spawn } from "node:child_process";
|
|
import { createServer, type IncomingMessage, type Server } from "node:http";
|
|
import type { AddressInfo } from "node:net";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
const SCRIPT = join(process.cwd(), "scripts/import-documents.mjs");
|
|
const PDF = Buffer.from("%PDF-1.7\n%fixture\n%%EOF\n");
|
|
|
|
const servers: Server[] = [];
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
servers.splice(0).map((s) => new Promise((r) => s.close(() => r(null)))),
|
|
);
|
|
});
|
|
|
|
type Handler = (
|
|
req: IncomingMessage,
|
|
url: URL,
|
|
body: Buffer,
|
|
) => Promise<{
|
|
status: number;
|
|
json?: unknown;
|
|
bytes?: Buffer;
|
|
headers?: Record<string, string>;
|
|
}>;
|
|
|
|
async function serve(handler: Handler): Promise<string> {
|
|
const server = createServer(async (req, res) => {
|
|
const chunks: Buffer[] = [];
|
|
for await (const c of req) chunks.push(c as Buffer);
|
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
const r = await handler(req, url, Buffer.concat(chunks));
|
|
const headers: Record<string, string> = { ...(r.headers ?? {}) };
|
|
if (r.json !== undefined) headers["content-type"] = "application/json";
|
|
res.writeHead(r.status, headers);
|
|
res.end(r.json !== undefined ? JSON.stringify(r.json) : (r.bytes ?? ""));
|
|
});
|
|
servers.push(server);
|
|
await new Promise<void>((r) => server.listen(0, "127.0.0.1", () => r()));
|
|
return `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
}
|
|
|
|
function runScript(
|
|
args: string[],
|
|
env: Record<string, string>,
|
|
): Promise<{ code: number | null; stdout: string; stderr: string }> {
|
|
return new Promise((resolve) => {
|
|
const child = spawn(process.execPath, [SCRIPT, ...args], {
|
|
env: { PATH: process.env.PATH ?? "", ...env },
|
|
});
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.on("data", (d) => (stdout += d));
|
|
child.stderr.on("data", (d) => (stderr += d));
|
|
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
});
|
|
}
|
|
|
|
interface Received {
|
|
auth: string | undefined;
|
|
fields: Record<string, string>;
|
|
filename: string | null;
|
|
size: number;
|
|
}
|
|
|
|
/**
|
|
* A HealthLog stand-in. Keys stored documents by source key the way the real
|
|
* route does, answers the first upload with a 429, and refuses keys listed as
|
|
* deleted or too large.
|
|
*/
|
|
async function healthlog(
|
|
opts: {
|
|
deleted?: string[];
|
|
tooLarge?: string[];
|
|
moduleOff?: boolean;
|
|
/** An older HealthLog with no lookup route. */
|
|
noLookup?: boolean;
|
|
/** The address redirects elsewhere (http → https, a moved host). */
|
|
redirect?: string;
|
|
/** Lookups are spent: every lookup answers 429 with a long Retry-After. */
|
|
lookup429?: boolean;
|
|
} = {},
|
|
) {
|
|
const stored = new Set<string>();
|
|
const received: Received[] = [];
|
|
const lookups: string[] = [];
|
|
let throttled = false;
|
|
const url = await serve(async (req, url, body) => {
|
|
if (opts.redirect) {
|
|
return {
|
|
status: 301,
|
|
headers: { location: `${opts.redirect}${url.pathname}` },
|
|
};
|
|
}
|
|
const queryKey = `${url.searchParams.get("sourceSystem")}:${url.searchParams.get("sourceId")}`;
|
|
if (
|
|
url.pathname === "/api/documents/inbound/source" &&
|
|
req.method === "GET" &&
|
|
!opts.noLookup
|
|
) {
|
|
lookups.push(queryKey);
|
|
if (opts.lookup429) {
|
|
return {
|
|
status: 429,
|
|
headers: { "retry-after": "3600" },
|
|
json: { data: null, error: "Too many lookups." },
|
|
};
|
|
}
|
|
const deleted = opts.deleted?.includes(queryKey) ?? false;
|
|
const known = deleted || stored.has(queryKey);
|
|
return {
|
|
status: 200,
|
|
json: {
|
|
data: { known, id: known ? queryKey : null, deleted },
|
|
error: null,
|
|
},
|
|
};
|
|
}
|
|
if (url.pathname !== "/api/documents/inbound" || req.method !== "POST") {
|
|
return { status: 404, json: { data: null, error: "Not found" } };
|
|
}
|
|
if (opts.moduleOff) {
|
|
return {
|
|
status: 403,
|
|
json: {
|
|
data: null,
|
|
error: 'Module "inboundDocuments" is not enabled',
|
|
meta: { errorCode: "module.disabled" },
|
|
},
|
|
};
|
|
}
|
|
if (!throttled) {
|
|
throttled = true;
|
|
return {
|
|
status: 429,
|
|
headers: { "retry-after": "0" },
|
|
json: {
|
|
data: null,
|
|
error: "Too many uploads.",
|
|
meta: { errorCode: "documents.inbound.rateLimited" },
|
|
},
|
|
};
|
|
}
|
|
const form = await new Response(body, {
|
|
headers: { "content-type": req.headers["content-type"] ?? "" },
|
|
}).formData();
|
|
const fields: Record<string, string> = {};
|
|
let filename: string | null = null;
|
|
let size = 0;
|
|
for (const [k, v] of form.entries()) {
|
|
if (typeof v === "string") fields[k] = v;
|
|
else {
|
|
filename = v.name;
|
|
size = v.size;
|
|
}
|
|
}
|
|
// The key rides the form only: in the address HealthLog would meter it
|
|
// against the lookup allowance, which a spent lookup has already used up.
|
|
if (
|
|
url.searchParams.has("sourceSystem") ||
|
|
url.searchParams.has("sourceId")
|
|
) {
|
|
return {
|
|
status: 429,
|
|
headers: { "retry-after": "3600" },
|
|
json: { data: null, error: "lookup allowance spent" },
|
|
};
|
|
}
|
|
received.push({ auth: req.headers.authorization, fields, filename, size });
|
|
const key = `${fields.sourceSystem}:${fields.sourceId}`;
|
|
if (opts.tooLarge?.includes(key)) {
|
|
return {
|
|
status: 413,
|
|
json: {
|
|
data: null,
|
|
error: "File is too large.",
|
|
meta: { reason: "fileTooLarge", maxFileBytes: 26_214_400 },
|
|
},
|
|
};
|
|
}
|
|
if (opts.deleted?.includes(key)) {
|
|
return {
|
|
status: 200,
|
|
json: {
|
|
data: { id: null, duplicate: true, deleted: true },
|
|
error: null,
|
|
meta: { duplicate: true, deleted: true },
|
|
},
|
|
};
|
|
}
|
|
if (stored.has(key)) {
|
|
return {
|
|
status: 200,
|
|
json: {
|
|
data: { id: key, duplicate: true },
|
|
error: null,
|
|
meta: { duplicate: true },
|
|
},
|
|
};
|
|
}
|
|
stored.add(key);
|
|
return {
|
|
status: 201,
|
|
json: { data: { id: key, duplicate: false }, error: null },
|
|
};
|
|
});
|
|
return { url, received, lookups };
|
|
}
|
|
|
|
/** Paperless-ngx v10 stand-in: two pages of documents, one outside the tag. */
|
|
async function paperless() {
|
|
const seen: { path: string; auth?: string; accept?: string }[] = [];
|
|
const url = await serve(async (req, url) => {
|
|
seen.push({
|
|
path: url.pathname + url.search,
|
|
auth: req.headers.authorization,
|
|
accept: req.headers.accept,
|
|
});
|
|
const base = `http://${req.headers.host}`;
|
|
if (url.pathname === "/api/tags/") {
|
|
return {
|
|
status: 200,
|
|
json: {
|
|
count: 1,
|
|
next: null,
|
|
results: [{ id: 7, name: "HealthLog" }],
|
|
},
|
|
};
|
|
}
|
|
if (url.pathname === "/api/document_types/") {
|
|
return {
|
|
status: 200,
|
|
json: { count: 1, next: null, results: [{ id: 3, name: "Labor" }] },
|
|
};
|
|
}
|
|
if (url.pathname === "/api/documents/") {
|
|
if (url.searchParams.get("page") === "2") {
|
|
return {
|
|
status: 200,
|
|
json: {
|
|
count: 3,
|
|
next: null,
|
|
results: [
|
|
{
|
|
id: 3,
|
|
title: "Car insurance",
|
|
created: "2024-05-01",
|
|
added: "2024-05-02T10:00:00Z",
|
|
tags: [9],
|
|
document_type: null,
|
|
original_file_name: "car.pdf",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
const next = new URL(url.toString(), base);
|
|
next.searchParams.set("page", "2");
|
|
return {
|
|
status: 200,
|
|
json: {
|
|
count: 3,
|
|
// Deliberately another origin, as behind a reverse proxy.
|
|
next: `https://paperless.internal${next.pathname}${next.search}`,
|
|
results: [
|
|
{
|
|
id: 1,
|
|
title: "Blutbild März",
|
|
created: "2024-03-05",
|
|
added: "2024-03-06T08:00:00Z",
|
|
tags: [7],
|
|
document_type: 3,
|
|
original_file_name: "blutbild.pdf",
|
|
},
|
|
{
|
|
id: 2,
|
|
// Blank in the source: the file name stands in.
|
|
title: " ",
|
|
created: null,
|
|
added: "2024-04-01T09:30:00Z",
|
|
tags: [7],
|
|
document_type: null,
|
|
original_file_name: "brief.pdf",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
const download = /^\/api\/documents\/(\d+)\/download\/$/.exec(url.pathname);
|
|
if (download) {
|
|
return {
|
|
status: 200,
|
|
bytes: PDF,
|
|
headers: {
|
|
"content-type": "application/pdf",
|
|
"content-disposition": `attachment; filename="doc-${download[1]}.pdf"`,
|
|
},
|
|
};
|
|
}
|
|
const metadata = /^\/api\/documents\/(\d+)\/metadata\/$/.exec(url.pathname);
|
|
if (metadata) {
|
|
return { status: 200, json: { original_size: 1_048_576 } };
|
|
}
|
|
return { status: 404, json: { detail: "Not found." } };
|
|
});
|
|
return { url, seen };
|
|
}
|
|
|
|
/** Papra stand-in: one organization, a tag, three documents. */
|
|
async function papra() {
|
|
const seen: { path: string; auth?: string }[] = [];
|
|
const url = await serve(async (req, url) => {
|
|
seen.push({
|
|
path: url.pathname + url.search,
|
|
auth: req.headers.authorization,
|
|
});
|
|
const org = "/api/organizations/org_1";
|
|
if (url.pathname === `${org}/tags`) {
|
|
return {
|
|
status: 200,
|
|
json: {
|
|
tags: [
|
|
{ id: "tag_h", name: "Health" },
|
|
{ id: "tag_l", name: "Lab" },
|
|
],
|
|
},
|
|
};
|
|
}
|
|
if (url.pathname === `${org}/documents`) {
|
|
return {
|
|
status: 200,
|
|
json: {
|
|
documentsCount: 4,
|
|
documents: [
|
|
{
|
|
id: "doc_a",
|
|
name: "Impfpass.pdf",
|
|
originalName: "impfpass.pdf",
|
|
originalSize: 2048,
|
|
createdAt: "2025-01-10T12:00:00.000Z",
|
|
documentDate: "2019-06-01",
|
|
tags: [{ id: "tag_h", name: "Health" }],
|
|
},
|
|
{
|
|
id: "doc_b",
|
|
name: "Labor.pdf",
|
|
originalName: "labor.pdf",
|
|
originalSize: 4096,
|
|
createdAt: "2025-02-10T12:00:00.000Z",
|
|
tags: [
|
|
{ id: "tag_h", name: "Health" },
|
|
{ id: "tag_l", name: "Lab" },
|
|
],
|
|
},
|
|
{
|
|
id: "doc_huge",
|
|
name: "Scan.pdf",
|
|
originalName: "scan.pdf",
|
|
originalSize: 40_000_000,
|
|
createdAt: "2025-03-10T12:00:00.000Z",
|
|
documentDate: "2020-03-10",
|
|
tags: [{ id: "tag_h", name: "Health" }],
|
|
},
|
|
{
|
|
id: "doc_old",
|
|
name: "Old.pdf",
|
|
originalName: "old.pdf",
|
|
originalSize: 100,
|
|
createdAt: "2015-02-10T12:00:00.000Z",
|
|
documentDate: "2015-02-10",
|
|
tags: [{ id: "tag_h", name: "Health" }],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
if (
|
|
/^\/api\/organizations\/org_1\/documents\/[\w]+\/file$/.test(url.pathname)
|
|
) {
|
|
return {
|
|
status: 200,
|
|
bytes: PDF,
|
|
headers: { "content-type": "application/pdf" },
|
|
};
|
|
}
|
|
return { status: 404, json: { error: "not found" } };
|
|
});
|
|
return { url, seen };
|
|
}
|
|
|
|
describe("import-documents.mjs — Paperless-ngx", () => {
|
|
it("imports the tagged documents with their title, date, kind and source key", async () => {
|
|
const hl = await healthlog();
|
|
const pl = await paperless();
|
|
const env = { HEALTHLOG_TOKEN: "hlk_test", PAPERLESS_TOKEN: "pl_test" };
|
|
const args = [
|
|
"paperless",
|
|
"--paperless-url",
|
|
pl.url,
|
|
"--healthlog-url",
|
|
hl.url,
|
|
"--tag",
|
|
"HealthLog",
|
|
"--kind-map",
|
|
"Labor=LAB_RESULT",
|
|
];
|
|
const first = await runScript(args, env);
|
|
|
|
expect(first.stderr).toBe("");
|
|
expect(first.code).toBe(0);
|
|
// The first upload met a 429 and was sent again, not dropped.
|
|
expect(hl.received).toHaveLength(2);
|
|
expect(hl.received[0]).toMatchObject({
|
|
auth: "Bearer hlk_test",
|
|
filename: "blutbild.pdf",
|
|
size: PDF.byteLength,
|
|
fields: {
|
|
title: "Blutbild März",
|
|
documentDate: "2024-03-05",
|
|
kind: "LAB_RESULT",
|
|
sourceSystem: "PAPERLESS",
|
|
sourceId: "1",
|
|
sourceInstance: new URL(pl.url).origin,
|
|
aiRead: "defer",
|
|
},
|
|
});
|
|
// No created date: filed under the day it was added, and said so.
|
|
expect(hl.received[1].fields.documentDate).toBe("2024-04-01");
|
|
expect(hl.received[1].fields.title).toBe("brief");
|
|
expect(hl.received[1].fields.kind).toBeUndefined();
|
|
expect(first.stdout).toMatch(/Imported: 2/);
|
|
expect(first.stdout).toMatch(/filed under the day it was added/);
|
|
expect(first.stdout).toMatch(/without automatic AI reading/);
|
|
|
|
// The API contract the script leans on.
|
|
const docs = pl.seen.filter((s) => s.path.startsWith("/api/documents/?"));
|
|
expect(docs[0].path).toContain("tags__id__all=7");
|
|
// API v9: the lowest the current Paperless-ngx releases accept.
|
|
expect(docs[0].accept).toBe("application/json; version=9");
|
|
expect(docs[0].auth).toBe("Token pl_test");
|
|
// `next` was followed on the source's own origin.
|
|
expect(docs[1].path).toContain("page=2");
|
|
expect(
|
|
pl.seen.filter((s) => s.path.includes("/download/")).map((s) => s.path),
|
|
).toEqual([
|
|
"/api/documents/1/download/?original=true",
|
|
"/api/documents/2/download/?original=true",
|
|
]);
|
|
|
|
// A second run changes nothing: HealthLog already holds both, and the
|
|
// lookup says so before anything is downloaded or uploaded again.
|
|
const downloadsBefore = pl.seen.filter((s) =>
|
|
s.path.includes("/download/"),
|
|
).length;
|
|
const second = await runScript(args, env);
|
|
expect(second.code).toBe(0);
|
|
expect(second.stdout).toMatch(/Imported: 0/);
|
|
expect(second.stdout).toMatch(/Already in HealthLog: 2/);
|
|
expect(pl.seen.filter((s) => s.path.includes("/download/")).length).toBe(
|
|
downloadsBefore,
|
|
);
|
|
expect(hl.received).toHaveLength(2);
|
|
expect(hl.lookups).toContain("PAPERLESS:1");
|
|
});
|
|
|
|
it("leaves a document deleted in HealthLog alone", async () => {
|
|
const hl = await healthlog({ deleted: ["PAPERLESS:2"] });
|
|
const pl = await paperless();
|
|
const res = await runScript(
|
|
[
|
|
"paperless",
|
|
"--paperless-url",
|
|
pl.url,
|
|
"--healthlog-url",
|
|
hl.url,
|
|
"--tag",
|
|
"healthlog",
|
|
],
|
|
{ HEALTHLOG_TOKEN: "hlk_test", PAPERLESS_TOKEN: "pl_test" },
|
|
);
|
|
expect(res.code).toBe(0);
|
|
expect(res.stdout).toMatch(/Imported: 1/);
|
|
expect(res.stdout).toMatch(/Deleted in HealthLog, left alone: 1/);
|
|
});
|
|
|
|
it("stops with a plain instruction when the vault is switched off", async () => {
|
|
const hl = await healthlog({ moduleOff: true });
|
|
const pl = await paperless();
|
|
const res = await runScript(
|
|
["paperless", "--paperless-url", pl.url, "--healthlog-url", hl.url],
|
|
{ HEALTHLOG_TOKEN: "hlk_test", PAPERLESS_TOKEN: "pl_test" },
|
|
);
|
|
expect(res.code).toBe(1);
|
|
expect(res.stderr).toMatch(/Turn on Documents in Settings/);
|
|
});
|
|
|
|
it("lists sizes on a dry run and sends nothing", async () => {
|
|
const pl = await paperless();
|
|
const res = await runScript(
|
|
[
|
|
"paperless",
|
|
"--paperless-url",
|
|
pl.url,
|
|
"--tag",
|
|
"HealthLog",
|
|
"--dry-run",
|
|
],
|
|
{ PAPERLESS_TOKEN: "pl_test" },
|
|
);
|
|
expect(res.code).toBe(0);
|
|
expect(res.stdout).toMatch(
|
|
/2 document\(s\) would be imported, about 2\.0 MB/,
|
|
);
|
|
expect(pl.seen.some((s) => s.path.includes("/download/"))).toBe(false);
|
|
});
|
|
|
|
it("refuses to start without a HealthLog token", async () => {
|
|
const res = await runScript(
|
|
[
|
|
"paperless",
|
|
"--paperless-url",
|
|
"http://x",
|
|
"--healthlog-url",
|
|
"http://y",
|
|
],
|
|
{ PAPERLESS_TOKEN: "pl_test" },
|
|
);
|
|
expect(res.code).toBe(2);
|
|
expect(res.stderr).toMatch(/HEALTHLOG_TOKEN is not set/);
|
|
});
|
|
|
|
it("names a missing tag instead of importing everything", async () => {
|
|
const pl = await paperless();
|
|
const res = await runScript(
|
|
["paperless", "--paperless-url", pl.url, "--tag", "Nope", "--dry-run"],
|
|
{ PAPERLESS_TOKEN: "pl_test" },
|
|
);
|
|
expect(res.code).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("import-documents.mjs — edges", () => {
|
|
const env = { HEALTHLOG_TOKEN: "hlk_test", PAPERLESS_TOKEN: "pl_test" };
|
|
|
|
it("still imports against a HealthLog without the lookup", async () => {
|
|
const hl = await healthlog({ noLookup: true });
|
|
const pl = await paperless();
|
|
const res = await runScript(
|
|
[
|
|
"paperless",
|
|
"--paperless-url",
|
|
pl.url,
|
|
"--healthlog-url",
|
|
hl.url,
|
|
"--tag",
|
|
"HealthLog",
|
|
],
|
|
env,
|
|
);
|
|
expect(res.code).toBe(0);
|
|
expect(res.stdout).toMatch(/Imported: 2/);
|
|
});
|
|
|
|
it("does not wait on spent lookups: the keyed upload answers instead", async () => {
|
|
const hl = await healthlog({ lookup429: true });
|
|
const pl = await paperless();
|
|
const started = Date.now();
|
|
const res = await runScript(
|
|
[
|
|
"paperless",
|
|
"--paperless-url",
|
|
pl.url,
|
|
"--healthlog-url",
|
|
hl.url,
|
|
"--tag",
|
|
"HealthLog",
|
|
],
|
|
env,
|
|
);
|
|
expect(res.code).toBe(0);
|
|
expect(res.stdout).toMatch(/Imported: 2/);
|
|
// A Retry-After of an hour was not honoured for a lookup.
|
|
expect(Date.now() - started).toBeLessThan(20_000);
|
|
});
|
|
|
|
it("stops on a redirecting HealthLog address and names where it points", async () => {
|
|
const hl = await healthlog({ redirect: "https://health.example" });
|
|
const pl = await paperless();
|
|
const res = await runScript(
|
|
[
|
|
"paperless",
|
|
"--paperless-url",
|
|
pl.url,
|
|
"--healthlog-url",
|
|
hl.url,
|
|
"--tag",
|
|
"HealthLog",
|
|
],
|
|
env,
|
|
);
|
|
expect(res.code).toBe(1);
|
|
expect(res.stderr).toMatch(/redirects to https:\/\/health\.example/);
|
|
});
|
|
|
|
it("applies --since itself when the server ignores the filter", async () => {
|
|
const hl = await healthlog();
|
|
const pl = await paperless();
|
|
const res = await runScript(
|
|
[
|
|
"paperless",
|
|
"--paperless-url",
|
|
pl.url,
|
|
"--healthlog-url",
|
|
hl.url,
|
|
"--tag",
|
|
"HealthLog",
|
|
"--since",
|
|
"2024-04-01",
|
|
],
|
|
env,
|
|
);
|
|
expect(res.code).toBe(0);
|
|
// Document 1 (created 2024-03-05) is before the cut and never fetched.
|
|
expect(hl.received.map((r) => r.fields.sourceId)).toEqual(["2"]);
|
|
expect(pl.seen.some((s) => s.path.startsWith("/api/documents/1/"))).toBe(
|
|
false,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("import-documents.mjs — Papra", () => {
|
|
it("imports by tag since a date, and skips what HealthLog refuses as too large", async () => {
|
|
const hl = await healthlog({ tooLarge: ["PAPRA:doc_b"] });
|
|
const pp = await papra();
|
|
const res = await runScript(
|
|
[
|
|
"papra",
|
|
"--papra-url",
|
|
pp.url,
|
|
"--papra-org",
|
|
"org_1",
|
|
"--healthlog-url",
|
|
hl.url,
|
|
"--tag",
|
|
"Health",
|
|
"--since",
|
|
"2016-01-01",
|
|
"--kind-map",
|
|
"Lab=LAB_RESULT",
|
|
"--ai-read",
|
|
],
|
|
{ HEALTHLOG_TOKEN: "hlk_test", PAPRA_TOKEN: "pp_test" },
|
|
);
|
|
|
|
// Exit 3: finished, with at least one document skipped.
|
|
expect(res.code).toBe(3);
|
|
const sent = hl.received.map((r) => r.fields);
|
|
expect(sent).toHaveLength(2);
|
|
expect(sent[0]).toMatchObject({
|
|
title: "Impfpass",
|
|
documentDate: "2019-06-01",
|
|
sourceSystem: "PAPRA",
|
|
sourceId: "doc_a",
|
|
sourceInstance: new URL(pp.url).origin,
|
|
});
|
|
// --ai-read: no deferral field at all.
|
|
expect(sent[0].aiRead).toBeUndefined();
|
|
expect(sent[1]).toMatchObject({ sourceId: "doc_b", kind: "LAB_RESULT" });
|
|
// doc_old is before --since and never downloaded.
|
|
expect(pp.seen.some((s) => s.path.includes("doc_old"))).toBe(false);
|
|
expect(pp.seen.find((s) => s.path.includes("/documents?"))?.path).toContain(
|
|
"searchQuery=tag%3AHealth",
|
|
);
|
|
expect(pp.seen[0].auth).toBe("Bearer pp_test");
|
|
expect(res.stdout).toMatch(/Imported: 1/);
|
|
// doc_b answered 413 with the cap; doc_huge is known to be larger than
|
|
// that and is skipped without being downloaded or sent.
|
|
expect(res.stdout).toMatch(/Skipped: 2/);
|
|
expect(res.stdout).toMatch(/larger than HealthLog accepts/);
|
|
expect(pp.seen.some((s) => s.path.includes("doc_huge"))).toBe(false);
|
|
});
|
|
|
|
it("shows small sizes in KB on a dry run", async () => {
|
|
const pp = await papra();
|
|
const res = await runScript(
|
|
[
|
|
"papra",
|
|
"--papra-url",
|
|
pp.url,
|
|
"--papra-org",
|
|
"org_1",
|
|
"--tag",
|
|
"Health",
|
|
"--since",
|
|
"2016-01-01",
|
|
"--dry-run",
|
|
],
|
|
{ PAPRA_TOKEN: "pp_test" },
|
|
);
|
|
expect(res.code).toBe(0);
|
|
expect(res.stdout).toMatch(/2\.0 KB\s+Papra doc_a/);
|
|
});
|
|
});
|