You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

161 lines
4.2 KiB
TypeScript

import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const here = path.dirname(fileURLToPath(import.meta.url))
const repoRoot = path.resolve(here, '../../../../../../')
const composeFile = path.join(repoRoot, 'infrastructure/prod/docker-compose.yml')
const envFile = path.join(repoRoot, 'infrastructure/e2e/.env.e2e')
const prodEnvFile = path.join(repoRoot, 'infrastructure/prod/.env.prod')
const postgresContainer = 'knowfoolery-postgres'
const redisContainer = 'knowfoolery-redis'
const apiBaseURL = process.env.FULLSTACK_API_BASE_URL ?? 'http://127.0.0.1:18096'
function run(command: string, args: string[]): string {
return execFileSync(command, args, {
cwd: repoRoot,
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8',
}).trim()
}
async function waitForURL(url: string, timeoutMs: number): Promise<void> {
const start = Date.now()
while (Date.now() - start < timeoutMs) {
try {
const res = await fetch(url)
if (res.ok) return
} catch {
// keep waiting
}
await new Promise((resolve) => setTimeout(resolve, 1000))
}
throw new Error(`Timed out waiting for ${url}`)
}
export function sqlLiteral(value: string): string {
return `'${value.replaceAll("'", "''")}'`
}
export function queryScalar(dbName: string, sql: string): string {
return run('docker', [
'exec',
postgresContainer,
'psql',
'-U',
'knowfoolery',
'-d',
dbName,
'-t',
'-A',
'-c',
sql,
])
}
export function queryRows(dbName: string, sql: string): string {
return run('docker', [
'exec',
postgresContainer,
'psql',
'-U',
'knowfoolery',
'-d',
dbName,
'-A',
'-F',
'|',
'-c',
sql,
])
}
export function resetDatabases(): void {
queryScalar('questions', 'TRUNCATE TABLE questions RESTART IDENTITY CASCADE;')
queryScalar(
'leaderboards',
'TRUNCATE TABLE leaderboard_entries, leaderboard_player_stats RESTART IDENTITY CASCADE;'
)
run('docker', ['exec', redisContainer, 'redis-cli', 'FLUSHALL'])
}
export function seedQuestions(): void {
queryScalar(
'questions',
`
INSERT INTO questions (id, theme, text, answer, hint, difficulty, is_active, created_at, updated_at)
VALUES
('00000000-0000-0000-0000-000000000001', 'Général', 'Quel est le plus petit nombre premier ?', '2', 'Le seul nombre premier pair.', 'easy', true, NOW(), NOW()),
('00000000-0000-0000-0000-000000000002', 'Astronomie', 'Quelle planète est surnommée la planète rouge ?', 'Mars', '4e planète du système solaire.', 'easy', true, NOW(), NOW())
ON CONFLICT (id) DO NOTHING;
`
)
}
export function countActiveQuestions(): number {
return Number.parseInt(
queryScalar('questions', 'SELECT COUNT(*) FROM questions WHERE is_active = true;'),
10
)
}
export function countLeaderboardEntries(): number {
return Number.parseInt(
queryScalar('leaderboards', 'SELECT COUNT(*) FROM leaderboard_entries;'),
10
)
}
export function countLeaderboardEntriesByPlayer(playerID: string): number {
return Number.parseInt(
queryScalar(
'leaderboards',
`SELECT COUNT(*) FROM leaderboard_entries WHERE player_id = ${sqlLiteral(playerID)};`
),
10
)
}
export function questionIsActive(questionID: string): boolean {
return (
queryScalar(
'questions',
`SELECT is_active::text FROM questions WHERE id = ${sqlLiteral(questionID)};`
) === 'true'
)
}
export async function bringUpFullStack(): Promise<void> {
fs.copyFileSync(envFile, prodEnvFile)
run('docker', [
'compose',
'-f',
composeFile,
'--env-file',
envFile,
'up',
'-d',
'postgres',
'redis',
'question-bank-service',
'leaderboard-service',
'user-service',
'admin-service',
'gateway-service',
'nginx',
])
await waitForURL(`${apiBaseURL}/nginx/health`, 180_000)
await waitForURL(`${apiBaseURL}/health`, 180_000)
await waitForURL(`${apiBaseURL}/api/v1/leaderboard/top10`, 180_000)
resetDatabases()
seedQuestions()
}
export function tearDownFullStack(): void {
run('docker', ['compose', '-f', composeFile, '--env-file', envFile, 'down', '-v'])
}