Add Playwright fixtures with 5 test user types for e2e tests
Creates test infrastructure to enable previously skipped e2e tests: - Onboarding user (no period data) for setup flow tests - Established user (period 14 days ago) for normal usage tests - Calendar user (with calendarToken) for ICS feed tests - Garmin user (valid tokens) for connected state tests - Garmin expired user (expired tokens) for expiry warning tests Also fixes ICS feed route to strip .ics suffix from Next.js dynamic route param, adds calendarToken to /api/user response, and sets viewRule on users collection for unauthenticated ICS access. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
// ABOUTME: PocketBase test harness for e2e tests - starts, configures, and stops PocketBase.
|
||||
// ABOUTME: Provides ephemeral PocketBase instances with test data for Playwright tests.
|
||||
|
||||
import { type ChildProcess, execSync, spawn } from "node:child_process";
|
||||
import { createCipheriv, randomBytes } from "node:crypto";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
@@ -11,6 +13,45 @@ import {
|
||||
getMissingCollections,
|
||||
} from "../scripts/setup-db";
|
||||
|
||||
/**
|
||||
* Test user presets for different e2e test scenarios.
|
||||
*/
|
||||
export type TestUserPreset =
|
||||
| "onboarding"
|
||||
| "established"
|
||||
| "calendar"
|
||||
| "garmin"
|
||||
| "garminExpired";
|
||||
|
||||
/**
|
||||
* Configuration for each test user type.
|
||||
*/
|
||||
export const TEST_USERS: Record<
|
||||
TestUserPreset,
|
||||
{ email: string; password: string }
|
||||
> = {
|
||||
onboarding: {
|
||||
email: "e2e-onboarding@phaseflow.local",
|
||||
password: "e2e-onboarding-123",
|
||||
},
|
||||
established: {
|
||||
email: "e2e-test@phaseflow.local",
|
||||
password: "e2e-test-password-123",
|
||||
},
|
||||
calendar: {
|
||||
email: "e2e-calendar@phaseflow.local",
|
||||
password: "e2e-calendar-123",
|
||||
},
|
||||
garmin: {
|
||||
email: "e2e-garmin@phaseflow.local",
|
||||
password: "e2e-garmin-123",
|
||||
},
|
||||
garminExpired: {
|
||||
email: "e2e-garmin-expired@phaseflow.local",
|
||||
password: "e2e-garmin-expired-123",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration for the test harness.
|
||||
*/
|
||||
@@ -174,8 +215,10 @@ async function addUserFields(pb: PocketBase): Promise<void> {
|
||||
*/
|
||||
async function setupApiRules(pb: PocketBase): Promise<void> {
|
||||
// Allow users to update their own user record
|
||||
// viewRule allows reading user records by ID (needed for ICS calendar feed)
|
||||
const usersCollection = await pb.collections.getOne("users");
|
||||
await pb.collections.update(usersCollection.id, {
|
||||
viewRule: "", // Empty string = allow all authenticated & unauthenticated reads
|
||||
updateRule: "id = @request.auth.id",
|
||||
});
|
||||
|
||||
@@ -255,19 +298,54 @@ async function retryAsync<T>(
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the test user with period data.
|
||||
* Encrypts a string using AES-256-GCM (matches src/lib/encryption.ts format).
|
||||
* Uses the test encryption key from playwright.config.ts.
|
||||
*/
|
||||
async function createTestUser(
|
||||
pb: PocketBase,
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<string> {
|
||||
// Calculate date 14 days ago for mid-cycle test data
|
||||
function encryptForTest(plaintext: string): string {
|
||||
const key = Buffer.from(
|
||||
"e2e-test-encryption-key-32chars".padEnd(32, "0").slice(0, 32),
|
||||
);
|
||||
const iv = randomBytes(16);
|
||||
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
||||
|
||||
let encrypted = cipher.update(plaintext, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
|
||||
const authTag = cipher.getAuthTag();
|
||||
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the onboarding test user (no period data).
|
||||
*/
|
||||
async function createOnboardingUser(pb: PocketBase): Promise<string> {
|
||||
const { email, password } = TEST_USERS.onboarding;
|
||||
|
||||
const user = await retryAsync(() =>
|
||||
pb.collection("users").create({
|
||||
email,
|
||||
password,
|
||||
passwordConfirm: password,
|
||||
emailVisibility: true,
|
||||
verified: true,
|
||||
cycleLength: 28,
|
||||
notificationTime: "07:00",
|
||||
timezone: "UTC",
|
||||
}),
|
||||
);
|
||||
|
||||
return user.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the established test user with period data (default user).
|
||||
*/
|
||||
async function createEstablishedUser(pb: PocketBase): Promise<string> {
|
||||
const { email, password } = TEST_USERS.established;
|
||||
const fourteenDaysAgo = new Date();
|
||||
fourteenDaysAgo.setDate(fourteenDaysAgo.getDate() - 14);
|
||||
const lastPeriodDate = fourteenDaysAgo.toISOString().split("T")[0];
|
||||
|
||||
// Create the test user (with retry for transient errors)
|
||||
const user = await retryAsync(() =>
|
||||
pb.collection("users").create({
|
||||
email,
|
||||
@@ -282,7 +360,6 @@ async function createTestUser(
|
||||
}),
|
||||
);
|
||||
|
||||
// Create a period log entry (with retry for transient errors)
|
||||
await retryAsync(() =>
|
||||
pb.collection("period_logs").create({
|
||||
user: user.id,
|
||||
@@ -293,6 +370,165 @@ async function createTestUser(
|
||||
return user.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the calendar test user with period data and calendar token.
|
||||
*/
|
||||
async function createCalendarUser(pb: PocketBase): Promise<string> {
|
||||
const { email, password } = TEST_USERS.calendar;
|
||||
const fourteenDaysAgo = new Date();
|
||||
fourteenDaysAgo.setDate(fourteenDaysAgo.getDate() - 14);
|
||||
const lastPeriodDate = fourteenDaysAgo.toISOString().split("T")[0];
|
||||
|
||||
const user = await retryAsync(() =>
|
||||
pb.collection("users").create({
|
||||
email,
|
||||
password,
|
||||
passwordConfirm: password,
|
||||
emailVisibility: true,
|
||||
verified: true,
|
||||
lastPeriodDate,
|
||||
cycleLength: 28,
|
||||
notificationTime: "07:00",
|
||||
timezone: "UTC",
|
||||
calendarToken: "e2e-test-calendar-token-12345678",
|
||||
}),
|
||||
);
|
||||
|
||||
await retryAsync(() =>
|
||||
pb.collection("period_logs").create({
|
||||
user: user.id,
|
||||
startDate: lastPeriodDate,
|
||||
}),
|
||||
);
|
||||
|
||||
return user.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the Garmin test user with period data and valid Garmin tokens.
|
||||
*/
|
||||
async function createGarminUser(pb: PocketBase): Promise<string> {
|
||||
const { email, password } = TEST_USERS.garmin;
|
||||
const fourteenDaysAgo = new Date();
|
||||
fourteenDaysAgo.setDate(fourteenDaysAgo.getDate() - 14);
|
||||
const lastPeriodDate = fourteenDaysAgo.toISOString().split("T")[0];
|
||||
|
||||
// Token expires 90 days in the future
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setDate(expiresAt.getDate() + 90);
|
||||
|
||||
const oauth1Token = encryptForTest(
|
||||
JSON.stringify({
|
||||
oauth_token: "test-oauth1-token",
|
||||
oauth_token_secret: "test-oauth1-secret",
|
||||
}),
|
||||
);
|
||||
|
||||
const oauth2Token = encryptForTest(
|
||||
JSON.stringify({
|
||||
access_token: "test-access-token",
|
||||
refresh_token: "test-refresh-token",
|
||||
token_type: "Bearer",
|
||||
expires_in: 7776000,
|
||||
}),
|
||||
);
|
||||
|
||||
const user = await retryAsync(() =>
|
||||
pb.collection("users").create({
|
||||
email,
|
||||
password,
|
||||
passwordConfirm: password,
|
||||
emailVisibility: true,
|
||||
verified: true,
|
||||
lastPeriodDate,
|
||||
cycleLength: 28,
|
||||
notificationTime: "07:00",
|
||||
timezone: "UTC",
|
||||
garminConnected: true,
|
||||
garminOauth1Token: oauth1Token,
|
||||
garminOauth2Token: oauth2Token,
|
||||
garminTokenExpiresAt: expiresAt.toISOString(),
|
||||
}),
|
||||
);
|
||||
|
||||
await retryAsync(() =>
|
||||
pb.collection("period_logs").create({
|
||||
user: user.id,
|
||||
startDate: lastPeriodDate,
|
||||
}),
|
||||
);
|
||||
|
||||
return user.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the Garmin expired test user with period data and expired Garmin tokens.
|
||||
*/
|
||||
async function createGarminExpiredUser(pb: PocketBase): Promise<string> {
|
||||
const { email, password } = TEST_USERS.garminExpired;
|
||||
const fourteenDaysAgo = new Date();
|
||||
fourteenDaysAgo.setDate(fourteenDaysAgo.getDate() - 14);
|
||||
const lastPeriodDate = fourteenDaysAgo.toISOString().split("T")[0];
|
||||
|
||||
// Token expired 1 day ago
|
||||
const expiredAt = new Date();
|
||||
expiredAt.setDate(expiredAt.getDate() - 1);
|
||||
|
||||
const oauth1Token = encryptForTest(
|
||||
JSON.stringify({
|
||||
oauth_token: "test-expired-oauth1-token",
|
||||
oauth_token_secret: "test-expired-oauth1-secret",
|
||||
}),
|
||||
);
|
||||
|
||||
const oauth2Token = encryptForTest(
|
||||
JSON.stringify({
|
||||
access_token: "test-expired-access-token",
|
||||
refresh_token: "test-expired-refresh-token",
|
||||
token_type: "Bearer",
|
||||
expires_in: 7776000,
|
||||
}),
|
||||
);
|
||||
|
||||
const user = await retryAsync(() =>
|
||||
pb.collection("users").create({
|
||||
email,
|
||||
password,
|
||||
passwordConfirm: password,
|
||||
emailVisibility: true,
|
||||
verified: true,
|
||||
lastPeriodDate,
|
||||
cycleLength: 28,
|
||||
notificationTime: "07:00",
|
||||
timezone: "UTC",
|
||||
garminConnected: true,
|
||||
garminOauth1Token: oauth1Token,
|
||||
garminOauth2Token: oauth2Token,
|
||||
garminTokenExpiresAt: expiredAt.toISOString(),
|
||||
}),
|
||||
);
|
||||
|
||||
await retryAsync(() =>
|
||||
pb.collection("period_logs").create({
|
||||
user: user.id,
|
||||
startDate: lastPeriodDate,
|
||||
}),
|
||||
);
|
||||
|
||||
return user.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates all test users for e2e tests.
|
||||
*/
|
||||
async function createAllTestUsers(pb: PocketBase): Promise<void> {
|
||||
await createOnboardingUser(pb);
|
||||
await createEstablishedUser(pb);
|
||||
await createCalendarUser(pb);
|
||||
await createGarminUser(pb);
|
||||
await createGarminExpiredUser(pb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a fresh PocketBase instance for e2e testing.
|
||||
*/
|
||||
@@ -339,8 +575,8 @@ export async function start(
|
||||
// Set up collections
|
||||
await setupCollections(pb);
|
||||
|
||||
// Create test user with period data
|
||||
await createTestUser(pb, config.testUserEmail, config.testUserPassword);
|
||||
// Create all test users for different e2e scenarios
|
||||
await createAllTestUsers(pb);
|
||||
|
||||
currentState = {
|
||||
process: pbProcess,
|
||||
|
||||
Reference in New Issue
Block a user