diff --git a/CHANGELOG.md b/CHANGELOG.md index db9b4b28..449568a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Tests**: Application and territory seed filters assert a smaller result than the unfiltered list. `internal` on territory 2 omits `SITMUN - Externa protegida`. `internal` on application 5 returns only `Provincia A` ([#312](https://github.com/sitmun/sitmun-backend-core/issues/312)). - **Tests**: A JDBC URL `jdbc:h2:mem:testdb;MODE=NOT_A_MODE` is rejected with an SQLException that names the unknown mode. `POST /api/connections/test` returns HTTP 400 for that URL. `GET /api/connections/{id}/test` no longer repeats the same rejection ([#314](https://github.com/sitmun/sitmun-backend-core/issues/314)). - **Tests**: Database connection, role, territory, territory group type, and user repository tests use MockMvc on the shared context. They no longer boot an embedded server ([#316](https://github.com/sitmun/sitmun-backend-core/issues/316)). +- **Tests**: `UserChecksServiceTest` calls `getWarnings` without a Spring context. `JsonWebTokenServiceTest` loads only `JsonWebTokenService` with a fixed clock. `SpatialReferenceSystemTest`, `UserPositionBusinessLogicTest`, and `UserPositionRepositoryDataRestTest` reuse an existing suite context. Password reset, MIA render security, language change, task literal enrollment, and database connection tests name why their context stays unique ([#315](https://github.com/sitmun/sitmun-backend-core/issues/315)). ### Security diff --git a/src/test/java/org/sitmun/administration/controller/DatabaseConnectionControllerTest.java b/src/test/java/org/sitmun/administration/controller/DatabaseConnectionControllerTest.java index 3406d404..3cd17f85 100644 --- a/src/test/java/org/sitmun/administration/controller/DatabaseConnectionControllerTest.java +++ b/src/test/java/org/sitmun/administration/controller/DatabaseConnectionControllerTest.java @@ -13,11 +13,14 @@ import org.sitmun.domain.database.DatabaseConnection; import org.sitmun.domain.database.DatabaseConnectionRepository; import org.sitmun.test.BaseTest; +import org.sitmun.test.UniqueContext; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.bean.override.mockito.MockitoBean; @DisplayName("Database connection controller test") +@UniqueContext( + "The connection repository is mocked so GET test can load a driver class the code list would reject on insert.") class DatabaseConnectionControllerTest extends BaseTest { @MockitoBean private DatabaseConnectionRepository repository; diff --git a/src/test/java/org/sitmun/administration/controller/DefaultLanguageChangeControllerTest.java b/src/test/java/org/sitmun/administration/controller/DefaultLanguageChangeControllerTest.java index cfbe6e13..93e21534 100644 --- a/src/test/java/org/sitmun/administration/controller/DefaultLanguageChangeControllerTest.java +++ b/src/test/java/org/sitmun/administration/controller/DefaultLanguageChangeControllerTest.java @@ -17,6 +17,7 @@ import org.sitmun.administration.dto.MissingTranslationDto; import org.sitmun.administration.service.DefaultLanguageChangeService; import org.sitmun.test.BaseTest; +import org.sitmun.test.UniqueContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.http.MediaType; @@ -24,6 +25,8 @@ import org.springframework.test.web.servlet.MockMvc; @AutoConfigureMockMvc +@UniqueContext( + "The language-change service is mocked so preview and apply return a fixed body or a conflict without changing the default language.") class DefaultLanguageChangeControllerTest extends BaseTest { @Autowired private MockMvc mockMvc; diff --git a/src/test/java/org/sitmun/domain/task/TaskLiteralEnrollmentDataRestTest.java b/src/test/java/org/sitmun/domain/task/TaskLiteralEnrollmentDataRestTest.java index bbb3e508..c2b83de2 100644 --- a/src/test/java/org/sitmun/domain/task/TaskLiteralEnrollmentDataRestTest.java +++ b/src/test/java/org/sitmun/domain/task/TaskLiteralEnrollmentDataRestTest.java @@ -17,6 +17,7 @@ import org.sitmun.infrastructure.persistence.type.i18n.LiteralTranslationValueRepository; import org.sitmun.test.BaseTest; import org.sitmun.test.URIConstants; +import org.sitmun.test.UniqueContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.WithMockUser; @@ -24,6 +25,8 @@ import org.springframework.transaction.annotation.Transactional; @DisplayName("Task Data REST literal enrollment") +@UniqueContext( + "The ensure service is a spy that throws for one create and runs the real enroll path for the other.") class TaskLiteralEnrollmentDataRestTest extends BaseTest { @Autowired private TaskRepository taskRepository; diff --git a/src/test/java/org/sitmun/domain/user/UserChecksServiceTest.java b/src/test/java/org/sitmun/domain/user/UserChecksServiceTest.java index e24c6ef6..5b281239 100644 --- a/src/test/java/org/sitmun/domain/user/UserChecksServiceTest.java +++ b/src/test/java/org/sitmun/domain/user/UserChecksServiceTest.java @@ -10,28 +10,55 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import org.sitmun.domain.territory.Territory; import org.sitmun.domain.user.configuration.UserConfiguration; import org.sitmun.domain.user.configuration.UserConfigurationRepository; import org.sitmun.domain.user.position.UserPosition; import org.sitmun.domain.user.position.UserPositionRepository; import org.sitmun.infrastructure.security.core.SecurityConstants; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; -@SpringBootTest +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) @DisplayName("UserChecksService Test") class UserChecksServiceTest { - @MockitoBean private UserConfigurationRepository userConfigurationRepository; + @Mock private UserConfigurationRepository userConfigurationRepository; - @MockitoBean private UserPositionRepository userPositionRepository; + @Mock private UserPositionRepository userPositionRepository; - @Autowired private UserChecksService userChecksService; + @InjectMocks private UserChecksService userChecksService; + + @BeforeEach + void authenticateAsAdmin() { + authenticate("ADMIN"); + } + + @AfterEach + void clearAuthentication() { + SecurityContextHolder.clearContext(); + } + + private static void authenticate(String... roles) { + List authorities = new ArrayList<>(); + for (String role : roles) { + authorities.add(new SimpleGrantedAuthority("ROLE_" + role)); + } + SecurityContextHolder.getContext() + .setAuthentication(new UsernamePasswordAuthenticationToken("admin", "n/a", authorities)); + } private static User mockCheckableUser(String username, boolean passwordSet) { User user = mock(User.class); @@ -41,10 +68,9 @@ private static User mockCheckableUser(String username, boolean passwordSet) { } @Test - @WithMockUser(roles = {"USER", "PUBLIC", "PROXY"}) @DisplayName("When user is not admin, getWarnings returns null") void getWarningsWhenUserIsNotAdminReturnsNull() { - // Arrange + authenticate("USER", "PUBLIC", "PROXY"); User user = mock(User.class); // Act @@ -56,7 +82,6 @@ void getWarningsWhenUserIsNotAdminReturnsNull() { } @Test - @WithMockUser(roles = "ADMIN") @DisplayName( "When user is admin, getWarnings returns a warning if any position is missing name or organization") void getWarningsWhenPositionDetailsAreMissingAddsWarning() { @@ -89,7 +114,6 @@ void getWarningsWhenPositionDetailsAreMissingAddsWarning() { } @Test - @WithMockUser(roles = "ADMIN") @DisplayName( "When user is admin, getWarnings returns a warning if the user has no positions for a territory in an user configuration") void getWarningsWhenRoleWithoutPositionAddsWarning() { @@ -113,7 +137,6 @@ void getWarningsWhenRoleWithoutPositionAddsWarning() { } @Test - @WithMockUser(roles = "ADMIN") @DisplayName( "When user is admin, getWarnings returns a warning if the user has no user configuration") void getWarningsWhenNoUserConfigurationPresentAddsWarning() { @@ -131,7 +154,6 @@ void getWarningsWhenNoUserConfigurationPresentAddsWarning() { } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When user has no password, no-password warning is added") void getWarningsWhenNoPasswordAddsWarning() { User user = mockCheckableUser("testUser", false); @@ -146,7 +168,6 @@ void getWarningsWhenNoPasswordAddsWarning() { } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When built-in admin user has no passwordSet, no-password warning is skipped") void getWarningsWhenBuiltInAdminSkipsNoPasswordWarning() { User user = mockCheckableUser("admin", false); @@ -161,7 +182,6 @@ void getWarningsWhenBuiltInAdminSkipsNoPasswordWarning() { } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When built-in admin user has no roles, no-roles warning is skipped") void getWarningsWhenBuiltInAdminSkipsNoRolesWarning() { User user = mockCheckableUser("admin", true); @@ -176,7 +196,6 @@ void getWarningsWhenBuiltInAdminSkipsNoRolesWarning() { } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When built-in admin has roles without positions, position warnings are skipped") void getWarningsWhenBuiltInAdminSkipsRoleWithoutPositionWarning() { User user = mockCheckableUser("admin", true); @@ -196,7 +215,6 @@ void getWarningsWhenBuiltInAdminSkipsRoleWithoutPositionWarning() { } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When user is public user, role and position checks are skipped") void getWarningsWhenPublicUserSkipsPositionChecks() { // Arrange @@ -215,7 +233,6 @@ void getWarningsWhenPublicUserSkipsPositionChecks() { } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When user is public user, enforce position checks are skipped") void getWarningsWhenPublicUserSkipsEnforcePositionChecks() { // Arrange @@ -239,28 +256,24 @@ void getWarningsWhenPublicUserSkipsEnforcePositionChecks() { } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When position has blank organization, warning is added") void getWarningsWhenPositionHasBlankOrganizationAddsWarning() { assertPositionWarningWhen("Name", " "); } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When position has blank name, warning is added") void getWarningsWhenPositionHasBlankNameAddsWarning() { assertPositionWarningWhen("", "Org"); } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When position has blank email only, no position-details warning") void getWarningsWhenPositionHasBlankEmailOnlyNoPositionWarning() { assertNoPositionDetailsWarningWhen("Name", "Org", null, ""); } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When position has blank type only, no position-details warning") void getWarningsWhenPositionHasBlankTypeOnlyNoPositionWarning() { assertNoPositionDetailsWarningWhen("Name", "Org", "email@example.com", null); @@ -313,7 +326,6 @@ private void assertNoPositionDetailsWarningWhen( } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When user has multiple issues, multiple warnings are returned") void getWarningsWhenMultipleIssuesReturnsMultipleWarnings() { // Arrange @@ -349,7 +361,6 @@ void getWarningsWhenMultipleIssuesReturnsMultipleWarnings() { } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When createdDate is after expirationDate, inverted-interval warning is added") void getWarningsWhenCreatedDateAfterExpirationDateAddsInvertedIntervalWarning() { User user = mockCheckableUser("testUser", true); @@ -375,7 +386,6 @@ void getWarningsWhenCreatedDateAfterExpirationDateAddsInvertedIntervalWarning() } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When createdDate or expirationDate is null, inverted-interval warning is not added") void getWarningsWhenOpenIntervalNoInvertedWarning() { User user = mockCheckableUser("testUser", true); @@ -409,7 +419,6 @@ private static Date atStartOfDay(int dayOffset) { } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When user has valid positions, no position warnings are returned") void getWarningsWhenValidPositionsNoPositionWarnings() { // Arrange @@ -437,14 +446,12 @@ void getWarningsWhenValidPositionsNoPositionWarnings() { } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When position has name and organization only, no position-details warning") void getWarningsWhenNameAndOrganizationPresentNoPositionWarningDespiteBlankEmailAndType() { assertNoPositionDetailsWarningWhen("Valid Name", "Valid Org", null, null); } @Test - @WithMockUser(roles = "ADMIN") @DisplayName("When user has positions for all territories, no role-without-position warning") void getWarningsWhenPositionsForAllTerritoriesNoRoleWarning() { // Arrange diff --git a/src/test/java/org/sitmun/domain/user/position/UserPositionBusinessLogicTest.java b/src/test/java/org/sitmun/domain/user/position/UserPositionBusinessLogicTest.java index fa5bbf2d..1fdda481 100644 --- a/src/test/java/org/sitmun/domain/user/position/UserPositionBusinessLogicTest.java +++ b/src/test/java/org/sitmun/domain/user/position/UserPositionBusinessLogicTest.java @@ -18,28 +18,19 @@ import org.sitmun.domain.user.User; import org.sitmun.domain.user.UserRepository; import org.sitmun.domain.user.configuration.UserConfiguration; -import org.sitmun.infrastructure.persistence.type.i18n.I18nTestConfiguration; import org.sitmun.infrastructure.security.core.SecurityConstants; +import org.sitmun.test.BaseTest; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; -import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Primary; -import org.springframework.core.task.SyncTaskExecutor; -import org.springframework.core.task.TaskExecutor; +import org.springframework.transaction.annotation.Transactional; /** * Tests for UserPositionBusinessLogic. Positions for a user are updated (created) when * UserConfiguration is saved (via UserConfigurationEventHandler), not when a user is retrieved with * a projection; projections are read-only. */ -@DataJpaTest -@Import({UserPositionBusinessLogic.class, I18nTestConfiguration.class}) @DisplayName("User Position Business Logic JPA Test") -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -class UserPositionBusinessLogicTest { +@Transactional +class UserPositionBusinessLogicTest extends BaseTest { @Autowired private UserPositionBusinessLogic userPositionBusinessLogic; @@ -225,13 +216,4 @@ void nullTerritoryDoesNotThrow() { () -> userPositionBusinessLogic.createUserPositionIfNotExists(configWithNullTerritory)) .doesNotThrowAnyException(); } - - @TestConfiguration - static class Configuration { - @Bean - @Primary - TaskExecutor taskExecutor() { - return new SyncTaskExecutor(); - } - } } diff --git a/src/test/java/org/sitmun/domain/user/position/UserPositionRepositoryDataRestTest.java b/src/test/java/org/sitmun/domain/user/position/UserPositionRepositoryDataRestTest.java index 2c30bfc2..41217df3 100644 --- a/src/test/java/org/sitmun/domain/user/position/UserPositionRepositoryDataRestTest.java +++ b/src/test/java/org/sitmun/domain/user/position/UserPositionRepositoryDataRestTest.java @@ -21,23 +21,21 @@ import org.sitmun.domain.user.User; import org.sitmun.domain.user.UserRepository; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureWebMvc; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.context.WebApplicationContext; @SpringBootTest -@AutoConfigureWebMvc +@AutoConfigureMockMvc @Transactional(rollbackFor = Exception.class) @DisplayName("User Position Repository Data REST Test") -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@WithMockUser(roles = "ADMIN") class UserPositionRepositoryDataRestTest { - @Autowired private WebApplicationContext webApplicationContext; + @Autowired private MockMvc mockMvc; @Autowired private UserPositionRepository userPositionRepository; @@ -49,7 +47,6 @@ class UserPositionRepositoryDataRestTest { @Autowired private TerritoryTypeRepository territoryTypeRepository; - private MockMvc mockMvc; private User user; private Territory territory; private Role role; @@ -58,8 +55,6 @@ class UserPositionRepositoryDataRestTest { @BeforeEach void setUp() { - mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build(); - // Create test territory type territoryType = TerritoryType.builder() diff --git a/src/test/java/org/sitmun/infrastructure/persistence/type/srs/SpatialReferenceSystemTest.java b/src/test/java/org/sitmun/infrastructure/persistence/type/srs/SpatialReferenceSystemTest.java index 582234ab..cb84568c 100644 --- a/src/test/java/org/sitmun/infrastructure/persistence/type/srs/SpatialReferenceSystemTest.java +++ b/src/test/java/org/sitmun/infrastructure/persistence/type/srs/SpatialReferenceSystemTest.java @@ -1,7 +1,5 @@ package org.sitmun.infrastructure.persistence.type.srs; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -10,29 +8,15 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.sitmun.infrastructure.persistence.type.codelist.CodeListValidator; -import org.sitmun.infrastructure.persistence.type.codelist.CodeListValueRepository; import org.sitmun.test.BaseTest; import org.sitmun.test.URIConstants; -import org.springframework.context.annotation.Import; import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.bean.override.mockito.MockitoBean; @DisplayName("SpatialReferenceSystem validation test") -@Import(CodeListValidator.class) class SpatialReferenceSystemTest extends BaseTest { - @MockitoBean private CodeListValueRepository repository; - - @BeforeEach - void mocks() { - when(repository.existsByCodeListNameAndValue(any(String.class), any(String.class))) - .thenReturn(true); - } - @Test @DisplayName("POST stores a single EPSG code") @WithMockUser(roles = "ADMIN") diff --git a/src/test/java/org/sitmun/infrastructure/security/config/TemplateMoreInfoAdvancedRenderSecurityTest.java b/src/test/java/org/sitmun/infrastructure/security/config/TemplateMoreInfoAdvancedRenderSecurityTest.java index b8b8263c..aaa9ee49 100644 --- a/src/test/java/org/sitmun/infrastructure/security/config/TemplateMoreInfoAdvancedRenderSecurityTest.java +++ b/src/test/java/org/sitmun/infrastructure/security/config/TemplateMoreInfoAdvancedRenderSecurityTest.java @@ -17,6 +17,7 @@ import org.sitmun.administration.service.template.TemplateExecutionService; import org.sitmun.authentication.controller.AuthenticationController; import org.sitmun.infrastructure.security.service.JsonWebTokenService; +import org.sitmun.test.UniqueContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; @@ -31,6 +32,8 @@ */ @SpringBootTest @AutoConfigureMockMvc +@UniqueContext( + "The real filter chain stays on, and the template service is mocked so a render or execute-child call is observable without running a template.") @DisplayName("MIA render SecurityFilterChain") class TemplateMoreInfoAdvancedRenderSecurityTest { diff --git a/src/test/java/org/sitmun/infrastructure/security/password/controller/ResetPasswordControllerTest.java b/src/test/java/org/sitmun/infrastructure/security/password/controller/ResetPasswordControllerTest.java index e6c31087..4c622788 100644 --- a/src/test/java/org/sitmun/infrastructure/security/password/controller/ResetPasswordControllerTest.java +++ b/src/test/java/org/sitmun/infrastructure/security/password/controller/ResetPasswordControllerTest.java @@ -24,6 +24,7 @@ import org.sitmun.infrastructure.security.password.service.CodeOTPService; import org.sitmun.test.AdditiveActiveProfiles; import org.sitmun.test.TestUtils; +import org.sitmun.test.UniqueContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; @@ -40,6 +41,8 @@ @AutoConfigureMockMvc @Import(MockMailConfig.class) @DisplayName("Password Recovery Controller Tests") +@UniqueContext( + "Password reset asserts mail sent through the mail profile and a mock JavaMailSender.") @AdditiveActiveProfiles(Profiles.MAIL) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class ResetPasswordControllerTest { diff --git a/src/test/java/org/sitmun/infrastructure/security/service/JsonWebTokenServiceTest.java b/src/test/java/org/sitmun/infrastructure/security/service/JsonWebTokenServiceTest.java index d5a466b7..497b1762 100644 --- a/src/test/java/org/sitmun/infrastructure/security/service/JsonWebTokenServiceTest.java +++ b/src/test/java/org/sitmun/infrastructure/security/service/JsonWebTokenServiceTest.java @@ -20,29 +20,29 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.sitmun.infrastructure.config.Profiles; -import org.sitmun.test.AdditiveActiveProfiles; +import org.sitmun.test.UniqueContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Primary; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; -@SpringBootTest -@AdditiveActiveProfiles(Profiles.TEST) -@Import(JsonWebTokenServiceTest.FixedClockConfig.class) +@SpringBootTest( + classes = JsonWebTokenServiceTest.FixedClockConfig.class, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +@UniqueContext( + "A fixed clock must not replace the suite clock. The context loads only JsonWebTokenService.") @DisplayName("JsonWebTokenService Tests") class JsonWebTokenServiceTest { private static final Instant FIXED_NOW = Instant.parse("2026-07-18T12:00:00Z"); - @TestConfiguration + @Configuration + @Import(JsonWebTokenService.class) static class FixedClockConfig { @Bean - @Primary Clock fixedClock() { return Clock.fixed(FIXED_NOW, ZoneOffset.UTC); } diff --git a/src/test/java/org/sitmun/test/UniqueContext.java b/src/test/java/org/sitmun/test/UniqueContext.java new file mode 100644 index 00000000..9da429be --- /dev/null +++ b/src/test/java/org/sitmun/test/UniqueContext.java @@ -0,0 +1,15 @@ +package org.sitmun.test; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Names why this class cannot share a suite Spring test context. */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface UniqueContext { + String value(); +} diff --git a/src/test/java/org/sitmun/test/UniqueSpringContextTest.java b/src/test/java/org/sitmun/test/UniqueSpringContextTest.java new file mode 100644 index 00000000..bff932a7 --- /dev/null +++ b/src/test/java/org/sitmun/test/UniqueSpringContextTest.java @@ -0,0 +1,101 @@ +package org.sitmun.test; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.sitmun.Application; +import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.BootstrapUtils; +import org.springframework.test.context.MergedContextConfiguration; + +class UniqueSpringContextTest { + + private static final String USER_POSITION_REPOSITORY = + "org.sitmun.domain.user.position.UserPositionRepositoryDataRestTest"; + private static final String USER_REPOSITORY = "org.sitmun.domain.user.UserRepositoryDataRestTest"; + private static final String SPATIAL_REFERENCE = + "org.sitmun.infrastructure.persistence.type.srs.SpatialReferenceSystemTest"; + private static final String EMAIL = "org.sitmun.infrastructure.persistence.type.basic.EmailTest"; + private static final String USER_POSITION_LOGIC = + "org.sitmun.domain.user.position.UserPositionBusinessLogicTest"; + private static final String USER_CHECKS = "org.sitmun.domain.user.UserChecksServiceTest"; + private static final String JWT = + "org.sitmun.infrastructure.security.service.JsonWebTokenServiceTest"; + private static final String RESET_PASSWORD = + "org.sitmun.infrastructure.security.password.controller.ResetPasswordControllerTest"; + private static final String TEMPLATE_SECURITY = + "org.sitmun.infrastructure.security.config.TemplateMoreInfoAdvancedRenderSecurityTest"; + private static final String LANGUAGE = + "org.sitmun.administration.controller.DefaultLanguageChangeControllerTest"; + private static final String TASK_LITERAL = + "org.sitmun.domain.task.TaskLiteralEnrollmentDataRestTest"; + private static final String DATABASE_CONNECTION = + "org.sitmun.administration.controller.DatabaseConnectionControllerTest"; + private static final String CODE_LIST = + "org.sitmun.infrastructure.persistence.type.codelist.CodeListTest"; + + @Test + void userPositionRepositorySharesTheDataRestContext() { + assertThat(key(USER_POSITION_REPOSITORY)).isEqualTo(key(USER_REPOSITORY)); + } + + @Test + void spatialReferenceSystemSharesTheBaseContext() { + assertThat(key(SPATIAL_REFERENCE)).isEqualTo(key(EMAIL)); + } + + @Test + void userPositionBusinessLogicSharesTheBaseContext() { + assertThat(key(USER_POSITION_LOGIC)).isEqualTo(key(EMAIL)); + } + + @Test + void userChecksServiceDoesNotBootAContext() { + Class testClass = type(USER_CHECKS); + assertThat(testClass.getAnnotation(SpringBootTest.class)).isNull(); + assertThat(testClass.getAnnotation(DataJpaTest.class)).isNull(); + } + + @Test + void jsonWebTokenServiceDoesNotBootTheApplication() { + assertThat(key(JWT).getClasses()).doesNotContain(Application.class); + assertThat(reason(JWT)).contains("clock"); + } + + @Test + void listedUniqueContextsNameTheBehavior() { + assertThat(reason(RESET_PASSWORD)).containsIgnoringCase("mail"); + assertThat(reason(TEMPLATE_SECURITY)).containsIgnoringCase("template"); + assertThat(reason(LANGUAGE)).containsIgnoringCase("language"); + assertThat(reason(TASK_LITERAL)).containsIgnoringCase("spy"); + assertThat(reason(DATABASE_CONNECTION)).containsIgnoringCase("driver"); + } + + @Test + void codeListStaysOnBaseTest() { + Class testClass = type(CODE_LIST); + assertThat(testClass.getSuperclass()).isEqualTo(BaseTest.class); + assertThat(testClass.getAnnotation(UniqueContext.class)).isNull(); + } + + private static MergedContextConfiguration key(String className) { + return BootstrapUtils.resolveTestContextBootstrapper(type(className)) + .buildMergedContextConfiguration(); + } + + private static String reason(String className) { + Class testClass = type(className); + UniqueContext marker = testClass.getAnnotation(UniqueContext.class); + assertThat(marker).as(testClass.getSimpleName()).isNotNull(); + return marker.value(); + } + + private static Class type(String className) { + try { + return Class.forName(className); + } catch (ClassNotFoundException exception) { + throw new IllegalStateException(exception); + } + } +}