mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
Closes the v3 audit "Locale Mixing" CRIT plus three of the cousin
findings the v1.4 discovery audit added:
1. **`getPhaseMessage` and `getPhaseKeyboard` were German-only.** Every
medication reminder shipped via Telegram, ntfy, and Web Push read
"Erinnerung", "Bald fällig", etc. regardless of the recipient's
locale. This is the user-facing surface most affected by the v3
"Locale Mixing" finding because the strings hit users on a 15-minute
cron, not just an admin / settings page they might never open. The
reminder worker now passes `med.user.locale` through; messages and
button labels resolve from `messages/{de,en}.json` under
`medicationReminders.*`. `callback_data` keys stay stable English
identifiers so the bot dispatcher keeps matching them across locale
changes.
2. **`/api/dashboard/summary` ships a hard-coded "Tage in Folge" label
and "Hi, X" salutation.** Both now come from
`dashboard.streakLabel` / `dashboard.greetingSalutation` resolved
server-side against the user's locale.
3. **Mixed-locale Zod messages.** `validations/measurement.ts:69` had
"Wert muss zwischen … liegen" while line 86 had "Value out of
plausible range" — the response envelope leaked German into English
sessions and vice versa. `validations/admin.ts` had four German
messages (web-push subject, Umami URL, Glitchtip DSN, GitHub
bug-report repo). All now resolve to a single English wording (the
app is English-first; the German UI translates the field labels
client-side, the server-side validation message is treated as a
developer-facing string).
Adds 13 new tests in `reminder-phases.test.ts` covering English,
German, and unknown-locale fallbacks for messages and keyboards;
updates `validations/measurement.test.ts` to match the unified English
message. The audit's remaining items
(`classifications.ts` server-returned EN category strings,
`pulse-targets.ts`, `targets/route.ts` labels,
`/api/admin/notifications/reminder-check`) ride a follow-up i18n PR —
those are admin-only or client-renderable surfaces with lower
user-facing priority.
Tests: 358 → 371 (+13). Typecheck clean. Locale-integrity test still
green.
Co-authored-by: Marc-André Bombeck <noreply@anthropic.com>
125 lines
4.0 KiB
TypeScript
125 lines
4.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
createMeasurementSchema,
|
|
getUnitForType,
|
|
validateMeasurementRange,
|
|
} from "../measurement";
|
|
|
|
describe("measurement validation", () => {
|
|
describe("getUnitForType", () => {
|
|
it("returns canonical unit for each measurement type", () => {
|
|
expect(getUnitForType("WEIGHT")).toBe("kg");
|
|
expect(getUnitForType("BLOOD_PRESSURE_SYS")).toBe("mmHg");
|
|
expect(getUnitForType("BLOOD_PRESSURE_DIA")).toBe("mmHg");
|
|
expect(getUnitForType("PULSE")).toBe("bpm");
|
|
expect(getUnitForType("BODY_FAT")).toBe("%");
|
|
expect(getUnitForType("SLEEP_DURATION")).toBe("hours");
|
|
expect(getUnitForType("ACTIVITY_STEPS")).toBe("steps");
|
|
expect(getUnitForType("BLOOD_GLUCOSE")).toBe("mg/dL");
|
|
expect(getUnitForType("TOTAL_BODY_WATER")).toBe("kg");
|
|
expect(getUnitForType("BONE_MASS")).toBe("kg");
|
|
});
|
|
|
|
it("returns 'unknown' for unrecognised types", () => {
|
|
expect(getUnitForType("MADE_UP_TYPE")).toBe("unknown");
|
|
});
|
|
});
|
|
|
|
describe("validateMeasurementRange", () => {
|
|
it("rejects values below the plausible minimum", () => {
|
|
expect(validateMeasurementRange("WEIGHT", 0.5)).toMatch(/between/i);
|
|
expect(validateMeasurementRange("BONE_MASS", 0.1)).toMatch(/between/i);
|
|
expect(validateMeasurementRange("TOTAL_BODY_WATER", 1)).toMatch(
|
|
/between/i,
|
|
);
|
|
});
|
|
|
|
it("rejects values above the plausible maximum", () => {
|
|
expect(validateMeasurementRange("WEIGHT", 600)).toMatch(/between/i);
|
|
expect(validateMeasurementRange("BONE_MASS", 12)).toMatch(/between/i);
|
|
expect(validateMeasurementRange("TOTAL_BODY_WATER", 200)).toMatch(
|
|
/between/i,
|
|
);
|
|
});
|
|
|
|
it("accepts values inside the plausible range", () => {
|
|
expect(validateMeasurementRange("WEIGHT", 75)).toBeNull();
|
|
expect(validateMeasurementRange("BONE_MASS", 3.0)).toBeNull();
|
|
expect(validateMeasurementRange("TOTAL_BODY_WATER", 42)).toBeNull();
|
|
});
|
|
|
|
it("returns null for unknown types (no range = no opinion)", () => {
|
|
expect(validateMeasurementRange("MADE_UP_TYPE", 9999)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("createMeasurementSchema", () => {
|
|
const validBase = {
|
|
value: 75,
|
|
measuredAt: "2026-04-27T08:00:00.000Z",
|
|
};
|
|
|
|
it("accepts a TOTAL_BODY_WATER measurement without glucoseContext", () => {
|
|
const parsed = createMeasurementSchema.safeParse({
|
|
...validBase,
|
|
type: "TOTAL_BODY_WATER",
|
|
value: 42,
|
|
});
|
|
expect(parsed.success).toBe(true);
|
|
});
|
|
|
|
it("accepts a BONE_MASS measurement without glucoseContext", () => {
|
|
const parsed = createMeasurementSchema.safeParse({
|
|
...validBase,
|
|
type: "BONE_MASS",
|
|
value: 3.2,
|
|
});
|
|
expect(parsed.success).toBe(true);
|
|
});
|
|
|
|
it("rejects out-of-range values for body composition types", () => {
|
|
const tooMuchWater = createMeasurementSchema.safeParse({
|
|
...validBase,
|
|
type: "TOTAL_BODY_WATER",
|
|
value: 500,
|
|
});
|
|
expect(tooMuchWater.success).toBe(false);
|
|
|
|
const tooLittleBone = createMeasurementSchema.safeParse({
|
|
...validBase,
|
|
type: "BONE_MASS",
|
|
value: 0.1,
|
|
});
|
|
expect(tooLittleBone.success).toBe(false);
|
|
});
|
|
|
|
it("rejects glucoseContext on non-glucose measurements", () => {
|
|
const parsed = createMeasurementSchema.safeParse({
|
|
...validBase,
|
|
type: "TOTAL_BODY_WATER",
|
|
value: 42,
|
|
glucoseContext: "FASTING",
|
|
});
|
|
expect(parsed.success).toBe(false);
|
|
});
|
|
|
|
it("requires glucoseContext on BLOOD_GLUCOSE measurements", () => {
|
|
const parsed = createMeasurementSchema.safeParse({
|
|
...validBase,
|
|
type: "BLOOD_GLUCOSE",
|
|
value: 95,
|
|
});
|
|
expect(parsed.success).toBe(false);
|
|
});
|
|
|
|
it("rejects unrecognised measurement types", () => {
|
|
const parsed = createMeasurementSchema.safeParse({
|
|
...validBase,
|
|
type: "TBD_NEW_TYPE",
|
|
value: 1,
|
|
});
|
|
expect(parsed.success).toBe(false);
|
|
});
|
|
});
|
|
});
|