Public Access
feat(library): count linked editions once in pile and stats
All game counts read the counted_user_game view, which keeps only the most advanced edition of a linked set (older edition wins a tie). Add the hideDuplicates filter to the library query.
This commit is contained in:
1 parent
e806670d26
commit
221b5594c5
14 files changed
+211
-38
No files matched your search
@@ -174,6 +174,7 @@ class GameLinkFlowTest extends AbstractIntegrationTest {
|
||||
void link_ChangesEveryUsersNumbersAndShowsTheEditions() throws Exception {
|
||||
requestAs(USER_A, GAME, GAME_2).andExpect(status().isNoContent());
|
||||
|
||||
mockMvc.perform(as(get("/api/library/summary"), USER_A, Role.USER)).andExpect(jsonPath("$.total").value(2));
|
||||
linkAs(Role.AUDITOR, GAME, GAME_2).andExpect(status().isNoContent());
|
||||
|
||||
queue().andExpect(jsonPath("$.content", hasSize(0)));
|
||||
@@ -185,6 +186,9 @@ class GameLinkFlowTest extends AbstractIntegrationTest {
|
||||
.andExpect(jsonPath("$.editions[0].appid").value(GAME_2));
|
||||
mockMvc.perform(as(get("/api/library/games"), user, Role.USER)).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.content[*].editions", hasItem(2)));
|
||||
mockMvc.perform(as(get("/api/library/summary"), user, Role.USER)).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.total").value(1))
|
||||
.andExpect(jsonPath("$.byStatus.BACKLOG").value(1));
|
||||
}
|
||||
requestAs(USER_A, GAME, GAME_2).andExpect(status().isConflict()).andExpect(jsonPath("$.code").value("ALREADY_LINKED"));
|
||||
linkAs(Role.AUDITOR, GAME_2, GAME).andExpect(status().isConflict());
|
||||
@@ -267,4 +271,14 @@ class GameLinkFlowTest extends AbstractIntegrationTest {
|
||||
.andExpect(jsonPath("$.content", hasSize(0))).andExpect(jsonPath("$.totalElements").value(1))
|
||||
.andExpect(jsonPath("$.page").value(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hideDuplicates_ListsOnlyTheEditionTheSetCounts() throws Exception {
|
||||
linkAs(Role.AUDITOR, GAME, GAME_2).andExpect(status().isNoContent());
|
||||
|
||||
mockMvc.perform(as(get("/api/library/games?hideDuplicates=true"), USER_A, Role.USER)).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.totalElements").value(1));
|
||||
mockMvc.perform(as(get("/api/library/games"), USER_A, Role.USER)).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.totalElements").value(2));
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,16 @@ class BacklogSnapshotRepositoryTest extends AbstractPersistenceTest {
|
||||
assertThat(repository.findSince(2L, DAY)).containsExactly(new UserStats.PileDay(DAY, 0, 1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeAll_ALinkedSetCountsOnce() {
|
||||
userGameRepository.saveAllAndFlush(List.of(game(1L, 220L, 0, GameStatus.BACKLOG), game(1L, 620L, 0, GameStatus.BACKLOG)));
|
||||
jdbc.update("insert into game_link (appid, set_id) values (220, 1), (620, 1)");
|
||||
|
||||
repository.writeAll(DAY);
|
||||
|
||||
assertThat(repository.findSince(1L, DAY)).containsExactly(new UserStats.PileDay(DAY, 1, 1, 600));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeAll_SecondRunOnTheSameDay_ReplacesTheRow() {
|
||||
UserGame game = game(1L, 220L, 0, GameStatus.BACKLOG);
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
@@ -37,6 +38,9 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
@Autowired
|
||||
private UserGameRepository userGameRepository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@Autowired
|
||||
private SteamGameRepository steamGameRepository;
|
||||
|
||||
@@ -65,7 +69,7 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
}
|
||||
|
||||
private List<Long> appids(GameStatus status, String query) {
|
||||
return userGameRepository.findOwnedGames(USER_ID, false, status, UserGameRepository.containsPattern(query), null, PAGE)
|
||||
return userGameRepository.findOwnedGames(USER_ID, false, status, UserGameRepository.containsPattern(query), null, false, PAGE)
|
||||
.map(UserGame::getAppid).getContent();
|
||||
}
|
||||
|
||||
@@ -194,7 +198,7 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
@Test
|
||||
void findOwnedGames_CountsAllMatchesAcrossPages() {
|
||||
var page = userGameRepository.findOwnedGames(USER_ID, false, null, UserGameRepository.containsPattern("half"), null,
|
||||
var page = userGameRepository.findOwnedGames(USER_ID, false, null, UserGameRepository.containsPattern("half"), null, false,
|
||||
PageRequest.of(0, 1, Sort.by("appid")));
|
||||
|
||||
assertEquals(List.of(220L), page.map(UserGame::getAppid).getContent());
|
||||
@@ -202,7 +206,7 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
}
|
||||
|
||||
private List<Long> sortedAppids(Sort sort) {
|
||||
return userGameRepository.findOwnedGames(USER_ID, false, null, null, null, PageRequest.of(0, 20, UserGameRepository.querySort(sort)))
|
||||
return userGameRepository.findOwnedGames(USER_ID, false, null, null, null, false, PageRequest.of(0, 20, UserGameRepository.querySort(sort)))
|
||||
.map(UserGame::getAppid).getContent();
|
||||
}
|
||||
|
||||
@@ -253,7 +257,7 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
new GameTimeToBeat(620L, 300, 480, 900, Instant.now()),
|
||||
new GameTimeToBeat(380L, null, null, null, Instant.now())));
|
||||
|
||||
List<Long> shortGames = userGameRepository.findOwnedGames(USER_ID, false, null, null, 600, PAGE).map(UserGame::getAppid).getContent();
|
||||
List<Long> shortGames = userGameRepository.findOwnedGames(USER_ID, false, null, null, 600, false, PAGE).map(UserGame::getAppid).getContent();
|
||||
|
||||
assertEquals(List.of(620L), shortGames);
|
||||
}
|
||||
@@ -280,7 +284,7 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
hide(620L, 700L);
|
||||
|
||||
assertEquals(List.of(220L, 380L), appids(null, null));
|
||||
assertEquals(List.of(620L, 700L), userGameRepository.findOwnedGames(USER_ID, true, null, null, null,
|
||||
assertEquals(List.of(620L, 700L), userGameRepository.findOwnedGames(USER_ID, true, null, null, null, false,
|
||||
PageRequest.of(0, 20, Sort.by("appid"))).map(UserGame::getAppid).getContent());
|
||||
assertEquals(Set.of(620L, 700L), Set.copyOf(userGameRepository.findHiddenAppids(USER_ID)));
|
||||
}
|
||||
@@ -345,6 +349,41 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
Set.copyOf(userGameRepository.countByStatus(USER_ID)));
|
||||
}
|
||||
|
||||
private void link(long... appids) {
|
||||
jdbc.update("delete from game_link");
|
||||
for (long appid : appids) {
|
||||
jdbc.update("insert into game_link (appid, set_id) values (?, 1)", appid);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void linkedBacklogEditions_CountOnceAndTheOnceFilterKeepsOne() {
|
||||
link(380L, 700L);
|
||||
userGameRepository.flush();
|
||||
|
||||
assertEquals(Set.of(new StatusCount(GameStatus.PLAYING, 2), new StatusCount(GameStatus.BACKLOG, 1)),
|
||||
Set.copyOf(userGameRepository.countByStatus(USER_ID)));
|
||||
assertEquals(1, userGameRepository.countListedNeverPlayed(USER_ID));
|
||||
assertEquals(List.of(220L, 380L, 620L), userGameRepository.findOwnedGames(USER_ID, false, null, null, null, true,
|
||||
PageRequest.of(0, 20, Sort.by("appid"))).map(UserGame::getAppid).getContent());
|
||||
assertEquals(List.of(220L, 380L, 620L, 700L), userGameRepository.findOwnedGames(USER_ID, false, null, null, null, false,
|
||||
PageRequest.of(0, 20, Sort.by("appid"))).map(UserGame::getAppid).getContent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void backlogEditionOfAPlayingSet_IsNeverPicked() {
|
||||
link(220L, 380L);
|
||||
userGameRepository.flush();
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
assertEquals(700L, userGameRepository.findRandomBacklogGame(USER_ID, UserGameRepository.NO_GAME).orElseThrow().getAppid());
|
||||
}
|
||||
assertEquals(List.of(700L), userGameRepository.findPickerGames(USER_ID, null, null, List.of(), 10)
|
||||
.stream().map(UserGame::getAppid).toList());
|
||||
assertEquals(Set.of(new StatusCount(GameStatus.PLAYING, 2), new StatusCount(GameStatus.BACKLOG, 1)),
|
||||
Set.copyOf(userGameRepository.countByStatus(USER_ID)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void countNeverPlayed_CountsOnlyOwnActiveGamesWithoutPlaytime() {
|
||||
UserGame removed = userGameRepository.findById(new UserGameId(USER_ID, 700L)).orElseThrow();
|
||||
|
||||
@@ -94,6 +94,48 @@ class UserStatsRepositoryTest extends AbstractPersistenceTest {
|
||||
assertThat(repository.pileTimes(USER_ID)).isEqualTo(new UserStats.PileTimes(600, 1200, 1));
|
||||
}
|
||||
|
||||
private void link(long... appids) {
|
||||
for (long appid : appids) {
|
||||
jdbc.update("insert into game_link (appid, set_id) values (?, 1)", appid);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void pileTimes_ALinkedSetCountsOnce() {
|
||||
userGameRepository.saveAllAndFlush(List.of(
|
||||
game(220L, 0, GameStatus.BACKLOG, Instant.now()),
|
||||
game(620L, 0, GameStatus.BACKLOG, Instant.now())));
|
||||
gameTimeToBeatRepository.saveAllAndFlush(List.of(new GameTimeToBeat(220L, 300, 600, 1200, Instant.now()),
|
||||
new GameTimeToBeat(620L, 300, 500, 800, Instant.now())));
|
||||
link(220L, 620L);
|
||||
|
||||
assertThat(repository.pileTimes(USER_ID)).isEqualTo(new UserStats.PileTimes(600, 1200, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void oldestUnplayed_LeavesOutABacklogEditionOfAPlayedSet() {
|
||||
userGameRepository.saveAllAndFlush(List.of(
|
||||
game(620L, 0, GameStatus.BACKLOG, Instant.EPOCH),
|
||||
game(700L, 60, GameStatus.PLAYING, Instant.now()),
|
||||
game(220L, 0, GameStatus.BACKLOG, Instant.parse("2026-01-01T00:00:00Z"))));
|
||||
link(620L, 700L);
|
||||
|
||||
assertThat(repository.oldestUnplayed(USER_ID).orElseThrow().appid()).isEqualTo(220L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void finishedPerYear_ALinkedSetCountsOnce() {
|
||||
userGameRepository.saveAllAndFlush(List.of(
|
||||
game(220L, 300, GameStatus.BACKLOG, Instant.now()),
|
||||
game(620L, 300, GameStatus.BACKLOG, Instant.now())));
|
||||
setStatus(220L, "FINISHED");
|
||||
setStatus(620L, "FINISHED");
|
||||
link(220L, 620L);
|
||||
|
||||
assertThat(repository.finishedPerYear(USER_ID, BERLIN))
|
||||
.containsExactly(new UserStats.YearCount(LocalDate.now(BERLIN).getYear(), 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void minutesPlayedSince_LeavesOutOlderDaysAndHiddenGames() {
|
||||
userGameRepository.saveAllAndFlush(List.of(game(220L, 300, GameStatus.PLAYING, Instant.now())));
|
||||
|
||||
@@ -66,6 +66,22 @@ class StatsRepositoryTest extends AbstractPersistenceTest {
|
||||
assertEquals(0, statsRepository.biggestBacklog());
|
||||
}
|
||||
|
||||
@Test
|
||||
void aLinkedSet_CountsOnceInLibraryBacklogAndFinishedPerYear() {
|
||||
jdbc.update("insert into game_link (appid, set_id) values (220, 1), (1000, 1)");
|
||||
userGameRepository.saveAllAndFlush(List.of(new UserGame(5L, 220L, 0, Instant.now(), Instant.now()),
|
||||
new UserGame(5L, 1000L, 0, Instant.now(), Instant.now())));
|
||||
|
||||
assertEquals(1, statsRepository.biggestLibrary());
|
||||
assertEquals(1, statsRepository.biggestBacklog());
|
||||
assertEquals(2, statsRepository.countDistinctGames());
|
||||
|
||||
jdbc.update("UPDATE user_game SET status = 'FINISHED' WHERE user_id = 5");
|
||||
|
||||
assertEquals(1L, statsRepository.finishedPerYear("Europe/Berlin").stream()
|
||||
.mapToLong(row -> ((Number) row.get("games")).longValue()).sum());
|
||||
}
|
||||
|
||||
@Test
|
||||
void hiddenGames_LeaveTheTopLists() {
|
||||
assertEquals(List.of(220L), appids(statsRepository.topOwnedGames(5)));
|
||||
|
||||
Reference in new issue
Block a user