feat(library): let auditors merge duplicate games
Build, Test & Push Core Service / quality-check (push) Successful in 1m3s
Build, Test & Push Core Service / docker (push) Successful in 1m26s

An auditor merges a game added by hand into its Steam game or another
game added by hand (V21). Every user's row moves; a user with both rows
keeps one, with the status, rating and notes set by hand. game_alias
sends later adds to the kept key, and admin_audit logs who merged what.
The game search shows games already in ToBePlayed first.
This commit is contained in:
lobo committed 2026-09-28 11:25:45 +02:00
1 parent 3c2f40036d
commit 5a089b2082
14 files changed
+804 -44

No files matched your search

@@ -0,0 +1,313 @@
package core_service.modules.library.api;
import core_service.configuration.security.ClientHeaderFilter;
import core_service.modules.library.domain.GamePriority;
import core_service.modules.library.domain.GameSource;
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.domain.UserGameId;
import core_service.modules.library.repo.GameCorrectionRepository;
import core_service.modules.library.repo.GameMoodsRepository;
import core_service.modules.library.repo.GameTimeToBeatRepository;
import core_service.modules.library.repo.SteamGameRepository;
import core_service.modules.library.repo.UserGameRepository;
import core_service.modules.library.repo.UserStatsRepository;
import core_service.modules.library.domain.UserStats;
import core_service.shared.security.Role;
import core_service.shared.test.AbstractIntegrationTest;
import io.jsonwebtoken.Jwts;
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.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import java.time.Instant;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
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;
/**
* Merges through the real (non-dev) filter chain, so the role in the token decides.
*/
@AutoConfigureMockMvc
@ActiveProfiles(value = "prod", inheritProfiles = false)
class GameMergeFlowTest extends AbstractIntegrationTest {
private static final long AUDITOR_ID = 4711L;
private static final long OTHER_USER = 4712L;
private static final long STEAM = 1_900_100L;
private static final long STEAM_2 = 1_900_200L;
private static final long MANUAL = -1_900_007L;
private static final long MANUAL_2 = -1_900_008L;
@Autowired
private MockMvc mockMvc;
@Autowired
private SteamGameRepository steamGameRepository;
@Autowired
private UserGameRepository userGameRepository;
@Autowired
private GameMoodsRepository gameMoodsRepository;
@Autowired
private GameTimeToBeatRepository gameTimeToBeatRepository;
@Autowired
private GameCorrectionRepository gameCorrectionRepository;
@Autowired
private JdbcTemplate jdbc;
@Autowired
private UserStatsRepository userStatsRepository;
@BeforeEach
void seed() {
jdbc.update("delete from game_alias");
jdbc.update("delete from admin_audit");
gameCorrectionRepository.deleteAll();
userGameRepository.deleteAll();
gameMoodsRepository.deleteAll();
gameTimeToBeatRepository.deleteAll();
for (long appid : List.of(STEAM, STEAM_2, MANUAL, MANUAL_2)) {
steamGameRepository.findById(appid).ifPresent(steamGameRepository::delete);
}
steamGameRepository.save(new SteamGame(STEAM, "Hades", "icon", Instant.now()));
steamGameRepository.save(new SteamGame(STEAM_2, "Hades II", "icon", Instant.now()));
steamGameRepository.save(new SteamGame(MANUAL, "Hades (Switch)", null, Instant.now()));
steamGameRepository.save(new SteamGame(MANUAL_2, "Hades Nintendo", null, Instant.now()));
}
private static String token(Role role) {
return Jwts.builder()
.subject("auditor@example.com")
.claim("userId", String.valueOf(AUDITOR_ID))
.claim("role", role.name())
.expiration(new Date(System.currentTimeMillis() + 60_000))
.signWith(TEST_KEYS.getPrivate(), Jwts.SIG.EdDSA)
.compact();
}
private ResultActions merge(Role role, long appid, long otherAppid) throws Exception {
return mockMvc.perform(post("/api/library/games/" + appid + "/merge")
.header("Authorization", "Bearer " + token(role)).header(ClientHeaderFilter.CLIENT_HEADER, "test")
.contentType(MediaType.APPLICATION_JSON).content("{\"otherAppid\": " + otherAppid + "}"));
}
private UserGame manualRow(long userId, long appid, GameStatus status) {
UserGame game = UserGame.manual(userId, appid, "Switch", status, 600, Instant.now());
return userGameRepository.save(game);
}
private UserGame row(long userId, long appid) {
return userGameRepository.findById(new UserGameId(userId, appid)).orElseThrow();
}
private boolean hasRow(long userId, long appid) {
return userGameRepository.findById(new UserGameId(userId, appid)).isPresent();
}
private Long aliasOf(long appid) {
List<Long> found = jdbc.queryForList("select to_appid from game_alias where from_appid = ?", Long.class, appid);
return found.isEmpty() ? null : found.getFirst();
}
/** Each row as "actor action appid targetAppid detail", oldest first. */
private List<String> audit() {
return jdbc.query("select * from admin_audit order by id", (rs, i) -> rs.getLong("actor_user_id") + " "
+ rs.getString("action") + " " + rs.getLong("appid") + " " + rs.getLong("target_appid") + " "
+ rs.getString("detail"));
}
private int logRows(long userId, long appid) {
Integer count = jdbc.queryForObject("select count(*) from user_game_status_change where user_id = ? and appid = ?",
Integer.class, userId, appid);
return count != null ? count : 0;
}
@Test
void manualIntoSteam_MovesTheRowWithItsHistoryAndDropsTheManualCatalogRows() throws Exception {
UserGame manual = manualRow(AUDITOR_ID, MANUAL, GameStatus.PLAYING);
manual.setNotes("on the couch");
userGameRepository.save(manual);
userGameRepository.save(new UserGame(OTHER_USER, STEAM, 120, Instant.now(), Instant.now()));
gameMoodsRepository.upsert(MANUAL, new String[]{"ACTION"}, new String[]{"Roguelike"}, false, Instant.now());
int historyBefore = logRows(AUDITOR_ID, MANUAL);
merge(Role.AUDITOR, MANUAL, STEAM).andExpect(status().isOk()).andExpect(jsonPath("$.appid").value(STEAM));
UserGame moved = row(AUDITOR_ID, STEAM);
assertThat(moved.getSource()).isEqualTo(GameSource.MANUAL);
assertThat(moved.getPlatform()).isEqualTo("Switch");
assertThat(moved.getStatus()).isEqualTo(GameStatus.PLAYING);
assertThat(moved.getNotes()).isEqualTo("on the couch");
assertThat(moved.getPlaytimeMinutes()).isEqualTo(600);
assertThat(logRows(AUDITOR_ID, STEAM)).isEqualTo(historyBefore);
assertThat(hasRow(AUDITOR_ID, MANUAL)).isFalse();
assertThat(row(OTHER_USER, STEAM).getPlaytimeMinutes()).isEqualTo(120);
assertThat(steamGameRepository.findById(MANUAL)).isEmpty();
assertThat(gameMoodsRepository.findById(MANUAL)).isEmpty();
assertThat(aliasOf(MANUAL)).isEqualTo(STEAM);
assertThat(audit()).containsExactly(AUDITOR_ID + " GAME_MERGE " + MANUAL + " " + STEAM
+ " \"Hades (Switch)\" into \"Hades\": 1 moved, 0 combined");
}
@Test
void userWithBothRows_KeepsTheSteamRowWithTheNewerHandSetFields() throws Exception {
UserGame steam = new UserGame(AUDITOR_ID, STEAM, 300, Instant.now(), Instant.now());
steam.setStatus(GameStatus.PLAYING);
steam.setStatusSetByUser(true);
steam.setRating(4);
steam.setPriority(GamePriority.HIGH);
userGameRepository.save(steam);
// Saved later, so its status change by the user is the newer one.
UserGame manual = manualRow(AUDITOR_ID, MANUAL, GameStatus.FINISHED);
manual.setNotes("beat it on Switch");
userGameRepository.save(manual);
int historyBefore = logRows(AUDITOR_ID, STEAM) + logRows(AUDITOR_ID, MANUAL);
merge(Role.AUDITOR, MANUAL, STEAM).andExpect(status().isOk());
UserGame kept = row(AUDITOR_ID, STEAM);
// Both histories, and no entry for the status the merge took over.
assertThat(logRows(AUDITOR_ID, STEAM)).isEqualTo(historyBefore);
assertThat(kept.getStatus()).isEqualTo(GameStatus.FINISHED);
assertThat(kept.isStatusSetByUser()).isTrue();
assertThat(kept.getNotes()).isEqualTo("beat it on Switch");
assertThat(kept.getRating()).isEqualTo(4);
assertThat(kept.getPriority()).isEqualTo(GamePriority.HIGH);
assertThat(kept.getSource()).isEqualTo(GameSource.STEAM);
assertThat(kept.getPlatform()).isNull();
assertThat(kept.getPlaytimeMinutes()).isEqualTo(300);
assertThat(hasRow(AUDITOR_ID, MANUAL)).isFalse();
assertThat(logRows(AUDITOR_ID, MANUAL)).isZero();
}
@Test
void userWithBothRows_SteamStatusChosenLater_KeepsTheSteamStatus() throws Exception {
manualRow(AUDITOR_ID, MANUAL, GameStatus.FINISHED);
UserGame steam = new UserGame(AUDITOR_ID, STEAM, 300, Instant.now(), Instant.now());
steam.setStatus(GameStatus.DROPPED);
steam.setStatusSetByUser(true);
userGameRepository.save(steam);
merge(Role.AUDITOR, STEAM, MANUAL).andExpect(status().isOk()).andExpect(jsonPath("$.appid").value(STEAM));
assertThat(row(AUDITOR_ID, STEAM).getStatus()).isEqualTo(GameStatus.DROPPED);
assertThat(hasRow(AUDITOR_ID, MANUAL)).isFalse();
}
@Test
void userWithBothRows_SteamCopyGone_KeepsTheCopyAddedByHand() throws Exception {
UserGame steam = new UserGame(AUDITOR_ID, STEAM, 300, Instant.now(), Instant.now());
steam.setRemovedAt(Instant.now());
userGameRepository.save(steam);
manualRow(AUDITOR_ID, MANUAL, GameStatus.PLAYING);
merge(Role.AUDITOR, MANUAL, STEAM).andExpect(status().isOk());
UserGame kept = row(AUDITOR_ID, STEAM);
assertThat(kept.getRemovedAt()).isNull();
assertThat(kept.getSource()).isEqualTo(GameSource.MANUAL);
assertThat(kept.getPlatform()).isEqualTo("Switch");
assertThat(kept.getPlaytimeMinutes()).isEqualTo(600);
assertThat(kept.getStatus()).isEqualTo(GameStatus.PLAYING);
}
@Test
void userWithBothRows_NoHistory_TheStatusSetByHandWins() throws Exception {
userGameRepository.save(new UserGame(AUDITOR_ID, STEAM, 300, Instant.now(), Instant.now()));
manualRow(AUDITOR_ID, MANUAL, GameStatus.FINISHED);
// Rows from before V14 have no status history.
jdbc.update("delete from user_game_status_change where user_id = ?", AUDITOR_ID);
merge(Role.AUDITOR, MANUAL, STEAM).andExpect(status().isOk());
UserGame kept = row(AUDITOR_ID, STEAM);
assertThat(kept.getStatus()).isEqualTo(GameStatus.FINISHED);
assertThat(kept.isStatusSetByUser()).isTrue();
}
@Test
void userWithBothRows_HiddenSteamCopyGone_KeepsTheCopyAddedByHandShown() throws Exception {
UserGame steam = new UserGame(AUDITOR_ID, STEAM, 300, Instant.now(), Instant.now());
steam.setHiddenAt(Instant.now());
steam.setRemovedAt(Instant.now());
userGameRepository.save(steam);
manualRow(AUDITOR_ID, MANUAL, GameStatus.PLAYING);
merge(Role.AUDITOR, MANUAL, STEAM).andExpect(status().isOk());
assertThat(row(AUDITOR_ID, STEAM).getHiddenAt()).isNull();
}
@Test
void chain_StaysFlat() throws Exception {
manualRow(AUDITOR_ID, MANUAL, GameStatus.BACKLOG);
merge(Role.AUDITOR, MANUAL, MANUAL_2).andExpect(status().isOk()).andExpect(jsonPath("$.appid").value(MANUAL_2));
merge(Role.AUDITOR, MANUAL_2, STEAM).andExpect(status().isOk()).andExpect(jsonPath("$.appid").value(STEAM));
assertThat(aliasOf(MANUAL)).isEqualTo(STEAM);
assertThat(aliasOf(MANUAL_2)).isEqualTo(STEAM);
assertThat(hasRow(AUDITOR_ID, STEAM)).isTrue();
// The audit keeps the first step, which the flat alias no longer shows.
assertThat(audit()).hasSize(2).first().asString().contains(MANUAL + " " + MANUAL_2);
}
@Test
void badInput_IsRefused() throws Exception {
merge(Role.AUDITOR, STEAM, STEAM_2).andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("STEAM_GAMES"));
merge(Role.AUDITOR, MANUAL, MANUAL).andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("SAME_GAME"));
merge(Role.AUDITOR, MANUAL, 999_999_999L).andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value("GAME_NOT_FOUND"));
mockMvc.perform(post("/api/library/games/" + MANUAL + "/merge")
.header("Authorization", "Bearer " + token(Role.AUDITOR)).header(ClientHeaderFilter.CLIENT_HEADER, "test")
.contentType(MediaType.APPLICATION_JSON).content("{}"))
.andExpect(status().isBadRequest());
assertThat(steamGameRepository.findById(MANUAL)).isPresent();
assertThat(audit()).isEmpty();
}
@Test
void roles_OnlyAuditorsAndAdminsMerge() throws Exception {
merge(Role.USER, MANUAL, STEAM).andExpect(status().isForbidden());
assertThat(steamGameRepository.findById(MANUAL)).isPresent();
merge(Role.PLATFORM_ADMIN, MANUAL, STEAM).andExpect(status().isOk());
}
@Test
void userWithBothRows_FinishedByHandBefore_KeepsTheFinishInItsYear() throws Exception {
manualRow(AUDITOR_ID, MANUAL, GameStatus.BACKLOG);
jdbc.update("update user_game set status = 'FINISHED' where user_id = ? and appid = ?", AUDITOR_ID, MANUAL);
jdbc.update("update user_game_status_change set changed_at = timestamptz '2025-06-01 12:00:00+00' "
+ "where user_id = ? and appid = ?", AUDITOR_ID, MANUAL);
// The sync's own row: BACKLOG, not set by the user.
userGameRepository.save(new UserGame(AUDITOR_ID, STEAM, 0, Instant.now(), Instant.now()));
ZoneId zone = ZoneId.of("Europe/Berlin");
assertThat(userStatsRepository.finishedPerYear(AUDITOR_ID, zone)).containsExactly(new UserStats.YearCount(2025, 1));
merge(Role.AUDITOR, MANUAL, STEAM).andExpect(status().isOk());
assertThat(row(AUDITOR_ID, STEAM).getStatus()).isEqualTo(GameStatus.FINISHED);
assertThat(userStatsRepository.finishedPerYear(AUDITOR_ID, zone)).containsExactly(new UserStats.YearCount(2025, 1));
assertThat(userStatsRepository.pileMoves(AUDITOR_ID, Instant.now().minus(1, ChronoUnit.DAYS)).done()).isZero();
}
}
@@ -17,6 +17,7 @@ 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.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
@@ -25,6 +26,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.hamcrest.Matchers.contains;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -65,12 +67,17 @@ class ManualGameControllerTest extends AbstractIntegrationTest {
@Autowired
private GameTimeToBeatRepository gameTimeToBeatRepository;
@Autowired
private JdbcTemplate jdbc;
@MockitoBean
private IgdbApiService igdbApiService;
@BeforeEach
void seed() {
jdbc.update("delete from game_alias");
userGameRepository.deleteAll();
jdbc.update("delete from steam_game where appid in (?, ?, ?, ?)", MERGED_INTO, -1_900_400L, -555_001L, -555_002L);
gameTimeToBeatRepository.deleteAll();
when(igdbApiService.isEnabled()).thenReturn(true);
when(igdbApiService.getGame(119388)).thenReturn(Optional.of(ZELDA));
@@ -231,8 +238,7 @@ class ManualGameControllerTest extends AbstractIntegrationTest {
ZELDA.name(), 2023, null, List.of("Switch"), 2_000_000L))));
mockMvc.perform(get("/api/library/game-search").param("q", "zelda"))
.andExpect(jsonPath("$.results[0].appid").value(2_000_000))
.andExpect(jsonPath("$.results[0].inLibrary").value(true));
.andExpect(jsonPath("$.results[?(@.appid == 2000000)].inLibrary", contains(true)));
}
@Test
@@ -359,4 +365,99 @@ class ManualGameControllerTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/library/games/-119388").cookie(login))
.andExpect(status().isOk());
}
private static final long MERGED_INTO = 1_900_300L;
private void mergeZeldaIntoSteam() {
steamGameRepository.save(new SteamGame(MERGED_INTO, "Zelda on Steam", "icon", Instant.now()));
// The merge deletes the catalog row of the merged key.
jdbc.update("delete from steam_game where appid = ?", -119388L);
jdbc.update("insert into game_alias (from_appid, to_appid) values (?, ?)", -119388L, MERGED_INTO);
}
@Test
void add_MergedGame_LandsOnTheGameItWasMergedInto() throws Exception {
mergeZeldaIntoSteam();
add(119388, "Switch").andExpect(status().isCreated()).andExpect(jsonPath("$.appid").value(MERGED_INTO));
assertEquals(GameSource.MANUAL, stored(MERGED_INTO).getSource());
assertEquals("Zelda on Steam", steamGameRepository.findById(MERGED_INTO).orElseThrow().getName());
assertTrue(userGameRepository.findById(new UserGameId(USER_ID, -119388L)).isEmpty());
add(119388, "Switch").andExpect(status().isConflict());
}
@Test
void add_GameMergedIntoAnotherManualGame_KeepsTheTargetName() throws Exception {
steamGameRepository.save(new SteamGame(-1_900_400L, "Zelda TotK", null, Instant.now()));
jdbc.update("insert into game_alias (from_appid, to_appid) values (?, ?)", -119388L, -1_900_400L);
add(119388, "Switch").andExpect(status().isCreated()).andExpect(jsonPath("$.appid").value(-1_900_400L));
assertEquals("Zelda TotK", steamGameRepository.findById(-1_900_400L).orElseThrow().getName());
}
@Test
void add_MergedGameIgdbLaterFindsOnSteam_IsStillInTheLibrary() throws Exception {
steamGameRepository.save(new SteamGame(-1_900_400L, "Zelda TotK", null, Instant.now()));
jdbc.update("insert into game_alias (from_appid, to_appid) values (?, ?)", -119388L, -1_900_400L);
userGameRepository.save(UserGame.manual(USER_ID, -1_900_400L, "Switch", GameStatus.BACKLOG, 0, Instant.now()));
when(igdbApiService.getGame(119388)).thenReturn(Optional.of(new IgdbApiService.Game(119388, ZELDA.name(),
2023, null, List.of("Switch"), 2_000_000L)));
add(119388, "Switch").andExpect(status().isConflict());
assertTrue(userGameRepository.findById(new UserGameId(USER_ID, 2_000_000L)).isEmpty());
}
@Test
void search_MergedGame_IsTheTargetAndInTheLibrary() throws Exception {
mergeZeldaIntoSteam();
userGameRepository.save(new UserGame(USER_ID, MERGED_INTO, 60, Instant.now(), Instant.now()));
when(igdbApiService.searchGames("zelda")).thenReturn(Optional.of(List.of(ZELDA)));
mockMvc.perform(get("/api/library/game-search").param("q", "zelda"))
.andExpect(jsonPath("$.results.length()").value(1))
.andExpect(jsonPath("$.results[0].appid").value(MERGED_INTO))
.andExpect(jsonPath("$.results[0].name").value("Zelda on Steam"))
.andExpect(jsonPath("$.results[0].inLibrary").value(true));
}
@Test
void search_SteamGameWithADuplicateAddedByHand_OffersBothKeys() throws Exception {
steamGameRepository.save(new SteamGame(MERGED_INTO, "Zelda on Steam", "icon", Instant.now()));
steamGameRepository.save(new SteamGame(-1_900_400L, "Zelda TotK", null, Instant.now()));
when(igdbApiService.searchGames("zelda totk")).thenReturn(Optional.of(List.of(
new IgdbApiService.Game(1_900_400, "Zelda TotK", 2023, null, List.of("Switch"), MERGED_INTO))));
mockMvc.perform(get("/api/library/game-search").param("q", "zelda totk"))
.andExpect(jsonPath("$.results[*].appid", contains((int) MERGED_INTO, -1_900_400)))
.andExpect(jsonPath("$.results[*].inCatalog", contains(true, true)));
}
@Test
void search_GamesInToBePlayedComeFirst() throws Exception {
steamGameRepository.save(new SteamGame(-555_001L, "Blue Prince (corrected)", null, Instant.now()));
steamGameRepository.save(new SteamGame(-555_002L, "Princess Farmer", null, Instant.now()));
IgdbApiService.Game persia = new IgdbApiService.Game(777, "Prince of Persia", 2008, null, List.of("PS3"), null);
IgdbApiService.Game bluePrince = new IgdbApiService.Game(555_001, "Blue Prince", 2025,
"https://images.igdb.com/igdb/image/upload/t_screenshot_med/bp.jpg", List.of("PS5"), null);
IgdbApiService.Game other = new IgdbApiService.Game(778, "Prince of Persia 2", 2009, null, List.of("PS3"), null);
when(igdbApiService.searchGames("prince")).thenReturn(Optional.of(List.of(persia, bluePrince, other)));
mockMvc.perform(get("/api/library/game-search").param("q", "prince"))
.andExpect(jsonPath("$.results.length()").value(4))
.andExpect(jsonPath("$.results[0].appid").value(-555_001))
.andExpect(jsonPath("$.results[0].name").value("Blue Prince (corrected)"))
.andExpect(jsonPath("$.results[0].imageUrl").value(bluePrince.imageUrl()))
.andExpect(jsonPath("$.results[0].platforms[0]").value("PS5"))
.andExpect(jsonPath("$.results[0].inCatalog").value(true))
.andExpect(jsonPath("$.results[1].appid").value(-555_002))
.andExpect(jsonPath("$.results[1].inCatalog").value(true))
.andExpect(jsonPath("$.results[1].igdbId").value(555_002))
.andExpect(jsonPath("$.results[1].platforms").isEmpty())
.andExpect(jsonPath("$.results[2].appid").value(-777))
.andExpect(jsonPath("$.results[2].inCatalog").value(false))
.andExpect(jsonPath("$.results[3].appid").value(-778));
}
}
@@ -4,6 +4,7 @@ import core_service.modules.igdb_adapter.service.IgdbApiService;
import core_service.modules.library.domain.GameStatus;
import core_service.modules.library.domain.SteamGame;
import core_service.modules.library.domain.UserGameId;
import core_service.modules.library.repo.GameMergeRepository;
import core_service.modules.library.repo.SteamGameRepository;
import core_service.modules.library.repo.UserGameRepository;
import core_service.shared.test.AbstractUnitTest;
@@ -40,6 +41,9 @@ class ManualGameServiceTest extends AbstractUnitTest {
@Mock
private SteamGameRepository steamGameRepository;
@Mock
private GameMergeRepository mergeRepository;
@Mock
private TimeToBeatService timeToBeatService;
@@ -53,7 +57,7 @@ class ManualGameServiceTest extends AbstractUnitTest {
@BeforeEach
void setUp() {
service = new ManualGameService(igdbApiService, userGameRepository, steamGameRepository, timeToBeatService,
service = new ManualGameService(igdbApiService, userGameRepository, steamGameRepository, mergeRepository, timeToBeatService,
moodService, transactionManager);
}