Public Access
feat(library): add IGDB editions, game links and auditor queue
Tables game_edition (IGDB "same game" id per library key), game_link (global sets set by auditors), game_link_pair (queue of IGDB and requested pairs), game_link_request and account_name. Fetch IGDB editions, link and unlink with audit rows, serve the paged auditor queue with request counts. WireMock recordings for the editions query.
This commit is contained in:
1 parent
a12ed20705
commit
e806670d26
43 files changed
+1877
-22
No files matched your search
@@ -45,6 +45,7 @@ class LocalMocksParseTest extends AbstractUnitTest {
|
||||
Map.entry("igdb-external-games", IgdbResponses.ExternalGame[].class),
|
||||
Map.entry("igdb-time-to-beats", IgdbResponses.TimeToBeat[].class),
|
||||
Map.entry("igdb-tags", IgdbResponses.GameTags[].class),
|
||||
Map.entry("igdb-editions", IgdbResponses.Edition[].class),
|
||||
Map.entry("igdb-game-", IgdbResponses.Game[].class),
|
||||
Map.entry("speedrun-games-", SpeedrunResponses.Games.class),
|
||||
Map.entry("speedrun-runs-", SpeedrunResponses.Runs.class));
|
||||
|
||||
@@ -17,7 +17,8 @@ class AccountCleanupRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
private static final List<String> USER_TABLES = List.of("user_game_status_change", "user_game_achievement",
|
||||
"user_game_achievement_meta", "user_game_playtime_day", "wishlist_game", "backlog_snapshot",
|
||||
"user_steam_sync", "user_game", "game_import", "reward_ledger", "reward_equipped");
|
||||
"user_steam_sync", "user_game", "game_link_request", "account_name", "game_import", "reward_ledger",
|
||||
"reward_equipped");
|
||||
|
||||
@Autowired
|
||||
private AccountCleanupRepository repository;
|
||||
@@ -42,6 +43,8 @@ class AccountCleanupRepositoryTest extends AbstractPersistenceTest {
|
||||
jdbc.update("""
|
||||
insert into user_steam_sync (user_id, steam_id, user_seen_at) values (?, '76561198000000001', now())""",
|
||||
userId);
|
||||
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 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);
|
||||
|
||||
@@ -109,9 +109,6 @@ class IgdbApiServiceTest extends AbstractUnitTest {
|
||||
Map.entry(220L, new IgdbApiService.GameTags(List.of(), List.of(), List.of())));
|
||||
}
|
||||
|
||||
// REVIEW-FINDING: one Steam app linked to two IGDB games (main entry + edition) keeps the tags of whichever game
|
||||
// the HashMap yields first and drops the other's. Here game 72 has no tags and game 233 is Horror; 72 sits in the
|
||||
// earlier bucket, so 620 is stored with no mood. Time to beat picks the most-voted entry; tags should be the union.
|
||||
@Test
|
||||
void getGameTags_SteamAppLinkedToTwoIgdbGames_KeepsTheTagsOfBoth() {
|
||||
expectToken();
|
||||
@@ -201,6 +198,56 @@ class IgdbApiServiceTest extends AbstractUnitTest {
|
||||
igdb.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEditions_ResolvesEditionsRemastersPortsAndSameGame() {
|
||||
expectToken();
|
||||
igdb.expect(requestTo(containsString("/external_games")))
|
||||
.andRespond(withSuccess("[{\"game\": 11, \"uid\": \"1\"}, {\"game\": 12, \"uid\": \"2\"},"
|
||||
+ " {\"game\": 13, \"uid\": \"3\"}, {\"game\": 14, \"uid\": \"4\"},"
|
||||
+ " {\"game\": 15, \"uid\": \"5\"}, {\"game\": 99, \"uid\": \"6\"},"
|
||||
+ " {\"game\": 22, \"uid\": \"7\"}, {\"game\": 21, \"uid\": \"7\"}]",
|
||||
MediaType.APPLICATION_JSON));
|
||||
igdb.expect(requestTo(containsString("/games")))
|
||||
.andExpect(content().string(containsString("fields version_parent,parent_game,game_type;")))
|
||||
.andRespond(withSuccess("[{\"id\": 11, \"version_parent\": 10, \"game_type\": 0},"
|
||||
+ " {\"id\": 12, \"parent_game\": 10, \"game_type\": 11},"
|
||||
+ " {\"id\": 13, \"parent_game\": 10, \"game_type\": 9},"
|
||||
+ " {\"id\": 14, \"parent_game\": 10, \"game_type\": 8},"
|
||||
+ " {\"id\": 15, \"game_type\": 0},"
|
||||
+ " {\"id\": 22, \"game_type\": 0}, {\"id\": 21, \"game_type\": 0}]",
|
||||
MediaType.APPLICATION_JSON));
|
||||
|
||||
Map<Long, IgdbApiService.Edition> editions = service.getEditions(List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L)).orElseThrow();
|
||||
|
||||
assertThat(editions).containsOnlyKeys(1L, 2L, 3L, 4L, 5L, 7L);
|
||||
assertThat(editions.get(1L)).isEqualTo(new IgdbApiService.Edition(11, 10));
|
||||
assertThat(editions.get(2L).sameGameId()).isEqualTo(10);
|
||||
assertThat(editions.get(3L).sameGameId()).isEqualTo(10);
|
||||
assertThat(editions.get(4L).sameGameId()).isEqualTo(14);
|
||||
assertThat(editions.get(5L).sameGameId()).isEqualTo(15);
|
||||
assertThat(editions.get(7L).igdbId()).isEqualTo(21);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEditionsByGameIds_KeepsOnlyTheAskedGames() {
|
||||
expectToken();
|
||||
igdb.expect(requestTo(containsString("/games")))
|
||||
.andExpect(content().string(containsString("id = (1025)")))
|
||||
.andRespond(withSuccess("[{\"id\": 1025, \"game_type\": 0}, {\"id\": 999, \"version_parent\": 1}]",
|
||||
MediaType.APPLICATION_JSON));
|
||||
|
||||
assertThat(service.getEditionsByGameIds(List.of(1025L)).orElseThrow())
|
||||
.containsOnly(Map.entry(1025L, new IgdbApiService.Edition(1025, 1025)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEditions_IgdbError_IsEmpty() {
|
||||
expectToken();
|
||||
igdb.expect(requestTo(containsString("/external_games"))).andRespond(withServerError());
|
||||
|
||||
assertThat(service.getEditions(List.of(1L))).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getGameTagsByGameIds_AsksTheGamesDirectly() {
|
||||
expectToken();
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
package core_service.modules.library.api;
|
||||
|
||||
import core_service.configuration.security.ClientHeaderFilter;
|
||||
import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.SteamGame;
|
||||
import core_service.modules.library.domain.UserGame;
|
||||
import core_service.modules.library.repo.GameLinkRepository;
|
||||
import core_service.modules.library.repo.SteamGameRepository;
|
||||
import core_service.modules.library.repo.UserGameRepository;
|
||||
import core_service.shared.security.Role;
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import core_service.shared.test.ContractTokens;
|
||||
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.http.MediaType;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BrokenBarrierException;
|
||||
import java.util.concurrent.CyclicBarrier;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
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.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Links and requests through the real (non-dev) filter chain, so the role in the token decides.
|
||||
*/
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles(value = "prod", inheritProfiles = false)
|
||||
class GameLinkFlowTest extends AbstractIntegrationTest {
|
||||
|
||||
private static final long AUDITOR_ID = 5003L;
|
||||
private static final long USER_A = 5001L;
|
||||
private static final long USER_B = 5002L;
|
||||
private static final long GAME = -1_910_007L;
|
||||
private static final long GAME_2 = -1_910_008L;
|
||||
private static final long FILLER = -1_920_000L;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private SteamGameRepository steamGameRepository;
|
||||
|
||||
@Autowired
|
||||
private UserGameRepository userGameRepository;
|
||||
|
||||
@MockitoSpyBean
|
||||
private GameLinkRepository linkRepository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
for (String table : List.of("game_link_request", "game_link_pair", "game_link", "game_edition", "admin_audit")) {
|
||||
jdbc.update("delete from " + table);
|
||||
}
|
||||
userGameRepository.deleteAll();
|
||||
jdbc.update("delete from steam_game where appid in (?, ?) or appid <= ?", GAME, GAME_2, FILLER);
|
||||
steamGameRepository.save(new SteamGame(GAME, "Hades (Switch)", null, Instant.now()));
|
||||
steamGameRepository.save(new SteamGame(GAME_2, "Hades (PS5)", null, Instant.now()));
|
||||
for (long user : List.of(USER_A, USER_B)) {
|
||||
own(user, GAME);
|
||||
own(user, GAME_2);
|
||||
}
|
||||
jdbc.update("insert into game_edition (appid, igdb_id, same_game_id, checked_at) values (?, 7, 7, now()), (?, 8, 7, now())",
|
||||
GAME, GAME_2);
|
||||
}
|
||||
|
||||
private void own(long userId, long appid) {
|
||||
userGameRepository.save(UserGame.manual(userId, appid, "Switch", GameStatus.BACKLOG, 0, Instant.now()));
|
||||
}
|
||||
|
||||
private MockHttpServletRequestBuilder as(MockHttpServletRequestBuilder request, long userId, Role role) {
|
||||
return request.header("Authorization", "Bearer " + ContractTokens.loginToken(userId, role))
|
||||
.header(ClientHeaderFilter.CLIENT_HEADER, "test");
|
||||
}
|
||||
|
||||
private ResultActions body(MockHttpServletRequestBuilder request, long userId, Role role, String json) throws Exception {
|
||||
return mockMvc.perform(as(request, userId, role).contentType(MediaType.APPLICATION_JSON).content(json));
|
||||
}
|
||||
|
||||
private ResultActions linkAs(Role role, long appid, long otherAppid) throws Exception {
|
||||
return body(post("/api/library/games/" + appid + "/link"), AUDITOR_ID, role, "{\"otherAppid\": " + otherAppid + "}");
|
||||
}
|
||||
|
||||
private ResultActions requestAs(long userId, long appid, long otherAppid) throws Exception {
|
||||
return body(post("/api/library/games/" + appid + "/link-requests"), userId, Role.USER,
|
||||
"{\"otherAppid\": " + otherAppid + "}");
|
||||
}
|
||||
|
||||
private ResultActions queue() throws Exception {
|
||||
return mockMvc.perform(as(get("/api/library/link-queue"), AUDITOR_ID, Role.AUDITOR));
|
||||
}
|
||||
|
||||
private List<String> audit() {
|
||||
return jdbc.query("select * from admin_audit order by id", (rs, i) -> rs.getString("action") + " "
|
||||
+ rs.getString("detail"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void queue_ShowsTheIgdbPair() throws Exception {
|
||||
queue().andExpect(status().isOk()).andExpect(jsonPath("$.content", hasSize(1)))
|
||||
.andExpect(jsonPath("$.content[0].igdb").value(true))
|
||||
.andExpect(jsonPath("$.content[0].requests").value(0))
|
||||
.andExpect(jsonPath("$.content[0].a.name").value("Hades (PS5)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void roles_AUserCannotUseTheAuditorCalls() throws Exception {
|
||||
mockMvc.perform(as(get("/api/library/link-queue"), USER_A, Role.USER)).andExpect(status().isForbidden());
|
||||
linkAs(Role.USER, GAME, GAME_2).andExpect(status().isForbidden());
|
||||
mockMvc.perform(as(delete("/api/library/games/" + GAME + "/link"), USER_A, Role.USER)).andExpect(status().isForbidden());
|
||||
mockMvc.perform(as(delete("/api/library/link-queue/" + GAME + "/" + GAME_2), USER_A, Role.USER))
|
||||
.andExpect(status().isForbidden());
|
||||
assertThat(audit()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void requests_AreCountedPerUserAndPair() throws Exception {
|
||||
requestAs(USER_A, GAME, GAME_2).andExpect(status().isNoContent());
|
||||
requestAs(USER_B, GAME_2, GAME).andExpect(status().isNoContent());
|
||||
requestAs(USER_A, GAME, GAME_2).andExpect(status().isNoContent());
|
||||
|
||||
queue().andExpect(jsonPath("$.content", hasSize(1))).andExpect(jsonPath("$.content[0].requests").value(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requests_BadInput_IsRefused() throws Exception {
|
||||
requestAs(USER_A, GAME, GAME).andExpect(status().isBadRequest()).andExpect(jsonPath("$.code").value("SAME_GAME"));
|
||||
requestAs(USER_A, GAME, -1_910_999L).andExpect(status().isNotFound())
|
||||
.andExpect(jsonPath("$.code").value("GAME_NOT_FOUND"));
|
||||
requestAs(AUDITOR_ID, GAME, GAME_2).andExpect(status().isNotFound());
|
||||
assertThat(jdbc.queryForObject("select count(*) from game_link_request", Integer.class)).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void requests_The21stOpenOneIsTooMany() throws Exception {
|
||||
for (int i = 0; i < 21; i++) {
|
||||
steamGameRepository.save(new SteamGame(FILLER - i, "Filler " + i, null, Instant.now()));
|
||||
own(USER_A, FILLER - i);
|
||||
}
|
||||
for (int i = 0; i < 20; i++) {
|
||||
requestAs(USER_A, GAME, FILLER - i).andExpect(status().isNoContent());
|
||||
}
|
||||
|
||||
requestAs(USER_A, GAME, FILLER - 20).andExpect(status().isTooManyRequests())
|
||||
.andExpect(jsonPath("$.code").value("TOO_MANY_REQUESTS"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void link_ChangesEveryUsersNumbersAndShowsTheEditions() throws Exception {
|
||||
requestAs(USER_A, GAME, GAME_2).andExpect(status().isNoContent());
|
||||
|
||||
linkAs(Role.AUDITOR, GAME, GAME_2).andExpect(status().isNoContent());
|
||||
|
||||
queue().andExpect(jsonPath("$.content", hasSize(0)));
|
||||
assertThat(audit()).containsExactly("GAME_LINK \"Hades (Switch)\" with \"Hades (PS5)\"");
|
||||
for (long user : List.of(USER_A, USER_B)) {
|
||||
mockMvc.perform(as(get("/api/library/games/" + GAME), user, Role.USER)).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.linked").value(true))
|
||||
.andExpect(jsonPath("$.editions", hasSize(1)))
|
||||
.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)));
|
||||
}
|
||||
requestAs(USER_A, GAME, GAME_2).andExpect(status().isConflict()).andExpect(jsonPath("$.code").value("ALREADY_LINKED"));
|
||||
linkAs(Role.AUDITOR, GAME_2, GAME).andExpect(status().isConflict());
|
||||
linkAs(Role.AUDITOR, GAME, GAME).andExpect(status().isBadRequest());
|
||||
linkAs(Role.AUDITOR, GAME, -1_910_999L).andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unlink_DismissesThePairForGood() throws Exception {
|
||||
linkAs(Role.AUDITOR, GAME, GAME_2).andExpect(status().isNoContent());
|
||||
|
||||
mockMvc.perform(as(delete("/api/library/games/" + GAME + "/link"), AUDITOR_ID, Role.AUDITOR))
|
||||
.andExpect(status().isNoContent());
|
||||
|
||||
assertThat(audit()).hasSize(2).last().asString().startsWith("GAME_UNLINK").contains("Hades (PS5)");
|
||||
queue().andExpect(jsonPath("$.content", hasSize(0)));
|
||||
queue().andExpect(jsonPath("$.content", hasSize(0)));
|
||||
requestAs(USER_A, GAME, GAME_2).andExpect(status().isConflict()).andExpect(jsonPath("$.code").value("PAIR_DISMISSED"));
|
||||
assertThat(jdbc.queryForObject("select count(*) from game_link_request", Integer.class)).isZero();
|
||||
mockMvc.perform(as(delete("/api/library/games/" + GAME + "/link"), AUDITOR_ID, Role.AUDITOR))
|
||||
.andExpect(status().isNotFound()).andExpect(jsonPath("$.code").value("NOT_LINKED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dismiss_WritesTheAuditRowAndAnswers404TheSecondTime() throws Exception {
|
||||
queue().andExpect(jsonPath("$.content", hasSize(1)));
|
||||
|
||||
mockMvc.perform(as(delete("/api/library/link-queue/" + GAME_2 + "/" + GAME), AUDITOR_ID, Role.AUDITOR))
|
||||
.andExpect(status().isNoContent());
|
||||
mockMvc.perform(as(delete("/api/library/link-queue/" + GAME_2 + "/" + GAME), AUDITOR_ID, Role.AUDITOR))
|
||||
.andExpect(status().isNotFound()).andExpect(jsonPath("$.code").value("PAIR_NOT_OPEN"));
|
||||
|
||||
assertThat(audit()).containsExactly("GAME_LINK_DISMISS null");
|
||||
queue().andExpect(jsonPath("$.content", hasSize(0)));
|
||||
}
|
||||
|
||||
// Without the per-user lock every call counts 19 before any call inserts (the barrier holds the count until all
|
||||
// calls have counted); with it the first call waits alone, the barrier times out, and the calls run one by one.
|
||||
@Test
|
||||
void requests_ParallelCallsCannotPassTheCap() throws Exception {
|
||||
int parallel = 12;
|
||||
for (int i = 0; i < 19 + parallel; i++) {
|
||||
steamGameRepository.save(new SteamGame(FILLER - i, "Filler " + i, null, Instant.now()));
|
||||
own(USER_A, FILLER - i);
|
||||
}
|
||||
for (int i = 0; i < 19; i++) {
|
||||
requestAs(USER_A, GAME, FILLER - i).andExpect(status().isNoContent());
|
||||
}
|
||||
CyclicBarrier counted = new CyclicBarrier(parallel);
|
||||
doAnswer(call -> {
|
||||
Object count = call.callRealMethod();
|
||||
try {
|
||||
counted.await(500, TimeUnit.MILLISECONDS);
|
||||
} catch (TimeoutException | BrokenBarrierException e) {
|
||||
// The other calls wait for the lock: the barrier is broken, the calls go on one by one.
|
||||
}
|
||||
return count;
|
||||
}).when(linkRepository).countOpenRequests(USER_A);
|
||||
ExecutorService pool = Executors.newFixedThreadPool(parallel);
|
||||
try {
|
||||
List<Future<ResultActions>> calls = new ArrayList<>();
|
||||
for (int i = 0; i < parallel; i++) {
|
||||
long other = FILLER - 19 - i;
|
||||
calls.add(pool.submit(() -> requestAs(USER_A, GAME, other)));
|
||||
}
|
||||
for (Future<ResultActions> call : calls) {
|
||||
call.get();
|
||||
}
|
||||
} finally {
|
||||
pool.shutdownNow();
|
||||
}
|
||||
|
||||
assertThat(jdbc.queryForObject("select count(*) from game_link_request where user_id = ?", Integer.class, USER_A))
|
||||
.isEqualTo(20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void queue_AnotherPageIsEmptyAndKeepsTheTotal() throws Exception {
|
||||
mockMvc.perform(as(get("/api/library/link-queue?page=1"), AUDITOR_ID, Role.AUDITOR)).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.content", hasSize(0))).andExpect(jsonPath("$.totalElements").value(1))
|
||||
.andExpect(jsonPath("$.page").value(1));
|
||||
}
|
||||
}
|
||||
@@ -161,6 +161,55 @@ class GameMergeFlowTest extends AbstractIntegrationTest {
|
||||
+ " \"Hades (Switch)\" into \"Hades\": 1 moved, 0 combined");
|
||||
}
|
||||
|
||||
@Test
|
||||
void merge_MovesTheSetRowAndDropsThePairsRequestsAndEditionOfTheMergedKey() throws Exception {
|
||||
manualRow(AUDITOR_ID, MANUAL, GameStatus.BACKLOG);
|
||||
jdbc.update("delete from game_link");
|
||||
jdbc.update("delete from game_link_pair");
|
||||
jdbc.update("delete from game_link_request");
|
||||
jdbc.update("delete from game_edition");
|
||||
jdbc.update("insert into game_link (appid, set_id) values (?, 1), (?, 1)", MANUAL, MANUAL_2);
|
||||
jdbc.update("insert into game_link_pair (appid_a, appid_b, state) values (?, ?, 'OPEN')", MANUAL_2, STEAM_2);
|
||||
jdbc.update("insert into game_link_pair (appid_a, appid_b, state) values (?, ?, 'OPEN')", MANUAL_2, MANUAL);
|
||||
jdbc.update("insert into game_link_request (user_id, appid_a, appid_b) values (?, ?, ?)", OTHER_USER, MANUAL_2, STEAM_2);
|
||||
jdbc.update("insert into game_edition (appid, same_game_id, checked_at) values (?, 1, now())", MANUAL);
|
||||
|
||||
merge(Role.AUDITOR, MANUAL, STEAM).andExpect(status().isOk());
|
||||
|
||||
assertThat(jdbc.queryForList("select appid from game_link order by appid", Long.class)).containsExactly(MANUAL_2, STEAM);
|
||||
assertThat(jdbc.queryForObject("select count(*) from game_link_pair where ? in (appid_a, appid_b)", Integer.class, MANUAL))
|
||||
.isZero();
|
||||
assertThat(jdbc.queryForObject("select count(*) from game_link_pair", Integer.class)).isEqualTo(1);
|
||||
assertThat(jdbc.queryForObject("select count(*) from game_link_request", Integer.class)).isEqualTo(1);
|
||||
assertThat(jdbc.queryForObject("select count(*) from game_edition", Integer.class)).isZero();
|
||||
assertThat(audit()).hasSize(1).first().asString().contains("GAME_MERGE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void merge_IntoAKeyWithItsOwnSet_JoinsBothSets() throws Exception {
|
||||
manualRow(AUDITOR_ID, MANUAL, GameStatus.BACKLOG);
|
||||
jdbc.update("delete from game_link");
|
||||
jdbc.update("insert into game_link (appid, set_id) values (?, 1), (?, 1), (?, 2), (?, 2)",
|
||||
MANUAL, MANUAL_2, STEAM, STEAM_2);
|
||||
|
||||
merge(Role.AUDITOR, MANUAL, STEAM).andExpect(status().isOk());
|
||||
|
||||
assertThat(jdbc.queryForList("select appid from game_link order by appid", Long.class))
|
||||
.containsExactly(MANUAL_2, STEAM, STEAM_2);
|
||||
assertThat(jdbc.queryForObject("select count(distinct set_id) from game_link", Integer.class)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void merge_TwoKeysOfOneSet_DropsTheSetThatHasOneKeyLeft() throws Exception {
|
||||
manualRow(AUDITOR_ID, MANUAL, GameStatus.BACKLOG);
|
||||
jdbc.update("delete from game_link");
|
||||
jdbc.update("insert into game_link (appid, set_id) values (?, 1), (?, 1)", MANUAL, STEAM);
|
||||
|
||||
merge(Role.AUDITOR, MANUAL, STEAM).andExpect(status().isOk());
|
||||
|
||||
assertThat(jdbc.queryForObject("select count(*) from game_link", Integer.class)).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void paidGameRows_MoveToTheKeptKey_UnlessTheUserHoldsTheKindThereAlready() throws Exception {
|
||||
manualRow(AUDITOR_ID, MANUAL, GameStatus.FINISHED);
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package core_service.modules.library.repo;
|
||||
|
||||
import core_service.shared.test.AbstractPersistenceTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class CountedUserGameViewTest extends AbstractPersistenceTest {
|
||||
|
||||
private static final String COLUMNS = "select column_name from information_schema.columns "
|
||||
+ "where table_schema = 'public' and table_name = ? order by ordinal_position";
|
||||
private static final long USER = 1L;
|
||||
private static final long OTHER_USER = 2L;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
jdbc.update("delete from game_link");
|
||||
jdbc.update("delete from user_game");
|
||||
for (long appid : List.of(-10L, 10L, 20L, 30L, 40L)) {
|
||||
jdbc.update("insert into steam_game (appid, name) values (?, ?) on conflict do nothing", appid, "Game " + appid);
|
||||
}
|
||||
}
|
||||
|
||||
private void set(long appid, long setId) {
|
||||
jdbc.update("insert into game_link (appid, set_id) values (?, ?)", appid, setId);
|
||||
}
|
||||
|
||||
private void own(long userId, long appid, String status, int playtime) {
|
||||
jdbc.update("insert into user_game (user_id, appid, status, playtime_minutes) values (?, ?, ?, ?)",
|
||||
userId, appid, status, playtime);
|
||||
}
|
||||
|
||||
private List<Long> counted(long userId) {
|
||||
return jdbc.queryForList("select appid from counted_user_game where user_id = ? order by appid", Long.class, userId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void view_HasEveryColumnOfUserGame() {
|
||||
assertEquals(jdbc.queryForList(COLUMNS, String.class, "user_game"),
|
||||
jdbc.queryForList(COLUMNS, String.class, "counted_user_game"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void set_CountsTheMostAdvancedEdition() {
|
||||
set(10, 1);
|
||||
set(20, 1);
|
||||
own(USER, 10, "BACKLOG", 0);
|
||||
own(USER, 20, "COMPLETED", 0);
|
||||
|
||||
assertThat(counted(USER)).containsExactly(20L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void set_AllBacklog_CountsTheOneWithMorePlaytimeOnce() {
|
||||
set(10, 1);
|
||||
set(20, 1);
|
||||
own(USER, 10, "BACKLOG", 5);
|
||||
own(USER, 20, "BACKLOG", 50);
|
||||
|
||||
assertThat(counted(USER)).containsExactly(20L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void set_FullTieOnStatusAndPlaytime_CountsTheOlderRow() {
|
||||
set(-10, 1);
|
||||
set(20, 1);
|
||||
own(USER, -10, "BACKLOG", 0);
|
||||
own(USER, 20, "BACKLOG", 0);
|
||||
jdbc.update("update user_game set first_seen_at = now() where appid = -10");
|
||||
jdbc.update("update user_game set first_seen_at = now() - interval '3 years' where appid = 20");
|
||||
|
||||
assertThat(counted(USER)).containsExactly(20L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void set_FullTieAlsoOnTime_CountsTheLowerKey() {
|
||||
set(10, 1);
|
||||
set(20, 1);
|
||||
own(USER, 10, "BACKLOG", 0);
|
||||
own(USER, 20, "BACKLOG", 0);
|
||||
jdbc.update("update user_game set first_seen_at = '2025-01-01T00:00:00Z'");
|
||||
|
||||
assertThat(counted(USER)).containsExactly(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void set_DroppedBeatsBacklog() {
|
||||
set(10, 1);
|
||||
set(20, 1);
|
||||
own(USER, 10, "BACKLOG", 0);
|
||||
own(USER, 20, "DROPPED", 0);
|
||||
|
||||
assertThat(counted(USER)).containsExactly(20L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void set_HiddenSiblingDoesNotWin() {
|
||||
set(10, 1);
|
||||
set(20, 1);
|
||||
own(USER, 10, "BACKLOG", 0);
|
||||
own(USER, 20, "COMPLETED", 0);
|
||||
jdbc.update("update user_game set hidden_at = now() where user_id = ? and appid = 20", USER);
|
||||
|
||||
assertThat(counted(USER)).containsExactly(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void keyInASetTheUserOwnsOnce_IsCounted_AndOtherUsersRowsHaveNoEffect() {
|
||||
set(10, 1);
|
||||
set(20, 1);
|
||||
own(USER, 10, "BACKLOG", 0);
|
||||
own(OTHER_USER, 20, "COMPLETED", 0);
|
||||
own(USER, 30, "BACKLOG", 0);
|
||||
|
||||
assertThat(counted(USER)).containsExactly(10L, 30L);
|
||||
assertThat(counted(OTHER_USER)).containsExactly(20L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package core_service.modules.library.repo;
|
||||
|
||||
import core_service.modules.library.domain.Edition;
|
||||
import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.LinkPair;
|
||||
import core_service.shared.test.AbstractPersistenceTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Import(GameLinkRepository.class)
|
||||
class GameLinkRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
private static final long USER = 1L;
|
||||
private static final long OTHER_USER = 2L;
|
||||
|
||||
@Autowired
|
||||
private GameLinkRepository repository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
for (String table : List.of("game_link_request", "game_link_pair", "game_link", "game_edition", "user_game")) {
|
||||
jdbc.update("delete from " + table);
|
||||
}
|
||||
for (long appid : List.of(100L, 200L, -7L, -8L, 300L)) {
|
||||
jdbc.update("insert into steam_game (appid, name, image_url) values (?, ?, ?) on conflict do nothing",
|
||||
appid, "Game " + appid, "img" + appid);
|
||||
}
|
||||
edition(100, 10);
|
||||
edition(-7, 10);
|
||||
edition(200, 10);
|
||||
edition(-8, 11);
|
||||
}
|
||||
|
||||
private void edition(long appid, long sameGame) {
|
||||
jdbc.update("insert into game_edition (appid, igdb_id, same_game_id, checked_at) values (?, ?, ?, now())",
|
||||
appid, Math.abs(appid), sameGame);
|
||||
}
|
||||
|
||||
private void own(long userId, long appid, String status) {
|
||||
jdbc.update("insert into user_game (user_id, appid, status) values (?, ?, ?)", userId, appid, status);
|
||||
}
|
||||
|
||||
private List<String> pairs(String state) {
|
||||
return jdbc.queryForList("select appid_a || ':' || appid_b from game_link_pair where state = ? order by 1",
|
||||
String.class, state);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addIgdbPairs_InsertsEachPairOnce() {
|
||||
assertThat(repository.addIgdbPairs()).isEqualTo(3);
|
||||
assertThat(repository.addIgdbPairs()).isZero();
|
||||
|
||||
assertThat(pairs("OPEN")).containsExactlyInAnyOrder("-7:100", "-7:200", "100:200");
|
||||
}
|
||||
|
||||
@Test
|
||||
void addIgdbPairs_SkipsKeysOfOneSet() {
|
||||
repository.link(100, 200);
|
||||
|
||||
repository.addIgdbPairs();
|
||||
|
||||
assertThat(pairs("OPEN")).containsExactlyInAnyOrder("-7:100", "-7:200");
|
||||
}
|
||||
|
||||
@Test
|
||||
void addIgdbPairs_DoesNotBringBackADismissedPair() {
|
||||
repository.addIgdbPairs();
|
||||
repository.dismiss(100, 200);
|
||||
|
||||
repository.addIgdbPairs();
|
||||
|
||||
assertThat(pairs("DISMISSED")).containsExactly("100:200");
|
||||
assertThat(pairs("OPEN")).doesNotContain("100:200");
|
||||
}
|
||||
|
||||
@Test
|
||||
void link_MakesExtendsAndMergesSets() {
|
||||
long first = repository.link(100, 200);
|
||||
assertThat(repository.setOf(100)).contains(first);
|
||||
assertThat(repository.setOf(200)).contains(first);
|
||||
|
||||
assertThat(repository.link(200, -7)).isEqualTo(first);
|
||||
assertThat(repository.setOf(-7)).contains(first);
|
||||
|
||||
long second = repository.link(-8, 300);
|
||||
assertThat(second).isNotEqualTo(first);
|
||||
long merged = repository.link(-8, 100);
|
||||
|
||||
assertThat(jdbc.queryForList("select distinct set_id from game_link", Long.class)).containsExactly(merged);
|
||||
assertThat(jdbc.queryForObject("select count(*) from game_link", Integer.class)).isEqualTo(5);
|
||||
assertThat(pairs("LINKED")).contains("100:200", "-8:300", "-8:100");
|
||||
}
|
||||
|
||||
@Test
|
||||
void link_DecidesTheOtherOpenPairsInsideTheSet() {
|
||||
repository.addIgdbPairs();
|
||||
|
||||
repository.link(-7, 100);
|
||||
repository.link(100, 200);
|
||||
|
||||
assertThat(pairs("OPEN")).isEmpty();
|
||||
assertThat(pairs("LINKED")).containsExactlyInAnyOrder("-7:100", "100:200", "-7:200");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unlink_RemovesTheKeyDismissesItsPairsAndDropsASetOfOne() {
|
||||
repository.link(100, 200);
|
||||
repository.link(100, -7);
|
||||
|
||||
assertThat(repository.unlink(-7)).containsExactly(100L, 200L);
|
||||
|
||||
assertThat(repository.setOf(-7)).isEmpty();
|
||||
assertThat(repository.setOf(100)).isPresent();
|
||||
assertThat(pairs("DISMISSED")).containsExactlyInAnyOrder("-7:100", "-7:200");
|
||||
|
||||
assertThat(repository.unlink(100)).containsExactly(200L);
|
||||
assertThat(repository.setOf(200)).isEmpty();
|
||||
assertThat(repository.unlink(100)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void dismiss_OnlyAnOpenPair() {
|
||||
repository.addIgdbPairs();
|
||||
|
||||
assertThat(repository.dismiss(200, 100)).isTrue();
|
||||
assertThat(repository.dismiss(100, 200)).isFalse();
|
||||
assertThat(repository.dismiss(100, 300)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void addRequest_CreatesThePairOnceAndOneRequestPerUser() {
|
||||
assertThat(repository.addRequest(USER, 300, 100)).isTrue();
|
||||
assertThat(repository.addRequest(USER, 100, 300)).isFalse();
|
||||
assertThat(repository.addRequest(OTHER_USER, 100, 300)).isTrue();
|
||||
|
||||
assertThat(pairs("OPEN")).containsExactly("100:300");
|
||||
assertThat(jdbc.queryForObject("select count(*) from game_link_request", Integer.class)).isEqualTo(2);
|
||||
assertThat(repository.pairState(300, 100)).isEqualTo("OPEN");
|
||||
assertThat(repository.pairState(100, 200)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void addIgdbPairs_MarksAnOpenRequestedPairAsIgdb() {
|
||||
repository.addRequest(USER, 100, 200);
|
||||
|
||||
repository.addIgdbPairs();
|
||||
|
||||
assertThat(repository.openPairs(0, 10)).filteredOn(pair -> pair.a().appid() == 100 && pair.b().appid() == 200)
|
||||
.singleElement().extracting(LinkPair::igdb).isEqualTo(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void countOpenRequests_IgnoresPairsThatAreNotOpen() {
|
||||
repository.addRequest(USER, 100, 300);
|
||||
repository.addRequest(USER, 100, 200);
|
||||
repository.addRequest(USER, -7, 300);
|
||||
repository.link(100, 300);
|
||||
repository.dismiss(100, 200);
|
||||
|
||||
assertThat(repository.countOpenRequests(USER)).isEqualTo(1);
|
||||
assertThat(repository.countOpenRequests(OTHER_USER)).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void openPairs_CarryNamesImagesRequestsAndTheOrder() {
|
||||
repository.addIgdbPairs();
|
||||
repository.addRequest(USER, 100, 300);
|
||||
repository.addRequest(USER, 200, 300);
|
||||
repository.addRequest(OTHER_USER, 200, 300);
|
||||
repository.link(-7, 100);
|
||||
|
||||
List<LinkPair> pairs = repository.openPairs(0, 10);
|
||||
|
||||
assertThat(pairs).hasSize(4);
|
||||
assertThat(pairs.getFirst().requests()).isEqualTo(2);
|
||||
assertThat(pairs.getFirst().igdb()).isFalse();
|
||||
assertThat(pairs.getFirst().a().appid()).isEqualTo(200);
|
||||
assertThat(pairs.getFirst().b().name()).isEqualTo("Game 300");
|
||||
assertThat(pairs.getFirst().b().imageUrl()).isEqualTo("img300");
|
||||
assertThat(pairs.get(1).requests()).isEqualTo(1);
|
||||
assertThat(pairs.get(2).igdb()).isTrue();
|
||||
assertThat(pairs.get(2).requests()).isZero();
|
||||
assertThat(pairs.get(3).igdb()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void openPairs_PageWithoutGapsAndCountOnlyOpenPairs() {
|
||||
repository.addIgdbPairs();
|
||||
|
||||
assertThat(repository.countOpenPairs()).isEqualTo(3);
|
||||
List<LinkPair> first = repository.openPairs(0, 2);
|
||||
List<LinkPair> second = repository.openPairs(2, 2);
|
||||
assertThat(first).hasSize(2);
|
||||
assertThat(second).hasSize(1);
|
||||
assertThat(Stream.concat(first.stream(), second.stream())
|
||||
.map(pair -> pair.a().appid() + ":" + pair.b().appid()))
|
||||
.containsExactlyInAnyOrder("-7:100", "-7:200", "100:200");
|
||||
|
||||
repository.dismiss(100, 200);
|
||||
|
||||
assertThat(repository.countOpenPairs()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addIgdbPairs_KeepsADismissedPairDismissed() {
|
||||
repository.addRequest(USER, 100, 200);
|
||||
repository.dismiss(100, 200);
|
||||
|
||||
repository.addIgdbPairs();
|
||||
|
||||
assertThat(pairs("DISMISSED")).containsExactly("100:200");
|
||||
assertThat(jdbc.queryForObject("select igdb from game_link_pair where appid_a = 100 and appid_b = 200",
|
||||
Boolean.class)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void linkedEditions_ListsTheOtherListedRowsOfTheUserOnly() {
|
||||
repository.link(100, 200);
|
||||
repository.link(100, -7);
|
||||
repository.link(100, -8);
|
||||
own(USER, 100, "PLAYING");
|
||||
own(USER, 200, "BACKLOG");
|
||||
own(USER, -7, "FINISHED");
|
||||
own(USER, -8, "BACKLOG");
|
||||
jdbc.update("update user_game set hidden_at = now() where user_id = ? and appid = -8", USER);
|
||||
own(OTHER_USER, 200, "COMPLETED");
|
||||
|
||||
List<Edition> editions = repository.linkedEditions(USER, 100);
|
||||
|
||||
assertThat(editions).extracting(Edition::appid).containsExactly(-7L, 200L);
|
||||
assertThat(editions.getFirst().status()).isEqualTo(GameStatus.FINISHED);
|
||||
assertThat(editions.getFirst().name()).isEqualTo("Game -7");
|
||||
}
|
||||
|
||||
@Test
|
||||
void linkedEditions_RemovedRowIsLeftOut() {
|
||||
repository.link(100, 200);
|
||||
own(USER, 100, "PLAYING");
|
||||
own(USER, 200, "BACKLOG");
|
||||
jdbc.update("update user_game set removed_at = now() where user_id = ? and appid = 200", USER);
|
||||
|
||||
assertThat(repository.linkedEditions(USER, 100)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void editionCounts_GivesTwoForBothKeysAndNothingForASingleOwnedKey() {
|
||||
repository.link(100, 200);
|
||||
repository.link(100, -7);
|
||||
own(USER, 100, "PLAYING");
|
||||
own(USER, 200, "BACKLOG");
|
||||
own(USER, -7, "BACKLOG");
|
||||
own(OTHER_USER, 100, "BACKLOG");
|
||||
|
||||
assertThat(repository.editionCounts(USER, List.of(100L, 200L, 300L))).isEqualTo(Map.of(100L, 3, 200L, 3));
|
||||
assertThat(repository.editionCounts(OTHER_USER, List.of(100L))).isEmpty();
|
||||
assertThat(repository.editionCounts(USER, List.of())).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package core_service.modules.library.service;
|
||||
|
||||
import core_service.modules.igdb_adapter.service.IgdbApiService;
|
||||
import core_service.modules.library.repo.GameEditionRepository;
|
||||
import core_service.shared.test.AbstractUnitTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.springframework.core.task.SyncTaskExecutor;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
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.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
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 GameEditionServiceTest extends AbstractUnitTest {
|
||||
|
||||
@Mock
|
||||
private IgdbApiService igdbApiService;
|
||||
|
||||
@Mock
|
||||
private GameEditionRepository gameEditionRepository;
|
||||
|
||||
@Spy
|
||||
private TaskExecutor applicationTaskExecutor = new SyncTaskExecutor();
|
||||
|
||||
@InjectMocks
|
||||
private GameEditionService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(service, "ttlDays", 30L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void refresh_StoresStaleGamesOnly_AndNullsForAnAnsweredUnknownGame() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(gameEditionRepository.findFreshAppids(anyCollection(), any())).thenReturn(Set.of(220L));
|
||||
when(igdbApiService.getEditions(Set.of(620L, 700L))).thenReturn(Optional.of(Map.of(
|
||||
620L, new IgdbApiService.Edition(72, 10))));
|
||||
|
||||
service.refresh(List.of(220L, 620L, 700L));
|
||||
|
||||
verify(gameEditionRepository).upsert(eq(620L), eq(72L), eq(10L), any(Instant.class));
|
||||
verify(gameEditionRepository).upsert(eq(700L), isNull(), isNull(), any(Instant.class));
|
||||
verify(gameEditionRepository, never()).upsert(eq(220L), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refresh_GamesOffSteam_AreAskedByIgdbIdAndStoredUnderTheirKey() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(gameEditionRepository.findFreshAppids(anyCollection(), any())).thenReturn(Set.of());
|
||||
when(igdbApiService.getEditionsByGameIds(Set.of(119388L))).thenReturn(Optional.of(Map.of(
|
||||
119388L, new IgdbApiService.Edition(119388, 5))));
|
||||
|
||||
service.refresh(List.of(-119388L));
|
||||
|
||||
verify(gameEditionRepository).upsert(eq(-119388L), eq(119388L), eq(5L), any(Instant.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void refresh_IgdbFailed_StoresNothingSoTheGamesAreAskedAgain() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(gameEditionRepository.findFreshAppids(anyCollection(), any())).thenReturn(Set.of());
|
||||
when(igdbApiService.getEditions(any())).thenReturn(Optional.empty());
|
||||
|
||||
service.refresh(List.of(620L));
|
||||
|
||||
verify(gameEditionRepository, never()).upsert(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refresh_IgdbOff_DoesNothing() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(false);
|
||||
|
||||
service.refresh(List.of(620L));
|
||||
|
||||
verifyNoInteractions(gameEditionRepository);
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,9 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
||||
@Mock
|
||||
private MoodService moodService;
|
||||
|
||||
@Mock
|
||||
private GameEditionService gameEditionService;
|
||||
|
||||
@Mock
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@@ -526,6 +529,7 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
||||
|
||||
verify(timeToBeatService).refresh(List.of(220L, -1025L));
|
||||
verify(moodService).refresh(List.of(220L, -1025L));
|
||||
verify(gameEditionService).refresh(List.of(220L, -1025L));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -50,6 +50,9 @@ class ManualGameServiceTest extends AbstractUnitTest {
|
||||
@Mock
|
||||
private MoodService moodService;
|
||||
|
||||
@Mock
|
||||
private GameEditionService gameEditionService;
|
||||
|
||||
@Mock
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
@@ -58,7 +61,7 @@ class ManualGameServiceTest extends AbstractUnitTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new ManualGameService(igdbApiService, userGameRepository, steamGameRepository, mergeRepository, timeToBeatService,
|
||||
moodService, transactionManager);
|
||||
moodService, gameEditionService, transactionManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in new issue
Block a user