Public Access
feat(picker): add series and studio rows from IGDB credits
New V37 tables game_credit and game_credit_check store IGDB collections and developer/publisher companies per game. A scheduled job backfills existing games (250 per 30 s); sync, import, manual add, merge and detail page refresh them. SERIES row: next part of a collection the user played. STUDIO row: backlog games from developers of top played games.
This commit is contained in:
1 parent
8dd398321c
commit
117ff61730
33 files changed
+1141
-32
No files matched your search
@@ -110,6 +110,71 @@ class IgdbApiServiceTest extends AbstractUnitTest {
|
||||
Map.entry(220L, new IgdbApiService.GameTags(List.of(), List.of(), List.of())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCredits_ReturnsSeriesAndCompaniesPerSteamApp() {
|
||||
expectToken();
|
||||
igdb.expect(requestTo(containsString("/external_games")))
|
||||
.andRespond(withSuccess("[{\"id\": 1, \"game\": 72, \"uid\": \"620\"}, {\"id\": 2, \"game\": 233, \"uid\": \"220\"}]",
|
||||
MediaType.APPLICATION_JSON));
|
||||
igdb.expect(requestTo(containsString("/games")))
|
||||
.andExpect(content().string(allOf(
|
||||
containsString("fields collections.name,involved_companies.company.name,"
|
||||
+ "involved_companies.developer,involved_companies.publisher;"),
|
||||
containsString("id = (72,233)"))))
|
||||
.andRespond(withSuccess("[{\"id\": 72, \"collections\": [{\"id\": 10, \"name\": \"Mass Effect\"}],"
|
||||
+ " \"involved_companies\": ["
|
||||
+ "{\"id\": 1, \"company\": {\"id\": 5, \"name\": \"BioWare\"}, \"developer\": true},"
|
||||
+ "{\"id\": 3, \"company\": {\"id\": 5, \"name\": \"BioWare\"}, \"publisher\": true},"
|
||||
+ "{\"id\": 2, \"company\": {\"id\": 6, \"name\": \"Port Studio\"}, \"developer\": false, \"publisher\": false}]},"
|
||||
+ " {\"id\": 233}]", MediaType.APPLICATION_JSON));
|
||||
|
||||
Map<Long, IgdbApiService.Credits> credits = service.getCredits(List.of(620L, 220L)).orElseThrow();
|
||||
|
||||
assertThat(credits).containsOnly(
|
||||
Map.entry(620L, new IgdbApiService.Credits(List.of(new IgdbApiService.Series(10, "Mass Effect")),
|
||||
List.of(new IgdbApiService.Company(5, "BioWare", true, true)))),
|
||||
Map.entry(220L, new IgdbApiService.Credits(List.of(), List.of())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCredits_TwoIgdbGamesOfOneApp_UnionByIds() {
|
||||
expectToken();
|
||||
igdb.expect(requestTo(containsString("/external_games")))
|
||||
.andRespond(withSuccess("[{\"id\": 1, \"game\": 72, \"uid\": \"620\"}, {\"id\": 2, \"game\": 233, \"uid\": \"620\"}]",
|
||||
MediaType.APPLICATION_JSON));
|
||||
igdb.expect(requestTo(containsString("/games")))
|
||||
.andRespond(withSuccess("[{\"id\": 72, \"collections\": [{\"id\": 10, \"name\": \"Mass Effect\"}],"
|
||||
+ " \"involved_companies\": [{\"company\": {\"id\": 5, \"name\": \"BioWare\"}, \"developer\": true}]},"
|
||||
+ " {\"id\": 233, \"collections\": [{\"id\": 10, \"name\": \"Mass Effect\"}],"
|
||||
+ " \"involved_companies\": [{\"company\": {\"id\": 5, \"name\": \"BioWare\"}, \"publisher\": true}]}]",
|
||||
MediaType.APPLICATION_JSON));
|
||||
|
||||
IgdbApiService.Credits credits = service.getCredits(List.of(620L)).orElseThrow().get(620L);
|
||||
|
||||
assertThat(credits.series()).containsExactly(new IgdbApiService.Series(10, "Mass Effect"));
|
||||
assertThat(credits.companies()).containsExactly(new IgdbApiService.Company(5, "BioWare", true, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCreditsByGameIds_AsksTheGamesDirectly() {
|
||||
expectToken();
|
||||
igdb.expect(requestTo(containsString("/games")))
|
||||
.andExpect(content().string(containsString("id = (119388)")))
|
||||
.andRespond(withSuccess("[{\"id\": 119388, \"collections\": [{\"id\": 3, \"name\": \"Zelda\"}]}]",
|
||||
MediaType.APPLICATION_JSON));
|
||||
|
||||
assertThat(service.getCreditsByGameIds(List.of(119388L)).orElseThrow().get(119388L).series())
|
||||
.containsExactly(new IgdbApiService.Series(3, "Zelda"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCredits_ServerError_IsEmpty() {
|
||||
expectToken();
|
||||
igdb.expect(requestTo(containsString("/external_games"))).andRespond(withServerError());
|
||||
|
||||
assertThat(service.getCredits(List.of(620L))).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getGameTags_SteamAppLinkedToTwoIgdbGames_KeepsTheTagsOfBoth() {
|
||||
expectToken();
|
||||
|
||||
@@ -191,6 +191,20 @@ class GameMergeFlowTest extends AbstractIntegrationTest {
|
||||
assertThat(audit()).hasSize(1).first().asString().contains("GAME_MERGE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeDropsTheCreditsOfTheMergedKey() throws Exception {
|
||||
manualRow(AUDITOR_ID, MANUAL, GameStatus.BACKLOG);
|
||||
jdbc.update("delete from game_credit");
|
||||
jdbc.update("delete from game_credit_check");
|
||||
jdbc.update("insert into game_credit (appid, kind, igdb_id, name) values (?, 'SERIES', 10, 'Hades')", MANUAL);
|
||||
jdbc.update("insert into game_credit_check (appid, checked_at) values (?, now())", MANUAL);
|
||||
|
||||
merge(Role.AUDITOR, MANUAL, STEAM).andExpect(status().isOk());
|
||||
|
||||
assertThat(jdbc.queryForObject("select count(*) from game_credit where appid = ?", Integer.class, MANUAL)).isZero();
|
||||
assertThat(jdbc.queryForObject("select count(*) from game_credit_check where appid = ?", Integer.class, MANUAL)).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void merge_IntoAKeyWithItsOwnSet_JoinsBothSets() throws Exception {
|
||||
manualRow(AUDITOR_ID, MANUAL, GameStatus.BACKLOG);
|
||||
|
||||
@@ -7,6 +7,8 @@ import core_service.modules.library.domain.GameTimeToBeat;
|
||||
import core_service.modules.library.domain.SteamGame;
|
||||
import core_service.modules.library.domain.UserGame;
|
||||
import core_service.modules.library.domain.UserGameId;
|
||||
import core_service.modules.library.repo.GameCreditCheckRepository;
|
||||
import core_service.modules.library.repo.GameCreditRepository;
|
||||
import core_service.modules.library.repo.GameMoodsRepository;
|
||||
import core_service.modules.library.repo.GameReleaseRepository;
|
||||
import core_service.modules.library.repo.GameSpeedrunRepository;
|
||||
@@ -76,9 +78,17 @@ class LibraryControllerPickerTest extends AbstractMockedLibraryServicesTest {
|
||||
@Autowired
|
||||
private UserGameAchievementMetaRepository achievementMetaRepository;
|
||||
|
||||
@Autowired
|
||||
private GameCreditRepository gameCreditRepository;
|
||||
|
||||
@Autowired
|
||||
private GameCreditCheckRepository gameCreditCheckRepository;
|
||||
|
||||
@BeforeEach
|
||||
void clean() {
|
||||
gameReleaseRepository.deleteAll();
|
||||
gameCreditRepository.deleteAll();
|
||||
gameCreditCheckRepository.deleteAll();
|
||||
// A game with unlocked achievements is never coming; other test classes leave such rows.
|
||||
achievementMetaRepository.deleteAll();
|
||||
userGameRepository.deleteAll();
|
||||
@@ -103,6 +113,14 @@ class LibraryControllerPickerTest extends AbstractMockedLibraryServicesTest {
|
||||
}
|
||||
}
|
||||
|
||||
private void credit(long appid, String kind, long igdbId, String name) {
|
||||
gameCreditRepository.insert(appid, kind, igdbId, name);
|
||||
}
|
||||
|
||||
private void release(long appid, LocalDate date) {
|
||||
gameReleaseRepository.upsert(appid, date, false, Instant.now());
|
||||
}
|
||||
|
||||
private void putOrder(Long... appids) throws Exception {
|
||||
mockMvc.perform(put("/api/library/backlog").header(ClientHeaderFilter.CLIENT_HEADER, "test")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
@@ -247,13 +265,20 @@ class LibraryControllerPickerTest extends AbstractMockedLibraryServicesTest {
|
||||
}
|
||||
gameMoodsRepository.upsert(1L, new String[]{"STORY"}, new String[0], false, Instant.now());
|
||||
gameMoodsRepository.upsert(10L, new String[]{"STORY"}, new String[0], false, Instant.now());
|
||||
credit(1L, "SERIES", 100L, "Series");
|
||||
credit(12L, "SERIES", 100L, "Series");
|
||||
credit(1L, "DEVELOPER", 200L, "Studio");
|
||||
credit(13L, "DEVELOPER", 200L, "Studio");
|
||||
|
||||
mockMvc.perform(get("/api/library/picker"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.categories[*].source", contains("NEXT_UP", "SIMILAR", "LONGEST", "RANDOM")))
|
||||
.andExpect(jsonPath("$.categories[*].source",
|
||||
contains("NEXT_UP", "SERIES", "SIMILAR", "STUDIO", "LONGEST", "RANDOM")))
|
||||
.andExpect(jsonPath("$.categories[*].games.length()", everyItem(lessThanOrEqualTo(3))))
|
||||
.andExpect(jsonPath("$.categories[0].games[*].reason.source", everyItem(is("NEXT_UP"))))
|
||||
.andExpect(jsonPath("$.categories[2].games[0].reason.firstSeenAt").exists());
|
||||
.andExpect(jsonPath("$.categories[1].games[0].reason.source").value("SERIES"))
|
||||
.andExpect(jsonPath("$.categories[3].games[0].reason.source").value("STUDIO"))
|
||||
.andExpect(jsonPath("$.categories[4].games[0].reason.firstSeenAt").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -329,12 +354,155 @@ class LibraryControllerPickerTest extends AbstractMockedLibraryServicesTest {
|
||||
game(10L, GameStatus.BACKLOG, null, 60);
|
||||
game(20L, GameStatus.BACKLOG, null, 3000);
|
||||
game(30L, GameStatus.BACKLOG, null, 60);
|
||||
game(5L, GameStatus.FINISHED, null, null, 600, SEEN);
|
||||
for (long appid : new long[]{5L, 10L, 20L, 30L}) {
|
||||
credit(appid, "SERIES", 100L, "Series");
|
||||
credit(appid, "DEVELOPER", 200L, "Studio");
|
||||
}
|
||||
|
||||
mockMvc.perform(get("/api/library/picker").param("maxMinutes", "300").param("exclude", "30"))
|
||||
.andExpect(jsonPath(row("SERIES"), contains(10)))
|
||||
.andExpect(jsonPath(row("STUDIO"), contains(10)))
|
||||
.andExpect(jsonPath("$.categories[*].games[*].appid", everyItem(is(10))))
|
||||
.andExpect(jsonPath("$.categories[*].games[*].appid", not(hasItem(20))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void picker_NextInSeries_PicksThePartReleasedAfterThePlayedOne() throws Exception {
|
||||
game(1L, GameStatus.FINISHED, null, null, 600, SEEN);
|
||||
game(2L, GameStatus.BACKLOG, null, null);
|
||||
game(3L, GameStatus.BACKLOG, null, null);
|
||||
game(4L, GameStatus.BACKLOG, null, null);
|
||||
for (long appid = 1; appid <= 4; appid++) {
|
||||
credit(appid, "SERIES", 100L, "Mass Effect");
|
||||
}
|
||||
release(1L, LocalDate.of(2007, 11, 20));
|
||||
release(2L, LocalDate.of(2010, 1, 26));
|
||||
release(3L, LocalDate.of(2012, 3, 6));
|
||||
release(4L, LocalDate.of(2005, 1, 1));
|
||||
|
||||
mockMvc.perform(get("/api/library/picker").param("category", "SERIES"))
|
||||
.andExpect(jsonPath("$.categories[0].games[*].appid", contains(2)))
|
||||
.andExpect(jsonPath("$.categories[0].games[0].reason.source").value("SERIES"))
|
||||
.andExpect(jsonPath("$.categories[0].games[0].reason.seriesName").value("Mass Effect"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void picker_NextInSeries_WithoutDates_StillPicksAPart() throws Exception {
|
||||
game(1L, GameStatus.PLAYING, null, null, 600, SEEN);
|
||||
game(2L, GameStatus.BACKLOG, null, null);
|
||||
credit(1L, "SERIES", 100L, "Mass Effect");
|
||||
credit(2L, "SERIES", 100L, "Mass Effect");
|
||||
|
||||
mockMvc.perform(get("/api/library/picker").param("category", "SERIES"))
|
||||
.andExpect(jsonPath("$.categories[0].games[*].appid", contains(2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void picker_NextInSeries_OneGamePerSeriesMostPlayedSeriesFirst() throws Exception {
|
||||
game(1L, GameStatus.FINISHED, null, null, 100, SEEN);
|
||||
game(2L, GameStatus.FINISHED, null, null, 900, SEEN);
|
||||
game(11L, GameStatus.BACKLOG, null, null);
|
||||
game(12L, GameStatus.BACKLOG, null, null);
|
||||
game(13L, GameStatus.BACKLOG, null, null);
|
||||
game(21L, GameStatus.BACKLOG, null, null);
|
||||
credit(1L, "SERIES", 100L, "Small");
|
||||
credit(2L, "SERIES", 200L, "Big");
|
||||
credit(11L, "SERIES", 100L, "Small");
|
||||
credit(12L, "SERIES", 100L, "Small");
|
||||
credit(13L, "SERIES", 100L, "Small");
|
||||
credit(21L, "SERIES", 200L, "Big");
|
||||
|
||||
mockMvc.perform(get("/api/library/picker").param("category", "SERIES"))
|
||||
.andExpect(jsonPath("$.categories[0].games[*].appid", contains(21, 11)))
|
||||
.andExpect(jsonPath("$.categories[0].games[*].reason.seriesName", contains("Big", "Small")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void picker_NextInSeries_SeriesOnlyInBacklog_IsNotPlayed() throws Exception {
|
||||
game(1L, GameStatus.BACKLOG, null, null, 500, SEEN);
|
||||
game(2L, GameStatus.BACKLOG, null, null);
|
||||
credit(1L, "SERIES", 100L, "Mass Effect");
|
||||
credit(2L, "SERIES", 100L, "Mass Effect");
|
||||
|
||||
mockMvc.perform(get("/api/library/picker"))
|
||||
.andExpect(jsonPath("$.categories[*].source", not(hasItem("SERIES"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void picker_NextInSeries_NeverPicksAComingGame() throws Exception {
|
||||
game(1L, GameStatus.FINISHED, null, null, 600, SEEN);
|
||||
game(2L, GameStatus.BACKLOG, null, null);
|
||||
credit(1L, "SERIES", 100L, "Mass Effect");
|
||||
credit(2L, "SERIES", 100L, "Mass Effect");
|
||||
gameReleaseRepository.upsert(2L, LocalDate.now(ZoneId.of("Europe/Berlin")).plusDays(3), false, Instant.now());
|
||||
|
||||
mockMvc.perform(get("/api/library/picker"))
|
||||
.andExpect(jsonPath("$.categories[*].source", not(hasItem("SERIES"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void picker_SameStudio_PrefersDeveloperAndOneGamePerStudio() throws Exception {
|
||||
game(1L, GameStatus.FINISHED, null, null, 600, SEEN);
|
||||
game(11L, GameStatus.BACKLOG, null, null);
|
||||
game(12L, GameStatus.BACKLOG, null, null);
|
||||
game(13L, GameStatus.BACKLOG, null, null);
|
||||
credit(1L, "DEVELOPER", 5L, "BioWare");
|
||||
credit(1L, "PUBLISHER", 6L, "EA");
|
||||
credit(11L, "DEVELOPER", 5L, "BioWare");
|
||||
credit(12L, "PUBLISHER", 5L, "BioWare");
|
||||
credit(13L, "PUBLISHER", 6L, "EA");
|
||||
|
||||
mockMvc.perform(get("/api/library/picker").param("category", "STUDIO"))
|
||||
.andExpect(jsonPath("$.categories[0].games[*].appid", contains(11)))
|
||||
.andExpect(jsonPath("$.categories[0].games[0].reason.source").value("STUDIO"))
|
||||
.andExpect(jsonPath("$.categories[0].games[0].reason.studioName").value("BioWare"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void picker_SameStudio_PublisherOfPlayedGameIsNoAnchor() throws Exception {
|
||||
game(1L, GameStatus.FINISHED, null, null, 600, SEEN);
|
||||
game(11L, GameStatus.BACKLOG, null, null);
|
||||
credit(1L, "PUBLISHER", 6L, "EA");
|
||||
credit(11L, "DEVELOPER", 6L, "EA");
|
||||
|
||||
mockMvc.perform(get("/api/library/picker"))
|
||||
.andExpect(jsonPath("$.categories[*].source", not(hasItem("STUDIO"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void picker_SameStudio_NeverRepeatsAGame() throws Exception {
|
||||
game(1L, GameStatus.FINISHED, null, null, 600, SEEN);
|
||||
game(2L, GameStatus.FINISHED, null, null, 500, SEEN);
|
||||
game(11L, GameStatus.BACKLOG, null, null);
|
||||
credit(1L, "DEVELOPER", 5L, "BioWare");
|
||||
credit(2L, "DEVELOPER", 7L, "Other");
|
||||
credit(11L, "DEVELOPER", 5L, "BioWare");
|
||||
credit(11L, "DEVELOPER", 7L, "Other");
|
||||
|
||||
mockMvc.perform(get("/api/library/picker").param("category", "STUDIO"))
|
||||
.andExpect(jsonPath("$.categories[0].games", hasSize(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameStudio_AStartedBacklogGameIsNotItsOwnAnchor() throws Exception {
|
||||
game(11L, GameStatus.BACKLOG, null, null, 300, SEEN);
|
||||
credit(11L, "DEVELOPER", 5L, "BioWare");
|
||||
|
||||
mockMvc.perform(get("/api/library/picker"))
|
||||
.andExpect(jsonPath("$.categories[*].source", not(hasItem("STUDIO"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void picker_NoCredits_LeavesOutSeriesAndStudioRows() throws Exception {
|
||||
game(1L, GameStatus.FINISHED, null, null, 600, SEEN);
|
||||
game(10L, GameStatus.BACKLOG, null, null);
|
||||
|
||||
mockMvc.perform(get("/api/library/picker"))
|
||||
.andExpect(jsonPath("$.categories[*].source", not(hasItem("SERIES"))))
|
||||
.andExpect(jsonPath("$.categories[*].source", not(hasItem("STUDIO"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void picker_Category_ReturnsOnlyThatRow() throws Exception {
|
||||
game(10L, GameStatus.BACKLOG, null, null);
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package core_service.modules.library.repo;
|
||||
|
||||
import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.PlayedSeries;
|
||||
import core_service.modules.library.domain.PlayedStudio;
|
||||
import core_service.modules.library.domain.SteamGame;
|
||||
import core_service.modules.library.domain.UserGame;
|
||||
import core_service.modules.library.domain.UserGameId;
|
||||
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.data.domain.PageRequest;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class GameCreditRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
private static final long USER_ID = 1L;
|
||||
private static final PageRequest TOP = PageRequest.of(0, 6);
|
||||
|
||||
@Autowired
|
||||
private GameCreditRepository repository;
|
||||
|
||||
@Autowired
|
||||
private GameCreditCheckRepository checkRepository;
|
||||
|
||||
@Autowired
|
||||
private SteamGameRepository steamGameRepository;
|
||||
|
||||
@Autowired
|
||||
private UserGameRepository userGameRepository;
|
||||
|
||||
@Autowired
|
||||
private GameReleaseRepository gameReleaseRepository;
|
||||
|
||||
@BeforeEach
|
||||
void clean() {
|
||||
userGameRepository.deleteAll();
|
||||
repository.deleteAll();
|
||||
checkRepository.deleteAll();
|
||||
gameReleaseRepository.deleteAll();
|
||||
}
|
||||
|
||||
private UserGame game(long appid, GameStatus status, int playtime) {
|
||||
steamGameRepository.save(new SteamGame(appid, "Game " + appid, null, Instant.now()));
|
||||
UserGame game = new UserGame(USER_ID, appid, playtime, Instant.now(), Instant.now());
|
||||
game.setStatus(status);
|
||||
return userGameRepository.saveAndFlush(game);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findPlayedSeries_MostPlayedFirstWithTheLatestPlayedRelease() {
|
||||
game(1L, GameStatus.FINISHED, 600);
|
||||
game(2L, GameStatus.FINISHED, 300);
|
||||
game(3L, GameStatus.BACKLOG, 5000);
|
||||
game(4L, GameStatus.DROPPED, 0);
|
||||
game(5L, GameStatus.PLAYING, 100);
|
||||
gameReleaseRepository.upsert(1L, LocalDate.of(2007, 11, 20), false, Instant.now());
|
||||
gameReleaseRepository.upsert(2L, LocalDate.of(2010, 1, 26), false, Instant.now());
|
||||
repository.insert(1L, "SERIES", 10L, "Mass Effect");
|
||||
repository.insert(2L, "SERIES", 10L, "Mass Effect");
|
||||
repository.insert(3L, "SERIES", 10L, "Mass Effect");
|
||||
repository.insert(4L, "SERIES", 20L, "Dropped Series");
|
||||
repository.insert(5L, "SERIES", 30L, "Portal");
|
||||
|
||||
List<PlayedSeries> series = repository.findPlayedSeries(USER_ID, TOP);
|
||||
|
||||
assertThat(series).containsExactly(
|
||||
new PlayedSeries(10L, "Mass Effect", LocalDate.of(2010, 1, 26)),
|
||||
new PlayedSeries(30L, "Portal", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findPlayedStudios_DevelopersOfPlayedGamesOnly() {
|
||||
game(1L, GameStatus.FINISHED, 600);
|
||||
game(2L, GameStatus.FINISHED, 900);
|
||||
game(3L, GameStatus.BACKLOG, 0);
|
||||
repository.insert(1L, "DEVELOPER", 5L, "BioWare");
|
||||
repository.insert(1L, "PUBLISHER", 6L, "EA");
|
||||
repository.insert(2L, "PUBLISHER", 6L, "EA");
|
||||
repository.insert(3L, "DEVELOPER", 7L, "Unplayed Studio");
|
||||
|
||||
assertThat(repository.findPlayedStudios(USER_ID, TOP)).containsExactly(new PlayedStudio(5L, "BioWare"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findUncheckedAppids_SkipsCheckedRemovedAndHidden() {
|
||||
game(1L, GameStatus.BACKLOG, 0);
|
||||
game(2L, GameStatus.BACKLOG, 0);
|
||||
game(3L, GameStatus.BACKLOG, 0);
|
||||
game(4L, GameStatus.BACKLOG, 0);
|
||||
game(5L, GameStatus.BACKLOG, 0);
|
||||
userGameRepository.findById(new UserGameId(USER_ID, 3L)).orElseThrow().setRemovedAt(Instant.now());
|
||||
userGameRepository.findById(new UserGameId(USER_ID, 4L)).orElseThrow().setHiddenAt(Instant.now());
|
||||
userGameRepository.flush();
|
||||
checkRepository.upsert(2L, Instant.now());
|
||||
|
||||
assertThat(checkRepository.findUncheckedAppids(10)).containsExactly(1L, 5L);
|
||||
assertThat(checkRepository.findUncheckedAppids(1)).containsExactly(1L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package core_service.modules.library.service;
|
||||
|
||||
import core_service.modules.igdb_adapter.service.IgdbApiService;
|
||||
import core_service.modules.library.repo.GameCreditCheckRepository;
|
||||
import core_service.modules.library.repo.GameCreditRepository;
|
||||
import core_service.shared.test.AbstractUnitTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.core.task.SyncTaskExecutor;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.support.SimpleTransactionStatus;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyCollection;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class GameCreditServiceTest extends AbstractUnitTest {
|
||||
|
||||
@Mock
|
||||
private IgdbApiService igdbApiService;
|
||||
|
||||
@Mock
|
||||
private GameCreditRepository creditRepository;
|
||||
|
||||
@Mock
|
||||
private GameCreditCheckRepository checkRepository;
|
||||
|
||||
private GameCreditService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
PlatformTransactionManager transactionManager = mock(PlatformTransactionManager.class);
|
||||
lenient().when(transactionManager.getTransaction(any())).thenReturn(new SimpleTransactionStatus());
|
||||
service = new GameCreditService(igdbApiService, creditRepository, checkRepository, new SyncTaskExecutor(),
|
||||
transactionManager, 30, 250);
|
||||
}
|
||||
|
||||
private static IgdbApiService.Credits credits() {
|
||||
return new IgdbApiService.Credits(List.of(new IgdbApiService.Series(10, "Mass Effect")),
|
||||
List.of(new IgdbApiService.Company(5, "BioWare", true, false),
|
||||
new IgdbApiService.Company(6, "EA", false, true)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void refresh_ReplacesTheCreditsAndWritesTheCheckOfAnsweredGames() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(checkRepository.findFreshAppids(anyCollection(), any())).thenReturn(Set.of());
|
||||
when(igdbApiService.getCredits(Set.of(620L, 700L))).thenReturn(Optional.of(Map.of(620L, credits())));
|
||||
|
||||
service.refresh(List.of(620L, 700L));
|
||||
|
||||
verify(creditRepository).deleteByAppid(620L);
|
||||
verify(creditRepository).insert(620L, "SERIES", 10L, "Mass Effect");
|
||||
verify(creditRepository).insert(620L, "DEVELOPER", 5L, "BioWare");
|
||||
verify(creditRepository).insert(620L, "PUBLISHER", 6L, "EA");
|
||||
verify(checkRepository).upsert(eq(620L), any(Instant.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void refresh_StoresADeveloperThatAlsoPublishedOnce() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(checkRepository.findFreshAppids(anyCollection(), any())).thenReturn(Set.of());
|
||||
IgdbApiService.Credits both = new IgdbApiService.Credits(List.of(),
|
||||
List.of(new IgdbApiService.Company(5, "BioWare", true, true)));
|
||||
when(igdbApiService.getCredits(any())).thenReturn(Optional.of(Map.of(620L, both)));
|
||||
|
||||
service.refresh(List.of(620L));
|
||||
|
||||
verify(creditRepository).insert(620L, "DEVELOPER", 5L, "BioWare");
|
||||
verify(creditRepository, never()).insert(anyLong(), eq("PUBLISHER"), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refresh_GameUnknownToIgdb_WritesOnlyTheCheck() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(checkRepository.findFreshAppids(anyCollection(), any())).thenReturn(Set.of());
|
||||
when(igdbApiService.getCredits(any())).thenReturn(Optional.of(Map.of()));
|
||||
|
||||
service.refresh(List.of(700L));
|
||||
|
||||
verify(creditRepository).deleteByAppid(700L);
|
||||
verify(creditRepository, never()).insert(anyLong(), anyString(), anyLong(), anyString());
|
||||
verify(checkRepository).upsert(eq(700L), any(Instant.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void refresh_IgdbFailed_WritesNothing() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(checkRepository.findFreshAppids(anyCollection(), any())).thenReturn(Set.of());
|
||||
when(igdbApiService.getCredits(any())).thenReturn(Optional.empty());
|
||||
|
||||
service.refresh(List.of(700L));
|
||||
|
||||
verify(checkRepository, never()).upsert(any(), any());
|
||||
verifyNoInteractions(creditRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void backfill_RefreshesTheNextUncheckedBatchOnly() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(checkRepository.findUncheckedAppids(250)).thenReturn(List.of(1L, 2L));
|
||||
when(checkRepository.findFreshAppids(anyCollection(), any())).thenReturn(Set.of());
|
||||
when(igdbApiService.getCredits(Set.of(1L, 2L))).thenReturn(Optional.of(Map.of(1L, credits())));
|
||||
|
||||
service.backfill();
|
||||
|
||||
verify(checkRepository).upsert(eq(1L), any(Instant.class));
|
||||
verify(checkRepository).upsert(eq(2L), any(Instant.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void backfill_NothingUnchecked_CallsNoIgdb() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(checkRepository.findUncheckedAppids(250)).thenReturn(List.of());
|
||||
|
||||
service.backfill();
|
||||
|
||||
verify(igdbApiService, never()).getCredits(any());
|
||||
verify(igdbApiService, never()).getCreditsByGameIds(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void backfill_IgdbDown_WritesNothingAndDoesNotThrow() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(checkRepository.findUncheckedAppids(250)).thenReturn(List.of(1L));
|
||||
when(checkRepository.findFreshAppids(anyCollection(), any())).thenReturn(Set.of());
|
||||
when(igdbApiService.getCredits(any())).thenThrow(new IllegalStateException("down"));
|
||||
|
||||
service.backfill();
|
||||
|
||||
verify(checkRepository, never()).upsert(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void backfill_IgdbOff_DoesNothing() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(false);
|
||||
|
||||
service.backfill();
|
||||
|
||||
verifyNoInteractions(checkRepository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshInBackground_AllFresh_QueuesNothing() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(checkRepository.findFreshAppids(anyCollection(), any())).thenReturn(Set.of(620L));
|
||||
|
||||
service.refreshInBackground(List.of(620L));
|
||||
|
||||
verify(igdbApiService, never()).getCredits(any());
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ class GameImportWorkerTest {
|
||||
.thenReturn(Optional.of(7L), Optional.empty());
|
||||
when(repository.findStalled(any())).thenReturn(Map.of());
|
||||
GameImportWorker worker = new GameImportWorker(repository, mock(ManualGameService.class),
|
||||
mock(TimeToBeatService.class), mock(MoodService.class), mock(ReleaseDateService.class), 5);
|
||||
mock(TimeToBeatService.class), mock(MoodService.class), mock(ReleaseDateService.class), mock(GameCreditService.class), 5);
|
||||
try {
|
||||
worker.wakeMatching();
|
||||
verify(repository, timeout(2000)).findNext(GameImportWorker.MATCHING_QUEUE);
|
||||
|
||||
@@ -132,6 +132,9 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
||||
@Mock
|
||||
private GameEditionService gameEditionService;
|
||||
|
||||
@Mock
|
||||
private GameCreditService gameCreditService;
|
||||
|
||||
@Mock
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@@ -576,6 +579,7 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
||||
verify(moodService).refresh(List.of(220L, -1025L));
|
||||
verify(releaseDateService).refresh(List.of(220L, -1025L));
|
||||
verify(gameEditionService).refresh(List.of(220L, -1025L));
|
||||
verify(gameCreditService).refresh(List.of(220L, -1025L));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -53,6 +53,9 @@ class ManualGameServiceTest extends AbstractUnitTest {
|
||||
@Mock
|
||||
private GameEditionService gameEditionService;
|
||||
|
||||
@Mock
|
||||
private GameCreditService gameCreditService;
|
||||
|
||||
@Mock
|
||||
private ReleaseDateService releaseDateService;
|
||||
|
||||
@@ -64,7 +67,7 @@ class ManualGameServiceTest extends AbstractUnitTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new ManualGameService(igdbApiService, userGameRepository, steamGameRepository, mergeRepository, timeToBeatService,
|
||||
moodService, gameEditionService, releaseDateService, transactionManager);
|
||||
moodService, gameEditionService, gameCreditService, releaseDateService, transactionManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -30,7 +30,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
// 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.inbox.purge-cron=-", "app.follow.digest-ms=86400000", "app.rewards.settle-cron=-", "app.speedrun.enabled=false",
|
||||
"app.library.import-watchdog-ms=86400000",
|
||||
"app.library.import-watchdog-ms=86400000", "app.library.credit-backfill-ms=86400000",
|
||||
"spring.datasource.hikari.maximum-pool-size=4"})
|
||||
@ActiveProfiles("dev")
|
||||
public abstract class AbstractIntegrationTest {
|
||||
|
||||
Reference in new issue
Block a user