feat(profile): store the public profile look

This commit is contained in:
lobo committed 2026-09-28 14:20:14 +02:00
1 parent f86f3c2e63
commit dd4fcd1546
27 files changed
+308 -15

No files matched your search

@@ -110,7 +110,7 @@ class JwtServiceTest extends AbstractUnitTest {
@Test
void generateProfileTicket_NamesTheUserAndExpiresSoon() {
var ticket = jwtService.generateProfileTicket(42L, List.of());
var ticket = jwtService.generateProfileTicket(42L, List.of(), List.of());
var claims = Jwts.parser().verifyWith(publicKey).build().parseSignedClaims(ticket).getPayload();
assertEquals("public-profile", claims.get("typ", String.class));
@@ -122,15 +122,16 @@ class JwtServiceTest extends AbstractUnitTest {
@Test
void generateProfileTicket_CarriesTheSharedStatGroups() {
String ticket = jwtService.generateProfileTicket(42L, List.of("PILE", "TREND"));
String ticket = jwtService.generateProfileTicket(42L, List.of("PILE", "TREND"), List.of("PLAYING", "STATS"));
Claims claims = Jwts.parser().verifyWith(publicKey).build().parseSignedClaims(ticket).getPayload();
assertEquals(List.of("PILE", "TREND"), claims.get("profileStats", List.class));
assertEquals(List.of("PLAYING", "STATS"), claims.get("sections", List.class));
}
@Test
void parseLoginToken_ProfileTicketIsNoLoginToken() {
assertEquals(Optional.empty(), jwtService.parseLoginToken(jwtService.generateProfileTicket(42L, List.of())));
assertEquals(Optional.empty(), jwtService.parseLoginToken(jwtService.generateProfileTicket(42L, List.of(), List.of())));
}
@Test
@@ -84,6 +84,40 @@ class PublicProfileControllerTest extends AbstractIntegrationTest {
assertEquals(List.of("PILE", "TREND"), claims.get("profileStats", List.class));
}
@Test
void publicProfile_AnswersWithTheOwnersStyle() throws Exception {
jdbc.update("update user_settings set profile_accent = 'red', profile_corners = 'round', profile_banner_appid = 620, "
+ "profile_sections = '{PLAYING,SUMMARY}' where user_id = ?", publicUserId);
mockMvc.perform(get("/api/public/profiles/Lobo"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.style.accent").value("red"))
.andExpect(jsonPath("$.style.corners").value("round"))
.andExpect(jsonPath("$.style.bannerAppid").value(620))
.andExpect(jsonPath("$.style.backgroundAppid").isEmpty())
.andExpect(jsonPath("$.style.sections[0]").value("PLAYING"))
.andExpect(jsonPath("$.style.sections[1]").value("SUMMARY"));
}
@Test
void publicProfile_NewRowShowsEverySection() throws Exception {
mockMvc.perform(get("/api/public/profiles/Lobo"))
.andExpect(jsonPath("$.style.accent").value("violet"))
.andExpect(jsonPath("$.style.sections.length()").value(5));
}
@Test
void publicProfile_TicketCarriesTheShownSections() throws Exception {
jdbc.update("update user_settings set profile_sections = '{PLAYING}' where user_id = ?", publicUserId);
String body = mockMvc.perform(get("/api/public/profiles/Lobo")).andReturn().getResponse().getContentAsString();
String ticket = body.replaceAll(".*\"ticket\"\\s*:\\s*\"([^\"]+)\".*", "$1");
PublicKey key = KeyFactory.getInstance("Ed25519").generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)));
Claims claims = Jwts.parser().verifyWith(key).build().parseSignedClaims(ticket).getPayload();
assertEquals(List.of("PLAYING"), claims.get("sections", List.class));
}
@Test
void publicProfile_NameIgnoresCase() throws Exception {
mockMvc.perform(get("/api/public/profiles/lOBO"))
@@ -1,5 +1,6 @@
package auth_service.modules.user.api;
import auth_service.modules.user.model.ProfileStyle;
import auth_service.configuration.security.JwtAuthenticationFilter;
import auth_service.configuration.security.JwtService;
import auth_service.shared.security.UserPrincipal;
@@ -390,6 +391,62 @@ class UserControllerTest extends AbstractWebMvcTest {
.andExpect(status().isBadRequest());
}
@Test
void getCurrentUserV1_ShowsTheProfileStyle() throws Exception {
String email = "test@example.com";
UserSettings settings = UserSettings.builder().language("en").theme("dark")
.profileStyle(new ProfileStyle("red", "sharp", 620L, null, List.of("PLAYING"))).build();
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", email, null), null, Collections.emptyList())
);
when(userService.getUserByEmail(email)).thenReturn(User.builder().email(email).settings(settings).build());
mockMvc.perform(get("/api/users/me").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.settings.profileStyle.accent").value("red"))
.andExpect(jsonPath("$.settings.profileStyle.corners").value("sharp"))
.andExpect(jsonPath("$.settings.profileStyle.bannerAppid").value(620))
.andExpect(jsonPath("$.settings.profileStyle.backgroundAppid").isEmpty())
.andExpect(jsonPath("$.settings.profileStyle.sections[0]").value("PLAYING"));
}
@Test
void updateProfileStyle_StoresTheValues() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
mockMvc.perform(put("/api/users/settings/profile-style")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"accent\":\"green\",\"corners\":\"round\",\"bannerAppid\":620,"
+ "\"sections\":[\"STATS\",\"SUMMARY\"]}"))
.andExpect(status().isNoContent());
verify(userService).updateProfileStyle(1L, new ProfileStyle("green", "round", 620L, null, List.of("STATS", "SUMMARY")));
}
// Each body differs from a valid one in one field only, so the 400 comes from that field's constraint.
@ParameterizedTest
@ValueSource(strings = {
"{\"accent\":\"hotpink\",\"corners\":\"default\",\"sections\":[]}",
"{\"accent\":\"violet\",\"corners\":\"pointy\",\"sections\":[]}",
"{\"accent\":\"violet\",\"corners\":\"default\",\"bannerAppid\":0,\"sections\":[]}",
"{\"accent\":\"violet\",\"corners\":\"default\",\"backgroundAppid\":-620,\"sections\":[]}",
"{\"accent\":\"violet\",\"corners\":\"default\",\"sections\":[\"NOTES\"]}",
"{\"accent\":\"violet\",\"corners\":\"default\",\"sections\":[null]}",
"{\"accent\":\"violet\",\"corners\":\"default\"}"
})
void updateProfileStyle_UnknownOrMissingValue_IsRejected(String body) throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
mockMvc.perform(put("/api/users/settings/profile-style")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isBadRequest());
verifyNoInteractions(userService);
}
@Test
void updateAppearance_StoresTheValues() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
@@ -0,0 +1,17 @@
package auth_service.modules.user.model;
import auth_service.shared.test.AbstractUnitTest;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ProfileStyleTest extends AbstractUnitTest {
@Test
void keepsEachSectionOnceAtItsFirstPlace() {
assertEquals(List.of("STATS", "PLAYING"),
new ProfileStyle("red", "round", 620L, null, List.of("STATS", "PLAYING", "STATS")).sections());
}
}
@@ -1,5 +1,6 @@
package auth_service.modules.user.persistence;
import auth_service.modules.user.model.ProfileStyle;
import auth_service.modules.user.exception.UserNotFoundException;
import auth_service.modules.user.model.Appearance;
import auth_service.modules.user.model.SteamIdHolder;
@@ -280,6 +281,26 @@ class UserRepositoryTest extends AbstractPersistenceTest {
.getSettings().getAppearance());
}
@Test
void updateProfileStyle_IsReadBackAndStartsWithTheDefaults() {
UserEntity userEntity = entityManager.persist(UserEntity.builder()
.email("style@example.com")
.password("password")
.enabled(true)
.registrationDate(LocalDateTime.now())
.build());
entityManager.flush();
assertEquals(ProfileStyle.DEFAULT, userRepository.findByEmail("style@example.com").orElseThrow()
.getSettings().getProfileStyle());
ProfileStyle style = new ProfileStyle("pink", "sharp", 620L, 1086940L, List.of("FINISHED", "SUMMARY"));
userRepository.updateProfileStyle(userEntity.getId(), style);
entityManager.flush();
entityManager.clear();
assertEquals(style, userRepository.findByEmail("style@example.com").orElseThrow().getSettings().getProfileStyle());
}
@Test
void updateProfileStats_IsReadBackAndKeptByUpdateSettings() {
UserEntity userEntity = entityManager.persist(UserEntity.builder()