diff --git a/src/test/data/utils/fetch-json.test.ts b/src/test/data/utils/fetch-json.test.ts new file mode 100644 index 00000000..ac8daec0 --- /dev/null +++ b/src/test/data/utils/fetch-json.test.ts @@ -0,0 +1,35 @@ +import * as fs from "fs/promises"; +import { describe, expect, it, vi } from "vitest"; +import { fetchJsonFromUrl } from "../../../data/utils"; + +vi.mock("fs/promises", () => ({ + readFile: vi.fn(), +})); + +describe("fetchJsonFromUrl", () => { + it("reads and parses JSON from a file path", async () => { + vi.mocked(fs.readFile).mockResolvedValueOnce('{"key":"value"}'); + const result = await fetchJsonFromUrl("./data/seed.json"); + expect(result).toEqual({ key: "value" }); + expect(fs.readFile).toHaveBeenCalledWith("./data/seed.json", "utf-8"); + }); + + it("fetches JSON from a URL", async () => { + const mockFetch = vi.fn().mockResolvedValueOnce({ + json: () => Promise.resolve({ remote: true }), + }); + vi.stubGlobal("fetch", mockFetch); + + const result = await fetchJsonFromUrl("https://example.com/data.json"); + expect(result).toEqual({ remote: true }); + expect(mockFetch).toHaveBeenCalledWith("https://example.com/data.json"); + + vi.unstubAllGlobals(); + }); + + it("treats absolute paths as file system paths", async () => { + vi.mocked(fs.readFile).mockResolvedValueOnce('{"absolute":true}'); + const result = await fetchJsonFromUrl("/etc/config.json"); + expect(result).toEqual({ absolute: true }); + }); +}); diff --git a/src/test/data/utils/get-rrule.test.ts b/src/test/data/utils/get-rrule.test.ts new file mode 100644 index 00000000..cf96fad5 --- /dev/null +++ b/src/test/data/utils/get-rrule.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { getRRULE } from "../../../data/utils"; + +describe("getRRULE", () => { + it("returns RRULE string for each valid weekday (case-insensitive)", () => { + expect(getRRULE("monday")).toBe("FREQ=WEEKLY;BYDAY=MO;"); + expect(getRRULE("Tuesday")).toBe("FREQ=WEEKLY;BYDAY=TU;"); + expect(getRRULE("WEDNESDAY")).toBe("FREQ=WEEKLY;BYDAY=WE;"); + expect(getRRULE("thursday")).toBe("FREQ=WEEKLY;BYDAY=TH;"); + expect(getRRULE("Friday")).toBe("FREQ=WEEKLY;BYDAY=FR;"); + expect(getRRULE("saturday")).toBe("FREQ=WEEKLY;BYDAY=SA;"); + expect(getRRULE("sunday")).toBe("FREQ=WEEKLY;BYDAY=SU;"); + }); + + it("returns null for invalid day names", () => { + expect(getRRULE("weekday")).toBeNull(); + expect(getRRULE("mon")).toBeNull(); + expect(getRRULE("")).toBeNull(); + expect(getRRULE("holiday")).toBeNull(); + }); + + it("returns null for non-string input", () => { + expect(getRRULE(null as any)).toBeNull(); + expect(getRRULE(undefined as any)).toBeNull(); + expect(getRRULE(1 as any)).toBeNull(); + }); + + it("returns null for strings with leading or trailing whitespace", () => { + expect(getRRULE(" monday")).toBeNull(); + expect(getRRULE("monday ")).toBeNull(); + }); +}); diff --git a/src/test/data/utils/get-start-end-dates.test.ts b/src/test/data/utils/get-start-end-dates.test.ts new file mode 100644 index 00000000..ede37a8b --- /dev/null +++ b/src/test/data/utils/get-start-end-dates.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import { getStartEnd, getStartEndDates } from "../../../data/utils"; + +describe("getStartEndDates", () => { + it("returns start and end Date objects with correct hours", () => { + const { start, end } = getStartEndDates(8, 11); + expect(start).toBeInstanceOf(Date); + expect(end).toBeInstanceOf(Date); + expect(start.getHours()).toBe(8); + expect(end.getHours()).toBe(11); + }); + + it("sets minutes, seconds, and ms to 0 on start", () => { + const { start } = getStartEndDates(10, 14); + expect(start.getMinutes()).toBe(0); + expect(start.getSeconds()).toBe(0); + expect(start.getMilliseconds()).toBe(0); + }); + + it("uses provided date for start, defaults to 2024-01-01 for end", () => { + const custom = new Date("2025-06-15"); + const { start, end } = getStartEndDates(9, 12, custom); + expect(start.getFullYear()).toBe(2025); + expect(start.getMonth()).toBe(5); // June = 5 + expect(end.getFullYear()).toBe(2024); + }); +}); + +describe("getStartEnd", () => { + it("maps known morning/noon/afternoon/evening labels to hour ranges", () => { + const morning = getStartEnd("morning"); + expect(morning!.start.getHours()).toBe(8); + expect(morning!.end.getHours()).toBe(11); + + const noon = getStartEnd("noon"); + expect(noon!.start.getHours()).toBe(11); + expect(noon!.end.getHours()).toBe(14); + + const afternoon = getStartEnd("afternoon"); + expect(afternoon!.start.getHours()).toBe(14); + expect(afternoon!.end.getHours()).toBe(17); + + const evening = getStartEnd("evening"); + expect(evening!.start.getHours()).toBe(17); + expect(evening!.end.getHours()).toBe(20); + }); + + it("maps time-range string formats", () => { + const r1 = getStartEnd("08-11"); + expect(r1!.start.getHours()).toBe(8); + expect(r1!.end.getHours()).toBe(11); + + // "8:00 - 10:00" is mapped to startHour:8, endHour:11 in the source map + const r2 = getStartEnd("8:00 - 10:00"); + expect(r2!.start.getHours()).toBe(8); + expect(r2!.end.getHours()).toBe(11); + }); + + it("returns null for unrecognized or unavailability strings", () => { + expect(getStartEnd("not")).toBeNull(); + expect(getStartEnd("available")).toBeNull(); + expect(getStartEnd("verfügbar")).toBeNull(); + expect(getStartEnd("unknown")).toBeNull(); + }); +}); diff --git a/src/test/data/utils/passwd.test.ts b/src/test/data/utils/passwd.test.ts new file mode 100644 index 00000000..7df649fe --- /dev/null +++ b/src/test/data/utils/passwd.test.ts @@ -0,0 +1,44 @@ +import * as bcrypt from "bcrypt"; +import { describe, expect, it, vi } from "vitest"; +import { hashPassword, verifyPassword } from "../../../data/utils"; + +vi.mock("bcrypt"); + +describe("hashPassword", () => { + it("uses 10 salt rounds and returns the resulting hash", async () => { + vi.mocked(bcrypt.genSalt).mockResolvedValue("fakesalt" as any); + vi.mocked(bcrypt.hash).mockResolvedValue("$2b$10$fakehash" as any); + + const result = await hashPassword("mypassword"); + + expect(bcrypt.genSalt).toHaveBeenCalledWith(10); + expect(bcrypt.hash).toHaveBeenCalledWith("mypassword", "fakesalt"); + expect(result).toBe("$2b$10$fakehash"); + }); + + it("wraps bcrypt errors in a generic message", async () => { + vi.mocked(bcrypt.genSalt).mockRejectedValue(new Error("bcrypt internal failure")); + await expect(hashPassword("password")).rejects.toThrow("Could not hash password."); + }); +}); + +describe("verifyPassword", () => { + it("returns true when bcrypt.compare resolves to true", async () => { + vi.mocked(bcrypt.compare).mockResolvedValue(true as any); + + const result = await verifyPassword("plain", "$2b$10$hash"); + + expect(bcrypt.compare).toHaveBeenCalledWith("plain", "$2b$10$hash"); + expect(result).toBe(true); + }); + + it("returns false when bcrypt.compare resolves to false", async () => { + vi.mocked(bcrypt.compare).mockResolvedValue(false as any); + expect(await verifyPassword("wrong", "$2b$10$hash")).toBe(false); + }); + + it("wraps bcrypt errors in a generic message", async () => { + vi.mocked(bcrypt.compare).mockRejectedValue(new Error("bcrypt internal failure")); + await expect(verifyPassword("plain", "hash")).rejects.toThrow("Could not verify password."); + }); +});