CI / changes (pull_request) Successful in 9s
CI / api (pull_request) Successful in 3m2s
CI / fast-checks (pull_request) Successful in 4m20s
CI / security (pull_request) Successful in 1m14s
CI / harness (pull_request) Successful in 6m56s
CI / gate (pull_request) Successful in 2s
Lint (eslint --max-warnings 0): - index.ts: disable no-unsafe-argument on the type-only Context mismatch when delegating to the OIDC handler inside the local-session skip wrapper - localAuth.ts: handleLogout is sync (no await) — drop async (require-await) - devBypass.ts: disable detect-possible-timing-attacks on the public well-known dev-placeholder string compare (not a secret comparison) - remove dead code / unused bindings flagged by no-unused-vars: makeTestApp (localSession.test), makeUnauthContext + BrowserContext import (login.spec), unused memberId (admin.test), unused txSelectCount counter (me.test) - localAuthMiddleware.test / me.test: fix unused + reflow-detached eslint-disable directives Format: prettier --write across the 20 Phase-19 files that were never formatted. Secret scan (gitleaks): allowlist two false positives — the synthetic >=32-char TEST_SECRET in localSession.test.ts, and .planning/ design prose (a generic-api-key regex hit on "credential atomically, 409-equivalent"). Neither is a real secret. Verified locally: format:check, lint, typecheck, md:lint, gitleaks (no leaks), PWA 266/266, API 452/452. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
118 lines
5.5 KiB
TypeScript
118 lines
5.5 KiB
TypeScript
/**
|
|
* login.spec.ts — Phase 19 AUTH-LOCAL-12/15/16
|
|
*
|
|
* Real-login-form e2e tests covering the PWA login gate + form interaction.
|
|
*
|
|
* Strategy (Option C):
|
|
* The global-setup seeds 'devuser'/'devpass' into local_credentials and the API's
|
|
* devSessionCookieMiddleware issues a local-session cookie on every /api/* request
|
|
* under DEV_AUTH_BYPASS=true. The OTHER specs (layout, calendar, lists) rely on that
|
|
* cookie being present and do NOT clear it — they still reach the authed app unchanged.
|
|
*
|
|
* IMPORTANT — bypass constraint: this harness is DEV_AUTH_BYPASS-only (global-setup
|
|
* refuses a non-bypass DB). Under the bypass, devAuthBypass() injects DEV_USER into
|
|
* every /api/* request, so /api/me is authed regardless of the local-session cookie —
|
|
* clearing the cookie does NOT produce a logged-out state in the browser. We therefore
|
|
* exercise the /login page DIRECTLY (the /login route always renders the form) for the
|
|
* form + real-login round-trip, and cover the unauthenticated root→/login redirect gate
|
|
* at the unit level in src/App.test.tsx (where meQuery.isError is controllable).
|
|
*
|
|
* Specs covered:
|
|
* 1. /login renders all brand + form surfaces (real browser, real CSS/tokens)
|
|
* 2. Wrong password → single "Incorrect username or password." error message
|
|
* 3. Correct devuser/devpass → navigates into the app (out of /login)
|
|
*
|
|
* Only runs on the desktop/chromium project (Chromium handles local-session cookies
|
|
* consistently; WebKit PWA restrictions are irrelevant here since the login form is
|
|
* a normal web page, not a Home Screen PWA). Other profiles inherit the bypass cookie.
|
|
*
|
|
* Run:
|
|
* pnpm --filter @familysync/pwa test:e2e --grep "login"
|
|
* pnpm --filter @familysync/pwa exec playwright test --project=desktop login.spec.ts
|
|
*/
|
|
import { test, expect } from '@playwright/test';
|
|
|
|
// Selectors derived from 19-UI-SPEC.md Surfaces 3-7 (locked by plan 04 implementation)
|
|
const SELECTORS = {
|
|
usernameInput: '#login-username',
|
|
// password input has id="login-password" (UI-SPEC Surface 5)
|
|
passwordInput: '#login-password',
|
|
// Primary submit: role=button with name "Sign in" (UI-SPEC Surface 7)
|
|
submitBtn: 'button[type="submit"]',
|
|
// Error message is in a role="status" element (UI-SPEC Surface 6)
|
|
errorMessage: '[role="status"]',
|
|
};
|
|
|
|
// Only run these specs on the desktop profile. The login form is a standard web
|
|
// page (not PWA-specific) and Chromium handles cookies most consistently for this test.
|
|
// iphone/pixel still reach the authed app via the bypass-issued cookie (unchanged behavior).
|
|
test.describe('Login form — real auth round-trip (desktop/Chromium only)', () => {
|
|
test.skip(
|
|
({ browserName }) => browserName !== 'chromium',
|
|
'Login form tests only run on Chromium (desktop profile) — other profiles use the bypass cookie',
|
|
);
|
|
|
|
test('/login renders all brand + form surfaces (UI-SPEC Surfaces 2-7)', async ({ page }) => {
|
|
// Navigate DIRECTLY to /login rather than asserting an unauthenticated root→/login
|
|
// redirect: under the always-on DEV_AUTH_BYPASS, /api/me is authed via DEV_USER
|
|
// injection regardless of the cookie, so visiting / lands on /calendar and a
|
|
// logged-out state is unreachable here. The redirect gate is unit-tested in
|
|
// src/App.test.tsx; this e2e proves /login renders every surface in a real browser.
|
|
await page.goto('/login', { waitUntil: 'domcontentloaded' });
|
|
|
|
// Assert we are on the /login route
|
|
await expect(page).toHaveURL(/\/login/);
|
|
|
|
// Brand slot: "FamilySync" text should be visible (UI-SPEC Surface 2)
|
|
await expect(page.getByText('FamilySync', { exact: true })).toBeVisible();
|
|
|
|
// Login card heading "Sign in" (UI-SPEC Surface 3)
|
|
await expect(page.getByRole('heading', { name: 'Sign in' })).toBeVisible();
|
|
|
|
// Username field (UI-SPEC Surface 4)
|
|
await expect(page.locator(SELECTORS.usernameInput)).toBeVisible();
|
|
|
|
// Password field (UI-SPEC Surface 5)
|
|
await expect(page.locator(SELECTORS.passwordInput)).toBeVisible();
|
|
|
|
// Submit button (UI-SPEC Surface 7)
|
|
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
|
|
});
|
|
|
|
test('wrong password shows single "Incorrect username or password." error', async ({
|
|
page,
|
|
context,
|
|
}) => {
|
|
await context.clearCookies();
|
|
|
|
await page.goto('/login', { waitUntil: 'domcontentloaded' });
|
|
|
|
// Fill in wrong credentials
|
|
await page.locator(SELECTORS.usernameInput).fill('devuser');
|
|
await page.locator(SELECTORS.passwordInput).fill('wrongpassword');
|
|
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
|
|
// Error message appears (UI-SPEC Surface 6 — "Incorrect username or password.")
|
|
const errorEl = page.locator(SELECTORS.errorMessage);
|
|
await expect(errorEl).toBeVisible({ timeout: 5_000 });
|
|
await expect(errorEl).toContainText('Incorrect username or password.');
|
|
|
|
// Still on /login
|
|
await expect(page).toHaveURL(/\/login/);
|
|
});
|
|
|
|
test('correct devuser/devpass logs in and navigates out of /login', async ({ page, context }) => {
|
|
await context.clearCookies();
|
|
|
|
await page.goto('/login', { waitUntil: 'domcontentloaded' });
|
|
|
|
// Fill in the seeded dev credentials (global-setup seeds devuser/devpass)
|
|
await page.locator(SELECTORS.usernameInput).fill('devuser');
|
|
await page.locator(SELECTORS.passwordInput).fill('devpass');
|
|
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
|
|
// After login, the page navigates away from /login (to / or /calendar)
|
|
await expect(page).not.toHaveURL(/\/login/, { timeout: 10_000 });
|
|
});
|
|
});
|