test: add full-stack Playwright integration test infrastructure

Sprint 12 Phase 2: Real integration tests with seed DB
- R__seed_test_data.sql (Flyway repeatable, 7 members, strains, batches, docs, board, events)
- TestResetController (profile-gated per-test DB reset)
- docker-compose.test.yml (self-contained, tmpfs Postgres)
- Dockerfile.playwright (v1.60.0, pre-installed deps)
- 13 integration spec files, 70+ test cases (@smoke + @full)
- seed-constants.ts, selectors.ts, api-client.ts test helpers
This commit is contained in:
Patrick Plate
2026-06-18 14:43:16 +02:00
parent 6e25914074
commit 776149e7d3
25 changed files with 2127 additions and 39 deletions
@@ -0,0 +1,93 @@
package de.cannamanage.api.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
/**
* Test-only controller for resetting the database to a known seed state.
* Only active when cannamanage.test.endpoints.enabled=true (test profile).
* NEVER activate this in production.
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/test")
@RequiredArgsConstructor
@ConditionalOnProperty(name = "cannamanage.test.endpoints.enabled", havingValue = "true")
public class TestResetController {
private final DataSource dataSource;
/**
* Truncates all application tables and re-seeds with test data.
* The Flyway schema_history table is preserved.
*/
@PostMapping("/reset-db")
public ResponseEntity<Void> resetDatabase() {
log.info("Test DB reset requested — truncating all tables and re-seeding");
try (Connection conn = dataSource.getConnection()) {
truncateAllTables(conn);
reseed();
log.info("Test DB reset complete — seed data re-applied");
return ResponseEntity.ok().build();
} catch (SQLException e) {
log.error("Failed to reset test database", e);
return ResponseEntity.internalServerError().build();
}
}
private void truncateAllTables(Connection conn) throws SQLException {
List<String> tables = getApplicationTables(conn);
try (Statement stmt = conn.createStatement()) {
// Disable FK constraints for truncation
stmt.execute("SET session_replication_role = 'replica'");
for (String table : tables) {
stmt.execute("TRUNCATE TABLE " + table + " CASCADE");
log.debug("Truncated table: {}", table);
}
// Re-enable FK constraints
stmt.execute("SET session_replication_role = 'origin'");
}
}
private List<String> getApplicationTables(Connection conn) throws SQLException {
List<String> tables = new ArrayList<>();
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT tablename FROM pg_tables " +
"WHERE schemaname = 'public' " +
"AND tablename != 'flyway_schema_history'")) {
while (rs.next()) {
tables.add(rs.getString("tablename"));
}
}
return tables;
}
private void reseed() {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScript(new ClassPathResource("db/testdata/R__seed_test_data.sql"));
populator.setSeparator(";");
populator.execute(dataSource);
}
}