From c89e0312fa1aa0b188081f1850b04d42e97dd118 Mon Sep 17 00:00:00 2001 From: counterweight Date: Mon, 22 Dec 2025 18:13:24 +0100 Subject: [PATCH] Phase 0.3: Update E2E tests for cleanup - Delete counter.spec.ts and random-jobs.spec.ts - Rewrite permissions.spec.ts for new permission structure - Update scripts/e2e.sh: remove worker.py execution - Update generated api.ts types --- frontend/app/generated/api.ts | 396 +------------------------------ frontend/e2e/counter.spec.ts | 217 ----------------- frontend/e2e/permissions.spec.ts | 190 ++++++--------- frontend/e2e/random-jobs.spec.ts | 80 ------- scripts/e2e.sh | 5 - 5 files changed, 72 insertions(+), 816 deletions(-) delete mode 100644 frontend/e2e/counter.spec.ts delete mode 100644 frontend/e2e/random-jobs.spec.ts diff --git a/frontend/app/generated/api.ts b/frontend/app/generated/api.ts index f0e2a9c..63a83ed 100644 --- a/frontend/app/generated/api.ts +++ b/frontend/app/generated/api.ts @@ -84,126 +84,6 @@ export interface paths { patch?: never; trace?: never; }; - "/api/sum": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Calculate Sum - * @description Calculate the sum of two numbers and record it. - */ - post: operations["calculate_sum_api_sum_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/counter": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Counter - * @description Get the current counter value. - */ - get: operations["get_counter_api_counter_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/counter/increment": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Increment Counter - * @description Increment the counter, record the action, and enqueue a random number job. - */ - post: operations["increment_counter_api_counter_increment_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/audit/counter": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Counter Records - * @description Get paginated counter action records. - */ - get: operations["get_counter_records_api_audit_counter_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/audit/sum": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Sum Records - * @description Get paginated sum action records. - */ - get: operations["get_sum_records_api_audit_sum_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/audit/random-jobs": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** - * Get Random Job Outcomes - * @description Get all random number job outcomes, newest first. - */ - get: operations["get_random_job_outcomes_api_audit_random_jobs_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/api/audit/price-history": { parameters: { query?: never; @@ -694,25 +574,6 @@ export interface components { /** Target Dates */ target_dates: string[]; }; - /** - * CounterRecordResponse - * @description Response model for a counter audit record. - */ - CounterRecordResponse: { - /** Id */ - id: number; - /** User Email */ - user_email: string; - /** Value Before */ - value_before: number; - /** Value After */ - value_after: number; - /** - * Created At - * Format: date-time - */ - created_at: string; - }; /** HTTPValidationError */ HTTPValidationError: { /** Detail */ @@ -786,19 +647,6 @@ export interface components { /** Total Pages */ total_pages: number; }; - /** PaginatedResponse[CounterRecordResponse] */ - PaginatedResponse_CounterRecordResponse_: { - /** Records */ - records: components["schemas"]["CounterRecordResponse"][]; - /** Total */ - total: number; - /** Page */ - page: number; - /** Per Page */ - per_page: number; - /** Total Pages */ - total_pages: number; - }; /** PaginatedResponse[InviteResponse] */ PaginatedResponse_InviteResponse_: { /** Records */ @@ -812,25 +660,12 @@ export interface components { /** Total Pages */ total_pages: number; }; - /** PaginatedResponse[SumRecordResponse] */ - PaginatedResponse_SumRecordResponse_: { - /** Records */ - records: components["schemas"]["SumRecordResponse"][]; - /** Total */ - total: number; - /** Page */ - page: number; - /** Per Page */ - per_page: number; - /** Total Pages */ - total_pages: number; - }; /** * Permission * @description All available permissions in the system. * @enum {string} */ - Permission: "view_counter" | "increment_counter" | "use_sum" | "view_audit" | "fetch_price" | "manage_own_profile" | "manage_invites" | "view_own_invites" | "book_appointment" | "view_own_appointments" | "cancel_own_appointment" | "manage_availability" | "view_all_appointments" | "cancel_any_appointment"; + Permission: "view_audit" | "fetch_price" | "manage_own_profile" | "manage_invites" | "view_own_invites" | "book_appointment" | "view_own_appointments" | "cancel_own_appointment" | "manage_availability" | "view_all_appointments" | "cancel_any_appointment"; /** * PriceHistoryResponse * @description Response model for a price history record. @@ -885,31 +720,6 @@ export interface components { /** Nostr Npub */ nostr_npub?: string | null; }; - /** - * RandomNumberOutcomeResponse - * @description Response model for a random number job outcome. - */ - RandomNumberOutcomeResponse: { - /** Id */ - id: number; - /** Job Id */ - job_id: number; - /** Triggered By User Id */ - triggered_by_user_id: number; - /** Triggered By Email */ - triggered_by_email: string; - /** Value */ - value: number; - /** Duration Ms */ - duration_ms: number; - /** Status */ - status: string; - /** - * Created At - * Format: date-time - */ - created_at: string; - }; /** * RegisterWithInvite * @description Request model for registration with invite. @@ -938,49 +748,6 @@ export interface components { /** Slots */ slots: components["schemas"]["TimeSlot"][]; }; - /** - * SumRecordResponse - * @description Response model for a sum audit record. - */ - SumRecordResponse: { - /** Id */ - id: number; - /** User Email */ - user_email: string; - /** A */ - a: number; - /** B */ - b: number; - /** Result */ - result: number; - /** - * Created At - * Format: date-time - */ - created_at: string; - }; - /** - * SumRequest - * @description Request model for sum calculation. - */ - SumRequest: { - /** A */ - a: number; - /** B */ - b: number; - }; - /** - * SumResponse - * @description Response model for sum calculation. - */ - SumResponse: { - /** A */ - a: number; - /** B */ - b: number; - /** Result */ - result: number; - }; /** * TimeSlot * @description A single time slot (start and end time). @@ -1171,167 +938,6 @@ export interface operations { }; }; }; - calculate_sum_api_sum_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SumRequest"]; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["SumResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_counter_api_counter_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - [key: string]: number; - }; - }; - }; - }; - }; - increment_counter_api_counter_increment_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": { - [key: string]: number; - }; - }; - }; - }; - }; - get_counter_records_api_audit_counter_get: { - parameters: { - query?: { - page?: number; - per_page?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["PaginatedResponse_CounterRecordResponse_"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_sum_records_api_audit_sum_get: { - parameters: { - query?: { - page?: number; - per_page?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["PaginatedResponse_SumRecordResponse_"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_random_job_outcomes_api_audit_random_jobs_get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["RandomNumberOutcomeResponse"][]; - }; - }; - }; - }; get_price_history_api_audit_price_history_get: { parameters: { query?: never; diff --git a/frontend/e2e/counter.spec.ts b/frontend/e2e/counter.spec.ts deleted file mode 100644 index 35245c0..0000000 --- a/frontend/e2e/counter.spec.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { test, expect, Page, APIRequestContext } from "@playwright/test"; - -const API_BASE = "http://localhost:8000"; -const ADMIN_EMAIL = "admin@example.com"; -const ADMIN_PASSWORD = "admin123"; - -// Helper to generate unique email for each test -function uniqueEmail(): string { - return `counter-${Date.now()}-${Math.random().toString(36).substring(7)}@example.com`; -} - -// Helper to create an invite via API -async function createInvite(request: APIRequestContext): Promise { - const loginResp = await request.post(`${API_BASE}/api/auth/login`, { - data: { email: ADMIN_EMAIL, password: ADMIN_PASSWORD }, - }); - const cookies = loginResp.headers()["set-cookie"]; - - const meResp = await request.get(`${API_BASE}/api/auth/me`, { - headers: { Cookie: cookies }, - }); - const admin = await meResp.json(); - - const inviteResp = await request.post(`${API_BASE}/api/admin/invites`, { - data: { godfather_id: admin.id }, - headers: { Cookie: cookies }, - }); - const invite = await inviteResp.json(); - return invite.identifier; -} - -// Helper to authenticate a user with invite-based signup -async function authenticate(page: Page, request: APIRequestContext): Promise { - const email = uniqueEmail(); - const inviteCode = await createInvite(request); - - await page.context().clearCookies(); - await page.goto("/signup"); - - // Enter invite code first - await page.fill("input#inviteCode", inviteCode); - - // Click and wait for invite check API to complete - await Promise.all([ - page.waitForResponse((resp) => resp.url().includes("/check") && resp.status() === 200), - page.click('button[type="submit"]'), - ]); - - // Wait for registration form - await expect(page.locator("h1")).toHaveText("Create account"); - - // Fill registration - await page.fill("input#email", email); - await page.fill("input#password", "password123"); - await page.fill("input#confirmPassword", "password123"); - await page.click('button[type="submit"]'); - - await expect(page).toHaveURL("/"); - return email; -} - -test.describe("Counter - Authenticated", () => { - test("displays counter value", async ({ page, request }) => { - await authenticate(page, request); - await expect(page.locator("h1")).toBeVisible(); - // Counter should be a number (not loading state) - const text = await page.locator("h1").textContent(); - expect(text).toMatch(/^\d+$/); - }); - - test("displays current count label", async ({ page, request }) => { - await authenticate(page, request); - await expect(page.getByText("Current Count")).toBeVisible(); - }); - - test("clicking increment button increases counter", async ({ page, request }) => { - await authenticate(page, request); - await expect(page.locator("h1")).not.toHaveText("..."); - - const before = await page.locator("h1").textContent(); - await page.click("text=Increment"); - await expect(page.locator("h1")).toHaveText(String(Number(before) + 1)); - }); - - test("clicking increment multiple times", async ({ page, request }) => { - await authenticate(page, request); - await expect(page.locator("h1")).not.toHaveText("..."); - - const before = Number(await page.locator("h1").textContent()); - - // Click increment and wait for each update to complete - await page.click("text=Increment"); - await expect(page.locator("h1")).not.toHaveText(String(before)); - - const afterFirst = Number(await page.locator("h1").textContent()); - await page.click("text=Increment"); - await expect(page.locator("h1")).not.toHaveText(String(afterFirst)); - - const afterSecond = Number(await page.locator("h1").textContent()); - await page.click("text=Increment"); - await expect(page.locator("h1")).not.toHaveText(String(afterSecond)); - - // Final value should be at least 3 more than we started with - const final = Number(await page.locator("h1").textContent()); - expect(final).toBeGreaterThanOrEqual(before + 3); - }); - - test("counter persists after page reload", async ({ page, request }) => { - await authenticate(page, request); - await expect(page.locator("h1")).not.toHaveText("..."); - - const before = await page.locator("h1").textContent(); - await page.click("text=Increment"); - const expected = String(Number(before) + 1); - await expect(page.locator("h1")).toHaveText(expected); - - await page.reload(); - await expect(page.locator("h1")).toHaveText(expected); - }); - - test("counter is shared between users", async ({ page, browser, request }) => { - // First user increments - await authenticate(page, request); - await expect(page.locator("h1")).not.toHaveText("..."); - - const initialValue = Number(await page.locator("h1").textContent()); - await page.click("text=Increment"); - await page.click("text=Increment"); - // Wait for the counter to update (value should increase by 2 from what this user started with) - await expect(page.locator("h1")).not.toHaveText(String(initialValue)); - const afterFirstUser = Number(await page.locator("h1").textContent()); - expect(afterFirstUser).toBeGreaterThan(initialValue); - - // Second user in new context sees the current value - const page2 = await browser.newPage(); - await authenticate(page2, request); - await expect(page2.locator("h1")).not.toHaveText("..."); - const page2InitialValue = Number(await page2.locator("h1").textContent()); - // The value should be at least what user 1 saw (might be higher due to parallel tests) - expect(page2InitialValue).toBeGreaterThanOrEqual(afterFirstUser); - - // Second user increments - await page2.click("text=Increment"); - // Wait for counter to update - use >= because parallel tests may also increment - await expect(page2.locator("h1")).not.toHaveText(String(page2InitialValue)); - const page2AfterIncrement = Number(await page2.locator("h1").textContent()); - expect(page2AfterIncrement).toBeGreaterThanOrEqual(page2InitialValue + 1); - - // First user reloads and sees the increment (value should be >= what page2 has) - await page.reload(); - await expect(page.locator("h1")).not.toHaveText("..."); - const page1Reloaded = Number(await page.locator("h1").textContent()); - expect(page1Reloaded).toBeGreaterThanOrEqual(page2InitialValue + 1); - - await page2.close(); - }); -}); - -test.describe("Counter - Unauthenticated", () => { - test("redirects to login when accessing counter without auth", async ({ page }) => { - await page.context().clearCookies(); - await page.goto("/"); - await expect(page).toHaveURL("/login"); - }); - - test("shows login form when redirected", async ({ page }) => { - await page.context().clearCookies(); - await page.goto("/"); - await expect(page.locator("h1")).toHaveText("Welcome back"); - }); -}); - -test.describe("Counter - Session Integration", () => { - test("can access counter after login", async ({ page, request }) => { - const email = uniqueEmail(); - const inviteCode = await createInvite(request); - - // Sign up with invite - await page.goto("/signup"); - await page.fill("input#inviteCode", inviteCode); - await page.click('button[type="submit"]'); - await expect(page.locator("h1")).toHaveText("Create account"); - - await page.fill("input#email", email); - await page.fill("input#password", "password123"); - await page.fill("input#confirmPassword", "password123"); - await page.click('button[type="submit"]'); - await expect(page).toHaveURL("/"); - - // Logout - await page.click("text=Sign out"); - await expect(page).toHaveURL("/login"); - - // Login again - await page.fill('input[type="email"]', email); - await page.fill('input[type="password"]', "password123"); - await page.click('button[type="submit"]'); - await expect(page).toHaveURL("/"); - - // Counter should be visible - wait for it to load (not showing "...") - await expect(page.locator("h1")).toBeVisible(); - await expect(page.locator("h1")).not.toHaveText("..."); - const text = await page.locator("h1").textContent(); - expect(text).toMatch(/^\d+$/); - }); - - test("counter API requires authentication", async ({ page }) => { - // Try to access counter API directly without auth - const response = await page.request.get("http://localhost:8000/api/counter"); - expect(response.status()).toBe(401); - }); - - test("counter increment API requires authentication", async ({ page }) => { - const response = await page.request.post("http://localhost:8000/api/counter/increment"); - expect(response.status()).toBe(401); - }); -}); diff --git a/frontend/e2e/permissions.spec.ts b/frontend/e2e/permissions.spec.ts index cd77046..4efae98 100644 --- a/frontend/e2e/permissions.spec.ts +++ b/frontend/e2e/permissions.spec.ts @@ -4,8 +4,8 @@ import { test, expect, Page } from "@playwright/test"; * Permission-based E2E tests * * These tests verify that: - * 1. Regular users can only access Counter and Sum pages - * 2. Admin users can only access the Audit page + * 1. Regular users can access booking and appointments pages + * 2. Admin users can access admin pages (invites, availability, appointments) * 3. Users are properly redirected based on their permissions * 4. API calls respect permission boundaries */ @@ -64,87 +64,44 @@ test.describe("Regular User Access", () => { await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); }); - test("can access counter page", async ({ page }) => { + test("redirected from home to booking page", async ({ page }) => { await page.goto("/"); - // Should stay on counter page - await expect(page).toHaveURL("/"); - - // Should see counter UI - await expect(page.getByText("Current Count")).toBeVisible(); - await expect(page.getByRole("button", { name: /increment/i })).toBeVisible(); + // Should be redirected to booking page + await expect(page).toHaveURL("/booking"); }); - test("can access sum page", async ({ page }) => { - await page.goto("/sum"); + test("can access booking page", async ({ page }) => { + await page.goto("/booking"); - // Should stay on sum page - await expect(page).toHaveURL("/sum"); + // Should stay on booking page + await expect(page).toHaveURL("/booking"); - // Should see sum UI - await expect(page.getByText("Sum Calculator")).toBeVisible(); + // Should see booking UI + await expect(page.getByText("Book an Appointment")).toBeVisible(); }); - test("cannot access audit page - redirected to counter", async ({ page }) => { - await page.goto("/audit"); + test("can access appointments page", async ({ page }) => { + await page.goto("/appointments"); - // Should be redirected to counter page (home) - await expect(page).toHaveURL("/"); + // Should stay on appointments page + await expect(page).toHaveURL("/appointments"); + + // Should see appointments UI + await expect(page.getByText("My Appointments")).toBeVisible(); }); - test("navigation only shows Counter and Sum", async ({ page }) => { - await page.goto("/"); + test("navigation shows booking and appointments", async ({ page }) => { + await page.goto("/appointments"); - // Should see Counter and Sum in nav - await expect(page.getByText("Counter")).toBeVisible(); - await expect(page.getByText("Sum")).toBeVisible(); + // From appointments page, we can see the nav links + // "Appointments" is the current page (shown as span, not link) + // "Book" should be a link - use first() since there may be other booking links on page + await expect(page.locator('a[href="/booking"]').first()).toBeVisible(); - // Should NOT see Audit in nav (for regular users) - const auditLinks = page.locator('a[href="/audit"]'); - await expect(auditLinks).toHaveCount(0); - }); - - test("can navigate between Counter and Sum", async ({ page }) => { - await page.goto("/"); - - // Go to Sum - await page.click('a[href="/sum"]'); - await expect(page).toHaveURL("/sum"); - - // Go back to Counter - await page.click('a[href="/"]'); - await expect(page).toHaveURL("/"); - }); - - test("can use counter functionality", async ({ page }) => { - await page.goto("/"); - - // Get initial count (might be any number) - const countElement = page.locator("h1").first(); - await expect(countElement).toBeVisible(); - - // Click increment - await page.click('button:has-text("Increment")'); - - // Wait for update - await page.waitForTimeout(500); - - // Counter should have updated (we just verify no error occurred) - await expect(countElement).toBeVisible(); - }); - - test("can use sum functionality", async ({ page }) => { - await page.goto("/sum"); - - // Fill in numbers - await page.fill('input[aria-label="First number"]', "5"); - await page.fill('input[aria-label="Second number"]', "3"); - - // Calculate - await page.click('button:has-text("Calculate")'); - - // Should show result - await expect(page.getByText("8")).toBeVisible(); + // Should NOT see admin links + const availabilityLinks = page.locator('a[href="/admin/availability"]'); + await expect(availabilityLinks).toHaveCount(0); }); }); @@ -154,53 +111,44 @@ test.describe("Admin User Access", () => { await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); }); - test("redirected from counter page to audit", async ({ page }) => { + test("redirected from home to admin appointments", async ({ page }) => { await page.goto("/"); - // Should be redirected to audit page - await expect(page).toHaveURL("/audit"); + // Should be redirected to admin appointments page + await expect(page).toHaveURL("/admin/appointments"); }); - test("redirected from sum page to audit", async ({ page }) => { - await page.goto("/sum"); + test("can access admin appointments page", async ({ page }) => { + await page.goto("/admin/appointments"); - // Should be redirected to audit page - await expect(page).toHaveURL("/audit"); + // Should stay on admin appointments page + await expect(page).toHaveURL("/admin/appointments"); + + // Should see appointments UI (use heading for specificity) + await expect(page.getByRole("heading", { name: "All Appointments" })).toBeVisible(); }); - test("can access audit page", async ({ page }) => { - await page.goto("/audit"); + test("can access admin availability page", async ({ page }) => { + await page.goto("/admin/availability"); - // Should stay on audit page - await expect(page).toHaveURL("/audit"); + // Should stay on availability page + await expect(page).toHaveURL("/admin/availability"); - // Should see audit tables - await expect(page.getByText("Counter Activity")).toBeVisible(); - await expect(page.getByText("Sum Activity")).toBeVisible(); + // Should see availability UI (use heading for specificity) + await expect(page.getByRole("heading", { name: "Availability" })).toBeVisible(); }); - test("navigation only shows Audit", async ({ page }) => { - await page.goto("/audit"); + test("navigation shows admin links", async ({ page }) => { + await page.goto("/admin/appointments"); - // Should see Audit as current - await expect(page.getByText("Audit")).toBeVisible(); + // Should see admin nav items (use locator for nav links) + await expect(page.locator('a[href="/admin/invites"]')).toBeVisible(); + await expect(page.locator('a[href="/admin/availability"]')).toBeVisible(); + await expect(page.locator('a[href="/admin/appointments"]')).toHaveCount(0); // Current page, shown as text not link - // Should NOT see Counter or Sum links (for admin users) - const counterLinks = page.locator('a[href="/"]'); - const sumLinks = page.locator('a[href="/sum"]'); - await expect(counterLinks).toHaveCount(0); - await expect(sumLinks).toHaveCount(0); - }); - - test("audit page shows records", async ({ page }) => { - await page.goto("/audit"); - - // Should see the tables - await expect(page.getByRole("table")).toHaveCount(2); - - // Should see column headers (use first() since there are two tables with same headers) - await expect(page.getByRole("columnheader", { name: "User" }).first()).toBeVisible(); - await expect(page.getByRole("columnheader", { name: "Date" }).first()).toBeVisible(); + // Should NOT see regular user links + const bookLinks = page.locator('a[href="/booking"]'); + await expect(bookLinks).toHaveCount(0); }); }); @@ -209,24 +157,24 @@ test.describe("Unauthenticated Access", () => { await clearAuth(page); }); - test("counter page redirects to login", async ({ page }) => { + test("home page redirects to login", async ({ page }) => { await page.goto("/"); await expect(page).toHaveURL("/login"); }); - test("sum page redirects to login", async ({ page }) => { - await page.goto("/sum"); + test("booking page redirects to login", async ({ page }) => { + await page.goto("/booking"); await expect(page).toHaveURL("/login"); }); - test("audit page redirects to login", async ({ page }) => { - await page.goto("/audit"); + test("admin page redirects to login", async ({ page }) => { + await page.goto("/admin/appointments"); await expect(page).toHaveURL("/login"); }); }); test.describe("Permission Boundary via API", () => { - test("regular user API call to audit returns 403", async ({ page, request }) => { + test("regular user API call to admin appointments returns 403", async ({ page, request }) => { // Login as regular user await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); @@ -236,8 +184,8 @@ test.describe("Permission Boundary via API", () => { const authCookie = cookies.find((c) => c.name === "auth_token"); if (authCookie) { - // Try to call audit API directly - const response = await request.get(`${API_URL}/api/audit/counter`, { + // Try to call admin appointments API directly + const response = await request.get(`${API_URL}/api/admin/appointments`, { headers: { Cookie: `auth_token=${authCookie.value}`, }, @@ -247,7 +195,7 @@ test.describe("Permission Boundary via API", () => { } }); - test("admin user API call to counter returns 403", async ({ page, request }) => { + test("admin user API call to booking slots returns 403", async ({ page, request }) => { // Login as admin await clearAuth(page); await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); @@ -257,8 +205,12 @@ test.describe("Permission Boundary via API", () => { const authCookie = cookies.find((c) => c.name === "auth_token"); if (authCookie) { - // Try to call counter API directly - const response = await request.get(`${API_URL}/api/counter`, { + // Try to call booking slots API directly (requires regular user permission) + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const dateStr = tomorrow.toISOString().split("T")[0]; + + const response = await request.get(`${API_URL}/api/booking/slots?date=${dateStr}`, { headers: { Cookie: `auth_token=${authCookie.value}`, }, @@ -274,14 +226,14 @@ test.describe("Session and Logout", () => { // Login await clearAuth(page); await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); - await expect(page).toHaveURL("/"); + await expect(page).toHaveURL("/booking"); // Logout await page.click("text=Sign out"); await expect(page).toHaveURL("/login"); - // Try to access counter - await page.goto("/"); + // Try to access booking + await page.goto("/booking"); await expect(page).toHaveURL("/login"); }); @@ -297,7 +249,7 @@ test.describe("Session and Logout", () => { ]); // Try to access protected page - await page.goto("/"); + await page.goto("/booking"); // Should be redirected to login await expect(page).toHaveURL("/login"); diff --git a/frontend/e2e/random-jobs.spec.ts b/frontend/e2e/random-jobs.spec.ts deleted file mode 100644 index 363f665..0000000 --- a/frontend/e2e/random-jobs.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { clearAuth, loginUser, REGULAR_USER, ADMIN_USER } from "./helpers/auth"; - -test.describe("Random Jobs - E2E", () => { - test("counter increment creates random job outcome visible to admin", async ({ page }) => { - // Step 1: Login as regular user - await clearAuth(page); - await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); - - // Wait for counter page to load - await expect(page.locator("h1")).toBeVisible(); - await expect(page.locator("h1")).not.toHaveText("..."); - - // Step 2: Click increment button - await page.click("text=Increment"); - - // Wait for the counter to update (value changes) - const counterValue = await page.locator("h1").textContent(); - expect(counterValue).toMatch(/^\d+$/); - - // Step 3: Logout - await page.click("text=Sign out"); - await expect(page).toHaveURL("/login"); - - // Step 4: Login as admin - await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); - - // Admin should be on audit page by default - await expect(page).toHaveURL("/audit"); - - // Step 5: Navigate to Random Jobs page - await page.click('a[href="/admin/random-jobs"]'); - await expect(page).toHaveURL("/admin/random-jobs"); - - // Step 6: Poll for job outcome to appear (worker may take time to process) - // Keep refreshing until we see the regular user's email in the table - await expect(async () => { - await page.reload(); - await expect(page.locator("table tbody")).toContainText(REGULAR_USER.email); - }).toPass({ timeout: 15000, intervals: [500, 1000, 2000] }); - - // Verify the outcome has expected fields - // Value should be a number 0-100 - const valueCell = page.locator("table tbody tr td").nth(3); - const valueText = await valueCell.textContent(); - const value = Number(valueText); - expect(value).toBeGreaterThanOrEqual(0); - expect(value).toBeLessThanOrEqual(100); - - // Status should be "completed" - await expect(page.locator("table tbody")).toContainText("completed"); - }); - - test("admin can view empty random jobs list", async ({ page }) => { - // This test just verifies the page loads correctly - // In a fresh DB there might be no outcomes yet - await clearAuth(page); - await loginUser(page, ADMIN_USER.email, ADMIN_USER.password); - - await page.goto("/admin/random-jobs"); - await expect(page).toHaveURL("/admin/random-jobs"); - - // Page title should be visible - await expect(page.locator("h2")).toContainText("Random Number Job Outcomes"); - - // Table should exist - await expect(page.locator("table")).toBeVisible(); - }); - - test("regular user cannot access random jobs page", async ({ page }) => { - await clearAuth(page); - await loginUser(page, REGULAR_USER.email, REGULAR_USER.password); - - // Try to navigate directly to the admin page - await page.goto("/admin/random-jobs"); - - // Should be redirected away (to "/" since fallbackRedirect is "/") - await expect(page).toHaveURL("/"); - }); -}); diff --git a/scripts/e2e.sh b/scripts/e2e.sh index df4375d..8635f52 100755 --- a/scripts/e2e.sh +++ b/scripts/e2e.sh @@ -6,7 +6,6 @@ cd "$(dirname "$0")/.." # Cleanup function to kill background processes cleanup() { kill $BACKEND_PID 2>/dev/null || true - kill $WORKER_PID 2>/dev/null || true } # Ensure cleanup runs on exit (normal, error, or interrupt) @@ -33,10 +32,6 @@ cd .. cd backend uv run uvicorn main:app --port 8000 --log-level warning & BACKEND_PID=$! - -# Start worker for job processing -uv run python worker.py & -WORKER_PID=$! cd .. # Wait for backend