refactor(api): one error body, token audiences, user lookup by id
Build, Test & Push Auth Service / quality-check (push) Successful in 54s
Build, Test & Push Auth Service / docker (push) Successful in 1m21s

This commit is contained in:
lobo committed 2026-09-29 14:48:28 +02:00
1 parent 68919eaab4
commit afcfacb68b
30 files changed
+662 -199

No files matched your search

@@ -4,11 +4,14 @@ import auth_service.shared.security.Role;
import auth_service.shared.security.SessionProperties;
import auth_service.shared.security.UserPrincipal;
import auth_service.shared.test.AbstractUnitTest;
import auth_service.shared.test.ContractFixture;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ObjectNode;
import java.security.KeyFactory;
import java.security.KeyPair;
@@ -66,7 +69,7 @@ class JwtServiceTest extends AbstractUnitTest {
@Test
void generateToken_Success() {
var token = jwtService.generateToken("0815", "test@example.com", null, Role.USER);
var token = jwtService.generateToken("0815", null, Role.USER);
assertNotNull(token);
assertFalse(token.isEmpty());
@@ -74,7 +77,7 @@ class JwtServiceTest extends AbstractUnitTest {
@Test
void generateToken_setsKidHeader() {
var token = jwtService.generateToken("0815", "test@example.com", null, Role.USER);
var token = jwtService.generateToken("0815", null, Role.USER);
var header = Jwts.parser()
.verifyWith(publicKey)
@@ -88,9 +91,9 @@ class JwtServiceTest extends AbstractUnitTest {
@Test
void parseLoginToken_ReturnsTheUser() {
var token = jwtService.generateToken("0815", "test@example.com", 123456789L, Role.USER);
var token = jwtService.generateToken("0815", 123456789L, Role.USER);
assertEquals(Optional.of(new UserPrincipal("0815", "test@example.com", 123456789L)),
assertEquals(Optional.of(new UserPrincipal("0815", 123456789L)),
jwtService.parseLoginToken(token));
}
@@ -101,7 +104,7 @@ class JwtServiceTest extends AbstractUnitTest {
@Test
void generateToken_ExpiresAfterTheAccessTokenTtl() {
var token = jwtService.generateToken("0815", "test@example.com", null, Role.USER);
var token = jwtService.generateToken("0815", null, Role.USER);
var claims = Jwts.parser().verifyWith(publicKey).build().parseSignedClaims(token).getPayload();
long secondsLeft = (claims.getExpiration().getTime() - System.currentTimeMillis()) / 1000;
@@ -162,7 +165,7 @@ class JwtServiceTest extends AbstractUnitTest {
System.arraycopy(prefix, 0, der, 0, prefix.length);
System.arraycopy(raw, 0, der, prefix.length, raw.length);
PublicKey fromJwk = KeyFactory.getInstance("Ed25519").generatePublic(new X509EncodedKeySpec(der));
String token = jwtService.generateToken("7", "a@b.de", null, Role.USER);
String token = jwtService.generateToken("7", null, Role.USER);
assertEquals("7", Jwts.parser().verifyWith(fromJwk).build().parseSignedClaims(token).getPayload().get("userId"));
}
@@ -181,7 +184,7 @@ class JwtServiceTest extends AbstractUnitTest {
@Test
void parseLoginToken_ReadsTheRole() {
String token = jwtService.generateToken("7", "a@b.de", null, Role.AUDITOR);
String token = jwtService.generateToken("7", null, Role.AUDITOR);
assertEquals(Role.AUDITOR, jwtService.parseLoginToken(token).orElseThrow().role());
}
@@ -194,4 +197,52 @@ class JwtServiceTest extends AbstractUnitTest {
assertEquals(Role.USER, jwtService.parseLoginToken(old).orElseThrow().role());
}
// The claims as the other side reads them: iat and exp change per token.
private static JsonNode payload(String token) {
ObjectNode payload = (ObjectNode) ContractFixture.MAPPER.readTree(Base64.getUrlDecoder().decode(token.split("\\.")[1]));
payload.remove(List.of("iat", "exp"));
return payload;
}
@SuppressWarnings("unchecked")
private String sign(JsonNode claims) {
return Jwts.builder().header().keyId(KID).and()
.claims(ContractFixture.MAPPER.treeToValue(claims, Map.class))
.expiration(new Date(System.currentTimeMillis() + 60_000))
.signWith(privateKey, Jwts.SIG.EdDSA).compact();
}
@Test
void tokens_MatchTheContractWithCore() {
assertEquals(ContractFixture.token("login"),
payload(jwtService.generateToken("42", 76561198000000001L, Role.USER)));
assertEquals(ContractFixture.token("publicProfile"),
payload(jwtService.generateProfileTicket(42L, List.of("OVERVIEW"), List.of("SUMMARY"))));
assertEquals(ContractFixture.token("service"), payload(jwtService.generateServiceToken()));
}
@Test
void parseLoginToken_ALoginOfAnAuthFromBeforeItem26_IsAccepted() {
assertEquals("42", jwtService.parseLoginToken(sign(ContractFixture.legacyToken("login"))).orElseThrow().id());
}
@Test
void parseLoginToken_ALoginWithoutTheOldUserIdClaim_IsAccepted() {
ObjectNode login = (ObjectNode) ContractFixture.token("login").deepCopy();
login.remove("userId");
assertEquals("42", jwtService.parseLoginToken(sign(login)).orElseThrow().id());
}
@Test
void parseLoginToken_ATokenForAnotherAudience_IsRejected() {
ObjectNode ticketAudience = (ObjectNode) ContractFixture.token("login").deepCopy();
ticketAudience.putArray("aud").add(JwtService.PROFILE_TICKET_AUDIENCE);
ObjectNode otherIssuer = (ObjectNode) ContractFixture.token("login").deepCopy();
otherIssuer.put("iss", "someone-else");
assertEquals(Optional.empty(), jwtService.parseLoginToken(sign(ticketAudience)));
assertEquals(Optional.empty(), jwtService.parseLoginToken(sign(otherIssuer)));
}
}
@@ -93,7 +93,7 @@ class RoleCheckFlowTest extends AbstractIntegrationTest {
private TokenGenerator tokenGenerator;
private String token(Role role) {
return tokenGenerator.generateToken("4711", "probe@example.com", null, role);
return tokenGenerator.generateToken("4711", null, role);
}
private ResultActions call(String path, Role role) throws Exception {
@@ -3,6 +3,7 @@ package auth_service.modules.account.service;
import auth_service.modules.account.model.CoreCleanupResult;
import auth_service.shared.security.ServiceTokenIssuer;
import auth_service.shared.test.AbstractUnitTest;
import auth_service.shared.test.ContractFixture;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
@@ -10,6 +11,7 @@ import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;
import tools.jackson.databind.JsonNode;
import java.util.List;
import java.util.Map;
@@ -39,15 +41,23 @@ class CoreAccountServiceTest extends AbstractUnitTest {
service = new CoreAccountService(builder.build(), TOKENS);
}
@Test
void markAndReplace_SendTheUserIdsWithTheServiceToken() {
core.expect(requestTo(CORE + "/internal/accounts/deleted")).andExpect(method(HttpMethod.POST))
private void expect(String name) {
JsonNode call = ContractFixture.internalCall(name);
var expectation = core.expect(requestTo(CORE + call.path("path").asString()))
.andExpect(method(HttpMethod.valueOf(call.path("method").asString())))
.andExpect(header("Authorization", "Bearer service-token"))
.andExpect(content().json("{\"userIds\":[42]}"))
.andRespond(withNoContent());
core.expect(requestTo(CORE + "/internal/accounts/deleted")).andExpect(method(HttpMethod.PUT))
.andExpect(content().json("{\"userIds\":[]}"))
.andRespond(withNoContent());
.andExpect(content().json(call.path("request").toString()));
if (call.has("response")) {
expectation.andRespond(withSuccess(call.path("response").toString(), MediaType.APPLICATION_JSON));
} else {
expectation.andRespond(withNoContent());
}
}
@Test
void markAndReplace_SendTheContractBodiesWithTheServiceToken() {
expect("markDeleted");
expect("replaceDeleted");
service.markDeleted(List.of(42L));
service.replaceDeleted(List.of());
@@ -56,12 +66,9 @@ class CoreAccountServiceTest extends AbstractUnitTest {
}
@Test
void purgeAndPrune_ReadTheResult() {
String result = "{\"dryRun\":true,\"users\":[42],\"rows\":{\"user_game\":3},\"refused\":false}";
core.expect(requestTo(CORE + "/internal/accounts/purge")).andExpect(content().json("{\"userIds\":[42],\"dryRun\":false}"))
.andRespond(withSuccess(result, MediaType.APPLICATION_JSON));
core.expect(requestTo(CORE + "/internal/accounts/prune")).andExpect(content().json("{\"knownUserIds\":[1,42],\"dryRun\":true}"))
.andRespond(withSuccess(result, MediaType.APPLICATION_JSON));
void purgeAndPrune_ReadTheContractResult() {
expect("purge");
expect("prune");
CoreCleanupResult expected = new CoreCleanupResult(true, List.of(42L), Map.of("user_game", 3), false);
assertThat(service.purge(List.of(42L), false)).isEqualTo(expected);
@@ -10,6 +10,8 @@ import auth_service.modules.login.model.User;
import auth_service.modules.login.persistence.MinimalUserRepository;
import auth_service.shared.test.AbstractUnitTest;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.security.crypto.password.PasswordEncoder;
@@ -17,6 +19,8 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
@@ -55,11 +59,13 @@ class LoginServiceTest extends AbstractUnitTest {
private User user;
private final CacheManager cacheManager = new ConcurrentMapCacheManager("users");
@BeforeEach
void setUp() {
lenient().when(passwordEncoder.encode(anyString())).thenReturn("dummy-hash");
loginService = new LoginService(minimalUserRepository, passwordEncoder, tokenGenerator, loginAttemptService,
refreshTokenService);
refreshTokenService, cacheManager);
user = User.builder()
.email("test@example.com")
.password("password")
@@ -77,7 +83,7 @@ class LoginServiceTest extends AbstractUnitTest {
.build();
when(minimalUserRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(dbUser));
when(passwordEncoder.matches(user.getPassword(), dbUser.getPassword())).thenReturn(true);
when(tokenGenerator.generateToken("815", "test@example.com", null, Role.USER)).thenReturn("mock-token");
when(tokenGenerator.generateToken("815", null, Role.USER)).thenReturn("mock-token");
when(refreshTokenService.startFamily(815L)).thenReturn("mock-refresh");
// Act
@@ -89,6 +95,20 @@ class LoginServiceTest extends AbstractUnitTest {
verify(loginAttemptService).recordSuccess("test@example.com", IP);
}
@Test
void login_Success_DropsOnlyThisUsersCachedProfile() {
var dbUser = User.builder().id(815L).email("test@example.com").password("encodedPassword").enabled(true).build();
when(minimalUserRepository.findByEmail(user.getEmail())).thenReturn(Optional.of(dbUser));
when(passwordEncoder.matches(user.getPassword(), dbUser.getPassword())).thenReturn(true);
cacheManager.getCache("users").put(815L, "old profile");
cacheManager.getCache("users").put(999L, "other user");
loginService.login(user, IP, null);
assertNull(cacheManager.getCache("users").get(815L));
assertNotNull(cacheManager.getCache("users").get(999L));
}
@Test
void login_Success_RevokesTheRefreshTokenThisBrowserStillSends() {
var dbUser = User.builder().id(815L).email("test@example.com").password("encodedPassword").enabled(true).build();
@@ -155,7 +175,7 @@ class LoginServiceTest extends AbstractUnitTest {
// Act & Assert
assertThrows(AccountBlockedException.class, () -> loginService.login(user, IP, null));
verify(tokenGenerator, never()).generateToken(any(), any(), any(), any());
verify(tokenGenerator, never()).generateToken(any(), any(), any());
verify(loginAttemptService).recordSuccess("test@example.com", IP);
}
@@ -177,7 +197,7 @@ class LoginServiceTest extends AbstractUnitTest {
loginService.login(user, IP, null);
verify(tokenGenerator).generateToken("815", "test@example.com", null, Role.PLATFORM_ADMIN);
verify(tokenGenerator).generateToken("815", null, Role.PLATFORM_ADMIN);
}
@Test
@@ -1,6 +1,12 @@
package auth_service.modules.registration.api;
import auth_service.configuration.security.JwtAuthenticationFilter;
import java.sql.SQLException;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.hibernate.exception.ConstraintViolationException;
import auth_service.configuration.security.JwtService;
import auth_service.modules.registration.api.dto.request.RegistrationRequest;
import auth_service.modules.registration.exception.EmailAlreadyInUseException;
@@ -23,14 +29,18 @@ import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@@ -59,6 +69,14 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
@Autowired
private JacksonTester<RegistrationRequest> registrationRequestJson;
// A German server: Hibernate reads no constraint name from this text, the handler must still find it.
private static DataIntegrityViolationException duplicate(String constraint) {
String text = "doppelter Schlüsselwert verletzt Unique-Constraint »" + constraint + "«";
return new DataIntegrityViolationException(text,
new ConstraintViolationException(text, new SQLException(text, "23505"), null));
}
@Test
void register_Success() throws Exception {
// Arrange
@@ -93,13 +111,11 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
when(registrationService.register(any(Registration.class), any()))
.thenThrow(new EmailAlreadyInUseException("The provided email address is already in use."));
// Act & Assert
// Depending on your GlobalExceptionHandler, this could also be 400 or 409.
// Assuming standard behavior without advice or mapping to 400 for now.
mockMvc.perform(post("/api/registration/new")
.contentType(MediaType.APPLICATION_JSON)
.content(registrationRequestJson.write(request).getJson()))
.andExpect(status().isBadRequest()); // Adjust status based on your @ExceptionHandler
.andExpect(status().isConflict())
.andExpect(jsonPath("$.code").value("EMAIL_ALREADY_IN_USE"));
}
@Test
@@ -114,20 +130,53 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
}
@Test
void register_DuplicateEmailRaceIsNotReportedAsConflict() throws Exception {
// Arrange
void register_DuplicateEmailRace_Is409EmailAlreadyInUse() throws Exception {
var request = RegistrationRequest.builder()
.email("test@example.com")
.password("SuperSecret123!")
.inviteCode("code")
.build();
when(registrationService.register(any(Registration.class), any()))
.thenThrow(new DataIntegrityViolationException("users_email_key"));
.thenThrow(duplicate("users_email_lower"));
// Act & Assert: the user module's 409 mapping must not apply to registration
assertThrows(ServletException.class, () -> mockMvc.perform(post("/api/registration/new")
.contentType(MediaType.APPLICATION_JSON)
.content(registrationRequestJson.write(request).getJson())));
mockMvc.perform(post("/api/registration/new")
.contentType(MediaType.APPLICATION_JSON)
.content(registrationRequestJson.write(request).getJson()))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.code").value("EMAIL_ALREADY_IN_USE"));
}
@Test
void register_DisplayNameRace_Is409DisplayNameAlreadyInUse() throws Exception {
when(registrationService.register(any(Registration.class), any()))
.thenThrow(duplicate("uq_users_display_name_lower"));
mockMvc.perform(post("/api/registration/new")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"email":"test@example.com","password":"SuperSecret123!","inviteCode":"code","displayName":"Lobo"}"""))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.code").value("DISPLAY_NAME_ALREADY_IN_USE"));
}
@Test
void register_UnreadableBody_Is400ValidationFailed() throws Exception {
mockMvc.perform(post("/api/registration/new").contentType(MediaType.APPLICATION_JSON).content("{not json"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("VALIDATION_FAILED"));
}
@Test
void register_UnexpectedError_Is500WithoutDetails() throws Exception {
when(registrationService.register(any(Registration.class), any())).thenThrow(new IllegalStateException("secret detail"));
mockMvc.perform(post("/api/registration/new")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"email":"test@example.com","password":"SuperSecret123!","inviteCode":"code"}"""))
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.code").value("INTERNAL_ERROR"))
.andExpect(content().string(not(containsString("secret detail"))));
}
@Test
@@ -136,7 +185,9 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"email":"test@example.com","password":"SuperSecret123!","inviteCode":"code","displayName":"x"}"""))
.andExpect(status().isBadRequest());
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("VALIDATION_FAILED"))
.andExpect(jsonPath("$.fields[0].field").value("displayName"));
verifyNoInteractions(registrationService);
}
@@ -238,4 +289,20 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
verify(registrationService).register(registration.capture(), any());
assertEquals("bob@example.com", registration.getValue().getEmail());
}
@Test
@ExtendWith(OutputCaptureExtension.class)
void aSpringServerError_IsLoggedAsInternalError(CapturedOutput output) throws Exception {
when(registrationService.register(any(Registration.class), any()))
.thenThrow(new HttpMessageNotWritableException("body not writable"));
mockMvc.perform(post("/api/registration/new")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"email":"test@example.com","password":"SuperSecret123!","inviteCode":"code"}"""))
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.code").value("INTERNAL_ERROR"));
assertTrue(output.getAll().contains("body not writable"), "the 500 left no trace in the log");
}
}
@@ -63,10 +63,13 @@ class RegisteredUserRepositoryTest extends AbstractPersistenceTest {
entityManager.flush();
assertTrue(registeredUserRepository.existsByEmail("Dup@Example.com"));
assertThrows(org.hibernate.exception.ConstraintViolationException.class, () -> {
var refused = assertThrows(org.hibernate.exception.ConstraintViolationException.class, () -> {
entityManager.persist(RegisteredUserEntity.builder().email("Dup@example.com").password("password")
.enabled(true).registrationDate(java.time.LocalDateTime.now()).build());
entityManager.flush();
});
// GlobalExceptionController decides on the state; on an English server Hibernate also gives the name.
assertEquals("23505", refused.getSQLState());
assertEquals("users_email_lower", refused.getConstraintName());
}
}
@@ -1,6 +1,8 @@
package auth_service.modules.user.api;
import auth_service.modules.user.model.ProfileStyle;
import java.sql.SQLException;
import org.hibernate.exception.ConstraintViolationException;
import auth_service.modules.user.model.Tile;
import auth_service.configuration.security.JwtAuthenticationFilter;
import auth_service.configuration.security.JwtService;
@@ -69,17 +71,25 @@ class UserControllerTest extends AbstractWebMvcTest {
SecurityContextHolder.clearContext();
}
// A German server: Hibernate reads no constraint name from this text, the handler must still find it.
private static DataIntegrityViolationException duplicate(String constraint) {
String text = "doppelter Schlüsselwert verletzt Unique-Constraint »" + constraint + "«";
return new DataIntegrityViolationException(text,
new ConstraintViolationException(text, new SQLException(text, "23505"), null));
}
@Test
void getCurrentUserV1_Success() throws Exception {
// Arrange
var email = "test@example.com";
var settings = UserSettings.builder().language("en").theme("dark").build();
var user = User.builder().email(email).settings(settings).build();
var principal = new UserPrincipal("1", email, null);
var principal = new UserPrincipal("1", null);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
);
when(userService.getUserByEmail(email)).thenReturn(user);
when(userService.getUser(1L)).thenReturn(user);
// Act & Assert
mockMvc.perform(get("/api/users/me")
@@ -94,11 +104,11 @@ class UserControllerTest extends AbstractWebMvcTest {
var email = "test@example.com";
var settings = UserSettings.builder().language("en").theme("dark").build();
var user = User.builder().email(email).steamId(76561198000000001L).steamIdVerified(true).settings(settings).build();
var principal = new UserPrincipal("1", email, null);
var principal = new UserPrincipal("1", null);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
);
when(userService.getUserByEmail(email)).thenReturn(user);
when(userService.getUser(1L)).thenReturn(user);
// Act & Assert
mockMvc.perform(get("/api/users/me")
@@ -114,11 +124,11 @@ class UserControllerTest extends AbstractWebMvcTest {
var email = "test@example.com";
var settings = UserSettings.builder().language("en").theme("dark").build();
var user = User.builder().email(email).settings(settings).build();
var principal = new UserPrincipal("1", email, null);
var principal = new UserPrincipal("1", null);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
);
when(userService.getUserByEmail(email)).thenReturn(user);
when(userService.getUser(1L)).thenReturn(user);
// Act & Assert
mockMvc.perform(get("/api/users/me")
@@ -131,11 +141,11 @@ class UserControllerTest extends AbstractWebMvcTest {
void getCurrentUserV1_NotFound() throws Exception {
// Arrange
var email = "notfound@example.com";
var principal = new UserPrincipal("1", email, null);
var principal = new UserPrincipal("1", null);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
);
when(userService.getUserByEmail(email)).thenThrow(new UserNotFoundException("User not found"));
when(userService.getUser(1L)).thenThrow(new UserNotFoundException("User not found"));
// Act & Assert
mockMvc.perform(get("/api/users/me")
@@ -145,7 +155,7 @@ class UserControllerTest extends AbstractWebMvcTest {
@Test
void updateUserSettings_OldClientSendingASteamId_SavesTheRestAndIssuesNoCookie() throws Exception {
var principal = new UserPrincipal("1", "test@example.com", null);
var principal = new UserPrincipal("1", null);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
);
@@ -164,11 +174,11 @@ class UserControllerTest extends AbstractWebMvcTest {
void updateUserSettings_OtherConstraintIsNotAConflict() throws Exception {
// Arrange
var email = "test@example.com";
var principal = new UserPrincipal("1", email, null);
var principal = new UserPrincipal("1", null);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
);
doThrow(new DataIntegrityViolationException("value too long for type character varying(10)")).when(userService).updateUserSettings(eq(1L), any(), any());
doThrow(new DataIntegrityViolationException("Wert zu lang", new SQLException("Wert zu lang", "22001"))).when(userService).updateUserSettings(eq(1L), any(), any());
// Act & Assert
mockMvc.perform(put("/api/users/settings")
@@ -185,11 +195,11 @@ class UserControllerTest extends AbstractWebMvcTest {
var email = "test@example.com";
var settings = UserSettings.builder().language("en").theme("dark").build();
var user = User.builder().email(email).displayName("Lobo").settings(settings).build();
var principal = new UserPrincipal("1", email, null);
var principal = new UserPrincipal("1", null);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
);
when(userService.getUserByEmail(email)).thenReturn(user);
when(userService.getUser(1L)).thenReturn(user);
// Act & Assert
mockMvc.perform(get("/api/users/me").accept(MediaType.APPLICATION_JSON))
@@ -201,7 +211,7 @@ class UserControllerTest extends AbstractWebMvcTest {
void updateUserSettings_PassesDisplayName() throws Exception {
// Arrange
var email = "test@example.com";
var principal = new UserPrincipal("1", email, null);
var principal = new UserPrincipal("1", null);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
);
@@ -218,7 +228,7 @@ class UserControllerTest extends AbstractWebMvcTest {
@ValueSource(strings = {"ab", " Lobo", "Lobo ", "<script>", "abcdefghijklmnopqrstuvwxy"})
void updateUserSettings_InvalidDisplayName(String displayName) throws Exception {
// Arrange
var principal = new UserPrincipal("1", "test@example.com", null);
var principal = new UserPrincipal("1", null);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
);
@@ -236,7 +246,7 @@ class UserControllerTest extends AbstractWebMvcTest {
void updateUserSettings_DisplayNameTaken() throws Exception {
// Arrange
var email = "test@example.com";
var principal = new UserPrincipal("1", email, null);
var principal = new UserPrincipal("1", null);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
);
@@ -255,11 +265,11 @@ class UserControllerTest extends AbstractWebMvcTest {
void updateUserSettings_DisplayNameRaceIsConflict() throws Exception {
// Arrange
var email = "test@example.com";
var principal = new UserPrincipal("1", email, null);
var principal = new UserPrincipal("1", null);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
);
doThrow(new DataIntegrityViolationException("duplicate key value violates unique constraint \"uq_users_display_name_lower\"")).when(userService).updateUserSettings(eq(1L), eq("Lobo"), any());
doThrow(duplicate("uq_users_display_name_lower")).when(userService).updateUserSettings(eq(1L), eq("Lobo"), any());
// Act & Assert
mockMvc.perform(put("/api/users/settings")
@@ -273,7 +283,7 @@ class UserControllerTest extends AbstractWebMvcTest {
@ParameterizedTest
@CsvSource({"',\"publicProfile\":true', true", "'', false", "',\"publicProfile\":null', false"})
void updateUserSettings_PassesPublicProfile_MissingIsFalse(String field, boolean expected) throws Exception {
var principal = new UserPrincipal("1", "test@example.com", null);
var principal = new UserPrincipal("1", null);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList())
);
@@ -290,7 +300,7 @@ class UserControllerTest extends AbstractWebMvcTest {
@Test
void updateUserSettings_PublicProfileWithoutDisplayNameIsRejected() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
new UserPrincipal("1", null), null, Collections.emptyList()));
mockMvc.perform(put("/api/users/settings")
.contentType(MediaType.APPLICATION_JSON)
@@ -303,9 +313,9 @@ class UserControllerTest extends AbstractWebMvcTest {
var email = "test@example.com";
var settings = UserSettings.builder().language("en").theme("dark").publicProfile(true).build();
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", email, null), null, Collections.emptyList())
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", null), null, Collections.emptyList())
);
when(userService.getUserByEmail(email)).thenReturn(User.builder().email(email).settings(settings).build());
when(userService.getUser(1L)).thenReturn(User.builder().email(email).settings(settings).build());
mockMvc.perform(get("/api/users/me").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
@@ -318,9 +328,9 @@ class UserControllerTest extends AbstractWebMvcTest {
var settings = UserSettings.builder().language("en").theme("dark")
.appearance(new Appearance("blue", "round", true, 620L, 1086940L, false)).build();
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", email, null), null, Collections.emptyList())
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", null), null, Collections.emptyList())
);
when(userService.getUserByEmail(email)).thenReturn(User.builder().email(email).settings(settings).build());
when(userService.getUser(1L)).thenReturn(User.builder().email(email).settings(settings).build());
mockMvc.perform(get("/api/users/me").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
@@ -337,9 +347,9 @@ class UserControllerTest extends AbstractWebMvcTest {
var email = "test@example.com";
var settings = UserSettings.builder().language("en").theme("dark").build();
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", email, null), null, Collections.emptyList())
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", null), null, Collections.emptyList())
);
when(userService.getUserByEmail(email)).thenReturn(User.builder().email(email).settings(settings).build());
when(userService.getUser(1L)).thenReturn(User.builder().email(email).settings(settings).build());
mockMvc.perform(get("/api/users/me").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
@@ -358,9 +368,9 @@ class UserControllerTest extends AbstractWebMvcTest {
.dashboardTiles(List.of(new Tile("STATS_TREND", "L")))
.profileTiles(List.of(new Tile("PLAYING", "S"))).favoritePlatform("PS5").build();
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", email, null), null, Collections.emptyList())
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", null), null, Collections.emptyList())
);
when(userService.getUserByEmail(email)).thenReturn(User.builder().email(email).settings(settings).build());
when(userService.getUser(1L)).thenReturn(User.builder().email(email).settings(settings).build());
mockMvc.perform(get("/api/users/me").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
@@ -375,9 +385,9 @@ class UserControllerTest extends AbstractWebMvcTest {
void getCurrentUserV1_NeverChangedDashboard_SendsNullTiles() throws Exception {
String email = "test@example.com";
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", email, null), null, Collections.emptyList())
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", null), null, Collections.emptyList())
);
when(userService.getUserByEmail(email)).thenReturn(User.builder().email(email)
when(userService.getUser(1L)).thenReturn(User.builder().email(email)
.settings(UserSettings.builder().language("en").theme("dark").build()).build());
mockMvc.perform(get("/api/users/me").accept(MediaType.APPLICATION_JSON))
@@ -389,7 +399,7 @@ class UserControllerTest extends AbstractWebMvcTest {
@Test
void updateDashboardTiles_StoresTheTiles() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
new UserPrincipal("1", null), null, Collections.emptyList()));
mockMvc.perform(put("/api/users/settings/dashboard-tiles")
.contentType(MediaType.APPLICATION_JSON)
@@ -402,7 +412,7 @@ class UserControllerTest extends AbstractWebMvcTest {
@Test
void updateDashboardTiles_NullTilesResets() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
new UserPrincipal("1", null), null, Collections.emptyList()));
mockMvc.perform(put("/api/users/settings/dashboard-tiles")
.contentType(MediaType.APPLICATION_JSON)
@@ -415,7 +425,7 @@ class UserControllerTest extends AbstractWebMvcTest {
@Test
void updateFavoritePlatform_StoresOrClearsIt() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
new UserPrincipal("1", null), null, Collections.emptyList()));
mockMvc.perform(put("/api/users/settings/favorite-platform")
.contentType(MediaType.APPLICATION_JSON)
@@ -433,7 +443,7 @@ class UserControllerTest extends AbstractWebMvcTest {
@Test
void updateFavoritePlatform_BlankOrTooLong_Is400() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
new UserPrincipal("1", null), null, Collections.emptyList()));
for (String platform : List.of(" ", "x".repeat(33))) {
mockMvc.perform(put("/api/users/settings/favorite-platform")
@@ -447,7 +457,7 @@ class UserControllerTest extends AbstractWebMvcTest {
@Test
void updateProfileTiles_StoresTheTiles() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
new UserPrincipal("1", null), null, Collections.emptyList()));
mockMvc.perform(put("/api/users/settings/profile-tiles")
.contentType(MediaType.APPLICATION_JSON)
@@ -471,7 +481,7 @@ class UserControllerTest extends AbstractWebMvcTest {
})
void updateTiles_UnknownIdBadSizeOrMissing_IsRejected(String pathAndBody) throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
new UserPrincipal("1", null), null, Collections.emptyList()));
String[] parts = pathAndBody.split("\\|", 2);
mockMvc.perform(put("/api/users/settings/" + parts[0])
@@ -485,7 +495,7 @@ class UserControllerTest extends AbstractWebMvcTest {
@Test
void updateTiles_MoreThanTwentyTiles_IsRejected() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
new UserPrincipal("1", null), null, Collections.emptyList()));
String tiles = String.join(",", Collections.nCopies(21, "{\"id\":\"PLAYING\",\"size\":\"M\"}"));
mockMvc.perform(put("/api/users/settings/dashboard-tiles")
@@ -500,9 +510,9 @@ class UserControllerTest extends AbstractWebMvcTest {
UserSettings settings = UserSettings.builder().language("en").theme("dark")
.profileStyle(new ProfileStyle("red", "sharp", 620L, null)).build();
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", email, null), null, Collections.emptyList())
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", null), null, Collections.emptyList())
);
when(userService.getUserByEmail(email)).thenReturn(User.builder().email(email).settings(settings).build());
when(userService.getUser(1L)).thenReturn(User.builder().email(email).settings(settings).build());
mockMvc.perform(get("/api/users/me").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
@@ -515,7 +525,7 @@ class UserControllerTest extends AbstractWebMvcTest {
@Test
void updateProfileStyle_StoresTheValues() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
new UserPrincipal("1", null), null, Collections.emptyList()));
mockMvc.perform(put("/api/users/settings/profile-style")
.contentType(MediaType.APPLICATION_JSON)
@@ -536,7 +546,7 @@ class UserControllerTest extends AbstractWebMvcTest {
})
void updateProfileStyle_UnknownOrMissingValue_IsRejected(String body) throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
new UserPrincipal("1", null), null, Collections.emptyList()));
mockMvc.perform(put("/api/users/settings/profile-style")
.contentType(MediaType.APPLICATION_JSON)
@@ -549,7 +559,7 @@ class UserControllerTest extends AbstractWebMvcTest {
@Test
void updateAppearance_StoresTheValues() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
new UserPrincipal("1", null), null, Collections.emptyList()));
mockMvc.perform(put("/api/users/settings/appearance")
.contentType(MediaType.APPLICATION_JSON)
@@ -563,7 +573,7 @@ class UserControllerTest extends AbstractWebMvcTest {
@Test
void updateAppearance_MissingOptionalFields_AreTheDefaults() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
new UserPrincipal("1", null), null, Collections.emptyList()));
mockMvc.perform(put("/api/users/settings/appearance")
.contentType(MediaType.APPLICATION_JSON)
@@ -587,7 +597,7 @@ class UserControllerTest extends AbstractWebMvcTest {
})
void updateAppearance_UnknownOrMissingValue_IsRejected(String body) throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
new UserPrincipal("1", null), null, Collections.emptyList()));
mockMvc.perform(put("/api/users/settings/appearance")
.contentType(MediaType.APPLICATION_JSON)
@@ -2,6 +2,7 @@ package auth_service.modules.user.persistence;
import auth_service.modules.user.model.ProfileStyle;
import auth_service.modules.user.model.Tile;
import auth_service.modules.user.model.User;
import auth_service.modules.user.exception.UserNotFoundException;
import auth_service.modules.user.model.Appearance;
import auth_service.modules.user.model.SteamIdHolder;
@@ -32,6 +33,16 @@ class UserRepositoryTest extends AbstractPersistenceTest {
@Autowired
private JdbcTemplate jdbc;
// The repository finds users by id; the tests know them by email.
private Optional<User> findByEmail(String email) {
return entityManager.getEntityManager()
.createQuery("select u.id from UserEntity u where u.email = :email", Long.class)
.setParameter("email", email)
.getResultStream()
.findFirst()
.flatMap(userRepository::findById);
}
@Test
void updateSettings_KeepsADeleteThatCommittedAfterTheLoad() {
var user = entityManager.persistFlushFind(UserEntity.builder().email("race@example.com").password("password")
@@ -60,7 +71,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
entityManager.flush();
// Act
var result = userRepository.findByEmail(email);
var result = findByEmail(email);
// Assert
assertTrue(result.isPresent());
@@ -110,7 +121,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
entityManager.flush();
// Act
var result = userRepository.findByEmail(email);
var result = findByEmail(email);
// Assert
assertTrue(result.isPresent());
@@ -179,7 +190,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
@Test
void migrations_LeaveNoSeededTestUser() {
// V1 seeded test@example.com with a known password; it must not exist after all migrations.
assertTrue(userRepository.findByEmail("test@example.com").isEmpty());
assertTrue(findByEmail("test@example.com").isEmpty());
}
@Test
@@ -205,7 +216,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
entityManager.flush();
// Assert
assertEquals("Lobo", userRepository.findByEmail("owner2@example.com").orElseThrow().getDisplayName());
assertEquals("Lobo", findByEmail("owner2@example.com").orElseThrow().getDisplayName());
assertFalse(userRepository.isDisplayNameUsedByOtherUser(owner.getId(), "lobo"));
assertTrue(userRepository.isDisplayNameUsedByOtherUser(other.getId(), "LOBO"));
assertFalse(userRepository.isDisplayNameUsedByOtherUser(other.getId(), "Wolf"));
@@ -226,7 +237,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
entityManager.flush();
entityManager.clear();
assertTrue(userRepository.findByEmail("public@example.com").orElseThrow().getSettings().isPublicProfile());
assertTrue(findByEmail("public@example.com").orElseThrow().getSettings().isPublicProfile());
}
@Test
@@ -243,7 +254,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
entityManager.flush();
entityManager.clear();
var settings = userRepository.findByEmail("appearance@example.com").orElseThrow().getSettings();
var settings = findByEmail("appearance@example.com").orElseThrow().getSettings();
assertEquals(new Appearance("orange", "round", true, 620L, 1086940L, false), settings.getAppearance());
}
@@ -264,7 +275,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
entityManager.flush();
entityManager.clear();
var settings = userRepository.findByEmail("keep@example.com").orElseThrow().getSettings();
var settings = findByEmail("keep@example.com").orElseThrow().getSettings();
assertEquals(new Appearance("pink", "sharp", false, 220L, null, true), settings.getAppearance());
}
@@ -278,7 +289,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
.build());
entityManager.flush();
assertEquals(Appearance.DEFAULT, userRepository.findByEmail("defaults@example.com").orElseThrow()
assertEquals(Appearance.DEFAULT, findByEmail("defaults@example.com").orElseThrow()
.getSettings().getAppearance());
}
@@ -291,7 +302,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
.registrationDate(LocalDateTime.now())
.build());
entityManager.flush();
assertEquals(ProfileStyle.DEFAULT, userRepository.findByEmail("style@example.com").orElseThrow()
assertEquals(ProfileStyle.DEFAULT, findByEmail("style@example.com").orElseThrow()
.getSettings().getProfileStyle());
ProfileStyle style = new ProfileStyle("pink", "sharp", 620L, 1086940L);
@@ -299,7 +310,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
entityManager.flush();
entityManager.clear();
assertEquals(style, userRepository.findByEmail("style@example.com").orElseThrow().getSettings().getProfileStyle());
assertEquals(style, findByEmail("style@example.com").orElseThrow().getSettings().getProfileStyle());
}
@Test
@@ -311,7 +322,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
.registrationDate(LocalDateTime.now())
.build());
entityManager.flush();
UserSettings fresh = userRepository.findByEmail("tiles@example.com").orElseThrow().getSettings();
UserSettings fresh = findByEmail("tiles@example.com").orElseThrow().getSettings();
assertNull(fresh.getDashboardTiles());
assertEquals(UserSettings.DEFAULT_PROFILE_TILES, fresh.getProfileTiles());
@@ -325,7 +336,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
entityManager.flush();
entityManager.clear();
UserSettings saved = userRepository.findByEmail("tiles@example.com").orElseThrow().getSettings();
UserSettings saved = findByEmail("tiles@example.com").orElseThrow().getSettings();
assertEquals(dashboard, saved.getDashboardTiles());
assertEquals(profile, saved.getProfileTiles());
}
@@ -338,18 +349,18 @@ class UserRepositoryTest extends AbstractPersistenceTest {
.enabled(true)
.registrationDate(LocalDateTime.now())
.build());
assertNull(userRepository.findByEmail("favorite@example.com").orElseThrow().getSettings().getFavoritePlatform());
assertNull(findByEmail("favorite@example.com").orElseThrow().getSettings().getFavoritePlatform());
userRepository.updateFavoritePlatform(userEntity.getId(), "Switch");
entityManager.flush();
entityManager.clear();
assertEquals("Switch", userRepository.findByEmail("favorite@example.com").orElseThrow().getSettings()
assertEquals("Switch", findByEmail("favorite@example.com").orElseThrow().getSettings()
.getFavoritePlatform());
userRepository.updateFavoritePlatform(userEntity.getId(), null);
entityManager.flush();
entityManager.clear();
assertNull(userRepository.findByEmail("favorite@example.com").orElseThrow().getSettings().getFavoritePlatform());
assertNull(findByEmail("favorite@example.com").orElseThrow().getSettings().getFavoritePlatform());
}
@Test
@@ -367,7 +378,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
entityManager.flush();
entityManager.clear();
assertNull(userRepository.findByEmail("reset@example.com").orElseThrow().getSettings().getDashboardTiles());
assertNull(findByEmail("reset@example.com").orElseThrow().getSettings().getDashboardTiles());
}
@Test
@@ -41,27 +41,18 @@ class UserServiceTest extends AbstractUnitTest {
private UserService userService;
@Test
void getUserByEmail_Success() {
// Arrange
var email = "test@example.com";
var user = User.builder().email(email).build();
when(userRepository.findByEmail(email)).thenReturn(Optional.of(user));
void getUser_Success() {
var user = User.builder().id(7L).email("test@example.com").build();
when(userRepository.findById(7L)).thenReturn(Optional.of(user));
// Act
var result = userService.getUserByEmail(email);
// Assert
assertEquals(user, result);
assertEquals(user, userService.getUser(7L));
}
@Test
void getUserByEmail_NotFound() {
// Arrange
var email = "notfound@example.com";
when(userRepository.findByEmail(email)).thenReturn(Optional.empty());
void getUser_NotFound() {
when(userRepository.findById(7L)).thenReturn(Optional.empty());
// Act & Assert
assertThrows(UserNotFoundException.class, () -> userService.getUserByEmail(email));
assertThrows(UserNotFoundException.class, () -> userService.getUser(7L));
}
@Test
@@ -161,9 +152,9 @@ class UserServiceTest extends AbstractUnitTest {
@Test
void unlinkSteamId_ClearsItAndReturnsATokenWithoutIt() {
when(tokenGenerator.generateToken("1", "test@example.com", null, Role.USER)).thenReturn("token-without-steam");
when(tokenGenerator.generateToken("1", null, Role.USER)).thenReturn("token-without-steam");
assertEquals("token-without-steam", userService.unlinkSteamId(1L, "test@example.com", Role.USER));
assertEquals("token-without-steam", userService.unlinkSteamId(1L, Role.USER));
verify(userRepository).clearSteamId(1L);
}
@@ -0,0 +1,41 @@
package auth_service.shared.test;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.json.JsonMapper;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
/**
* {@code contract/auth-core-contract.json}: what Core expects of Auth's tokens and calls. Core builds its test tokens
* from the same file.
*/
public final class ContractFixture {
public static final JsonMapper MAPPER = JsonMapper.builder().build();
private static final JsonNode CONTRACT = load();
private ContractFixture() {
}
private static JsonNode load() {
try (InputStream in = ContractFixture.class.getResourceAsStream("/contract/auth-core-contract.json")) {
return MAPPER.readTree(in);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static JsonNode token(String kind) {
return CONTRACT.path("tokens").required(kind);
}
public static JsonNode legacyToken(String kind) {
return CONTRACT.path("legacyTokens").required(kind);
}
public static JsonNode internalCall(String name) {
return CONTRACT.path("internal").required(name);
}
}
@@ -0,0 +1,71 @@
{
"_about": "The token and internal-call contract between Auth and Core. The same file is in both repos; ToBePlayed-Deployment/scripts/check-contract.sh compares them. iat and exp are left out. legacyTokens are the shapes of an Auth from before item 26: Core and Auth accept them until item 29.",
"tokens": {
"login": {
"iss": "tbp-auth",
"aud": ["tbp-login"],
"sub": "42",
"userId": "42",
"role": "USER",
"steamId": 76561198000000001
},
"publicProfile": {
"iss": "tbp-auth",
"aud": ["tbp-public-profile"],
"sub": "public-profile",
"typ": "public-profile",
"profileUserId": "42",
"profileStats": ["OVERVIEW"],
"sections": ["SUMMARY"]
},
"service": {
"iss": "tbp-auth",
"aud": ["tbp-internal"],
"sub": "tbp-auth",
"typ": "service"
}
},
"legacyTokens": {
"login": {
"sub": "user@example.com",
"userId": "42",
"role": "USER",
"steamId": 76561198000000001
},
"publicProfile": {
"sub": "public-profile",
"typ": "public-profile",
"profileUserId": "42",
"profileStats": ["OVERVIEW"],
"sections": ["SUMMARY"]
},
"service": {
"sub": "tbp-auth",
"typ": "service"
}
},
"internal": {
"markDeleted": {
"method": "POST",
"path": "/internal/accounts/deleted",
"request": {"userIds": [42]}
},
"replaceDeleted": {
"method": "PUT",
"path": "/internal/accounts/deleted",
"request": {"userIds": []}
},
"purge": {
"method": "POST",
"path": "/internal/accounts/purge",
"request": {"userIds": [42], "dryRun": false},
"response": {"dryRun": true, "users": [42], "rows": {"user_game": 3}, "refused": false}
},
"prune": {
"method": "POST",
"path": "/internal/accounts/prune",
"request": {"knownUserIds": [1, 42], "dryRun": true},
"response": {"dryRun": true, "users": [42], "rows": {"user_game": 3}, "refused": false}
}
}
}