arbret/frontend/e2e/admin-invites.spec.ts

174 lines
6.5 KiB
TypeScript
Raw Normal View History

2025-12-20 11:12:11 +01:00
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("/admin/trades");
2025-12-20 11:12:11 +01:00
}
test.describe("Admin Invites Page", () => {
test.beforeEach(async ({ context, page }) => {
await context.addInitScript(() => {
window.localStorage.setItem("arbret-locale", "en");
});
2025-12-20 11:12:11 +01:00
await page.context().clearCookies();
await loginAsAdmin(page);
});
2025-12-24 23:52:52 +01:00
test("admin can access invites page and UI elements are correct", async ({ page }) => {
2025-12-20 11:12:11 +01:00
await page.goto("/admin/invites");
2025-12-24 23:52:52 +01:00
// Check page headings
2025-12-20 11:12:11 +01:00
await expect(page.getByRole("heading", { name: "Create Invite" })).toBeVisible();
await expect(page.getByRole("heading", { name: "All Invites" })).toBeVisible();
2025-12-20 11:12:11 +01:00
// The godfather selector should be a <select> element, not an <input type="number">
const selectElement = page.locator("select").first();
await expect(selectElement).toBeVisible();
2025-12-20 12:11:22 +01:00
// Wait for users to load by checking for a known user in the dropdown
await expect(selectElement).toContainText(REGULAR_USER_EMAIL);
2025-12-20 11:12:11 +01:00
// Verify it has user options (at least the seeded users)
const options = selectElement.locator("option");
const optionCount = await options.count();
2025-12-20 11:12:11 +01:00
// Should have at least 2 options: placeholder + at least one user
expect(optionCount).toBeGreaterThanOrEqual(2);
2025-12-20 11:12:11 +01:00
// There should NOT be a number input for godfather ID
const numberInput = page.locator('input[type="number"]');
await expect(numberInput).toHaveCount(0);
});
2025-12-24 23:52:52 +01:00
test("can create invite with proper button state management", async ({ page }) => {
2025-12-20 11:12:11 +01:00
await page.goto("/admin/invites");
2025-12-20 11:12:11 +01:00
// Wait for page to load
await page.waitForSelector("select");
2025-12-24 23:52:52 +01:00
// 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
2025-12-20 11:12:11 +01:00
const godfatherSelect = page.locator("select").first();
await godfatherSelect.selectOption({ label: REGULAR_USER_EMAIL });
2025-12-24 23:52:52 +01:00
// Now the button should be enabled
await expect(createButton).toBeEnabled();
2025-12-20 11:12:11 +01:00
// Click create invite
await page.click('button:has-text("Create Invite")');
2025-12-20 11:12:11 +01:00
// Wait for the invite to appear in the table
await expect(page.locator("table")).toContainText(REGULAR_USER_EMAIL);
2025-12-20 11:12:11 +01:00
// 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}$/);
});
2025-12-24 23:52:52 +01:00
test("can revoke invite and filter by status", async ({ page }) => {
2025-12-20 11:12:11 +01:00
await page.goto("/admin/invites");
await page.waitForSelector("select");
2025-12-20 11:12:11 +01:00
// Create an invite first
const godfatherSelect = page.locator("select").first();
await godfatherSelect.selectOption({ label: REGULAR_USER_EMAIL });
2025-12-25 00:48:22 +01:00
// Wait for create invite response
const createPromise = page.waitForResponse(
(resp) => resp.url().includes("/api/admin/invites") && resp.request().method() === "POST"
);
2025-12-20 11:12:11 +01:00
await page.click('button:has-text("Create Invite")');
2025-12-25 00:48:22 +01:00
await createPromise;
// Wait for table to update with new invite
2025-12-24 23:52:52 +01:00
await expect(page.locator("table")).toContainText("ready");
2025-12-20 22:18:14 +01:00
// Wait for the new invite to appear and capture its code
const newInviteRow = page
.locator("tr")
.filter({ hasText: REGULAR_USER_EMAIL })
.filter({ hasText: "ready" })
.first();
2025-12-20 22:18:14 +01:00
await expect(newInviteRow).toBeVisible();
2025-12-20 22:18:14 +01:00
// Get the invite code from this row (first cell)
const inviteCode = await newInviteRow.locator("td").first().textContent();
2025-12-25 00:48:22 +01:00
// Click revoke and wait for the response
// The revoke endpoint is POST /api/admin/invites/{invite_id}/revoke
const revokePromise = page.waitForResponse(
(resp) =>
resp.url().includes("/api/admin/invites") &&
resp.url().includes("/revoke") &&
resp.request().method() === "POST"
);
2025-12-20 22:18:14 +01:00
await newInviteRow.locator('button:has-text("Revoke")').click();
2025-12-25 00:48:22 +01:00
await revokePromise;
2025-12-25 00:48:22 +01:00
// Wait for table to refresh and verify this specific invite now shows "revoked"
2025-12-20 22:18:14 +01:00
const revokedRow = page.locator("tr").filter({ hasText: inviteCode! });
2025-12-25 00:48:22 +01:00
await expect(revokedRow).toContainText("revoked", { timeout: 5000 });
2025-12-20 11:12:11 +01:00
2025-12-24 23:52:52 +01:00
// Test status filter - filter by "revoked" status
2025-12-20 11:12:11 +01:00
const statusFilter = page.locator("select").nth(1); // Second select is the status filter
2025-12-26 19:21:34 +01:00
// Wait for filter response, but don't fail if it doesn't come (might be cached)
const filterPromise = page
.waitForResponse((resp) => resp.url().includes("status=revoked"), { timeout: 5000 })
.catch(() => null); // Ignore timeout - filter might be cached
2025-12-20 11:12:11 +01:00
await statusFilter.selectOption("revoked");
2025-12-26 19:21:34 +01:00
await filterPromise; // Wait for response if it comes
2025-12-26 19:21:34 +01:00
// Verify revoked invite is visible
2025-12-25 00:48:22 +01:00
await expect(revokedRow).toBeVisible({ timeout: 5000 });
2025-12-25 00:48:22 +01:00
// Filter by "ready" status - should not show our revoked invite
2025-12-26 19:21:34 +01:00
const readyFilterPromise = page
.waitForResponse((resp) => resp.url().includes("status=ready"), { timeout: 5000 })
.catch(() => null); // Ignore timeout - filter might be cached
2025-12-20 11:12:11 +01:00
await statusFilter.selectOption("ready");
2025-12-26 19:21:34 +01:00
await readyFilterPromise; // Wait for response if it comes
2025-12-25 00:48:22 +01:00
await expect(revokedRow).not.toBeVisible({ timeout: 5000 });
2025-12-20 11:12:11 +01:00
});
});
test.describe("Admin Invites Access Control", () => {
2025-12-24 23:52:52 +01:00
test("regular user and unauthenticated user cannot access admin invites page", async ({
page,
}) => {
// Test unauthenticated access
await page.context().clearCookies();
await page.goto("/admin/invites");
await expect(page).toHaveURL("/login");
// Test regular user access
2025-12-20 11:12:11 +01:00
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"]');
2025-12-26 19:21:34 +01:00
// Regular users are redirected to /exchange after login
await expect(page).toHaveURL("/exchange");
2025-12-20 11:12:11 +01:00
// Try to access admin invites page
await page.goto("/admin/invites");
2025-12-26 19:21:34 +01:00
// Should be redirected away (to exchange page based on fallbackRedirect)
2025-12-20 11:12:11 +01:00
await expect(page).not.toHaveURL("/admin/invites");
});
});