Public Access
feat(user): add owner-lists opt-in setting and user listing endpoint
This commit is contained in:
1 parent
d9b816904a
commit
d924c09953
35 files changed
+693
-40
No files matched your search
@@ -0,0 +1,77 @@
|
||||
package auth_service.modules.account.api;
|
||||
|
||||
import auth_service.configuration.security.ClientHeaderFilter;
|
||||
import auth_service.shared.account.AccountStatePublisher;
|
||||
import auth_service.shared.api.AuthCookies;
|
||||
import auth_service.shared.test.AbstractIntegrationTest;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.mockito.Mockito.timeout;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* A listed user (#26) who deletes the account. Core is not reachable in tests, like Core down in production.
|
||||
*/
|
||||
class DeletedAccountListingTest extends AbstractIntegrationTest {
|
||||
|
||||
private static final String EMAIL = "delete-listed@example.com";
|
||||
private static final String PASSWORD = "correct horse battery";
|
||||
|
||||
@MockitoBean
|
||||
private AccountStatePublisher accountStatePublisher;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
private long userId;
|
||||
|
||||
@BeforeEach
|
||||
void seedListedUser() {
|
||||
jdbc.update("DELETE FROM users WHERE email = ?", EMAIL);
|
||||
userId = jdbc.queryForObject("INSERT INTO users (email, password, enabled, display_name) VALUES (?, ?, true, ?) "
|
||||
+ "RETURNING id", Long.class, EMAIL, passwordEncoder.encode(PASSWORD), "DeleteListed");
|
||||
jdbc.update("INSERT INTO user_settings (user_id, language, theme, public_profile, show_in_owner_lists) "
|
||||
+ "VALUES (?, 'de', 'dark', true, true)", userId);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanUp() {
|
||||
jdbc.update("DELETE FROM users WHERE email = ?", EMAIL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteWhileCoreIsDown_PushesTheUnlistedState() throws Exception {
|
||||
Cookie access = mockMvc.perform(post("/api/auth/login")
|
||||
.header(ClientHeaderFilter.CLIENT_HEADER, "web")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"email\":\"" + EMAIL + "\",\"password\":\"" + PASSWORD + "\"}"))
|
||||
.andReturn().getResponse().getCookie(AuthCookies.ACCESS_COOKIE);
|
||||
|
||||
mockMvc.perform(delete("/api/users/me")
|
||||
.header(ClientHeaderFilter.CLIENT_HEADER, "web")
|
||||
.cookie(access)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"password\":\"" + PASSWORD + "\"}"))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
verify(accountStatePublisher, timeout(2000)).publish(userId);
|
||||
}
|
||||
}
|
||||
@@ -56,12 +56,44 @@ class AccountRepositoryTest extends AbstractPersistenceTest {
|
||||
void findStates_CarryTheSteamIdTheDisplayNameAndTheBlock() {
|
||||
jdbc.update("update users set enabled = false, steam_id = 76561198000000001, display_name = 'Alice' where id = ?", alice);
|
||||
|
||||
assertThat(repository.findState(alice)).contains(new CoreAccountState(alice, 76561198000000001L, "Alice", true, null));
|
||||
assertThat(repository.findAllStates()).containsExactly(new CoreAccountState(alice, 76561198000000001L, "Alice", true, null),
|
||||
new CoreAccountState(bob, null, null, false, null));
|
||||
assertThat(repository.findState(alice)).contains(new CoreAccountState(alice, 76561198000000001L, "Alice", true, null, false));
|
||||
assertThat(repository.findAllStates()).containsExactly(new CoreAccountState(alice, 76561198000000001L, "Alice", true, null, false),
|
||||
new CoreAccountState(bob, null, null, false, null, false));
|
||||
assertThat(repository.findState(-1L)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findStates_ListedOnlyForAnActivePublicOptedInUserWithAName() {
|
||||
long listed = insertUser("listed@example.com");
|
||||
jdbc.update("update users set display_name = 'Listed' where id = ?", listed);
|
||||
jdbc.update("update user_settings set public_profile = true, show_in_owner_lists = true where user_id = ?", listed);
|
||||
assertThat(repository.findState(listed)).get().extracting(CoreAccountState::listed).isEqualTo(true);
|
||||
|
||||
String[] breakers = {"update users set enabled = false where id = ?",
|
||||
"update users set deleted_at = now() where id = ?",
|
||||
"update users set status = 'PENDING' where id = ?",
|
||||
"update users set display_name = null where id = ?",
|
||||
"update user_settings set public_profile = false where user_id = ?",
|
||||
"update user_settings set show_in_owner_lists = false where user_id = ?"};
|
||||
for (String breaker : breakers) {
|
||||
jdbc.update("update users set enabled = true, deleted_at = null, status = 'ACTIVE', display_name = 'Listed' where id = ?", listed);
|
||||
jdbc.update("update user_settings set public_profile = true, show_in_owner_lists = true where user_id = ?", listed);
|
||||
jdbc.update(breaker, listed);
|
||||
|
||||
assertThat(repository.findState(listed)).as(breaker).get().extracting(CoreAccountState::listed).isEqualTo(false);
|
||||
assertThat(repository.findAllStates()).as(breaker).filteredOn(state -> state.userId() == listed)
|
||||
.extracting(CoreAccountState::listed).containsExactly(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void findAllStates_CarryTheListedFlag() {
|
||||
jdbc.update("update users set display_name = 'Alice' where id = ?", alice);
|
||||
jdbc.update("update user_settings set public_profile = true, show_in_owner_lists = true where user_id = ?", alice);
|
||||
|
||||
assertThat(repository.findAllStates()).extracting(CoreAccountState::listed).containsExactly(true, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findStates_CarryTheRoleChangeTime() {
|
||||
jdbc.update("update users set role_changed_at = ? where id = ?", Timestamp.from(NOW), alice);
|
||||
|
||||
@@ -31,7 +31,7 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
class AccountCleanupServiceTest extends AbstractUnitTest {
|
||||
|
||||
private static final List<CoreAccountState> STATES = List.of(new CoreAccountState(1L, 76561198000000001L, "lobo", true, null));
|
||||
private static final List<CoreAccountState> STATES = List.of(new CoreAccountState(1L, 76561198000000001L, "lobo", true, null, false));
|
||||
|
||||
private static final Duration RETENTION = Duration.ofDays(30);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import auth_service.modules.account.exception.InvalidPasswordException;
|
||||
import auth_service.modules.account.exception.LastAdminException;
|
||||
import auth_service.modules.account.exception.TooManyAttemptsException;
|
||||
import auth_service.modules.account.repo.AccountRepository;
|
||||
import auth_service.shared.account.AccountStatePublisher;
|
||||
import auth_service.shared.mail.AccountMailer;
|
||||
import auth_service.shared.security.OpaqueToken;
|
||||
import auth_service.shared.test.AbstractUnitTest;
|
||||
@@ -51,12 +52,15 @@ class AccountServiceTest extends AbstractUnitTest {
|
||||
@Mock
|
||||
private AccountMailer accountMailer;
|
||||
|
||||
@Mock
|
||||
private AccountStatePublisher accountStatePublisher;
|
||||
|
||||
private AccountService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new AccountService(accountRepository, passwordEncoder, coreAccountService, accountMailer,
|
||||
TransactionOperations.withoutTransaction(), Duration.ofDays(30));
|
||||
accountStatePublisher, TransactionOperations.withoutTransaction(), Duration.ofDays(30));
|
||||
lenient().when(accountRepository.findPasswordHashOfActive(USER_ID)).thenReturn(Optional.of("hash"));
|
||||
lenient().when(passwordEncoder.matches("right", "hash")).thenReturn(true);
|
||||
lenient().when(accountRepository.flagDeleted(eq(USER_ID), any())).thenReturn(1);
|
||||
@@ -141,6 +145,7 @@ class AccountServiceTest extends AbstractUnitTest {
|
||||
assertThatNoException().isThrownBy(() -> service.deleteAccount(USER_ID, "right"));
|
||||
verify(accountRepository).flagDeleted(eq(USER_ID), any());
|
||||
verify(accountMailer).accountDeleted(eq(USER_ID), any());
|
||||
verify(accountStatePublisher).publish(USER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -73,8 +73,8 @@ class CoreAccountServiceTest extends AbstractUnitTest {
|
||||
expect("replaceStates");
|
||||
|
||||
service.applyStates(List.of(new CoreAccountState(42L, 76561198000000001L, "lobo", true,
|
||||
Instant.parse("2026-10-04T12:00:00Z"))));
|
||||
service.replaceStates(List.of(new CoreAccountState(42L, null, "lobo", false, null)));
|
||||
Instant.parse("2026-10-04T12:00:00Z"), true)));
|
||||
service.replaceStates(List.of(new CoreAccountState(42L, null, "lobo", false, null, false)));
|
||||
|
||||
core.verify();
|
||||
}
|
||||
|
||||
+95
-2
@@ -5,24 +5,37 @@ import auth_service.modules.account.repo.AccountRepository;
|
||||
import auth_service.shared.test.AbstractUnitTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class CoreAccountStatePublisherTest extends AbstractUnitTest {
|
||||
|
||||
private static final CoreAccountState STATE = new CoreAccountState(7L, 76561198000000001L, "lobo", true, null);
|
||||
private static final CoreAccountState STATE = new CoreAccountState(7L, 76561198000000001L, "lobo", true, null, false);
|
||||
|
||||
@Mock
|
||||
private AccountRepository accountRepository;
|
||||
@@ -32,10 +45,13 @@ class CoreAccountStatePublisherTest extends AbstractUnitTest {
|
||||
|
||||
private CoreAccountStatePublisher publisher;
|
||||
|
||||
@Mock
|
||||
private TaskScheduler scheduler;
|
||||
|
||||
// Runs each task at once, so the test sees the call.
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
publisher = new CoreAccountStatePublisher(accountRepository, coreAccountService, Runnable::run);
|
||||
publisher = new CoreAccountStatePublisher(accountRepository, coreAccountService, Runnable::run, scheduler);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -71,6 +87,58 @@ class CoreAccountStatePublisherTest extends AbstractUnitTest {
|
||||
assertDoesNotThrow(() -> publisher.publish(7L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void coreDownOnce_RetriesAfter10sAndSendsTheCurrentState() {
|
||||
CoreAccountState newer = new CoreAccountState(7L, 76561198000000001L, "lobo", true, null, true);
|
||||
when(accountRepository.findState(7L)).thenReturn(Optional.of(STATE), Optional.of(newer));
|
||||
doThrow(new RestClientException("Core down")).doNothing().when(coreAccountService).applyStates(any());
|
||||
ArgumentCaptor<Runnable> retry = ArgumentCaptor.forClass(Runnable.class);
|
||||
ArgumentCaptor<Instant> at = ArgumentCaptor.forClass(Instant.class);
|
||||
|
||||
Instant before = Instant.now();
|
||||
publisher.publish(7L);
|
||||
verify(scheduler).schedule(retry.capture(), at.capture());
|
||||
assertThat(Duration.between(before, at.getValue())).isBetween(Duration.ofSeconds(9), Duration.ofSeconds(11));
|
||||
retry.getValue().run();
|
||||
|
||||
verify(coreAccountService).applyStates(List.of(STATE));
|
||||
verify(coreAccountService).applyStates(List.of(newer));
|
||||
verify(accountRepository, times(2)).findState(7L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void coreDownFourTimes_StopsAfterThreeRetries() {
|
||||
when(accountRepository.findState(7L)).thenReturn(Optional.of(STATE));
|
||||
doThrow(new RestClientException("Core down")).when(coreAccountService).applyStates(any());
|
||||
List<Runnable> tasks = new ArrayList<>();
|
||||
List<Duration> delays = new ArrayList<>();
|
||||
doAnswer(call -> {
|
||||
tasks.add(call.getArgument(0));
|
||||
delays.add(Duration.between(Instant.now(), call.<Instant>getArgument(1)));
|
||||
return null;
|
||||
}).when(scheduler).schedule(any(Runnable.class), any(Instant.class));
|
||||
|
||||
publisher.publish(7L);
|
||||
for (int next = 0; next < 3; next++) {
|
||||
tasks.get(next).run();
|
||||
}
|
||||
|
||||
assertThat(tasks).hasSize(3);
|
||||
verify(coreAccountService, times(4)).applyStates(any());
|
||||
assertThat(delays.get(0)).isBetween(Duration.ofSeconds(9), Duration.ofSeconds(11));
|
||||
assertThat(delays.get(1)).isBetween(Duration.ofSeconds(55), Duration.ofSeconds(61));
|
||||
assertThat(delays.get(2)).isBetween(Duration.ofSeconds(295), Duration.ofSeconds(301));
|
||||
}
|
||||
|
||||
@Test
|
||||
void success_SchedulesNoRetry() {
|
||||
when(accountRepository.findState(7L)).thenReturn(Optional.of(STATE));
|
||||
|
||||
publisher.publish(7L);
|
||||
|
||||
verifyNoInteractions(scheduler);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownUser_SendsNothing() {
|
||||
when(accountRepository.findState(7L)).thenReturn(Optional.empty());
|
||||
@@ -79,4 +147,29 @@ class CoreAccountStatePublisherTest extends AbstractUnitTest {
|
||||
|
||||
verify(coreAccountService, never()).applyStates(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parallelSendsOfOneUser_NeverRunAtTheSameTime() throws Exception {
|
||||
when(accountRepository.findState(7L)).thenReturn(Optional.of(STATE));
|
||||
AtomicInteger running = new AtomicInteger();
|
||||
AtomicInteger mostRunning = new AtomicInteger();
|
||||
doAnswer(call -> {
|
||||
mostRunning.accumulateAndGet(running.incrementAndGet(), Math::max);
|
||||
Thread.sleep(50);
|
||||
running.decrementAndGet();
|
||||
return null;
|
||||
}).when(coreAccountService).applyStates(any());
|
||||
ExecutorService threads = Executors.newFixedThreadPool(4);
|
||||
CoreAccountStatePublisher parallel = new CoreAccountStatePublisher(accountRepository, coreAccountService,
|
||||
threads, scheduler);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
parallel.publish(7L);
|
||||
}
|
||||
threads.shutdown();
|
||||
assertThat(threads.awaitTermination(5, TimeUnit.SECONDS)).isTrue();
|
||||
|
||||
verify(coreAccountService, times(4)).applyStates(any());
|
||||
assertThat(mostRunning.get()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package auth_service.modules.stats.api;
|
||||
|
||||
import auth_service.configuration.security.ClientHeaderFilter;
|
||||
import auth_service.modules.stats.model.UserCountKey;
|
||||
import auth_service.modules.stats.repo.StatsRepository;
|
||||
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.http.MediaType;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
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;
|
||||
|
||||
class UserListControllerTest extends AbstractIntegrationTest {
|
||||
|
||||
private static final String PASSWORD = "correct horse battery";
|
||||
private static final String VIEWER = "viewer@owner.test";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Autowired
|
||||
private StatsRepository statsRepository;
|
||||
|
||||
private final JsonMapper json = JsonMapper.builder().build();
|
||||
|
||||
private void insertUser(String email, String name, String status, boolean enabled, boolean deleted,
|
||||
boolean publicProfile, boolean optIn, Long steamId) {
|
||||
Long id = jdbc.queryForObject("INSERT INTO users (email, password, enabled, status, display_name, steam_id, deleted_at) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?, CASE WHEN ? THEN now() ELSE NULL END) RETURNING id", Long.class,
|
||||
email, passwordEncoder.encode(PASSWORD), enabled, status, name, steamId, deleted);
|
||||
jdbc.update("INSERT INTO user_settings (user_id, language, theme, public_profile, show_in_owner_lists) "
|
||||
+ "VALUES (?, 'en', 'dark', ?, ?)", id, publicProfile, optIn);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
jdbc.update("DELETE FROM users WHERE email LIKE '%@owner.test'");
|
||||
insertUser(VIEWER, "TheViewer", "ACTIVE", true, false, true, true, 76561198000000001L);
|
||||
insertUser("listed@owner.test", "VisibleLobo", "ACTIVE", true, false, true, true, 76561198000000002L);
|
||||
insertUser("a@owner.test", "HiddenOptOut", "ACTIVE", true, false, true, false, 76561198000000003L);
|
||||
insertUser("b@owner.test", "HiddenNotPublic", "ACTIVE", true, false, false, true, 76561198000000004L);
|
||||
insertUser("c@owner.test", "HiddenBlocked", "ACTIVE", false, false, true, true, 76561198000000005L);
|
||||
insertUser("d@owner.test", "HiddenDeleted", "ACTIVE", true, true, true, true, 76561198000000006L);
|
||||
insertUser("e@owner.test", "HiddenPending", "PENDING", true, false, true, true, 76561198000000007L);
|
||||
insertUser("f@owner.test", null, "ACTIVE", true, false, true, true, 76561198000000008L);
|
||||
}
|
||||
|
||||
private Cookie login() throws Exception {
|
||||
Cookie access = mockMvc.perform(post("/api/auth/login").header(ClientHeaderFilter.CLIENT_HEADER, "web")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"email\":\"" + VIEWER + "\",\"password\":\"" + PASSWORD + "\"}"))
|
||||
.andReturn().getResponse().getCookie(AuthCookies.ACCESS_COOKIE);
|
||||
return new Cookie(AuthCookies.ACCESS_COOKIE, access.getValue());
|
||||
}
|
||||
|
||||
private String list(UserCountKey key) throws Exception {
|
||||
return mockMvc.perform(get("/api/stats/users").param("key", key.name()).cookie(login()))
|
||||
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
|
||||
}
|
||||
|
||||
@Test
|
||||
void withoutLogin_Answers401() throws Exception {
|
||||
mockMvc.perform(get("/api/stats/users").param("key", "TOTAL_USERS")).andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void privateUsersNeverAppearByNameOrIdAndEveryoneIsCounted() throws Exception {
|
||||
for (UserCountKey key : UserCountKey.values()) {
|
||||
String body = list(key);
|
||||
JsonNode root = json.readTree(body);
|
||||
|
||||
assertThat(body).doesNotContain("Hidden", "TheViewer", "@owner.test");
|
||||
List<String> names = new ArrayList<>();
|
||||
root.get("listed").forEach(person -> {
|
||||
assertThat(person.propertyNames()).containsExactly("displayName");
|
||||
names.add(person.get("displayName").asString());
|
||||
});
|
||||
assertThat(names).containsExactly("VisibleLobo");
|
||||
assertThat(root.get("includesYou").asBoolean()).isTrue();
|
||||
long expected = key == UserCountKey.TOTAL_USERS ? statsRepository.countTotalUsers()
|
||||
: statsRepository.countUsersWithSteamLinked();
|
||||
assertThat(root.get("listedTotal").asLong() + root.get("privateCount").asLong() + 1).isEqualTo(expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -387,6 +387,33 @@ class UserControllerTest extends AbstractWebMvcTest {
|
||||
.andExpect(jsonPath("$.settings.statCharts.PLATFORMS").value("PIE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCurrentUser_SendsShowInOwnerLists() throws Exception {
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", null), null, Collections.emptyList())
|
||||
);
|
||||
when(userService.getUser(1L)).thenReturn(User.builder().email("test@example.com")
|
||||
.settings(UserSettings.builder().language("en").theme("dark").showInOwnerLists(true).build()).build());
|
||||
|
||||
mockMvc.perform(get("/api/users/me").accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(jsonPath("$.settings.showInOwnerLists").value(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateOwnerLists_Answers204AndPassesTheFlag_MissingEnabledIs400() throws Exception {
|
||||
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
|
||||
new UserPrincipal("1", null), null, Collections.emptyList()));
|
||||
|
||||
mockMvc.perform(put("/api/users/settings/owner-lists").contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"enabled\":true}"))
|
||||
.andExpect(status().isNoContent());
|
||||
mockMvc.perform(put("/api/users/settings/owner-lists").contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
|
||||
verify(userService).updateOwnerLists(1L, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCurrentUser_SendsWelcomeSeenAtNullForANewUser() throws Exception {
|
||||
SecurityContextHolder.getContext().setAuthentication(
|
||||
|
||||
@@ -253,6 +253,25 @@ class UserRepositoryTest extends AbstractPersistenceTest {
|
||||
assertTrue(findByEmail("public@example.com").orElseThrow().getSettings().isPublicProfile());
|
||||
}
|
||||
|
||||
@Test
|
||||
void showInOwnerLists_IsOffForANewUserAndRoundTrips() {
|
||||
var userEntity = persisted(UserEntity.builder()
|
||||
.email("owners@example.com")
|
||||
.password("password")
|
||||
.enabled(true)
|
||||
.registrationDate(LocalDateTime.now())
|
||||
.build());
|
||||
entityManager.flush();
|
||||
entityManager.clear();
|
||||
assertFalse(findByEmail("owners@example.com").orElseThrow().getSettings().isShowInOwnerLists());
|
||||
|
||||
userRepository.update(userEntity.getId(), user -> user.getSettings().setShowInOwnerLists(true));
|
||||
entityManager.flush();
|
||||
entityManager.clear();
|
||||
|
||||
assertTrue(findByEmail("owners@example.com").orElseThrow().getSettings().isShowInOwnerLists());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateSettings_KeepsTheAppearance() {
|
||||
var userEntity = persisted(UserEntity.builder()
|
||||
|
||||
@@ -153,7 +153,7 @@ class UserServiceTest extends AbstractUnitTest {
|
||||
@Test
|
||||
void updateUserSettings_UnchangedDisplayName_PublishesNothing() {
|
||||
var userSettings = UserSettings.builder().language("en").build();
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(User.builder().id(1L).displayName("Lobo").build()));
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(stored("Lobo", false)));
|
||||
|
||||
userService.updateUserSettings(1L, "Lobo", userSettings);
|
||||
|
||||
@@ -161,6 +161,60 @@ class UserServiceTest extends AbstractUnitTest {
|
||||
verifyNoInteractions(accountStatePublisher);
|
||||
}
|
||||
|
||||
private static User stored(String displayName, boolean publicProfile) {
|
||||
return User.builder().id(1L).displayName(displayName)
|
||||
.settings(UserSettings.builder().publicProfile(publicProfile).build()).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUserSettings_PublicProfileChange_Publishes() {
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(stored("Lobo", true)));
|
||||
|
||||
userService.updateUserSettings(1L, "Lobo", UserSettings.builder().language("en").publicProfile(false).build());
|
||||
|
||||
verify(accountStatePublisher).publish(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUserSettings_NameRemoval_Publishes() {
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(stored("Lobo", false)));
|
||||
|
||||
userService.updateUserSettings(1L, null, UserSettings.builder().language("en").build());
|
||||
|
||||
verify(accountStatePublisher).publish(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUserSettings_ThemeOnly_PublishesNothing() {
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(stored("Lobo", true)));
|
||||
|
||||
userService.updateUserSettings(1L, "Lobo", UserSettings.builder().theme("light").publicProfile(true).build());
|
||||
|
||||
verifyNoInteractions(accountStatePublisher);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void updateUserSettings_KeepsShowInOwnerLists() {
|
||||
when(userRepository.findById(1L)).thenReturn(Optional.of(stored("Lobo", true)));
|
||||
|
||||
userService.updateUserSettings(1L, "Lobo", UserSettings.builder().publicProfile(true).build());
|
||||
|
||||
ArgumentCaptor<Consumer<User>> change = ArgumentCaptor.forClass(Consumer.class);
|
||||
verify(userRepository).update(eq(1L), change.capture());
|
||||
User user = User.builder().id(1L).settings(UserSettings.builder().showInOwnerLists(true).build()).build();
|
||||
change.getValue().accept(user);
|
||||
assertTrue(user.getSettings().isShowInOwnerLists());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateOwnerLists_StoresAndPublishes() {
|
||||
userService.updateOwnerLists(1L, true);
|
||||
|
||||
assertTrue(appliedChanges(1).getFirst().getSettings().isShowInOwnerLists());
|
||||
verify(accountStatePublisher).publish(1L);
|
||||
}
|
||||
|
||||
private static final long STEAM_ID = 76561198000000001L;
|
||||
|
||||
@Test
|
||||
|
||||
@@ -37,12 +37,12 @@
|
||||
"applyStates": {
|
||||
"method": "POST",
|
||||
"path": "/internal/accounts/states",
|
||||
"request": {"accounts": [{"userId": 42, "steamId": 76561198000000001, "displayName": "lobo", "blocked": true, "roleChangedAt": "2026-10-04T12:00:00Z"}]}
|
||||
"request": {"accounts": [{"userId": 42, "steamId": 76561198000000001, "displayName": "lobo", "blocked": true, "roleChangedAt": "2026-10-04T12:00:00Z", "listed": true}]}
|
||||
},
|
||||
"replaceStates": {
|
||||
"method": "PUT",
|
||||
"path": "/internal/accounts/states",
|
||||
"request": {"accounts": [{"userId": 42, "steamId": null, "displayName": "lobo", "blocked": false, "roleChangedAt": null}]}
|
||||
"request": {"accounts": [{"userId": 42, "steamId": null, "displayName": "lobo", "blocked": false, "roleChangedAt": null, "listed": false}]}
|
||||
},
|
||||
"purge": {
|
||||
"method": "POST",
|
||||
|
||||
Reference in new issue
Block a user