Public Access
feat(library): time to beat from IGDB
This commit is contained in:
1 parent
2f2f8a2918
commit
00a7991380
23 files changed
+853
-22
No files matched your search
@@ -33,8 +33,8 @@ class ArchitectureTest extends AbstractArchitectureTest {
|
||||
.should().notDependOnEachOther()
|
||||
.ignoreDependency(
|
||||
JavaClass.Predicates.resideInAPackage("core_service.modules.library.."),
|
||||
JavaClass.Predicates.resideInAPackage("core_service.modules.steam_adapter.."))
|
||||
.because("Feature modules should be isolated; steam_adapter is an explicit shared port.");
|
||||
JavaClass.Predicates.resideInAnyPackage("core_service.modules.steam_adapter..", "core_service.modules.igdb_adapter.."))
|
||||
.because("Feature modules should be isolated; steam_adapter and igdb_adapter are explicit shared ports.");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule no_cyclic_dependencies = SlicesRuleDefinition.slices()
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
package core_service.modules.igdb_adapter.service;
|
||||
|
||||
import core_service.shared.test.AbstractUnitTest;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.LongStream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.allOf;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.CoreMatchers.not;
|
||||
import static org.springframework.test.web.client.ExpectedCount.once;
|
||||
import static org.springframework.test.web.client.ExpectedCount.times;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.queryParam;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.withUnauthorizedRequest;
|
||||
|
||||
class IgdbApiServiceTest extends AbstractUnitTest {
|
||||
|
||||
private static final String TOKEN = "{\"access_token\": \"token-1\", \"expires_in\": 5000000, \"token_type\": \"bearer\"}";
|
||||
|
||||
private MockRestServiceServer twitch;
|
||||
private MockRestServiceServer igdb;
|
||||
private IgdbApiService service;
|
||||
|
||||
private IgdbApiService create(String clientId, String clientSecret) {
|
||||
RestClient.Builder twitchBuilder = RestClient.builder();
|
||||
RestClient.Builder igdbBuilder = RestClient.builder();
|
||||
twitch = MockRestServiceServer.bindTo(twitchBuilder).build();
|
||||
igdb = MockRestServiceServer.bindTo(igdbBuilder).build();
|
||||
return new IgdbApiService(igdbBuilder.build(), twitchBuilder.build(), clientId, clientSecret, RateLimiterRegistry.ofDefaults());
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = create("client-1", "secret-1");
|
||||
}
|
||||
|
||||
private void expectToken() {
|
||||
twitch.expect(once(), requestTo(containsString("/oauth2/token")))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(queryParam("client_id", "client-1"))
|
||||
.andExpect(queryParam("client_secret", "secret-1"))
|
||||
.andExpect(queryParam("grant_type", "client_credentials"))
|
||||
.andRespond(withSuccess(TOKEN, MediaType.APPLICATION_JSON));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isEnabled_OnlyWithIdAndSecret() {
|
||||
assertThat(service.isEnabled()).isTrue();
|
||||
assertThat(create("", "secret").isEnabled()).isFalse();
|
||||
assertThat(create("id", null).isEnabled()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTimeToBeat_MapsSteamAppsToIgdbTimesInMinutes() {
|
||||
expectToken();
|
||||
igdb.expect(requestTo(containsString("/external_games")))
|
||||
.andExpect(header("Client-ID", "client-1"))
|
||||
.andExpect(header("Authorization", "Bearer token-1"))
|
||||
.andExpect(content().string(allOf(containsString("external_game_source = 1"), containsString("uid = (\"620\",\"220\")"))))
|
||||
.andRespond(withSuccess("[{\"id\": 1, \"game\": 72, \"uid\": \"620\"}, {\"id\": 2, \"game\": 233, \"uid\": \"220\"}]",
|
||||
MediaType.APPLICATION_JSON));
|
||||
igdb.expect(requestTo(containsString("/game_time_to_beats")))
|
||||
.andExpect(content().string(containsString("game_id = (72,233)")))
|
||||
.andRespond(withSuccess("[{\"id\": 9, \"game_id\": 72, \"hastily\": 30600, \"normally\": 45000, \"completely\": 81000, \"count\": 12}]",
|
||||
MediaType.APPLICATION_JSON));
|
||||
|
||||
Optional<Map<Long, IgdbApiService.TimeToBeat>> result = service.getTimeToBeat(List.of(620L, 220L));
|
||||
|
||||
assertThat(result).isPresent();
|
||||
assertThat(result.get()).containsOnlyKeys(620L);
|
||||
assertThat(result.get().get(620L)).isEqualTo(new IgdbApiService.TimeToBeat(510, 750, 1350));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTimeToBeat_ReusesTheToken() {
|
||||
expectToken();
|
||||
igdb.expect(requestTo(containsString("/external_games"))).andRespond(withSuccess("[]", MediaType.APPLICATION_JSON));
|
||||
igdb.expect(requestTo(containsString("/external_games"))).andRespond(withSuccess("[]", MediaType.APPLICATION_JSON));
|
||||
|
||||
service.getTimeToBeat(List.of(620L));
|
||||
service.getTimeToBeat(List.of(220L));
|
||||
|
||||
twitch.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTimeToBeat_AsksIn250erBatches() {
|
||||
expectToken();
|
||||
igdb.expect(requestTo(containsString("/external_games")))
|
||||
.andExpect(content().string(allOf(containsString("\"250\""), not(containsString("\"251\"")), containsString("limit 500"))))
|
||||
.andRespond(withSuccess("[]", MediaType.APPLICATION_JSON));
|
||||
igdb.expect(requestTo(containsString("/external_games")))
|
||||
.andExpect(content().string(allOf(containsString("\"251\""), not(containsString("\"250\"")))))
|
||||
.andRespond(withSuccess("[]", MediaType.APPLICATION_JSON));
|
||||
|
||||
assertThat(service.getTimeToBeat(LongStream.rangeClosed(1, 251).boxed().toList())).contains(Map.of());
|
||||
|
||||
igdb.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTimeToBeat_Error_IsEmptySoNothingIsStoredAsUnknown() {
|
||||
expectToken();
|
||||
igdb.expect(requestTo(containsString("/external_games"))).andRespond(withServerError());
|
||||
|
||||
assertThat(service.getTimeToBeat(List.of(620L))).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTimeToBeat_401_RequestsANewTokenOnTheNextCall() {
|
||||
twitch.expect(times(2), requestTo(containsString("/oauth2/token")))
|
||||
.andRespond(withSuccess(TOKEN, MediaType.APPLICATION_JSON));
|
||||
igdb.expect(requestTo(containsString("/external_games")))
|
||||
.andRespond(withUnauthorizedRequest());
|
||||
igdb.expect(requestTo(containsString("/external_games"))).andRespond(withSuccess("[]", MediaType.APPLICATION_JSON));
|
||||
|
||||
assertThat(service.getTimeToBeat(List.of(620L))).isEmpty();
|
||||
assertThat(service.getTimeToBeat(List.of(620L))).contains(Map.of());
|
||||
|
||||
twitch.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTimeToBeat_OneIgdbGameWithTwoSteamApps_TimesBothApps() {
|
||||
expectToken();
|
||||
igdb.expect(requestTo(containsString("/external_games")))
|
||||
.andRespond(withSuccess("[{\"id\": 1, \"game\": 72, \"uid\": \"620\"}, {\"id\": 2, \"game\": 72, \"uid\": \"221\"}]",
|
||||
MediaType.APPLICATION_JSON));
|
||||
igdb.expect(requestTo(containsString("/game_time_to_beats")))
|
||||
.andRespond(withSuccess("[{\"id\": 9, \"game_id\": 72, \"hastily\": 600, \"normally\": 1200, \"completely\": 1800}]",
|
||||
MediaType.APPLICATION_JSON));
|
||||
|
||||
Optional<Map<Long, IgdbApiService.TimeToBeat>> result = service.getTimeToBeat(List.of(620L, 221L));
|
||||
|
||||
assertThat(result).isPresent();
|
||||
assertThat(result.get()).containsOnlyKeys(620L, 221L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTimeToBeat_OneSteamAppWithTwoIgdbGames_TakesTheTimeMostPlayersSubmitted() {
|
||||
expectToken();
|
||||
igdb.expect(requestTo(containsString("/external_games")))
|
||||
.andRespond(withSuccess("[{\"id\": 1, \"game\": 72, \"uid\": \"620\"}, {\"id\": 2, \"game\": 73, \"uid\": \"620\"}]",
|
||||
MediaType.APPLICATION_JSON));
|
||||
igdb.expect(requestTo(containsString("/game_time_to_beats")))
|
||||
.andExpect(content().string(containsString("count")))
|
||||
.andRespond(withSuccess("[{\"game_id\": 72, \"normally\": 600, \"count\": 2}, {\"game_id\": 73, \"normally\": 1200, \"count\": 40}]",
|
||||
MediaType.APPLICATION_JSON));
|
||||
|
||||
assertThat(service.getTimeToBeat(List.of(620L)).orElseThrow().get(620L).normallyMinutes()).isEqualTo(20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTimeToBeat_Disabled_CallsNothing() {
|
||||
IgdbApiService disabled = create("", "");
|
||||
|
||||
assertThat(disabled.getTimeToBeat(List.of(620L))).isEmpty();
|
||||
|
||||
twitch.verify();
|
||||
igdb.verify();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package core_service.modules.library.api;
|
||||
|
||||
import core_service.modules.library.domain.GameStatus;
|
||||
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.repo.GameTimeToBeatRepository;
|
||||
import core_service.modules.library.repo.SteamGameRepository;
|
||||
import core_service.modules.library.repo.UserGameRepository;
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
@@ -37,6 +39,9 @@ class LibraryControllerSummaryTest extends AbstractIntegrationTest {
|
||||
@Autowired
|
||||
private UserGameRepository userGameRepository;
|
||||
|
||||
@Autowired
|
||||
private GameTimeToBeatRepository gameTimeToBeatRepository;
|
||||
|
||||
@BeforeEach
|
||||
void clean() {
|
||||
userGameRepository.deleteAll();
|
||||
@@ -50,6 +55,7 @@ class LibraryControllerSummaryTest extends AbstractIntegrationTest {
|
||||
.andExpect(jsonPath("$.neverPlayed").value(0))
|
||||
.andExpect(jsonPath("$.byStatus.BACKLOG").value(0))
|
||||
.andExpect(jsonPath("$.byStatus.DROPPED").value(0))
|
||||
.andExpect(jsonPath("$.backlogMinutes").value(0))
|
||||
.andExpect(jsonPath("$.suggestion").value(nullValue()));
|
||||
}
|
||||
|
||||
@@ -61,6 +67,9 @@ class LibraryControllerSummaryTest extends AbstractIntegrationTest {
|
||||
UserGame finished = new UserGame(USER_ID, 620L, 600, Instant.now(), Instant.now());
|
||||
finished.setStatus(GameStatus.FINISHED);
|
||||
userGameRepository.saveAll(List.of(new UserGame(USER_ID, 220L, 0, Instant.now(), Instant.now()), finished));
|
||||
gameTimeToBeatRepository.saveAll(List.of(
|
||||
new GameTimeToBeat(220L, 500, 700, 900, Instant.now()),
|
||||
new GameTimeToBeat(620L, 1, 9999, 1, Instant.now())));
|
||||
|
||||
mockMvc.perform(get("/api/library/summary"))
|
||||
.andExpect(status().isOk())
|
||||
@@ -71,7 +80,9 @@ class LibraryControllerSummaryTest extends AbstractIntegrationTest {
|
||||
.andExpect(jsonPath("$.byStatus.PLAYING").value(0))
|
||||
.andExpect(jsonPath("$.suggestion.appid").value(220))
|
||||
.andExpect(jsonPath("$.suggestion.name").value("Half-Life 2"))
|
||||
.andExpect(jsonPath("$.suggestion.status").value("BACKLOG"));
|
||||
.andExpect(jsonPath("$.suggestion.status").value("BACKLOG"))
|
||||
.andExpect(jsonPath("$.suggestion.timeToBeatMinutes").value(700))
|
||||
.andExpect(jsonPath("$.backlogMinutes").value(700));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package core_service.modules.library.repo;
|
||||
|
||||
import core_service.modules.library.domain.GamePriority;
|
||||
import core_service.modules.library.domain.GameTimeToBeat;
|
||||
import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.StatusCount;
|
||||
import core_service.modules.library.domain.SteamGame;
|
||||
@@ -37,6 +38,9 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
@Autowired
|
||||
private SteamGameRepository steamGameRepository;
|
||||
|
||||
@Autowired
|
||||
private GameTimeToBeatRepository gameTimeToBeatRepository;
|
||||
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
@@ -56,7 +60,7 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
}
|
||||
|
||||
private List<Long> appids(GameStatus status, String query) {
|
||||
return userGameRepository.findOwnedGames(USER_ID, status, UserGameRepository.containsPattern(query), PAGE)
|
||||
return userGameRepository.findOwnedGames(USER_ID, status, UserGameRepository.containsPattern(query), null, PAGE)
|
||||
.map(UserGame::getAppid).getContent();
|
||||
}
|
||||
|
||||
@@ -89,7 +93,7 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
@Test
|
||||
void findOwnedGames_CountsAllMatchesAcrossPages() {
|
||||
var page = userGameRepository.findOwnedGames(USER_ID, null, UserGameRepository.containsPattern("half"),
|
||||
var page = userGameRepository.findOwnedGames(USER_ID, null, UserGameRepository.containsPattern("half"), null,
|
||||
PageRequest.of(0, 1, Sort.by("appid")));
|
||||
|
||||
assertEquals(List.of(220L), page.map(UserGame::getAppid).getContent());
|
||||
@@ -97,7 +101,7 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
}
|
||||
|
||||
private List<Long> sortedAppids(Sort sort) {
|
||||
return userGameRepository.findOwnedGames(USER_ID, null, null, PageRequest.of(0, 20, UserGameRepository.querySort(sort)))
|
||||
return userGameRepository.findOwnedGames(USER_ID, null, null, null, PageRequest.of(0, 20, UserGameRepository.querySort(sort)))
|
||||
.map(UserGame::getAppid).getContent();
|
||||
}
|
||||
|
||||
@@ -141,6 +145,28 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
userGameRepository.flush();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findOwnedGames_MaxMinutes_KeepsOnlyGamesWithAKnownShortTime() {
|
||||
gameTimeToBeatRepository.saveAll(List.of(
|
||||
new GameTimeToBeat(220L, 500, 700, 1000, Instant.now()),
|
||||
new GameTimeToBeat(620L, 300, 480, 900, Instant.now()),
|
||||
new GameTimeToBeat(380L, null, null, null, Instant.now())));
|
||||
|
||||
List<Long> shortGames = userGameRepository.findOwnedGames(USER_ID, null, null, 600, PAGE).map(UserGame::getAppid).getContent();
|
||||
|
||||
assertEquals(List.of(620L), shortGames);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sumBacklogMinutes_AddsTheNormalTimeOfBacklogGamesWithData() {
|
||||
gameTimeToBeatRepository.saveAll(List.of(
|
||||
new GameTimeToBeat(380L, 100, 200, 300, Instant.now()),
|
||||
new GameTimeToBeat(700L, null, 150, null, Instant.now()),
|
||||
new GameTimeToBeat(620L, 1, 9999, 1, Instant.now())));
|
||||
|
||||
assertEquals(350, userGameRepository.sumBacklogMinutes(USER_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
void containsPattern_BlankIsNoFilter() {
|
||||
assertNull(UserGameRepository.containsPattern(" "));
|
||||
|
||||
@@ -96,6 +96,9 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
||||
@Mock
|
||||
private WishlistGameRepository wishlistGameRepository;
|
||||
|
||||
@Mock
|
||||
private TimeToBeatService timeToBeatService;
|
||||
|
||||
@InjectMocks
|
||||
private LibrarySyncWorker worker;
|
||||
|
||||
@@ -351,4 +354,13 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
||||
|
||||
verify(wishlistGameRepository).deleteByUserId(USER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_RefreshesTheTimeToBeatOfTheOwnedGames() {
|
||||
steamReturns(game(0, 0), STEAM_ID);
|
||||
|
||||
worker.runSync(USER_ID, STEAM_ID);
|
||||
|
||||
verify(timeToBeatService).refresh(List.of(220L));
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package core_service.modules.library.service;
|
||||
|
||||
import core_service.modules.igdb_adapter.service.IgdbApiService;
|
||||
import core_service.modules.library.repo.GameTimeToBeatRepository;
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Two users' syncs run in parallel and can miss the same app, e.g. every app right after the first deploy.
|
||||
*/
|
||||
class TimeToBeatServiceConcurrencyTest extends AbstractIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private TimeToBeatService timeToBeatService;
|
||||
|
||||
@Autowired
|
||||
private GameTimeToBeatRepository gameTimeToBeatRepository;
|
||||
|
||||
@Autowired
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
@MockitoBean
|
||||
private IgdbApiService igdbApiService;
|
||||
|
||||
// Before as well: other integration tests leave rows in this shared table, and a fresh row would hide the race.
|
||||
@BeforeEach
|
||||
@AfterEach
|
||||
void clean() {
|
||||
gameTimeToBeatRepository.deleteAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void refresh_TwoParallelSyncsOfTheSameNewApp_BothCommit() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(igdbApiService.getTimeToBeat(any())).thenReturn(Optional.of(Map.of(
|
||||
620L, new IgdbApiService.TimeToBeat(510, 750, 1350))));
|
||||
CountDownLatch firstWrote = new CountDownLatch(1);
|
||||
CountDownLatch secondStarted = new CountDownLatch(1);
|
||||
|
||||
CompletableFuture<Void> first = CompletableFuture.runAsync(() -> new TransactionTemplate(transactionManager)
|
||||
.executeWithoutResult(tx -> {
|
||||
timeToBeatService.refresh(List.of(620L));
|
||||
firstWrote.countDown();
|
||||
await(secondStarted);
|
||||
// Keep the transaction open while the second sync writes the same row.
|
||||
sleep();
|
||||
}));
|
||||
CompletableFuture<Void> second = CompletableFuture.runAsync(() -> {
|
||||
await(firstWrote);
|
||||
secondStarted.countDown();
|
||||
new TransactionTemplate(transactionManager).executeWithoutResult(tx -> timeToBeatService.refresh(List.of(620L)));
|
||||
});
|
||||
|
||||
assertDoesNotThrow(() -> CompletableFuture.allOf(first, second).get(30, TimeUnit.SECONDS));
|
||||
assertEquals(750, gameTimeToBeatRepository.findById(620L).orElseThrow().getNormallyMinutes());
|
||||
}
|
||||
|
||||
private static void await(CountDownLatch latch) {
|
||||
try {
|
||||
if (!latch.await(10, TimeUnit.SECONDS)) throw new IllegalStateException("latch timed out");
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sleep() {
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package core_service.modules.library.service;
|
||||
|
||||
import core_service.modules.igdb_adapter.service.IgdbApiService;
|
||||
import core_service.modules.library.repo.GameTimeToBeatRepository;
|
||||
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.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.when;
|
||||
|
||||
class TimeToBeatServiceTest extends AbstractUnitTest {
|
||||
|
||||
@Mock
|
||||
private IgdbApiService igdbApiService;
|
||||
|
||||
@Mock
|
||||
private GameTimeToBeatRepository gameTimeToBeatRepository;
|
||||
|
||||
@InjectMocks
|
||||
private TimeToBeatService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(service, "ttlDays", 30L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void refresh_AsksOnlyForMissingOrOldAppsAndStoresUnknownOnesToo() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(gameTimeToBeatRepository.findFreshAppids(anyCollection(), any())).thenReturn(Set.of(220L));
|
||||
when(igdbApiService.getTimeToBeat(Set.of(620L, 700L))).thenReturn(Optional.of(Map.of(
|
||||
620L, new IgdbApiService.TimeToBeat(510, 750, 1350))));
|
||||
|
||||
service.refresh(List.of(220L, 620L, 700L));
|
||||
|
||||
verify(gameTimeToBeatRepository).upsert(eq(620L), eq(510), eq(750), eq(1350), any(Instant.class));
|
||||
verify(gameTimeToBeatRepository).upsert(eq(700L), isNull(), isNull(), isNull(), any(Instant.class));
|
||||
verify(gameTimeToBeatRepository, never()).upsert(eq(220L), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refresh_IgdbError_StoresNothing() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(gameTimeToBeatRepository.findFreshAppids(anyCollection(), any())).thenReturn(Set.of());
|
||||
when(igdbApiService.getTimeToBeat(any())).thenReturn(Optional.empty());
|
||||
|
||||
service.refresh(List.of(620L));
|
||||
|
||||
verify(gameTimeToBeatRepository, never()).upsert(any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refresh_Disabled_DoesNotEvenReadTheCache() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(false);
|
||||
|
||||
service.refresh(List.of(620L));
|
||||
|
||||
verify(gameTimeToBeatRepository, never()).findFreshAppids(anyCollection(), any(Instant.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void refresh_AllFresh_AsksIgdbNothing() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(gameTimeToBeatRepository.findFreshAppids(anyCollection(), any())).thenReturn(Set.of(620L));
|
||||
|
||||
service.refresh(List.of(620L));
|
||||
|
||||
verify(igdbApiService, never()).getTimeToBeat(any());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user