test: one test container and one context per test kind
Build, Test & Push Auth Service / quality-check (push) Successful in 43s
Build, Test & Push Auth Service / docker (push) Successful in 1m29s

This commit is contained in:
lobo committed 2026-09-30 13:41:57 +02:00
1 parent 1f94c87a20
commit 3336181cdb
20 files changed
+65 -130

No files matched your search

+3 -1
View File
@@ -153,7 +153,9 @@ use each other.
ArchUnit enforces these rules in `src/test/java/auth_service/architecture/ArchitectureTest.java`, so a layer ArchUnit enforces these rules in `src/test/java/auth_service/architecture/ArchitectureTest.java`, so a layer
violation fails the build. It also keeps the modules independent of each other and free of cycles. Test base violation fails the build. It also keeps the modules independent of each other and free of cycles. Test base
classes (`AbstractUnitTest`, `AbstractWebMvcTest`, `AbstractPersistenceTest`, `AbstractIntegrationTest`) live in classes (`AbstractUnitTest`, `AbstractWebMvcTest`, `AbstractPersistenceTest`, `AbstractIntegrationTest`) live in
`auth_service.shared.test`; all integration tests share one PostgreSQL container. `auth_service.shared.test`. Integration and persistence tests share one PostgreSQL container (`TestPostgres`), and
the base classes carry every annotation, so each kind starts one Spring context. A test with its own annotations
starts another one.
### Login flow ### Login flow
@@ -1,13 +0,0 @@
package auth_service;
import auth_service.shared.test.AbstractIntegrationTest;
import org.junit.jupiter.api.Test;
class AuthServiceApplicationTests extends AbstractIntegrationTest {
@Test
void contextLoads() {
// If the context fails to load (e.g. Flyway migration fails or beans are missing),
// this test will fail automatically. No explicit assertions needed.
}
}
@@ -1,66 +0,0 @@
package auth_service.configuration.security;
import auth_service.shared.api.AuthCookies;
import auth_service.shared.test.AbstractIntegrationTest;
import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Every access token from this context is already expired when it is issued.
*/
@AutoConfigureMockMvc
@TestPropertySource(properties = "app.auth.access-token-ttl=-1s")
class ExpiredAccessTokenTest extends AbstractIntegrationTest {
private static final String EMAIL = "review-findings@example.com";
private static final String PASSWORD = "correct horse battery";
@Autowired
private MockMvc mockMvc;
@Autowired
private JdbcTemplate jdbc;
@Autowired
private PasswordEncoder passwordEncoder;
@BeforeEach
void seedUser() {
jdbc.update("DELETE FROM users WHERE email = ?", EMAIL);
jdbc.update("INSERT INTO users (email, password, enabled, steam_id) VALUES (?, ?, true, NULL)",
EMAIL, passwordEncoder.encode(PASSWORD));
addSettingsRows();
}
private String expiredAccessCookie() throws Exception {
Cookie cookie = mockMvc.perform(post("/api/auth/login")
.header(ClientHeaderFilter.CLIENT_HEADER, "web")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"email\":\"" + EMAIL + "\",\"password\":\"" + PASSWORD + "\"}"))
.andExpect(status().isOk())
.andReturn().getResponse().getCookie(AuthCookies.ACCESS_COOKIE);
assertNotNull(cookie);
return cookie.getValue();
}
@Test
void expiredAccessCookie_Is401SoTheFrontendRefreshes() throws Exception {
String expired = expiredAccessCookie();
mockMvc.perform(get("/api/users/me").cookie(new Cookie(AuthCookies.ACCESS_COOKIE, expired)))
.andExpect(status().isUnauthorized());
}
}
@@ -3,7 +3,6 @@ package auth_service.configuration.security;
import auth_service.shared.test.AbstractIntegrationTest; import auth_service.shared.test.AbstractIntegrationTest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import java.util.Set; import java.util.Set;
@@ -14,7 +13,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@AutoConfigureMockMvc
class PublicEndpointsTest extends AbstractIntegrationTest { class PublicEndpointsTest extends AbstractIntegrationTest {
@Autowired @Autowired
@@ -10,7 +10,6 @@ import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -27,7 +26,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
/** /**
* The role markers through the real filter chain, on a probe that only this test sees. * The role markers through the real filter chain, on a probe that only this test sees.
*/ */
@AutoConfigureMockMvc
@Import({RoleCheckFlowTest.ProbeService.class, RoleCheckFlowTest.ProbeController.class}) @Import({RoleCheckFlowTest.ProbeService.class, RoleCheckFlowTest.ProbeController.class})
class RoleCheckFlowTest extends AbstractIntegrationTest { class RoleCheckFlowTest extends AbstractIntegrationTest {
@@ -8,7 +8,6 @@ import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
@@ -38,7 +37,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* The whole cookie session against a real database: login, refresh rotation, reuse detection, logout, the CSRF * The whole cookie session against a real database: login, refresh rotation, reuse detection, logout, the CSRF
* header and CORS. * header and CORS.
*/ */
@AutoConfigureMockMvc
class SessionFlowTest extends AbstractIntegrationTest { class SessionFlowTest extends AbstractIntegrationTest {
private static final String EMAIL = "session-flow@example.com"; private static final String EMAIL = "session-flow@example.com";
@@ -98,6 +96,14 @@ class SessionFlowTest extends AbstractIntegrationTest {
.andExpect(jsonPath("$.email").value(EMAIL)); .andExpect(jsonPath("$.email").value(EMAIL));
} }
@Test
void expiredAccessCookie_Is401SoTheFrontendRefreshes() throws Exception {
String expired = expiredCopy(cookie(login(), AuthCookies.ACCESS_COOKIE));
mockMvc.perform(get("/api/users/me").cookie(new Cookie(AuthCookies.ACCESS_COOKIE, expired)))
.andExpect(status().isUnauthorized());
}
@Test @Test
void noLogin_Is401() throws Exception { void noLogin_Is401() throws Exception {
mockMvc.perform(get("/api/users/me")) mockMvc.perform(get("/api/users/me"))
@@ -7,7 +7,6 @@ import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
@@ -35,7 +34,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
/** /**
* "Sign in through Steam" through the whole filter chain and a real database; only Steam itself is mocked. * "Sign in through Steam" through the whole filter chain and a real database; only Steam itself is mocked.
*/ */
@AutoConfigureMockMvc
class SteamLinkFlowTest extends AbstractIntegrationTest { class SteamLinkFlowTest extends AbstractIntegrationTest {
private static final String EMAIL = "steam-link@example.com"; private static final String EMAIL = "steam-link@example.com";
@@ -10,7 +10,6 @@ import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
@@ -30,7 +29,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* Delete my account against a real database. Core is not reachable in tests, so this also proves that a failed * Delete my account against a real database. Core is not reachable in tests, so this also proves that a failed
* mark in Core does not fail the delete. * mark in Core does not fail the delete.
*/ */
@AutoConfigureMockMvc
class DeletedAccountFlowTest extends AbstractIntegrationTest { class DeletedAccountFlowTest extends AbstractIntegrationTest {
private static final String EMAIL = "delete-flow@example.com"; private static final String EMAIL = "delete-flow@example.com";
@@ -4,7 +4,6 @@ import auth_service.shared.test.AbstractPersistenceTest;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import java.sql.Timestamp; import java.sql.Timestamp;
@@ -15,7 +14,6 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@Import(AccountRepository.class)
class AccountRepositoryTest extends AbstractPersistenceTest { class AccountRepositoryTest extends AbstractPersistenceTest {
private static final Instant NOW = Instant.now().truncatedTo(ChronoUnit.SECONDS); private static final Instant NOW = Instant.now().truncatedTo(ChronoUnit.SECONDS);
@@ -45,6 +43,8 @@ class AccountRepositoryTest extends AbstractPersistenceTest {
@BeforeEach @BeforeEach
void seed() { void seed() {
// Integration tests share the database and commit their rows.
jdbc.update("DELETE FROM users");
alice = insertUser("alice@example.com"); alice = insertUser("alice@example.com");
bob = insertUser("bob@example.com"); bob = insertUser("bob@example.com");
} }
@@ -7,7 +7,6 @@ import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
@@ -26,7 +25,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
/** /**
* Roles, "view as" and the admin users API against a real database. * Roles, "view as" and the admin users API against a real database.
*/ */
@AutoConfigureMockMvc
class AdminFlowTest extends AbstractIntegrationTest { class AdminFlowTest extends AbstractIntegrationTest {
private static final String PASSWORD = "correct horse battery"; private static final String PASSWORD = "correct horse battery";
@@ -6,14 +6,12 @@ import auth_service.shared.test.AbstractPersistenceTest;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import java.time.Instant; import java.time.Instant;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@Import(AdminUserRepository.class)
class AdminUserRepositoryTest extends AbstractPersistenceTest { class AdminUserRepositoryTest extends AbstractPersistenceTest {
@Autowired @Autowired
@@ -7,7 +7,6 @@ import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@@ -24,7 +23,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@AutoConfigureMockMvc
class HintFlowTest extends AbstractIntegrationTest { class HintFlowTest extends AbstractIntegrationTest {
private static final String PASSWORD = "correct horse battery"; private static final String PASSWORD = "correct horse battery";
@@ -9,7 +9,6 @@ import auth_service.shared.test.AbstractPersistenceTest;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import java.time.Duration; import java.time.Duration;
@@ -19,7 +18,6 @@ import java.time.temporal.ChronoUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple; import static org.assertj.core.api.Assertions.tuple;
@Import(HintRepository.class)
class HintRepositoryTest extends AbstractPersistenceTest { class HintRepositoryTest extends AbstractPersistenceTest {
private static final Instant NOW = Instant.now().truncatedTo(ChronoUnit.MILLIS); private static final Instant NOW = Instant.now().truncatedTo(ChronoUnit.MILLIS);
@@ -5,7 +5,6 @@ import auth_service.shared.security.Role;
import auth_service.shared.test.AbstractPersistenceTest; import auth_service.shared.test.AbstractPersistenceTest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -15,7 +14,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@Import(MinimalUserRepositoryImpl.class)
class MinimalUserRepositoryTest extends AbstractPersistenceTest { class MinimalUserRepositoryTest extends AbstractPersistenceTest {
@Autowired @Autowired
@@ -9,7 +9,6 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
@@ -25,7 +24,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@AutoConfigureMockMvc
class PublicProfileControllerTest extends AbstractIntegrationTest { class PublicProfileControllerTest extends AbstractIntegrationTest {
@Autowired @Autowired
@@ -4,7 +4,6 @@ import auth_service.modules.registration.model.Registration;
import auth_service.shared.test.AbstractPersistenceTest; import auth_service.shared.test.AbstractPersistenceTest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
@@ -15,7 +14,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@Import(RegisteredUserRepositoryImpl.class)
class RegisteredUserRepositoryTest extends AbstractPersistenceTest { class RegisteredUserRepositoryTest extends AbstractPersistenceTest {
@Autowired @Autowired
@@ -38,9 +36,9 @@ class RegisteredUserRepositoryTest extends AbstractPersistenceTest {
@Test @Test
void existsByDisplayName_IgnoresCase() { void existsByDisplayName_IgnoresCase() {
registeredUserRepository.save(registration("named@example.com", "Lobo")); registeredUserRepository.save(registration("named@example.com", "RegisteredName"));
assertTrue(registeredUserRepository.existsByDisplayName("lobo")); assertTrue(registeredUserRepository.existsByDisplayName("registeredname"));
assertFalse(registeredUserRepository.existsByDisplayName("Wolf")); assertFalse(registeredUserRepository.existsByDisplayName("Wolf"));
} }
@@ -12,7 +12,6 @@ import auth_service.shared.test.AbstractPersistenceTest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.jpa.test.autoconfigure.TestEntityManager; import org.springframework.boot.jpa.test.autoconfigure.TestEntityManager;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -21,7 +20,6 @@ import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@Import(UserRepositoryImpl.class)
class UserRepositoryTest extends AbstractPersistenceTest { class UserRepositoryTest extends AbstractPersistenceTest {
@Autowired @Autowired
@@ -224,13 +222,13 @@ class UserRepositoryTest extends AbstractPersistenceTest {
var settings = UserSettings.builder().language("en").theme("light").build(); var settings = UserSettings.builder().language("en").theme("light").build();
// Act // Act
updateSettings(owner.getId(), "Lobo", settings); updateSettings(owner.getId(), "RepoName", settings);
entityManager.flush(); entityManager.flush();
// Assert // Assert
assertEquals("Lobo", findByEmail("owner2@example.com").orElseThrow().getDisplayName()); assertEquals("RepoName", findByEmail("owner2@example.com").orElseThrow().getDisplayName());
assertFalse(userRepository.isDisplayNameUsedByOtherUser(owner.getId(), "lobo")); assertFalse(userRepository.isDisplayNameUsedByOtherUser(owner.getId(), "reponame"));
assertTrue(userRepository.isDisplayNameUsedByOtherUser(other.getId(), "LOBO")); assertTrue(userRepository.isDisplayNameUsedByOtherUser(other.getId(), "REPONAME"));
assertFalse(userRepository.isDisplayNameUsedByOtherUser(other.getId(), "Wolf")); assertFalse(userRepository.isDisplayNameUsedByOtherUser(other.getId(), "Wolf"));
} }
@@ -243,7 +241,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
.registrationDate(LocalDateTime.now()) .registrationDate(LocalDateTime.now())
.build()); .build());
updateSettings(userEntity.getId(), "Lobo", updateSettings(userEntity.getId(), "RepoName",
UserSettings.builder().language("de").theme("dark").publicProfile(true).build()); UserSettings.builder().language("de").theme("dark").publicProfile(true).build());
entityManager.flush(); entityManager.flush();
entityManager.clear(); entityManager.clear();
@@ -1,8 +1,12 @@
package auth_service.shared.test; package auth_service.shared.test;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tag;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertyRegistry;
@@ -13,25 +17,21 @@ import java.security.KeyPair;
import java.security.KeyPairGenerator; import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Base64; import java.util.Base64;
import java.util.Date;
/** /**
* Base class for integration tests. * Base class for integration tests.
* Full Spring context with Testcontainers. * Full Spring context with Testcontainers.
*/ */
@Tag("IntegrationTest") @Tag("IntegrationTest")
@AutoConfigureMockMvc
// The account cleanup must not run by itself, and Core is never reachable in tests. // The account cleanup must not run by itself, and Core is never reachable in tests.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {"app.account-cleanup.cron=-", "app.core.internal-url=http://127.0.0.1:1"}) properties = {"app.account-cleanup.cron=-", "app.core.internal-url=http://127.0.0.1:1"})
public abstract class AbstractIntegrationTest { public abstract class AbstractIntegrationTest {
// One container per JVM, not per class: Spring caches the context across test classes,
// and a per-class container would be stopped under the cached context.
@ServiceConnection @ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17-alpine"); static PostgreSQLContainer<?> postgres = TestPostgres.CONTAINER;
static {
postgres.start();
}
@Autowired @Autowired
private JdbcTemplate baseJdbc; private JdbcTemplate baseJdbc;
@@ -54,6 +54,19 @@ public abstract class AbstractIntegrationTest {
registry.add("auth.registration.invitation-code", () -> "test-invite-code"); registry.add("auth.registration.invitation-code", () -> "test-invite-code");
} }
/**
* The same token, signed again with an expiry in the past.
*/
protected static String expiredCopy(String token) {
Jws<Claims> jws = Jwts.parser().verifyWith(KEYS.getPublic()).build().parseSignedClaims(token);
return Jwts.builder()
.header().keyId(jws.getHeader().getKeyId()).and()
.claims(jws.getPayload())
.expiration(new Date(System.currentTimeMillis() - 1000))
.signWith(KEYS.getPrivate(), Jwts.SIG.EdDSA)
.compact();
}
private static KeyPair generateKeys() { private static KeyPair generateKeys() {
try { try {
return KeyPairGenerator.getInstance("Ed25519").generateKeyPair(); return KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
@@ -4,6 +4,9 @@ import org.junit.jupiter.api.Tag;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest; import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Repository;
import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.containers.PostgreSQLContainer;
/** /**
@@ -11,16 +14,11 @@ import org.testcontainers.containers.PostgreSQLContainer;
* Slices only the persistence layer with Testcontainers. * Slices only the persistence layer with Testcontainers.
*/ */
@Tag("PersistenceTest") @Tag("PersistenceTest")
@DataJpaTest // Every @Repository, so all persistence tests share one Spring context.
@DataJpaTest(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class))
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public abstract class AbstractPersistenceTest { public abstract class AbstractPersistenceTest {
// One container per JVM, not per class: Spring caches the context across test classes,
// and a per-class container would be stopped under the cached context.
@ServiceConnection @ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:17-alpine"); static PostgreSQLContainer<?> postgres = TestPostgres.CONTAINER;
static {
postgres.start();
}
} }
@@ -0,0 +1,19 @@
package auth_service.shared.test;
import org.testcontainers.containers.PostgreSQLContainer;
/**
* One database for all integration and persistence tests of a JVM. Integration tests commit their rows, so every
* test uses emails and display names no other test uses.
*/
final class TestPostgres {
static final PostgreSQLContainer<?> CONTAINER = new PostgreSQLContainer<>("postgres:17-alpine");
static {
CONTAINER.start();
}
private TestPostgres() {
}
}