refactor(library): own IgdbGame record, one owned rule, layer rules

Stored import candidates keep their JSON shape, so READY imports from before still read.
This commit is contained in:
lobo committed 2026-09-30 09:37:16 +02:00
1 parent 1abc03d2c1
commit c1472c7d59
9 files changed
+126 -51

No files matched your search

@@ -1,6 +1,7 @@
package core_service.architecture;
import core_service.shared.test.AbstractArchitectureTest;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
@@ -59,6 +60,33 @@ class ArchitectureTest extends AbstractArchitectureTest {
.should().dependOnClassesThat().resideInAPackage("..api..")
.because("Persistence layer must be independent of the API layer.");
@ArchTest
static final ArchRule domain_should_not_depend_on_other_layers = noClasses()
.that().resideInAPackage("core_service.modules..domain..")
.should().dependOnClassesThat().resideInAnyPackage("..api..", "..service..", "..repo..",
"core_service.modules.steam_adapter..", "core_service.modules.igdb_adapter..",
"core_service.modules.speedrun_adapter..")
.because("Entities and stored shapes must not change when a controller, service or external API changes.");
// The listed services build their module's response DTOs; the list only gets shorter.
@ArchTest
static final ArchRule services_should_not_depend_on_apis = noClasses()
.that().resideInAPackage("core_service.modules..service..")
.and().doNotHaveFullyQualifiedName("core_service.modules.library.service.BacklogService")
.and().doNotHaveFullyQualifiedName("core_service.modules.library.service.GameDetailService")
.and().doNotHaveFullyQualifiedName("core_service.modules.library.service.GameImportService")
.and().doNotHaveFullyQualifiedName("core_service.modules.library.service.LibraryQueryService")
.and().doNotHaveFullyQualifiedName("core_service.modules.library.service.ManualGameService")
.and().doNotHaveFullyQualifiedName("core_service.modules.library.service.SteamProfileService")
.and().doNotHaveFullyQualifiedName("core_service.modules.library.service.UserStatsService")
.and().doNotHaveFullyQualifiedName("core_service.modules.stats.service.PublicStatsService")
.should().dependOnClassesThat(JavaClass.Predicates.resideInAPackage("..api..")
.and(DescribedPredicate.not(JavaClass.Predicates.resideInAnyPackage(
"core_service.modules.steam_adapter.api.dto.response..",
"core_service.modules.igdb_adapter.api.dto.response..",
"core_service.modules.speedrun_adapter.api.dto.response.."))))
.because("Services must not depend on the HTTP layer; the adapters' response DTOs are the external APIs' shapes.");
@ArchTest
static final ArchRule repos_should_be_interfaces_named_repository = classes()
.that().resideInAPackage("..repo..")
@@ -146,6 +146,23 @@ class GameImportControllerTest extends AbstractIntegrationTest {
USER_ID, appid);
}
@Test
void get_ReadyImportStoredBeforeIgdbGame_ReadsItsCandidates() throws Exception {
long id = insertImport(USER_ID, "READY", "total = 1");
// The JSON the import wrote with IgdbApiService.Game, before the own record IgdbGame.
jdbc.update("insert into game_import_row (import_id, line_no, title, matched_at, exact, candidates) "
+ "values (?, 1, 'Zelda', now(), true, cast(? as jsonb))", id,
"[{\"igdbId\":119388,\"name\":\"Zelda: Tears of the Kingdom\",\"year\":2023,"
+ "\"imageUrl\":null,\"platforms\":[\"Switch\"],\"steamAppid\":null}]");
mockMvc.perform(get("/api/library/imports/" + id))
.andExpect(status().isOk())
.andExpect(jsonPath("$.rows[0].candidates[0].igdbId").value(119388))
.andExpect(jsonPath("$.rows[0].candidates[0].name").value("Zelda: Tears of the Kingdom"))
.andExpect(jsonPath("$.rows[0].candidates[0].year").value(2023))
.andExpect(jsonPath("$.rows[0].candidates[0].platforms[0]").value("Switch"));
}
@Test
void create_MatchesInTheBackground_SameNameFirstAndMarksGamesInTheLibrary() throws Exception {
jdbc.update("insert into steam_game (appid, name) values (1245620, 'ELDEN RING') on conflict do nothing");
@@ -1,6 +1,6 @@
package core_service.modules.library.service;
import core_service.modules.igdb_adapter.service.IgdbApiService;
import core_service.modules.library.domain.IgdbGame;
import core_service.modules.library.domain.GameImport;
import core_service.modules.library.repo.GameImportRepository;
import org.junit.jupiter.api.Test;
@@ -20,8 +20,8 @@ import static org.mockito.Mockito.when;
class GameImportWorkerTest {
private static IgdbApiService.Game game(long igdbId, String name, String... platforms) {
return new IgdbApiService.Game(igdbId, name, null, null, List.of(platforms), null);
private static IgdbGame game(long igdbId, String name, String... platforms) {
return new IgdbGame(igdbId, name, null, null, List.of(platforms), null);
}
private static GameImport.Line line(String title, String platform) {
@@ -37,12 +37,12 @@ class GameImportWorkerTest {
@Test
void rank_SameNameFirst_ThenTheLinesPlatform_ThenIgdbsOrder_AtMostFive() {
List<IgdbApiService.Game> found = List.of(game(1, "Doom Eternal", "PC"), game(2, "DOOM", "PC"),
List<IgdbGame> found = List.of(game(1, "Doom Eternal", "PC"), game(2, "DOOM", "PC"),
game(3, "Doom", "Switch"), game(4, "Doom 3"), game(5, "Doom 64"), game(6, "Doom II"));
assertThat(GameImportWorker.rank(line("Doom", "switch"), found)).extracting(IgdbApiService.Game::igdbId)
assertThat(GameImportWorker.rank(line("Doom", "switch"), found)).extracting(IgdbGame::igdbId)
.containsExactly(3L, 2L, 1L, 4L, 5L);
assertThat(GameImportWorker.rank(line("Doom", null), found)).extracting(IgdbApiService.Game::igdbId)
assertThat(GameImportWorker.rank(line("Doom", null), found)).extracting(IgdbGame::igdbId)
.startsWith(2L, 3L);
assertThat(GameImportWorker.rank(line("x", null), IntStream.range(0, 9)
.mapToObj(i -> game(i, "Game " + i)).toList())).hasSize(GameImportWorker.MAX_CANDIDATES);
@@ -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(IgdbApiService.class), mock(TimeToBeatService.class), mock(MoodService.class), 5);
mock(TimeToBeatService.class), mock(MoodService.class), 5);
try {
worker.wakeMatching();
verify(repository, timeout(2000)).findNext(GameImportWorker.MATCHING_QUEUE);