Public Access
feat(library): soft delete removed games and add restore
Removed games could not come back (#39). Remove now sets removed_at and restore clears it. V46 brings back PSN games removed before.
This commit is contained in:
1 parent
696e06e7fe
commit
4d49b0f023
16 files changed
+620
-20
No files matched your search
@@ -243,6 +243,8 @@ ArchUnit role rules (`RoleAnnotationRules`) and `src/test/resources/contract/aut
|
||||
| `GET` | `/api/library/questions`, `/api/library/questions/count` (`{results: [{appid, name, imageUrl, question, status, playtimeMinutes, normallyMinutes, lastActivity}]}`, `{count}`) | JWT |
|
||||
| `POST` | `/api/library/questions/{appid}` (`{question, yes, rating?}`; `200 {next}`, `400 INVALID_ANSWER`, `404 GAME_NOT_FOUND`, `409 QUESTION_GONE`) | JWT |
|
||||
| `PUT` | `/api/library/games/{appid}/finished-hint` (`{dismissed: ENDING\|PLAYTIME}`, "Not yet"; `204`) | JWT |
|
||||
| `GET` | `/api/library/removed-games` (`{results: [{appid, name, imageUrl, source, platform, removedAt}]}`: the caller's removed `MANUAL` and `PSN` games, newest first) | JWT |
|
||||
| `POST` | `/api/library/games/{appid}/restore` (a removed game comes back with its old data; `204`, also when it is already back; `404 GAME_NOT_FOUND`, `409 STEAM_GAME`) | JWT |
|
||||
| `PUT` | `/api/library/games/{appid}/hidden` (`{hidden}`; Steam and manual games) | JWT |
|
||||
| `POST` | `/api/library/imports` (`{source: TEXT\|CSV\|SEARCH\|GALAXY\|GROUVEE\|HLTB\|PLAYNITE\|BOOKMARKLET, rows: [{title, platform, playtimeMinutes, status, igdbId}]}`, 1–1000 rows; `202 {id}` in `QUEUED`, `409 IMPORT_RUNNING`, `503 IGDB_OFF`) | JWT |
|
||||
| `GET` | `/api/library/imports` (`{imports}`, the last 20), `/api/library/imports/{id}` (status, progress counts, `ahead` while it waits in a queue, `attempts`; the rows with their candidates while `READY`) | JWT |
|
||||
@@ -647,11 +649,21 @@ A game the user adds by hand comes from IGDB (`ManualGameService`). Steam games
|
||||
`user_game.source` (`GameSource`) says who owns the row. The sync never changes or removes a `MANUAL` row, with
|
||||
one exception: when the user later owns the game on Steam under the same appid, the sync takes the row over: it becomes
|
||||
`STEAM`, and status, rating and notes stay. `PSN` rows (a PlayStation import, see below) live the same way. Only `MANUAL` and `PSN` rows can be edited (`PUT /api/library/games/{appid}/manual`)
|
||||
or deleted (`DELETE /api/library/games/{appid}`). Time to beat uses the Steam appid for positive keys and the IGDB
|
||||
or removed (`DELETE /api/library/games/{appid}`, soft: it sets `removed_at`, the row keeps status, rating, notes
|
||||
and history) and restored (`POST /api/library/games/{appid}/restore`, see the diagram). Time to beat uses the Steam appid for positive keys and the IGDB
|
||||
id for negative keys. Search and add check both keys of a game. A game added under minus its IGDB id can still get
|
||||
a second row when IGDB learns its Steam appid later and the user owns it on Steam; an auditor then merges the two
|
||||
(see "Merging duplicates").
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Listed: add / sync
|
||||
Listed --> Removed: DELETE /games/{appid} (MANUAL, PSN)
|
||||
Removed --> Listed: POST /games/{appid}/restore or Undo
|
||||
Removed: removed_at set, data kept
|
||||
Removed: PSN sync skips the row
|
||||
```
|
||||
|
||||
### Imports
|
||||
|
||||
Many games at once (V22, `GameImportService`): a list, a CSV or several picks of the game search become one
|
||||
@@ -723,8 +735,10 @@ project ships it; all values in `app.psn.*`). `PsnAccountService` in `library` h
|
||||
- Every `LINKED` or `STEAM_OWNED` title also keeps its own platform, minutes and last played in `user_psn_title` (V44),
|
||||
so the detail shows PlayStation play next to Steam's on a shared row. The Steam row's own playtime stays Steam's.
|
||||
- A `LINKED` title updates its row (`source = PSN`, not removed): playtime, `last_played_at`, `PLAYTIME` changes and the
|
||||
playtime history; `playtime_2weeks` is the sum of the history of the last 14 days. A game the user deleted stays
|
||||
`LINKED` and is not added again. A game also owned on Steam stays one row (the Steam row wins).
|
||||
playtime history; `playtime_2weeks` is the sum of the history of the last 14 days. A game the user removed stays
|
||||
`LINKED` and the sync does not add it again; its row stays removed, so the sync skips it until the user restores it:
|
||||
a restore takes the stored title play (`psn_title_play`) and the sync updates the row again. Flyway V46 gave games
|
||||
removed before (soft delete) a removed `PSN` row and turned a `MANUAL` row re-added on a `LINKED` key back to `PSN`. A game also owned on Steam stays one row (the Steam row wins).
|
||||
- The first run of a user (no `user_psn_title` yet) is a baseline like the first Steam sync: its adds are a count in
|
||||
`sync_result.imported_games`, not a list. PSN changes share the `sync_result` of the Steam part, so the banner and the
|
||||
follow digest need no change.
|
||||
|
||||
@@ -41,20 +41,20 @@ public class FollowDigestRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hidden games never count. Wishlist adds are counted apart, see {@link #countWishlist}.
|
||||
* Hidden and removed games never count. Wishlist adds are counted apart, see {@link #countWishlist}.
|
||||
*/
|
||||
public Digest countSync(long userId) {
|
||||
return jdbc.queryForObject("""
|
||||
select
|
||||
(select count(distinct c.appid) from sync_change c
|
||||
join user_game ug on ug.user_id = c.user_id and ug.appid = c.appid
|
||||
where c.user_id = :userId and c.kind = 'ADDED' and ug.hidden_at is null) as new_games,
|
||||
where c.user_id = :userId and c.kind = 'ADDED' and ug.hidden_at is null and ug.removed_at is null) as new_games,
|
||||
(select coalesce(sum(c.amount), 0) from sync_change c
|
||||
join user_game ug on ug.user_id = c.user_id and ug.appid = c.appid
|
||||
where c.user_id = :userId and c.kind = 'ACHIEVEMENTS' and ug.hidden_at is null) as achievements,
|
||||
where c.user_id = :userId and c.kind = 'ACHIEVEMENTS' and ug.hidden_at is null and ug.removed_at is null) as achievements,
|
||||
(select count(*) from sync_change c
|
||||
join user_game ug on ug.user_id = c.user_id and ug.appid = c.appid
|
||||
where c.user_id = :userId and c.kind = 'STATUS' and ug.hidden_at is null) as status_changes""",
|
||||
where c.user_id = :userId and c.kind = 'STATUS' and ug.hidden_at is null and ug.removed_at is null) as status_changes""",
|
||||
user(userId),
|
||||
(rs, i) -> new Digest(rs.getInt("new_games"), rs.getInt("achievements"), rs.getInt("status_changes"), 0));
|
||||
}
|
||||
@@ -93,7 +93,7 @@ public class FollowDigestRepository {
|
||||
from user_game_status_change c
|
||||
join user_game ug on ug.user_id = c.user_id and ug.appid = c.appid
|
||||
where c.user_id = :userId and c.by_user and c.id > :afterId and c.id <= :upToId
|
||||
and ug.hidden_at is null""",
|
||||
and ug.hidden_at is null and ug.removed_at is null""",
|
||||
user(userId).addValue("afterId", afterId).addValue("upToId", upToId),
|
||||
(rs, i) -> new Digest(rs.getInt("new_games"), 0, rs.getInt("status_changes"), 0));
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import core_service.modules.library.api.dto.AddGameRequest;
|
||||
import core_service.modules.library.api.dto.AddedGameDto;
|
||||
import core_service.modules.library.api.dto.GameSearchDto;
|
||||
import core_service.modules.library.api.dto.ManualGameUpdate;
|
||||
import core_service.modules.library.api.dto.RemovedGamesDto;
|
||||
import core_service.modules.library.service.ManualGameService;
|
||||
import core_service.shared.security.UserPrincipal;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -79,6 +80,20 @@ public class ManualGameController {
|
||||
return toResponse(manualGameService.remove(principal.getIdAsLong(), appid));
|
||||
}
|
||||
|
||||
@PostMapping("/games/{appid}/restore")
|
||||
public ResponseEntity<?> restore(@AuthenticationPrincipal UserPrincipal principal,
|
||||
@PathVariable("appid") long appid) {
|
||||
return toResponse(manualGameService.restore(principal.getIdAsLong(), appid));
|
||||
}
|
||||
|
||||
@GetMapping("/removed-games")
|
||||
public RemovedGamesDto removedGames(@AuthenticationPrincipal UserPrincipal principal) {
|
||||
return new RemovedGamesDto(manualGameService.removedGames(principal.getIdAsLong()).stream()
|
||||
.map(game -> new RemovedGamesDto.Game(game.appid(), game.name(), game.imageUrl(), game.source().name(),
|
||||
game.platform(), game.removedAt()))
|
||||
.toList());
|
||||
}
|
||||
|
||||
private static ResponseEntity<?> toResponse(ManualGameService.ChangeResult result) {
|
||||
return switch (result) {
|
||||
case DONE -> ResponseEntity.noContent().build();
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package core_service.modules.library.api.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public record RemovedGamesDto(List<Game> results) {
|
||||
|
||||
public record Game(long appid, String name, String imageUrl, String source, String platform, Instant removedAt) {}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package core_service.modules.library.domain;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* A game the user removed and can restore.
|
||||
*/
|
||||
public record RemovedGame(long appid, String name, String imageUrl, GameSource source, String platform, Instant removedAt) {}
|
||||
@@ -104,7 +104,7 @@ public class SyncChangeRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hidden games are left out when read, so hiding a game after the sync hides it here too.
|
||||
* Hidden games are left out when read, so hiding a game after the sync hides it here too. A removed game stays only for its own REMOVED entry.
|
||||
*/
|
||||
public Optional<SyncChanges> find(long userId) {
|
||||
MapSqlParameterSource params = new MapSqlParameterSource("userId", userId);
|
||||
@@ -131,7 +131,7 @@ public class SyncChangeRepository {
|
||||
from sync_change c
|
||||
join user_game ug on ug.user_id = c.user_id and ug.appid = c.appid
|
||||
left join steam_game sg on sg.appid = c.appid
|
||||
where c.user_id = :userId and ug.hidden_at is null and c.kind <> 'WISHLIST'
|
||||
where c.user_id = :userId and ug.hidden_at is null and (ug.removed_at is null or c.kind = 'REMOVED') and c.kind <> 'WISHLIST'
|
||||
group by c.kind, c.appid, c.from_status, c.to_status, sg.name, sg.image_url
|
||||
order by c.kind, amount desc nulls last, sg.name
|
||||
""", params, (rs, i) -> new SyncChanges.Entry(
|
||||
|
||||
@@ -6,6 +6,7 @@ import core_service.modules.library.domain.GameSource;
|
||||
import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.Mood;
|
||||
import core_service.modules.library.domain.RecentEntry;
|
||||
import core_service.modules.library.domain.RemovedGame;
|
||||
import core_service.modules.library.domain.StatusCount;
|
||||
import core_service.modules.library.domain.UserGame;
|
||||
import core_service.modules.library.domain.UserGameId;
|
||||
@@ -450,6 +451,16 @@ public interface UserGameRepository extends JpaRepository<UserGame, UserGameId>
|
||||
"core_service.modules.library.domain.GameStatus.PLAYING)")
|
||||
int finishIfUntouched(@Param("userId") Long userId, @Param("appid") Long appid);
|
||||
|
||||
/**
|
||||
* Games the user removed (MANUAL and PSN rows); a Steam row the sync removed is not listed, the next sync would
|
||||
* remove it again.
|
||||
*/
|
||||
@Query("select new core_service.modules.library.domain.RemovedGame(ug.appid, sg.name, sg.imageUrl, ug.source, ug.platform, ug.removedAt) " +
|
||||
"from UserGame ug join SteamGame sg on sg.appid = ug.appid " +
|
||||
"where ug.userId = :userId and ug.removedAt is not null " +
|
||||
"and ug.source <> core_service.modules.library.domain.GameSource.STEAM order by ug.removedAt desc, ug.appid")
|
||||
List<RemovedGame> findUserRemoved(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* Only games with an unlocked achievement: the others cannot get an estimate.
|
||||
*/
|
||||
|
||||
@@ -7,10 +7,13 @@ import core_service.modules.library.domain.GameKeys;
|
||||
import core_service.modules.library.domain.IgdbGame;
|
||||
import core_service.modules.library.domain.GameSource;
|
||||
import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.RemovedGame;
|
||||
import core_service.modules.library.domain.SteamGame;
|
||||
import core_service.modules.library.domain.TitlePlay;
|
||||
import core_service.modules.library.domain.UserGame;
|
||||
import core_service.modules.library.domain.UserGameId;
|
||||
import core_service.modules.library.repo.GameMergeRepository;
|
||||
import core_service.modules.library.repo.PsnTitleRepository;
|
||||
import core_service.modules.library.repo.SteamGameRepository;
|
||||
import core_service.modules.library.repo.UserGameRepository;
|
||||
import core_service.shared.security.RequiresRole;
|
||||
@@ -29,6 +32,7 @@ import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
@@ -69,6 +73,7 @@ public class ManualGameService {
|
||||
private final GameEditionService gameEditionService;
|
||||
private final GameCreditService gameCreditService;
|
||||
private final ReleaseDateService releaseDateService;
|
||||
private final PsnTitleRepository psnTitleRepository;
|
||||
private final TransactionTemplate transaction;
|
||||
|
||||
public ManualGameService(IgdbApiService igdbApiService, UserGameRepository userGameRepository,
|
||||
@@ -76,7 +81,7 @@ public class ManualGameService {
|
||||
TimeToBeatService timeToBeatService, MoodService moodService,
|
||||
GameEditionService gameEditionService, GameCreditService gameCreditService,
|
||||
ReleaseDateService releaseDateService,
|
||||
PlatformTransactionManager transactionManager) {
|
||||
PsnTitleRepository psnTitleRepository, PlatformTransactionManager transactionManager) {
|
||||
this.igdbApiService = igdbApiService;
|
||||
this.userGameRepository = userGameRepository;
|
||||
this.steamGameRepository = steamGameRepository;
|
||||
@@ -86,6 +91,7 @@ public class ManualGameService {
|
||||
this.gameEditionService = gameEditionService;
|
||||
this.gameCreditService = gameCreditService;
|
||||
this.releaseDateService = releaseDateService;
|
||||
this.psnTitleRepository = psnTitleRepository;
|
||||
this.transaction = new TransactionTemplate(transactionManager);
|
||||
}
|
||||
|
||||
@@ -287,11 +293,51 @@ public class ManualGameService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the row with its status, rating and notes. Steam games cannot be removed: the next sync would add
|
||||
* them again.
|
||||
* Sets removed_at; the row keeps status, rating, notes and history, and {@link #restore} brings it back. Steam
|
||||
* games cannot be removed: the next sync would add them again.
|
||||
*/
|
||||
public ChangeResult remove(Long userId, long appid) {
|
||||
return transaction.execute(tx -> changeManual(userId, appid, userGameRepository::delete));
|
||||
return transaction.execute(tx -> changeManual(userId, appid, game -> game.setRemovedAt(Instant.now())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Brings a removed game back with its old data. A PSN row takes the stored title play, so no outside call can
|
||||
* fail; the next sync corrects it. A removed Steam row belongs to the sync and is not restorable.
|
||||
*/
|
||||
public ChangeResult restore(Long userId, long appid) {
|
||||
return transaction.execute(tx -> {
|
||||
Optional<UserGame> found = userGameRepository.findById(new UserGameId(userId, appid));
|
||||
if (found.isEmpty()) {
|
||||
return ChangeResult.NOT_FOUND;
|
||||
}
|
||||
UserGame game = found.get();
|
||||
if (game.getRemovedAt() == null) {
|
||||
return ChangeResult.DONE;
|
||||
}
|
||||
if (game.getSource() == GameSource.STEAM) {
|
||||
return ChangeResult.STEAM_GAME;
|
||||
}
|
||||
game.setRemovedAt(null);
|
||||
game.setLastSeenAt(Instant.now());
|
||||
if (game.getSource() == GameSource.PSN) {
|
||||
applyPsnPlay(userId, game);
|
||||
}
|
||||
return ChangeResult.DONE;
|
||||
});
|
||||
}
|
||||
|
||||
public List<RemovedGame> removedGames(Long userId) {
|
||||
return userGameRepository.findUserRemoved(userId);
|
||||
}
|
||||
|
||||
private void applyPsnPlay(Long userId, UserGame game) {
|
||||
List<TitlePlay> plays = psnTitleRepository.findPlays(userId, List.of(game.getAppid()));
|
||||
if (plays.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
game.setPlaytimeMinutes((int) Math.min(plays.stream().mapToLong(TitlePlay::playtimeMinutes).sum(), Integer.MAX_VALUE));
|
||||
plays.stream().map(TitlePlay::lastPlayedAt).filter(Objects::nonNull).max(Comparator.naturalOrder())
|
||||
.ifPresent(game::setLastPlayedAt);
|
||||
}
|
||||
|
||||
private ChangeResult changeManual(Long userId, long appid, Consumer<UserGame> change) {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
-- Gitea #39: a remove keeps the row now (removed_at), and the user can restore it. Rows from before need two fixes.
|
||||
-- Both statements are safe to run again.
|
||||
|
||||
-- A game re-added by search after a remove became MANUAL, so the PSN sync skipped it. It is PSN again; the user's
|
||||
-- status, rating, notes, sets and history stay on the same row. PlayStation owns the playtime of a PSN row.
|
||||
UPDATE user_game ug
|
||||
SET source = 'PSN',
|
||||
playtime_minutes = p.minutes,
|
||||
last_played_at = COALESCE(p.last_played_at, ug.last_played_at)
|
||||
FROM (SELECT t.user_id, COALESCE(a.to_appid, t.appid) AS appid,
|
||||
LEAST(SUM(COALESCE(t.playtime_minutes, 0)), 2147483647)::int AS minutes,
|
||||
MAX(t.last_played_at) AS last_played_at
|
||||
FROM user_psn_title t LEFT JOIN game_alias a ON a.from_appid = t.appid
|
||||
WHERE t.state = 'LINKED' AND t.appid IS NOT NULL
|
||||
GROUP BY t.user_id, COALESCE(a.to_appid, t.appid)) p
|
||||
WHERE ug.user_id = p.user_id AND ug.appid = p.appid AND ug.source = 'MANUAL';
|
||||
|
||||
-- A remove deleted the row before; a LINKED title kept its appid, so the sync never added it again. One
|
||||
-- soft-removed PSN row per such game, so the user can restore it. The status is a guess, so it is no user choice. Old status, rating and notes are gone: PLAYING
|
||||
-- when PlayStation has playtime, else BACKLOG.
|
||||
INSERT INTO user_game (user_id, appid, source, platform, status, status_set_by_user, playtime_minutes,
|
||||
last_played_at, first_seen_at, last_seen_at, removed_at)
|
||||
SELECT p.user_id, p.appid, 'PSN', (array_agg(p.platform ORDER BY p.playtime_minutes DESC NULLS LAST))[1],
|
||||
CASE WHEN SUM(COALESCE(p.playtime_minutes, 0)) > 0 THEN 'PLAYING' ELSE 'BACKLOG' END,
|
||||
false, LEAST(SUM(COALESCE(p.playtime_minutes, 0)), 2147483647)::int,
|
||||
MAX(p.last_played_at), MIN(t.created_at), NOW(), NOW()
|
||||
FROM user_psn_title t
|
||||
LEFT JOIN game_alias a ON a.from_appid = t.appid
|
||||
CROSS JOIN LATERAL (SELECT t.user_id, COALESCE(a.to_appid, t.appid) AS appid, t.platform,
|
||||
t.playtime_minutes, t.last_played_at) p
|
||||
WHERE t.state = 'LINKED' AND t.appid IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM steam_game g WHERE g.appid = p.appid)
|
||||
AND NOT EXISTS (SELECT 1 FROM user_game ug WHERE ug.user_id = p.user_id AND ug.appid = p.appid)
|
||||
AND p.user_id NOT IN (SELECT user_id FROM deleted_user)
|
||||
GROUP BY p.user_id, p.appid;
|
||||
@@ -0,0 +1,52 @@
|
||||
package core_service.modules.follow.repo;
|
||||
|
||||
import core_service.modules.follow.domain.Digest;
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Import(FollowDigestRepository.class)
|
||||
class FollowDigestRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
private static final long USER = 1L;
|
||||
private static final long LISTED = 10L;
|
||||
private static final long REMOVED = 11L;
|
||||
|
||||
@Autowired
|
||||
private FollowDigestRepository repository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
jdbc.update("insert into sync_result (user_id, sync_started_at, started_by, imported_games) values (?, now(), 'AUTO', 0)", USER);
|
||||
for (long appid : new long[]{LISTED, REMOVED}) {
|
||||
jdbc.update("insert into steam_game (appid, name) values (?, 'Game')", appid);
|
||||
jdbc.update("insert into user_game (user_id, appid, status, source, platform, status_set_by_user) "
|
||||
+ "values (?, ?, 'BACKLOG', 'MANUAL', 'PC', true)", USER, appid);
|
||||
jdbc.update("insert into sync_change (user_id, kind, appid, amount) values (?, 'ADDED', ?, null)", USER, appid);
|
||||
jdbc.update("insert into sync_change (user_id, kind, appid, amount) values (?, 'ACHIEVEMENTS', ?, 3)", USER, appid);
|
||||
jdbc.update("insert into sync_change (user_id, kind, appid, from_status, to_status) "
|
||||
+ "values (?, 'STATUS', ?, 'BACKLOG', 'PLAYING')", USER, appid);
|
||||
}
|
||||
jdbc.update("update user_game set removed_at = now() where appid = ?", REMOVED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void countSync_SkipsRemovedGames() {
|
||||
assertThat(repository.countSync(USER)).isEqualTo(new Digest(1, 3, 1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void countHand_SkipsRemovedGames() {
|
||||
Digest digest = repository.countHand(USER, 0, Long.MAX_VALUE);
|
||||
|
||||
assertThat(digest.newGames()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
@@ -313,8 +313,9 @@ class ManualGameControllerTest extends AbstractMockedAdaptersTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void remove_DeletesAGameAddedByHandButNoSteamGame() throws Exception {
|
||||
void remove_KeepsTheRowWithItsNotesAndHidesItButRemovesNoSteamGame() throws Exception {
|
||||
add(119388, "Switch");
|
||||
jdbc.update("update user_game set notes = 'keep me' where appid = -119388");
|
||||
steamGameRepository.save(new SteamGame(220L, "Half-Life 2", null, Instant.now()));
|
||||
userGameRepository.save(new UserGame(USER_ID, 220L, 300, Instant.now(), Instant.now()));
|
||||
|
||||
@@ -325,10 +326,147 @@ class ManualGameControllerTest extends AbstractMockedAdaptersTest {
|
||||
mockMvc.perform(delete("/api/library/games/-1").header(ClientHeaderFilter.CLIENT_HEADER, "test"))
|
||||
.andExpect(status().isNotFound());
|
||||
|
||||
assertTrue(userGameRepository.findById(new UserGameId(USER_ID, -119388L)).isEmpty());
|
||||
UserGame removed = stored(-119388);
|
||||
assertTrue(removed.getRemovedAt() != null);
|
||||
assertEquals("keep me", removed.getNotes());
|
||||
mockMvc.perform(get("/api/library/games/-119388")).andExpect(status().isNotFound());
|
||||
mockMvc.perform(get("/api/library/games").param("q", "zelda"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.content").isEmpty());
|
||||
assertNull(stored(220).getRemovedAt());
|
||||
}
|
||||
|
||||
private void remove(long appid) throws Exception {
|
||||
mockMvc.perform(delete("/api/library/games/" + appid).header(ClientHeaderFilter.CLIENT_HEADER, "test"))
|
||||
.andExpect(status().isNoContent());
|
||||
}
|
||||
|
||||
private org.springframework.test.web.servlet.ResultActions restore(long appid) throws Exception {
|
||||
return mockMvc.perform(post("/api/library/games/" + appid + "/restore")
|
||||
.header(ClientHeaderFilter.CLIENT_HEADER, "test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removedGames_ListsOnlyTheCallersManualAndPsnRows() throws Exception {
|
||||
add(119388, "Switch");
|
||||
add(119133, "PS5");
|
||||
steamGameRepository.save(new SteamGame(220L, "Half-Life 2", null, Instant.now()));
|
||||
steamGameRepository.save(new SteamGame(221L, "Other", null, Instant.now()));
|
||||
UserGame syncRemoved = new UserGame(USER_ID, 220L, 300, Instant.now(), Instant.now());
|
||||
syncRemoved.setRemovedAt(Instant.now());
|
||||
userGameRepository.save(syncRemoved);
|
||||
UserGame other = new UserGame(2L, 221L, 300, Instant.now(), Instant.now());
|
||||
other.setSource(GameSource.MANUAL);
|
||||
other.setRemovedAt(Instant.now());
|
||||
userGameRepository.save(other);
|
||||
remove(-119388);
|
||||
|
||||
mockMvc.perform(get("/api/library/removed-games"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.results.length()").value(1))
|
||||
.andExpect(jsonPath("$.results[0].appid").value(-119388))
|
||||
.andExpect(jsonPath("$.results[0].name").value("Zelda: Tears of the Kingdom"))
|
||||
.andExpect(jsonPath("$.results[0].source").value("MANUAL"))
|
||||
.andExpect(jsonPath("$.results[0].platform").value("Switch"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void restore_BringsBackStatusRatingNotesAndListsTheGame() throws Exception {
|
||||
add(119388, "Switch");
|
||||
jdbc.update("update user_game set status = 'FINISHED', rating = 5, notes = 'loved it' where appid = -119388");
|
||||
remove(-119388);
|
||||
|
||||
restore(-119388).andExpect(status().isNoContent());
|
||||
|
||||
mockMvc.perform(get("/api/library/games/-119388"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("FINISHED"))
|
||||
.andExpect(jsonPath("$.rating").value(5))
|
||||
.andExpect(jsonPath("$.notes").value("loved it"));
|
||||
mockMvc.perform(get("/api/library/removed-games")).andExpect(jsonPath("$.results").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void restore_Twice_Is204AndKeepsOneRow() throws Exception {
|
||||
add(119388, "Switch");
|
||||
remove(-119388);
|
||||
|
||||
restore(-119388).andExpect(status().isNoContent());
|
||||
restore(-119388).andExpect(status().isNoContent());
|
||||
|
||||
assertEquals(1, userGameRepository.count());
|
||||
}
|
||||
|
||||
@Test
|
||||
void restore_GameOfAnotherUser_Is404() throws Exception {
|
||||
steamGameRepository.save(new SteamGame(221L, "Other", null, Instant.now()));
|
||||
UserGame other = new UserGame(2L, 221L, 300, Instant.now(), Instant.now());
|
||||
other.setSource(GameSource.MANUAL);
|
||||
other.setRemovedAt(Instant.now());
|
||||
userGameRepository.save(other);
|
||||
|
||||
restore(221).andExpect(status().isNotFound());
|
||||
|
||||
assertTrue(userGameRepository.findById(new UserGameId(2L, 221L)).orElseThrow().getRemovedAt() != null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void restore_SyncRemovedSteamGame_Is409() throws Exception {
|
||||
steamGameRepository.save(new SteamGame(220L, "Half-Life 2", null, Instant.now()));
|
||||
UserGame game = new UserGame(USER_ID, 220L, 300, Instant.now(), Instant.now());
|
||||
game.setRemovedAt(Instant.now());
|
||||
userGameRepository.save(game);
|
||||
|
||||
restore(220).andExpect(status().isConflict());
|
||||
|
||||
assertTrue(stored(220).getRemovedAt() != null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void restore_PsnGame_TakesTheStoredTitlePlaytime() throws Exception {
|
||||
add(119388, "PS5");
|
||||
jdbc.update("update user_game set source = 'PSN' where appid = -119388");
|
||||
jdbc.update("""
|
||||
insert into user_psn_title (user_id, psn_key, state, appid, platform, playtime_minutes, last_played_at)
|
||||
values (?, 'c1', 'LINKED', -119388, 'PS5', 90, now()), (?, 't1', 'LINKED', -119388, 'PS4', 30, null)""",
|
||||
USER_ID, USER_ID);
|
||||
remove(-119388);
|
||||
|
||||
restore(-119388).andExpect(status().isNoContent());
|
||||
|
||||
UserGame game = stored(-119388);
|
||||
assertNull(game.getRemovedAt());
|
||||
assertEquals(120, game.getPlaytimeMinutes());
|
||||
assertTrue(game.getLastPlayedAt() != null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void restore_ManualGame_KeepsItsTypedHours() throws Exception {
|
||||
add(119388, "Switch");
|
||||
remove(-119388);
|
||||
|
||||
restore(-119388).andExpect(status().isNoContent());
|
||||
|
||||
assertEquals(600, stored(-119388).getPlaytimeMinutes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void restore_DoesNotPayPointsTwice() throws Exception {
|
||||
add(119388, "Switch");
|
||||
jdbc.update("update user_game set status = 'FINISHED' where appid = -119388");
|
||||
jdbc.update("insert into reward_ledger (user_id, kind, ref, points, event_at) values (?, 'FINISHED', '-119388', 50, now())",
|
||||
USER_ID);
|
||||
int logBefore = jdbc.queryForObject("select count(*) from user_game_status_change where appid = -119388", Integer.class);
|
||||
remove(-119388);
|
||||
|
||||
restore(-119388).andExpect(status().isNoContent());
|
||||
|
||||
assertEquals(1, jdbc.queryForObject("select count(*) from reward_ledger where user_id = ? and kind = 'FINISHED' "
|
||||
+ "and ref = '-119388'", Integer.class, USER_ID));
|
||||
assertEquals(logBefore, jdbc.queryForObject("select count(*) from user_game_status_change where appid = -119388",
|
||||
Integer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void gameAddedByHand_ShowsUpInTheLibraryAndItsDetail() throws Exception {
|
||||
add(119388, "Switch");
|
||||
|
||||
@@ -310,7 +310,8 @@ class PsnAccountFlowTest extends AbstractMockedAdaptersTest {
|
||||
|
||||
worker.runSyncNow(1L, null, SyncTrigger.AUTO);
|
||||
|
||||
assertThat(jdbc.queryForObject("select count(*) from user_game where appid = -301", Integer.class)).isZero();
|
||||
assertThat(jdbc.queryForObject("select count(*) from user_game where appid = -301 and removed_at is null",
|
||||
Integer.class)).isZero();
|
||||
assertThat(jdbc.queryForObject("select count(*) from game_import", Integer.class)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@@ -414,14 +415,15 @@ class PsnAccountFlowTest extends AbstractMockedAdaptersTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void psnRow_CanBeDeletedByHand() throws Exception {
|
||||
void psnRow_CanBeRemovedByHand() throws Exception {
|
||||
jdbc.update("insert into steam_game (appid, name) values (-301, 'X') on conflict do nothing");
|
||||
jdbc.update("insert into user_game (user_id, appid, playtime_minutes, first_seen_at, last_seen_at, source, platform, status, "
|
||||
+ "status_set_by_user) values (1, -301, 0, now(), now(), 'PSN', 'PS5', 'BACKLOG', true)");
|
||||
|
||||
mockMvc.perform(delete("/api/library/games/-301").header(ClientHeaderFilter.CLIENT_HEADER, "test"))
|
||||
.andExpect(status().isNoContent());
|
||||
assertThat(jdbc.queryForObject("select count(*) from user_game where appid = -301", Integer.class)).isZero();
|
||||
assertThat(jdbc.queryForObject("select count(*) from user_game where appid = -301 and removed_at is not null",
|
||||
Integer.class)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
package core_service.modules.library.repo;
|
||||
|
||||
import core_service.modules.follow.repo.FollowDigestRepository;
|
||||
import core_service.shared.test.AbstractPersistenceTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.jdbc.core.ConnectionCallback;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.init.ScriptUtils;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* V46 runs on a database that already ran it once: the test seeds the old shapes and runs the file again.
|
||||
*/
|
||||
class RemovedPsnGamesMigrationTest extends AbstractPersistenceTest {
|
||||
|
||||
private static final long USER = 7L;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
private void catalog(long appid) {
|
||||
jdbc.update("insert into steam_game (appid, name) values (?, 'Game') on conflict do nothing", appid);
|
||||
}
|
||||
|
||||
private void title(long user, String key, String state, Long appid, Integer minutes) {
|
||||
jdbc.update("""
|
||||
insert into user_psn_title (user_id, psn_key, state, appid, platform, playtime_minutes, last_played_at)
|
||||
values (?, ?, ?, ?, 'PS5', ?, case when ?::int is null then null else now() end)""",
|
||||
user, key, state, appid, minutes, minutes);
|
||||
}
|
||||
|
||||
private void row(long user, long appid, String source, String status) {
|
||||
catalog(appid);
|
||||
jdbc.update("insert into user_game (user_id, appid, status, source, platform, playtime_minutes) "
|
||||
+ "values (?, ?, ?, ?, 'Switch', 5)", user, appid, status, source);
|
||||
}
|
||||
|
||||
private void migrate() throws SQLException {
|
||||
// The JdbcTemplate connection is the one of the test transaction; a pool connection would not see the seed.
|
||||
jdbc.execute((ConnectionCallback<Void>) connection -> {
|
||||
ScriptUtils.executeSqlScript(connection, new ClassPathResource("db/migration/V46__Removed_Psn_Games.sql"));
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> rows(long user) {
|
||||
return jdbc.queryForList("select * from user_game where user_id = ? order by appid", user);
|
||||
}
|
||||
|
||||
@Test
|
||||
void orphanTitles_BecomeOneRemovedPsnRowPerGame() throws SQLException {
|
||||
catalog(-10);
|
||||
title(USER, "c1", "LINKED", -10L, 30);
|
||||
title(USER, "t1", "LINKED", -10L, 45);
|
||||
|
||||
migrate();
|
||||
|
||||
assertThat(rows(USER)).singleElement().satisfies(game -> {
|
||||
assertThat(game.get("source")).isEqualTo("PSN");
|
||||
assertThat(game.get("removed_at")).isNotNull();
|
||||
assertThat(game.get("playtime_minutes")).isEqualTo(75);
|
||||
assertThat(game.get("status_set_by_user")).isEqualTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void orphanTitleWithPlaytimeIsPlaying_WithoutIsBacklog() throws SQLException {
|
||||
catalog(-10);
|
||||
catalog(-11);
|
||||
title(USER, "c1", "LINKED", -10L, 30);
|
||||
title(USER, "c2", "LINKED", -11L, 0);
|
||||
|
||||
migrate();
|
||||
|
||||
assertThat(jdbc.queryForObject("select status from user_game where appid = -10", String.class)).isEqualTo("PLAYING");
|
||||
assertThat(jdbc.queryForObject("select status from user_game where appid = -11", String.class)).isEqualTo("BACKLOG");
|
||||
}
|
||||
|
||||
@Test
|
||||
void aliasedTitle_UsesTheKeptKey() throws SQLException {
|
||||
catalog(-10);
|
||||
catalog(-20);
|
||||
jdbc.update("insert into game_alias (from_appid, to_appid) values (-10, -20)");
|
||||
title(USER, "c1", "LINKED", -10L, 30);
|
||||
|
||||
migrate();
|
||||
|
||||
assertThat(rows(USER)).extracting(game -> game.get("appid")).containsExactly(-20L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletedUser_IsSkipped() throws SQLException {
|
||||
catalog(-10);
|
||||
jdbc.update("insert into deleted_user (user_id) values (?)", USER);
|
||||
title(USER, "c1", "LINKED", -10L, 30);
|
||||
|
||||
migrate();
|
||||
|
||||
assertThat(rows(USER)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void titlesThatAreNotLinked_OrHaveNoCatalogEntry_AddNothing() throws SQLException {
|
||||
catalog(-10);
|
||||
title(USER, "c1", "DISMISSED", null, 30);
|
||||
title(USER, "c2", "STEAM_OWNED", -10L, 30);
|
||||
title(USER, "c3", "LINKED", -99L, 30);
|
||||
|
||||
migrate();
|
||||
|
||||
assertThat(rows(USER)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingPsnOrSteamRow_IsUntouched() throws SQLException {
|
||||
row(USER, -10, "PSN", "FINISHED");
|
||||
row(USER, -11, "STEAM", "BACKLOG");
|
||||
title(USER, "c1", "LINKED", -10L, 30);
|
||||
title(USER, "c2", "LINKED", -11L, 30);
|
||||
|
||||
migrate();
|
||||
|
||||
assertThat(rows(USER)).hasSize(2).allSatisfy(game -> {
|
||||
assertThat(game.get("removed_at")).isNull();
|
||||
assertThat(game.get("playtime_minutes")).isEqualTo(5);
|
||||
});
|
||||
assertThat(jdbc.queryForObject("select status from user_game where appid = -10", String.class)).isEqualTo("FINISHED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void removedRow_GetsNoBacklogOrder() throws SQLException {
|
||||
catalog(-10);
|
||||
title(USER, "c1", "LINKED", -10L, 0);
|
||||
|
||||
migrate();
|
||||
|
||||
assertThat(jdbc.queryForObject("select count(*) from backlog_order where user_id = ?", Integer.class, USER)).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void reAddedManualRow_BecomesPsnAndKeepsUserData() throws SQLException {
|
||||
row(USER, -10, "MANUAL", "FINISHED");
|
||||
row(USER, -11, "MANUAL", "BACKLOG");
|
||||
jdbc.update("update user_game set rating = 4, notes = 'great', priority = 'HIGH' where appid = -10");
|
||||
jdbc.update("insert into user_game_split (user_id, appid) values (?, -10)", USER);
|
||||
title(USER, "c1", "LINKED", -10L, 120);
|
||||
int logBefore = jdbc.queryForObject("select count(*) from user_game_status_change where appid = -10", Integer.class);
|
||||
|
||||
migrate();
|
||||
|
||||
Map<String, Object> game = jdbc.queryForMap("select * from user_game where user_id = ? and appid = -10", USER);
|
||||
assertThat(game).containsEntry("source", "PSN").containsEntry("playtime_minutes", 120)
|
||||
.containsEntry("status", "FINISHED").containsEntry("rating", 4).containsEntry("notes", "great")
|
||||
.containsEntry("priority", "HIGH").containsEntry("platform", "Switch");
|
||||
assertThat(game.get("last_played_at")).isNotNull();
|
||||
assertThat(rows(USER)).hasSize(2);
|
||||
assertThat(jdbc.queryForObject("select count(*) from user_game_split where appid = -10", Integer.class)).isEqualTo(1);
|
||||
assertThat(jdbc.queryForObject("select count(*) from user_game_status_change where appid = -10", Integer.class))
|
||||
.isEqualTo(logBefore);
|
||||
assertThat(jdbc.queryForObject("select source from user_game where appid = -11", String.class)).isEqualTo("MANUAL");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reAddedManualRowOnAliasedKey_BecomesPsn() throws SQLException {
|
||||
row(USER, -20, "MANUAL", "BACKLOG");
|
||||
jdbc.update("insert into game_alias (from_appid, to_appid) values (-10, -20)");
|
||||
title(USER, "c1", "LINKED", -10L, 60);
|
||||
|
||||
migrate();
|
||||
|
||||
assertThat(rows(USER)).singleElement().satisfies(game -> {
|
||||
assertThat(game.get("source")).isEqualTo("PSN");
|
||||
assertThat(game.get("playtime_minutes")).isEqualTo(60);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualRowOfAnotherUserOrWithoutLinkedTitle_IsUntouched() throws SQLException {
|
||||
row(USER, -10, "MANUAL", "BACKLOG");
|
||||
row(USER, -11, "MANUAL", "BACKLOG");
|
||||
row(USER, -12, "MANUAL", "BACKLOG");
|
||||
title(USER + 1, "c1", "LINKED", -10L, 60);
|
||||
title(USER, "c2", "DISMISSED", null, 60);
|
||||
title(USER, "c3", "STEAM_OWNED", -12L, 60);
|
||||
|
||||
migrate();
|
||||
|
||||
assertThat(rows(USER)).extracting(game -> game.get("source")).containsOnly("MANUAL");
|
||||
}
|
||||
|
||||
@Test
|
||||
void runTwice_ChangesNothing() throws SQLException {
|
||||
row(USER, -11, "MANUAL", "BACKLOG");
|
||||
catalog(-10);
|
||||
title(USER, "c1", "LINKED", -10L, 30);
|
||||
title(USER, "c2", "LINKED", -11L, 30);
|
||||
migrate();
|
||||
List<Map<String, Object>> once = rows(USER);
|
||||
|
||||
migrate();
|
||||
|
||||
assertThat(rows(USER)).isEqualTo(once);
|
||||
}
|
||||
|
||||
@Test
|
||||
void migratedOrphan_IsNoNewGameInTheFollowDigest() throws SQLException {
|
||||
catalog(-10);
|
||||
title(USER, "c1", "LINKED", -10L, 30);
|
||||
|
||||
migrate();
|
||||
|
||||
assertThat(new FollowDigestRepository(new NamedParameterJdbcTemplate(jdbc)).countHand(USER, 0, Long.MAX_VALUE).newGames())
|
||||
.isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void migratedOrphanInASet_KeepsTheAutoStatusOfTheListedSteamRow() throws SQLException {
|
||||
row(USER, 500, "STEAM", "BACKLOG");
|
||||
catalog(-10);
|
||||
jdbc.update("insert into game_link (appid, set_id) values (500, 1), (-10, 1)");
|
||||
title(USER, "c1", "LINKED", -10L, 30);
|
||||
|
||||
migrate();
|
||||
|
||||
assertThat(jdbc.queryForObject("select status_set_by_user from user_game where user_id = ? and appid = 500",
|
||||
Boolean.class, USER)).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -189,6 +189,16 @@ class SyncChangeRepositoryTest extends AbstractPersistenceTest {
|
||||
assertThat(rows()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void find_LeavesOutTheOtherChangesOfARemovedGame() {
|
||||
claim(USER_ID, FIRST_RUN);
|
||||
repository.replace(USER_ID, FIRST_RUN, SyncTrigger.AUTO, 0,
|
||||
List.of(SyncChange.added(HALF_LIFE), SyncChange.added(REMOVED), SyncChange.removed(REMOVED)));
|
||||
|
||||
assertThat(stored().changes()).extracting(e -> e.change())
|
||||
.containsExactlyInAnyOrder(SyncChange.added(HALF_LIFE), SyncChange.removed(REMOVED));
|
||||
}
|
||||
|
||||
@Test
|
||||
void findStatusMovesOfThisTransaction_ReturnsOnlyMovesOfTheSync() {
|
||||
// Same transaction as the seed: the inserts (from null) are in the log with this now() too.
|
||||
|
||||
@@ -2,6 +2,7 @@ package core_service.modules.library.repo;
|
||||
|
||||
import core_service.modules.library.domain.GamePriority;
|
||||
import core_service.modules.library.domain.GameSource;
|
||||
import core_service.modules.library.domain.RemovedGame;
|
||||
import core_service.modules.library.domain.GameTimeToBeat;
|
||||
import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.Mood;
|
||||
@@ -93,6 +94,25 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
.map(UserGame::getAppid).getContent();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findUserRemoved_OrdersNewestFirstAndSkipsSteamAndOtherUsers() {
|
||||
Instant now = Instant.now();
|
||||
removed(USER_ID, 220L, GameSource.MANUAL, now.minusSeconds(60));
|
||||
removed(USER_ID, 380L, GameSource.PSN, now);
|
||||
removed(USER_ID, 620L, GameSource.STEAM, now);
|
||||
removed(2L, 220L, GameSource.PSN, now);
|
||||
userGameRepository.flush();
|
||||
entityManager.clear();
|
||||
|
||||
assertEquals(List.of(380L, 220L), userGameRepository.findUserRemoved(USER_ID).stream().map(RemovedGame::appid).toList());
|
||||
}
|
||||
|
||||
private void removed(long userId, long appid, GameSource source, Instant at) {
|
||||
UserGame game = userGameRepository.findById(new UserGameId(userId, appid)).orElseThrow();
|
||||
game.setSource(source);
|
||||
game.setRemovedAt(at);
|
||||
}
|
||||
|
||||
@Test
|
||||
void playFromPileIfUntouched_OnlyUntouchedListedBacklogGames() {
|
||||
userGameRepository.findById(new UserGameId(USER_ID, 220L)).orElseThrow().setStatus(GameStatus.BACKLOG);
|
||||
|
||||
@@ -7,6 +7,7 @@ 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.GameMergeRepository;
|
||||
import core_service.modules.library.repo.PsnTitleRepository;
|
||||
import core_service.modules.library.repo.SteamGameRepository;
|
||||
import core_service.modules.library.repo.UserGameRepository;
|
||||
import core_service.shared.test.AbstractUnitTest;
|
||||
@@ -61,6 +62,9 @@ class ManualGameServiceTest extends AbstractUnitTest {
|
||||
@Mock
|
||||
private ReleaseDateService releaseDateService;
|
||||
|
||||
@Mock
|
||||
private PsnTitleRepository psnTitleRepository;
|
||||
|
||||
@Mock
|
||||
private PlatformTransactionManager transactionManager;
|
||||
|
||||
@@ -69,7 +73,7 @@ class ManualGameServiceTest extends AbstractUnitTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new ManualGameService(igdbApiService, userGameRepository, steamGameRepository, mergeRepository, timeToBeatService,
|
||||
moodService, gameEditionService, gameCreditService, releaseDateService, transactionManager);
|
||||
moodService, gameEditionService, gameCreditService, releaseDateService, psnTitleRepository, transactionManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in new issue
Block a user