Public Access
feat(profile): let users share stat groups
Store the groups in user_settings.profile_stats and send them in the profile ticket, so Core shows only these on the public profile.
This commit is contained in:
1 parent
4b2acef381
commit
4ec47f1b2c
22 files changed
+206
-25
No files matched your search
@@ -3,6 +3,7 @@ package auth_service.configuration.security;
|
||||
import auth_service.shared.security.SessionProperties;
|
||||
import auth_service.shared.security.UserPrincipal;
|
||||
import auth_service.shared.test.AbstractUnitTest;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -104,7 +105,7 @@ class JwtServiceTest extends AbstractUnitTest {
|
||||
|
||||
@Test
|
||||
void generateProfileTicket_NamesTheUserAndExpiresSoon() {
|
||||
var ticket = jwtService.generateProfileTicket(42L);
|
||||
var ticket = jwtService.generateProfileTicket(42L, List.of());
|
||||
|
||||
var claims = Jwts.parser().verifyWith(publicKey).build().parseSignedClaims(ticket).getPayload();
|
||||
assertEquals("public-profile", claims.get("typ", String.class));
|
||||
@@ -114,9 +115,17 @@ class JwtServiceTest extends AbstractUnitTest {
|
||||
assertTrue(secondsLeft > 0 && secondsLeft <= 600, "expires in " + secondsLeft + "s");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateProfileTicket_CarriesTheSharedStatGroups() {
|
||||
String ticket = jwtService.generateProfileTicket(42L, List.of("PILE", "TREND"));
|
||||
|
||||
Claims claims = Jwts.parser().verifyWith(publicKey).build().parseSignedClaims(ticket).getPayload();
|
||||
assertEquals(List.of("PILE", "TREND"), claims.get("profileStats", List.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseLoginToken_ProfileTicketIsNoLoginToken() {
|
||||
assertEquals(Optional.empty(), jwtService.parseLoginToken(jwtService.generateProfileTicket(42L)));
|
||||
assertEquals(Optional.empty(), jwtService.parseLoginToken(jwtService.generateProfileTicket(42L, List.of())));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package auth_service.modules.profile.api;
|
||||
|
||||
import auth_service.shared.test.AbstractIntegrationTest;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -13,8 +14,10 @@ import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -66,6 +69,19 @@ class PublicProfileControllerTest extends AbstractIntegrationTest {
|
||||
var key = KeyFactory.getInstance("Ed25519").generatePublic(new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)));
|
||||
var claims = Jwts.parser().verifyWith(key).build().parseSignedClaims(ticket).getPayload();
|
||||
assertEquals(String.valueOf(publicUserId), claims.get("profileUserId", String.class));
|
||||
assertEquals(List.of(), claims.get("profileStats", List.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicProfile_TicketCarriesTheSharedStatGroups() throws Exception {
|
||||
jdbc.update("update user_settings set profile_stats = '{PILE,TREND}' 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("PILE", "TREND"), claims.get("profileStats", List.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
@@ -347,6 +348,48 @@ class UserControllerTest extends AbstractWebMvcTest {
|
||||
.andExpect(jsonPath("$.settings.appearance.showBanner").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCurrentUserV1_ShowsTheSharedStatGroups() throws Exception {
|
||||
String email = "test@example.com";
|
||||
UserSettings settings = UserSettings.builder().language("en").theme("dark")
|
||||
.profileStats(List.of("PILE", "TREND")).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.profileStats[0]").value("PILE"))
|
||||
.andExpect(jsonPath("$.settings.profileStats[1]").value("TREND"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateProfileStats_StoresTheGroups() throws Exception {
|
||||
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
|
||||
new UserPrincipal("1", "test@example.com", null), null, Collections.emptyList()));
|
||||
|
||||
mockMvc.perform(put("/api/users/settings/profile-stats")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"groups\":[\"PILE\",\"TREND\"]}"))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
verify(userService).updateProfileStats(1L, List.of("PILE", "TREND"));
|
||||
}
|
||||
|
||||
// OVERVIEW is public anyway, so it is no choice.
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"{}", "{\"groups\":[\"OVERVIEW\"]}", "{\"groups\":[\"MONEY\"]}", "{\"groups\":[null]}"})
|
||||
void updateProfileStats_UnknownOrMissingGroups_Is400(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-stats")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(body))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateAppearance_StoresTheValues() throws Exception {
|
||||
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.springframework.boot.jpa.test.autoconfigure.TestEntityManager;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
@@ -253,6 +254,29 @@ class UserRepositoryTest extends AbstractPersistenceTest {
|
||||
.getSettings().getAppearance());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateProfileStats_IsReadBackAndKeptByUpdateSettings() {
|
||||
UserEntity userEntity = entityManager.persist(UserEntity.builder()
|
||||
.email("stats@example.com")
|
||||
.password("password")
|
||||
.enabled(true)
|
||||
.registrationDate(LocalDateTime.now())
|
||||
.build());
|
||||
entityManager.flush();
|
||||
assertEquals(List.of(), userRepository.findByEmail("stats@example.com").orElseThrow()
|
||||
.getSettings().getProfileStats());
|
||||
|
||||
userRepository.updateProfileStats(userEntity.getId(), List.of("PILE", "TREND"));
|
||||
entityManager.flush();
|
||||
userRepository.updateSettings(userEntity.getId(), null,
|
||||
UserSettings.builder().language("de").theme("dark").build());
|
||||
entityManager.flush();
|
||||
entityManager.clear();
|
||||
|
||||
assertEquals(List.of("PILE", "TREND"), userRepository.findByEmail("stats@example.com").orElseThrow()
|
||||
.getSettings().getProfileStats());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateAppearance_UnknownUser() {
|
||||
assertThrows(UserNotFoundException.class, () -> userRepository.updateAppearance(-1L, Appearance.DEFAULT));
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.mockito.InOrder;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
@@ -61,6 +62,13 @@ class UserServiceTest extends AbstractUnitTest {
|
||||
assertThrows(UserNotFoundException.class, () -> userService.getUserByEmail(email));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateProfileStats_StoresEachGroupOnce() {
|
||||
userService.updateProfileStats(1L, List.of("PILE", "TREND", "PILE"));
|
||||
|
||||
verify(userRepository).updateProfileStats(1L, List.of("PILE", "TREND"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUserSettings_NeverTouchesTheSteamIdAndIssuesNoToken() {
|
||||
var userSettings = UserSettings.builder().language("en").build();
|
||||
|
||||
Reference in new issue
Block a user