Some checks are pending
Go Benchmark / Performance Regression Check (push) Waiting to run
Cerberus Integration / Cerberus Security Stack Integration (push) Waiting to run
Upload Coverage to Codecov / Backend Codecov Upload (push) Waiting to run
Upload Coverage to Codecov / Frontend Codecov Upload (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (go) (push) Waiting to run
CodeQL - Analyze / CodeQL analysis (javascript-typescript) (push) Waiting to run
CrowdSec Integration / CrowdSec Bouncer Integration (push) Waiting to run
Docker Build, Publish & Test / build-and-push (push) Waiting to run
Docker Build, Publish & Test / Security Scan PR Image (push) Blocked by required conditions
Quality Checks / Auth Route Protection Contract (push) Waiting to run
Quality Checks / Codecov Trigger/Comment Parity Guard (push) Waiting to run
Quality Checks / Backend (Go) (push) Waiting to run
Quality Checks / Frontend (React) (push) Waiting to run
Rate Limit integration / Rate Limiting Integration (push) Waiting to run
Security Scan (PR) / Trivy Binary Scan (push) Waiting to run
Supply Chain Verification (PR) / Verify Supply Chain (push) Waiting to run
WAF integration / Coraza WAF Integration (push) Waiting to run
288 lines
9.3 KiB
TypeScript
Executable File
288 lines
9.3 KiB
TypeScript
Executable File
import { test as setup } from './fixtures/test';
|
|
import { request as playwrightRequest } from '@playwright/test';
|
|
import { STORAGE_STATE } from './constants';
|
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
import { dirname } from 'path';
|
|
import { TestDataManager } from './utils/TestDataManager';
|
|
|
|
/**
|
|
* Authentication Setup for E2E Tests
|
|
*
|
|
* This setup handles authentication before running tests:
|
|
* 1. Checks if initial setup is required
|
|
* 2. If required, creates an admin user via the setup API
|
|
* 3. Logs in and stores the auth state for reuse
|
|
*
|
|
* Environment variables:
|
|
* - E2E_TEST_EMAIL: Email for test user (default: e2e-test@example.com)
|
|
* - E2E_TEST_PASSWORD: Password for test user (default: TestPassword123!)
|
|
* - E2E_TEST_NAME: Name for test user (default: E2E Test User)
|
|
*/
|
|
|
|
const TEST_EMAIL = process.env.E2E_TEST_EMAIL || 'e2e-test@example.com';
|
|
const TEST_PASSWORD = process.env.E2E_TEST_PASSWORD || 'TestPassword123!';
|
|
const TEST_NAME = process.env.E2E_TEST_NAME || 'E2E Test User';
|
|
|
|
const EMERGENCY_TOKEN = process.env.CHARON_EMERGENCY_TOKEN;
|
|
|
|
// Re-export STORAGE_STATE for backwards compatibility with playwright.config.js
|
|
export { STORAGE_STATE };
|
|
|
|
/**
|
|
* Performs login and stores auth state
|
|
*/
|
|
async function resetAdminCredentials(baseURL: string | undefined): Promise<boolean> {
|
|
if (!baseURL || !EMERGENCY_TOKEN) {
|
|
return false;
|
|
}
|
|
|
|
const recoveryContext = await playwrightRequest.newContext({
|
|
baseURL,
|
|
extraHTTPHeaders: {
|
|
Accept: 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'X-Emergency-Token': EMERGENCY_TOKEN,
|
|
},
|
|
});
|
|
|
|
try {
|
|
const usersResponse = await recoveryContext.get('/api/v1/users');
|
|
if (!usersResponse.ok()) {
|
|
return false;
|
|
}
|
|
|
|
const users = await usersResponse.json();
|
|
const normalizedEmail = TEST_EMAIL.toLowerCase();
|
|
const existingUser = users.find((user: { email?: string }) =>
|
|
(user.email || '').toLowerCase() === normalizedEmail
|
|
);
|
|
|
|
if (!existingUser) {
|
|
const manager = new TestDataManager(recoveryContext, 'auth-credentials');
|
|
await manager.createUser(
|
|
{
|
|
name: TEST_NAME,
|
|
email: TEST_EMAIL,
|
|
password: TEST_PASSWORD,
|
|
role: 'admin',
|
|
},
|
|
{ useNamespace: false }
|
|
);
|
|
return true;
|
|
}
|
|
|
|
const updates: Record<string, unknown> = {
|
|
password: TEST_PASSWORD,
|
|
};
|
|
|
|
if (existingUser.enabled === false) {
|
|
updates.enabled = true;
|
|
}
|
|
|
|
if (existingUser.role && existingUser.role !== 'admin') {
|
|
updates.role = 'admin';
|
|
}
|
|
|
|
const updateResponse = await recoveryContext.put(`/api/v1/users/${existingUser.id}`, {
|
|
data: updates,
|
|
});
|
|
|
|
if (!updateResponse.ok()) {
|
|
const errorBody = await updateResponse.text();
|
|
throw new Error(`Credential reset failed: ${updateResponse.status()} - ${errorBody}`);
|
|
}
|
|
|
|
return true;
|
|
} catch (err) {
|
|
console.warn('⚠️ Admin credential reset failed:', err instanceof Error ? err.message : err);
|
|
return false;
|
|
} finally {
|
|
await recoveryContext.dispose();
|
|
}
|
|
}
|
|
|
|
async function recoverFromAclLockout(baseURL: string | undefined): Promise<boolean> {
|
|
if (!baseURL || !EMERGENCY_TOKEN) {
|
|
return false;
|
|
}
|
|
|
|
const emergencyURL = new URL(baseURL);
|
|
emergencyURL.port = process.env.EMERGENCY_SERVER_PORT || '2020';
|
|
|
|
const recoveryContext = await playwrightRequest.newContext({
|
|
baseURL: emergencyURL.toString(),
|
|
httpCredentials: {
|
|
username: process.env.CHARON_EMERGENCY_USERNAME || 'admin',
|
|
password: process.env.CHARON_EMERGENCY_PASSWORD || 'changeme',
|
|
},
|
|
});
|
|
|
|
try {
|
|
const response = await recoveryContext.post('/emergency/security-reset', {
|
|
headers: {
|
|
'X-Emergency-Token': EMERGENCY_TOKEN,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
data: { reason: 'Auth setup ACL lockout recovery' },
|
|
});
|
|
|
|
if (!response.ok()) {
|
|
console.warn(`⚠️ ACL lockout recovery failed with status ${response.status()}`);
|
|
return false;
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
return true;
|
|
} catch (err) {
|
|
console.warn('⚠️ ACL lockout recovery request failed:', err instanceof Error ? err.message : err);
|
|
return false;
|
|
} finally {
|
|
await recoveryContext.dispose();
|
|
}
|
|
}
|
|
|
|
async function performLoginAndSaveState(
|
|
request: APIRequestContext,
|
|
setupRequired: boolean,
|
|
baseURL: string | undefined,
|
|
retryAttempted = false
|
|
): Promise<void> {
|
|
console.log('Logging in as test user...');
|
|
const loginResponse = await request.post('/api/v1/auth/login', {
|
|
data: {
|
|
email: TEST_EMAIL,
|
|
password: TEST_PASSWORD,
|
|
},
|
|
});
|
|
|
|
if (!loginResponse.ok()) {
|
|
const status = loginResponse.status();
|
|
const errorBody = await loginResponse.text();
|
|
console.log(`Login failed: ${status} - ${errorBody}`);
|
|
|
|
if (status === 401 && !retryAttempted) {
|
|
const recovered = await resetAdminCredentials(baseURL);
|
|
if (recovered) {
|
|
console.log('Admin recovery completed, retrying login...');
|
|
return performLoginAndSaveState(request, setupRequired, baseURL, true);
|
|
}
|
|
}
|
|
|
|
if (!setupRequired) {
|
|
console.log('Login failed - existing user may have different credentials.');
|
|
console.log('Please set E2E_TEST_EMAIL and E2E_TEST_PASSWORD environment variables');
|
|
console.log('to match an existing user, or clear the database for fresh setup.');
|
|
}
|
|
throw new Error(`Login failed: ${status} - ${errorBody}`);
|
|
}
|
|
|
|
console.log('Login successful');
|
|
|
|
// Extract token from response body
|
|
const loginData = await loginResponse.json();
|
|
const token = loginData.token;
|
|
|
|
if (!token) {
|
|
throw new Error('Login response did not include a token');
|
|
}
|
|
|
|
// Store the authentication state (cookies)
|
|
await request.storageState({ path: STORAGE_STATE });
|
|
console.log(`Auth state saved to ${STORAGE_STATE}`);
|
|
|
|
// Add localStorage with token to the storage state
|
|
// This is required because the frontend checks localStorage for the token
|
|
try {
|
|
const savedState = JSON.parse(readFileSync(STORAGE_STATE, 'utf-8'));
|
|
|
|
// Add localStorage data
|
|
if (!baseURL) {
|
|
throw new Error('baseURL is required to save localStorage in storage state');
|
|
}
|
|
|
|
savedState.origins = [{
|
|
origin: baseURL,
|
|
localStorage: [
|
|
{ name: 'charon_auth_token', value: token }
|
|
]
|
|
}];
|
|
|
|
// Write updated storage state
|
|
const storageDir = dirname(STORAGE_STATE);
|
|
if (!existsSync(storageDir)) {
|
|
throw new Error(`Storage directory does not exist: ${storageDir}`);
|
|
}
|
|
|
|
writeFileSync(STORAGE_STATE, JSON.stringify(savedState, null, 2));
|
|
console.log('✅ Added auth token to localStorage in storage state');
|
|
|
|
// Verify cookie domain matches expected base URL
|
|
const cookies = savedState.cookies || [];
|
|
const authCookie = cookies.find((c: { name: string }) => c.name === 'auth_token');
|
|
|
|
if (authCookie?.domain) {
|
|
const expectedHost = new URL(baseURL).hostname;
|
|
if (authCookie.domain !== expectedHost && authCookie.domain !== `.${expectedHost}`) {
|
|
console.warn(`⚠️ Cookie domain mismatch: cookie domain "${authCookie.domain}" does not match baseURL host "${expectedHost}"`);
|
|
console.warn('TestDataManager API calls may fail with 401. Ensure PLAYWRIGHT_BASE_URL matches the configured Playwright base URL host.');
|
|
} else {
|
|
console.log(`✅ Cookie domain "${authCookie.domain}" matches baseURL host "${expectedHost}"`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn('⚠️ Could not validate storage state:', err instanceof Error ? err.message : err);
|
|
}
|
|
}
|
|
|
|
setup('authenticate', async ({ request, baseURL }) => {
|
|
// Step 1: Check if setup is required
|
|
let setupStatusResponse = await request.get('/api/v1/setup');
|
|
|
|
if (setupStatusResponse.status() === 403) {
|
|
const recovered = await recoverFromAclLockout(baseURL);
|
|
if (recovered) {
|
|
setupStatusResponse = await request.get('/api/v1/setup');
|
|
}
|
|
}
|
|
|
|
// Accept 200 (normal) or 401 (already initialized/auth required)
|
|
// Provide diagnostic info on unexpected status for actionable failures
|
|
const status = setupStatusResponse.status();
|
|
if (![200, 401].includes(status)) {
|
|
const body = await setupStatusResponse.text();
|
|
throw new Error(
|
|
`GET /api/v1/setup failed with unexpected status ${status}: ${body}\n` +
|
|
`Verify PLAYWRIGHT_BASE_URL (${baseURL}) points to a running backend.`
|
|
);
|
|
}
|
|
|
|
// If 401, setup is already complete - proceed to login
|
|
if (status === 401) {
|
|
console.log('Setup endpoint returned 401 - setup already complete, proceeding to login');
|
|
return await performLoginAndSaveState(request, false, baseURL);
|
|
}
|
|
|
|
const setupStatus = await setupStatusResponse.json();
|
|
|
|
if (setupStatus.setupRequired) {
|
|
// Step 2: Run initial setup to create admin user
|
|
console.log('Running initial setup to create test admin user...');
|
|
const setupResponse = await request.post('/api/v1/setup', {
|
|
data: {
|
|
name: TEST_NAME,
|
|
email: TEST_EMAIL,
|
|
password: TEST_PASSWORD,
|
|
},
|
|
});
|
|
|
|
if (!setupResponse.ok()) {
|
|
const errorBody = await setupResponse.text();
|
|
throw new Error(`Setup failed: ${setupResponse.status()} - ${errorBody}`);
|
|
}
|
|
console.log('Initial setup completed successfully');
|
|
}
|
|
|
|
// Step 3: Login and save auth state
|
|
await performLoginAndSaveState(request, setupStatus.setupRequired, baseURL);
|
|
});
|