Files
caddy-proxy-manager/tests/global-setup.ts
fuomag9 e1c97038d4 Migrate analytics from SQLite to ClickHouse
SQLite was too slow for analytical aggregations on traffic_events and
waf_events (millions of rows, GROUP BY, COUNT DISTINCT). ClickHouse is
a columnar OLAP database purpose-built for this workload.

- Add ClickHouse container to Docker Compose with health check
- Create src/lib/clickhouse/client.ts with singleton client, table DDL,
  insert helpers, and all analytics query functions
- Update log-parser.ts and waf-log-parser.ts to write to ClickHouse
- Remove purgeOldEntries — ClickHouse TTL handles 90-day retention
- Rewrite analytics-db.ts and waf-events.ts to query ClickHouse
- Remove trafficEvents/wafEvents from SQLite schema, add migration
- CLICKHOUSE_PASSWORD is required (no hardcoded default)
- Update .env.example, README, and test infrastructure

API response shapes are unchanged — no frontend modifications needed.
Parse state (file offsets) remains in SQLite.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 00:05:38 +02:00

86 lines
3.0 KiB
TypeScript

import { chromium } from '@playwright/test';
import { execFileSync } from 'node:child_process';
import { mkdirSync } from 'node:fs';
import { resolve } from 'node:path';
const COMPOSE_ARGS = [
'compose',
'-f', 'docker-compose.yml',
'-f', 'tests/docker-compose.test.yml',
];
const HEALTH_URL = 'http://localhost:3000/api/health';
export const AUTH_DIR = resolve(__dirname, '.auth');
export const AUTH_FILE = resolve(AUTH_DIR, 'admin.json');
const MAX_WAIT_MS = 180_000;
const POLL_INTERVAL_MS = 3_000;
async function waitForHealth(): Promise<void> {
const start = Date.now();
let attempt = 0;
while (Date.now() - start < MAX_WAIT_MS) {
attempt++;
try {
const res = await fetch(HEALTH_URL);
if (res.status === 200) {
console.log(`[global-setup] App is healthy (attempt ${attempt})`);
return;
}
console.log(`[global-setup] Health check attempt ${attempt}: HTTP ${res.status}, retrying...`);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
console.log(`[global-setup] Health check attempt ${attempt}: ${msg}, retrying in ${POLL_INTERVAL_MS / 1000}s...`);
}
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
}
console.error('[global-setup] Health check timed out. Container logs:');
try {
execFileSync('docker', [...COMPOSE_ARGS, 'logs', '--tail=50'], { stdio: 'inherit', cwd: process.cwd(), env: { ...process.env, CLICKHOUSE_PASSWORD: 'test-clickhouse-password-2026' } });
} catch { /* ignore */ }
throw new Error(`App did not become healthy within ${MAX_WAIT_MS}ms`);
}
async function seedAuthState(): Promise<void> {
console.log('[global-setup] Seeding auth state via browser login...');
mkdirSync(AUTH_DIR, { recursive: true });
const browser = await chromium.launch();
const page = await browser.newPage();
try {
await page.goto('http://localhost:3000/login');
await page.getByRole('textbox', { name: /username/i }).fill('testadmin');
await page.getByRole('textbox', { name: /password/i }).fill('TestPassword2026!');
await page.getByRole('button', { name: /sign in/i }).click();
// Wait for redirect away from /login
await page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 15_000 });
console.log(`[global-setup] Login succeeded, landed on: ${page.url()}`);
await page.context().storageState({ path: AUTH_FILE });
console.log('[global-setup] Auth state saved to', AUTH_FILE);
} finally {
await browser.close();
}
}
export default async function globalSetup() {
console.log('[global-setup] Starting Docker Compose test stack...');
execFileSync('docker', [
...COMPOSE_ARGS,
'up', '-d', '--build',
'--wait', '--wait-timeout', '120',
], {
stdio: 'inherit',
cwd: process.cwd(),
env: { ...process.env, CLICKHOUSE_PASSWORD: 'test-clickhouse-password-2026' },
});
console.log('[global-setup] Containers up. Waiting for /api/health...');
await waitForHealth();
await seedAuthState();
console.log('[global-setup] Done.');
}