feat(library): add source filter to owned games

A row matches when its own source, or the source of a listed
linked edition, is selected. The hidden view ignores the filter.
This commit is contained in:
lobo committed 2026-10-09 10:03:05 +02:00
1 parent 6cd92f3e83
commit 8e4c382563
7 files changed
+186 -15

No files matched your search

@@ -282,4 +282,73 @@ class GameLinkFlowTest extends AbstractIntegrationTest {
mockMvc.perform(as(get("/api/library/games"), USER_A, Role.USER)).andExpect(status().isOk())
.andExpect(jsonPath("$.totalElements").value(2));
}
private void setSource(long appid, GameSource source) {
jdbc.update("update user_game set source = ? where user_id = ? and appid = ?", source.name(), USER_A, appid);
}
private ResultActions games(String query) throws Exception {
return mockMvc.perform(as(get("/api/library/games" + query), USER_A, Role.USER)).andExpect(status().isOk());
}
@Test
void sourceFilter_ALinkedSetMatchesByAnyListedEdition() throws Exception {
setSource(GAME_2, GameSource.PSN);
linkAs(Role.AUDITOR, GAME, GAME_2).andExpect(status().isNoContent());
games("?source=PSN").andExpect(jsonPath("$.totalElements").value(2));
games("?source=PSN&hideDuplicates=true").andExpect(jsonPath("$.totalElements").value(1));
games("?source=STEAM").andExpect(jsonPath("$.totalElements").value(0));
games("?source=MANUAL,STEAM").andExpect(jsonPath("$.totalElements").value(2));
}
@Test
void sourceFilter_WithoutALinkOnlyTheOwnSourceMatches() throws Exception {
setSource(GAME_2, GameSource.PSN);
games("?source=PSN").andExpect(jsonPath("$.totalElements").value(1))
.andExpect(jsonPath("$.content[0].appid").value(GAME_2));
}
@Test
void sourceFilter_AHiddenEditionDoesNotCount() throws Exception {
setSource(GAME_2, GameSource.PSN);
linkAs(Role.AUDITOR, GAME, GAME_2).andExpect(status().isNoContent());
jdbc.update("update user_game set hidden_at = now() where user_id = ? and appid = ?", USER_A, GAME_2);
games("?source=PSN").andExpect(jsonPath("$.totalElements").value(0));
games("?source=MANUAL").andExpect(jsonPath("$.totalElements").value(1));
}
@Test
void sourceFilter_CombinesWithStatus() throws Exception {
setSource(GAME_2, GameSource.PSN);
jdbc.update("update user_game set status = 'FINISHED' where user_id = ? and appid = ?", USER_A, GAME);
games("?source=MANUAL&status=FINISHED").andExpect(jsonPath("$.totalElements").value(1));
games("?source=PSN&status=FINISHED").andExpect(jsonPath("$.totalElements").value(0));
games("?source=PSN&status=BACKLOG").andExpect(jsonPath("$.totalElements").value(1));
}
@Test
void sourceFilter_HiddenViewIgnoresIt() throws Exception {
jdbc.update("update user_game set hidden_at = now() where user_id = ? and appid = ?", USER_A, GAME);
games("?hidden=true&source=PSN").andExpect(jsonPath("$.totalElements").value(1));
}
@Test
void sourceFilter_UnknownValueIs400() throws Exception {
mockMvc.perform(as(get("/api/library/games?source=XBOX"), USER_A, Role.USER)).andExpect(status().isBadRequest());
}
@Test
void gamesCount_AnswersTheSources() throws Exception {
setSource(GAME_2, GameSource.PSN);
mockMvc.perform(as(get("/api/library/games-count"), USER_A, Role.USER)).andExpect(status().isOk())
.andExpect(jsonPath("$.sources[0]").value("MANUAL"))
.andExpect(jsonPath("$.sources[1]").value("PSN"))
.andExpect(jsonPath("$.sources", hasSize(2)));
}
}
@@ -26,6 +26,7 @@ import org.springframework.transaction.support.TransactionTemplate;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
@@ -37,6 +38,7 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
private static final long USER_ID = 1L;
private static final ZoneId BERLIN = ZoneId.of("Europe/Berlin");
private static final Set<GameSource> ALL_SOURCES = EnumSet.allOf(GameSource.class);
private static final PageRequest PAGE = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "playtimeMinutes"));
@Autowired
@@ -79,7 +81,7 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
}
private List<Long> appids(GameStatus status, String query) {
return userGameRepository.findOwnedGames(USER_ID, false, status, UserGameRepository.containsPattern(query), null, false, PAGE)
return userGameRepository.findOwnedGames(USER_ID, false, status, UserGameRepository.containsPattern(query), null, false, ALL_SOURCES, PAGE)
.map(UserGame::getAppid).getContent();
}
@@ -208,7 +210,7 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
@Test
void findOwnedGames_CountsAllMatchesAcrossPages() {
var page = userGameRepository.findOwnedGames(USER_ID, false, null, UserGameRepository.containsPattern("half"), null, false,
var page = userGameRepository.findOwnedGames(USER_ID, false, null, UserGameRepository.containsPattern("half"), null, false, ALL_SOURCES,
PageRequest.of(0, 1, Sort.by("appid")));
assertEquals(List.of(220L), page.map(UserGame::getAppid).getContent());
@@ -216,7 +218,7 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
}
private List<Long> sortedAppids(Sort sort) {
return userGameRepository.findOwnedGames(USER_ID, false, null, null, null, false, PageRequest.of(0, 20, UserGameRepository.querySort(sort)))
return userGameRepository.findOwnedGames(USER_ID, false, null, null, null, false, ALL_SOURCES, PageRequest.of(0, 20, UserGameRepository.querySort(sort)))
.map(UserGame::getAppid).getContent();
}
@@ -267,7 +269,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, false, PAGE).map(UserGame::getAppid).getContent();
List<Long> shortGames = userGameRepository.findOwnedGames(USER_ID, false, null, null, 600, false, ALL_SOURCES, PAGE).map(UserGame::getAppid).getContent();
assertEquals(List.of(620L), shortGames);
}
@@ -294,7 +296,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, false,
assertEquals(List.of(620L, 700L), userGameRepository.findOwnedGames(USER_ID, true, null, null, null, false, ALL_SOURCES,
PageRequest.of(0, 20, Sort.by("appid"))).map(UserGame::getAppid).getContent());
assertEquals(Set.of(620L, 700L), Set.copyOf(userGameRepository.findHiddenAppids(USER_ID)));
}
@@ -359,6 +361,58 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
Set.copyOf(userGameRepository.countByStatus(USER_ID)));
}
private void setSource(GameSource source, long... appids) {
entityManager.flush();
for (long appid : appids) {
jdbc.update("update user_game set source = ? where user_id = ? and appid = ?", source.name(), USER_ID, appid);
}
entityManager.clear();
}
private List<Long> appidsOfSources(GameSource... sources) {
return userGameRepository.findOwnedGames(USER_ID, false, null, null, null, false, Set.of(sources),
PageRequest.of(0, 20, Sort.by("appid"))).map(UserGame::getAppid).getContent();
}
@Test
void findOwnedGames_BySource_OnlyRowsOfTheSource() {
setSource(GameSource.PSN, 380L, 700L);
assertEquals(List.of(380L, 700L), appidsOfSources(GameSource.PSN));
assertEquals(List.of(220L, 620L), appidsOfSources(GameSource.STEAM));
assertEquals(List.of(), appidsOfSources(GameSource.MANUAL));
}
@Test
void findOwnedGames_BySource_SeveralSourcesMatchEither() {
setSource(GameSource.PSN, 380L);
setSource(GameSource.MANUAL, 700L);
assertEquals(List.of(220L, 380L, 620L), appidsOfSources(GameSource.STEAM, GameSource.PSN));
assertEquals(List.of(380L, 700L), appidsOfSources(GameSource.PSN, GameSource.MANUAL));
}
@Test
void findOwnedGames_BySource_FollowsASteamTakeOver() {
setSource(GameSource.MANUAL, 700L);
assertEquals(List.of(700L), appidsOfSources(GameSource.MANUAL));
setSource(GameSource.STEAM, 700L);
assertEquals(List.of(), appidsOfSources(GameSource.MANUAL));
assertEquals(List.of(220L, 380L, 620L, 700L), appidsOfSources(GameSource.STEAM));
}
@Test
void findSources_DistinctNotRemoved() {
setSource(GameSource.PSN, 380L, 700L);
setSource(GameSource.MANUAL, 620L);
jdbc.update("update user_game set removed_at = now() where user_id = ? and appid = ?", USER_ID, 620L);
assertEquals(Set.of(GameSource.STEAM, GameSource.PSN), Set.copyOf(userGameRepository.findSources(USER_ID)));
assertEquals(2, userGameRepository.findSources(USER_ID).size());
}
private void link(long... appids) {
jdbc.update("delete from game_link");
for (long appid : appids) {
@@ -374,9 +428,9 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
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,
assertEquals(List.of(220L, 380L, 620L), userGameRepository.findOwnedGames(USER_ID, false, null, null, null, true, ALL_SOURCES,
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,
assertEquals(List.of(220L, 380L, 620L, 700L), userGameRepository.findOwnedGames(USER_ID, false, null, null, null, false, ALL_SOURCES,
PageRequest.of(0, 20, Sort.by("appid"))).map(UserGame::getAppid).getContent());
}