Files
paskia/e2e/tests/global-teardown.ts
T
LeoVasanko 84985501f5 MultiSite: one instance serves authentication across many domains (#4)
- Serve multiple domains (RP IDs) from one instance: host-based dispatch,
  per-domain credentials and sessions, domains managed at runtime in the
  admin UI — previously one RP per instance
- Cross-domain sign-in via Related Origin Requests: per-domain related-origins
  list with a served .well-known/webauthn document
- Explicit per-domain origin lists with shell-glob wildcards (**. for apex +
  any subdomain depth, *. for one level), editable in the admin UI with
  validation and self-lockout guards
- Per-domain auth hosts: the account/admin UI can live on a different host
  per domain, no longer confined to subdomains of a single RP
- CLI: 'paskia init <rp-id [rp-name]' initializes or adds a domain to an
  existing database; 'paskia migrate' converts legacy databases

BREAKING CHANGES (v2.0):
- Database schema: config is now per-domain and credentials/sessions carry
  an rp_id — existing databases must be converted with 'paskia migrate'
- Origins are now explicit: main implicitly allowed every subdomain of the
  RP; configure '**.' origins to reproduce that behavior
- CLI: the flat '--rp-id/--rp-name/--origin/--auth/--save' flags are
  replaced by the 'init' and 'migrate' subcommandsReviewed-on: #4
2026-09-07 22:14:42 +00:00

144 lines
5.1 KiB
TypeScript

import { join, dirname } from 'path'
import { existsSync, rmSync, readFileSync, readdirSync, writeFileSync } from 'fs'
import { fileURLToPath } from 'url'
import { execSync } from 'child_process'
const __dirname = dirname(fileURLToPath(import.meta.url))
const testDataDir = join(__dirname, '..', 'test-data')
const stateFile = join(testDataDir, 'test-state.json')
const projectRoot = join(__dirname, '..', '..')
const coverageDir = join(__dirname, '..', 'coverage-frontend')
// Check if coverage is enabled
const COLLECT_COVERAGE = process.env.COVERAGE === '1' || process.env.COVERAGE === 'true'
interface TestState {
resetToken?: string
serverPid?: number
}
interface CoverageEntry {
url: string
functions: Array<{
functionName: string
ranges: Array<{ count: number }>
}>
}
/**
* Global teardown for E2E tests.
*
* This cleans up the test server and optionally removes the test database.
*/
export default async function globalTeardown() {
console.log('\n🧹 Cleaning up E2E test environment...\n')
// Read state file to get server PID
if (existsSync(stateFile)) {
try {
const state: TestState = JSON.parse(readFileSync(stateFile, 'utf-8'))
if (state.serverPid) {
console.log(` Stopping server (PID: ${state.serverPid})...`)
try {
process.kill(state.serverPid, 'SIGTERM')
// Wait longer for graceful shutdown and coverage data flush
await new Promise(r => setTimeout(r, COLLECT_COVERAGE ? 2000 : 500))
} catch (err: any) {
// Process may already be dead
if (err.code !== 'ESRCH') {
console.warn(` Warning: Could not kill server: ${err.message}`)
}
}
}
} catch (err) {
console.warn(' Warning: Could not read state file')
}
// Clean up state file
rmSync(stateFile, { force: true })
}
// Clean up test database and auxiliary data
for (const name of ['paskia.kantadb', 'paskia.data']) {
const p = join(testDataDir, name)
if (existsSync(p)) {
console.log(` Removing ${name}...`)
rmSync(p, { force: true, recursive: true })
}
}
// Generate Python coverage report if coverage was collected
if (COLLECT_COVERAGE) {
console.log(' 📊 Generating Python coverage report...')
try {
// Combine parallel coverage data and generate reports
execSync('uv run coverage combine', { cwd: projectRoot, stdio: 'inherit' })
execSync('uv run coverage report', { cwd: projectRoot, stdio: 'inherit' })
execSync('uv run coverage html', { cwd: projectRoot, stdio: 'inherit' })
console.log(` ✅ Python coverage report: ${join(projectRoot, 'coverage-html', 'index.html')}\n`)
} catch (err: any) {
console.warn(` Warning: Failed to generate coverage report: ${err.message}`)
}
// Merge and report frontend coverage
if (existsSync(coverageDir)) {
try {
const files = readdirSync(coverageDir).filter(f => f.startsWith('coverage-') && f.endsWith('.json') && f !== 'coverage-merged.json')
if (files.length > 0) {
const merged: Map<string, CoverageEntry> = new Map()
for (const file of files) {
const data: CoverageEntry[] = JSON.parse(readFileSync(join(coverageDir, file), 'utf-8'))
for (const entry of data) {
const existing = merged.get(entry.url)
if (!existing) {
merged.set(entry.url, entry)
} else {
// Merge function coverage counts
for (const func of entry.functions) {
const existingFunc = existing.functions.find(f => f.functionName === func.functionName)
if (existingFunc) {
for (let i = 0; i < func.ranges.length && i < existingFunc.ranges.length; i++) {
existingFunc.ranges[i].count += func.ranges[i].count
}
} else {
existing.functions.push(func)
}
}
}
}
}
// Write merged coverage
writeFileSync(
join(coverageDir, 'coverage-merged.json'),
JSON.stringify(Array.from(merged.values()), null, 2)
)
// Generate simple coverage summary
let totalFunctions = 0
let coveredFunctions = 0
for (const entry of merged.values()) {
for (const func of entry.functions) {
totalFunctions++
const hasCoverage = func.ranges.some(r => r.count > 0)
if (hasCoverage) coveredFunctions++
}
}
const percentage = totalFunctions > 0 ? Math.round((coveredFunctions / totalFunctions) * 100) : 0
console.log(` 📊 Frontend JS Coverage: ${coveredFunctions}/${totalFunctions} functions (${percentage}%)`)
console.log(` ✅ Frontend coverage data: ${coverageDir}/coverage-merged.json\n`)
}
} catch (err: any) {
console.warn(` Warning: Failed to merge frontend coverage: ${err.message}`)
}
}
}
console.log(' ✅ Cleanup complete\n')
}