enable agent to run e2e tests agaisnt fresh db

This commit is contained in:
Maxime Duchêne-Savard 2026-07-29 16:04:44 -04:00
parent d3ccae9c75
commit 446568963f
4 changed files with 57 additions and 5 deletions

View File

@ -21,3 +21,41 @@
## Tests ## Tests
Use JUnit 5 for backend unit test Use JUnit 5 for backend unit test
Use Playwright for frontend end-to-end tests 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=<backend-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:<backend-port> \
PLAYWRIGHT_BASE_URL=http://127.0.0.1:<frontend-port> \
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.

View File

@ -1,5 +1,8 @@
import { defineConfig, devices } from '@playwright/test' 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({ export default defineConfig({
testDir: './tests', testDir: './tests',
fullyParallel: false, fullyParallel: false,
@ -8,7 +11,7 @@ export default defineConfig({
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
reporter: 'html', reporter: 'html',
use: { use: {
baseURL: 'http://localhost:3000', baseURL,
trace: 'on-first-retry', trace: 'on-first-retry',
}, },
projects: [ projects: [
@ -18,8 +21,8 @@ export default defineConfig({
}, },
], ],
webServer: { webServer: {
command: 'npm run dev', command: `npm run dev -- --port ${frontendPort}`,
url: 'http://localhost:3000', url: baseURL,
reuseExistingServer: !process.env.CI, reuseExistingServer: !process.env.CI,
}, },
}) })

View File

@ -13,7 +13,7 @@ import io.javalin.http.HttpStatus
fun main() { fun main() {
val database = Database(DatabaseConfig.fromEnvironment()) val database = Database(DatabaseConfig.fromEnvironment())
database.initialize() database.initialize(reinitialize = System.getenv("DB_REINITIALIZE").toBoolean())
val users = UserRepository(database) val users = UserRepository(database)
val loginService = LoginService(users, SessionRepository(database)) val loginService = LoginService(users, SessionRepository(database))
val entities = EntityDefinitionRepository(database) val entities = EntityDefinitionRepository(database)

View File

@ -35,9 +35,15 @@ class Database(private val config: DatabaseConfig) {
} }
} }
fun initialize() { fun initialize(reinitialize: Boolean = false) {
getConnection().use { connection -> getConnection().use { connection ->
connection.createStatement().use { statement -> 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(CREATE_USERS_TABLE)
statement.execute(ADD_USER_ROLE) statement.execute(ADD_USER_ROLE)
statement.execute(CREATE_SESSIONS_TABLE) statement.execute(CREATE_SESSIONS_TABLE)
@ -67,6 +73,11 @@ class Database(private val config: DatabaseConfig) {
} }
private companion object { 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 = """ const val CREATE_USERS_TABLE = """
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,