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;
|
patch?: never;
|
||||||
trace?: 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": {
|
"/api/audit/price-history": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|
@ -694,25 +574,6 @@ export interface components {
|
||||||
/** Target Dates */
|
/** Target Dates */
|
||||||
target_dates: string[];
|
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 */
|
||||||
HTTPValidationError: {
|
HTTPValidationError: {
|
||||||
/** Detail */
|
/** Detail */
|
||||||
|
|
@ -786,19 +647,6 @@ export interface components {
|
||||||
/** Total Pages */
|
/** Total Pages */
|
||||||
total_pages: number;
|
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] */
|
||||||
PaginatedResponse_InviteResponse_: {
|
PaginatedResponse_InviteResponse_: {
|
||||||
/** Records */
|
/** Records */
|
||||||
|
|
@ -812,25 +660,12 @@ export interface components {
|
||||||
/** Total Pages */
|
/** Total Pages */
|
||||||
total_pages: number;
|
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
|
* Permission
|
||||||
* @description All available permissions in the system.
|
* @description All available permissions in the system.
|
||||||
* @enum {string}
|
* @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
|
* PriceHistoryResponse
|
||||||
* @description Response model for a price history record.
|
* @description Response model for a price history record.
|
||||||
|
|
@ -885,31 +720,6 @@ export interface components {
|
||||||
/** Nostr Npub */
|
/** Nostr Npub */
|
||||||
nostr_npub?: string | null;
|
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
|
* RegisterWithInvite
|
||||||
* @description Request model for registration with invite.
|
* @description Request model for registration with invite.
|
||||||
|
|
@ -938,49 +748,6 @@ export interface components {
|
||||||
/** Slots */
|
/** Slots */
|
||||||
slots: components["schemas"]["TimeSlot"][];
|
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
|
* TimeSlot
|
||||||
* @description A single time slot (start and end time).
|
* @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: {
|
get_price_history_api_audit_price_history_get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
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
|
* Permission-based E2E tests
|
||||||
*
|
*
|
||||||
* These tests verify that:
|
* These tests verify that:
|
||||||
* 1. Regular users can only access Counter and Sum pages
|
* 1. Regular users can access booking and appointments pages
|
||||||
* 2. Admin users can only access the Audit page
|
* 2. Admin users can access admin pages (invites, availability, appointments)
|
||||||
* 3. Users are properly redirected based on their permissions
|
* 3. Users are properly redirected based on their permissions
|
||||||
* 4. API calls respect permission boundaries
|
* 4. API calls respect permission boundaries
|
||||||
*/
|
*/
|
||||||
|
|
@ -64,87 +64,44 @@ test.describe("Regular User Access", () => {
|
||||||
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
|
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("/");
|
await page.goto("/");
|
||||||
|
|
||||||
// Should stay on counter page
|
// Should be redirected to booking page
|
||||||
await expect(page).toHaveURL("/");
|
await expect(page).toHaveURL("/booking");
|
||||||
|
|
||||||
// Should see counter UI
|
|
||||||
await expect(page.getByText("Current Count")).toBeVisible();
|
|
||||||
await expect(page.getByRole("button", { name: /increment/i })).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("can access sum page", async ({ page }) => {
|
test("can access booking page", async ({ page }) => {
|
||||||
await page.goto("/sum");
|
await page.goto("/booking");
|
||||||
|
|
||||||
// Should stay on sum page
|
// Should stay on booking page
|
||||||
await expect(page).toHaveURL("/sum");
|
await expect(page).toHaveURL("/booking");
|
||||||
|
|
||||||
// Should see sum UI
|
// Should see booking UI
|
||||||
await expect(page.getByText("Sum Calculator")).toBeVisible();
|
await expect(page.getByText("Book an Appointment")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("cannot access audit page - redirected to counter", async ({ page }) => {
|
test("can access appointments page", async ({ page }) => {
|
||||||
await page.goto("/audit");
|
await page.goto("/appointments");
|
||||||
|
|
||||||
// Should be redirected to counter page (home)
|
// Should stay on appointments page
|
||||||
await expect(page).toHaveURL("/");
|
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 }) => {
|
test("navigation shows booking and appointments", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/appointments");
|
||||||
|
|
||||||
// Should see Counter and Sum in nav
|
// From appointments page, we can see the nav links
|
||||||
await expect(page.getByText("Counter")).toBeVisible();
|
// "Appointments" is the current page (shown as span, not link)
|
||||||
await expect(page.getByText("Sum")).toBeVisible();
|
// "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)
|
// Should NOT see admin links
|
||||||
const auditLinks = page.locator('a[href="/audit"]');
|
const availabilityLinks = page.locator('a[href="/admin/availability"]');
|
||||||
await expect(auditLinks).toHaveCount(0);
|
await expect(availabilityLinks).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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -154,53 +111,44 @@ test.describe("Admin User Access", () => {
|
||||||
await loginUser(page, ADMIN_USER.email, ADMIN_USER.password);
|
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("/");
|
await page.goto("/");
|
||||||
|
|
||||||
// Should be redirected to audit page
|
// Should be redirected to admin appointments page
|
||||||
await expect(page).toHaveURL("/audit");
|
await expect(page).toHaveURL("/admin/appointments");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("redirected from sum page to audit", async ({ page }) => {
|
test("can access admin appointments page", async ({ page }) => {
|
||||||
await page.goto("/sum");
|
await page.goto("/admin/appointments");
|
||||||
|
|
||||||
// Should be redirected to audit page
|
// Should stay on admin appointments page
|
||||||
await expect(page).toHaveURL("/audit");
|
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 }) => {
|
test("can access admin availability page", async ({ page }) => {
|
||||||
await page.goto("/audit");
|
await page.goto("/admin/availability");
|
||||||
|
|
||||||
// Should stay on audit page
|
// Should stay on availability page
|
||||||
await expect(page).toHaveURL("/audit");
|
await expect(page).toHaveURL("/admin/availability");
|
||||||
|
|
||||||
// Should see audit tables
|
// Should see availability UI (use heading for specificity)
|
||||||
await expect(page.getByText("Counter Activity")).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Availability" })).toBeVisible();
|
||||||
await expect(page.getByText("Sum Activity")).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("navigation only shows Audit", async ({ page }) => {
|
test("navigation shows admin links", async ({ page }) => {
|
||||||
await page.goto("/audit");
|
await page.goto("/admin/appointments");
|
||||||
|
|
||||||
// Should see Audit as current
|
// Should see admin nav items (use locator for nav links)
|
||||||
await expect(page.getByText("Audit")).toBeVisible();
|
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)
|
// Should NOT see regular user links
|
||||||
const counterLinks = page.locator('a[href="/"]');
|
const bookLinks = page.locator('a[href="/booking"]');
|
||||||
const sumLinks = page.locator('a[href="/sum"]');
|
await expect(bookLinks).toHaveCount(0);
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -209,24 +157,24 @@ test.describe("Unauthenticated Access", () => {
|
||||||
await clearAuth(page);
|
await clearAuth(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("counter page redirects to login", async ({ page }) => {
|
test("home page redirects to login", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await expect(page).toHaveURL("/login");
|
await expect(page).toHaveURL("/login");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("sum page redirects to login", async ({ page }) => {
|
test("booking page redirects to login", async ({ page }) => {
|
||||||
await page.goto("/sum");
|
await page.goto("/booking");
|
||||||
await expect(page).toHaveURL("/login");
|
await expect(page).toHaveURL("/login");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("audit page redirects to login", async ({ page }) => {
|
test("admin page redirects to login", async ({ page }) => {
|
||||||
await page.goto("/audit");
|
await page.goto("/admin/appointments");
|
||||||
await expect(page).toHaveURL("/login");
|
await expect(page).toHaveURL("/login");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Permission Boundary via API", () => {
|
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
|
// Login as regular user
|
||||||
await clearAuth(page);
|
await clearAuth(page);
|
||||||
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
|
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");
|
const authCookie = cookies.find((c) => c.name === "auth_token");
|
||||||
|
|
||||||
if (authCookie) {
|
if (authCookie) {
|
||||||
// Try to call audit API directly
|
// Try to call admin appointments API directly
|
||||||
const response = await request.get(`${API_URL}/api/audit/counter`, {
|
const response = await request.get(`${API_URL}/api/admin/appointments`, {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `auth_token=${authCookie.value}`,
|
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
|
// Login as admin
|
||||||
await clearAuth(page);
|
await clearAuth(page);
|
||||||
await loginUser(page, ADMIN_USER.email, ADMIN_USER.password);
|
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");
|
const authCookie = cookies.find((c) => c.name === "auth_token");
|
||||||
|
|
||||||
if (authCookie) {
|
if (authCookie) {
|
||||||
// Try to call counter API directly
|
// Try to call booking slots API directly (requires regular user permission)
|
||||||
const response = await request.get(`${API_URL}/api/counter`, {
|
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: {
|
headers: {
|
||||||
Cookie: `auth_token=${authCookie.value}`,
|
Cookie: `auth_token=${authCookie.value}`,
|
||||||
},
|
},
|
||||||
|
|
@ -274,14 +226,14 @@ test.describe("Session and Logout", () => {
|
||||||
// Login
|
// Login
|
||||||
await clearAuth(page);
|
await clearAuth(page);
|
||||||
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
|
await loginUser(page, REGULAR_USER.email, REGULAR_USER.password);
|
||||||
await expect(page).toHaveURL("/");
|
await expect(page).toHaveURL("/booking");
|
||||||
|
|
||||||
// Logout
|
// Logout
|
||||||
await page.click("text=Sign out");
|
await page.click("text=Sign out");
|
||||||
await expect(page).toHaveURL("/login");
|
await expect(page).toHaveURL("/login");
|
||||||
|
|
||||||
// Try to access counter
|
// Try to access booking
|
||||||
await page.goto("/");
|
await page.goto("/booking");
|
||||||
await expect(page).toHaveURL("/login");
|
await expect(page).toHaveURL("/login");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -297,7 +249,7 @@ test.describe("Session and Logout", () => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Try to access protected page
|
// Try to access protected page
|
||||||
await page.goto("/");
|
await page.goto("/booking");
|
||||||
|
|
||||||
// Should be redirected to login
|
// Should be redirected to login
|
||||||
await expect(page).toHaveURL("/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("/");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -6,7 +6,6 @@ cd "$(dirname "$0")/.."
|
||||||
# Cleanup function to kill background processes
|
# Cleanup function to kill background processes
|
||||||
cleanup() {
|
cleanup() {
|
||||||
kill $BACKEND_PID 2>/dev/null || true
|
kill $BACKEND_PID 2>/dev/null || true
|
||||||
kill $WORKER_PID 2>/dev/null || true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ensure cleanup runs on exit (normal, error, or interrupt)
|
# Ensure cleanup runs on exit (normal, error, or interrupt)
|
||||||
|
|
@ -33,10 +32,6 @@ cd ..
|
||||||
cd backend
|
cd backend
|
||||||
uv run uvicorn main:app --port 8000 --log-level warning &
|
uv run uvicorn main:app --port 8000 --log-level warning &
|
||||||
BACKEND_PID=$!
|
BACKEND_PID=$!
|
||||||
|
|
||||||
# Start worker for job processing
|
|
||||||
uv run python worker.py &
|
|
||||||
WORKER_PID=$!
|
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
# Wait for backend
|
# Wait for backend
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue