feat(user): add owner-lists opt-in setting and user listing endpoint
Build, Test & Push Auth Service / quality-check (push) Successful in 1m5s
Build, Test & Push Auth Service / docker (push) Successful in 1m31s

This commit is contained in:
lobo committed 2026-10-06 13:19:58 +02:00
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();
}
@@ -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",