Files
HealthLog/src/lib/validations/__tests__/medication-drug-codes.test.ts
T
Marc-André Bombeck f0f2caab79 fix(medications): bound the RxNorm code length
The RxCUI field validates against `^\d+$`, which is unbounded — a caller
could submit an arbitrarily long digit string. Add a `.max(20)` ceiling
as defence-in-depth so the column write stays tight; real RxCUIs are at
most a handful of digits. `atcCode` needs no max — its regex already
fixes the length. Regenerate the OpenAPI contract for the new
`maxLength` on both the create and update bodies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8b97d35bc375ef3b2e70f938c0d6729a5fa6bccb)
2026-06-02 17:34:52 +02:00

123 lines
3.7 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
createMedicationSchema,
updateMedicationSchema,
} from "@/lib/validations/medication";
/**
* v1.9.0 — validation coverage for the optional drug-classification
* codes (`atcCode` / `rxNormCode`) on the medication create + update
* bodies. ATC format `^[A-Z]\d{2}[A-Z]{2}\d{2}$`, RxCUI `^\d+$`.
*/
const baseCreate = {
name: "Mounjaro",
dose: "10mg",
schedules: [{ windowStart: "08:00", windowEnd: "09:00" }],
};
describe("medication drug-code validation", () => {
describe("createMedicationSchema", () => {
it("accepts a well-formed ATC + RxNorm code", () => {
const parsed = createMedicationSchema.safeParse({
...baseCreate,
atcCode: "A10BX10",
rxNormCode: "2601723",
});
expect(parsed.success).toBe(true);
if (parsed.success) {
expect(parsed.data.atcCode).toBe("A10BX10");
expect(parsed.data.rxNormCode).toBe("2601723");
}
});
it("accepts a medication with no codes (the pre-v1.9.0 shape)", () => {
const parsed = createMedicationSchema.safeParse(baseCreate);
expect(parsed.success).toBe(true);
if (parsed.success) {
expect(parsed.data.atcCode).toBeUndefined();
expect(parsed.data.rxNormCode).toBeUndefined();
}
});
it("rejects a malformed ATC code", () => {
// Too short (anatomical-group prefix, not a leaf substance class).
expect(
createMedicationSchema.safeParse({ ...baseCreate, atcCode: "A10B" })
.success,
).toBe(false);
// Lowercase.
expect(
createMedicationSchema.safeParse({ ...baseCreate, atcCode: "a10bx10" })
.success,
).toBe(false);
// Wrong digit/letter layout.
expect(
createMedicationSchema.safeParse({ ...baseCreate, atcCode: "AA0BX10" })
.success,
).toBe(false);
});
it("rejects a non-numeric RxNorm code", () => {
expect(
createMedicationSchema.safeParse({
...baseCreate,
rxNormCode: "rx-260",
}).success,
).toBe(false);
expect(
createMedicationSchema.safeParse({ ...baseCreate, rxNormCode: "" })
.success,
).toBe(false);
});
it("rejects an over-long RxNorm code (defence-in-depth length bound)", () => {
// The `^\d+$` regex alone is unbounded; the `.max(20)` ceiling
// rejects an absurd digit string while still admitting real RxCUIs.
expect(
createMedicationSchema.safeParse({
...baseCreate,
rxNormCode: "1".repeat(21),
}).success,
).toBe(false);
// 20 digits is the boundary and stays valid.
expect(
createMedicationSchema.safeParse({
...baseCreate,
rxNormCode: "1".repeat(20),
}).success,
).toBe(true);
});
});
describe("updateMedicationSchema", () => {
it("accepts null to clear a code", () => {
const parsed = updateMedicationSchema.safeParse({
atcCode: null,
rxNormCode: null,
});
expect(parsed.success).toBe(true);
if (parsed.success) {
expect(parsed.data.atcCode).toBeNull();
expect(parsed.data.rxNormCode).toBeNull();
}
});
it("accepts a well-formed code on update", () => {
const parsed = updateMedicationSchema.safeParse({ atcCode: "C09AA05" });
expect(parsed.success).toBe(true);
if (parsed.success) expect(parsed.data.atcCode).toBe("C09AA05");
});
it("rejects a malformed code on update", () => {
expect(
updateMedicationSchema.safeParse({ atcCode: "not-a-code" }).success,
).toBe(false);
expect(
updateMedicationSchema.safeParse({ rxNormCode: "12.3" }).success,
).toBe(false);
});
});
});