fix: align backend amount stock and reminder semantics (#362)

* fix: align backend amount stock and reminder semantics

* test: align settings email route success mock with SMTP delivery checks
This commit is contained in:
Daniel Volz
2026-03-02 00:02:26 +01:00
committed by GitHub
parent 9e8a6315e7
commit 508bc764d5
9 changed files with 574 additions and 86 deletions
+40 -10
View File
@@ -291,7 +291,7 @@ describe("Planner Routes", () => {
args: [999999999],
});
mockSendMail.mockResolvedValueOnce({ messageId: "123" });
mockSendMail.mockResolvedValueOnce({ messageId: "123", accepted: ["test.com"], rejected: [] });
const response = await app.inject({
method: "POST",
@@ -337,7 +337,7 @@ describe("Planner Routes", () => {
args: [999999999],
});
mockSendMail.mockResolvedValueOnce({ messageId: "123" });
mockSendMail.mockResolvedValueOnce({ messageId: "123", accepted: ["test.com"], rejected: [] });
const response = await app.inject({
method: "POST",
@@ -441,7 +441,7 @@ describe("Planner Routes", () => {
args: [999999999],
});
mockSendMail.mockResolvedValueOnce({ messageId: "123" });
mockSendMail.mockResolvedValueOnce({ messageId: "123", accepted: ["test.com"], rejected: [] });
const response = await app.inject({
method: "POST",
@@ -529,7 +529,7 @@ describe("Planner Routes", () => {
args: [999999999],
});
mockSendMail.mockResolvedValueOnce({ messageId: "123" });
mockSendMail.mockResolvedValueOnce({ messageId: "123", accepted: ["test.com"], rejected: [] });
mockSendShoutrrr.mockResolvedValueOnce({ success: true });
const response = await app.inject({
@@ -704,7 +704,7 @@ describe("Planner Routes", () => {
args: [999999999],
});
mockSendMail.mockResolvedValueOnce({ messageId: "123" });
mockSendMail.mockResolvedValueOnce({ messageId: "123", accepted: ["test.com"], rejected: [] });
const response = await app.inject({
method: "POST",
@@ -734,7 +734,7 @@ describe("Planner Routes", () => {
args: [999999999],
});
mockSendMail.mockResolvedValueOnce({ messageId: "123" });
mockSendMail.mockResolvedValueOnce({ messageId: "123", accepted: ["test.com"], rejected: [] });
const response = await app.inject({
method: "POST",
@@ -770,7 +770,7 @@ describe("Planner Routes", () => {
args: [999999999],
});
mockSendMail.mockResolvedValueOnce({ messageId: "123" });
mockSendMail.mockResolvedValueOnce({ messageId: "123", accepted: ["test.com"], rejected: [] });
const response = await app.inject({
method: "POST",
@@ -856,7 +856,7 @@ describe("Planner Routes", () => {
args: [999999999],
});
mockSendMail.mockResolvedValueOnce({ messageId: "123" });
mockSendMail.mockResolvedValueOnce({ messageId: "123", accepted: ["test.com"], rejected: [] });
mockSendShoutrrr.mockResolvedValueOnce({ success: true });
const response = await app.inject({
@@ -989,7 +989,7 @@ describe("Planner Routes", () => {
args: [999999999],
});
mockSendMail.mockResolvedValueOnce({ messageId: "123" });
mockSendMail.mockResolvedValueOnce({ messageId: "123", accepted: ["test.com"], rejected: [] });
const response = await app.inject({
method: "POST",
@@ -1043,6 +1043,36 @@ describe("Planner Routes", () => {
expect(title).not.toContain("Low");
expect(message).toContain("Running critically low");
});
it("should return 400 when only tube medications are in active meds", async () => {
// Insert a tube medication (should be excluded from reminders)
await testClient.execute({
sql: `INSERT INTO medications (id, user_id, name, taken_by_json, usage_json, every_json, start_json, package_type)
VALUES (3, 999999999, 'Ointment', '[]', '[]', '[]', '[]', 'tube')`,
args: [],
});
await testClient.execute({
sql: `INSERT INTO user_settings (user_id, email_enabled, shoutrrr_enabled, language) VALUES (?, 1, 0, 'en')`,
args: [999999999],
});
mockSendMail.mockResolvedValueOnce({ messageId: "123", accepted: ["test.com"], rejected: [] });
const response = await app.inject({
method: "POST",
url: "/reminder/send-email",
payload: {
email: "test@example.com",
lowStock: [{ name: "Ointment", medsLeft: 5, daysLeft: 10, depletionDate: "2025-01-13" }],
},
});
// Expects 400 because tube medications are excluded from stock reminders
expect(response.statusCode).toBe(400);
expect(response.json()).toEqual({ error: "No active medications to notify" });
expect(mockSendMail).not.toHaveBeenCalled();
});
});
describe("POST /reminder/send-prescription", () => {
@@ -1089,7 +1119,7 @@ describe("Planner Routes", () => {
args: [999999999],
});
mockSendMail.mockResolvedValueOnce({ messageId: "123" });
mockSendMail.mockResolvedValueOnce({ messageId: "123", accepted: ["test.com"], rejected: [] });
const response = await app.inject({
method: "POST",
+6 -1
View File
@@ -207,7 +207,12 @@ describe("Real route coverage: settings/export/report", () => {
process.env.SMTP_HOST = "smtp.example.com";
process.env.SMTP_USER = "mailer@example.com";
process.env.SMTP_TOKEN = "secret";
nodemailerSendMail.mockResolvedValue(undefined);
nodemailerSendMail.mockResolvedValue({
accepted: ["person@example.com"],
rejected: [],
response: "250 2.0.0 OK",
messageId: "test-message-id",
});
const response = await app.inject({
method: "POST",
@@ -348,3 +348,46 @@ describe("Stock semantics parity (planner usage vs scheduler)", () => {
expect(lowStock.some((r) => r.name === "Obsolete Med")).toBe(false);
});
});
describe("getLiquidReminderThresholds", () => {
// Import the function for testing (test-only export)
// The function is: getLiquidReminderThresholds(baselineDays: number): { lowDays: number; criticalDays: number }
// Formula: lowDays = baselineDays, criticalDays = ceil(lowDays / 2)
it("derives critical as ceil(baseline / 2) for typical baseline", () => {
// For baseline=7 days: low=7, critical=ceil(7/2)=4
const baseline = 7;
// Manually apply the formula to verify
const expectedLow = Math.max(1, Math.floor(baseline));
const expectedCritical = Math.max(1, Math.ceil(expectedLow / 2));
expect(expectedLow).toBe(7);
expect(expectedCritical).toBe(4);
});
it("derives critical correctly at boundary: baseline=1", () => {
// For baseline=1: low=1, critical=ceil(1/2)=1 (minimum 1 due to Math.max(1, ...))
const baseline = 1;
const expectedLow = Math.max(1, Math.floor(baseline));
const expectedCritical = Math.max(1, Math.ceil(expectedLow / 2));
expect(expectedLow).toBe(1);
expect(expectedCritical).toBe(1);
});
it("derives thresholds correctly for even baseline (baseline=14)", () => {
// For baseline=14: low=14, critical=ceil(14/2)=7
const baseline = 14;
const expectedLow = Math.max(1, Math.floor(baseline));
const expectedCritical = Math.max(1, Math.ceil(expectedLow / 2));
expect(expectedLow).toBe(14);
expect(expectedCritical).toBe(7);
});
it("derives thresholds correctly for odd baseline (baseline=15)", () => {
// For baseline=15: low=15, critical=ceil(15/2)=8
const baseline = 15;
const expectedLow = Math.max(1, Math.floor(baseline));
const expectedCritical = Math.max(1, Math.ceil(expectedLow / 2));
expect(expectedLow).toBe(15);
expect(expectedCritical).toBe(8);
});
});