408 lines
14 KiB
TypeScript
408 lines
14 KiB
TypeScript
import { test, expect, Page, APIRequestContext } from "@playwright/test";
|
|
|
|
// Helper to generate unique email for each test
|
|
function uniqueEmail(): string {
|
|
return `test-${Date.now()}-${Math.random().toString(36).substring(7)}@example.com`;
|
|
}
|
|
|
|
// Helper to clear auth cookies
|
|
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);
|
|
});
|
|
|
|
test("redirects to login when not authenticated", async ({ page }) => {
|
|
await page.goto("/");
|
|
await expect(page).toHaveURL("/login");
|
|
});
|
|
|
|
test("login page has correct form elements", async ({ page }) => {
|
|
await page.goto("/login");
|
|
await expect(page.locator("h1")).toHaveText("Welcome back");
|
|
await expect(page.locator('input[type="email"]')).toBeVisible();
|
|
await expect(page.locator('input[type="password"]')).toBeVisible();
|
|
await expect(page.locator('button[type="submit"]')).toHaveText("Sign in");
|
|
await expect(page.locator('a[href="/signup"]')).toBeVisible();
|
|
});
|
|
|
|
test("signup page has invite code form", async ({ page }) => {
|
|
await page.goto("/signup");
|
|
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();
|
|
});
|
|
|
|
test("can navigate from login to signup", async ({ page }) => {
|
|
await page.goto("/login");
|
|
await page.click('a[href="/signup"]');
|
|
await expect(page).toHaveURL("/signup");
|
|
});
|
|
|
|
test("can navigate from signup to login", async ({ page }) => {
|
|
await page.goto("/signup");
|
|
await page.click('a[href="/login"]');
|
|
await expect(page).toHaveURL("/login");
|
|
});
|
|
});
|
|
|
|
test.describe("Logged-in User Visiting Invite URL", () => {
|
|
test("redirects to home when logged-in user visits direct invite URL", async ({ page, request }) => {
|
|
const email = uniqueEmail();
|
|
const inviteCode = await createInvite(request);
|
|
|
|
// First sign up to create a user
|
|
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("/");
|
|
|
|
// Create another invite
|
|
const anotherInvite = await createInvite(request);
|
|
|
|
// Visit invite URL while logged in - should redirect to home
|
|
await page.goto(`/signup/${anotherInvite}`);
|
|
await expect(page).toHaveURL("/");
|
|
});
|
|
|
|
test("redirects to home when logged-in user visits signup page", async ({ page, request }) => {
|
|
const email = uniqueEmail();
|
|
const inviteCode = await createInvite(request);
|
|
|
|
// Sign up and stay logged in
|
|
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("/");
|
|
|
|
// Try to visit signup page while logged in - should redirect to home
|
|
await page.goto("/signup");
|
|
await expect(page).toHaveURL("/");
|
|
});
|
|
});
|
|
|
|
test.describe("Signup with Invite", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await clearAuth(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");
|
|
|
|
// 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
|
|
await expect(page).toHaveURL("/");
|
|
// Should show user email
|
|
await expect(page.getByText(email)).toBeVisible();
|
|
});
|
|
|
|
test("signup with direct invite URL works", async ({ page, request }) => {
|
|
const email = uniqueEmail();
|
|
const inviteCode = await createInvite(request);
|
|
|
|
// 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"]');
|
|
|
|
// 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#inviteCode', "fake-code-99");
|
|
await page.click('button[type="submit"]');
|
|
|
|
// Should show error
|
|
await expect(page.getByText(/not found/i)).toBeVisible();
|
|
});
|
|
|
|
test("shows error for password mismatch", async ({ page, request }) => {
|
|
const inviteCode = await createInvite(request);
|
|
|
|
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', 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, request }) => {
|
|
const inviteCode = await createInvite(request);
|
|
|
|
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', 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.describe("Login", () => {
|
|
let testEmail: string;
|
|
const testPassword = "testpassword123";
|
|
|
|
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 }) => {
|
|
await clearAuth(page);
|
|
});
|
|
|
|
test("can login with valid credentials", async ({ page }) => {
|
|
await page.goto("/login");
|
|
await page.fill('input[type="email"]', testEmail);
|
|
await page.fill('input[type="password"]', testPassword);
|
|
await page.click('button[type="submit"]');
|
|
|
|
await expect(page).toHaveURL("/");
|
|
await expect(page.getByText(testEmail)).toBeVisible();
|
|
});
|
|
|
|
test("shows error for wrong password", async ({ page }) => {
|
|
await page.goto("/login");
|
|
await page.fill('input[type="email"]', testEmail);
|
|
await page.fill('input[type="password"]', "wrongpassword");
|
|
await page.click('button[type="submit"]');
|
|
|
|
await expect(page.getByText("Incorrect email or password")).toBeVisible();
|
|
});
|
|
|
|
test("shows error for non-existent user", async ({ page }) => {
|
|
await page.goto("/login");
|
|
await page.fill('input[type="email"]', "nonexistent@example.com");
|
|
await page.fill('input[type="password"]', "password123");
|
|
await page.click('button[type="submit"]');
|
|
|
|
await expect(page.getByText("Incorrect email or password")).toBeVisible();
|
|
});
|
|
|
|
test("shows loading state while submitting", async ({ page }) => {
|
|
await page.goto("/login");
|
|
await page.fill('input[type="email"]', testEmail);
|
|
await page.fill('input[type="password"]', testPassword);
|
|
|
|
const submitPromise = page.click('button[type="submit"]');
|
|
await expect(page.locator('button[type="submit"]')).toHaveText("Signing in...");
|
|
await submitPromise;
|
|
});
|
|
});
|
|
|
|
test.describe("Logout", () => {
|
|
test("can logout", async ({ page, request }) => {
|
|
const email = uniqueEmail();
|
|
const inviteCode = await createInvite(request);
|
|
|
|
// Sign up
|
|
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("/");
|
|
|
|
// Click logout
|
|
await page.click("text=Sign out");
|
|
|
|
// Should redirect to login
|
|
await expect(page).toHaveURL("/login");
|
|
});
|
|
|
|
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#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");
|
|
|
|
// Try to access home
|
|
await page.goto("/");
|
|
await expect(page).toHaveURL("/login");
|
|
});
|
|
});
|
|
|
|
test.describe("Session Persistence", () => {
|
|
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#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();
|
|
|
|
// Reload page
|
|
await page.reload();
|
|
|
|
// Should still be logged in
|
|
await expect(page).toHaveURL("/");
|
|
await expect(page.getByText(email)).toBeVisible();
|
|
});
|
|
|
|
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#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("/");
|
|
|
|
// Check cookies
|
|
const cookies = await page.context().cookies();
|
|
const authCookie = cookies.find((c) => c.name === "auth_token");
|
|
expect(authCookie).toBeTruthy();
|
|
expect(authCookie!.httpOnly).toBe(true);
|
|
});
|
|
|
|
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#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");
|
|
await expect(page).toHaveURL("/login");
|
|
|
|
const cookies = await page.context().cookies();
|
|
const authCookie = cookies.find((c) => c.name === "auth_token");
|
|
expect(!authCookie || authCookie.value === "").toBe(true);
|
|
});
|
|
});
|