// ABOUTME: Tests for the database setup script that creates PocketBase collections. // ABOUTME: Verifies collection definitions and setup logic without hitting real PocketBase. import { describe, expect, it, vi } from "vitest"; import { DAILY_LOGS_COLLECTION, getExistingCollectionNames, getMissingCollections, PERIOD_LOGS_COLLECTION, USER_CUSTOM_FIELDS, } from "./setup-db"; describe("PERIOD_LOGS_COLLECTION", () => { it("has correct name", () => { expect(PERIOD_LOGS_COLLECTION.name).toBe("period_logs"); }); it("has required fields", () => { const fieldNames = PERIOD_LOGS_COLLECTION.fields.map((f) => f.name); expect(fieldNames).toContain("user"); expect(fieldNames).toContain("startDate"); expect(fieldNames).toContain("predictedDate"); }); it("has user field as relation to users", () => { const userField = PERIOD_LOGS_COLLECTION.fields.find( (f) => f.name === "user", ); expect(userField?.type).toBe("relation"); expect(userField?.collectionId).toBe("users"); expect(userField?.maxSelect).toBe(1); expect(userField?.cascadeDelete).toBe(true); }); it("has startDate as required date", () => { const startDateField = PERIOD_LOGS_COLLECTION.fields.find( (f) => f.name === "startDate", ); expect(startDateField?.type).toBe("date"); expect(startDateField?.required).toBe(true); }); it("has predictedDate as optional date", () => { const predictedDateField = PERIOD_LOGS_COLLECTION.fields.find( (f) => f.name === "predictedDate", ); expect(predictedDateField?.type).toBe("date"); expect(predictedDateField?.required).toBe(false); }); }); describe("DAILY_LOGS_COLLECTION", () => { it("has correct name", () => { expect(DAILY_LOGS_COLLECTION.name).toBe("dailyLogs"); }); it("has all required fields", () => { const fieldNames = DAILY_LOGS_COLLECTION.fields.map((f) => f.name); // Core fields expect(fieldNames).toContain("user"); expect(fieldNames).toContain("date"); expect(fieldNames).toContain("cycleDay"); expect(fieldNames).toContain("phase"); // Garmin biometric fields expect(fieldNames).toContain("bodyBatteryCurrent"); expect(fieldNames).toContain("bodyBatteryYesterdayLow"); expect(fieldNames).toContain("hrvStatus"); expect(fieldNames).toContain("weekIntensityMinutes"); // Decision fields expect(fieldNames).toContain("phaseLimit"); expect(fieldNames).toContain("remainingMinutes"); expect(fieldNames).toContain("trainingDecision"); expect(fieldNames).toContain("decisionReason"); expect(fieldNames).toContain("notificationSentAt"); }); it("has user field as relation to users", () => { const userField = DAILY_LOGS_COLLECTION.fields.find( (f) => f.name === "user", ); expect(userField?.type).toBe("relation"); expect(userField?.collectionId).toBe("users"); expect(userField?.maxSelect).toBe(1); expect(userField?.cascadeDelete).toBe(true); }); it("has trainingDecision as required text", () => { const field = DAILY_LOGS_COLLECTION.fields.find( (f) => f.name === "trainingDecision", ); expect(field?.type).toBe("text"); expect(field?.required).toBe(true); }); }); describe("getExistingCollectionNames", () => { it("extracts collection names from PocketBase response", async () => { const mockPb = { collections: { getFullList: vi .fn() .mockResolvedValue([ { name: "users" }, { name: "period_logs" }, { name: "_superusers" }, ]), }, }; // biome-ignore lint/suspicious/noExplicitAny: test mock const names = await getExistingCollectionNames(mockPb as any); expect(names).toEqual(["users", "period_logs", "_superusers"]); }); it("returns empty array when no collections exist", async () => { const mockPb = { collections: { getFullList: vi.fn().mockResolvedValue([]), }, }; // biome-ignore lint/suspicious/noExplicitAny: test mock const names = await getExistingCollectionNames(mockPb as any); expect(names).toEqual([]); }); }); describe("getMissingCollections", () => { it("returns both collections when none exist", () => { const existing = ["users"]; const missing = getMissingCollections(existing); expect(missing).toHaveLength(2); expect(missing.map((c) => c.name)).toContain("period_logs"); expect(missing.map((c) => c.name)).toContain("dailyLogs"); }); it("returns only dailyLogs when period_logs exists", () => { const existing = ["users", "period_logs"]; const missing = getMissingCollections(existing); expect(missing).toHaveLength(1); expect(missing[0].name).toBe("dailyLogs"); }); it("returns only period_logs when dailyLogs exists", () => { const existing = ["users", "dailyLogs"]; const missing = getMissingCollections(existing); expect(missing).toHaveLength(1); expect(missing[0].name).toBe("period_logs"); }); it("returns empty array when all collections exist", () => { const existing = ["users", "period_logs", "dailyLogs"]; const missing = getMissingCollections(existing); expect(missing).toHaveLength(0); }); }); describe("USER_CUSTOM_FIELDS garmin token max lengths", () => { it("should have sufficient max length for garminOauth2Token field", () => { const oauth2Field = USER_CUSTOM_FIELDS.find( (f) => f.name === "garminOauth2Token", ); expect(oauth2Field).toBeDefined(); expect(oauth2Field?.max).toBeGreaterThanOrEqual(10000); }); it("should have sufficient max length for garminOauth1Token field", () => { const oauth1Field = USER_CUSTOM_FIELDS.find( (f) => f.name === "garminOauth1Token", ); expect(oauth1Field).toBeDefined(); expect(oauth1Field?.max).toBeGreaterThanOrEqual(10000); }); }); describe("setupApiRules", () => { it("configures user-owned record rules for period_logs and dailyLogs", async () => { const { setupApiRules } = await import("./setup-db"); const updateMock = vi.fn().mockResolvedValue({}); const mockPb = { collections: { getOne: vi.fn().mockImplementation((name: string) => { return Promise.resolve({ id: `${name}-id`, name }); }), update: updateMock, }, }; // biome-ignore lint/suspicious/noExplicitAny: test mock await setupApiRules(mockPb as any); // Should have called getOne for users, period_logs, and dailyLogs expect(mockPb.collections.getOne).toHaveBeenCalledWith("users"); expect(mockPb.collections.getOne).toHaveBeenCalledWith("period_logs"); expect(mockPb.collections.getOne).toHaveBeenCalledWith("dailyLogs"); // Check users collection rules expect(updateMock).toHaveBeenCalledWith("users-id", { viewRule: "", updateRule: "id = @request.auth.id", }); // Check period_logs collection rules expect(updateMock).toHaveBeenCalledWith("period_logs-id", { listRule: "user = @request.auth.id", viewRule: "user = @request.auth.id", createRule: "user = @request.auth.id", updateRule: "user = @request.auth.id", deleteRule: "user = @request.auth.id", }); // Check dailyLogs collection rules expect(updateMock).toHaveBeenCalledWith("dailyLogs-id", { listRule: "user = @request.auth.id", viewRule: "user = @request.auth.id", createRule: "user = @request.auth.id", updateRule: "user = @request.auth.id", deleteRule: "user = @request.auth.id", }); }); });