Set up development infrastructure for TDD: JPA + Liquibase for database migrations, Testcontainers for integration tests against real PostgreSQL, profile-based configuration (prod/local), and README deployment documentation with docker-compose example. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
360 lines
20 KiB
Markdown
360 lines
20 KiB
Markdown
---
|
|
date: 2026-03-04T19:37:59.203261+00:00
|
|
git_commit: cb0bcad145b03fec63be0ee3c1fca46ee545329e
|
|
branch: master
|
|
topic: "T-4: Development Infrastructure Setup"
|
|
tags: [research, codebase, t4, database, liquibase, testcontainers, router, test-infrastructure, docker-compose]
|
|
status: complete
|
|
---
|
|
|
|
# Research: T-4 Development Infrastructure Setup
|
|
|
|
## Research Question
|
|
|
|
What is the current state of the codebase relative to T-4's acceptance criteria? What already exists, what is missing, and what are the technical considerations for each criterion?
|
|
|
|
## Summary
|
|
|
|
T-4 is the final infrastructure task before user story implementation can begin. It bridges the gap between the existing project scaffolds (T-1, T-2, T-3, T-5 — all complete) and actual feature development with TDD. The task covers six areas: database migration framework, database connectivity, environment variable configuration, SPA router, backend test infrastructure, frontend test infrastructure, docker-compose documentation, and container verification with PostgreSQL.
|
|
|
|
The codebase already has partial coverage: Vue Router exists with placeholder routes, frontend test infrastructure (Vitest + @vue/test-utils) is operational, and backend test infrastructure (JUnit 5 + Spring Boot Test + MockMvc) is partially in place. What's missing: JPA/Liquibase, Testcontainers, environment variable wiring, and docker-compose documentation.
|
|
|
|
## Detailed Findings
|
|
|
|
### AC 1: Database Migration Framework (Liquibase)
|
|
|
|
**Current state:** Not present. No Liquibase or Liquibase dependency in `pom.xml`. No migration files anywhere in the project. The CLAUDE.md explicitly states "No JPA until T-4."
|
|
|
|
**What's needed:**
|
|
- Add `spring-boot-starter-data-jpa` dependency to `backend/pom.xml`
|
|
- Add `liquibase-core` dependency (Spring Boot manages the version)
|
|
- Create changelog directory at `backend/src/main/resources/db/changelog/`
|
|
- Create master changelog: `db.changelog-master.xml` that includes individual changesets
|
|
- Create first empty/baseline changeset to prove the tooling works
|
|
- Spring Boot auto-configures Liquibase when it's on the classpath and a datasource is available — no explicit `@Bean` config needed
|
|
|
|
**Spring Boot + Liquibase conventions:**
|
|
- Default changelog location: `classpath:db/changelog/db.changelog-master.xml`
|
|
- Format: XML (chosen for schema validation and explicitness)
|
|
- Changelogs are DB-agnostic — Liquibase generates dialect-specific SQL at runtime
|
|
- Spring Boot 3.5.x ships Liquibase via its dependency management
|
|
- Liquibase runs automatically on startup before JPA entity validation
|
|
|
|
**Architectural note:** The hexagonal architecture has an existing `adapter.out.persistence` package (currently empty, with `package-info.java`). JPA repositories and entity classes will go here. Domain model classes remain in `domain.model` without JPA annotations — the persistence adapter maps between them. The existing ArchUnit tests already enforce this boundary.
|
|
|
|
### AC 2: Database Connectivity via Environment Variables
|
|
|
|
**Current state:** `application.properties` has no datasource configuration. Only `spring.application.name=fete` and actuator settings.
|
|
|
|
**What's needed:**
|
|
- Configure Spring datasource properties to read from environment variables via profile-based separation
|
|
|
|
**Chosen approach: Profile-based separation with generic env vars.**
|
|
|
|
The properties are split across three files with clear responsibilities:
|
|
|
|
**`application.properties`** — environment-independent, always active:
|
|
```properties
|
|
spring.application.name=fete
|
|
spring.jpa.hibernate.ddl-auto=validate
|
|
spring.jpa.open-in-view=false
|
|
management.endpoints.web.exposure.include=health
|
|
management.endpoint.health.show-details=never
|
|
```
|
|
|
|
**`application-prod.properties`** — committed, production profile, activated in Docker via `ENV SPRING_PROFILES_ACTIVE=prod`:
|
|
```properties
|
|
spring.datasource.url=${DATABASE_URL}
|
|
spring.datasource.username=${DATABASE_USERNAME}
|
|
spring.datasource.password=${DATABASE_PASSWORD}
|
|
```
|
|
|
|
**`application-local.properties`** — gitignored, developer creates from `.example` template:
|
|
```properties
|
|
spring.datasource.url=jdbc:postgresql://localhost:5432/fete
|
|
spring.datasource.username=fete
|
|
spring.datasource.password=fete
|
|
```
|
|
|
|
**`application-local.properties.example`** — committed as template, never directly used.
|
|
|
|
**Dockerfile:**
|
|
```dockerfile
|
|
ENV SPRING_PROFILES_ACTIVE=prod
|
|
```
|
|
|
|
Key points:
|
|
- No datasource defaults in `application.properties` — if neither profile nor env vars are set, the app fails to start (intentional: no silent fallback to a nonexistent DB)
|
|
- Generic env var names (`DATABASE_URL`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`) — the container user never sees Spring property names
|
|
- `ddl-auto=validate` ensures Hibernate validates entities against the Liquibase-managed schema but never modifies it
|
|
- `open-in-view=false` prevents the anti-pattern of lazy-loading in views (also avoids Spring Boot's startup warning)
|
|
- PostgreSQL JDBC driver (`org.postgresql:postgresql`) is needed — Spring Boot manages the version
|
|
- Tests use `@ServiceConnection` (Testcontainers) which auto-configures the datasource — no profile or env vars needed for tests
|
|
|
|
### AC 3: All Runtime Configuration via Environment Variables
|
|
|
|
**Current state:** No environment-variable-driven configuration exists beyond Spring Boot defaults.
|
|
|
|
**What's needed beyond database:**
|
|
- Unsplash API key: optional, used by US-16
|
|
- Max active events: optional, used by US-13
|
|
|
|
**Implementation pattern:** `@ConfigurationProperties(prefix = "fete")` class (`FeteProperties`) in `de.fete.config`. Type-safe, validatable, testable.
|
|
|
|
These properties also go in `application-prod.properties` with generic env var mapping:
|
|
```properties
|
|
fete.unsplash.api-key=${UNSPLASH_API_KEY:}
|
|
fete.max-active-events=${MAX_ACTIVE_EVENTS:0}
|
|
```
|
|
|
|
Empty `UNSPLASH_API_KEY` = feature disabled. `MAX_ACTIVE_EVENTS=0` = unlimited.
|
|
|
|
**Note:** These properties are only scaffolded in T-4 (the `FeteProperties` class with fields and defaults). The business logic using them comes with US-13/US-16.
|
|
|
|
### AC 4: SPA Router Configuration
|
|
|
|
**Current state:** Vue Router IS configured and operational.
|
|
|
|
**File:** `frontend/src/router/index.ts`
|
|
- Uses `createWebHistory` (HTML5 History API — clean URLs, no hash)
|
|
- Two routes defined: `/` (HomeView, eager) and `/about` (AboutView, lazy-loaded)
|
|
- Router is registered in `main.ts` via `app.use(router)`
|
|
|
|
**Backend SPA support:** Already implemented in `WebConfig.java`:
|
|
- `PathResourceResolver` falls back to `index.html` for any path not matching a static file
|
|
- This enables client-side routing — the backend serves `index.html` for all non-API, non-static paths
|
|
|
|
**Assessment:** This AC is effectively already met. The router exists, uses history mode, and the backend supports it. What will change during user stories: routes will be added (e.g., `/event/:token`, `/event/:token/edit`), but the infrastructure is in place.
|
|
|
|
### AC 5: Backend Test Infrastructure
|
|
|
|
**Current state:** Partially in place.
|
|
|
|
**What exists:**
|
|
- JUnit 5 (via `spring-boot-starter-test`) — operational
|
|
- Spring Boot Test with `@SpringBootTest` — operational
|
|
- MockMvc for REST endpoint testing — operational (`FeteApplicationTest.java`, `WebConfigTest.java`)
|
|
- ArchUnit for architecture validation — operational (`HexagonalArchitectureTest.java`)
|
|
- Surefire configured with fail-fast (`skipAfterFailureCount=1`)
|
|
- Test logging configured (`logback-test.xml` at WARN level)
|
|
|
|
**What's missing:**
|
|
- **Testcontainers** — not in `pom.xml`, no test configuration for it
|
|
- **Integration test support with real PostgreSQL** — currently no database tests exist (because no database exists yet)
|
|
|
|
**What's needed:**
|
|
- Add `org.testcontainers:postgresql` dependency (test scope)
|
|
- Add `org.testcontainers:junit-jupiter` dependency (test scope) — JUnit 5 integration
|
|
- Add `spring-boot-testcontainers` dependency (test scope) — Spring Boot 3.1+ Testcontainers integration
|
|
- Create a test configuration class or use `@ServiceConnection` annotation (Spring Boot 3.1+) for automatic datasource wiring in tests
|
|
|
|
**Spring Boot 3.5 + Testcontainers pattern (TestApplication):**
|
|
|
|
A `TestFeteApplication.java` in `src/test/` registers Testcontainers beans. All `@SpringBootTest` tests automatically get a PostgreSQL instance — no per-test wiring needed. Existing tests (`FeteApplicationTest`, `WebConfigTest`) continue to work without modification.
|
|
|
|
```java
|
|
// src/test/java/de/fete/TestFeteApplication.java
|
|
@TestConfiguration(proxyBeanMethods = false)
|
|
public class TestcontainersConfig {
|
|
@Bean
|
|
@ServiceConnection
|
|
PostgreSQLContainer<?> postgresContainer() {
|
|
return new PostgreSQLContainer<>("postgres:17-alpine");
|
|
}
|
|
}
|
|
```
|
|
|
|
With `@ServiceConnection`, Spring Boot auto-configures the datasource to point at the Testcontainers-managed PostgreSQL — no manual URL/username/password wiring needed. Testcontainers starts one shared container per test suite run, not per test class.
|
|
|
|
**Important:** Once JPA is on the classpath, every `@SpringBootTest` needs a datasource. The TestApplication pattern ensures this globally. Without it, all existing `@SpringBootTest` tests would break immediately.
|
|
|
|
**Test categories after T-4:**
|
|
- Unit tests: no Spring context, no database — fast, test domain logic
|
|
- Integration tests: `@SpringBootTest` + Testcontainers — test full stack including database
|
|
- Architecture tests: ArchUnit — already in place
|
|
|
|
### AC 6: Frontend Test Infrastructure
|
|
|
|
**Current state:** Already in place and operational.
|
|
|
|
**What exists:**
|
|
- Vitest configured (`vitest.config.ts`): jsdom environment, bail=1, e2e excluded
|
|
- `@vue/test-utils` v2.4.6 installed — Vue component mounting and assertions
|
|
- TypeScript test config (`tsconfig.vitest.json`) with jsdom types
|
|
- Sample test exists: `components/__tests__/HelloWorld.spec.ts` — mounts component, asserts text
|
|
- Test command: `npm run test:unit` (runs Vitest in watch mode) / `npm run test:unit -- --run` (single run)
|
|
|
|
**Assessment:** This AC is already met. The test infrastructure is functional with a passing sample test.
|
|
|
|
### AC 7: Both Test Suites Executable
|
|
|
|
**Current state:** Both work.
|
|
|
|
- Backend: `cd backend && ./mvnw test` — runs JUnit 5 tests (3 tests in 3 classes)
|
|
- Frontend: `cd frontend && npm run test:unit -- --run` — runs Vitest (1 test in 1 file)
|
|
- CI pipeline (`ci.yaml`) already runs both in parallel
|
|
|
|
**Assessment:** Already met. Will remain met after adding Testcontainers (new tests use the same `./mvnw test` command).
|
|
|
|
### AC 8: README Docker-Compose Documentation
|
|
|
|
**Current state:** No docker-compose file or documentation exists. The README covers development setup and code quality but has no deployment section.
|
|
|
|
**What's needed:**
|
|
- A `docker-compose.yml` example (either in-repo or documented in README)
|
|
- Must include: app service (the fete container) + postgres service
|
|
- Must document required environment variables: `DATABASE_URL`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`
|
|
- Must document optional environment variables: `UNSPLASH_API_KEY`, `MAX_ACTIVE_EVENTS`
|
|
- Per CLAUDE.md: "A docker-compose example in the README is sufficient" — no separate file in repo
|
|
|
|
**Example structure:**
|
|
```yaml
|
|
services:
|
|
db:
|
|
image: postgres:17-alpine
|
|
environment:
|
|
POSTGRES_DB: fete
|
|
POSTGRES_USER: fete
|
|
POSTGRES_PASSWORD: changeme
|
|
volumes:
|
|
- fete-db:/var/lib/postgresql/data
|
|
|
|
app:
|
|
image: gitea.example.com/user/fete:latest
|
|
ports:
|
|
- "8080:8080"
|
|
environment:
|
|
DATABASE_URL: jdbc:postgresql://db:5432/fete
|
|
DATABASE_USERNAME: fete
|
|
DATABASE_PASSWORD: changeme
|
|
# MAX_ACTIVE_EVENTS: 100 # optional
|
|
# UNSPLASH_API_KEY: abc123 # optional
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
|
|
volumes:
|
|
fete-db:
|
|
```
|
|
|
|
### AC 9: Container Health Check with PostgreSQL
|
|
|
|
**Current state:** The Dockerfile has a HEALTHCHECK directive that queries `/actuator/health`. Currently the app starts without a database and the health check passes.
|
|
|
|
**After T-4:** With JPA and Liquibase on the classpath, Spring Boot will:
|
|
- Fail to start if no database is reachable (datasource auto-configuration fails)
|
|
- Include database health in `/actuator/health` automatically (via `DataSourceHealthIndicator`)
|
|
- Run Liquibase migrations on startup — if migrations fail, the app won't start
|
|
|
|
**What's needed for verification:**
|
|
- Start the app with docker-compose (app + postgres)
|
|
- Verify `/actuator/health` returns `{"status":"UP"}` (which now includes DB connectivity)
|
|
- Verify Liquibase ran the baseline migration (check `flyway_schema_history` table or app logs)
|
|
|
|
## Code References
|
|
|
|
### Existing Files (will be modified)
|
|
|
|
- `backend/pom.xml:1-170` — Add JPA, Liquibase, PostgreSQL driver, Testcontainers dependencies
|
|
- `backend/src/main/resources/application.properties:1-4` — Add datasource, JPA, Liquibase, app-specific config
|
|
- `README.md:1-134` — Add deployment section with docker-compose example
|
|
|
|
### Existing Files (relevant context, likely untouched)
|
|
|
|
- `backend/src/main/java/de/fete/config/WebConfig.java:1-40` — SPA routing already configured
|
|
- `backend/src/main/java/de/fete/FeteApplication.java` — Entry point, no changes needed
|
|
- `frontend/src/router/index.ts:1-23` — Router already configured
|
|
- `frontend/vitest.config.ts:1-15` — Test infra already configured
|
|
- `frontend/package.json:1-52` — Test dependencies already present
|
|
- `.gitea/workflows/ci.yaml` — CI pipeline, may need Testcontainers Docker access for backend tests
|
|
|
|
### New Files (to be created)
|
|
|
|
- `backend/src/main/resources/db/changelog/db.changelog-master.xml` — Liquibase master changelog
|
|
- `backend/src/main/resources/application-prod.properties` — Production profile with env var placeholders
|
|
- `backend/src/main/resources/application-local.properties.example` — Template for local development
|
|
- `backend/src/test/java/de/fete/TestFeteApplication.java` (or similar) — Testcontainers PostgreSQL bean via TestApplication pattern
|
|
- `de/fete/config/FeteProperties.java` — `@ConfigurationProperties` class for app-specific settings
|
|
- README deployment section — docker-compose example inline (no standalone file)
|
|
- `backend/src/main/resources/application-prod.properties` — Production profile with env var placeholders
|
|
- `backend/src/main/resources/application-local.properties.example` — Template for local development
|
|
|
|
### Package Structure (existing, will gain content)
|
|
|
|
- `de.fete.adapter.out.persistence` — JPA entities and Spring Data repositories (empty now)
|
|
- `de.fete.domain.model` — Domain entities (empty now, no JPA annotations here)
|
|
- `de.fete.config` — App configuration (WebConfig exists, may add `@ConfigurationProperties` class)
|
|
|
|
## Architecture Documentation
|
|
|
|
### Hexagonal Architecture and JPA
|
|
|
|
The existing ArchUnit tests (`HexagonalArchitectureTest.java`) enforce:
|
|
- Domain layer must not depend on Spring, adapters, application, or config
|
|
- Ports (in/out) must be interfaces
|
|
- Web adapter and persistence adapter must not cross-depend
|
|
|
|
This means JPA integration must follow the pattern:
|
|
1. Domain entities in `domain.model` — plain Java, no JPA annotations
|
|
2. JPA entities in `adapter.out.persistence` — annotated with `@Entity`, `@Table`, etc.
|
|
3. Mapping between domain and JPA entities in the persistence adapter
|
|
4. Repository interfaces (Spring Data) in `adapter.out.persistence`
|
|
5. Port interfaces in `domain.port.out` — define what the domain needs from persistence
|
|
6. Service implementations in `application.service` — use port interfaces, not repositories directly
|
|
|
|
This is a well-established hexagonal pattern. The ArchUnit tests will automatically validate any new code follows these boundaries.
|
|
|
|
### Test Architecture After T-4
|
|
|
|
```
|
|
Test Type | Context | Database | Speed | Purpose
|
|
-------------------|---------------|-----------------|---------|---------------------------
|
|
Unit tests | None | None | Fast | Domain logic, services
|
|
Integration tests | SpringBoot | Testcontainers | Medium | Full stack, DB queries
|
|
Architecture tests | None (static) | None | Fast | Structural validation
|
|
```
|
|
|
|
All test types run via `./mvnw test`. Testcontainers starts/stops PostgreSQL containers automatically — no external setup needed. The CI pipeline already has Docker available (runs on `ubuntu-latest` with Docker socket).
|
|
|
|
### CI Pipeline Considerations
|
|
|
|
The current CI pipeline runs `./mvnw -B verify` for backend tests. Testcontainers requires Docker socket access. On Gitea Actions with `ubuntu-latest` runners, Docker is typically available. If the runner uses a Docker-in-Docker setup, the Testcontainers `DOCKER_HOST` environment variable may need configuration — but this is a runtime concern, not a code concern.
|
|
|
|
### Spring Boot Profiles
|
|
|
|
Currently no profiles are configured. For T-4, a `test` profile may be useful to separate test-specific configuration (e.g., Testcontainers datasource) from production defaults. Spring Boot's `@ActiveProfiles("test")` on test classes or `application-test.properties` can handle this. However, with `@ServiceConnection`, Testcontainers auto-configures the datasource without profile-specific properties.
|
|
|
|
## Acceptance Criteria Status Matrix
|
|
|
|
| # | Criterion | Current Status | Work Required |
|
|
|---|-----------|----------------|---------------|
|
|
| 1 | Liquibase configured with first changelog | Not started | Add `liquibase-core`, create changelog dir and master XML |
|
|
| 2 | External PostgreSQL via env var | Not started | Add datasource properties with env var placeholders |
|
|
| 3 | All runtime config via env vars | Not started | Add datasource + app-specific properties |
|
|
| 4 | SPA router configured | **Done** | Vue Router with history mode already works |
|
|
| 5 | Backend test infra (Testcontainers) | Partial | JUnit 5 + MockMvc exist; add Testcontainers |
|
|
| 6 | Frontend test infra | **Done** | Vitest + @vue/test-utils operational |
|
|
| 7 | Both test suites executable | **Done** | Both `./mvnw test` and `npm run test:unit` work |
|
|
| 8 | README docker-compose documentation | Not started | Add deployment section with example |
|
|
| 9 | Container health with PostgreSQL | Not started | Verify after JPA/Liquibase are added |
|
|
|
|
## Resolved Decisions
|
|
|
|
1. **Liquibase** for database migrations, **XML** format. DB-agnostic changelogs — Liquibase generates dialect-specific SQL at runtime. XML chosen over YAML for schema validation and explicitness. The project intentionally avoids PostgreSQL-specific features in migrations to keep the database layer portable.
|
|
|
|
2. **Profile-based properties separation** with generic environment variable names. Three files: `application.properties` (environment-independent, always active), `application-prod.properties` (committed, maps `${DATABASE_URL}` etc. to Spring properties, activated in Docker via `ENV SPRING_PROFILES_ACTIVE=prod`), `application-local.properties` (gitignored, concrete local values, activated via `-Dspring-boot.run.profiles=local`). A committed `.example` template guides developers. The container user sets `DATABASE_URL`, `DATABASE_USERNAME`, `DATABASE_PASSWORD` — never sees Spring internals.
|
|
|
|
3. **`@ConfigurationProperties`** for app-specific settings (`FeteProperties` class). Type-safe, validatable, testable. Properties: `fete.unsplash.api-key` (from `UNSPLASH_API_KEY`) and `fete.max-active-events` (from `MAX_ACTIVE_EVENTS`). Both are only scaffolded in T-4; business logic using them comes with US-13/US-16.
|
|
|
|
4. **docker-compose example in README only** — no standalone `docker-compose.yml` in the repo. Per CLAUDE.md: "A docker-compose example in the README is sufficient." A local docker-compose for development may be added later separately.
|
|
|
|
5. **TestApplication pattern** for Testcontainers integration. A `TestFeteApplication.java` in `src/test/` registers a `@ServiceConnection` PostgreSQL container. All `@SpringBootTest` tests automatically get a database — existing tests continue to work without modification.
|
|
|
|
6. **README erweitern** with local development setup documentation (how to copy `application-local.properties.example`, start with profile, PostgreSQL prerequisites).
|
|
|
|
## Open Questions
|
|
|
|
1. **Testcontainers in CI:** The Gitea Actions runner needs Docker available for Testcontainers. This works out-of-the-box on `ubuntu-latest` but should be verified after implementation.
|
|
|
|
2. **Baseline changelog content:** The first Liquibase changeset should be a minimal, empty changeset that proves the tooling works. No schema needed yet — US-1 will create the first real table.
|