import { describe, expect, it, vi } from "vitest"; import { calculateUsageInRange, normalizeDateTime, parseIntakesWithUnits, parseRawIntakeUnits, } from "../services/medications-service.js"; import { escapeHtml, getDeliveryError, getPlannerUnit, isContainerPackage } from "../services/planner-service.js"; describe("medications-service decomposition regression", () => { it("preserves intake unit parsing from unified intakes_json", () => { const intakesJson = JSON.stringify([ { usage: 1, every: 1, start: "2026-01-01T08:00:00.000Z", intakeUnit: "ml" }, { usage: 2, every: 1, start: "2026-01-01T20:00:00.000Z", intakeUnit: "bogus" }, ]); expect(parseRawIntakeUnits(intakesJson)).toEqual(["ml", null]); const parsed = parseIntakesWithUnits( intakesJson, { usageJson: "[1,2]", everyJson: "[1,1]", startJson: '["2026-01-01T08:00:00.000Z","2026-01-01T20:00:00.000Z"]', }, false ); expect(parsed[0]?.intakeUnit).toBe("ml"); expect(parsed[1]?.intakeUnit).toBeNull(); }); it("normalizes date-time values and keeps invalid input null-safe", () => { expect(normalizeDateTime("2026-01-01T00:00:00.000Z")).toBe("2026-01-01T00:00:00.000Z"); expect(normalizeDateTime(1_767_225_600)).toBe("2026-01-01T00:00:00.000Z"); expect(normalizeDateTime("not-a-date")).toBeNull(); expect(normalizeDateTime(undefined)).toBeNull(); }); it("calculates range usage with split-safe helper behavior", () => { const usage = calculateUsageInRange( [ { usage: 1, every: 1, start: "2026-01-01T08:00:00.000Z", scheduleMode: "interval", weekdays: [] }, { usage: 0.5, every: 1, start: "2026-01-01T20:00:00.000Z", scheduleMode: "interval", weekdays: [] }, ], new Date("2026-01-01T00:00:00.000Z"), new Date("2026-01-02T00:00:00.000Z") ); expect(usage).toBe(1.5); }); }); describe("planner-service decomposition regression", () => { it("keeps HTML escaping and SMTP delivery error parsing stable", () => { expect(escapeHtml(``)).toBe("<script>alert('x')</script>"); expect(getDeliveryError({ accepted: ["ok@example.com"], rejected: [] })).toBeNull(); expect(getDeliveryError({ accepted: [], rejected: ["bad@example.com"] })).toContain("SMTP rejected all recipients"); expect(getDeliveryError({ accepted: [], rejected: [], response: "550 relay denied" })).toContain( "550 relay denied" ); }); it("maps package type to expected planner units after service extraction", () => { const tr = { common: { units: "units", ml: "ml", pills: "pills" } }; expect(isContainerPackage("bottle")).toBe(true); expect(isContainerPackage("blister")).toBe(false); expect(getPlannerUnit("tube", tr)).toBe("units"); expect(getPlannerUnit("liquid_container", tr)).toBe("ml"); expect(getPlannerUnit("bottle", tr)).toBe("pills"); expect(getPlannerUnit("blister", tr)).toBe("pills"); }); }); describe("settings-service decomposition regression", () => { it("keeps notification URL and classification helpers stable", async () => { vi.resetModules(); vi.doMock("../db/client.js", () => ({ db: {} })); vi.doMock("../db/schema.js", () => ({ userSettings: { userId: "userId" } })); const { classifyTestEmailFailure, getNotificationProvider, sanitizeNotificationUrl, validateNotificationHostname } = await import("../services/settings-service.js"); expect(classifyTestEmailFailure(new Error("SMTP rejected all recipients: person@example.com"))).toMatchObject({ status: 400, code: "EMAIL_RECIPIENT_REJECTED", }); expect(classifyTestEmailFailure(new Error("SMTP did not confirm accepted recipients."))).toMatchObject({ status: 502, code: "SMTP_DELIVERY_UNCONFIRMED", }); expect(getNotificationProvider("telegram://token@chat-id")).toBe("telegram"); expect(getNotificationProvider("https://hooks.slack.com/services/a/b/c")).toBe("hooks.slack.com"); expect(validateNotificationHostname("127.0.0.1")).toContain("not allowed"); expect(validateNotificationHostname("example.com")).toBeNull(); expect(sanitizeNotificationUrl("discord://abc@not-a-number")).toEqual({ error: "Invalid Discord webhook ID" }); expect(sanitizeNotificationUrl("ntfy://user:pass@ntfy.sh/topic")).toMatchObject({ url: "https://ntfy.sh/topic", isNtfy: true, auth: { user: "user", pass: "pass" }, }); }); });