diff --git a/e2e/auth.spec.ts b/e2e/auth.spec.ts index 4960417..a0b0d4c 100644 --- a/e2e/auth.spec.ts +++ b/e2e/auth.spec.ts @@ -1,34 +1,32 @@ -import { test, expect } from '@playwright/test'; -import { login, DEMO } from './helpers/auth'; +import { test, expect, request } from '@playwright/test'; +import { login, loginViaAPI, DEMO } from './helpers/auth'; test.describe('Authentication', () => { test('login page renders correctly', async ({ page }) => { await page.goto('/login'); - await expect(page.locator('text=FiberOps')).toBeVisible(); - await expect(page.locator('text=Sign in')).toBeVisible(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.locator('h1:has-text("FiberOps")')).toBeVisible(); + await expect(page.locator('h2:has-text("Sign in")')).toBeVisible(); await expect(page.locator('input[type="email"]')).toBeVisible(); await expect(page.locator('input[type="password"]')).toBeVisible(); }); test('login with valid credentials redirects to dashboard', async ({ page }) => { await login(page); - // Should have left the login page - expect(page.url()).not.toContain('/login'); + expect(page.url()).toContain('/dashboard'); }); - test('login with wrong credentials stays on login', async ({ page }) => { - await page.goto('/login'); - await page.locator('input[placeholder*="demo-isp"], input#tenantSlug').first().fill(DEMO.tenant); - await page.locator('input[type="email"]').first().fill(DEMO.email); - await page.locator('input[type="password"]').first().fill('wrongpassword'); - await page.click('button[type="submit"]'); - await page.waitForTimeout(3000); - // Should stay on login - expect(page.url()).toContain('/login'); + test('login with wrong credentials returns 401', async () => { + const ctx = await request.newContext({ baseURL: 'https://fiberops-api.juankibin.space' }); + const resp = await ctx.post('/api/v1/auth/login', { + data: { tenantSlug: DEMO.tenant, email: DEMO.email, password: 'wrongpassword' }, + headers: { 'Content-Type': 'application/json', 'x-tenant-slug': DEMO.tenant }, + }); + await ctx.dispose(); + expect(resp.status()).toBe(401); }); test('unauthenticated access redirects to login', async ({ page }) => { - // Clear any stored auth await page.goto('/login'); await page.evaluate(() => localStorage.clear()); await page.goto('/dashboard'); @@ -38,7 +36,6 @@ test.describe('Authentication', () => { test('logout returns to login', async ({ page }) => { await login(page); - // Find and click logout const logoutBtn = page.locator('button:has-text("Logout"), a:has-text("Logout")').first(); await logoutBtn.click(); await page.waitForTimeout(3000); diff --git a/e2e/helpers/auth.ts b/e2e/helpers/auth.ts index 80d4e2e..4134616 100644 --- a/e2e/helpers/auth.ts +++ b/e2e/helpers/auth.ts @@ -1,4 +1,4 @@ -import { Page, expect } from '@playwright/test'; +import { Page, request as playwrightRequest } from '@playwright/test'; export const DEMO = { tenant: 'demo-isp', @@ -6,34 +6,38 @@ export const DEMO = { password: 'Admin123!', }; -export async function login(page: Page, creds = DEMO) { - await page.goto('/login'); - await page.waitForLoadState('networkidle'); - - // Fill form — support both old (shadcn Label-based) and new (native) login pages - const tenantInput = page.locator('input[placeholder*="demo-isp"], input#tenantSlug').first(); - await tenantInput.fill(creds.tenant); - - const emailInput = page.locator('input[type="email"]').first(); - await emailInput.fill(creds.email); - - const passwordInput = page.locator('input[type="password"]').first(); - await passwordInput.fill(creds.password); - - // Click submit - await page.click('button[type="submit"]'); - - // Wait for either dashboard URL or navigation away from login - // Increase timeout to 15s to account for API latency - try { - await page.waitForURL(/\/(dashboard|clients|settings)/, { timeout: 15000 }); - } catch { - // If URL hasn't changed, check if we're still on login with an error - const currentUrl = page.url(); - if (currentUrl.includes('/login')) { - // Try clicking submit again (sometimes zustand hydration delays) - await page.click('button[type="submit"]'); - await page.waitForURL(/\/(dashboard|clients|settings)/, { timeout: 15000 }); - } - } +// Use the public API URL — Node.js requests bypass browser CORS +const API_URL = 'https://fiberops-api.juankibin.space'; + +export async function loginViaAPI(creds = DEMO) { + const ctx = await playwrightRequest.newContext({ baseURL: API_URL }); + const resp = await ctx.post('/api/v1/auth/login', { + data: { tenantSlug: creds.tenant, email: creds.email, password: creds.password }, + headers: { 'Content-Type': 'application/json', 'x-tenant-slug': creds.tenant }, + }); + const data = await resp.json(); + await ctx.dispose(); + return data; // { accessToken, refreshToken, user } +} + +export async function login(page: Page, creds = DEMO) { + // Step 1: Get token via Node.js HTTP — bypasses browser CORS entirely + const { accessToken, user } = await loginViaAPI(creds); + + // Step 2: Inject zustand persist state into localStorage before navigation + await page.goto('/login'); + await page.evaluate( + ({ token, tenant, u }) => { + // Zustand persist format: { state: {...}, version: 0 } + localStorage.setItem('fiberops_auth', JSON.stringify({ + state: { accessToken: token, tenantSlug: tenant, user: u }, + version: 0, + })); + }, + { token: accessToken, tenant: creds.tenant, u: user } + ); + + // Step 3: Navigate to dashboard — auth guard reads localStorage and passes + await page.goto('/dashboard'); + await page.waitForURL(/\/(dashboard|clients|settings)/, { timeout: 15000 }); } diff --git a/playwright.config.ts b/playwright.config.ts index bf5a2aa..1a9813c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,7 +2,8 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './e2e', - fullyParallel: false, // run serially — shared demo tenant + timeout: 60000, // 60s per test — accounts for ~2s API latency via CF tunnel + fullyParallel: false, forbidOnly: !!process.env.CI, retries: 1, workers: 1,