Public Access
feat(account): hide deleted accounts and clean up their rows
Auth marks a deleted user in deleted_user (no sync, not in landing stats or pile snapshots) and later purges the rows through /internal/accounts with a service token. Prune removes rows of user ids Auth does not know. Dry run by default.
This commit is contained in:
1 parent
88aae0101f
commit
351cda411d
28 files changed
+941
-16
No files matched your search
@@ -136,4 +136,34 @@ class JwtServiceTest extends AbstractUnitTest {
|
||||
void parseLoginToken_ProfileTicketIsNoLogin() {
|
||||
assertEquals(Optional.empty(), jwtService.parseLoginToken(signTicket("public-profile", "42", 60_000)));
|
||||
}
|
||||
|
||||
private String signServiceToken(PrivateKey key, String type, Long expiresInMs) {
|
||||
io.jsonwebtoken.JwtBuilder builder = Jwts.builder().subject("tbp-auth").claim("typ", type).issuedAt(new Date());
|
||||
if (expiresInMs != null) {
|
||||
builder.expiration(new Date(System.currentTimeMillis() + expiresInMs));
|
||||
}
|
||||
return builder.signWith(key, Jwts.SIG.EdDSA).compact();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isServiceToken_AcceptsAServiceTokenOfAuth() {
|
||||
assertTrue(jwtService.isServiceToken(signServiceToken(signingKey, "service", 60_000L)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isServiceToken_RejectsEverythingElse() throws Exception {
|
||||
PrivateKey otherKey = KeyPairGenerator.getInstance("Ed25519").generateKeyPair().getPrivate();
|
||||
|
||||
assertFalse(jwtService.isServiceToken(signServiceToken(signingKey, "service", null)), "no exp");
|
||||
assertFalse(jwtService.isServiceToken(signServiceToken(signingKey, "service", -1_000L)), "expired");
|
||||
assertFalse(jwtService.isServiceToken(signServiceToken(otherKey, "service", 60_000L)), "foreign key");
|
||||
assertFalse(jwtService.isServiceToken(signToken("42", "a@b.de", null)), "login token");
|
||||
assertFalse(jwtService.isServiceToken(signTicket("public-profile", "42", 60_000)), "profile ticket");
|
||||
assertFalse(jwtService.isServiceToken("garbage"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseLoginToken_ServiceTokenIsNoLogin() {
|
||||
assertEquals(Optional.empty(), jwtService.parseLoginToken(signServiceToken(signingKey, "service", 60_000L)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package core_service.configuration.security;
|
||||
|
||||
import core_service.shared.test.AbstractUnitTest;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
|
||||
class ServiceTokenFilterTest extends AbstractUnitTest {
|
||||
|
||||
@Mock
|
||||
private JwtService jwtService;
|
||||
|
||||
@AfterEach
|
||||
void clearContext() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
private Authentication authenticationAfter(MockHttpServletRequest request) throws Exception {
|
||||
lenient().when(jwtService.isServiceToken("good")).thenReturn(true);
|
||||
new ServiceTokenFilter(jwtService).doFilter(request, new MockHttpServletResponse(), new MockFilterChain());
|
||||
return SecurityContextHolder.getContext().getAuthentication();
|
||||
}
|
||||
|
||||
@Test
|
||||
void bearerServiceTokenGivesTheServiceAuthority() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader("Authorization", "Bearer good");
|
||||
|
||||
assertThat(authenticationAfter(request).getAuthorities()).extracting(GrantedAuthority::getAuthority)
|
||||
.containsExactly(ServiceTokenFilter.SERVICE_AUTHORITY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cookieAndInvalidTokensAuthenticateNothing() throws Exception {
|
||||
MockHttpServletRequest cookie = new MockHttpServletRequest();
|
||||
cookie.setCookies(new Cookie(AccessTokens.ACCESS_COOKIE, "good"));
|
||||
assertThat(authenticationAfter(cookie)).isNull();
|
||||
|
||||
MockHttpServletRequest invalid = new MockHttpServletRequest();
|
||||
invalid.addHeader("Authorization", "Bearer bad");
|
||||
assertThat(authenticationAfter(invalid)).isNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package core_service.modules.account.api;
|
||||
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import io.jsonwebtoken.JwtBuilder;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
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.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* The real (non-dev) filter chain, like ExpiredAccessTokenTest, so both share one Spring context.
|
||||
*/
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles(value = "prod", inheritProfiles = false)
|
||||
class InternalAccountControllerTest extends AbstractIntegrationTest {
|
||||
|
||||
private static final long USER_ID = 910_001L;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@AfterEach
|
||||
void cleanUp() {
|
||||
jdbc.update("delete from deleted_user where user_id = ?", USER_ID);
|
||||
}
|
||||
|
||||
private static String token(JwtBuilder builder) {
|
||||
return builder.expiration(new Date(System.currentTimeMillis() + 60_000))
|
||||
.signWith(TEST_KEYS.getPrivate(), Jwts.SIG.EdDSA).compact();
|
||||
}
|
||||
|
||||
private static String serviceToken() {
|
||||
return token(Jwts.builder().subject("tbp-auth").claim("typ", "service"));
|
||||
}
|
||||
|
||||
private static String loginToken() {
|
||||
return token(Jwts.builder().subject("a@b.de").claim("userId", String.valueOf(USER_ID)));
|
||||
}
|
||||
|
||||
private ResultActions send(MockHttpServletRequestBuilder request, String body, String token) throws Exception {
|
||||
request.contentType(MediaType.APPLICATION_JSON).content(body);
|
||||
if (token != null) {
|
||||
request.header("Authorization", "Bearer " + token);
|
||||
}
|
||||
return mockMvc.perform(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withoutAServiceToken_Is401() throws Exception {
|
||||
String body = "{\"userIds\":[" + USER_ID + "]}";
|
||||
|
||||
send(post("/internal/accounts/deleted"), body, null).andExpect(status().isUnauthorized());
|
||||
send(post("/internal/accounts/deleted"), body, loginToken()).andExpect(status().isUnauthorized());
|
||||
assertThat(jdbc.queryForList("select user_id from deleted_user where user_id = ?", Long.class, USER_ID)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void serviceToken_IsNoLoginForTheApi() throws Exception {
|
||||
mockMvc.perform(get("/api/library/summary").header("Authorization", "Bearer " + serviceToken()))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void markThenPurge_InDryRunCountsAndKeepsTheTombstone() throws Exception {
|
||||
String body = "{\"userIds\":[" + USER_ID + "]}";
|
||||
|
||||
send(post("/internal/accounts/deleted"), body, serviceToken()).andExpect(status().isNoContent());
|
||||
send(post("/internal/accounts/purge"), "{\"userIds\":[" + USER_ID + "],\"dryRun\":false}", serviceToken())
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.dryRun").value(true))
|
||||
.andExpect(jsonPath("$.users[0]").value(USER_ID))
|
||||
.andExpect(jsonPath("$.rows.user_game").value(0))
|
||||
.andExpect(jsonPath("$.refused").value(false));
|
||||
|
||||
assertThat(jdbc.queryForList("select user_id from deleted_user where user_id = ?", Long.class, USER_ID))
|
||||
.containsExactly(USER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceDeleted_RemovesTombstonesAuthNoLongerFlags() throws Exception {
|
||||
send(post("/internal/accounts/deleted"), "{\"userIds\":[" + USER_ID + "]}", serviceToken())
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
send(put("/internal/accounts/deleted"), "{\"userIds\":[]}", serviceToken()).andExpect(status().isNoContent());
|
||||
|
||||
assertThat(jdbc.queryForList("select user_id from deleted_user where user_id = ?", Long.class, USER_ID)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void prune_WithAnEmptyListIs400() throws Exception {
|
||||
send(post("/internal/accounts/prune"), "{\"knownUserIds\":[],\"dryRun\":false}", serviceToken()).andExpect(status().isBadRequest());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package core_service.modules.account.api;
|
||||
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* The dev profile authenticates every API request as user 1; /internal must still need a service token.
|
||||
*/
|
||||
@AutoConfigureMockMvc
|
||||
class InternalAccountDevProfileTest extends AbstractIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void withoutAServiceToken_Is401() throws Exception {
|
||||
mockMvc.perform(post("/internal/accounts/deleted").contentType(MediaType.APPLICATION_JSON).content("{\"userIds\":[1]}"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package core_service.modules.account.repo;
|
||||
|
||||
import core_service.shared.test.AbstractPersistenceTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Import(AccountCleanupRepository.class)
|
||||
class AccountCleanupRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
private static final List<String> USER_TABLES = List.of("user_game_status_change", "user_game_achievement",
|
||||
"user_game_achievement_meta", "user_game_playtime_day", "wishlist_game", "backlog_snapshot",
|
||||
"user_steam_sync", "user_game");
|
||||
|
||||
@Autowired
|
||||
private AccountCleanupRepository repository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
jdbc.update("delete from backlog_snapshot");
|
||||
jdbc.update("insert into steam_game (appid, name) values (220, 'Half-Life 2') on conflict do nothing");
|
||||
for (long userId : List.of(1L, 2L)) {
|
||||
// The V14 trigger adds the user_game_status_change row.
|
||||
jdbc.update("insert into user_game (user_id, appid, status) values (?, 220, 'BACKLOG')", userId);
|
||||
jdbc.update("insert into user_game_achievement (user_id, appid, api_name) values (?, 220, 'ACH_1')", userId);
|
||||
jdbc.update("insert into user_game_achievement_meta (user_id, appid) values (?, 220)", userId);
|
||||
jdbc.update("insert into user_game_playtime_day (user_id, day, appid, minutes) values (?, current_date, 220, 30)", userId);
|
||||
jdbc.update("insert into wishlist_game (user_id, appid) values (?, 620)", userId);
|
||||
jdbc.update("""
|
||||
insert into backlog_snapshot (user_id, day, backlog_games, never_played, backlog_minutes, listed_games)
|
||||
values (?, current_date, 1, 1, 0, 1)""", userId);
|
||||
jdbc.update("""
|
||||
insert into user_steam_sync (user_id, steam_id, user_seen_at) values (?, '76561198000000001', now())""",
|
||||
userId);
|
||||
}
|
||||
}
|
||||
|
||||
private int rows(String table, long userId) {
|
||||
Integer count = jdbc.queryForObject("select count(*) from " + table + " where user_id = ?", Integer.class, userId);
|
||||
return count != null ? count : 0;
|
||||
}
|
||||
|
||||
private List<Long> deleted() {
|
||||
return jdbc.queryForList("select user_id from deleted_user order by user_id", Long.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addDeleted_IsIdempotentAndStopsTheNightlySync() {
|
||||
repository.addDeleted(List.of(1L));
|
||||
repository.addDeleted(List.of(1L));
|
||||
|
||||
assertThat(deleted()).containsExactly(1L);
|
||||
assertThat(jdbc.queryForObject("select user_seen_at from user_steam_sync where user_id = 1", Object.class)).isNull();
|
||||
assertThat(jdbc.queryForObject("select user_seen_at from user_steam_sync where user_id = 2", Object.class)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceDeleted_KeepsOnlyTheGivenUsers() {
|
||||
repository.addDeleted(List.of(1L, 2L));
|
||||
|
||||
repository.replaceDeleted(List.of(2L, 3L));
|
||||
assertThat(deleted()).containsExactly(2L, 3L);
|
||||
|
||||
repository.replaceDeleted(List.of());
|
||||
assertThat(deleted()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findDeleted_ReturnsOnlyTombstonedUsers() {
|
||||
repository.addDeleted(List.of(2L));
|
||||
|
||||
assertThat(repository.findDeleted(List.of(1L, 2L))).containsExactly(2L);
|
||||
assertThat(repository.findDeleted(List.of())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void countRows_CountsPerTableAndDeletesNothing() {
|
||||
Map<String, Integer> counts = repository.countRows(List.of(1L));
|
||||
|
||||
assertThat(counts.keySet()).containsAll(USER_TABLES);
|
||||
USER_TABLES.forEach(table -> assertThat(counts.get(table)).as(table).isEqualTo(1));
|
||||
USER_TABLES.forEach(table -> assertThat(rows(table, 1L)).as(table).isEqualTo(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteRows_RemovesEveryRowOfTheUserAndKeepsTheOthers() {
|
||||
repository.addDeleted(List.of(1L));
|
||||
|
||||
Map<String, Integer> deletedRows = repository.deleteRows(List.of(1L));
|
||||
|
||||
USER_TABLES.forEach(table -> assertThat(deletedRows.get(table)).as(table).isEqualTo(1));
|
||||
assertThat(deletedRows.get("deleted_user")).isEqualTo(1);
|
||||
USER_TABLES.forEach(table -> assertThat(rows(table, 1L)).as(table).isZero());
|
||||
USER_TABLES.forEach(table -> assertThat(rows(table, 2L)).as(table).isEqualTo(1));
|
||||
assertThat(deleted()).isEmpty();
|
||||
assertThat(jdbc.queryForObject("select count(*) from steam_game where appid = 220", Integer.class)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findOrphans_ReturnsUsersNotInTheList() {
|
||||
repository.addDeleted(List.of(7L));
|
||||
|
||||
assertThat(repository.findOrphans(List.of(2L))).containsExactly(1L, 7L);
|
||||
assertThat(repository.findOrphans(List.of(1L, 2L, 7L))).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package core_service.modules.account.service;
|
||||
|
||||
import core_service.modules.account.domain.CleanupResult;
|
||||
import core_service.modules.account.repo.AccountCleanupRepository;
|
||||
import core_service.shared.test.AbstractUnitTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class AccountCleanupServiceTest extends AbstractUnitTest {
|
||||
|
||||
private static final Map<String, Integer> ROWS = Map.of("user_game", 3);
|
||||
|
||||
@Mock
|
||||
private AccountCleanupRepository repository;
|
||||
|
||||
private AccountCleanupService service(boolean dryRun) {
|
||||
return new AccountCleanupService(repository, dryRun, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void purge_OnlyTouchesTombstonedUsers() {
|
||||
when(repository.findDeleted(List.of(1L, 2L))).thenReturn(List.of(2L));
|
||||
when(repository.deleteRows(List.of(2L))).thenReturn(ROWS);
|
||||
|
||||
CleanupResult result = service(false).purge(List.of(1L, 2L), false);
|
||||
|
||||
assertThat(result).isEqualTo(new CleanupResult(false, List.of(2L), ROWS, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void purge_InDryRunOnlyCounts() {
|
||||
when(repository.findDeleted(List.of(2L))).thenReturn(List.of(2L));
|
||||
when(repository.countRows(List.of(2L))).thenReturn(ROWS);
|
||||
|
||||
CleanupResult result = service(true).purge(List.of(2L), false);
|
||||
|
||||
assertThat(result).isEqualTo(new CleanupResult(true, List.of(2L), ROWS, false));
|
||||
verify(repository, never()).deleteRows(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void prune_RefusesAnEmptyList() {
|
||||
assertThatThrownBy(() -> service(false).prune(List.of(), false)).isInstanceOf(IllegalArgumentException.class);
|
||||
verify(repository, never()).findOrphans(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void prune_RefusesMoreOrphansThanAllowed() {
|
||||
when(repository.findOrphans(List.of(9L))).thenReturn(List.of(1L, 2L, 3L));
|
||||
|
||||
CleanupResult result = service(false).prune(List.of(9L), false);
|
||||
|
||||
assertThat(result).isEqualTo(new CleanupResult(false, List.of(1L, 2L, 3L), Map.of(), true));
|
||||
verify(repository, never()).deleteRows(any());
|
||||
verify(repository, never()).countRows(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void prune_DeletesTheOrphans() {
|
||||
when(repository.findOrphans(List.of(9L))).thenReturn(List.of(1L, 2L));
|
||||
when(repository.deleteRows(List.of(1L, 2L))).thenReturn(ROWS);
|
||||
|
||||
assertThat(service(false).prune(List.of(9L), false)).isEqualTo(new CleanupResult(false, List.of(1L, 2L), ROWS, false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void prune_InDryRunOnlyCounts() {
|
||||
when(repository.findOrphans(List.of(9L))).thenReturn(List.of(1L));
|
||||
when(repository.countRows(List.of(1L))).thenReturn(ROWS);
|
||||
|
||||
assertThat(service(true).prune(List.of(9L), false)).isEqualTo(new CleanupResult(true, List.of(1L), ROWS, false));
|
||||
verify(repository, never()).deleteRows(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void prune_WithoutOrphansDoesNothing() {
|
||||
when(repository.findOrphans(List.of(9L))).thenReturn(List.of());
|
||||
|
||||
assertThat(service(false).prune(List.of(9L), false)).isEqualTo(new CleanupResult(false, List.of(), Map.of(), false));
|
||||
verify(repository, never()).deleteRows(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void purge_AuthsDryRunOnlyCounts() {
|
||||
when(repository.findDeleted(List.of(2L))).thenReturn(List.of(2L));
|
||||
when(repository.countRows(List.of(2L))).thenReturn(ROWS);
|
||||
|
||||
assertThat(service(false).purge(List.of(2L), true)).isEqualTo(new CleanupResult(true, List.of(2L), ROWS, false));
|
||||
verify(repository, never()).deleteRows(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void prune_AuthsDryRunOnlyCounts() {
|
||||
when(repository.findOrphans(List.of(9L))).thenReturn(List.of(1L));
|
||||
when(repository.countRows(List.of(1L))).thenReturn(ROWS);
|
||||
|
||||
assertThat(service(false).prune(List.of(9L), true)).isEqualTo(new CleanupResult(true, List.of(1L), ROWS, false));
|
||||
verify(repository, never()).deleteRows(any());
|
||||
}
|
||||
}
|
||||
@@ -103,4 +103,15 @@ class BacklogSnapshotRepositoryTest extends AbstractPersistenceTest {
|
||||
assertThat(repository.live(1L, DAY)).isEqualTo(new UserStats.PileDay(DAY, 1, 1, 600));
|
||||
assertThat(repository.live(3L, DAY)).isEqualTo(new UserStats.PileDay(DAY, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeAll_SkipsDeletedUsers() {
|
||||
userGameRepository.saveAllAndFlush(List.of(game(1L, 220L, 0, GameStatus.BACKLOG),
|
||||
game(2L, 220L, 0, GameStatus.BACKLOG)));
|
||||
jdbc.update("insert into deleted_user (user_id) values (2)");
|
||||
|
||||
assertThat(repository.writeAll(DAY)).isEqualTo(1);
|
||||
|
||||
assertThat(repository.findSince(2L, DAY)).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package core_service.modules.library.repo;
|
||||
|
||||
import core_service.modules.library.domain.UserSteamSync;
|
||||
import core_service.shared.test.AbstractPersistenceTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class UserSteamSyncRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
private static final String STEAM_ID = "76561198000000001";
|
||||
|
||||
@Autowired
|
||||
private UserSteamSyncRepository repository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
private int claim(long userId) {
|
||||
return repository.tryClaimRun(userId, STEAM_ID, Instant.now().minusSeconds(600));
|
||||
}
|
||||
|
||||
@Test
|
||||
void tryClaimRun_ClaimsForALiveUser() {
|
||||
assertThat(claim(1L)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tryClaimRun_RefusesADeletedUserWithoutASyncRow() {
|
||||
jdbc.update("insert into deleted_user (user_id) values (2)");
|
||||
|
||||
assertThat(claim(2L)).isZero();
|
||||
assertThat(repository.findById(2L)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void tryClaimRun_RefusesADeletedUserWithAnIdleSyncRow() {
|
||||
repository.saveAndFlush(new UserSteamSync(3L, STEAM_ID));
|
||||
jdbc.update("insert into deleted_user (user_id) values (3)");
|
||||
|
||||
assertThat(claim(3L)).isZero();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import core_service.shared.test.AbstractPersistenceTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -28,6 +29,9 @@ class StatsRepositoryTest extends AbstractPersistenceTest {
|
||||
@Autowired
|
||||
private UserGameRepository userGameRepository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
userGameRepository.deleteAll();
|
||||
@@ -61,4 +65,15 @@ class StatsRepositoryTest extends AbstractPersistenceTest {
|
||||
private static List<Long> appids(List<Map<String, Object>> rows) {
|
||||
return rows.stream().map(row -> ((Number) row.get("appid")).longValue()).toList();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletedUsers_LeaveEveryNumberAndTheTopLists() {
|
||||
steamGameRepository.saveAndFlush(new SteamGame(620L, "Portal 2", null, Instant.now()));
|
||||
userGameRepository.saveAllAndFlush(List.of(new UserGame(3L, 620L, 900, Instant.now(), Instant.now()),
|
||||
new UserGame(3L, 220L, 0, Instant.now(), Instant.now())));
|
||||
jdbc.update("INSERT INTO deleted_user (user_id) VALUES (3)");
|
||||
|
||||
hiddenGames_LeaveEveryNumber();
|
||||
hiddenGames_LeaveTheTopLists();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user