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
This commit is contained in:
parent
a5c1eccb4b
commit
c89e0312fa
5 changed files with 72 additions and 816 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<string> {
|
||||
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<string> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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("/");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue