Public Access
feat(follow): add user follows with activity inbox digest
This commit is contained in:
1 parent
2dba863c54
commit
867dc4c4d1
45 files changed
+2118
-69
No files matched your search
@@ -47,6 +47,7 @@ class InternalAccountControllerTest extends AbstractIntegrationTest {
|
||||
jdbc.update("delete from deleted_user where user_id in (?, 42)", USER_ID);
|
||||
jdbc.update("delete from blocked_user where user_id in (?, 42)", USER_ID);
|
||||
jdbc.update("delete from listed_user where user_id in (?, 42)", USER_ID);
|
||||
jdbc.update("delete from public_user where user_id in (?, 42)", USER_ID);
|
||||
jdbc.update("delete from account_name where user_id in (?, 42)", USER_ID);
|
||||
jdbc.update("delete from account_role_change where user_id in (?, 42)", USER_ID);
|
||||
}
|
||||
@@ -145,6 +146,59 @@ class InternalAccountControllerTest extends AbstractIntegrationTest {
|
||||
assertThat(jdbc.queryForList("select user_id from listed_user where user_id = 42", Long.class)).containsExactly(42L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void stateCallWithPublicProfileAndSections_StoresThem() throws Exception {
|
||||
JsonNode call = ContractTokens.internalCall("applyStates");
|
||||
|
||||
send(contractRequest(call), call.path("request").toString(), serviceToken()).andExpect(status().isNoContent());
|
||||
|
||||
// The sample is a blocked user: blocked wins, no public row.
|
||||
assertThat(jdbc.queryForList("select user_id from public_user where user_id = 42", Long.class)).isEmpty();
|
||||
send(post("/internal/accounts/states"), "{\"accounts\":[{\"userId\":" + USER_ID
|
||||
+ ",\"blocked\":false,\"publicProfile\":true,\"profileSections\":[\"SUMMARY\",\"STATS\"]}]}", serviceToken())
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
assertThat(jdbc.queryForObject("select array_to_string(sections, ',') from public_user where user_id = ?",
|
||||
String.class, USER_ID)).isEqualTo("SUMMARY,STATS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aLongUnknownSectionNameOfOneUserDoesNotRejectTheStatesOfOthers() throws Exception {
|
||||
send(post("/internal/accounts/states"), "{\"accounts\":["
|
||||
+ "{\"userId\":42,\"blocked\":false,\"publicProfile\":true,\"profileSections\":[\"SUMMARY\",\"ABCDEFGHIJKLMNOPQ\"]},"
|
||||
+ "{\"userId\":" + USER_ID + ",\"blocked\":true}]}", serviceToken())
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
assertThat(jdbc.queryForList("select user_id from blocked_user where user_id = ?", Long.class, USER_ID))
|
||||
.containsExactly(USER_ID);
|
||||
assertThat(jdbc.queryForObject("select array_to_string(sections, ',') from public_user where user_id = 42",
|
||||
String.class)).isEqualTo("SUMMARY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aNullSectionNameOfOneUserDoesNotRejectTheStatesOfOthers() throws Exception {
|
||||
send(post("/internal/accounts/states"), "{\"accounts\":["
|
||||
+ "{\"userId\":42,\"blocked\":false,\"publicProfile\":true,\"profileSections\":[\"SUMMARY\",null]},"
|
||||
+ "{\"userId\":" + USER_ID + ",\"blocked\":true}]}", serviceToken())
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
assertThat(jdbc.queryForList("select user_id from blocked_user where user_id = ?", Long.class, USER_ID))
|
||||
.containsExactly(USER_ID);
|
||||
assertThat(jdbc.queryForObject("select array_to_string(sections, ',') from public_user where user_id = 42",
|
||||
String.class)).isEqualTo("SUMMARY");
|
||||
}
|
||||
|
||||
@Test
|
||||
void stateCallWithoutPublicProfile_AnOldAuth_StillWorksAndKeepsTheRow() throws Exception {
|
||||
jdbc.update("insert into public_user (user_id) values (?)", USER_ID);
|
||||
|
||||
send(post("/internal/accounts/states"), "{\"accounts\":[{\"userId\":" + USER_ID + ",\"blocked\":false}]}",
|
||||
serviceToken()).andExpect(status().isNoContent());
|
||||
|
||||
assertThat(jdbc.queryForList("select user_id from public_user where user_id = ?", Long.class, USER_ID))
|
||||
.containsExactly(USER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void stateCallWithoutARoleChange_AnOldAuth_StoresTheBlockAndNoRoleChange() throws Exception {
|
||||
send(post("/internal/accounts/states"), "{\"accounts\":[{\"userId\":" + USER_ID + ",\"blocked\":true}]}",
|
||||
|
||||
@@ -18,7 +18,7 @@ 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",
|
||||
"sync_change", "sync_result", "user_steam_sync", "game_merge_undo_user", "user_game", "game_link_request", "account_name", "game_import", "reward_ledger",
|
||||
"reward_equipped", "listed_user");
|
||||
"reward_equipped", "listed_user", "public_user", "follow", "follow_digest", "inbox_note");
|
||||
|
||||
@Autowired
|
||||
private AccountCleanupRepository repository;
|
||||
@@ -51,6 +51,10 @@ class AccountCleanupRepositoryTest extends AbstractPersistenceTest {
|
||||
jdbc.update("insert into game_link_request (user_id, appid_a, appid_b) values (?, 220, 620)", userId);
|
||||
jdbc.update("insert into account_name (user_id, display_name) values (?, 'lobo')", userId);
|
||||
jdbc.update("insert into listed_user (user_id) values (?)", userId);
|
||||
jdbc.update("insert into public_user (user_id) values (?)", userId);
|
||||
jdbc.update("insert into follow_digest (user_id) values (?)", userId);
|
||||
jdbc.update("insert into follow (user_id, followed_user_id) values (?, ?)", userId, userId + 100);
|
||||
jdbc.update("insert into inbox_note (user_id, kind, subject_user_id) values (?, 'DIGEST', ?)", userId, userId + 100);
|
||||
jdbc.update("insert into game_import (user_id, source, status, total) values (?, 'TEXT', 'SAVED', 0)", userId);
|
||||
jdbc.update("insert into reward_ledger (user_id, kind, ref, points, event_at) values (?, 'STARTER', 'STARTER', 20, now())", userId);
|
||||
jdbc.update("insert into reward_equipped (user_id, slot, item_code) values (?, 'BADGE', 'BADGE_PILE_SLAYER')", userId);
|
||||
@@ -76,6 +80,29 @@ class AccountCleanupRepositoryTest extends AbstractPersistenceTest {
|
||||
assertThat(jdbc.queryForObject("select user_seen_at from user_steam_sync where user_id = 2", Object.class)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void addDeleted_EndsFollowsInBothDirectionsWithoutANote() {
|
||||
jdbc.update("insert into follow (user_id, followed_user_id) values (2, 1)");
|
||||
|
||||
repository.addDeleted(List.of(1L));
|
||||
|
||||
assertThat(jdbc.queryForList("select user_id from follow where user_id in (1, 2) or followed_user_id in (1, 2)",
|
||||
Long.class)).containsExactly(2L);
|
||||
assertThat(rows("follow_digest", 1L)).isZero();
|
||||
assertThat(rows("public_user", 1L)).isZero();
|
||||
assertThat(jdbc.queryForObject("select count(*) from inbox_note where kind = 'VISIBILITY'", Integer.class)).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteRows_AlsoDropsTheNotesAboutTheUser() {
|
||||
jdbc.update("insert into inbox_note (user_id, kind, subject_user_id) values (2, 'VISIBILITY', 1)");
|
||||
|
||||
repository.deleteRows(List.of(1L));
|
||||
|
||||
assertThat(jdbc.queryForObject("select count(*) from inbox_note where subject_user_id = 1", Integer.class)).isZero();
|
||||
assertThat(jdbc.queryForObject("select count(*) from inbox_note where user_id = 2", Integer.class)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceDeleted_KeepsOnlyTheGivenUsers() {
|
||||
repository.addDeleted(List.of(1L, 2L));
|
||||
@@ -110,7 +137,10 @@ class AccountCleanupRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
Map<String, Integer> deletedRows = repository.deleteRows(List.of(1L));
|
||||
|
||||
USER_TABLES.forEach(table -> assertThat(deletedRows.get(table)).as(table).isEqualTo(1));
|
||||
// addDeleted already ended the follows and dropped the public profile.
|
||||
List<String> endedByAddDeleted = List.of("public_user", "follow", "follow_digest");
|
||||
USER_TABLES.stream().filter(table -> !endedByAddDeleted.contains(table))
|
||||
.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));
|
||||
|
||||
@@ -32,6 +32,9 @@ class AccountStateRepositoryTest extends AbstractPersistenceTest {
|
||||
void seed() {
|
||||
jdbc.update("delete from blocked_user");
|
||||
jdbc.update("delete from listed_user");
|
||||
jdbc.update("delete from public_user");
|
||||
jdbc.update("delete from follow");
|
||||
jdbc.update("delete from inbox_note");
|
||||
jdbc.update("delete from account_name");
|
||||
jdbc.update("delete from account_role_change");
|
||||
for (long userId : List.of(1L, 2L)) {
|
||||
@@ -51,24 +54,24 @@ class AccountStateRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
@Test
|
||||
void displayName_IsStoredAndReplacedAndAStateWithoutOneKeepsIt() {
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, "lobo", false, null, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, "lobo", false, null, null, null, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, null, null, null));
|
||||
assertThat(jdbc.queryForObject("select display_name from account_name where user_id = 1", String.class)).isEqualTo("lobo");
|
||||
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, "wolf", false, null, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, "wolf", false, null, null, null, null));
|
||||
|
||||
assertThat(jdbc.queryForList("select display_name from account_name", String.class)).containsExactly("wolf");
|
||||
}
|
||||
|
||||
@Test
|
||||
void block_StopsTheNightlySyncAndUnblockLiftsTheMarker() {
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, true, null, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, true, null, null, null, null));
|
||||
|
||||
assertThat(blocked()).containsExactly(1L);
|
||||
assertThat(sync(1L).get("user_seen_at")).isNull();
|
||||
assertThat(sync(2L).get("user_seen_at")).isNotNull();
|
||||
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, null, null, null));
|
||||
|
||||
assertThat(blocked()).isEmpty();
|
||||
}
|
||||
@@ -76,7 +79,7 @@ class AccountStateRepositoryTest extends AbstractPersistenceTest {
|
||||
// steam_id stays: the next sync with the new ID compares with it and drops the old account's games.
|
||||
@Test
|
||||
void newSteamId_StopsTheNightlySyncOfTheOldIdAndSyncsOnTheNextVisit() {
|
||||
repository.apply(new AccountState(1L, NEW_STEAM_ID, null, false, null, null));
|
||||
repository.apply(new AccountState(1L, NEW_STEAM_ID, null, false, null, null, null, null));
|
||||
|
||||
assertThat(sync(1L).get("steam_id")).isEqualTo(String.valueOf(OLD_STEAM_ID));
|
||||
assertThat(sync(1L).get("last_synced_at")).isNull();
|
||||
@@ -85,14 +88,14 @@ class AccountStateRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
@Test
|
||||
void sameSteamId_KeepsTheLastSync() {
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, null, null, null));
|
||||
|
||||
assertThat(sync(1L).get("last_synced_at")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noSteamId_StopsTheNightlySync() {
|
||||
repository.apply(new AccountState(1L, null, null, false, null, null));
|
||||
repository.apply(new AccountState(1L, null, null, false, null, null, null, null));
|
||||
|
||||
assertThat(sync(1L).get("user_seen_at")).isNull();
|
||||
assertThat(sync(1L).get("steam_id")).isEqualTo(String.valueOf(OLD_STEAM_ID));
|
||||
@@ -100,7 +103,7 @@ class AccountStateRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
@Test
|
||||
void userWithoutASyncRow_GetsOnlyTheBlock() {
|
||||
repository.apply(new AccountState(3L, NEW_STEAM_ID, null, true, null, null));
|
||||
repository.apply(new AccountState(3L, NEW_STEAM_ID, null, true, null, null, null, null));
|
||||
|
||||
assertThat(blocked()).containsExactly(3L);
|
||||
assertThat(jdbc.queryForList("select user_id from user_steam_sync where user_id = 3", Long.class)).isEmpty();
|
||||
@@ -108,10 +111,10 @@ class AccountStateRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
@Test
|
||||
void replaceAll_LiftsTheBlocksAuthNoLongerSends() {
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, true, null, null));
|
||||
repository.apply(new AccountState(2L, OLD_STEAM_ID, null, true, null, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, true, null, null, null, null));
|
||||
repository.apply(new AccountState(2L, OLD_STEAM_ID, null, true, null, null, null, null));
|
||||
|
||||
repository.replaceAll(List.of(new AccountState(2L, OLD_STEAM_ID, null, true, null, null)));
|
||||
repository.replaceAll(List.of(new AccountState(2L, OLD_STEAM_ID, null, true, null, null, null, null)));
|
||||
assertThat(blocked()).containsExactly(2L);
|
||||
|
||||
repository.replaceAll(List.of());
|
||||
@@ -128,13 +131,13 @@ class AccountStateRepositoryTest extends AbstractPersistenceTest {
|
||||
Instant first = Instant.parse("2026-10-04T12:00:00Z");
|
||||
Instant second = first.plusSeconds(60);
|
||||
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, first, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, first, null, null, null));
|
||||
assertThat(changedAt(1L)).isEqualTo(first);
|
||||
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, null, null, null));
|
||||
assertThat(changedAt(1L)).isEqualTo(first);
|
||||
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, second, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, second, null, null, null));
|
||||
assertThat(changedAt(1L)).isEqualTo(second);
|
||||
}
|
||||
|
||||
@@ -143,8 +146,8 @@ class AccountStateRepositoryTest extends AbstractPersistenceTest {
|
||||
Instant first = Instant.parse("2026-10-04T12:00:01Z");
|
||||
Instant second = first.plusSeconds(1);
|
||||
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, second, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, first, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, second, null, null, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, first, null, null, null));
|
||||
|
||||
assertThat(changedAt(1L)).isEqualTo(second);
|
||||
}
|
||||
@@ -152,8 +155,8 @@ class AccountStateRepositoryTest extends AbstractPersistenceTest {
|
||||
@Test
|
||||
void findLoginState_ReadsTheBlockAndTheRoleChange() {
|
||||
Instant change = Instant.parse("2026-10-04T12:00:00Z");
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, true, change, null));
|
||||
repository.apply(new AccountState(2L, OLD_STEAM_ID, null, false, null, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, true, change, null, null, null));
|
||||
repository.apply(new AccountState(2L, OLD_STEAM_ID, null, false, null, null, null, null));
|
||||
|
||||
assertThat(repository.findLoginState(1L)).isEqualTo(new LoginState(true, change));
|
||||
assertThat(repository.findLoginState(2L)).isEqualTo(LoginState.NONE);
|
||||
@@ -166,33 +169,169 @@ class AccountStateRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
@Test
|
||||
void listed_TrueInsertsFalseDeletesNullKeeps() {
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, true));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, true, null, null));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, null, null, null));
|
||||
assertThat(listed()).containsExactly(1L);
|
||||
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, false));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, false, null, null));
|
||||
assertThat(listed()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceAll_ListsExactlyTheListedUsers() {
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, true));
|
||||
repository.apply(new AccountState(2L, OLD_STEAM_ID, null, false, null, true));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, true, null, null));
|
||||
repository.apply(new AccountState(2L, OLD_STEAM_ID, null, false, null, true, null, null));
|
||||
|
||||
repository.replaceAll(List.of(new AccountState(2L, OLD_STEAM_ID, null, false, null, true),
|
||||
new AccountState(3L, OLD_STEAM_ID, null, false, null, false)));
|
||||
repository.replaceAll(List.of(new AccountState(2L, OLD_STEAM_ID, null, false, null, true, null, null),
|
||||
new AccountState(3L, OLD_STEAM_ID, null, false, null, false, null, null)));
|
||||
assertThat(listed()).containsExactly(2L);
|
||||
|
||||
repository.replaceAll(List.of(new AccountState(2L, OLD_STEAM_ID, null, false, null, false)));
|
||||
repository.replaceAll(List.of(new AccountState(2L, OLD_STEAM_ID, null, false, null, false, null, null)));
|
||||
assertThat(listed()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceAll_WithAStateWithoutListing_KeepsTheListing() {
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, true));
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, false, null, true, null, null));
|
||||
|
||||
repository.replaceAll(List.of(new AccountState(2L, OLD_STEAM_ID, null, false, null, null)));
|
||||
repository.replaceAll(List.of(new AccountState(2L, OLD_STEAM_ID, null, false, null, null, null, null)));
|
||||
|
||||
assertThat(listed()).containsExactly(1L);
|
||||
}
|
||||
|
||||
private static AccountState publicState(long userId, Boolean publicProfile, List<String> sections) {
|
||||
return new AccountState(userId, OLD_STEAM_ID, null, false, null, null, publicProfile, sections);
|
||||
}
|
||||
|
||||
private void follows(long follower, long followed) {
|
||||
jdbc.update("insert into follow (user_id, followed_user_id) values (?, ?)", follower, followed);
|
||||
}
|
||||
|
||||
private List<String> sections(long userId) {
|
||||
return jdbc.query("select sections from public_user where user_id = ?", (rs, i) -> {
|
||||
java.sql.Array array = rs.getArray("sections");
|
||||
try {
|
||||
return array == null ? null : java.util.Arrays.asList((String[]) array.getArray());
|
||||
} catch (java.sql.SQLException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}, userId).get(0);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> notes() {
|
||||
return jdbc.queryForList("select user_id, kind, subject_user_id, subject_name from inbox_note order by user_id");
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicProfileFalse_EndsFollowsAndWritesOneVisibilityNotePerFollower() {
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, "lobo", false, null, null, true, null));
|
||||
follows(2L, 1L);
|
||||
follows(3L, 1L);
|
||||
follows(1L, 4L);
|
||||
|
||||
repository.apply(publicState(1L, false, null));
|
||||
|
||||
assertThat(jdbc.queryForList("select user_id from public_user", Long.class)).isEmpty();
|
||||
assertThat(jdbc.queryForList("select user_id from follow", Long.class)).containsExactly(1L);
|
||||
assertThat(notes()).extracting(n -> n.get("user_id")).containsExactly(2L, 3L);
|
||||
assertThat(notes()).allSatisfy(note -> {
|
||||
assertThat(note.get("kind")).isEqualTo("VISIBILITY");
|
||||
assertThat(note.get("subject_user_id")).isEqualTo(1L);
|
||||
assertThat(note.get("subject_name")).isEqualTo("lobo");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicProfileFalseAgain_WritesNoSecondNote() {
|
||||
repository.apply(publicState(1L, true, null));
|
||||
follows(2L, 1L);
|
||||
|
||||
repository.apply(publicState(1L, false, null));
|
||||
repository.apply(publicState(1L, false, null));
|
||||
|
||||
assertThat(notes()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void blocked_EndsFollowsInBothDirectionsWithoutANote() {
|
||||
repository.apply(publicState(1L, true, null));
|
||||
follows(2L, 1L);
|
||||
follows(1L, 3L);
|
||||
follows(2L, 3L);
|
||||
|
||||
repository.apply(new AccountState(1L, OLD_STEAM_ID, null, true, null, null, true, null));
|
||||
|
||||
assertThat(jdbc.queryForList("select user_id from public_user", Long.class)).isEmpty();
|
||||
assertThat(jdbc.queryForList("select user_id from follow", Long.class)).containsExactly(2L);
|
||||
assertThat(notes()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicProfileNull_AnOldAuth_KeepsTheRowItsSectionsAndTheFollows() {
|
||||
repository.apply(publicState(1L, true, List.of("STATUS", "STATS")));
|
||||
follows(2L, 1L);
|
||||
|
||||
repository.apply(publicState(1L, null, null));
|
||||
|
||||
assertThat(sections(1L)).containsExactly("STATUS", "STATS");
|
||||
assertThat(jdbc.queryForList("select user_id from follow", Long.class)).containsExactly(2L);
|
||||
assertThat(notes()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicProfileTrue_StoresTheSectionsAndAnotherPushReplacesThem() {
|
||||
repository.apply(publicState(1L, true, List.of("SUMMARY", "STATUS")));
|
||||
assertThat(sections(1L)).containsExactly("SUMMARY", "STATUS");
|
||||
|
||||
repository.apply(publicState(1L, true, List.of("PLAYING")));
|
||||
|
||||
assertThat(sections(1L)).containsExactly("PLAYING");
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicProfileTrueWithoutSections_StoresNull() {
|
||||
repository.apply(publicState(1L, true, List.of("SUMMARY")));
|
||||
|
||||
repository.apply(publicState(1L, true, null));
|
||||
|
||||
assertThat(sections(1L)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceAll_ReplacesPublicUsersWithSectionsAndDropsFollowsOfOthersWithoutANote() {
|
||||
repository.apply(publicState(1L, true, null));
|
||||
repository.apply(publicState(2L, true, null));
|
||||
follows(3L, 1L);
|
||||
follows(3L, 2L);
|
||||
|
||||
repository.replaceAll(List.of(publicState(2L, true, List.of("STATUS")), publicState(1L, false, null)));
|
||||
|
||||
assertThat(jdbc.queryForList("select user_id from public_user", Long.class)).containsExactly(2L);
|
||||
assertThat(sections(2L)).containsExactly("STATUS");
|
||||
assertThat(jdbc.queryForList("select followed_user_id from follow", Long.class)).containsExactly(2L);
|
||||
// The note of the going-private push of user 1 comes from apply(), once.
|
||||
assertThat(notes()).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceAll_FollowsOfAUserMissingFromTheListEndSilently() {
|
||||
repository.apply(publicState(1L, true, null));
|
||||
follows(3L, 1L);
|
||||
|
||||
repository.replaceAll(List.of(publicState(2L, true, null)));
|
||||
|
||||
assertThat(jdbc.queryForList("select user_id from follow", Long.class)).isEmpty();
|
||||
assertThat(notes()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void replaceAll_WithAStateWithoutTheFlag_KeepsPublicUsersAndFollows() {
|
||||
repository.apply(publicState(1L, true, null));
|
||||
follows(3L, 1L);
|
||||
|
||||
repository.replaceAll(List.of(publicState(2L, null, null)));
|
||||
|
||||
assertThat(jdbc.queryForList("select user_id from public_user", Long.class)).containsExactly(1L);
|
||||
assertThat(jdbc.queryForList("select user_id from follow", Long.class)).containsExactly(3L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package core_service.modules.follow.api;
|
||||
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import core_service.shared.test.ContractTokens;
|
||||
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.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
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.request.MockHttpServletRequestBuilder;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
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, so the login requirement is the production one.
|
||||
*/
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles(value = "prod", inheritProfiles = false)
|
||||
class FollowControllerTest extends AbstractIntegrationTest {
|
||||
|
||||
private static final long VIEWER = 930_001L;
|
||||
private static final long LOBO = 930_002L;
|
||||
private static final long SECRET = 930_003L;
|
||||
private static final long BLOCKED = 930_004L;
|
||||
private static final long DELETED = 930_005L;
|
||||
private static final long OTHER = 930_006L;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
cleanUp();
|
||||
name(VIEWER, "Viewer");
|
||||
name(LOBO, "Lobo");
|
||||
name(SECRET, "SecretThree");
|
||||
name(BLOCKED, "BlockedFour");
|
||||
name(DELETED, "DeletedFive");
|
||||
name(OTHER, "OtherSix");
|
||||
jdbc.update("insert into public_user (user_id) values (?), (?), (?), (?), (?)", VIEWER, LOBO, BLOCKED, DELETED, OTHER);
|
||||
jdbc.update("insert into blocked_user (user_id) values (?)", BLOCKED);
|
||||
jdbc.update("insert into deleted_user (user_id) values (?)", DELETED);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanUp() {
|
||||
jdbc.update("delete from follow");
|
||||
jdbc.update("delete from follow_digest");
|
||||
jdbc.update("delete from public_user");
|
||||
jdbc.update("delete from account_name");
|
||||
jdbc.update("delete from blocked_user");
|
||||
jdbc.update("delete from deleted_user");
|
||||
}
|
||||
|
||||
private void name(long userId, String displayName) {
|
||||
jdbc.update("insert into account_name (user_id, display_name) values (?, ?)", userId, displayName);
|
||||
}
|
||||
|
||||
private static MockHttpServletRequestBuilder as(MockHttpServletRequestBuilder request, long userId) {
|
||||
return request.header("Authorization", "Bearer " + ContractTokens.loginToken(userId));
|
||||
}
|
||||
|
||||
private int follows() {
|
||||
return jdbc.queryForObject("select count(*) from follow", Integer.class);
|
||||
}
|
||||
|
||||
private JsonNode list(long userId) throws Exception {
|
||||
String body = mockMvc.perform(as(get("/api/follows"), userId)).andExpect(status().isOk())
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
return JsonMapper.builder().build().readTree(body);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withoutLogin_Is401() throws Exception {
|
||||
mockMvc.perform(get("/api/follows")).andExpect(status().isUnauthorized());
|
||||
mockMvc.perform(put("/api/follows/by-name/Lobo").header("X-TBP-Client", "web")).andExpect(status().isUnauthorized());
|
||||
mockMvc.perform(delete("/api/follows/by-name/Lobo").header("X-TBP-Client", "web")).andExpect(status().isUnauthorized());
|
||||
mockMvc.perform(delete("/api/follows/1").header("X-TBP-Client", "web")).andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void follow_IsStoredAndSurvivesANewRequestAndIsFoundCaseInsensitive() throws Exception {
|
||||
mockMvc.perform(as(put("/api/follows/by-name/lOBO"), VIEWER)).andExpect(status().isNoContent());
|
||||
|
||||
JsonNode json = list(VIEWER);
|
||||
assertThat(json.get("following")).hasSize(1);
|
||||
assertThat(json.get("following").get(0).get("displayName").asString()).isEqualTo("Lobo");
|
||||
assertThat(json.get("followers")).isEmpty();
|
||||
assertThat(jdbc.queryForList("select user_id from follow_digest", Long.class)).containsExactly(LOBO);
|
||||
}
|
||||
|
||||
@Test
|
||||
void followTwice_IsOneRow() throws Exception {
|
||||
mockMvc.perform(as(put("/api/follows/by-name/Lobo"), VIEWER)).andExpect(status().isNoContent());
|
||||
mockMvc.perform(as(put("/api/follows/by-name/Lobo"), VIEWER)).andExpect(status().isNoContent());
|
||||
|
||||
assertThat(follows()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void followYourself_Is400() throws Exception {
|
||||
mockMvc.perform(as(put("/api/follows/by-name/Viewer"), VIEWER))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("FOLLOW_SELF"));
|
||||
assertThat(follows()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void privateBlockedDeletedAndUnknown_AnswerTheSame404() throws Exception {
|
||||
String unknown = mockMvc.perform(as(put("/api/follows/by-name/Nobody"), VIEWER)).andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value("PROFILE_NOT_FOUND")).andReturn().getResponse().getContentAsString();
|
||||
|
||||
for (String name : List.of("SecretThree", "BlockedFour", "DeletedFive")) {
|
||||
String body = mockMvc.perform(as(put("/api/follows/by-name/" + name), VIEWER)).andExpect(status().isNotFound())
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
assertThat(body).as(name).isEqualTo(unknown);
|
||||
}
|
||||
assertThat(follows()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void twoUsersWithOneName_Is404() throws Exception {
|
||||
name(OTHER + 1, "lobo");
|
||||
jdbc.update("insert into public_user (user_id) values (?)", OTHER + 1);
|
||||
|
||||
mockMvc.perform(as(put("/api/follows/by-name/Lobo"), VIEWER)).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
void followLimit_Is400() throws Exception {
|
||||
jdbc.update("insert into follow (user_id, followed_user_id) select ?, ?::bigint + g from generate_series(1, 500) g",
|
||||
VIEWER, 940_000L);
|
||||
|
||||
mockMvc.perform(as(put("/api/follows/by-name/Lobo"), VIEWER))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("FOLLOW_LIMIT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nameLongerThan100_Is400() throws Exception {
|
||||
mockMvc.perform(as(put("/api/follows/by-name/" + "x".repeat(101)), VIEWER)).andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unfollowByName_RemovesTheFollowAndUnknownNamesAnswer204() throws Exception {
|
||||
mockMvc.perform(as(put("/api/follows/by-name/Lobo"), VIEWER)).andExpect(status().isNoContent());
|
||||
|
||||
mockMvc.perform(as(delete("/api/follows/by-name/Lobo"), VIEWER)).andExpect(status().isNoContent());
|
||||
mockMvc.perform(as(delete("/api/follows/by-name/Nobody"), VIEWER)).andExpect(status().isNoContent());
|
||||
|
||||
assertThat(follows()).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteById_EndsOwnFollowsAndRemovesFollowers() throws Exception {
|
||||
jdbc.update("insert into follow (user_id, followed_user_id) values (?, ?), (?, ?)", VIEWER, LOBO, OTHER, VIEWER);
|
||||
long following = jdbc.queryForObject("select id from follow where user_id = ?", Long.class, VIEWER);
|
||||
long follower = jdbc.queryForObject("select id from follow where user_id = ?", Long.class, OTHER);
|
||||
|
||||
mockMvc.perform(as(delete("/api/follows/" + following), VIEWER)).andExpect(status().isNoContent());
|
||||
mockMvc.perform(as(delete("/api/follows/" + follower), VIEWER)).andExpect(status().isNoContent());
|
||||
|
||||
assertThat(follows()).isZero();
|
||||
}
|
||||
|
||||
// IDOR: the id of a row of two other users changes nothing.
|
||||
@Test
|
||||
void deleteById_OfAForeignFollow_Is204AndTheRowStays() throws Exception {
|
||||
jdbc.update("insert into follow (user_id, followed_user_id) values (?, ?)", OTHER, LOBO);
|
||||
long foreign = jdbc.queryForObject("select id from follow", Long.class);
|
||||
|
||||
mockMvc.perform(as(delete("/api/follows/" + foreign), VIEWER)).andExpect(status().isNoContent());
|
||||
|
||||
assertThat(follows()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void followers_HidePrivateNamesAndNeverShowAUserId() throws Exception {
|
||||
jdbc.update("insert into follow (user_id, followed_user_id) values (?, ?), (?, ?)", LOBO, VIEWER, SECRET, VIEWER);
|
||||
|
||||
JsonNode json = list(VIEWER);
|
||||
|
||||
JsonNode followers = json.get("followers");
|
||||
assertThat(followers).hasSize(2);
|
||||
assertThat(followers.get(0).get("displayName").asString()).isEqualTo("Lobo");
|
||||
assertThat(followers.get(1).get("displayName").isNull()).isTrue();
|
||||
assertThat(json.toString()).doesNotContain("SecretThree", String.valueOf(LOBO), String.valueOf(SECRET));
|
||||
Set<String> keys = new HashSet<>();
|
||||
followers.get(0).propertyNames().forEach(keys::add);
|
||||
assertThat(keys).containsExactlyInAnyOrder("id", "displayName");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package core_service.modules.follow.domain;
|
||||
|
||||
import core_service.shared.test.AbstractUnitTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class DigestTest extends AbstractUnitTest {
|
||||
|
||||
private static final Digest ALL = new Digest(12, 40, 3, 2);
|
||||
|
||||
@Test
|
||||
void visibleOn_NullSectionsLeavesEverything() {
|
||||
assertThat(ALL.visibleOn(null)).isEqualTo(ALL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void visibleOn_SummaryKeepsGamesAndAchievementsButNotStatusChanges() {
|
||||
assertThat(ALL.visibleOn(Set.of("SUMMARY"))).isEqualTo(new Digest(12, 40, 0, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void visibleOn_EachStatusSectionKeepsStatusChanges() {
|
||||
for (String section : List.of("STATUS", "PLAYING", "FINISHED")) {
|
||||
assertThat(ALL.visibleOn(Set.of(section))).as(section).isEqualTo(new Digest(0, 0, 3, 2));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void visibleOn_OnlyAStatsSectionLeavesTheWishlistOnly() {
|
||||
assertThat(ALL.visibleOn(Set.of("STATS"))).isEqualTo(new Digest(0, 0, 0, 2));
|
||||
assertThat(ALL.visibleOn(Set.of())).isEqualTo(new Digest(0, 0, 0, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isEmptyAndPlus() {
|
||||
assertThat(new Digest(0, 0, 0, 0).isEmpty()).isTrue();
|
||||
assertThat(new Digest(0, 0, 0, 1).isEmpty()).isFalse();
|
||||
assertThat(new Digest(1, 2, 3, 4).plus(new Digest(1, 1, 1, 1))).isEqualTo(new Digest(2, 3, 4, 5));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
package core_service.modules.follow.service;
|
||||
|
||||
import core_service.modules.account.domain.AccountState;
|
||||
import core_service.modules.account.repo.AccountCleanupRepository;
|
||||
import core_service.modules.account.repo.AccountStateRepository;
|
||||
import core_service.shared.event.LibraryActivityEvent;
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
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.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class FollowDigestServiceTest extends AbstractIntegrationTest {
|
||||
|
||||
private static final long FOLLOWED = 910_010L;
|
||||
private static final long FOLLOWER_ONE = 910_011L;
|
||||
private static final long FOLLOWER_TWO = 910_012L;
|
||||
private static final long FIRST_APPID = 910_000L;
|
||||
private static final Instant SYNC = Instant.now().minusSeconds(600).truncatedTo(ChronoUnit.MICROS);
|
||||
|
||||
@Autowired
|
||||
private FollowDigestService service;
|
||||
|
||||
@Autowired
|
||||
private AccountStateRepository accountStateRepository;
|
||||
|
||||
@Autowired
|
||||
private AccountCleanupRepository accountCleanupRepository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
cleanUp();
|
||||
jdbc.update("insert into account_name (user_id, display_name) values (?, 'Followed')", FOLLOWED);
|
||||
jdbc.update("insert into public_user (user_id) values (?)", FOLLOWED);
|
||||
for (long follower : List.of(FOLLOWER_ONE, FOLLOWER_TWO)) {
|
||||
jdbc.update("insert into follow (user_id, followed_user_id) values (?, ?)", follower, FOLLOWED);
|
||||
}
|
||||
jdbc.update("insert into follow_digest (user_id) values (?)", FOLLOWED);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanUp() {
|
||||
jdbc.update("delete from inbox_note");
|
||||
jdbc.update("delete from follow");
|
||||
jdbc.update("delete from follow_digest");
|
||||
jdbc.update("delete from public_user");
|
||||
jdbc.update("delete from account_name");
|
||||
jdbc.update("delete from blocked_user");
|
||||
jdbc.update("delete from deleted_user");
|
||||
jdbc.update("delete from sync_result");
|
||||
jdbc.update("delete from user_game where user_id = ?", FOLLOWED);
|
||||
}
|
||||
|
||||
private void games(int count) {
|
||||
jdbc.update("insert into steam_game (appid, name) select ?::bigint + g, 'Game ' || g from generate_series(1, ?) g "
|
||||
+ "on conflict do nothing", FIRST_APPID, count);
|
||||
jdbc.update("insert into user_game (user_id, appid, status) select ?, ?::bigint + g, 'BACKLOG' "
|
||||
+ "from generate_series(1, ?) g on conflict do nothing", FOLLOWED, FIRST_APPID, count);
|
||||
}
|
||||
|
||||
private void sync() {
|
||||
jdbc.update("insert into sync_result (user_id, sync_started_at, started_by) values (?, ?, 'AUTO') "
|
||||
+ "on conflict (user_id) do update set sync_started_at = excluded.sync_started_at", FOLLOWED, Timestamp.from(SYNC));
|
||||
}
|
||||
|
||||
private void change(String kind, long appid, Integer amount) {
|
||||
jdbc.update("insert into sync_change (user_id, kind, appid, amount) values (?, ?, ?, ?)", FOLLOWED, kind, appid, amount);
|
||||
}
|
||||
|
||||
private void added(int count) {
|
||||
games(count);
|
||||
sync();
|
||||
for (int i = 1; i <= count; i++) {
|
||||
change("ADDED", FIRST_APPID + i, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void sections(String... sections) {
|
||||
jdbc.update("update public_user set sections = ? where user_id = ?",
|
||||
ps -> {
|
||||
ps.setObject(1, sections);
|
||||
ps.setLong(2, FOLLOWED);
|
||||
});
|
||||
}
|
||||
|
||||
private void event() {
|
||||
service.onLibraryActivity(new LibraryActivityEvent(FOLLOWED));
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> notes() {
|
||||
return jdbc.queryForList("select user_id, new_games, achievements, status_changes, wishlist_adds "
|
||||
+ "from inbox_note where kind = 'DIGEST' order by user_id");
|
||||
}
|
||||
|
||||
private Timestamp cursorSync() {
|
||||
return jdbc.queryForObject("select sync_started_at from follow_digest where user_id = ?", Timestamp.class, FOLLOWED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void syncBatch_WritesOneNoteForEachFollowerWithTheCounts() {
|
||||
added(12);
|
||||
change("ACHIEVEMENTS", FIRST_APPID + 1, 25);
|
||||
change("ACHIEVEMENTS", FIRST_APPID + 2, 15);
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
change("STATUS", FIRST_APPID + i, null);
|
||||
}
|
||||
change("WISHLIST", 777_001L, null);
|
||||
change("WISHLIST", 777_002L, null);
|
||||
|
||||
event();
|
||||
|
||||
assertThat(notes()).hasSize(2).allSatisfy(note -> {
|
||||
assertThat(note.get("new_games")).isEqualTo(12);
|
||||
assertThat(note.get("achievements")).isEqualTo(40);
|
||||
assertThat(note.get("status_changes")).isEqualTo(3);
|
||||
assertThat(note.get("wishlist_adds")).isEqualTo(2);
|
||||
});
|
||||
assertThat(notes()).extracting(note -> note.get("user_id")).containsExactly(FOLLOWER_ONE, FOLLOWER_TWO);
|
||||
assertThat(jdbc.queryForObject("select subject_name from inbox_note limit 1", String.class)).isEqualTo("Followed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noActivity_WritesNoNoteButMovesTheCursor() {
|
||||
sync();
|
||||
|
||||
event();
|
||||
|
||||
assertThat(notes()).isEmpty();
|
||||
assertThat(cursorSync()).isEqualTo(Timestamp.from(SYNC));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bigBatch_IsStillOneNotePerFollower() {
|
||||
added(300);
|
||||
|
||||
event();
|
||||
|
||||
assertThat(notes()).hasSize(2);
|
||||
assertThat(notes().get(0).get("new_games")).isEqualTo(300);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameBatchTwice_WritesNoSecondNote() {
|
||||
added(2);
|
||||
|
||||
event();
|
||||
event();
|
||||
|
||||
assertThat(notes()).hasSize(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void hiddenGame_IsNotCounted() {
|
||||
added(3);
|
||||
jdbc.update("update user_game set hidden_at = now() where user_id = ? and appid = ?", FOLLOWED, FIRST_APPID + 1);
|
||||
|
||||
event();
|
||||
|
||||
assertThat(notes().get(0).get("new_games")).isEqualTo(2);
|
||||
}
|
||||
|
||||
// The result of a sync can be kept for an unseen one: its new wishlist rows join it after it was reported.
|
||||
@Test
|
||||
void wishlistAddJoiningAReportedResult_IsReportedOnceAndOnlyOnce() {
|
||||
added(1);
|
||||
change("WISHLIST", 777_001L, null);
|
||||
event();
|
||||
assertThat(notes()).extracting(note -> note.get("wishlist_adds")).containsExactly(1, 1);
|
||||
|
||||
change("WISHLIST", 777_002L, null);
|
||||
event();
|
||||
event();
|
||||
|
||||
assertThat(notes()).hasSize(4);
|
||||
assertThat(jdbc.queryForObject("select count(*) from inbox_note where wishlist_adds = 1 and new_games = 0",
|
||||
Integer.class)).isEqualTo(2);
|
||||
}
|
||||
|
||||
// Privacy: no note when the followed user is not public, blocked or deleted, or the follower is deleted.
|
||||
|
||||
@Test
|
||||
void followedUserWithoutAPublicProfile_GetsNoNote() {
|
||||
added(2);
|
||||
jdbc.update("delete from public_user");
|
||||
|
||||
event();
|
||||
|
||||
assertThat(notes()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void blockedFollowedUser_GetsNoNote() {
|
||||
added(2);
|
||||
jdbc.update("insert into blocked_user (user_id) values (?)", FOLLOWED);
|
||||
|
||||
event();
|
||||
|
||||
assertThat(notes()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletedFollowedUser_GetsNoNote() {
|
||||
added(2);
|
||||
jdbc.update("insert into deleted_user (user_id) values (?)", FOLLOWED);
|
||||
|
||||
event();
|
||||
|
||||
assertThat(notes()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletedFollower_GetsNoNoteAndTheOtherStillDoes() {
|
||||
added(2);
|
||||
jdbc.update("insert into deleted_user (user_id) values (?)", FOLLOWER_ONE);
|
||||
|
||||
event();
|
||||
|
||||
assertThat(notes()).extracting(note -> note.get("user_id")).containsExactly(FOLLOWER_TWO);
|
||||
}
|
||||
|
||||
@Test
|
||||
void goingPrivateAndPublicAgain_LeavesNoFollowerAndNoDigest() {
|
||||
accountStateRepository.apply(new AccountState(FOLLOWED, null, null, false, null, null, false, null));
|
||||
accountStateRepository.apply(new AccountState(FOLLOWED, null, null, false, null, null, true, null));
|
||||
added(2);
|
||||
|
||||
event();
|
||||
|
||||
assertThat(notes()).isEmpty();
|
||||
assertThat(jdbc.queryForObject("select count(*) from follow", Integer.class)).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void followedUserDeletedByCleanup_GetsNoNote() {
|
||||
added(2);
|
||||
accountCleanupRepository.addDeleted(List.of(FOLLOWED));
|
||||
|
||||
event();
|
||||
|
||||
assertThat(notes()).isEmpty();
|
||||
}
|
||||
|
||||
// Decision 1: a kind that the profile hides is left out.
|
||||
|
||||
@Test
|
||||
void statusSectionOnly_DropsGamesAndAchievementsAndKeepsStatusChangesAndWishlist() {
|
||||
added(4);
|
||||
change("ACHIEVEMENTS", FIRST_APPID + 1, 9);
|
||||
change("STATUS", FIRST_APPID + 2, null);
|
||||
change("STATUS", FIRST_APPID + 3, null);
|
||||
change("WISHLIST", 777_001L, null);
|
||||
sections("STATUS");
|
||||
|
||||
event();
|
||||
|
||||
assertThat(notes()).hasSize(2).allSatisfy(note -> {
|
||||
assertThat(note.get("new_games")).isEqualTo(0);
|
||||
assertThat(note.get("achievements")).isEqualTo(0);
|
||||
assertThat(note.get("status_changes")).isEqualTo(2);
|
||||
assertThat(note.get("wishlist_adds")).isEqualTo(1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void statsSectionOnly_WritesNoNoteAndTheHiddenKindIsNeverReportedLater() {
|
||||
added(3);
|
||||
sections("STATS");
|
||||
|
||||
event();
|
||||
|
||||
assertThat(notes()).isEmpty();
|
||||
assertThat(cursorSync()).isEqualTo(Timestamp.from(SYNC));
|
||||
|
||||
sections("SUMMARY");
|
||||
event();
|
||||
|
||||
assertThat(notes()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sectionsNull_CountsEverything() {
|
||||
added(2);
|
||||
change("ACHIEVEMENTS", FIRST_APPID + 1, 5);
|
||||
change("STATUS", FIRST_APPID + 1, null);
|
||||
|
||||
event();
|
||||
|
||||
assertThat(notes().get(0)).containsEntry("new_games", 2).containsEntry("achievements", 5)
|
||||
.containsEntry("status_changes", 1);
|
||||
}
|
||||
|
||||
// Hand changes: one batch after a quiet time; the hand job never takes the sync result.
|
||||
|
||||
private void handGame(long appid, long ageMinutes) {
|
||||
jdbc.update("insert into steam_game (appid, name) values (?, 'Hand') on conflict do nothing", appid);
|
||||
jdbc.update("insert into user_game (user_id, appid, status, status_set_by_user) values (?, ?, 'BACKLOG', true)",
|
||||
FOLLOWED, appid);
|
||||
jdbc.update("update user_game_status_change set changed_at = now() - make_interval(mins => ?) "
|
||||
+ "where user_id = ? and appid = ?", (int) ageMinutes, FOLLOWED, appid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void handChangeYoungerThanTheQuietTime_WritesNoNote() {
|
||||
handGame(920_001L, 1);
|
||||
|
||||
service.digestHandChanges();
|
||||
|
||||
assertThat(notes()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void handChangeOlderThanTheQuietTime_WritesOneNotePerFollowerOnce() {
|
||||
handGame(920_001L, 30);
|
||||
handGame(920_002L, 30);
|
||||
|
||||
service.digestHandChanges();
|
||||
service.digestHandChanges();
|
||||
|
||||
assertThat(notes()).hasSize(2).allSatisfy(note -> assertThat(note.get("new_games")).isEqualTo(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handJob_LeavesAnUnreportedSyncResultForTheEvent() {
|
||||
added(2);
|
||||
change("ACHIEVEMENTS", FIRST_APPID + 1, 7);
|
||||
handGame(920_001L, 30);
|
||||
|
||||
service.digestHandChanges();
|
||||
|
||||
assertThat(notes()).hasSize(2).allSatisfy(note -> {
|
||||
assertThat(note.get("new_games")).isEqualTo(1);
|
||||
assertThat(note.get("achievements")).isEqualTo(0);
|
||||
});
|
||||
assertThat(cursorSync()).isNull();
|
||||
|
||||
event();
|
||||
|
||||
assertThat(notes()).hasSize(4);
|
||||
assertThat(jdbc.queryForObject("select count(*) from inbox_note where achievements = 7 and new_games = 2",
|
||||
Integer.class)).isEqualTo(2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package core_service.modules.inbox.api;
|
||||
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import core_service.shared.test.ContractTokens;
|
||||
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.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
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.request.MockHttpServletRequestBuilder;
|
||||
|
||||
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.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles(value = "prod", inheritProfiles = false)
|
||||
class InboxControllerTest extends AbstractIntegrationTest {
|
||||
|
||||
private static final long READER = 931_001L;
|
||||
private static final long OTHER = 931_002L;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
@AfterEach
|
||||
void cleanUp() {
|
||||
jdbc.update("delete from inbox_note");
|
||||
}
|
||||
|
||||
private static MockHttpServletRequestBuilder as(MockHttpServletRequestBuilder request, long userId) {
|
||||
return request.header("Authorization", "Bearer " + ContractTokens.loginToken(userId));
|
||||
}
|
||||
|
||||
private void note(long userId, String name, int minutesAgo, int newGames) {
|
||||
jdbc.update("insert into inbox_note (user_id, kind, subject_user_id, subject_name, new_games, created_at) "
|
||||
+ "values (?, 'DIGEST', 7, ?, ?, now() - make_interval(mins => ?))", userId, name, newGames, minutesAgo);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withoutLogin_Is401() throws Exception {
|
||||
mockMvc.perform(get("/api/inbox")).andExpect(status().isUnauthorized());
|
||||
mockMvc.perform(put("/api/inbox/read").header("X-TBP-Client", "web"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void inbox_ShowsOwnNotesNewestFirstWithoutAUserId() throws Exception {
|
||||
note(READER, "Older", 10, 1);
|
||||
note(READER, "Newer", 1, 2);
|
||||
note(OTHER, "NotMine", 1, 3);
|
||||
|
||||
String body = mockMvc.perform(as(get("/api/inbox"), READER)).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.results.length()").value(2))
|
||||
.andExpect(jsonPath("$.results[0].displayName").value("Newer"))
|
||||
.andExpect(jsonPath("$.results[0].kind").value("DIGEST"))
|
||||
.andExpect(jsonPath("$.results[0].newGames").value(2))
|
||||
.andExpect(jsonPath("$.results[0].read").value(false))
|
||||
.andExpect(jsonPath("$.results[1].displayName").value("Older"))
|
||||
.andExpect(jsonPath("$.unread").value(2))
|
||||
.andReturn().getResponse().getContentAsString();
|
||||
|
||||
assertThat(body).doesNotContain("NotMine", "subject", "userId");
|
||||
}
|
||||
|
||||
@Test
|
||||
void inbox_ListsAtMost50ButCountsAllUnread() throws Exception {
|
||||
for (int i = 0; i < 55; i++) {
|
||||
note(READER, "Note" + i, i, 1);
|
||||
}
|
||||
|
||||
mockMvc.perform(as(get("/api/inbox"), READER)).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.results.length()").value(50))
|
||||
.andExpect(jsonPath("$.results[0].displayName").value("Note0"))
|
||||
.andExpect(jsonPath("$.unread").value(55));
|
||||
}
|
||||
|
||||
@Test
|
||||
void markRead_ClearsTheUnreadCountOfTheCallerOnly() throws Exception {
|
||||
note(READER, "Mine", 1, 1);
|
||||
note(OTHER, "Theirs", 1, 1);
|
||||
|
||||
mockMvc.perform(as(put("/api/inbox/read"), READER)).andExpect(status().isNoContent());
|
||||
|
||||
mockMvc.perform(as(get("/api/inbox"), READER)).andExpect(jsonPath("$.unread").value(0))
|
||||
.andExpect(jsonPath("$.results[0].read").value(true));
|
||||
mockMvc.perform(as(get("/api/inbox"), OTHER)).andExpect(jsonPath("$.unread").value(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package core_service.modules.inbox.repo;
|
||||
|
||||
import core_service.shared.test.AbstractPersistenceTest;
|
||||
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.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Import(InboxRepository.class)
|
||||
class InboxRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
@Autowired
|
||||
private InboxRepository repository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
private void note(long id, int daysAgo, boolean read) {
|
||||
jdbc.update("insert into inbox_note (id, user_id, kind, subject_user_id, created_at, read_at) "
|
||||
+ "values (?, 1, 'DIGEST', 2, now() - make_interval(days => ?), case when ? then now() end)",
|
||||
id, daysAgo, read);
|
||||
}
|
||||
|
||||
private java.util.List<Long> ids() {
|
||||
return jdbc.queryForList("select id from inbox_note order by id", Long.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteBefore_RemovesNotesOlderThan90DaysReadOrUnreadAndKeepsYoungerOnes() {
|
||||
jdbc.update("delete from inbox_note");
|
||||
note(1, 91, true);
|
||||
note(2, 91, false);
|
||||
note(3, 89, false);
|
||||
note(4, 89, true);
|
||||
|
||||
int deleted = repository.deleteBefore(Instant.now().minus(Duration.ofDays(90)));
|
||||
|
||||
assertThat(deleted).isEqualTo(2);
|
||||
assertThat(ids()).containsExactly(3L, 4L);
|
||||
}
|
||||
}
|
||||
@@ -258,4 +258,71 @@ class SyncChangeRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
assertThat(stored().seen()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void find_LeavesWishlistRowsOut() {
|
||||
claim(USER_ID, FIRST_RUN);
|
||||
repository.replace(USER_ID, FIRST_RUN, SyncTrigger.AUTO, 0,
|
||||
List.of(SyncChange.added(HALF_LIFE), SyncChange.wishlist(PORTAL)));
|
||||
|
||||
assertThat(rows()).isEqualTo(2);
|
||||
assertThat(stored().changes()).extracting(e -> e.change()).containsExactly(SyncChange.added(HALF_LIFE));
|
||||
}
|
||||
|
||||
// A manual game can also be on the Steam wishlist: the banner must never get the new kind for it.
|
||||
@Test
|
||||
void find_WishlistRowOfAnOwnedGameIsStillLeftOut() {
|
||||
claim(USER_ID, FIRST_RUN);
|
||||
repository.replace(USER_ID, FIRST_RUN, SyncTrigger.AUTO, 0, List.of(SyncChange.wishlist(HALF_LIFE)));
|
||||
|
||||
assertThat(stored().changes()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void replace_WishlistOnlyResultOverAnUnseenWishlistOnlyOne_ReplacesIt() {
|
||||
claim(USER_ID, FIRST_RUN);
|
||||
repository.replace(USER_ID, FIRST_RUN, SyncTrigger.AUTO, 0, List.of(SyncChange.wishlist(PORTAL)));
|
||||
claim(USER_ID, SECOND_RUN);
|
||||
|
||||
boolean written = repository.replace(USER_ID, SECOND_RUN, SyncTrigger.AUTO, 0, List.of(SyncChange.wishlist(HALF_LIFE)));
|
||||
|
||||
assertThat(written).isTrue();
|
||||
assertThat(stored().syncedAt()).isEqualTo(SECOND_RUN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void replace_WishlistOnlyResultOverAnUnseenOneWithChanges_KeepsThatResult() {
|
||||
claim(USER_ID, FIRST_RUN);
|
||||
repository.replace(USER_ID, FIRST_RUN, SyncTrigger.AUTO, 0, List.of(SyncChange.added(HALF_LIFE)));
|
||||
claim(USER_ID, SECOND_RUN);
|
||||
|
||||
boolean written = repository.replace(USER_ID, SECOND_RUN, SyncTrigger.AUTO, 0, List.of(SyncChange.wishlist(PORTAL)));
|
||||
|
||||
assertThat(written).isFalse();
|
||||
assertThat(stored().changes()).extracting(e -> e.change()).containsExactly(SyncChange.added(HALF_LIFE));
|
||||
}
|
||||
|
||||
// The digest reads WISHLIST rows by id, so an add that meets a kept result must still be stored.
|
||||
@Test
|
||||
void replace_WishlistOnlyResultOverAnUnseenOneWithChanges_StillStoresTheWishlistAdds() {
|
||||
claim(USER_ID, FIRST_RUN);
|
||||
repository.replace(USER_ID, FIRST_RUN, SyncTrigger.AUTO, 0, List.of(SyncChange.added(HALF_LIFE)));
|
||||
claim(USER_ID, SECOND_RUN);
|
||||
|
||||
repository.replace(USER_ID, SECOND_RUN, SyncTrigger.AUTO, 0, List.of(SyncChange.wishlist(PORTAL)));
|
||||
|
||||
assertThat(jdbc.queryForList("select appid from sync_change where user_id = ? and kind = 'WISHLIST'", Long.class,
|
||||
USER_ID)).containsExactly(PORTAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void replace_RunThatLostItsClaim_StoresNoWishlistAdds() {
|
||||
claim(USER_ID, FIRST_RUN);
|
||||
repository.replace(USER_ID, FIRST_RUN, SyncTrigger.AUTO, 0, List.of(SyncChange.added(HALF_LIFE)));
|
||||
claim(USER_ID, SECOND_RUN);
|
||||
|
||||
repository.replace(USER_ID, FIRST_RUN.minusSeconds(5), SyncTrigger.AUTO, 0, List.of(SyncChange.wishlist(PORTAL)));
|
||||
|
||||
assertThat(rows()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,13 @@ import core_service.modules.library.repo.UserGameRepository;
|
||||
import core_service.modules.steam_adapter.api.dto.response.SteamPlayerAchievementsResponse;
|
||||
import core_service.modules.steam_adapter.api.dto.response.SteamSchemaForGameResponse;
|
||||
import core_service.modules.steam_adapter.service.SteamApiService;
|
||||
import core_service.shared.event.LibraryActivityEvent;
|
||||
import core_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.context.ApplicationEventPublisher;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
|
||||
@@ -71,6 +73,8 @@ class AchievementSyncServiceTest extends AbstractUnitTest {
|
||||
private SteamApiService steamApiService;
|
||||
@Mock
|
||||
private SyncChangeRepository syncChangeRepository;
|
||||
@Mock
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
private AchievementSyncService service;
|
||||
|
||||
@@ -78,7 +82,7 @@ class AchievementSyncServiceTest extends AbstractUnitTest {
|
||||
void setUp() {
|
||||
service = new AchievementSyncService(userGameRepository, schemaRepository, schemaMetaRepository,
|
||||
userAchievementRepository, userAchievementMetaRepository, steamApiService, syncChangeRepository,
|
||||
mock(PlatformTransactionManager.class), 30, 0);
|
||||
eventPublisher, mock(PlatformTransactionManager.class), 30, 0);
|
||||
}
|
||||
|
||||
private static UserGame game(long appid, int playtime) {
|
||||
@@ -301,6 +305,40 @@ class AchievementSyncServiceTest extends AbstractUnitTest {
|
||||
verify(userAchievementRepository, never()).deleteByUserIdAndAppid(anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void syncLibrary_PublishesTheActivityEventOnceAfterTheRun() {
|
||||
library(List.of(game(620L, 60)), List.of(schema(620L, 10)), List.of());
|
||||
when(steamApiService.getPlayerAchievements(eq(STEAM_ID), anyLong())).thenReturn(visible());
|
||||
|
||||
service.syncLibrary(USER_ID, STEAM_ID);
|
||||
|
||||
verify(eventPublisher, times(1)).publishEvent(new LibraryActivityEvent(USER_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
void syncLibrary_PublishesTheActivityEventAlsoAfterAnEarlyStop() {
|
||||
List<UserGame> games = LongStream.rangeClosed(1, 6).mapToObj(appid -> game(appid, 60)).toList();
|
||||
library(games, LongStream.rangeClosed(1, 6).mapToObj(appid -> schema(appid, 10)).toList(), List.of());
|
||||
when(steamApiService.getPlayerAchievements(eq(STEAM_ID), anyLong())).thenThrow(new ResourceAccessException("down"));
|
||||
|
||||
service.syncLibrary(USER_ID, STEAM_ID);
|
||||
|
||||
verify(eventPublisher, times(1)).publishEvent(new LibraryActivityEvent(USER_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
void syncLibrary_PublishesTheActivityEventAlsoWhenTheRunThrows() {
|
||||
when(userGameRepository.findListedSteamGames(USER_ID)).thenThrow(new IllegalStateException("db gone"));
|
||||
|
||||
try {
|
||||
service.syncLibrary(USER_ID, STEAM_ID);
|
||||
} catch (IllegalStateException ignored) {
|
||||
// the exception itself is pre-existing behavior; the finding is the missing event
|
||||
}
|
||||
|
||||
verify(eventPublisher, times(1)).publishEvent(new LibraryActivityEvent(USER_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
void syncLibrary_StopsWhenSteamHidesTheFirstGames() {
|
||||
List<UserGame> games = LongStream.rangeClosed(1, 8).mapToObj(appid -> game(appid, 60)).toList();
|
||||
@@ -381,7 +419,7 @@ class AchievementSyncServiceTest extends AbstractUnitTest {
|
||||
long pauseMillis = 300;
|
||||
AchievementSyncService paced = new AchievementSyncService(userGameRepository, schemaRepository, schemaMetaRepository,
|
||||
userAchievementRepository, userAchievementMetaRepository, steamApiService, syncChangeRepository,
|
||||
mock(PlatformTransactionManager.class), 30, pauseMillis);
|
||||
eventPublisher, mock(PlatformTransactionManager.class), 30, pauseMillis);
|
||||
when(userGameRepository.findListedSteamGames(1L)).thenReturn(List.of(new UserGame(1L, 620L, 60, Instant.now(), Instant.now())));
|
||||
when(userGameRepository.findListedSteamGames(2L)).thenReturn(List.of(new UserGame(2L, 620L, 60, Instant.now(), Instant.now())));
|
||||
when(schemaMetaRepository.findAllById(any())).thenReturn(List.of(schema(620L, 10)));
|
||||
|
||||
@@ -468,6 +468,33 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
||||
assertEquals(added, wish.getAddedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_NewWishlistAppids_BecomeWishlistChanges() {
|
||||
owned(0);
|
||||
steamReturns(game(0, 0), STEAM_ID);
|
||||
when(steamApiService.getWishlist(STEAM_ID)).thenReturn(Optional.of(List.of(
|
||||
new SteamApiService.WishlistItem(1257610L, null), new SteamApiService.WishlistItem(1000L, null))));
|
||||
when(steamApiService.getStoreItems(any())).thenReturn(Map.of());
|
||||
when(wishlistGameRepository.findForUser(USER_ID)).thenReturn(List.of(
|
||||
new WishlistGame(USER_ID, 1000L, "Old wish", null, null, null, 0)));
|
||||
|
||||
worker.runSync(USER_ID, STEAM_ID, SyncTrigger.AUTO);
|
||||
|
||||
assertThat(replacedChanges(SyncTrigger.AUTO, 0)).containsExactly(SyncChange.wishlist(1257610L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_BaselineSync_WritesNoWishlistChanges() {
|
||||
steamReturns(game(0, 0), null);
|
||||
when(steamApiService.getWishlist(STEAM_ID)).thenReturn(Optional.of(List.of(
|
||||
new SteamApiService.WishlistItem(1257610L, null))));
|
||||
when(steamApiService.getStoreItems(any())).thenReturn(Map.of());
|
||||
|
||||
worker.runSync(USER_ID, STEAM_ID, SyncTrigger.AUTO);
|
||||
|
||||
assertThat(replacedChanges(SyncTrigger.AUTO, 1)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_WishlistError_KeepsTheOldWishlistAndTheLibrarySyncIsOk() {
|
||||
steamReturns(game(0, 0), STEAM_ID);
|
||||
|
||||
@@ -29,7 +29,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
// import watchdog runs only when a test calls it.
|
||||
// Every cached context keeps its own pool: small pools keep them all below Postgres' 100 clients.
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
properties = {"app.library.nightly-sync-cron=-", "app.library.image-repair-cron=-", "app.library.merge-undo-purge-cron=-", "app.rewards.settle-cron=-", "app.speedrun.enabled=false",
|
||||
properties = {"app.library.nightly-sync-cron=-", "app.library.image-repair-cron=-", "app.library.merge-undo-purge-cron=-", "app.inbox.purge-cron=-", "app.follow.digest-ms=86400000", "app.rewards.settle-cron=-", "app.speedrun.enabled=false",
|
||||
"app.library.import-watchdog-ms=86400000",
|
||||
"spring.datasource.hikari.maximum-pool-size=4"})
|
||||
@ActiveProfiles("dev")
|
||||
|
||||
@@ -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", "listed": true}]}
|
||||
"request": {"accounts": [{"userId": 42, "steamId": 76561198000000001, "displayName": "lobo", "blocked": true, "roleChangedAt": "2026-10-04T12:00:00Z", "listed": true, "publicProfile": true, "profileSections": ["SUMMARY", "STATUS", "PLAYING", "FINISHED"]}]}
|
||||
},
|
||||
"replaceStates": {
|
||||
"method": "PUT",
|
||||
"path": "/internal/accounts/states",
|
||||
"request": {"accounts": [{"userId": 42, "steamId": null, "displayName": "lobo", "blocked": false, "roleChangedAt": null, "listed": false}]}
|
||||
"request": {"accounts": [{"userId": 42, "steamId": null, "displayName": "lobo", "blocked": false, "roleChangedAt": null, "listed": false, "publicProfile": false, "profileSections": ["SUMMARY", "STATUS", "PLAYING", "FINISHED"]}]}
|
||||
},
|
||||
"purge": {
|
||||
"method": "POST",
|
||||
|
||||
Reference in new issue
Block a user