From 446568963f43b35ed2b5a22a58f47e9738cdf642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20Duch=C3=AAne-Savard?= Date: Wed, 29 Jul 2026 16:04:44 -0400 Subject: [PATCH] enable agent to run e2e tests agaisnt fresh db --- AGENTS.md | 38 +++++++++++++++++++ frontend/playwright.config.ts | 9 +++-- src/main/kotlin/Main.kt | 2 +- .../mduchene/bolts/persistence/Database.kt | 13 ++++++- 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 04e611d..471e96a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,3 +21,41 @@ ## Tests Use JUnit 5 for backend unit test Use Playwright for frontend end-to-end tests + +## Full-stack end-to-end tests + +Agents running end-to-end tests must use free ports so they do not interfere with +an existing development server. Find two currently available ports with: + +```bash +node -e "const net=require('net');let left=2,ports=[];for(let i=0;i<2;i++){const s=net.createServer();s.listen(0,'127.0.0.1',()=>{ports.push(s.address().port);s.close(()=>{if(!--left)console.log(ports.join(' '))})})}" +``` + +Start the backend from the repository root, substituting the first free port and +the normal local database credentials: + +```bash +DB_URL=jdbc:postgresql://localhost:5432/postgres \ +DB_USERNAME=root \ +DB_PASSWORD=root \ +DB_REINITIALIZE=true \ +SERVER_PORT= \ +mvn exec:java +``` + +`DB_REINITIALIZE=true` drops and recreates the application's tables at startup, +so only use it for a disposable test database. `SERVER_PORT` defaults to `7070` +when omitted. + +From `frontend/`, run Playwright using the second free port. Playwright starts +Vite on that port, and Vite proxies API requests to the backend: + +```bash +VITE_API_TARGET=http://127.0.0.1: \ +PLAYWRIGHT_BASE_URL=http://127.0.0.1: \ +npm run test:e2e +``` + +Keep the backend running until Playwright finishes, then stop it. The free-port +check and server startup should happen close together because another process +can claim a released port in between. diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 65c9138..42eaca6 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,5 +1,8 @@ import { defineConfig, devices } from '@playwright/test' +const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:3000' +const frontendPort = new URL(baseURL).port || (baseURL.startsWith('https:') ? '443' : '80') + export default defineConfig({ testDir: './tests', fullyParallel: false, @@ -8,7 +11,7 @@ export default defineConfig({ workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { - baseURL: 'http://localhost:3000', + baseURL, trace: 'on-first-retry', }, projects: [ @@ -18,8 +21,8 @@ export default defineConfig({ }, ], webServer: { - command: 'npm run dev', - url: 'http://localhost:3000', + command: `npm run dev -- --port ${frontendPort}`, + url: baseURL, reuseExistingServer: !process.env.CI, }, }) diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index cf226d2..bc51b9e 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -13,7 +13,7 @@ import io.javalin.http.HttpStatus fun main() { val database = Database(DatabaseConfig.fromEnvironment()) - database.initialize() + database.initialize(reinitialize = System.getenv("DB_REINITIALIZE").toBoolean()) val users = UserRepository(database) val loginService = LoginService(users, SessionRepository(database)) val entities = EntityDefinitionRepository(database) diff --git a/src/main/kotlin/dev/mduchene/bolts/persistence/Database.kt b/src/main/kotlin/dev/mduchene/bolts/persistence/Database.kt index 223cd9c..28bd0db 100644 --- a/src/main/kotlin/dev/mduchene/bolts/persistence/Database.kt +++ b/src/main/kotlin/dev/mduchene/bolts/persistence/Database.kt @@ -35,9 +35,15 @@ class Database(private val config: DatabaseConfig) { } } - fun initialize() { + fun initialize(reinitialize: Boolean = false) { getConnection().use { connection -> connection.createStatement().use { statement -> + if (reinitialize) { + statement.execute(DROP_ENTITY_FIELDS_TABLE) + statement.execute(DROP_ENTITY_DEFINITIONS_TABLE) + statement.execute(DROP_SESSIONS_TABLE) + statement.execute(DROP_USERS_TABLE) + } statement.execute(CREATE_USERS_TABLE) statement.execute(ADD_USER_ROLE) statement.execute(CREATE_SESSIONS_TABLE) @@ -67,6 +73,11 @@ class Database(private val config: DatabaseConfig) { } private companion object { + const val DROP_ENTITY_FIELDS_TABLE = "DROP TABLE IF EXISTS entity_fields" + const val DROP_ENTITY_DEFINITIONS_TABLE = "DROP TABLE IF EXISTS entity_definitions" + const val DROP_SESSIONS_TABLE = "DROP TABLE IF EXISTS sessions" + const val DROP_USERS_TABLE = "DROP TABLE IF EXISTS users" + const val CREATE_USERS_TABLE = """ CREATE TABLE IF NOT EXISTS users ( id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,