Public Access
feat(library): show play per platform on game detail
Store PSN play per title. A PS title on a Steam row keeps its minutes. Steam rtime_last_played now fills last played. Dropped? questions read the newest PSN last played too.
This commit is contained in:
1 parent
413381d404
commit
1ff1e061c7
30 files changed
+707
-33
No files matched your search
@@ -80,7 +80,7 @@ class GameLinkFlowTest extends AbstractIntegrationTest {
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
for (String table : List.of("game_link_request", "game_link_pair", "game_link", "game_edition", "admin_audit")) {
|
||||
for (String table : List.of("user_psn_title", "game_link_request", "game_link_pair", "game_link", "game_edition", "admin_audit")) {
|
||||
jdbc.update("delete from " + table);
|
||||
}
|
||||
userGameRepository.deleteAll();
|
||||
@@ -293,6 +293,19 @@ class GameLinkFlowTest extends AbstractIntegrationTest {
|
||||
.andExpect(jsonPath("$.content[0].platforms[1].platform").value("Switch"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void games_ASetRowCarriesTheTotalPlaytimeOfAllItsPlatforms() throws Exception {
|
||||
jdbc.update("update user_game set playtime_minutes = 500 where user_id = ? and appid = ?", USER_A, GAME);
|
||||
jdbc.update("update user_game set source = 'PSN', platform = 'PS5', playtime_minutes = 300 where user_id = ? and appid = ?",
|
||||
USER_A, GAME_2);
|
||||
jdbc.update("insert into user_psn_title (user_id, psn_key, state, appid, platform, playtime_minutes) "
|
||||
+ "values (?, 'c9', 'LINKED', ?, 'PS5', 300)", USER_A, GAME_2);
|
||||
linkAs(Role.AUDITOR, GAME, GAME_2).andExpect(status().isNoContent());
|
||||
|
||||
mockMvc.perform(as(get("/api/library/games"), USER_A, Role.USER)).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.content[0].totalPlaytimeMinutes").value(800));
|
||||
}
|
||||
|
||||
@Test
|
||||
void games_AnOldClientsHideDuplicatesParamChangesNothing() throws Exception {
|
||||
linkAs(Role.AUDITOR, GAME, GAME_2).andExpect(status().isNoContent());
|
||||
|
||||
+1
-1
@@ -217,7 +217,7 @@ class LibraryControllerAchievementsTest extends AbstractMockedAdaptersTest {
|
||||
@Test
|
||||
void librarySync_StartsTheAchievementSyncAfterCommit() {
|
||||
when(steamApiService.getOwnedGames("76561198000000001"))
|
||||
.thenReturn(List.of(new SteamOwnedGamesResponse.Game(620, "Portal 2", 90, null, true, 0)));
|
||||
.thenReturn(List.of(new SteamOwnedGamesResponse.Game(620, "Portal 2", 90, null, true, 0, null)));
|
||||
when(steamApiService.getWishlist(anyString())).thenReturn(Optional.of(List.of()));
|
||||
when(steamApiService.getSchemaForGame(anyLong())).thenReturn(Optional.empty());
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class SyncChangesFlowTest extends AbstractMockedAdaptersTest {
|
||||
}
|
||||
|
||||
private static SteamOwnedGamesResponse.Game owned(int appid, int playtime) {
|
||||
return new SteamOwnedGamesResponse.Game(appid, "Game " + appid, playtime, "icon", true, playtime);
|
||||
return new SteamOwnedGamesResponse.Game(appid, "Game " + appid, playtime, "icon", true, playtime, null);
|
||||
}
|
||||
|
||||
private void sync(SyncTrigger trigger, SteamOwnedGamesResponse.Game... games) {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package core_service.modules.library.domain;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PlatformPlayTest {
|
||||
|
||||
private static final long USER = 1L;
|
||||
private static final Instant STEAM_PLAYED = Instant.parse("2026-10-01T10:00:00Z");
|
||||
private static final Instant PSN_PLAYED = Instant.parse("2026-10-05T18:00:00Z");
|
||||
|
||||
private static UserGame row(long appid, GameSource source, String platform, int minutes, Instant lastPlayedAt) {
|
||||
UserGame game = UserGame.manual(USER, appid, source, platform, GameStatus.PLAYING, minutes, Instant.EPOCH);
|
||||
game.setLastPlayedAt(lastPlayedAt);
|
||||
return game;
|
||||
}
|
||||
|
||||
private static TitlePlay title(String key, long appid, String platform, int minutes) {
|
||||
return new TitlePlay(key, appid, platform, minutes, PSN_PLAYED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lines_ASteamRowWithAnOwnedPlayStationTitleIsTwoLines_AndTheTotalIsTheirSum() {
|
||||
List<PlatformPlay> lines = PlatformPlay.lines(List.of(row(100, GameSource.STEAM, null, 500, STEAM_PLAYED)),
|
||||
List.of(title("c1", 100, "PS5", 120)));
|
||||
|
||||
assertThat(lines).containsExactly(
|
||||
new PlatformPlay(GameSource.STEAM, null, 100, 500, STEAM_PLAYED),
|
||||
new PlatformPlay(GameSource.PSN, "PS5", 100, 120, PSN_PLAYED));
|
||||
assertThat(PlatformPlay.total(lines)).isEqualTo(620);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lines_APlayStationRowWithTwoTitlesIsTheTitleLines_TheRowSumIsNotAddedOnTop() {
|
||||
List<PlatformPlay> lines = PlatformPlay.lines(List.of(row(200, GameSource.PSN, "PS5", 300, PSN_PLAYED)),
|
||||
List.of(title("c4", 200, "PS4", 100), title("c5", 200, "PS5", 200)));
|
||||
|
||||
assertThat(lines).extracting(PlatformPlay::platform).containsExactly("PS5", "PS4");
|
||||
assertThat(lines).extracting(PlatformPlay::playtimeMinutes).containsExactly(200, 100);
|
||||
assertThat(PlatformPlay.total(lines)).isEqualTo(300);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lines_APlayStationRowWithoutStoredTitlePlayIsOneLineFromTheRow() {
|
||||
List<PlatformPlay> lines = PlatformPlay.lines(List.of(row(200, GameSource.PSN, "PS5", 300, PSN_PLAYED)), List.of());
|
||||
|
||||
assertThat(lines).containsExactly(new PlatformPlay(GameSource.PSN, "PS5", 200, 300, PSN_PLAYED));
|
||||
assertThat(PlatformPlay.total(lines)).isEqualTo(300);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lines_AManualRowWithALinkedTitleIsTheRowLineAndTheTitleLine() {
|
||||
List<PlatformPlay> lines = PlatformPlay.lines(List.of(row(-7, GameSource.MANUAL, "Switch", 60, null)),
|
||||
List.of(title("c1", -7, "PS4", 30)));
|
||||
|
||||
assertThat(lines).containsExactly(
|
||||
new PlatformPlay(GameSource.PSN, "PS4", -7, 30, PSN_PLAYED),
|
||||
new PlatformPlay(GameSource.MANUAL, "Switch", -7, 60, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void lines_ARowTheSteamSyncTookOverIsASteamLineAndTheTitleLine() {
|
||||
List<PlatformPlay> lines = PlatformPlay.lines(List.of(row(100, GameSource.STEAM, null, 40, STEAM_PLAYED)),
|
||||
List.of(title("c1", 100, "PS5", 900)));
|
||||
|
||||
assertThat(lines).extracting(PlatformPlay::source).containsExactly(GameSource.STEAM, GameSource.PSN);
|
||||
assertThat(PlatformPlay.total(lines)).isEqualTo(940);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lines_OrderIsSteamThenPs5ThenPs4ThenManual_AndTitlesOfOtherRowsAreIgnored() {
|
||||
List<PlatformPlay> lines = PlatformPlay.lines(List.of(
|
||||
row(-7, GameSource.MANUAL, "Switch", 60, null),
|
||||
row(200, GameSource.PSN, "PS4", 10, null),
|
||||
row(100, GameSource.STEAM, null, 500, STEAM_PLAYED)),
|
||||
List.of(title("c1", 200, "PS4", 10), title("c2", 100, "PS5", 20), title("c3", 999, "PS5", 7777)));
|
||||
|
||||
assertThat(lines).extracting(l -> l.source() + ":" + l.platform())
|
||||
.containsExactly("STEAM:null", "PSN:PS5", "PSN:PS4", "MANUAL:Switch");
|
||||
}
|
||||
|
||||
@Test
|
||||
void total_IsCappedAtIntegerMax() {
|
||||
List<PlatformPlay> lines = List.of(new PlatformPlay(GameSource.STEAM, null, 1, Integer.MAX_VALUE, null),
|
||||
new PlatformPlay(GameSource.PSN, "PS5", 1, 5, null));
|
||||
|
||||
assertThat(PlatformPlay.total(lines)).isEqualTo(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lines_AManualRowWithALinkedTitleOfItsPlatformIsOneLine() {
|
||||
List<PlatformPlay> lines = PlatformPlay.lines(List.of(row(-7, GameSource.MANUAL, "PS5", 1200, null)),
|
||||
List.of(title("c1", -7, "PS5", 1300)));
|
||||
|
||||
assertThat(lines).containsExactly(new PlatformPlay(GameSource.PSN, "PS5", -7, 1300, PSN_PLAYED));
|
||||
assertThat(PlatformPlay.total(lines)).isEqualTo(1300);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
package core_service.modules.library.repo;
|
||||
|
||||
import core_service.modules.library.domain.Edition;
|
||||
import core_service.modules.library.domain.EntryPlatform;
|
||||
import core_service.modules.library.domain.GameSource;
|
||||
import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.LinkPair;
|
||||
import core_service.modules.library.domain.PlatformPlay;
|
||||
import core_service.modules.library.domain.TitlePlay;
|
||||
import core_service.modules.library.domain.UserGame;
|
||||
import core_service.shared.test.AbstractPersistenceTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -10,6 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
@@ -259,6 +265,81 @@ class GameLinkRepositoryTest extends AbstractPersistenceTest {
|
||||
USER, appid, source, platform);
|
||||
}
|
||||
|
||||
private void playtime(long appid, int minutes) {
|
||||
jdbc.update("update user_game set playtime_minutes = ? where user_id = ? and appid = ?", minutes, USER, appid);
|
||||
}
|
||||
|
||||
private void titlePlay(String key, long appid, String state, String platform, int minutes) {
|
||||
jdbc.update("insert into user_psn_title (user_id, psn_key, state, appid, platform, playtime_minutes) "
|
||||
+ "values (?, ?, ?, ?, ?, ?)", USER, key, state, appid, platform, minutes);
|
||||
}
|
||||
|
||||
private static UserGame row(long appid, GameSource source, String platform, int minutes) {
|
||||
return UserGame.manual(USER, appid, source, platform, GameStatus.BACKLOG, minutes, Instant.EPOCH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void entryPlaytimes_ASteamAndAnOwnedTitleSet_EqualsThePlatformPlayTotal() {
|
||||
ownOn(100, "STEAM", null);
|
||||
playtime(100, 500);
|
||||
titlePlay("c1", 100, "STEAM_OWNED", "PS5", 120);
|
||||
|
||||
var playtimes = repository.entryPlaytimes(USER, List.of(100L));
|
||||
|
||||
int expected = PlatformPlay.total(PlatformPlay.lines(List.of(row(100, GameSource.STEAM, null, 500)),
|
||||
List.of(new TitlePlay("c1", 100L, "PS5", 120, null))));
|
||||
assertThat(expected).isEqualTo(620);
|
||||
assertThat(playtimes).containsOnly(Map.entry(100L, expected));
|
||||
}
|
||||
|
||||
@Test
|
||||
void entryPlaytimes_ASteamAndAPlayStationRowSet_EqualsThePlatformPlayTotal_TheRowSumIsNotAddedOnTop() {
|
||||
repository.link(100, 200);
|
||||
ownOn(100, "STEAM", null);
|
||||
ownOn(200, "PSN", "PS5");
|
||||
playtime(100, 500);
|
||||
playtime(200, 300);
|
||||
titlePlay("c4", 200, "LINKED", "PS4", 100);
|
||||
titlePlay("c5", 200, "LINKED", "PS5", 200);
|
||||
// A title without stored play counts nothing.
|
||||
jdbc.update("insert into user_psn_title (user_id, psn_key, state, appid) values (?, 'c6', 'LINKED', 200)", USER);
|
||||
|
||||
var playtimes = repository.entryPlaytimes(USER, List.of(100L, 200L));
|
||||
|
||||
int expected = PlatformPlay.total(PlatformPlay.lines(
|
||||
List.of(row(100, GameSource.STEAM, null, 500), row(200, GameSource.PSN, "PS5", 300)),
|
||||
List.of(new TitlePlay("c4", 200L, "PS4", 100, null), new TitlePlay("c5", 200L, "PS5", 200, null))));
|
||||
assertThat(expected).isEqualTo(800);
|
||||
assertThat(playtimes.values()).containsExactly(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void entryPlaytimes_AManualRowWithATitleOfItsPlatform_EqualsThePlatformPlayTotal() {
|
||||
repository.link(100, -7);
|
||||
ownOn(100, "STEAM", null);
|
||||
ownOn(-7, "MANUAL", "PS5");
|
||||
playtime(100, 500);
|
||||
playtime(-7, 1200);
|
||||
titlePlay("c1", -7, "LINKED", "PS5", 1300);
|
||||
titlePlay("c2", -7, "LINKED", "PS4", 60);
|
||||
|
||||
int expected = PlatformPlay.total(PlatformPlay.lines(
|
||||
List.of(row(100, GameSource.STEAM, null, 500), row(-7, GameSource.MANUAL, "PS5", 1200)),
|
||||
List.of(new TitlePlay("c1", -7L, "PS5", 1300, null), new TitlePlay("c2", -7L, "PS4", 60, null))));
|
||||
|
||||
assertThat(expected).isEqualTo(1860);
|
||||
assertThat(repository.entryPlaytimes(USER, List.of(100L, -7L)).values()).containsExactly(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void entryPlaytimes_APlayStationRowWithoutTitlePlay_IsItsOwnMinutes() {
|
||||
ownOn(200, "PSN", "PS5");
|
||||
playtime(200, 300);
|
||||
|
||||
assertThat(repository.entryPlaytimes(USER, List.of(200L))).containsOnly(Map.entry(200L, 300));
|
||||
assertThat(repository.entryPlaytimes(USER, List.of())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void entryPlatforms_SteamFirstThenPlayStationThenManual_AndAPlayStationTitleWithoutNameIsDropped() {
|
||||
repository.link(100, 200);
|
||||
@@ -367,4 +448,17 @@ class GameLinkRepositoryTest extends AbstractPersistenceTest {
|
||||
assertThat(repository.editionCounts(USER, List.of(100L, 200L, -7L))).isEqualTo(Map.of(100L, 2, 200L, 2));
|
||||
assertThat(repository.editionCounts(OTHER_USER, List.of(100L, -7L))).isEqualTo(Map.of(100L, 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void entryPlatforms_ATitleOnAMergedKeyStillGivesThePlayStationIcon() {
|
||||
jdbc.update("insert into steam_game (appid, name) values (-9, 'Old key') on conflict do nothing");
|
||||
jdbc.update("insert into game_alias (from_appid, to_appid) values (-9, 100) on conflict do nothing");
|
||||
ownOn(100, "STEAM", null);
|
||||
playtime(100, 500);
|
||||
titlePlay("c1", -9, "STEAM_OWNED", "PS5", 120);
|
||||
|
||||
assertThat(repository.entryPlaytimes(USER, List.of(100L))).containsOnly(Map.entry(100L, 620));
|
||||
assertThat(repository.entryPlatforms(USER, List.of(100L)).get(100L)).extracting(EntryPlatform::source)
|
||||
.contains(GameSource.PSN);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
package core_service.modules.library.repo;
|
||||
|
||||
import core_service.modules.library.domain.PsnTitle;
|
||||
import core_service.modules.library.domain.TitlePlay;
|
||||
import core_service.shared.test.AbstractPersistenceTest;
|
||||
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.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.tuple;
|
||||
|
||||
@Import(PsnTitleRepository.class)
|
||||
class PsnTitleRepositoryTest extends AbstractPersistenceTest {
|
||||
@@ -72,4 +75,42 @@ class PsnTitleRepositoryTest extends AbstractPersistenceTest {
|
||||
assertThat(repository.findAll(3L).get("c1").appid()).isEqualTo(-1L);
|
||||
assertThat(repository.findAll(5L)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void savePlays_UpdatesLinkedAndSteamOwnedTitles_NotADismissedOne() {
|
||||
Instant played = Instant.parse("2026-10-05T18:00:00Z");
|
||||
repository.link(6L, "c1", -1L);
|
||||
repository.linkOwned(6L, "c2", -2L);
|
||||
repository.dismiss(6L, List.of("c3"));
|
||||
|
||||
repository.savePlays(6L, List.of(new TitlePlay("c1", null, "PS5", 120, played),
|
||||
new TitlePlay("c2", null, "PS4", 30, null), new TitlePlay("c3", null, "PS5", 999, played)));
|
||||
|
||||
assertThat(jdbc.queryForObject("select count(*) from user_psn_title where user_id = 6 and playtime_minutes is not null",
|
||||
Integer.class)).isEqualTo(2);
|
||||
assertThat(jdbc.queryForObject("select playtime_minutes from user_psn_title where user_id = 6 and psn_key = 'c1'",
|
||||
Integer.class)).isEqualTo(120);
|
||||
assertThat(jdbc.queryForObject("select platform from user_psn_title where user_id = 6 and psn_key = 'c2'",
|
||||
String.class)).isEqualTo("PS4");
|
||||
assertThat(jdbc.queryForObject("select playtime_minutes from user_psn_title where user_id = 6 and psn_key = 'c3'",
|
||||
Integer.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findPlays_MapsAMergedKeyThroughTheAlias_AndLeavesOutTitlesWithoutPlay() {
|
||||
game(7L, -1L, "STEAM");
|
||||
jdbc.update("insert into steam_game (appid, name) values (-9, 'Old') on conflict do nothing");
|
||||
jdbc.update("insert into game_alias (from_appid, to_appid) values (-9, -1) on conflict do nothing");
|
||||
repository.link(7L, "c1", -9L);
|
||||
repository.linkOwned(7L, "c2", -1L);
|
||||
repository.link(7L, "c3", -1L);
|
||||
repository.savePlays(7L, List.of(new TitlePlay("c1", null, "PS5", 120, null),
|
||||
new TitlePlay("c2", null, "PS4", 30, null)));
|
||||
|
||||
List<TitlePlay> plays = repository.findPlays(7L, List.of(-1L));
|
||||
|
||||
assertThat(plays).extracting(TitlePlay::psnKey, TitlePlay::appid, TitlePlay::platform, TitlePlay::playtimeMinutes)
|
||||
.containsExactlyInAnyOrder(tuple("c1", -1L, "PS5", 120), tuple("c2", -1L, "PS4", 30));
|
||||
assertThat(repository.findPlays(7L, List.of())).isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -155,4 +155,13 @@ class QuestionRepositoryTest extends AbstractPersistenceTest {
|
||||
jdbc.update("delete from user_game where appid = 50");
|
||||
assertThat(jdbc.queryForObject("select count(*) from user_game_question", Integer.class)).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void facts_ThePlayStationLastPlayedOfATitleOnASteamRowCounts() {
|
||||
jdbc.update("delete from user_psn_title");
|
||||
jdbc.update("insert into user_psn_title (user_id, psn_key, state, appid, platform, playtime_minutes, last_played_at) "
|
||||
+ "values (?, 'c1', 'STEAM_OWNED', 10, 'PS5', 600, '2026-10-08T20:00:00Z')", USER);
|
||||
|
||||
assertThat(only(10).lastPlayedAt()).isEqualTo(java.time.Instant.parse("2026-10-08T20:00:00Z"));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package core_service.modules.library.service;
|
||||
|
||||
import core_service.modules.library.api.dto.AchievementDto;
|
||||
import core_service.modules.library.api.dto.GameDetailDto;
|
||||
import core_service.modules.library.api.dto.PlatformPlayDto;
|
||||
import core_service.modules.library.domain.AchievementKind;
|
||||
import core_service.modules.library.domain.PsnEarned;
|
||||
import core_service.modules.library.domain.PsnTrophy;
|
||||
@@ -17,6 +18,7 @@ import core_service.modules.library.domain.UserGame;
|
||||
import core_service.modules.library.domain.UserGameAchievementMeta;
|
||||
import core_service.modules.library.domain.UserGameId;
|
||||
import core_service.modules.library.repo.GameAchievementSchemaMetaRepository;
|
||||
import core_service.modules.library.repo.GameLinkRepository;
|
||||
import core_service.modules.library.repo.SteamGameRepository;
|
||||
import core_service.modules.library.repo.GameAchievementSchemaRepository;
|
||||
import core_service.modules.library.repo.GameSpeedrunRepository;
|
||||
@@ -34,6 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
@@ -86,6 +89,9 @@ class GameDetailServiceTest extends AbstractMockedAdaptersTest {
|
||||
@Autowired
|
||||
private PsnTrophyRepository psnTrophyRepository;
|
||||
|
||||
@Autowired
|
||||
private GameLinkRepository gameLinkRepository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@@ -99,6 +105,8 @@ class GameDetailServiceTest extends AbstractMockedAdaptersTest {
|
||||
gameSpeedrunRepository.deleteAll();
|
||||
gameTimeToBeatRepository.deleteAll();
|
||||
gameMoodsRepository.deleteAll();
|
||||
jdbc.update("delete from user_psn_title");
|
||||
jdbc.update("delete from game_link");
|
||||
jdbc.update("delete from user_psn_trophy");
|
||||
jdbc.update("delete from user_psn_trophy_set");
|
||||
jdbc.update("delete from psn_trophy");
|
||||
@@ -292,4 +300,44 @@ class GameDetailServiceTest extends AbstractMockedAdaptersTest {
|
||||
assertEquals(4, detail.achievements().size());
|
||||
assertEquals(1, detail.achievementsTotal());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getGameDetail_ASteamAndAPlayStationEdition_HasOneLinePerPlatformWithItsOwnPlayAndProgress() {
|
||||
Instant steamPlayed = Instant.parse("2026-10-01T10:00:00Z");
|
||||
Instant psnPlayed = Instant.parse("2026-10-05T18:00:00Z");
|
||||
UserGame steam = userGameRepository.findById(new UserGameId(USER_ID, 620L)).orElseThrow();
|
||||
steam.setPlaytimeMinutes(500);
|
||||
steam.setLastPlayedAt(steamPlayed);
|
||||
userGameRepository.save(steam);
|
||||
schemaMetaRepository.save(new GameAchievementSchemaMeta(620L, 3, true, Instant.now()));
|
||||
for (String name : List.of("A", "B", "C")) {
|
||||
schemaRepository.save(new GameAchievementSchema(620L, name, name, null, null, null, false));
|
||||
}
|
||||
userAchievementMetaRepository.save(new UserGameAchievementMeta(USER_ID, 620L, Instant.now(), true, 2, 60));
|
||||
steamGameRepository.save(new SteamGame(621L, "Portal 2 (PS5)", null, Instant.now()));
|
||||
userGameRepository.save(UserGame.manual(USER_ID, 621L, GameSource.PSN, "PS5", GameStatus.PLAYING, 300, Instant.now()));
|
||||
gameLinkRepository.link(620L, 621L);
|
||||
psnTrophyRepository.saveSet(USER_ID, 621L, new PsnTrophySet("NPWR9_00", "trophy2", "01.00", "Portal 2", null, "PS5",
|
||||
2, 0, 0, 1, 2, false, Instant.now()));
|
||||
jdbc.update("insert into user_psn_title (user_id, psn_key, state, appid, platform, playtime_minutes, last_played_at) "
|
||||
+ "values (?, 'c9', 'LINKED', 621, 'PS5', 300, ?)", USER_ID, Timestamp.from(psnPlayed));
|
||||
|
||||
GameDetailDto detail = gameDetailService.getGameDetail(USER_ID, null, 620L).orElseThrow();
|
||||
|
||||
assertEquals(List.of(
|
||||
new PlatformPlayDto(GameSource.STEAM, null, 620L, 500, steamPlayed, 2, 3, false),
|
||||
new PlatformPlayDto(GameSource.PSN, "PS5", 621L, 300, psnPlayed, 2, 3, false)), detail.platforms());
|
||||
assertEquals(800, detail.totalPlaytimeMinutes());
|
||||
assertEquals(500, detail.playtimeMinutes());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getGameDetail_AGameOfOnePlatform_HasOneLineAndItsOwnMinutesAsTotal() {
|
||||
GameDetailDto detail = gameDetailService.getGameDetail(USER_ID, null, 620L).orElseThrow();
|
||||
|
||||
assertEquals(1, detail.platforms().size());
|
||||
assertEquals(GameSource.STEAM, detail.platforms().getFirst().source());
|
||||
assertEquals(60, detail.platforms().getFirst().playtimeMinutes());
|
||||
assertEquals(60, detail.totalPlaytimeMinutes());
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -61,7 +61,7 @@ class LibrarySyncWorkerConcurrencyTest extends AbstractMockedAdaptersTest {
|
||||
void runSyncNow_TheFollowUpStoresTheSpeedrunRow() {
|
||||
when(steamApiService.getWishlist(STEAM_ID)).thenReturn(Optional.empty());
|
||||
when(steamApiService.getOwnedGames(STEAM_ID))
|
||||
.thenReturn(List.of(new SteamOwnedGamesResponse.Game((int) APPID, "Half-Life 2", 0, "icon", true, 0)));
|
||||
.thenReturn(List.of(new SteamOwnedGamesResponse.Game((int) APPID, "Half-Life 2", 0, "icon", true, 0, null)));
|
||||
when(speedrunApiService.isEnabled()).thenReturn(true);
|
||||
when(speedrunApiService.findGames("Half-Life 2")).thenReturn(Optional.of(List.of(
|
||||
new SpeedrunApiService.Game("hl2", "Half-Life 2", 2004, "https://www.speedrun.com/hl2"))));
|
||||
@@ -153,6 +153,6 @@ class LibrarySyncWorkerConcurrencyTest extends AbstractMockedAdaptersTest {
|
||||
}
|
||||
|
||||
private static SteamOwnedGamesResponse.Game game(String iconUrl) {
|
||||
return new SteamOwnedGamesResponse.Game((int) APPID, "Half-Life 2", 60, iconUrl, true, 0);
|
||||
return new SteamOwnedGamesResponse.Game((int) APPID, "Half-Life 2", 60, iconUrl, true, 0, null);
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
||||
private static final String OTHER_STEAM_ID = "76561198005679790";
|
||||
|
||||
private static SteamOwnedGamesResponse.Game game(int playtime, int playtime2Weeks) {
|
||||
return new SteamOwnedGamesResponse.Game(220, "Half-Life 2", playtime, "icon", true, playtime2Weeks);
|
||||
return new SteamOwnedGamesResponse.Game(220, "Half-Life 2", playtime, "icon", true, playtime2Weeks, null);
|
||||
}
|
||||
|
||||
private static LocalDate today() {
|
||||
@@ -297,6 +297,44 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
||||
verify(playtimeDayRepository).addMinutes(USER_ID, 220L, today(), 120);
|
||||
}
|
||||
|
||||
private static SteamOwnedGamesResponse.Game gameLastPlayed(Long rtimeLastPlayed) {
|
||||
return new SteamOwnedGamesResponse.Game(220, "Half-Life 2", 100, "icon", true, 0, rtimeLastPlayed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_StoresSteamsLastPlayed() {
|
||||
UserGame existing = owned(100);
|
||||
steamReturns(gameLastPlayed(1_760_000_000L), STEAM_ID);
|
||||
|
||||
worker.runSync(USER_ID, STEAM_ID, SyncTrigger.AUTO);
|
||||
|
||||
assertEquals(Instant.ofEpochSecond(1_760_000_000L), existing.getLastPlayedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_ZeroOrAbsentLastPlayed_KeepsTheOldValue() {
|
||||
Instant old = Instant.parse("2026-10-05T18:00:00Z");
|
||||
for (Long none : new Long[]{0L, null}) {
|
||||
UserGame existing = owned(100);
|
||||
existing.setLastPlayedAt(old);
|
||||
steamReturns(gameLastPlayed(none), STEAM_ID);
|
||||
|
||||
worker.runSync(USER_ID, STEAM_ID, SyncTrigger.AUTO);
|
||||
|
||||
assertEquals(old, existing.getLastPlayedAt());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_NewSteamGame_GetsLastPlayedFromSteam() {
|
||||
steamReturns(gameLastPlayed(1_760_000_000L), null);
|
||||
|
||||
worker.runSync(USER_ID, STEAM_ID, SyncTrigger.AUTO);
|
||||
|
||||
verify(userGameRepository).save(argThat((UserGame created) ->
|
||||
Instant.ofEpochSecond(1_760_000_000L).equals(created.getLastPlayedAt())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void runSync_PlaytimeGrew_RecordsTheDifferenceForToday() {
|
||||
owned(100);
|
||||
@@ -618,7 +656,7 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
||||
void runSync_GameWithoutName_GetsTheAppidAsName() {
|
||||
when(userSteamSyncRepository.tryClaimRun(eq(USER_ID), eq(STEAM_ID), any(), any())).thenReturn(1);
|
||||
when(steamApiService.getOwnedGames(STEAM_ID)).thenReturn(List.of(
|
||||
new SteamOwnedGamesResponse.Game(2000001, null, 12, null, null, null),
|
||||
new SteamOwnedGamesResponse.Game(2000001, null, 12, null, null, null, null),
|
||||
game(90, 0)));
|
||||
|
||||
worker.runSync(USER_ID, STEAM_ID, SyncTrigger.AUTO);
|
||||
@@ -633,7 +671,7 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
||||
when(userSteamSyncRepository.tryClaimRun(eq(USER_ID), eq(STEAM_ID), any(), any())).thenReturn(1);
|
||||
when(steamGameRepository.findAllById(List.of(2000001L))).thenReturn(List.of(stored));
|
||||
when(steamApiService.getOwnedGames(STEAM_ID)).thenReturn(List.of(
|
||||
new SteamOwnedGamesResponse.Game(2000001, " ", 12, null, null, null)));
|
||||
new SteamOwnedGamesResponse.Game(2000001, " ", 12, null, null, null, null)));
|
||||
|
||||
worker.runSync(USER_ID, STEAM_ID, SyncTrigger.AUTO);
|
||||
|
||||
@@ -673,7 +711,7 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
||||
}
|
||||
|
||||
private static SteamOwnedGamesResponse.Game gameOf(int appid, int playtime) {
|
||||
return new SteamOwnedGamesResponse.Game(appid, "Game " + appid, playtime, "icon", true, playtime);
|
||||
return new SteamOwnedGamesResponse.Game(appid, "Game " + appid, playtime, "icon", true, playtime, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -7,6 +7,7 @@ import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.IgdbGame;
|
||||
import core_service.modules.library.domain.PsnTitle;
|
||||
import core_service.modules.library.domain.SyncChange;
|
||||
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.GameImportRepository;
|
||||
@@ -195,6 +196,38 @@ class PsnSyncStepTest extends AbstractUnitTest {
|
||||
verifyNoInteractions(manualGameService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_SavesThePlayOfALinkedAndASteamOwnedTitle_AndGivesNoPlaytimeToTheSteamRow() {
|
||||
PsnApiService.Title other = new PsnApiService.Title("PPSA2_00", "Other", "ps5_native_game", Duration.ofMinutes(30), 8L, null);
|
||||
fetches(ghost(), other);
|
||||
when(psnTitleRepository.findAll(USER)).thenReturn(Map.of(
|
||||
"c7", new PsnTitle("c7", PsnTitle.State.LINKED, -1L),
|
||||
"c8", new PsnTitle("c8", PsnTitle.State.STEAM_OWNED, 100L)));
|
||||
|
||||
PsnSyncStep.Prepared prepared = step.run(USER);
|
||||
|
||||
ArgumentCaptor<List<TitlePlay>> plays = ArgumentCaptor.captor();
|
||||
verify(psnTitleRepository).savePlays(eq(USER), plays.capture());
|
||||
assertThat(plays.getValue()).containsExactlyInAnyOrder(new TitlePlay("c7", null, "PS5", 120, LAST_PLAYED),
|
||||
new TitlePlay("c8", null, "PS5", 30, null));
|
||||
assertThat(prepared.playtimes()).extracting(PsnSyncStep.Playtime::appid).containsExactly(-1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_ATitleLinkedInThisRunIsAlsoInTheSavedPlay() {
|
||||
fetches(ghost());
|
||||
IgdbGame game = igdb(1, "Ghost of Yotei", "PS5");
|
||||
when(manualGameService.findGames("Ghost of Yotei")).thenReturn(Optional.of(List.of(game)));
|
||||
when(manualGameService.addGame(eq(USER), eq(game), eq("PS5"), eq(GameStatus.PLAYING), eq(120),
|
||||
eq(GameSource.PSN), isNull())).thenReturn(new ManualGameService.AddResult.Added(-1L));
|
||||
|
||||
step.run(USER);
|
||||
|
||||
ArgumentCaptor<List<TitlePlay>> plays = ArgumentCaptor.captor();
|
||||
verify(psnTitleRepository).savePlays(eq(USER), plays.capture());
|
||||
assertThat(plays.getValue()).containsExactly(new TitlePlay("c7", null, "PS5", 120, LAST_PLAYED));
|
||||
}
|
||||
|
||||
@Test
|
||||
void run_DismissedTitleAndTitleOfAnOpenImport_AreNotSearchedNorOffered() {
|
||||
PsnApiService.Title other = new PsnApiService.Title("PPSA2_00", "Other", "ps5_native_game", Duration.ZERO, 8L, null);
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@ class SteamApiServiceCacheTest extends AbstractIntegrationTest {
|
||||
@Test
|
||||
void getOwnedGames_TransientSteamError_IsTriedAgain() {
|
||||
SteamOwnedGamesResponse games = new SteamOwnedGamesResponse(new SteamOwnedGamesResponse.OwnedGamesResponse(
|
||||
List.of(new SteamOwnedGamesResponse.Game(220, "Half-Life 2", 60, null, true, 0))));
|
||||
List.of(new SteamOwnedGamesResponse.Game(220, "Half-Life 2", 60, null, true, 0, null))));
|
||||
when(steamRestClient.get().uri(any(Function.class)).retrieve().body(SteamOwnedGamesResponse.class))
|
||||
.thenAnswer(invocation -> {
|
||||
if (steamCalls.incrementAndGet() == 1) {
|
||||
|
||||
@@ -88,7 +88,8 @@ class SteamApiServiceTest {
|
||||
"name": "Counter-Strike",
|
||||
"playtime_forever": 32,
|
||||
"img_icon_url": "icon_url",
|
||||
"has_community_visible_stats": true
|
||||
"has_community_visible_stats": true,
|
||||
"rtime_last_played": 1760000000
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -104,6 +105,7 @@ class SteamApiServiceTest {
|
||||
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.getFirst().name()).isEqualTo("Counter-Strike");
|
||||
assertThat(result.getFirst().rtimeLastPlayed()).isEqualTo(1_760_000_000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in new issue
Block a user