All checks were successful
Deploy / deploy (push) Successful in 2m29s
The period_logs collection was returning 403 errors because API rules were only configured in the e2e test harness, not in the production setup script. This consolidates the setup logic so both prod and test use the same setupApiRules() function. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
232 lines
7.4 KiB
TypeScript
232 lines
7.4 KiB
TypeScript
// 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",
|
|
});
|
|
});
|
|
});
|