first implementation
This commit is contained in:
parent
79458bcba4
commit
870804e7b9
24 changed files with 5485 additions and 184 deletions
162
frontend/e2e/admin-invites.spec.ts
Normal file
162
frontend/e2e/admin-invites.spec.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import { test, expect, Page } from "@playwright/test";
|
||||
|
||||
// Admin credentials from seed data
|
||||
const ADMIN_EMAIL = "admin@example.com";
|
||||
const ADMIN_PASSWORD = "admin123";
|
||||
|
||||
// Regular user from seed data
|
||||
const REGULAR_USER_EMAIL = "user@example.com";
|
||||
|
||||
async function loginAsAdmin(page: Page) {
|
||||
await page.goto("/login");
|
||||
await page.fill('input[type="email"]', ADMIN_EMAIL);
|
||||
await page.fill('input[type="password"]', ADMIN_PASSWORD);
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL("/audit");
|
||||
}
|
||||
|
||||
test.describe("Admin Invites Page", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.context().clearCookies();
|
||||
await loginAsAdmin(page);
|
||||
});
|
||||
|
||||
test("admin can access invites page", async ({ page }) => {
|
||||
await page.goto("/admin/invites");
|
||||
await expect(page.getByRole("heading", { name: "Create Invite" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "All Invites" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("godfather selection is a dropdown with users, not a number input", async ({ page }) => {
|
||||
await page.goto("/admin/invites");
|
||||
|
||||
// Wait for users to load
|
||||
await page.waitForSelector("select");
|
||||
|
||||
// The godfather selector should be a <select> element, not an <input type="number">
|
||||
const selectElement = page.locator("select").first();
|
||||
await expect(selectElement).toBeVisible();
|
||||
|
||||
// Verify it has user options (at least the seeded users)
|
||||
const options = selectElement.locator("option");
|
||||
const optionCount = await options.count();
|
||||
|
||||
// Should have at least 2 options: placeholder + at least one user
|
||||
expect(optionCount).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Verify the regular user appears as an option
|
||||
await expect(selectElement).toContainText(REGULAR_USER_EMAIL);
|
||||
|
||||
// There should NOT be a number input for godfather ID
|
||||
const numberInput = page.locator('input[type="number"]');
|
||||
await expect(numberInput).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("can create invite by selecting user from dropdown", async ({ page }) => {
|
||||
await page.goto("/admin/invites");
|
||||
|
||||
// Wait for page to load
|
||||
await page.waitForSelector("select");
|
||||
|
||||
// Select the regular user as godfather
|
||||
const godfatherSelect = page.locator("select").first();
|
||||
await godfatherSelect.selectOption({ label: REGULAR_USER_EMAIL });
|
||||
|
||||
// Click create invite
|
||||
await page.click('button:has-text("Create Invite")');
|
||||
|
||||
// Wait for the invite to appear in the table
|
||||
await expect(page.locator("table")).toContainText(REGULAR_USER_EMAIL);
|
||||
|
||||
// Verify an invite code appears (format: word-word-NN)
|
||||
const inviteCodeCell = page.locator("td").first();
|
||||
await expect(inviteCodeCell).toHaveText(/^[a-z]+-[a-z]+-\d{2}$/);
|
||||
});
|
||||
|
||||
test("create button is disabled when no user selected", async ({ page }) => {
|
||||
await page.goto("/admin/invites");
|
||||
|
||||
// Wait for page to load
|
||||
await page.waitForSelector("select");
|
||||
|
||||
// The create button should be disabled initially (no user selected)
|
||||
const createButton = page.locator('button:has-text("Create Invite")');
|
||||
await expect(createButton).toBeDisabled();
|
||||
|
||||
// Select a user
|
||||
const godfatherSelect = page.locator("select").first();
|
||||
await godfatherSelect.selectOption({ label: REGULAR_USER_EMAIL });
|
||||
|
||||
// Now the button should be enabled
|
||||
await expect(createButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test("can revoke a ready invite", async ({ page }) => {
|
||||
await page.goto("/admin/invites");
|
||||
await page.waitForSelector("select");
|
||||
|
||||
// Create an invite first
|
||||
const godfatherSelect = page.locator("select").first();
|
||||
await godfatherSelect.selectOption({ label: REGULAR_USER_EMAIL });
|
||||
await page.click('button:has-text("Create Invite")');
|
||||
|
||||
// Wait for the invite to appear
|
||||
await expect(page.locator("table")).toContainText("ready");
|
||||
|
||||
// Click revoke on the first ready invite
|
||||
const revokeButton = page.locator('button:has-text("Revoke")').first();
|
||||
await revokeButton.click();
|
||||
|
||||
// Verify the status changed to revoked
|
||||
await expect(page.locator("table")).toContainText("revoked");
|
||||
});
|
||||
|
||||
test("status filter works", async ({ page }) => {
|
||||
await page.goto("/admin/invites");
|
||||
await page.waitForSelector("select");
|
||||
|
||||
// Create an invite
|
||||
const godfatherSelect = page.locator("select").first();
|
||||
await godfatherSelect.selectOption({ label: REGULAR_USER_EMAIL });
|
||||
await page.click('button:has-text("Create Invite")');
|
||||
await expect(page.locator("table")).toContainText("ready");
|
||||
|
||||
// Filter by "revoked" status - should show no ready invites
|
||||
const statusFilter = page.locator("select").nth(1); // Second select is the status filter
|
||||
await statusFilter.selectOption("revoked");
|
||||
|
||||
// Wait for the filter to apply
|
||||
await page.waitForResponse((resp) => resp.url().includes("status=revoked"));
|
||||
|
||||
// Filter by "ready" status - should show our invite
|
||||
await statusFilter.selectOption("ready");
|
||||
await page.waitForResponse((resp) => resp.url().includes("status=ready"));
|
||||
await expect(page.locator("table")).toContainText("ready");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Admin Invites Access Control", () => {
|
||||
test("regular user cannot access admin invites page", async ({ page }) => {
|
||||
// Login as regular user
|
||||
await page.goto("/login");
|
||||
await page.fill('input[type="email"]', REGULAR_USER_EMAIL);
|
||||
await page.fill('input[type="password"]', "user123");
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL("/");
|
||||
|
||||
// Try to access admin invites page
|
||||
await page.goto("/admin/invites");
|
||||
|
||||
// Should be redirected away (to home page based on fallbackRedirect)
|
||||
await expect(page).not.toHaveURL("/admin/invites");
|
||||
});
|
||||
|
||||
test("unauthenticated user cannot access admin invites page", async ({ page }) => {
|
||||
await page.context().clearCookies();
|
||||
await page.goto("/admin/invites");
|
||||
|
||||
// Should be redirected to login
|
||||
await expect(page).toHaveURL("/login");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { test, expect, Page } from "@playwright/test";
|
||||
import { test, expect, Page, APIRequestContext } from "@playwright/test";
|
||||
|
||||
// Helper to generate unique email for each test
|
||||
function uniqueEmail(): string {
|
||||
|
|
@ -10,6 +10,35 @@ async function clearAuth(page: Page) {
|
|||
await page.context().clearCookies();
|
||||
}
|
||||
|
||||
// Admin credentials from seed data
|
||||
const ADMIN_EMAIL = "admin@example.com";
|
||||
const ADMIN_PASSWORD = "admin123";
|
||||
|
||||
// Helper to create an invite via the API
|
||||
const API_BASE = "http://localhost:8000";
|
||||
|
||||
async function createInvite(request: APIRequestContext): Promise<string> {
|
||||
// Login as admin
|
||||
const loginResp = await request.post(`${API_BASE}/api/auth/login`, {
|
||||
data: { email: ADMIN_EMAIL, password: ADMIN_PASSWORD },
|
||||
});
|
||||
const cookies = loginResp.headers()["set-cookie"];
|
||||
|
||||
// Get admin user ID (we'll use admin as godfather for simplicity)
|
||||
const meResp = await request.get(`${API_BASE}/api/auth/me`, {
|
||||
headers: { Cookie: cookies },
|
||||
});
|
||||
const admin = await meResp.json();
|
||||
|
||||
// Create invite
|
||||
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;
|
||||
}
|
||||
|
||||
test.describe("Authentication Flow", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await clearAuth(page);
|
||||
|
|
@ -29,13 +58,11 @@ test.describe("Authentication Flow", () => {
|
|||
await expect(page.locator('a[href="/signup"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test("signup page has correct form elements", async ({ page }) => {
|
||||
test("signup page has invite code form", async ({ page }) => {
|
||||
await page.goto("/signup");
|
||||
await expect(page.locator("h1")).toHaveText("Create account");
|
||||
await expect(page.locator('input[type="email"]')).toBeVisible();
|
||||
await expect(page.locator('input[type="password"]').first()).toBeVisible();
|
||||
await expect(page.locator('input[type="password"]').nth(1)).toBeVisible();
|
||||
await expect(page.locator('button[type="submit"]')).toHaveText("Create account");
|
||||
await expect(page.locator("h1")).toHaveText("Join with Invite");
|
||||
await expect(page.locator('input#inviteCode')).toBeVisible();
|
||||
await expect(page.locator('button[type="submit"]')).toHaveText("Continue");
|
||||
await expect(page.locator('a[href="/login"]')).toBeVisible();
|
||||
});
|
||||
|
||||
|
|
@ -52,18 +79,28 @@ test.describe("Authentication Flow", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test.describe("Signup", () => {
|
||||
test.describe("Signup with Invite", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await clearAuth(page);
|
||||
});
|
||||
|
||||
test("can create a new account", async ({ page }) => {
|
||||
test("can create a new account with valid invite", async ({ page, request }) => {
|
||||
const email = uniqueEmail();
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
await page.goto("/signup");
|
||||
await page.fill('input[type="email"]', email);
|
||||
await page.fill('input[type="password"]', "password123");
|
||||
await page.locator('input[type="password"]').nth(1).fill("password123");
|
||||
|
||||
// Step 1: Enter invite code
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Wait for form to transition to registration form
|
||||
await expect(page.locator("h1")).toHaveText("Create account");
|
||||
|
||||
// Step 2: Fill registration form
|
||||
await page.fill('input#email', email);
|
||||
await page.fill('input#password', "password123");
|
||||
await page.fill('input#confirmPassword', "password123");
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Should redirect to home after signup
|
||||
|
|
@ -72,76 +109,90 @@ test.describe("Signup", () => {
|
|||
await expect(page.getByText(email)).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows error for duplicate email", async ({ page }) => {
|
||||
test("signup with direct invite URL works", async ({ page, request }) => {
|
||||
const email = uniqueEmail();
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
// First registration
|
||||
await page.goto("/signup");
|
||||
await page.fill('input[type="email"]', email);
|
||||
await page.fill('input[type="password"]', "password123");
|
||||
await page.locator('input[type="password"]').nth(1).fill("password123");
|
||||
// Use direct URL with code
|
||||
await page.goto(`/signup/${inviteCode}`);
|
||||
|
||||
// Should redirect to signup with code in query and validate
|
||||
await page.waitForURL(/\/signup\?code=/);
|
||||
|
||||
// Wait for form to transition to registration form
|
||||
await expect(page.locator("h1")).toHaveText("Create account");
|
||||
|
||||
// Fill registration form
|
||||
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("/");
|
||||
|
||||
// Clear cookies and try again with same email
|
||||
await clearAuth(page);
|
||||
// Should redirect to home
|
||||
await expect(page).toHaveURL("/");
|
||||
});
|
||||
|
||||
test("shows error for invalid invite code", async ({ page }) => {
|
||||
await page.goto("/signup");
|
||||
await page.fill('input[type="email"]', email);
|
||||
await page.fill('input[type="password"]', "password123");
|
||||
await page.locator('input[type="password"]').nth(1).fill("password123");
|
||||
await page.fill('input#inviteCode', "fake-code-99");
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Should show error
|
||||
await expect(page.getByText("Email already registered")).toBeVisible();
|
||||
await expect(page.getByText(/not found/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows error for password mismatch", async ({ page }) => {
|
||||
test("shows error for password mismatch", async ({ page, request }) => {
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
await page.goto("/signup");
|
||||
await page.fill('input[type="email"]', uniqueEmail());
|
||||
await page.fill('input[type="password"]', "password123");
|
||||
await page.locator('input[type="password"]').nth(1).fill("differentpassword");
|
||||
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', uniqueEmail());
|
||||
await page.fill('input#password', "password123");
|
||||
await page.fill('input#confirmPassword', "differentpassword");
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page.getByText("Passwords do not match")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows error for short password", async ({ page }) => {
|
||||
test("shows error for short password", async ({ page, request }) => {
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
await page.goto("/signup");
|
||||
await page.fill('input[type="email"]', uniqueEmail());
|
||||
await page.fill('input[type="password"]', "short");
|
||||
await page.locator('input[type="password"]').nth(1).fill("short");
|
||||
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', uniqueEmail());
|
||||
await page.fill('input#password', "short");
|
||||
await page.fill('input#confirmPassword', "short");
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
await expect(page.getByText("Password must be at least 6 characters")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows loading state while submitting", async ({ page }) => {
|
||||
await page.goto("/signup");
|
||||
await page.fill('input[type="email"]', uniqueEmail());
|
||||
await page.fill('input[type="password"]', "password123");
|
||||
await page.locator('input[type="password"]').nth(1).fill("password123");
|
||||
|
||||
// Start submission and check for loading state
|
||||
const submitPromise = page.click('button[type="submit"]');
|
||||
await expect(page.locator('button[type="submit"]')).toHaveText("Creating account...");
|
||||
await submitPromise;
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Login", () => {
|
||||
const testEmail = `login-test-${Date.now()}@example.com`;
|
||||
let testEmail: string;
|
||||
const testPassword = "testpassword123";
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
// Create a test user
|
||||
const page = await browser.newPage();
|
||||
await page.goto("/signup");
|
||||
await page.fill('input[type="email"]', testEmail);
|
||||
await page.fill('input[type="password"]', testPassword);
|
||||
await page.locator('input[type="password"]').nth(1).fill(testPassword);
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL("/");
|
||||
await page.close();
|
||||
test.beforeAll(async ({ request }) => {
|
||||
// Create a test user with invite
|
||||
testEmail = uniqueEmail();
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
// Register the test user via backend API
|
||||
await request.post(`${API_BASE}/api/auth/register`, {
|
||||
data: {
|
||||
email: testEmail,
|
||||
password: testPassword,
|
||||
invite_identifier: inviteCode,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
|
@ -188,14 +239,19 @@ test.describe("Login", () => {
|
|||
});
|
||||
|
||||
test.describe("Logout", () => {
|
||||
test("can logout", async ({ page }) => {
|
||||
test("can logout", async ({ page, request }) => {
|
||||
const email = uniqueEmail();
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
// Sign up first
|
||||
// Sign up
|
||||
await page.goto("/signup");
|
||||
await page.fill('input[type="email"]', email);
|
||||
await page.fill('input[type="password"]', "password123");
|
||||
await page.locator('input[type="password"]').nth(1).fill("password123");
|
||||
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("/");
|
||||
|
||||
|
|
@ -206,14 +262,19 @@ test.describe("Logout", () => {
|
|||
await expect(page).toHaveURL("/login");
|
||||
});
|
||||
|
||||
test("cannot access home after logout", async ({ page }) => {
|
||||
test("cannot access home after logout", async ({ page, request }) => {
|
||||
const email = uniqueEmail();
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
// Sign up
|
||||
await page.goto("/signup");
|
||||
await page.fill('input[type="email"]', email);
|
||||
await page.fill('input[type="password"]', "password123");
|
||||
await page.locator('input[type="password"]').nth(1).fill("password123");
|
||||
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("/");
|
||||
|
||||
|
|
@ -228,14 +289,19 @@ test.describe("Logout", () => {
|
|||
});
|
||||
|
||||
test.describe("Session Persistence", () => {
|
||||
test("session persists after page reload", async ({ page }) => {
|
||||
test("session persists after page reload", async ({ page, request }) => {
|
||||
const email = uniqueEmail();
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
// Sign up
|
||||
await page.goto("/signup");
|
||||
await page.fill('input[type="email"]', email);
|
||||
await page.fill('input[type="password"]', "password123");
|
||||
await page.locator('input[type="password"]').nth(1).fill("password123");
|
||||
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("/");
|
||||
await expect(page.getByText(email)).toBeVisible();
|
||||
|
|
@ -248,13 +314,18 @@ test.describe("Session Persistence", () => {
|
|||
await expect(page.getByText(email)).toBeVisible();
|
||||
});
|
||||
|
||||
test("auth cookie is set after login", async ({ page }) => {
|
||||
test("auth cookie is set after signup", async ({ page, request }) => {
|
||||
const email = uniqueEmail();
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
await page.goto("/signup");
|
||||
await page.fill('input[type="email"]', email);
|
||||
await page.fill('input[type="password"]', "password123");
|
||||
await page.locator('input[type="password"]').nth(1).fill("password123");
|
||||
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("/");
|
||||
|
||||
|
|
@ -265,24 +336,26 @@ test.describe("Session Persistence", () => {
|
|||
expect(authCookie!.httpOnly).toBe(true);
|
||||
});
|
||||
|
||||
test("auth cookie is cleared on logout", async ({ page }) => {
|
||||
test("auth cookie is cleared on logout", async ({ page, request }) => {
|
||||
const email = uniqueEmail();
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
await page.goto("/signup");
|
||||
await page.fill('input[type="email"]', email);
|
||||
await page.fill('input[type="password"]', "password123");
|
||||
await page.locator('input[type="password"]').nth(1).fill("password123");
|
||||
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("/");
|
||||
|
||||
await page.click("text=Sign out");
|
||||
// Wait for navigation to complete - ensures the logout request finished
|
||||
// and the Set-Cookie header was processed by the browser
|
||||
await expect(page).toHaveURL("/login");
|
||||
|
||||
const cookies = await page.context().cookies();
|
||||
const authCookie = cookies.find((c) => c.name === "auth_token");
|
||||
// Cookie should be deleted or have empty value
|
||||
expect(!authCookie || authCookie.value === "").toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,39 +1,75 @@
|
|||
import { test, expect, Page } from "@playwright/test";
|
||||
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 authenticate a user
|
||||
async function authenticate(page: Page): Promise<string> {
|
||||
// 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");
|
||||
await page.fill('input[type="email"]', email);
|
||||
await page.fill('input[type="password"]', "password123");
|
||||
await page.locator('input[type="password"]').nth(1).fill("password123");
|
||||
|
||||
// Enter invite code first
|
||||
await page.fill('input#inviteCode', inviteCode);
|
||||
await 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 }) => {
|
||||
await authenticate(page);
|
||||
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 }) => {
|
||||
await authenticate(page);
|
||||
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 }) => {
|
||||
await authenticate(page);
|
||||
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();
|
||||
|
|
@ -41,8 +77,8 @@ test.describe("Counter - Authenticated", () => {
|
|||
await expect(page.locator("h1")).toHaveText(String(Number(before) + 1));
|
||||
});
|
||||
|
||||
test("clicking increment multiple times", async ({ page }) => {
|
||||
await authenticate(page);
|
||||
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());
|
||||
|
|
@ -64,8 +100,8 @@ test.describe("Counter - Authenticated", () => {
|
|||
expect(final).toBeGreaterThanOrEqual(before + 3);
|
||||
});
|
||||
|
||||
test("counter persists after page reload", async ({ page }) => {
|
||||
await authenticate(page);
|
||||
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();
|
||||
|
|
@ -77,9 +113,9 @@ test.describe("Counter - Authenticated", () => {
|
|||
await expect(page.locator("h1")).toHaveText(expected);
|
||||
});
|
||||
|
||||
test("counter is shared between users", async ({ page, browser }) => {
|
||||
test("counter is shared between users", async ({ page, browser, request }) => {
|
||||
// First user increments
|
||||
await authenticate(page);
|
||||
await authenticate(page, request);
|
||||
await expect(page.locator("h1")).not.toHaveText("...");
|
||||
|
||||
const initialValue = Number(await page.locator("h1").textContent());
|
||||
|
|
@ -92,7 +128,7 @@ test.describe("Counter - Authenticated", () => {
|
|||
|
||||
// Second user in new context sees the current value
|
||||
const page2 = await browser.newPage();
|
||||
await authenticate(page2);
|
||||
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)
|
||||
|
|
@ -130,14 +166,19 @@ test.describe("Counter - Unauthenticated", () => {
|
|||
});
|
||||
|
||||
test.describe("Counter - Session Integration", () => {
|
||||
test("can access counter after login", async ({ page }) => {
|
||||
test("can access counter after login", async ({ page, request }) => {
|
||||
const email = uniqueEmail();
|
||||
const inviteCode = await createInvite(request);
|
||||
|
||||
// Sign up first
|
||||
// Sign up with invite
|
||||
await page.goto("/signup");
|
||||
await page.fill('input[type="email"]', email);
|
||||
await page.fill('input[type="password"]', "password123");
|
||||
await page.locator('input[type="password"]').nth(1).fill("password123");
|
||||
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("/");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue