diff --git a/src/routes/api.test.ts b/src/routes/api.test.ts index 54e921f..f8bb6f9 100644 --- a/src/routes/api.test.ts +++ b/src/routes/api.test.ts @@ -104,6 +104,23 @@ describe("POST /api/mask", () => { expect(body.entities).toEqual([]); }); + test("preserves leading and trailing whitespace in masked text", async () => { + const text = " John Smith lives in Berlin\n\n"; + mockDetectPII.mockResolvedValueOnce([{ entity_type: "PERSON", start: 2, end: 12, score: 0.9 }]); + + const res = await app.request("/api/mask", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text }), + }); + + expect(res.status).toBe(200); + expect(mockDetectPII).toHaveBeenLastCalledWith(text); + const body = (await res.json()) as { masked: string; context: Record }; + expect(body.masked).toBe(" [[PERSON_1]] lives in Berlin\n\n"); + expect(body.context["[[PERSON_1]]"]).toBe("John Smith"); + }); + test("masks PII entities", async () => { mockDetectPII.mockResolvedValueOnce([ { entity_type: "EMAIL_ADDRESS", start: 6, end: 22, score: 0.9 }, diff --git a/src/routes/api.ts b/src/routes/api.ts index 854b8cb..99710cd 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -24,7 +24,8 @@ import { createLogData } from "./utils"; // Request schema const MaskRequestSchema = z.object({ - text: z.string().trim().min(1, "text is required"), + // Reject blank text without trimming it: clients align `masked` against the original input. + text: z.string().refine((text) => text.trim().length > 0, "text is required"), startFrom: z.record(z.string(), z.number()).optional(), detect: z.array(z.enum(["pii", "secrets"])).optional(), });