Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 0 additions & 45 deletions src/test/java/org/nkcoder/TestConfig.java

This file was deleted.

21 changes: 21 additions & 0 deletions src/test/java/org/nkcoder/config/DataJpaIntegrationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.nkcoder.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.test.context.ActiveProfiles;

/** Why @EnableJpaAuditing: because @CreatedDate and @LastModifiedDate is set by JPA auditing. */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@DataJpaTest
@Import(TestContainersConfiguration.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ActiveProfiles("test")
@EnableJpaAuditing //
public @interface DataJpaIntegrationTest {}
24 changes: 24 additions & 0 deletions src/test/java/org/nkcoder/config/IntegrationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.nkcoder.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;

/**
* Note: @SpringBootTest performs full component scanning and will include the {@link
* JpaAuditingConfig} and enable JPA auditing. So don't add @EnableJPAAuditing on this annotation,
* otherwise it will be registered multiple times during AOT processing.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Import(TestContainersConfiguration.class)
@ActiveProfiles("test")
@Transactional // Auto-rollback after each test
public @interface IntegrationTest {}
45 changes: 45 additions & 0 deletions src/test/java/org/nkcoder/config/TestContainersConfiguration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.nkcoder.config;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.PostgreSQLContainer;

/**
* The @TestConfiguration with @ServiceConnection and a reusable PostgreSQL container is the modern,
* Spring Boot 3.1+ recommended pattern for integration tests. Centralized Testcontainers
* configuration for all integration tests.
*
* <p>Why this approach is recommended:
*
* <ul>
* <li><b>@TestConfiguration</b>: Spring Boot loads this only in test contexts, keeping prod code
* clean.
* <li><b>@ServiceConnection</b> (Spring Boot 3.1+): Auto-wires container connection properties
* (spring.datasource.url, username, password) without manual @DynamicPropertySource.
* <li><b>Singleton Bean</b>: Spring creates a single container instance across all test classes
* that import this config, avoiding repeated startup overhead.
* <li><b>withReuse(true)</b>: Testcontainers reuses the container across test runs (requires
* testcontainers.reuse.enable=true in ~/.testcontainers.properties). Speeds up local TDD.
* </ul>
*
* <p>Usage: Import this config in your test classes with:
*
* <pre>@Import(TestContainersConfiguration.class)</pre>
*
* or apply via a custom meta-annotation like @IntegrationTest.
*/
@TestConfiguration(proxyBeanMethods = false)
public class TestContainersConfiguration {
private static final String POSTGRES_IMAGE = "postgres:17-alpine";

@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>(POSTGRES_IMAGE)
.withDatabaseName("test-db")
.withUsername("test")
.withPassword("test")
.withReuse(true);
}
}
13 changes: 6 additions & 7 deletions src/test/java/org/nkcoder/controller/AuthControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,17 @@
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

@DisplayName("AuthController tests")
@ActiveProfiles("test")
@WebMvcTest(
value = AuthController.class,
controllers = AuthController.class,
excludeAutoConfiguration = {SecurityAutoConfiguration.class},
excludeFilters =
@ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = JwtAuthenticationFilter.class))
excludeFilters = {
@ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {JwtAuthenticationFilter.class})
})
class AuthControllerTest extends BaseControllerTest {
@MockitoBean private AuthService authService;

Expand Down
6 changes: 2 additions & 4 deletions src/test/java/org/nkcoder/controller/BaseControllerTest.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
package org.nkcoder.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.nkcoder.TestConfig;
import org.nkcoder.config.JpaAuditingConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest
@Import(TestConfig.class)
/** Base class for controller slice tests. */
@ImportAutoConfiguration(exclude = {JpaAuditingConfig.class})
@WebMvcTest
public class BaseControllerTest {
@Autowired protected ObjectMapper objectMapper;

Expand Down
10 changes: 2 additions & 8 deletions src/test/java/org/nkcoder/controller/UserControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.nkcoder.config.SecurityConfig;
import org.nkcoder.dto.user.ChangePasswordRequest;
import org.nkcoder.dto.user.UpdateProfileRequest;
import org.nkcoder.dto.user.UserResponse;
import org.nkcoder.enums.Role;
import org.nkcoder.security.JwtAuthenticationEntryPoint;
import org.nkcoder.security.JwtAuthenticationFilter;
import org.nkcoder.service.UserService;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
Expand All @@ -33,16 +31,12 @@

@DisplayName("UserController tests")
@WebMvcTest(
value = UserController.class,
controllers = UserController.class,
excludeAutoConfiguration = {SecurityAutoConfiguration.class},
excludeFilters = {
@ComponentScan.Filter(
type = FilterType.ASSIGNABLE_TYPE,
classes = {
JwtAuthenticationFilter.class,
SecurityConfig.class,
JwtAuthenticationEntryPoint.class
})
classes = {JwtAuthenticationFilter.class})
})
class UserControllerTest extends BaseControllerTest {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.nkcoder.controller;
package org.nkcoder.integration;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
Expand All @@ -12,6 +12,7 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.nkcoder.config.IntegrationTest;
import org.nkcoder.dto.auth.AuthResponse;
import org.nkcoder.dto.auth.AuthTokens;
import org.nkcoder.dto.user.UserResponse;
Expand All @@ -21,43 +22,18 @@
import org.nkcoder.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.PostgreSQLContainer;

/**
* Use @SpringBootTest to load the full context to test the authentication of endpoints.
* Because @WebMvcTest won't load SecurityConfig and JwtAuthenticationEntryPoint.
*/
@SpringBootTest
@AutoConfigureMockMvc
@IntegrationTest
@DisplayName("AuthController Security Tests")
@ActiveProfiles("test")
public class AuthControllerSecurityTest {

// Singleton container for database (needed by JPA)
static PostgreSQLContainer<?> postgres;

static {
postgres =
new PostgreSQLContainer<>("postgres:17")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
postgres.start();
}

@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
public class AuthControllerIntegrationTest {

@Autowired private MockMvc mockMvc;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,27 @@
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.nkcoder.config.IntegrationTest;
import org.springframework.boot.test.web.server.LocalServerPort;

@IntegrationTest
@DisplayName("Auth Flow Integration Tests")
class AuthFlowIntegrationTest extends BaseIntegrationTest {
class AuthFlowIntegrationTest {

@LocalServerPort private int port;

@BeforeEach
void setupRestAssured() {
RestAssured.port = port;
RestAssured.basePath = "/api/users";
}

@Nested
@DisplayName("Complete Authentication Flow")
Expand Down
53 changes: 0 additions & 53 deletions src/test/java/org/nkcoder/integration/BaseIntegrationTest.java

This file was deleted.

Loading
Loading