first implementation

This commit is contained in:
counterweight 2025-12-20 11:12:11 +01:00
parent 79458bcba4
commit 870804e7b9
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
24 changed files with 5485 additions and 184 deletions

View file

@ -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);
});
});