Files
phaseflow/scripts/setup-db.test.ts
Petru Paler 6df145d916 Fix Garmin token storage and flaky e2e test
1. Increase garminOauth1Token and garminOauth2Token max length from
   5000 to 20000 characters to accommodate encrypted OAuth tokens.
   Add logic to update existing field constraints in addUserFields().

2. Fix flaky pocketbase-harness e2e test by adding retry logic with
   exponential backoff to createAdminUser() and createTestUser().
   Handles SQLite database lock during PocketBase startup migrations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 12:52:01 +00:00

184 lines
5.8 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);
});
});