Public Access
fix(library): merge manual game into its Steam row on sync
Steam sync added a second row for a game the user added by hand (#9). The manual row moves to the Steam appid and keeps its data, such as status, notes, sets and PSN links. Manual rows on other platforms stay and join the Steam game's set.
This commit is contained in:
1 parent
59d6b3394d
commit
1de8acef2e
8 files changed
+720
-9
No files matched your search
@@ -20,7 +20,8 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Merges one library key into another (V21). Plain SQL: a merge rewrites the key of rows in many tables at once.
|
||||
* Merges one library key into another (V21), and moves one user's row to another key (Steam takeover).
|
||||
* Plain SQL: a merge rewrites the key of rows in many tables at once.
|
||||
*/
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
@@ -39,6 +40,10 @@ public class GameMergeRepository {
|
||||
+ " and not exists (select 1 from reward_ledger o where o.user_id = l.user_id and o.kind = l.kind"
|
||||
+ " and o.ref = cast(:to as text))";
|
||||
|
||||
// A row added by hand for PC, Steam, Mac or Linux (or with no platform) is the Steam copy.
|
||||
private static final String TAKEOVER_PLATFORM =
|
||||
"lower(btrim(coalesce(ug.platform, ''))) in ('', 'pc', 'steam', 'mac', 'linux')";
|
||||
|
||||
// What the snapshot saves besides the catalog tables: the link sets of both keys and the pairs and requests of from.
|
||||
private static final String LINK_SET_ROWS = "select * from game_link where set_id in "
|
||||
+ "(select set_id from game_link where appid in (:from, :to))";
|
||||
@@ -366,4 +371,75 @@ public class GameMergeRepository {
|
||||
return jdbc.update("delete from game_merge_undo where created_at < :cutoff",
|
||||
new MapSqlParameterSource("cutoff", Timestamp.from(cutoff)));
|
||||
}
|
||||
|
||||
public boolean hasSteamTakeoverCandidates(long userId) {
|
||||
return Boolean.TRUE.equals(jdbc.queryForObject("""
|
||||
select exists (select 1 from user_game ug
|
||||
where ug.user_id = :user and ug.appid < 0 and ug.source = 'MANUAL' and %s)
|
||||
""".formatted(TAKEOVER_PLATFORM), new MapSqlParameterSource("user", userId), Boolean.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the keys of {@code appids} the user has no row for and whose stored edition has no IGDB game
|
||||
*/
|
||||
public List<Long> findUnmappedNewlyOwned(long userId, Collection<Long> appids) {
|
||||
if (appids.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return jdbc.queryForList("""
|
||||
select e.appid from game_edition e
|
||||
where e.appid in (:appids) and e.igdb_id is null
|
||||
and not exists (select 1 from user_game t where t.user_id = :user and t.appid = e.appid)
|
||||
""", new MapSqlParameterSource("user", userId).addValue("appids", appids), Long.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a manual key of the user, by the Steam appid that takes its row; only a Steam appid the user has no row for
|
||||
*/
|
||||
public Map<Long, Long> findSteamTakeovers(long userId, Collection<Long> steamAppids) {
|
||||
if (steamAppids.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<Long, Long> takeovers = new LinkedHashMap<>();
|
||||
jdbc.query("""
|
||||
select distinct on (ug.appid) ug.appid as from_appid, e.appid as to_appid
|
||||
from user_game ug
|
||||
join game_edition e on e.igdb_id = -ug.appid and e.appid in (:appids)
|
||||
where ug.user_id = :user and ug.appid < 0 and ug.source = 'MANUAL' and %s
|
||||
and not exists (select 1 from user_game t where t.user_id = ug.user_id and t.appid = e.appid)
|
||||
order by ug.appid, e.appid
|
||||
""".formatted(TAKEOVER_PLATFORM),
|
||||
new MapSqlParameterSource("user", userId).addValue("appids", steamAppids),
|
||||
rs -> { takeovers.put(rs.getLong("from_appid"), rs.getLong("to_appid")); });
|
||||
return takeovers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves one user's row to another key. The cascading FKs move status log, backlog order, questions, splits and
|
||||
* set backups. Needs the steam_game row of {@code to} flushed.
|
||||
*
|
||||
* @return false when the user has no row on {@code from}, or already has one on {@code to}
|
||||
*/
|
||||
public boolean rekeyUserGame(long userId, long from, long to) {
|
||||
MapSqlParameterSource params = new MapSqlParameterSource("user", userId).addValue("from", from).addValue("to", to);
|
||||
if (jdbc.update("""
|
||||
update user_game set appid = :to where user_id = :user and appid = :from
|
||||
and not exists (select 1 from user_game t where t.user_id = :user and t.appid = :to)
|
||||
""", params) == 0) {
|
||||
return false;
|
||||
}
|
||||
// A day both keys have adds up on the target.
|
||||
jdbc.update("""
|
||||
insert into user_game_playtime_day (user_id, day, appid, minutes)
|
||||
select user_id, day, :to, minutes from user_game_playtime_day where user_id = :user and appid = :from
|
||||
on conflict (user_id, day, appid) do update
|
||||
set minutes = user_game_playtime_day.minutes + excluded.minutes
|
||||
""", params);
|
||||
jdbc.update("delete from user_game_playtime_day where user_id = :user and appid = :from", params);
|
||||
jdbc.update("update user_psn_title set appid = :to where user_id = :user and appid = :from", params);
|
||||
jdbc.update("update user_psn_trophy_set set appid = :to where user_id = :user and appid = :from", params);
|
||||
jdbc.update("update reward_ledger l set ref = cast(:to as text) where l.user_id = :user and " + LEDGER_MOVED,
|
||||
params);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -45,10 +45,25 @@ public class GameEditionService {
|
||||
this.transaction = new TransactionTemplate(transactionManager);
|
||||
}
|
||||
|
||||
public void refresh(Collection<Long> appids) {
|
||||
if (!igdbApiService.isEnabled() || appids.isEmpty()) return;
|
||||
Set<Long> stale = stale(appids);
|
||||
if (stale.isEmpty()) return;
|
||||
/**
|
||||
* @return the keys stored now; linkSameGames ran for them, but a key without a steam_game row is not linked yet
|
||||
*/
|
||||
public Set<Long> refresh(Collection<Long> appids) {
|
||||
if (!igdbApiService.isEnabled() || appids.isEmpty()) return Set.of();
|
||||
return store(stale(appids));
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #refresh}, but for keys whose stored edition has no IGDB game: IGDB may know them by now, and a
|
||||
* fresh null would block a takeover until the TTL ends.
|
||||
*/
|
||||
public Set<Long> refreshAgain(Collection<Long> appids) {
|
||||
if (!igdbApiService.isEnabled() || appids.isEmpty()) return Set.of();
|
||||
return store(Set.copyOf(appids));
|
||||
}
|
||||
|
||||
private Set<Long> store(Set<Long> stale) {
|
||||
if (stale.isEmpty()) return Set.of();
|
||||
|
||||
IgdbLookup.Answer<IgdbApiService.Edition> answer = IgdbLookup.byAppid(stale, igdbApiService::getEditions,
|
||||
igdbApiService::getEditionsByGameIds);
|
||||
@@ -63,6 +78,7 @@ public class GameEditionService {
|
||||
edition == null ? null : edition.sameGameId(), now);
|
||||
}
|
||||
linkSameGames(answer.answered());
|
||||
return answer.answered();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@ import core_service.modules.library.domain.UserGame;
|
||||
import core_service.modules.library.domain.UserSteamSync;
|
||||
import core_service.modules.library.domain.WishlistGame;
|
||||
import core_service.modules.library.repo.GameCorrectionRepository;
|
||||
import core_service.modules.library.repo.GameMergeRepository;
|
||||
import core_service.modules.library.repo.PlaytimeDayRepository;
|
||||
import core_service.modules.library.repo.SteamGameRepository;
|
||||
import core_service.modules.library.repo.SyncChangeRepository;
|
||||
@@ -59,6 +60,7 @@ public class LibrarySyncWorker {
|
||||
private final SteamApiService steamApiService;
|
||||
private final SteamGameRepository steamGameRepository;
|
||||
private final GameCorrectionRepository gameCorrectionRepository;
|
||||
private final GameMergeRepository gameMergeRepository;
|
||||
private final UserGameRepository userGameRepository;
|
||||
private final UserSteamSyncRepository userSteamSyncRepository;
|
||||
private final SyncChangeRepository syncChangeRepository;
|
||||
@@ -137,6 +139,16 @@ public class LibrarySyncWorker {
|
||||
}
|
||||
// Before the empty check: an account without games can still have a public wishlist.
|
||||
Optional<Wishlist> wishlist = hasSteam ? fetchWishlist(steamId, games) : Optional.empty();
|
||||
List<Long> ownedAppids = games.stream().map(g -> g.appid().longValue()).toList();
|
||||
// A game added by hand can be a Steam game this sync owns for the first time. Its mapping (game_edition)
|
||||
// must be known before the save; the refresh after the save then skips these keys.
|
||||
Set<Long> learned = new HashSet<>();
|
||||
if (!games.isEmpty() && gameMergeRepository.hasSteamTakeoverCandidates(userId)) {
|
||||
// First: a fresh edition without an IGDB game (stored before IGDB knew the app) is asked again.
|
||||
learned.addAll(gameEditionService.refreshAgain(
|
||||
gameMergeRepository.findUnmappedNewlyOwned(userId, ownedAppids)));
|
||||
learned.addAll(gameEditionService.refresh(ownedAppids));
|
||||
}
|
||||
PsnSyncStep.Prepared psn = psnSyncStep.run(userId);
|
||||
boolean saved = transaction.execute(tx -> {
|
||||
List<SyncChange> wishlistAdds = wishlist.map(found -> saveWishlist(userId, found)).orElseGet(() -> {
|
||||
@@ -174,8 +186,12 @@ public class LibrarySyncWorker {
|
||||
if (!saved) {
|
||||
return;
|
||||
}
|
||||
// Keys stored before their steam_game row existed were not linked by that refresh.
|
||||
if (!learned.isEmpty()) {
|
||||
gameEditionService.linkSameGames(learned);
|
||||
}
|
||||
// Also the games added by hand: here a failed refresh from the add is asked again.
|
||||
List<Long> igdbAppids = new ArrayList<>(games.stream().map(g -> g.appid().longValue()).toList());
|
||||
List<Long> igdbAppids = new ArrayList<>(ownedAppids);
|
||||
igdbAppids.addAll(userGameRepository.findActiveManualAppids(userId));
|
||||
igdbAppids.removeAll(new HashSet<>(userGameRepository.findHiddenAppids(userId)));
|
||||
timeToBeatService.refresh(igdbAppids);
|
||||
@@ -214,6 +230,10 @@ public class LibrarySyncWorker {
|
||||
Set<Long> correctedNames = gameCorrectionRepository.findCorrectedAppids(CorrectionField.NAME, appids);
|
||||
Map<Long, SteamGame> knownGames = steamGameRepository.findAllById(appids).stream()
|
||||
.collect(Collectors.toMap(SteamGame::getAppid, game -> game));
|
||||
for (SteamOwnedGamesResponse.Game g : games) {
|
||||
upsertGame(g, knownGames.get(g.appid().longValue()), now, correctedNames);
|
||||
}
|
||||
takeOverManualGames(userId, appids);
|
||||
Map<Long, UserGame> userGames = userGameRepository.findByUserId(userId).stream()
|
||||
.collect(Collectors.toMap(UserGame::getAppid, game -> game));
|
||||
List<SyncChange> changes = new ArrayList<>();
|
||||
@@ -226,7 +246,6 @@ public class LibrarySyncWorker {
|
||||
List<Long> played = new ArrayList<>();
|
||||
for (SteamOwnedGamesResponse.Game g : games) {
|
||||
Long appid = g.appid().longValue();
|
||||
upsertGame(g, knownGames.get(appid), now, correctedNames);
|
||||
if (upsertUserGame(userId, g, userGames.get(appid), now, playedOn, changes)) {
|
||||
played.add(appid);
|
||||
}
|
||||
@@ -243,6 +262,21 @@ public class LibrarySyncWorker {
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* A game the user added by hand for a PC platform becomes the Steam row of its appid: the row moves to the
|
||||
* Steam key here, and upsertUserGame takes it over. Runs before any UserGame is loaded in this transaction,
|
||||
* so the persistence context holds no row under the old key.
|
||||
*/
|
||||
private void takeOverManualGames(Long userId, List<Long> appids) {
|
||||
// user_game refers to steam_game; the JDBC update below does not flush JPA.
|
||||
steamGameRepository.flush();
|
||||
gameMergeRepository.findSteamTakeovers(userId, appids).forEach((from, to) -> {
|
||||
if (gameMergeRepository.rekeyUserGame(userId, from, to)) {
|
||||
log.info("Sync user={} moved the game added by hand appid={} to its Steam appid={}", userId, from, to);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private record Wishlist(List<SteamApiService.WishlistItem> wanted, Map<Long, SteamApiService.StoreItem> store) {}
|
||||
|
||||
private Optional<Wishlist> fetchWishlist(String steamId, List<SteamOwnedGamesResponse.Game> ownedGames) {
|
||||
@@ -304,6 +338,7 @@ public class LibrarySyncWorker {
|
||||
Long appid = g.appid().longValue();
|
||||
int playtime = g.playtimeForever() != null ? g.playtimeForever() : 0;
|
||||
// Hours typed by hand are no Steam playtime, so the first Steam value of a game added by hand is a baseline.
|
||||
// Also a row moved here from minus its IGDB id (takeOverManualGames).
|
||||
boolean takesOverManualGame = existing != null && existing.getSource() != GameSource.STEAM;
|
||||
int previousPlaytime = existing != null ? existing.getPlaytimeMinutes() : 0;
|
||||
int playtime2Weeks = g.playtime2Weeks() != null ? g.playtime2Weeks() : 0;
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
package core_service.modules.library.api;
|
||||
|
||||
import core_service.modules.igdb_adapter.service.IgdbApiService;
|
||||
import core_service.modules.library.domain.SyncTrigger;
|
||||
import core_service.modules.library.service.LibrarySyncWorker;
|
||||
import core_service.modules.rewards.service.RewardService;
|
||||
import core_service.modules.steam_adapter.api.dto.response.SteamOwnedGamesResponse;
|
||||
import core_service.shared.test.AbstractMockedAdaptersTest;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyCollection;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* A game added by hand for PC becomes the Steam row when the sync sees the user own it on Steam. Real syncs with a
|
||||
* mocked Steam; the first sync of an account is a baseline, so every test syncs once before the game is owned.
|
||||
*/
|
||||
class SteamTakeoverFlowTest extends AbstractMockedAdaptersTest {
|
||||
|
||||
private static final long USER = 1L;
|
||||
private static final long OTHER_USER = 2L;
|
||||
private static final String STEAM_ID = "76561198000000001";
|
||||
private static final long STEAM = 920_001L;
|
||||
private static final long STEAM_2 = 920_002L;
|
||||
private static final long FILLER = 920_003L;
|
||||
private static final long IGDB = 920_101L;
|
||||
private static final long MANUAL = -IGDB;
|
||||
private static final long OTHER_IGDB = 920_102L;
|
||||
|
||||
@Autowired
|
||||
private LibrarySyncWorker worker;
|
||||
|
||||
@Autowired
|
||||
private RewardService rewardService;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
@AfterEach
|
||||
void clean() {
|
||||
for (String table : List.of("user_game_playtime_day", "user_game_split", "user_game_set_backup",
|
||||
"user_game_question", "backlog_order", "user_game_status_change", "user_game", "reward_ledger",
|
||||
"sync_result", "user_steam_sync")) {
|
||||
jdbc.update("delete from " + table + " where user_id in (?, ?)", USER, OTHER_USER);
|
||||
}
|
||||
jdbc.update("delete from game_link_pair where appid_a between -920200 and 920200 or appid_b between -920200 and 920200");
|
||||
jdbc.update("delete from game_link where appid between -920200 and 920200");
|
||||
jdbc.update("delete from game_edition where appid between -920200 and 920200");
|
||||
jdbc.update("delete from steam_game where appid between -920200 and 920200");
|
||||
}
|
||||
|
||||
private void manual(long userId, String platform) {
|
||||
jdbc.update("insert into steam_game (appid, name) values (?, ?) on conflict do nothing", MANUAL, "By hand");
|
||||
jdbc.update("""
|
||||
insert into user_game (user_id, appid, source, platform, status, status_set_by_user, rating, notes,
|
||||
priority, playtime_minutes)
|
||||
values (?, ?, 'MANUAL', ?, 'FINISHED', true, 4, 'my notes', 'HIGH', 120)
|
||||
""", userId, MANUAL, platform);
|
||||
jdbc.update("insert into reward_ledger (user_id, kind, ref, points, event_at) values (?, 'FINISHED_MANUAL', ?, 10, now())",
|
||||
userId, String.valueOf(MANUAL));
|
||||
}
|
||||
|
||||
private void edition(long appid, long igdbId, long sameGameId) {
|
||||
jdbc.update("insert into game_edition (appid, igdb_id, same_game_id, checked_at) values (?, ?, ?, now()) "
|
||||
+ "on conflict (appid) do update set igdb_id = excluded.igdb_id, same_game_id = excluded.same_game_id",
|
||||
appid, igdbId, sameGameId);
|
||||
}
|
||||
|
||||
private void mapped() {
|
||||
edition(STEAM, IGDB, IGDB);
|
||||
edition(MANUAL, IGDB, IGDB);
|
||||
}
|
||||
|
||||
private static SteamOwnedGamesResponse.Game owned(long appid, int playtime) {
|
||||
return new SteamOwnedGamesResponse.Game((int) appid, "Game " + appid, playtime, "icon", true, playtime, 1_700_000_000L);
|
||||
}
|
||||
|
||||
private void sync(SteamOwnedGamesResponse.Game... games) {
|
||||
when(steamApiService.getWishlist(anyString())).thenReturn(Optional.empty());
|
||||
when(steamApiService.getOwnedGames(STEAM_ID)).thenReturn(List.of(games));
|
||||
worker.runSyncNow(USER, STEAM_ID, SyncTrigger.AUTO);
|
||||
}
|
||||
|
||||
/** The baseline sync, then the one that sees the game for the first time. */
|
||||
private void syncUntilOwned(SteamOwnedGamesResponse.Game... games) {
|
||||
sync(owned(FILLER, 0));
|
||||
sync(games);
|
||||
}
|
||||
|
||||
private List<Long> keys(long userId) {
|
||||
return jdbc.queryForList("select appid from user_game where user_id = ? and appid between -920200 and 920200 and appid <> 920003 "
|
||||
+ "order by appid", Long.class, userId);
|
||||
}
|
||||
|
||||
private Map<String, Object> row(long userId, long appid) {
|
||||
return jdbc.queryForMap("select * from user_game where user_id = ? and appid = ?", userId, appid);
|
||||
}
|
||||
|
||||
private int count(String sql, Object... args) {
|
||||
return jdbc.queryForObject(sql, Integer.class, args);
|
||||
}
|
||||
|
||||
private void assertTookOver() {
|
||||
assertThat(keys(USER)).containsExactly(STEAM);
|
||||
Map<String, Object> steam = row(USER, STEAM);
|
||||
assertThat(steam).containsEntry("source", "STEAM").containsEntry("playtime_minutes", 300)
|
||||
.containsEntry("status", "FINISHED").containsEntry("rating", 4).containsEntry("notes", "my notes")
|
||||
.containsEntry("priority", "HIGH");
|
||||
assertThat(steam.get("platform")).isNull();
|
||||
assertThat(steam.get("last_played_at")).isNotNull();
|
||||
assertThat(count("select count(*) from user_game_status_change where user_id = ? and appid = ?", USER, STEAM))
|
||||
.isPositive();
|
||||
assertThat(count("select count(*) from user_game_status_change where user_id = ? and appid = ?", USER, MANUAL))
|
||||
.isZero();
|
||||
assertThat(count("select count(*) from user_game_playtime_day where user_id = ? and appid = ?", USER, STEAM))
|
||||
.isZero();
|
||||
assertThat(jdbc.queryForList("select ref from reward_ledger where user_id = ? and kind = 'FINISHED_MANUAL'",
|
||||
String.class, USER)).containsExactly(String.valueOf(STEAM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_GameAddedByHandForPcIsOwnedOnSteam_BecomesTheSteamRow() {
|
||||
manual(USER, "PC");
|
||||
mapped();
|
||||
jdbc.update("insert into user_game_question (user_id, appid, question, playtime_minutes) values (?, ?, 'RATE', 120)",
|
||||
USER, MANUAL);
|
||||
|
||||
syncUntilOwned(owned(FILLER, 0), owned(STEAM, 300));
|
||||
|
||||
assertTookOver();
|
||||
assertThat(count("select count(*) from user_game_question where user_id = ? and appid = ?", USER, STEAM)).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_BacklogGameAddedByHand_KeepsItsBacklogPosition() {
|
||||
manual(USER, "PC");
|
||||
jdbc.update("update user_game set status = 'BACKLOG', status_set_by_user = false where user_id = ?", USER);
|
||||
mapped();
|
||||
|
||||
syncUntilOwned(owned(FILLER, 0), owned(STEAM, 300));
|
||||
|
||||
assertThat(keys(USER)).containsExactly(STEAM);
|
||||
assertThat(count("select count(*) from backlog_order where user_id = ? and appid = ?", USER, MANUAL)).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_MappingLearnedInThisSync_BecomesTheSteamRowAndLinksTheSwitchRow() {
|
||||
manual(USER, "PC");
|
||||
long switchKey = -OTHER_IGDB;
|
||||
jdbc.update("insert into steam_game (appid, name) values (?, 'Switch copy')", switchKey);
|
||||
jdbc.update("insert into user_game (user_id, appid, source, platform, status) values (?, ?, 'MANUAL', 'Switch', 'BACKLOG')",
|
||||
USER, switchKey);
|
||||
edition(switchKey, OTHER_IGDB, IGDB);
|
||||
edition(MANUAL, IGDB, IGDB);
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(igdbApiService.getEditions(anyCollection()))
|
||||
.thenReturn(Optional.of(Map.of(STEAM, new IgdbApiService.Edition(IGDB, IGDB))));
|
||||
|
||||
syncUntilOwned(owned(FILLER, 0), owned(STEAM, 300));
|
||||
|
||||
assertThat(keys(USER)).containsExactlyInAnyOrder(STEAM, switchKey);
|
||||
assertThat(count("select count(distinct set_id) from game_link where appid in (?, ?, ?)", STEAM, MANUAL, switchKey))
|
||||
.isOne();
|
||||
assertThat(count("select count(*) from game_link where appid in (?, ?)", STEAM, switchKey)).isEqualTo(2);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@NullAndEmptySource
|
||||
@ValueSource(strings = {"Steam", "pc", "Mac", "Linux"})
|
||||
void runSync_PlatformOfTheSteamCopy_BecomesTheSteamRow(String platform) {
|
||||
manual(USER, platform);
|
||||
mapped();
|
||||
|
||||
syncUntilOwned(owned(FILLER, 0), owned(STEAM, 300));
|
||||
|
||||
assertTookOver();
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_GameAddedByHandForSwitch_AddsTheSteamRowInOneSet() {
|
||||
manual(USER, "Switch");
|
||||
edition(MANUAL, IGDB, IGDB);
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(igdbApiService.getEditions(anyCollection()))
|
||||
.thenReturn(Optional.of(Map.of(STEAM, new IgdbApiService.Edition(IGDB, IGDB))));
|
||||
|
||||
syncUntilOwned(owned(FILLER, 0), owned(STEAM, 300));
|
||||
|
||||
assertThat(keys(USER)).containsExactlyInAnyOrder(MANUAL, STEAM);
|
||||
assertThat(row(USER, MANUAL)).containsEntry("source", "MANUAL");
|
||||
assertThat(count("select count(distinct set_id) from game_link where appid in (?, ?)", STEAM, MANUAL)).isOne();
|
||||
assertThat(count("select count(*) from counted_user_game where user_id = ? and appid in (?, ?)", USER, STEAM, MANUAL))
|
||||
.isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_NoMapping_AddsASecondRowAsToday() {
|
||||
manual(USER, "PC");
|
||||
edition(STEAM, OTHER_IGDB, OTHER_IGDB);
|
||||
|
||||
syncUntilOwned(owned(FILLER, 0), owned(STEAM, 300));
|
||||
|
||||
assertThat(keys(USER)).containsExactlyInAnyOrder(MANUAL, STEAM);
|
||||
assertThat(row(USER, MANUAL)).containsEntry("source", "MANUAL").containsEntry("playtime_minutes", 120);
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_TwiceAfterTakeover_ChangesNothingMore() {
|
||||
manual(USER, "PC");
|
||||
mapped();
|
||||
syncUntilOwned(owned(FILLER, 0), owned(STEAM, 300));
|
||||
int changes = count("select count(*) from user_game_status_change where user_id = ?", USER);
|
||||
|
||||
sync(owned(FILLER, 0), owned(STEAM, 300));
|
||||
|
||||
assertTookOver();
|
||||
assertThat(count("select count(*) from user_game_status_change where user_id = ?", USER)).isEqualTo(changes);
|
||||
assertThat(count("select count(*) from reward_ledger where user_id = ?", USER)).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_RemovedManualRow_ComesBackAsTheSteamRow() {
|
||||
manual(USER, "PC");
|
||||
jdbc.update("update user_game set removed_at = now() where user_id = ?", USER);
|
||||
mapped();
|
||||
|
||||
syncUntilOwned(owned(FILLER, 0), owned(STEAM, 300));
|
||||
|
||||
assertThat(keys(USER)).containsExactly(STEAM);
|
||||
assertThat(row(USER, STEAM).get("removed_at")).isNull();
|
||||
assertThat(row(USER, STEAM)).containsEntry("rating", 4);
|
||||
assertThat(count("select count(*) from sync_change where user_id = ? and kind = 'ADDED' and appid = ?", USER, STEAM))
|
||||
.isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_HiddenManualRow_StaysHiddenUnderTheSteamKey() {
|
||||
manual(USER, "PC");
|
||||
jdbc.update("update user_game set hidden_at = now() where user_id = ?", USER);
|
||||
mapped();
|
||||
|
||||
syncUntilOwned(owned(FILLER, 0), owned(STEAM, 300));
|
||||
|
||||
assertThat(keys(USER)).containsExactly(STEAM);
|
||||
assertThat(row(USER, STEAM).get("hidden_at")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_UserHasARemovedSteamRow_KeepsBothRows() {
|
||||
manual(USER, "PC");
|
||||
mapped();
|
||||
jdbc.update("insert into steam_game (appid, name) values (?, 'Steam copy')", STEAM);
|
||||
jdbc.update("insert into user_game (user_id, appid, status, removed_at) values (?, ?, 'BACKLOG', now())", USER, STEAM);
|
||||
|
||||
syncUntilOwned(owned(FILLER, 0), owned(STEAM, 300));
|
||||
|
||||
assertThat(keys(USER)).containsExactlyInAnyOrder(MANUAL, STEAM);
|
||||
assertThat(row(USER, STEAM).get("removed_at")).isNull();
|
||||
assertThat(row(USER, MANUAL)).containsEntry("source", "MANUAL");
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_TwoSteamAppsOfOneGame_TheLowestTakesTheRow() {
|
||||
manual(USER, "PC");
|
||||
mapped();
|
||||
edition(STEAM_2, IGDB, IGDB);
|
||||
|
||||
syncUntilOwned(owned(FILLER, 0), owned(STEAM_2, 50), owned(STEAM, 300));
|
||||
|
||||
assertThat(keys(USER)).containsExactlyInAnyOrder(STEAM, STEAM_2);
|
||||
assertThat(row(USER, STEAM)).containsEntry("rating", 4);
|
||||
assertThat(row(USER, STEAM_2).get("rating")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_OtherUserHasTheSameManualGame_IsUntouched() {
|
||||
manual(USER, "PC");
|
||||
manual(OTHER_USER, "PC");
|
||||
mapped();
|
||||
|
||||
syncUntilOwned(owned(FILLER, 0), owned(STEAM, 300));
|
||||
|
||||
assertThat(keys(OTHER_USER)).containsExactly(MANUAL);
|
||||
assertThat(row(OTHER_USER, MANUAL)).containsEntry("source", "MANUAL");
|
||||
assertThat(jdbc.queryForList("select ref from reward_ledger where user_id = ?", String.class, OTHER_USER))
|
||||
.containsExactly(String.valueOf(MANUAL));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_FinishedByHandBeforeTakeover_PaysNoSteamFinish() {
|
||||
manual(USER, "PC");
|
||||
mapped();
|
||||
|
||||
syncUntilOwned(owned(FILLER, 0), owned(STEAM, 300));
|
||||
rewardService.settleAll();
|
||||
|
||||
assertThat(jdbc.queryForList("select kind || ':' || ref from reward_ledger where user_id = ? "
|
||||
+ "and kind in ('FINISHED', 'FINISHED_MANUAL', 'FINISHED_EARLY')", String.class, USER))
|
||||
.containsExactly("FINISHED_MANUAL:" + STEAM);
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_FreshEditionWithoutIgdbGame_StillBecomesTheSteamRow() {
|
||||
manual(USER, "PC");
|
||||
edition(MANUAL, IGDB, IGDB);
|
||||
jdbc.update("insert into steam_game (appid, name) values (?, 'Owned by another user') on conflict do nothing", STEAM);
|
||||
jdbc.update("insert into game_edition (appid, igdb_id, same_game_id, checked_at) values (?, null, null, now())",
|
||||
STEAM);
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(igdbApiService.getEditions(anyCollection()))
|
||||
.thenReturn(Optional.of(Map.of(STEAM, new IgdbApiService.Edition(IGDB, IGDB))));
|
||||
|
||||
syncUntilOwned(owned(FILLER, 0), owned(STEAM, 300));
|
||||
|
||||
assertThat(keys(USER)).containsExactly(STEAM);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
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.context.annotation.Import;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Import(GameMergeRepository.class)
|
||||
class GameMergeRepositoryTakeoverTest extends AbstractPersistenceTest {
|
||||
|
||||
private static final long USER = 1L;
|
||||
private static final long OTHER_USER = 2L;
|
||||
private static final long STEAM = 100L;
|
||||
private static final long STEAM_2 = 200L;
|
||||
private static final long MANUAL = -7L;
|
||||
|
||||
@Autowired
|
||||
private GameMergeRepository repository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
for (String table : List.of("reward_ledger", "user_psn_trophy_set", "user_psn_title", "user_game_playtime_day", "user_game_split",
|
||||
"user_game_set_backup", "user_game_question", "backlog_order", "user_game_status_change", "game_link",
|
||||
"game_edition", "user_game")) {
|
||||
jdbc.update("delete from " + table);
|
||||
}
|
||||
for (long appid : List.of(STEAM, STEAM_2, MANUAL, -8L, -9L, 300L)) {
|
||||
jdbc.update("insert into steam_game (appid, name) values (?, ?) on conflict do nothing", appid, "Game " + appid);
|
||||
}
|
||||
edition(STEAM, 7);
|
||||
edition(STEAM_2, 7);
|
||||
edition(MANUAL, 7);
|
||||
}
|
||||
|
||||
private void edition(long appid, long igdbId) {
|
||||
jdbc.update("insert into game_edition (appid, igdb_id, same_game_id, checked_at) values (?, ?, ?, now())",
|
||||
appid, appid < 0 ? -appid : igdbId, igdbId);
|
||||
}
|
||||
|
||||
private void game(long userId, long appid, String source, String platform) {
|
||||
jdbc.update("insert into user_game (user_id, appid, status, source, platform) values (?, ?, 'BACKLOG', ?, ?)",
|
||||
userId, appid, source, platform);
|
||||
}
|
||||
|
||||
private Map<Long, Long> takeovers() {
|
||||
return repository.findSteamTakeovers(USER, List.of(STEAM, STEAM_2));
|
||||
}
|
||||
|
||||
private int count(String sql, Object... args) {
|
||||
return jdbc.queryForObject(sql, Integer.class, args);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findSteamTakeovers_PlatformRule() {
|
||||
for (String platform : new String[]{"PC", "pc", "Steam", " ", null, "Mac", "Linux"}) {
|
||||
jdbc.update("delete from user_game");
|
||||
game(USER, MANUAL, "MANUAL", platform);
|
||||
assertThat(takeovers()).as("platform %s", platform).containsEntry(MANUAL, STEAM);
|
||||
assertThat(repository.hasSteamTakeoverCandidates(USER)).isTrue();
|
||||
}
|
||||
for (String platform : new String[]{"Switch", "PS5"}) {
|
||||
jdbc.update("delete from user_game");
|
||||
game(USER, MANUAL, "MANUAL", platform);
|
||||
assertThat(takeovers()).as("platform %s", platform).isEmpty();
|
||||
assertThat(repository.hasSteamTakeoverCandidates(USER)).isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void findSteamTakeovers_SkipsNonManualPositiveKeysAndKeysTheUserHas() {
|
||||
game(USER, MANUAL, "PSN", "PC");
|
||||
game(USER, 300, "MANUAL", "PC");
|
||||
assertThat(takeovers()).isEmpty();
|
||||
|
||||
jdbc.update("delete from user_game");
|
||||
game(USER, MANUAL, "MANUAL", "PC");
|
||||
game(USER, STEAM, "STEAM", null);
|
||||
game(USER, STEAM_2, "STEAM", null);
|
||||
for (String state : new String[]{"removed_at = now()", "hidden_at = now()", "status = status"}) {
|
||||
jdbc.update("update user_game set " + state + " where user_id = ? and appid in (?, ?)", USER, STEAM, STEAM_2);
|
||||
assertThat(takeovers()).as(state).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void findSteamTakeovers_TwoSteamAppsOfOneGame_TakesTheLowest() {
|
||||
game(USER, MANUAL, "MANUAL", "PC");
|
||||
|
||||
assertThat(takeovers()).containsExactly(Map.entry(MANUAL, STEAM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findSteamTakeovers_EmptyAppids_ReturnsEmpty() {
|
||||
game(USER, MANUAL, "MANUAL", "PC");
|
||||
|
||||
assertThat(repository.findSteamTakeovers(USER, List.of())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rekeyUserGame_MovesRowAndChildren() {
|
||||
game(USER, MANUAL, "MANUAL", "PC");
|
||||
jdbc.update("update user_game set status = 'PLAYING' where user_id = ?", USER);
|
||||
jdbc.update("insert into user_game_question (user_id, appid, question, playtime_minutes) values (?, ?, 'RATE', 1)",
|
||||
USER, MANUAL);
|
||||
jdbc.update("insert into user_game_playtime_day (user_id, day, appid, minutes) values (?, current_date, ?, 5)",
|
||||
USER, MANUAL);
|
||||
jdbc.update("insert into reward_ledger (user_id, kind, ref, points, event_at) values (?, 'FINISHED_MANUAL', ?, 10, now())",
|
||||
USER, String.valueOf(MANUAL));
|
||||
|
||||
assertThat(repository.rekeyUserGame(USER, MANUAL, STEAM)).isTrue();
|
||||
|
||||
assertThat(count("select count(*) from user_game where user_id = ? and appid = ?", USER, MANUAL)).isZero();
|
||||
assertThat(count("select count(*) from user_game where user_id = ? and appid = ?", USER, STEAM)).isOne();
|
||||
assertThat(count("select count(*) from user_game_status_change where user_id = ? and appid = ?", USER, STEAM))
|
||||
.isEqualTo(2);
|
||||
assertThat(count("select count(*) from user_game_question where appid = ?", STEAM)).isOne();
|
||||
assertThat(count("select count(*) from user_game_playtime_day where appid = ?", STEAM)).isOne();
|
||||
assertThat(jdbc.queryForObject("select ref from reward_ledger where user_id = ?", String.class, USER))
|
||||
.isEqualTo("100");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rekeyUserGame_DayBothKeysHave_AddsUpOnTheTarget() {
|
||||
game(USER, MANUAL, "MANUAL", "PC");
|
||||
for (long appid : new long[]{MANUAL, STEAM}) {
|
||||
jdbc.update("insert into user_game_playtime_day (user_id, day, appid, minutes) values (?, current_date, ?, 5)",
|
||||
USER, appid);
|
||||
}
|
||||
|
||||
repository.rekeyUserGame(USER, MANUAL, STEAM);
|
||||
|
||||
assertThat(jdbc.queryForList("select appid || ':' || minutes from user_game_playtime_day", String.class))
|
||||
.containsExactly("100:10");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rekeyUserGame_KindHeldOnTarget_StaysOnOldRef() {
|
||||
game(USER, MANUAL, "MANUAL", "PC");
|
||||
for (long ref : new long[]{MANUAL, STEAM}) {
|
||||
jdbc.update("insert into reward_ledger (user_id, kind, ref, points, event_at) values (?, 'FINISHED_MANUAL', ?, 10, now())",
|
||||
USER, String.valueOf(ref));
|
||||
}
|
||||
|
||||
assertThat(repository.rekeyUserGame(USER, MANUAL, STEAM)).isTrue();
|
||||
|
||||
assertThat(jdbc.queryForList("select ref from reward_ledger where user_id = ?", String.class, USER))
|
||||
.containsExactlyInAnyOrder("-7", "100");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rekeyUserGame_TargetRowExists_ReturnsFalseAndChangesNothing() {
|
||||
game(USER, MANUAL, "MANUAL", "PC");
|
||||
game(USER, STEAM, "STEAM", null);
|
||||
|
||||
assertThat(repository.rekeyUserGame(USER, MANUAL, STEAM)).isFalse();
|
||||
|
||||
assertThat(count("select count(*) from user_game where user_id = ? and appid = ?", USER, MANUAL)).isOne();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rekeyUserGame_OtherUser_IsUntouched() {
|
||||
game(USER, MANUAL, "MANUAL", "PC");
|
||||
game(OTHER_USER, MANUAL, "MANUAL", "PC");
|
||||
jdbc.update("insert into reward_ledger (user_id, kind, ref, points, event_at) values (?, 'FINISHED_MANUAL', ?, 10, now())",
|
||||
OTHER_USER, String.valueOf(MANUAL));
|
||||
|
||||
repository.rekeyUserGame(USER, MANUAL, STEAM);
|
||||
|
||||
assertThat(count("select count(*) from user_game where user_id = ? and appid = ?", OTHER_USER, MANUAL)).isOne();
|
||||
assertThat(jdbc.queryForObject("select ref from reward_ledger where user_id = ?", String.class, OTHER_USER))
|
||||
.isEqualTo("-7");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rekeyUserGame_MovesTrophySets() {
|
||||
game(USER, MANUAL, "MANUAL", "PC");
|
||||
jdbc.update("delete from psn_trophy_set where np_communication_id = 'NPWR00001_00'");
|
||||
jdbc.update("""
|
||||
insert into psn_trophy_set (np_communication_id, service_name, platform, name, defined_bronze,
|
||||
defined_silver, defined_gold, defined_platinum)
|
||||
values ('NPWR00001_00', 'trophy2', 'PS5', 'Set', 1, 0, 0, 0)""");
|
||||
jdbc.update("""
|
||||
insert into user_psn_trophy_set (user_id, np_communication_id, appid, earned_total, platinum_earned)
|
||||
values (?, 'NPWR00001_00', ?, 1, false)""", USER, MANUAL);
|
||||
|
||||
assertThat(repository.rekeyUserGame(USER, MANUAL, STEAM)).isTrue();
|
||||
|
||||
assertThat(jdbc.queryForObject("select appid from user_psn_trophy_set where user_id = ?", Long.class, USER))
|
||||
.isEqualTo(STEAM);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyCollection;
|
||||
@@ -63,8 +64,9 @@ class GameEditionServiceTest extends AbstractUnitTest {
|
||||
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));
|
||||
Set<Long> stored = service.refresh(List.of(220L, 620L, 700L));
|
||||
|
||||
assertThat(stored).containsExactlyInAnyOrder(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());
|
||||
@@ -97,8 +99,9 @@ class GameEditionServiceTest extends AbstractUnitTest {
|
||||
void refresh_IgdbOff_DoesNothing() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(false);
|
||||
|
||||
service.refresh(List.of(620L));
|
||||
Set<Long> stored = service.refresh(List.of(620L));
|
||||
|
||||
assertThat(stored).isEmpty();
|
||||
verifyNoInteractions(gameEditionRepository, gameLinkRepository);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import core_service.modules.library.domain.UserGame;
|
||||
import core_service.modules.library.domain.UserSteamSync;
|
||||
import core_service.modules.library.domain.WishlistGame;
|
||||
import core_service.modules.library.repo.GameCorrectionRepository;
|
||||
import core_service.modules.library.repo.GameMergeRepository;
|
||||
import core_service.modules.library.repo.PlaytimeDayRepository;
|
||||
import core_service.modules.library.repo.SteamGameRepository;
|
||||
import core_service.modules.library.repo.SyncChangeRepository;
|
||||
@@ -107,6 +108,9 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
||||
@Mock
|
||||
private GameCorrectionRepository gameCorrectionRepository;
|
||||
|
||||
@Mock
|
||||
private GameMergeRepository gameMergeRepository;
|
||||
|
||||
@Mock
|
||||
private UserGameRepository userGameRepository;
|
||||
|
||||
@@ -297,6 +301,45 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
||||
verify(playtimeDayRepository).addMinutes(USER_ID, 220L, today(), 120);
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_ManualCandidate_RefreshesEditionsBeforeTheSaveAndLinksAfter() {
|
||||
steamReturns(game(100, 0), STEAM_ID);
|
||||
when(gameMergeRepository.hasSteamTakeoverCandidates(USER_ID)).thenReturn(true);
|
||||
when(gameEditionService.refresh(List.of(220L))).thenReturn(Set.of(220L));
|
||||
|
||||
worker.runSync(USER_ID, STEAM_ID, SyncTrigger.AUTO);
|
||||
|
||||
InOrder order = inOrder(gameEditionService, gameMergeRepository);
|
||||
order.verify(gameEditionService).refresh(List.of(220L));
|
||||
order.verify(gameMergeRepository).findSteamTakeovers(eq(USER_ID), any());
|
||||
order.verify(gameEditionService).linkSameGames(Set.of(220L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_NoManualCandidate_DoesNotRefreshBeforeTheSave() {
|
||||
steamReturns(game(100, 0), STEAM_ID);
|
||||
|
||||
worker.runSync(USER_ID, STEAM_ID, SyncTrigger.AUTO);
|
||||
|
||||
verify(gameEditionService).refresh(anyList());
|
||||
verify(gameEditionService, never()).linkSameGames(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_Takeover_MovesTheRowBeforeLoadingTheUserGames() {
|
||||
steamReturns(game(100, 0), STEAM_ID);
|
||||
when(gameMergeRepository.findSteamTakeovers(eq(USER_ID), any())).thenReturn(Map.of(-119388L, 220L));
|
||||
when(gameMergeRepository.rekeyUserGame(USER_ID, -119388L, 220L)).thenReturn(true);
|
||||
|
||||
worker.runSync(USER_ID, STEAM_ID, SyncTrigger.AUTO);
|
||||
|
||||
InOrder order = inOrder(steamGameRepository, gameMergeRepository, userGameRepository);
|
||||
order.verify(steamGameRepository).flush();
|
||||
order.verify(gameMergeRepository).findSteamTakeovers(eq(USER_ID), any());
|
||||
order.verify(gameMergeRepository).rekeyUserGame(USER_ID, -119388L, 220L);
|
||||
order.verify(userGameRepository).findByUserId(USER_ID);
|
||||
}
|
||||
|
||||
private static SteamOwnedGamesResponse.Game gameLastPlayed(Long rtimeLastPlayed) {
|
||||
return new SteamOwnedGamesResponse.Game(220, "Half-Life 2", 100, "icon", true, 0, rtimeLastPlayed);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user