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
@@ -77,12 +77,13 @@ public class JwtService implements TokenGenerator, ProfileTicketIssuer, PublicKe
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateProfileTicket(long userId) {
|
||||
public String generateProfileTicket(long userId, List<String> profileStats) {
|
||||
return Jwts.builder()
|
||||
.header().keyId(jwtConfig.getKid()).and()
|
||||
.subject(PROFILE_TICKET_TYPE)
|
||||
.claim(TYPE_CLAIM, PROFILE_TICKET_TYPE)
|
||||
.claim("profileUserId", String.valueOf(userId))
|
||||
.claim("profileStats", profileStats)
|
||||
.issuedAt(new Date(System.currentTimeMillis()))
|
||||
.expiration(new Date(System.currentTimeMillis() + PROFILE_TICKET_EXPIRATION_MS))
|
||||
.signWith(privateKey, Jwts.SIG.EdDSA)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package auth_service.modules.profile.model;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public record PublicProfile(long userId, String displayName, LocalDate memberSince) {}
|
||||
public record PublicProfile(long userId, String displayName, LocalDate memberSince, List<String> profileStats) {}
|
||||
@@ -4,6 +4,7 @@ import auth_service.modules.profile.model.PublicProfile;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
@@ -20,13 +21,14 @@ public class PublicProfileRepository {
|
||||
*/
|
||||
public Optional<PublicProfile> findPublicByDisplayName(String displayName) {
|
||||
return jdbc.query("""
|
||||
SELECT u.id, u.display_name, u.registration_date
|
||||
SELECT u.id, u.display_name, u.registration_date, s.profile_stats
|
||||
FROM users u JOIN user_settings s ON s.user_id = u.id
|
||||
WHERE LOWER(u.display_name) = LOWER(?) AND u.enabled AND s.public_profile
|
||||
""",
|
||||
(rs, row) -> new PublicProfile(rs.getLong("id"), rs.getString("display_name"),
|
||||
rs.getTimestamp("registration_date") != null
|
||||
? rs.getTimestamp("registration_date").toLocalDateTime().toLocalDate() : null),
|
||||
? rs.getTimestamp("registration_date").toLocalDateTime().toLocalDate() : null,
|
||||
List.of((String[]) rs.getArray("profile_stats").getArray())),
|
||||
displayName).stream().findFirst();
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,6 @@ public class PublicProfileService {
|
||||
*/
|
||||
public Optional<TicketedProfile> findPublicProfile(String displayName) {
|
||||
return publicProfileRepository.findPublicByDisplayName(displayName)
|
||||
.map(profile -> new TicketedProfile(profile, profileTicketIssuer.generateProfileTicket(profile.userId())));
|
||||
.map(profile -> new TicketedProfile(profile, profileTicketIssuer.generateProfileTicket(profile.userId(), profile.profileStats())));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package auth_service.modules.user.api;
|
||||
|
||||
import auth_service.shared.security.UserPrincipal;
|
||||
import auth_service.modules.user.api.dto.request.AppearanceRequest;
|
||||
import auth_service.modules.user.api.dto.request.ProfileStatsRequest;
|
||||
import auth_service.modules.user.api.dto.request.UserSettingsRequest;
|
||||
import auth_service.modules.user.api.dto.response.UserProfileResponse;
|
||||
import auth_service.modules.user.api.mapper.ApiMapper;
|
||||
@@ -45,6 +46,13 @@ public class UserController {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PutMapping(value = "/settings/profile-stats", version = "1.0")
|
||||
public ResponseEntity<Void> updateProfileStats(@AuthenticationPrincipal UserPrincipal principal,
|
||||
@Valid @RequestBody ProfileStatsRequest request) {
|
||||
this.userService.updateProfileStats(principal.getIdAsLong(), request.groups());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PutMapping(value = "/settings/appearance", version = "1.0")
|
||||
public ResponseEntity<Void> updateAppearance(@AuthenticationPrincipal UserPrincipal principal,
|
||||
@Valid @RequestBody AppearanceRequest request) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package auth_service.modules.user.api.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @param groups the stat groups of Core the public profile shows; empty shows none
|
||||
*/
|
||||
public record ProfileStatsRequest(
|
||||
@NotNull List<@NotNull @Pattern(regexp = "PILE|TOP_GAMES|TASTE|HABITS|FINISHED|TREND") String> groups) {
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package auth_service.modules.user.api.dto.response;
|
||||
|
||||
import lombok.Builder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Builder
|
||||
public record SettingsResponse(long id, boolean useDarkTheme, String language, boolean publicProfile,
|
||||
AppearanceResponse appearance) {
|
||||
AppearanceResponse appearance, List<String> profileStats) {
|
||||
}
|
||||
@@ -30,6 +30,7 @@ public abstract class ApiMapper {
|
||||
.language(user.getSettings().getLanguage())
|
||||
.publicProfile(user.getSettings().isPublicProfile())
|
||||
.appearance(toApi(user.getSettings().getAppearance()))
|
||||
.profileStats(user.getSettings().getProfileStats())
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package auth_service.modules.user.model;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class UserSettings {
|
||||
@@ -11,4 +13,7 @@ public class UserSettings {
|
||||
private boolean publicProfile;
|
||||
@Builder.Default
|
||||
private Appearance appearance = Appearance.DEFAULT;
|
||||
/** The stat groups of Core the public profile shows. */
|
||||
@Builder.Default
|
||||
private List<String> profileStats = List.of();
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import auth_service.modules.user.model.UserSettings;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
@@ -31,6 +32,8 @@ public interface UserRepository {
|
||||
@Transactional
|
||||
void updateAppearance(long userId, Appearance appearance);
|
||||
|
||||
void updateProfileStats(long userId, List<String> groups);
|
||||
|
||||
Optional<SteamIdHolder> findSteamIdHolder(long steamId);
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,8 @@ import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -86,21 +88,7 @@ public class UserRepositoryImpl implements UserRepository {
|
||||
@Transactional
|
||||
@Override
|
||||
public void updateAppearance(long userId, Appearance appearance) {
|
||||
var userEntity = entityManager.find(UserEntity.class, userId);
|
||||
if (userEntity == null) {
|
||||
throw new UserNotFoundException("Could not find user with id " + userId);
|
||||
}
|
||||
|
||||
var settingsEntity = userEntity.getSettings();
|
||||
if (settingsEntity == null) {
|
||||
// Same values as the defaults of the user_settings columns.
|
||||
settingsEntity = new UserSettingsEntity();
|
||||
settingsEntity.setUserId(userId);
|
||||
settingsEntity.setLanguage("");
|
||||
settingsEntity.setTheme("light");
|
||||
userEntity.setSettings(settingsEntity);
|
||||
}
|
||||
|
||||
UserSettingsEntity settingsEntity = settingsOf(userId);
|
||||
settingsEntity.setAccent(appearance.accent());
|
||||
settingsEntity.setCorners(appearance.corners());
|
||||
settingsEntity.setWideLayout(appearance.wideLayout());
|
||||
@@ -109,6 +97,31 @@ public class UserRepositoryImpl implements UserRepository {
|
||||
settingsEntity.setShowBanner(appearance.showBanner());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void updateProfileStats(long userId, List<String> groups) {
|
||||
settingsOf(userId).setProfileStats(new ArrayList<>(groups));
|
||||
}
|
||||
|
||||
/**
|
||||
* The settings row of the user, created with the defaults of the user_settings columns when there is none.
|
||||
*/
|
||||
private UserSettingsEntity settingsOf(long userId) {
|
||||
UserEntity userEntity = entityManager.find(UserEntity.class, userId);
|
||||
if (userEntity == null) {
|
||||
throw new UserNotFoundException("Could not find user with id " + userId);
|
||||
}
|
||||
UserSettingsEntity settingsEntity = userEntity.getSettings();
|
||||
if (settingsEntity == null) {
|
||||
settingsEntity = new UserSettingsEntity();
|
||||
settingsEntity.setUserId(userId);
|
||||
settingsEntity.setLanguage("");
|
||||
settingsEntity.setTheme("light");
|
||||
userEntity.setSettings(settingsEntity);
|
||||
}
|
||||
return settingsEntity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<SteamIdHolder> findSteamIdHolder(long steamId) {
|
||||
// A projection, not the entity: the bulk updates that follow would leave a loaded entity stale.
|
||||
|
||||
@@ -7,6 +7,11 @@ import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "user_settings")
|
||||
@@ -46,4 +51,8 @@ public class UserSettingsEntity {
|
||||
|
||||
@Column(name = "show_banner", nullable = false)
|
||||
private boolean showBanner = Appearance.DEFAULT.showBanner();
|
||||
|
||||
@JdbcTypeCode(SqlTypes.ARRAY)
|
||||
@Column(name = "profile_stats", nullable = false)
|
||||
private List<String> profileStats = new ArrayList<>();
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import auth_service.modules.user.model.UserSettings;
|
||||
import auth_service.modules.user.persistence.entities.UserEntity;
|
||||
import auth_service.modules.user.persistence.entities.UserSettingsEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class EntityMapper {
|
||||
|
||||
private EntityMapper() {
|
||||
@@ -26,6 +28,7 @@ public abstract class EntityMapper {
|
||||
.appearance(new Appearance(entity.getSettings().getAccent(), entity.getSettings().getCorners(),
|
||||
entity.getSettings().isWideLayout(), entity.getSettings().getBackgroundAppid(),
|
||||
entity.getSettings().getBannerAppid(), entity.getSettings().isShowBanner()))
|
||||
.profileStats(List.copyOf(entity.getSettings().getProfileStats()))
|
||||
.build();
|
||||
} else {
|
||||
// Fallback default settings for users without an entry in the database
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@@ -87,4 +88,13 @@ public class UserService {
|
||||
public void updateAppearance(long userId, Appearance appearance) {
|
||||
userRepository.updateAppearance(userId, appearance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores each group once; the request may repeat one.
|
||||
*/
|
||||
@Transactional
|
||||
@CacheEvict(value = "usersByEmail", allEntries = true)
|
||||
public void updateProfileStats(long userId, List<String> groups) {
|
||||
userRepository.updateProfileStats(userId, groups.stream().distinct().toList());
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
package auth_service.shared.security;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Port for the short-lived ticket that lets Core show the public profile data of one user. The ticket is signed
|
||||
* like a login token but is no login: the login filters of Auth and Core reject it.
|
||||
*/
|
||||
public interface ProfileTicketIssuer {
|
||||
String generateProfileTicket(long userId);
|
||||
/**
|
||||
* @param profileStats the stat groups of Core the profile shows
|
||||
*/
|
||||
String generateProfileTicket(long userId, List<String> profileStats);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
-- The stat groups of Core's stats page the user shows on the public profile, e.g. {PILE,TREND}. Empty for everyone
|
||||
-- until they pick some.
|
||||
ALTER TABLE user_settings ADD COLUMN profile_stats VARCHAR(16)[] NOT NULL DEFAULT '{}';
|
||||
@@ -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