test: add comprehensive e2e test suite with shared helpers and DB seeding
Extract shared test runner (helpers.js), add file-based SQLite with setup_db.py for fixture seeding, and add tests for auth guard, credentials management, full registration flow, health endpoint, password auth, and magic link registration errors. 66 checks across 7 test files.
This commit is contained in:
parent
dbd7449ea1
commit
c381896de4
10 changed files with 422 additions and 30 deletions
49
tests/e2e/helpers.js
Normal file
49
tests/e2e/helpers.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// tests/e2e/helpers.js
|
||||
// Shared utilities for Porchlight e2e tests.
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const TARGET_URL = process.env.TARGET_URL || 'http://localhost:8099';
|
||||
|
||||
/**
|
||||
* Simple test runner with pass/fail counting.
|
||||
*
|
||||
* Usage:
|
||||
* const { run } = require('./helpers');
|
||||
* run(async (page, assert) => {
|
||||
* await page.goto(TARGET_URL + '/login');
|
||||
* assert(true, 'page loaded');
|
||||
* });
|
||||
*/
|
||||
async function run(testFn) {
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, description) {
|
||||
if (condition) {
|
||||
console.log(` PASS: ${description}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(` FAIL: ${description}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
const headless = process.env.E2E_HEADLESS !== '0';
|
||||
const browser = await chromium.launch({ headless });
|
||||
const page = await browser.newPage();
|
||||
|
||||
try {
|
||||
await testFn(page, assert);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
console.log(`\n========================================`);
|
||||
console.log(`Results: ${passed} passed, ${failed} failed`);
|
||||
console.log(`========================================\n`);
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
module.exports = { TARGET_URL, run };
|
||||
|
|
@ -16,11 +16,16 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|||
PORT="${E2E_PORT:-8099}"
|
||||
export TARGET_URL="http://127.0.0.1:${PORT}"
|
||||
|
||||
# --- Temp directory for e2e state ---
|
||||
E2E_TMPDIR="$(mktemp -d)"
|
||||
export OIDC_OP_SQLITE_PATH="${E2E_TMPDIR}/e2e_test.db"
|
||||
export OIDC_OP_SIGNING_KEY_PATH="${E2E_TMPDIR}/keys"
|
||||
|
||||
# --- Start the app ---
|
||||
echo "Starting Porchlight on port ${PORT}..."
|
||||
echo " DB: ${OIDC_OP_SQLITE_PATH}"
|
||||
OIDC_OP_ISSUER="${TARGET_URL}" \
|
||||
OIDC_OP_DEBUG=true \
|
||||
OIDC_OP_SQLITE_PATH=:memory: \
|
||||
uv run --directory "$PROJECT_ROOT" \
|
||||
uvicorn fastapi_oidc_op.app:create_app \
|
||||
--factory --host 127.0.0.1 --port "$PORT" \
|
||||
|
|
@ -31,6 +36,7 @@ cleanup() {
|
|||
echo "Stopping server (pid ${SERVER_PID})..."
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
rm -rf "$E2E_TMPDIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
|
|
@ -48,6 +54,12 @@ for i in $(seq 1 30); do
|
|||
sleep 1
|
||||
done
|
||||
|
||||
# --- Seed test data ---
|
||||
echo "Seeding test data..."
|
||||
E2E_FIXTURES=$(uv run --directory "$PROJECT_ROOT" python tests/e2e/setup_db.py)
|
||||
export E2E_FIXTURES
|
||||
echo "Test fixtures: ${E2E_FIXTURES}"
|
||||
|
||||
# --- Run tests ---
|
||||
FAILED=0
|
||||
|
||||
|
|
|
|||
75
tests/e2e/setup_db.py
Normal file
75
tests/e2e/setup_db.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Seed the e2e test database with test fixtures.
|
||||
|
||||
Outputs JSON with the created test data (magic link tokens, usernames, etc.)
|
||||
so the JS tests can use them.
|
||||
|
||||
Requires OIDC_OP_SQLITE_PATH env var pointing to the app's SQLite DB.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from fastapi_oidc_op.authn.password import PasswordService
|
||||
from fastapi_oidc_op.invite.service import MagicLinkService
|
||||
from fastapi_oidc_op.models import PasswordCredential, User
|
||||
from fastapi_oidc_op.store.sqlite.repositories import (
|
||||
SQLiteCredentialRepository,
|
||||
SQLiteMagicLinkRepository,
|
||||
SQLiteUserRepository,
|
||||
)
|
||||
|
||||
|
||||
async def seed() -> None:
|
||||
db_path = os.environ.get("OIDC_OP_SQLITE_PATH")
|
||||
if not db_path:
|
||||
print("OIDC_OP_SQLITE_PATH not set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
db = await aiosqlite.connect(db_path)
|
||||
db.row_factory = aiosqlite.Row
|
||||
|
||||
user_repo = SQLiteUserRepository(db)
|
||||
cred_repo = SQLiteCredentialRepository(db)
|
||||
magic_link_repo = SQLiteMagicLinkRepository(db)
|
||||
password_service = PasswordService()
|
||||
magic_link_service = MagicLinkService(repo=magic_link_repo)
|
||||
|
||||
result = {}
|
||||
|
||||
# 1. Create a magic link for registration test
|
||||
link = await magic_link_service.create(username="newuser")
|
||||
result["register_token"] = link.token
|
||||
result["register_username"] = "newuser"
|
||||
|
||||
# 2. Create a user with a password for login test
|
||||
user = User(userid="test-user-01", username="testuser", groups=["users"])
|
||||
await user_repo.create(user)
|
||||
password_hash = password_service.hash("testpassword123")
|
||||
await cred_repo.create_password(PasswordCredential(user_id=user.userid, password_hash=password_hash))
|
||||
result["login_username"] = "testuser"
|
||||
result["login_password"] = "testpassword123"
|
||||
|
||||
# 3. Create a separate user for credentials management test
|
||||
cred_user = User(userid="test-user-02", username="creduser", groups=["users"])
|
||||
await user_repo.create(cred_user)
|
||||
cred_password_hash = password_service.hash("credpassword123")
|
||||
await cred_repo.create_password(PasswordCredential(user_id=cred_user.userid, password_hash=cred_password_hash))
|
||||
result["cred_username"] = "creduser"
|
||||
result["cred_password"] = "credpassword123"
|
||||
|
||||
# 4. Create an expired/used magic link for negative test
|
||||
expired_link = await magic_link_service.create(username="expired")
|
||||
await magic_link_service.mark_used(expired_link.token)
|
||||
result["used_token"] = expired_link.token
|
||||
|
||||
await db.commit()
|
||||
await db.close()
|
||||
print(json.dumps(result))
|
||||
|
||||
|
||||
asyncio.run(seed())
|
||||
18
tests/e2e/test_auth_guard.js
Normal file
18
tests/e2e/test_auth_guard.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// tests/e2e/test_auth_guard.js
|
||||
// Tests that protected routes redirect unauthenticated users to /login.
|
||||
|
||||
const { TARGET_URL, run } = require('./helpers');
|
||||
|
||||
run(async (page, assert) => {
|
||||
// ---- Unauthenticated access to /manage/credentials ----
|
||||
console.log('\n--- Auth guard: /manage/credentials ---');
|
||||
await page.goto(`${TARGET_URL}/manage/credentials`);
|
||||
await page.waitForURL('**/login', { timeout: 5000 });
|
||||
assert(page.url().includes('/login'), 'GET /manage/credentials redirects to /login');
|
||||
|
||||
// ---- Unauthenticated access to /manage/credentials?setup=1 ----
|
||||
console.log('\n--- Auth guard: /manage/credentials?setup=1 ---');
|
||||
await page.goto(`${TARGET_URL}/manage/credentials?setup=1`);
|
||||
await page.waitForURL('**/login', { timeout: 5000 });
|
||||
assert(page.url().includes('/login'), 'GET /manage/credentials?setup=1 redirects to /login');
|
||||
});
|
||||
75
tests/e2e/test_credentials.js
Normal file
75
tests/e2e/test_credentials.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// tests/e2e/test_credentials.js
|
||||
// Tests credential management page: structure, password set/change, validation errors.
|
||||
|
||||
const { TARGET_URL, run } = require('./helpers');
|
||||
|
||||
run(async (page, assert) => {
|
||||
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
|
||||
|
||||
// ---- Setup: Log in with dedicated credentials user ----
|
||||
console.log('\n--- Setup: login ---');
|
||||
await page.goto(`${TARGET_URL}/login`);
|
||||
await page.fill('#username', fixtures.cred_username);
|
||||
await page.fill('#password', fixtures.cred_password);
|
||||
await page.click('form[hx-post="/login/password"] button[type="submit"]');
|
||||
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
|
||||
|
||||
// ---- Page structure ----
|
||||
console.log('\n--- Credentials page structure ---');
|
||||
const title = await page.title();
|
||||
assert(title.includes('Credentials'), `Title contains "Credentials" (got: "${title}")`);
|
||||
assert(title.includes('Porchlight'), `Title contains "Porchlight" (got: "${title}")`);
|
||||
|
||||
const h1 = await page.locator('h1').textContent();
|
||||
assert(h1 === 'Credentials', `H1 says "Credentials" (got: "${h1}")`);
|
||||
|
||||
// Security keys section
|
||||
const securityKeysH2 = page.locator('h2:has-text("Security keys")');
|
||||
assert(await securityKeysH2.isVisible(), 'Security keys heading visible');
|
||||
|
||||
const registerBtn = page.locator('#webauthn-register-btn');
|
||||
assert(await registerBtn.isVisible(), 'Add security key button visible');
|
||||
|
||||
// Password section
|
||||
const passwordH2 = page.locator('h2:has-text("Password")');
|
||||
assert(await passwordH2.isVisible(), 'Password heading visible');
|
||||
|
||||
const passwordSection = page.locator('#password-section');
|
||||
assert(await passwordSection.isVisible(), 'Password section visible');
|
||||
|
||||
// ---- Password validation: mismatch ----
|
||||
console.log('\n--- Password validation: mismatch ---');
|
||||
await page.fill('#password', 'newpassword1');
|
||||
await page.fill('#confirm', 'newpassword2');
|
||||
await page.click('#password-section button[type="submit"]');
|
||||
|
||||
await page.waitForSelector('#password-section [role="alert"]', { timeout: 5000 });
|
||||
const mismatchErr = await page.locator('#password-section [role="alert"]').textContent();
|
||||
assert(
|
||||
mismatchErr.includes('do not match'),
|
||||
`Shows mismatch error (got: "${mismatchErr}")`
|
||||
);
|
||||
|
||||
// ---- Password validation: minlength enforced client-side ----
|
||||
console.log('\n--- Password validation: minlength attribute ---');
|
||||
// Reload page to clear HTMX state (the form was replaced by the error div)
|
||||
await page.goto(`${TARGET_URL}/manage/credentials`);
|
||||
const pwMinlength = await page.locator('#password').getAttribute('minlength');
|
||||
assert(pwMinlength === '8', `Password input has minlength="8" (got: "${pwMinlength}")`);
|
||||
const confirmMinlength = await page.locator('#confirm').getAttribute('minlength');
|
||||
assert(confirmMinlength === '8', `Confirm input has minlength="8" (got: "${confirmMinlength}")`);
|
||||
|
||||
// ---- Password change: success ----
|
||||
console.log('\n--- Password change: success ---');
|
||||
await page.goto(`${TARGET_URL}/manage/credentials`);
|
||||
await page.fill('#password', 'newpassword123');
|
||||
await page.fill('#confirm', 'newpassword123');
|
||||
await page.click('#password-section button[type="submit"]');
|
||||
|
||||
await page.waitForSelector('#password-section [role="status"]', { timeout: 5000 });
|
||||
const successMsg = await page.locator('#password-section [role="status"]').textContent();
|
||||
assert(
|
||||
successMsg.includes('Password updated'),
|
||||
`Shows success message (got: "${successMsg}")`
|
||||
);
|
||||
});
|
||||
84
tests/e2e/test_full_flow.js
Normal file
84
tests/e2e/test_full_flow.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// tests/e2e/test_full_flow.js
|
||||
// Full user journey: magic link registration -> set password -> logout -> login.
|
||||
|
||||
const { TARGET_URL, run } = require('./helpers');
|
||||
|
||||
run(async (page, assert) => {
|
||||
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
|
||||
assert(fixtures.register_token, 'Test fixtures loaded (register_token present)');
|
||||
|
||||
// ---- Step 1: Register via magic link ----
|
||||
console.log('\n--- Magic link registration ---');
|
||||
await page.goto(`${TARGET_URL}/register/${fixtures.register_token}`);
|
||||
|
||||
// Should redirect to /manage/credentials?setup=1
|
||||
await page.waitForURL('**/manage/credentials?setup=1', { timeout: 5000 });
|
||||
assert(
|
||||
page.url().includes('/manage/credentials'),
|
||||
`Redirected to credentials page (url: ${page.url()})`
|
||||
);
|
||||
|
||||
// Should show welcome message
|
||||
const welcome = page.locator('[role="status"]');
|
||||
assert(await welcome.isVisible(), 'Welcome/setup message is visible');
|
||||
const welcomeText = await welcome.textContent();
|
||||
assert(
|
||||
welcomeText.includes('Welcome'),
|
||||
`Welcome message shown (got: "${welcomeText}")`
|
||||
);
|
||||
|
||||
// Page title should be Porchlight
|
||||
const title = await page.title();
|
||||
assert(title.includes('Porchlight'), 'Credentials page title contains Porchlight');
|
||||
|
||||
// ---- Step 2: Set password ----
|
||||
console.log('\n--- Set password ---');
|
||||
const passwordInput = page.locator('#password');
|
||||
const confirmInput = page.locator('#confirm');
|
||||
assert(await passwordInput.isVisible(), 'Password input is visible');
|
||||
assert(await confirmInput.isVisible(), 'Confirm password input is visible');
|
||||
|
||||
await passwordInput.fill('mypassword123');
|
||||
await confirmInput.fill('mypassword123');
|
||||
await page.click('#password-section button[type="submit"]');
|
||||
|
||||
// Wait for HTMX response — password-section innerHTML gets replaced
|
||||
// The success message uses role="status"
|
||||
const successMsg = page.locator('#password-section [role="status"]');
|
||||
await successMsg.waitFor({ timeout: 5000 });
|
||||
const successText = await successMsg.textContent();
|
||||
assert(
|
||||
successText.includes('Password updated'),
|
||||
`Password set successfully (got: "${successText}")`
|
||||
);
|
||||
|
||||
// ---- Step 3: Logout ----
|
||||
console.log('\n--- Logout ---');
|
||||
// POST /logout returns an HX-Redirect header, not a standard redirect.
|
||||
// Use page.request to call it, then navigate manually.
|
||||
await page.request.post(`${TARGET_URL}/logout`);
|
||||
|
||||
// Navigate to credentials — should redirect to login since we're logged out
|
||||
await page.goto(`${TARGET_URL}/manage/credentials`);
|
||||
await page.waitForURL('**/login', { timeout: 5000 });
|
||||
assert(page.url().includes('/login'), 'Redirected to login after logout');
|
||||
|
||||
// ---- Step 4: Login with the password we just set ----
|
||||
console.log('\n--- Login with new password ---');
|
||||
await page.fill('#username', fixtures.register_username);
|
||||
await page.fill('#password', 'mypassword123');
|
||||
|
||||
// Submit via HTMX — on success, HX-Redirect header triggers redirect
|
||||
await page.click('form[hx-post="/login/password"] button[type="submit"]');
|
||||
|
||||
// Wait for redirect to credentials page
|
||||
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
|
||||
assert(
|
||||
page.url().includes('/manage/credentials'),
|
||||
`Login succeeded, redirected to credentials (url: ${page.url()})`
|
||||
);
|
||||
|
||||
// Should NOT show setup message (no ?setup=1)
|
||||
const setupMsgCount = await page.locator('[role="status"]:has-text("Welcome")').count();
|
||||
assert(setupMsgCount === 0, 'No welcome/setup message on normal login');
|
||||
});
|
||||
13
tests/e2e/test_health.js
Normal file
13
tests/e2e/test_health.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// tests/e2e/test_health.js
|
||||
// Smoke test: health endpoint returns OK.
|
||||
|
||||
const { TARGET_URL, run } = require('./helpers');
|
||||
|
||||
run(async (page, assert) => {
|
||||
console.log('\n--- Health endpoint ---');
|
||||
const resp = await page.request.get(`${TARGET_URL}/health`);
|
||||
assert(resp.ok(), `Health endpoint returns 200 (status: ${resp.status()})`);
|
||||
|
||||
const body = await resp.json();
|
||||
assert(body.status === 'ok', `Health response is {"status":"ok"} (got: ${JSON.stringify(body)})`);
|
||||
});
|
||||
|
|
@ -1,28 +1,10 @@
|
|||
// Porchlight — Login page e2e test
|
||||
//
|
||||
// Tests branding, page structure, accessibility, theme, and responsive layout.
|
||||
// Requires: TARGET_URL env var or defaults to http://localhost:8099
|
||||
//
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const TARGET_URL = process.env.TARGET_URL || 'http://localhost:8099';
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage();
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, description) {
|
||||
if (condition) {
|
||||
console.log(` PASS: ${description}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.log(` FAIL: ${description}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
const { TARGET_URL, run } = require('./helpers');
|
||||
|
||||
run(async (page, assert) => {
|
||||
// ---- Branding ----
|
||||
console.log('\n--- Branding ---');
|
||||
await page.goto(`${TARGET_URL}/login`);
|
||||
|
|
@ -134,12 +116,4 @@ const TARGET_URL = process.env.TARGET_URL || 'http://localhost:8099';
|
|||
assert(cssBody.includes('#d97706'), 'CSS contains amber accent color #d97706');
|
||||
assert(cssBody.includes('prefers-color-scheme: dark'), 'CSS contains dark mode media query');
|
||||
assert(cssBody.includes('prefers-reduced-motion'), 'CSS contains reduced motion media query');
|
||||
|
||||
// ---- Summary ----
|
||||
console.log(`\n========================================`);
|
||||
console.log(`Results: ${passed} passed, ${failed} failed`);
|
||||
console.log(`========================================\n`);
|
||||
|
||||
await browser.close();
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
})();
|
||||
});
|
||||
|
|
|
|||
63
tests/e2e/test_password_auth.js
Normal file
63
tests/e2e/test_password_auth.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// tests/e2e/test_password_auth.js
|
||||
// Tests password login error states: wrong password, nonexistent user, form validation.
|
||||
// Also tests successful login with seeded fixtures.
|
||||
|
||||
const { TARGET_URL, run } = require('./helpers');
|
||||
|
||||
run(async (page, assert) => {
|
||||
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
|
||||
|
||||
// ---- Test: Nonexistent user ----
|
||||
console.log('\n--- Login: nonexistent user ---');
|
||||
await page.goto(`${TARGET_URL}/login`);
|
||||
await page.fill('#username', 'nobody');
|
||||
await page.fill('#password', 'whatever');
|
||||
await page.click('form[hx-post="/login/password"] button[type="submit"]');
|
||||
|
||||
await page.waitForSelector('[role="alert"]', { timeout: 5000 });
|
||||
const error1 = await page.locator('[role="alert"]').textContent();
|
||||
assert(
|
||||
error1.includes('Invalid username or password'),
|
||||
`Error shown for nonexistent user (got: "${error1}")`
|
||||
);
|
||||
|
||||
// ---- Test: Wrong password for existing user ----
|
||||
console.log('\n--- Login: wrong password ---');
|
||||
await page.goto(`${TARGET_URL}/login`);
|
||||
await page.fill('#username', fixtures.login_username);
|
||||
await page.fill('#password', 'wrongpassword');
|
||||
await page.click('form[hx-post="/login/password"] button[type="submit"]');
|
||||
|
||||
await page.waitForSelector('[role="alert"]', { timeout: 5000 });
|
||||
const error2 = await page.locator('[role="alert"]').textContent();
|
||||
assert(
|
||||
error2.includes('Invalid username or password'),
|
||||
`Error shown for wrong password (got: "${error2}")`
|
||||
);
|
||||
|
||||
// ---- Test: Successful login ----
|
||||
console.log('\n--- Login: correct password ---');
|
||||
await page.goto(`${TARGET_URL}/login`);
|
||||
await page.fill('#username', fixtures.login_username);
|
||||
await page.fill('#password', fixtures.login_password);
|
||||
await page.click('form[hx-post="/login/password"] button[type="submit"]');
|
||||
|
||||
await page.waitForURL('**/manage/credentials', { timeout: 5000 });
|
||||
assert(
|
||||
page.url().includes('/manage/credentials'),
|
||||
`Successful login redirects to credentials (url: ${page.url()})`
|
||||
);
|
||||
|
||||
// ---- Test: Form validation attributes ----
|
||||
console.log('\n--- Form validation attributes ---');
|
||||
await page.goto(`${TARGET_URL}/login`);
|
||||
const usernameRequired = await page.locator('#username').getAttribute('required');
|
||||
assert(usernameRequired !== null, 'Username has required attribute');
|
||||
const passwordRequired = await page.locator('#password').getAttribute('required');
|
||||
assert(passwordRequired !== null, 'Password has required attribute');
|
||||
|
||||
const usernameAutocomplete = await page.locator('#username').getAttribute('autocomplete');
|
||||
assert(usernameAutocomplete === 'username', 'Username autocomplete is "username"');
|
||||
const passwordAutocomplete = await page.locator('#password').getAttribute('autocomplete');
|
||||
assert(passwordAutocomplete === 'current-password', 'Password autocomplete is "current-password"');
|
||||
});
|
||||
29
tests/e2e/test_registration.js
Normal file
29
tests/e2e/test_registration.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// tests/e2e/test_registration.js
|
||||
// Tests magic link registration error states: invalid token, used token.
|
||||
|
||||
const { TARGET_URL, run } = require('./helpers');
|
||||
|
||||
run(async (page, assert) => {
|
||||
const fixtures = JSON.parse(process.env.E2E_FIXTURES || '{}');
|
||||
|
||||
// ---- Invalid token ----
|
||||
console.log('\n--- Invalid registration token ---');
|
||||
const resp1 = await page.goto(`${TARGET_URL}/register/invalid-token-12345`);
|
||||
assert(resp1.status() === 400, `Invalid token returns 400 (got: ${resp1.status()})`);
|
||||
const body1 = await page.locator('body').textContent();
|
||||
assert(
|
||||
body1.includes('Invalid or expired'),
|
||||
`Shows invalid/expired message (got: "${body1.trim()}")`
|
||||
);
|
||||
|
||||
// ---- Used token ----
|
||||
console.log('\n--- Used registration token ---');
|
||||
assert(fixtures.used_token, 'Used token fixture exists');
|
||||
const resp2 = await page.goto(`${TARGET_URL}/register/${fixtures.used_token}`);
|
||||
assert(resp2.status() === 400, `Used token returns 400 (got: ${resp2.status()})`);
|
||||
const body2 = await page.locator('body').textContent();
|
||||
assert(
|
||||
body2.includes('Invalid or expired'),
|
||||
`Shows invalid/expired for used token (got: "${body2.trim()}")`
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue