diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index f5320486..f790c9b4 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -51,8 +51,7 @@ jobs: - uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0 with: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - - run: docker compose -f docker-compose.yml --profile app up --wait --build - - run: "./gradlew integrationTest --continue" + - run: "./gradlew integrationTest --continue --info" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: "gradle-integration-artifacts" diff --git a/.gitignore b/.gitignore index 3ab5f9a7..44157c1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,17 @@ +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..e0f15db2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/client/src/main/kotlin/hu/bsstudio/bssweb/config/DefaultFeignConfig.kt b/client/src/main/kotlin/hu/bsstudio/bssweb/config/DefaultFeignConfig.kt index 9ff1b9c1..7d5defd0 100644 --- a/client/src/main/kotlin/hu/bsstudio/bssweb/config/DefaultFeignConfig.kt +++ b/client/src/main/kotlin/hu/bsstudio/bssweb/config/DefaultFeignConfig.kt @@ -6,10 +6,7 @@ import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration -class DefaultFeignConfig( - @param:Value("\${bss.client.username}") val username: String, - @param:Value("\${bss.client.password}") val password: String, -) { +class DefaultFeignConfig { @Bean fun interceptor() = RequestInterceptor { template -> diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index 14615108..d07a7b6a 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -1,14 +1,14 @@ services: app: - ports: !override + ports: - "8080" postgres: - ports: !override + ports: - "5432" mock-file-api: - ports: !override + ports: - "8080" volumes: [] mock-oidc: - ports: !override + ports: - "5556" diff --git a/integration/README.md b/integration/README.md index 621cac06..d403ef8d 100644 --- a/integration/README.md +++ b/integration/README.md @@ -4,15 +4,54 @@ It stores the integration tests for the application. To run the integration tests, you need to have Docker installed on your machine. -Testcontainers didn't have a clean way to start the compose file before all the tests. -So it's required to start the compose file manually before running the tests. -Each test will clear the database tables. +The integration tests now use **Testcontainers with Docker Compose** managed by **Spring's dependency injection** to automatically start and configure the required services from your existing `docker-compose.yml`: +- PostgreSQL database +- Your Spring Boot application +- WireMock servers for external API mocking (file-api and OIDC) + +## Architecture + +The integration test setup uses Spring's proper dependency injection patterns: + +- **`TestContainerConfiguration`**: A Spring `@TestConfiguration` that manages Docker Compose as a singleton bean +- **`SharedDockerComposeContainer`**: Thread-safe singleton holder that ensures only one Docker Compose instance +- **`ContainerPropertyConfigurer`**: Spring-managed bean for configuring dynamic properties +- **`IntegrationTest`**: Base class that uses Spring's `@DynamicPropertySource` to configure properties + +### Spring Benefits + +✅ **Proper Spring patterns**: Uses `@TestConfiguration`, `@Bean`, and `@Scope("singleton")` +✅ **Dependency injection**: Container lifecycle managed by Spring's IoC container +✅ **Thread safety**: Double-checked locking pattern for safe singleton initialization +✅ **Automatic cleanup**: Spring handles bean lifecycle and cleanup +✅ **Configuration management**: Spring manages all container-related beans + +The Docker Compose services are automatically started before the first test runs and stopped after all tests complete. +Each test will clear the database tables to ensure test isolation. Each Integration test has to extend the `IntegrationTest` class. ```shell -docker compose up -d ./gradlew integrationTest -docker compose down ``` +## What's changed from manual Docker Compose + +- **Spring-managed lifecycle**: Docker Compose containers are managed as Spring beans +- **Singleton scope**: Spring ensures only one Docker Compose instance across all tests +- **Dependency injection**: Proper Spring IoC patterns instead of static objects +- **Thread-safe initialization**: Safe concurrent access to shared container instance +- **Dynamic port mapping**: Tests use the actual exposed ports from Docker Compose +- **Same service configuration**: Uses your existing `docker-compose.yml` exactly as configured +- **No manual setup required**: No need to run `docker compose up/down` manually + +## Container Configuration + +The integration tests use your existing `docker-compose.yml` services: +- **PostgreSQL 16.3**: Exact same configuration as your compose file +- **Spring Boot App**: Your actual application with all its dependencies +- **WireMock services**: File-api and OIDC mocks with the same stub mappings +- **Spring-managed cleanup**: All compose services are cleaned up by Spring's lifecycle management + +This approach ensures your integration tests run against the exact same service configuration as your development and production environments, while using proper Spring dependency injection patterns for container management. + diff --git a/integration/build.gradle.kts b/integration/build.gradle.kts index f84975aa..302e4ac3 100644 --- a/integration/build.gradle.kts +++ b/integration/build.gradle.kts @@ -13,4 +13,7 @@ dependencies { integrationTestImplementation("org.springframework.cloud:spring-cloud-starter-openfeign") integrationTestImplementation("org.springframework.boot:spring-boot-starter-data-jpa") integrationTestImplementation("org.springframework.boot:spring-boot-starter-json") + integrationTestImplementation("org.springframework.boot:spring-boot-testcontainers") + integrationTestImplementation("org.testcontainers:testcontainers") + integrationTestImplementation("org.testcontainers:junit-jupiter") } diff --git a/integration/src/integrationTest/kotlin/hu/bsstudio/bssweb/IntegrationTest.kt b/integration/src/integrationTest/kotlin/hu/bsstudio/bssweb/IntegrationTest.kt index 8f814d02..617d696b 100644 --- a/integration/src/integrationTest/kotlin/hu/bsstudio/bssweb/IntegrationTest.kt +++ b/integration/src/integrationTest/kotlin/hu/bsstudio/bssweb/IntegrationTest.kt @@ -9,22 +9,11 @@ import hu.bsstudio.bssweb.video.repository.DetailedVideoRepository import hu.bsstudio.bssweb.videocrew.repository.VideoCrewRepository import org.junit.jupiter.api.BeforeEach import org.springframework.beans.factory.annotation.Autowired -import org.springframework.test.context.TestPropertySource import org.springframework.test.context.junit.jupiter.SpringJUnitConfig -@SpringJUnitConfig(classes = [BssFeignConfig::class, DataConfig::class]) -@TestPropertySource( - properties = [ - "bss.client.url=http://localhost:8080", - "bss.client.username=user", - "bss.client.password=password", - "spring.flyway.enabled=false", - "spring.datasource.url=jdbc:postgresql://localhost:5432/bss?currentSchema=private", - "spring.datasource.username=user", - "spring.datasource.password=password", - ], -) +@SpringJUnitConfig(classes = [TestContainerConfiguration::class, BssFeignConfig::class, DataConfig::class]) open class IntegrationTest { + @Autowired protected lateinit var eventRepository: DetailedEventRepository @Autowired protected lateinit var videoRepository: DetailedVideoRepository diff --git a/integration/src/integrationTest/kotlin/hu/bsstudio/bssweb/TestContainerConfiguration.kt b/integration/src/integrationTest/kotlin/hu/bsstudio/bssweb/TestContainerConfiguration.kt new file mode 100644 index 00000000..add253cb --- /dev/null +++ b/integration/src/integrationTest/kotlin/hu/bsstudio/bssweb/TestContainerConfiguration.kt @@ -0,0 +1,37 @@ +package hu.bsstudio.bssweb + +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.test.context.DynamicPropertyRegistry +import org.springframework.test.context.DynamicPropertySource +import org.testcontainers.containers.DockerComposeContainer +import java.io.File + +@TestConfiguration +class TestContainerConfiguration { + + @Bean + fun dockerComposeContainer(): DockerComposeContainer<*> { + return DockerComposeContainer(File("../docker-compose.yml"), File("../docker-compose.ci.yml")) + .withExposedService("postgres", 5432) + .withExposedService("app", 8080) + .withOptions("--profile app") + .withBuild(true) + } + + @DynamicPropertySource + fun containerPropertyConfigurer(registry: DynamicPropertyRegistry, dockerComposeContainer: DockerComposeContainer<*>) { + val postgresHost = dockerComposeContainer.getServiceHost("postgres", 5432) + val postgresPort = dockerComposeContainer.getServicePort("postgres", 5432) + val appHost = dockerComposeContainer.getServiceHost("app", 8080) + val appPort = dockerComposeContainer.getServicePort("app", 8080) + + registry.add("spring.datasource.url") { + "jdbc:postgresql://$postgresHost:$postgresPort/bss?currentSchema=private" + } + registry.add("spring.datasource.username") { "user" } + registry.add("spring.datasource.password") { "password" } + registry.add("spring.flyway.enabled") { "false" } + registry.add("bss.client.url") { "http://$appHost:$appPort" } + } +}