Files
Charon/tests/global-setup.ts
GitHub Actions 892b89fc9d feat: break-glass security reset
Implement dual-registry container publishing to both GHCR and Docker Hub
for maximum distribution reach. Add emergency security reset endpoint
("break-glass" mechanism) to recover from ACL lockout situations.

Key changes:

Docker Hub + GHCR dual publishing with Cosign signing and SBOM
Emergency reset endpoint POST /api/v1/emergency/security-reset
Token-based authentication bypasses Cerberus middleware
Rate limited (5/hour) with audit logging
30 new security enforcement E2E tests covering ACL, WAF, CrowdSec,
Rate Limiting, Security Headers, and Combined scenarios
Fixed container startup permission issue (tmpfs directory ownership)
Playwright config updated with testIgnore for browser projects
Security: Token via CHARON_EMERGENCY_TOKEN env var (32+ chars recommended)
Tests: 689 passed, 86% backend coverage, 85% frontend coverage
2026-01-25 20:14:06 +00:00

145 lines
4.8 KiB
TypeScript

/**
* Global Setup - Runs once before all tests
*
* This setup ensures a clean test environment by:
* 1. Cleaning up any orphaned test data from previous runs
* 2. Verifying the application is accessible
* 3. Performing emergency ACL reset to prevent deadlock from previous failed runs
*/
import { request, APIRequestContext } from '@playwright/test';
import { existsSync } from 'fs';
import { TestDataManager } from './utils/TestDataManager';
import { STORAGE_STATE } from './constants';
/**
* Get the base URL for the application
*/
function getBaseURL(): string {
return process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8080';
}
async function globalSetup(): Promise<void> {
console.log('\n🧹 Running global test setup...');
const baseURL = getBaseURL();
console.log(`📍 Base URL: ${baseURL}`);
// Pre-auth security reset attempt (crash protection failsafe)
// This attempts to disable security modules BEFORE auth, in case a previous run crashed
// with security enabled blocking the auth endpoint.
const preAuthContext = await request.newContext({ baseURL });
try {
await emergencySecurityReset(preAuthContext);
} catch (e) {
console.log('Pre-auth security reset skipped (may require auth)');
}
await preAuthContext.dispose();
// Create a request context
const requestContext = await request.newContext({
baseURL,
extraHTTPHeaders: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
try {
// Verify the application is accessible
console.log('🔍 Checking application health...');
const healthResponse = await requestContext.get('/api/v1/health', {
timeout: 10000,
}).catch(() => null);
if (!healthResponse || !healthResponse.ok()) {
console.warn('⚠️ Health check failed - application may not be ready');
// Try the base URL as fallback
const baseResponse = await requestContext.get('/').catch(() => null);
if (!baseResponse || !baseResponse.ok()) {
console.error('❌ Application is not accessible at', baseURL);
throw new Error(`Application not accessible at ${baseURL}`);
}
}
console.log('✅ Application is accessible');
// Clean up orphaned test data from previous runs
console.log('🗑️ Cleaning up orphaned test data...');
const cleanupResults = await TestDataManager.forceCleanupAll(requestContext);
if (
cleanupResults.proxyHosts > 0 ||
cleanupResults.accessLists > 0 ||
cleanupResults.dnsProviders > 0 ||
cleanupResults.certificates > 0
) {
console.log(' Cleaned up:');
if (cleanupResults.proxyHosts > 0) {
console.log(` - ${cleanupResults.proxyHosts} proxy hosts`);
}
if (cleanupResults.accessLists > 0) {
console.log(` - ${cleanupResults.accessLists} access lists`);
}
if (cleanupResults.dnsProviders > 0) {
console.log(` - ${cleanupResults.dnsProviders} DNS providers`);
}
if (cleanupResults.certificates > 0) {
console.log(` - ${cleanupResults.certificates} certificates`);
}
} else {
console.log(' No orphaned test data found');
}
console.log('✅ Global setup complete\n');
} catch (error) {
console.error('❌ Global setup failed:', error);
throw error;
} finally {
await requestContext.dispose();
}
// Emergency security reset with auth (more complete)
if (existsSync(STORAGE_STATE)) {
const authenticatedContext = await request.newContext({
baseURL,
storageState: STORAGE_STATE,
});
try {
await emergencySecurityReset(authenticatedContext);
console.log('✓ Authenticated security reset complete');
} catch (error) {
console.warn('⚠️ Authenticated security reset failed:', error);
}
await authenticatedContext.dispose();
} else {
console.log('⏭️ Skipping authenticated security reset (no auth state file)');
}
}
/**
* Perform emergency security reset to disable ALL security modules.
* This prevents deadlock if a previous test run left any security module enabled.
*/
async function emergencySecurityReset(requestContext: APIRequestContext): Promise<void> {
console.log('Performing emergency security reset...');
const modules = [
{ key: 'security.acl.enabled', value: 'false' },
{ key: 'security.waf.enabled', value: 'false' },
{ key: 'security.crowdsec.enabled', value: 'false' },
{ key: 'security.rate_limit.enabled', value: 'false' },
{ key: 'feature.cerberus.enabled', value: 'false' },
];
for (const { key, value } of modules) {
try {
await requestContext.post('/api/v1/settings', { data: { key, value } });
console.log(` ✓ Disabled: ${key}`);
} catch (e) {
console.log(` ⚠ Could not disable ${key}: ${e}`);
}
}
}
export default globalSetup;