Public Access
feat(library): finish games from the ending achievement or suggest it
This commit is contained in:
1 parent
c2d17d0bc7
commit
b959542e23
31 files changed
+1072
-34
No files matched your search
+205
@@ -0,0 +1,205 @@
|
||||
package core_service.modules.library.api;
|
||||
|
||||
import core_service.configuration.security.ClientHeaderFilter;
|
||||
import core_service.modules.library.domain.FinishedHintType;
|
||||
import core_service.modules.library.domain.GameAchievementSchema;
|
||||
import core_service.modules.library.domain.GameAchievementSchemaMeta;
|
||||
import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.GameTimeToBeat;
|
||||
import core_service.modules.library.domain.SteamGame;
|
||||
import core_service.modules.library.domain.UserGame;
|
||||
import core_service.modules.library.domain.UserGameAchievement;
|
||||
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.GameAchievementSchemaRepository;
|
||||
import core_service.modules.library.repo.GameMoodsRepository;
|
||||
import core_service.modules.library.repo.GameTimeToBeatRepository;
|
||||
import core_service.modules.library.repo.SteamGameRepository;
|
||||
import core_service.modules.library.repo.UserGameAchievementMetaRepository;
|
||||
import core_service.modules.library.repo.UserGameAchievementRepository;
|
||||
import core_service.modules.library.repo.UserGameRepository;
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* The dev profile authenticates every request as user 1. Fresh schemas, so the detail page asks Steam nothing.
|
||||
*/
|
||||
@AutoConfigureMockMvc
|
||||
class LibraryControllerFinishedHintTest extends AbstractIntegrationTest {
|
||||
|
||||
private static final long USER_ID = 1L;
|
||||
private static final long ENDING_GAME = 322110L;
|
||||
private static final long MANUAL_GAME = -1942L;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private SteamGameRepository steamGameRepository;
|
||||
@Autowired
|
||||
private UserGameRepository userGameRepository;
|
||||
@Autowired
|
||||
private GameAchievementSchemaRepository schemaRepository;
|
||||
@Autowired
|
||||
private GameAchievementSchemaMetaRepository schemaMetaRepository;
|
||||
@Autowired
|
||||
private UserGameAchievementRepository userAchievementRepository;
|
||||
@Autowired
|
||||
private UserGameAchievementMetaRepository userAchievementMetaRepository;
|
||||
@Autowired
|
||||
private GameTimeToBeatRepository gameTimeToBeatRepository;
|
||||
@Autowired
|
||||
private GameMoodsRepository gameMoodsRepository;
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
userGameRepository.deleteAll();
|
||||
schemaRepository.deleteAll();
|
||||
schemaMetaRepository.deleteAll();
|
||||
userAchievementRepository.deleteAll();
|
||||
userAchievementMetaRepository.deleteAll();
|
||||
steamGameRepository.save(new SteamGame(ENDING_GAME, "20XX", null, Instant.now()));
|
||||
steamGameRepository.save(new SteamGame(MANUAL_GAME, "Zelda", null, Instant.now()));
|
||||
|
||||
GameAchievementSchema ending = new GameAchievementSchema(ENDING_GAME, "ACH_SURVIVOR", "Survivor", "Complete the game.", null, null, false);
|
||||
ending.setGlobalPercent(19.2);
|
||||
GameAchievementSchema other = new GameAchievementSchema(ENDING_GAME, "ACH_KILL", "First Blood", "Kill an enemy", null, null, false);
|
||||
other.setGlobalPercent(90.0);
|
||||
schemaRepository.saveAll(List.of(ending, other));
|
||||
schemaMetaRepository.save(new GameAchievementSchemaMeta(ENDING_GAME, 2, true, Instant.now()));
|
||||
userAchievementRepository.save(new UserGameAchievement(USER_ID, ENDING_GAME, "ACH_SURVIVOR", true, Instant.now()));
|
||||
userAchievementMetaRepository.save(new UserGameAchievementMeta(USER_ID, ENDING_GAME, Instant.now(), true, 1, 600));
|
||||
|
||||
gameTimeToBeatRepository.save(new GameTimeToBeat(MANUAL_GAME, null, 600, null, Instant.now()));
|
||||
gameMoodsRepository.upsert(MANUAL_GAME, new String[]{"STORY"}, false, Instant.now());
|
||||
}
|
||||
|
||||
private UserGame steamGame(boolean statusSetByUser) {
|
||||
UserGame game = new UserGame(USER_ID, ENDING_GAME, 600, Instant.now(), Instant.now());
|
||||
game.setStatusSetByUser(statusSetByUser);
|
||||
return userGameRepository.save(game);
|
||||
}
|
||||
|
||||
private UserGame stored(long appid) {
|
||||
return userGameRepository.findById(new UserGameId(USER_ID, appid)).orElseThrow();
|
||||
}
|
||||
|
||||
private ResultActions dismiss(long appid, String body) throws Exception {
|
||||
return mockMvc.perform(put("/api/library/games/" + appid + "/finished-hint").header(ClientHeaderFilter.CLIENT_HEADER, "test")
|
||||
.contentType(MediaType.APPLICATION_JSON).content(body));
|
||||
}
|
||||
|
||||
@Test
|
||||
void detail_UntouchedGameWithTheEndingUnlocked_IsFinishedByCore() throws Exception {
|
||||
steamGame(false);
|
||||
|
||||
mockMvc.perform(get("/api/library/games/" + ENDING_GAME))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.status").value("FINISHED"))
|
||||
.andExpect(jsonPath("$.finishedHint").doesNotExist());
|
||||
|
||||
assertEquals(GameStatus.FINISHED, stored(ENDING_GAME).getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detail_UntouchedGameAfterNotYet_IsNotFinishedByCore() throws Exception {
|
||||
UserGame game = steamGame(false);
|
||||
game.setFinishedHintDismissed(FinishedHintType.PLAYTIME);
|
||||
userGameRepository.save(game);
|
||||
|
||||
mockMvc.perform(get("/api/library/games/" + ENDING_GAME))
|
||||
.andExpect(jsonPath("$.status").value("PLAYING"))
|
||||
.andExpect(jsonPath("$.finishedHint.type").value("ENDING"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void detail_StatusChosenByTheUser_KeepsItAndShowsTheEndingHint() throws Exception {
|
||||
steamGame(true);
|
||||
|
||||
mockMvc.perform(get("/api/library/games/" + ENDING_GAME))
|
||||
.andExpect(jsonPath("$.status").value("PLAYING"))
|
||||
.andExpect(jsonPath("$.finishedHint.type").value("ENDING"))
|
||||
.andExpect(jsonPath("$.finishedHint.achievement").value("Survivor"));
|
||||
|
||||
assertEquals(GameStatus.PLAYING, stored(ENDING_GAME).getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detail_GameAddedByHand_GetsThePlaytimeHint() throws Exception {
|
||||
userGameRepository.save(UserGame.manual(USER_ID, MANUAL_GAME, "Switch", GameStatus.PLAYING, 600, Instant.now()));
|
||||
|
||||
mockMvc.perform(get("/api/library/games/" + MANUAL_GAME))
|
||||
.andExpect(jsonPath("$.finishedHint.type").value("PLAYTIME"))
|
||||
.andExpect(jsonPath("$.finishedHint.achievement").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
void notYet_HidesTheHintAndPlaytimeNeverWeakensEnding() throws Exception {
|
||||
steamGame(true);
|
||||
|
||||
dismiss(ENDING_GAME, "{\"dismissed\":\"ENDING\"}").andExpect(status().isNoContent());
|
||||
dismiss(ENDING_GAME, "{\"dismissed\":\"PLAYTIME\"}").andExpect(status().isNoContent());
|
||||
|
||||
assertEquals(FinishedHintType.ENDING, stored(ENDING_GAME).getFinishedHintDismissed());
|
||||
mockMvc.perform(get("/api/library/games/" + ENDING_GAME)).andExpect(jsonPath("$.finishedHint").doesNotExist());
|
||||
}
|
||||
|
||||
@Test
|
||||
void settingTheStatusByHand_MarksItAsChosen() throws Exception {
|
||||
steamGame(false);
|
||||
|
||||
mockMvc.perform(put("/api/library/games/" + ENDING_GAME + "/status").header(ClientHeaderFilter.CLIENT_HEADER, "test")
|
||||
.contentType(MediaType.APPLICATION_JSON).content("{\"status\":\"DROPPED\"}"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
assertTrue(stored(ENDING_GAME).isStatusSetByUser());
|
||||
assertEquals(0, userGameRepository.finishIfUntouched(USER_ID, ENDING_GAME));
|
||||
assertEquals(GameStatus.DROPPED, stored(ENDING_GAME).getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void notYet_UnknownGameIs404() throws Exception {
|
||||
dismiss(620L, "{\"dismissed\":\"ENDING\"}").andExpect(status().isNotFound());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"{}", "{\"dismissed\":\"MAYBE\"}"})
|
||||
void notYet_BadBodyIs400(String body) throws Exception {
|
||||
steamGame(true);
|
||||
|
||||
dismiss(ENDING_GAME, body).andExpect(status().isBadRequest());
|
||||
|
||||
assertNull(stored(ENDING_GAME).getFinishedHintDismissed());
|
||||
}
|
||||
|
||||
@Test
|
||||
void notYet_WithoutClientHeaderIs403() throws Exception {
|
||||
steamGame(true);
|
||||
|
||||
mockMvc.perform(put("/api/library/games/" + ENDING_GAME + "/finished-hint")
|
||||
.contentType(MediaType.APPLICATION_JSON).content("{\"dismissed\":\"ENDING\"}"))
|
||||
.andExpect(status().isForbidden());
|
||||
|
||||
assertFalse(stored(ENDING_GAME).getFinishedHintDismissed() != null);
|
||||
}
|
||||
}
|
||||
@@ -93,9 +93,9 @@ class LibraryControllerPickerTest extends AbstractIntegrationTest {
|
||||
game(20L, GameStatus.BACKLOG, null, null);
|
||||
game(30L, GameStatus.BACKLOG, null, null);
|
||||
game(40L, GameStatus.FINISHED, null, null);
|
||||
gameMoodsRepository.upsert(10L, new String[]{"CHILL", "COOP"}, Instant.now());
|
||||
gameMoodsRepository.upsert(20L, new String[]{"ACTION"}, Instant.now());
|
||||
gameMoodsRepository.upsert(40L, new String[]{"CHILL"}, Instant.now());
|
||||
gameMoodsRepository.upsert(10L, new String[]{"CHILL", "COOP"}, false, Instant.now());
|
||||
gameMoodsRepository.upsert(20L, new String[]{"ACTION"}, false, Instant.now());
|
||||
gameMoodsRepository.upsert(40L, new String[]{"CHILL"}, false, Instant.now());
|
||||
|
||||
mockMvc.perform(get("/api/library/picker").param("mood", "CHILL"))
|
||||
.andExpect(status().isOk())
|
||||
@@ -124,8 +124,8 @@ class LibraryControllerPickerTest extends AbstractIntegrationTest {
|
||||
void picker_MoodAndBudget_BothMustFit() throws Exception {
|
||||
game(10L, GameStatus.BACKLOG, null, 300);
|
||||
game(20L, GameStatus.BACKLOG, null, 3000);
|
||||
gameMoodsRepository.upsert(10L, new String[]{"STORY"}, Instant.now());
|
||||
gameMoodsRepository.upsert(20L, new String[]{"STORY"}, Instant.now());
|
||||
gameMoodsRepository.upsert(10L, new String[]{"STORY"}, false, Instant.now());
|
||||
gameMoodsRepository.upsert(20L, new String[]{"STORY"}, false, Instant.now());
|
||||
|
||||
mockMvc.perform(get("/api/library/picker").param("mood", "STORY").param("maxMinutes", "900"))
|
||||
.andExpect(jsonPath("$.games[*].appid", containsInAnyOrder(10)));
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package core_service.modules.library.domain;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* The cases are rows measured on production (ROADMAP item 9), with their Steam global rates.
|
||||
*/
|
||||
class EndingAchievementTest {
|
||||
|
||||
private static GameAchievementSchema achievement(String apiName, String name, String description, Double percent) {
|
||||
GameAchievementSchema row = new GameAchievementSchema(1L, apiName, name, description, null, null, false);
|
||||
row.setGlobalPercent(percent);
|
||||
return row;
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(delimiter = '|', nullValues = "-", textBlock = """
|
||||
Survivor | Complete the game. | 19.2
|
||||
To Link the Fire | Reach "To Link the Fire" ending. | 31.2
|
||||
The End of Fire | - | 21.4
|
||||
Fate's Conclusion | - | 16.2
|
||||
A Stroll In The Demonic Park | Complete the game on any difficulty setting | 21.7
|
||||
A Cozy Completion | Complete the Campaign | 50.0
|
||||
Passed the Trial | Finish the game on any difficulty. | 22.0
|
||||
""")
|
||||
void isCandidate_StoryEnds(String name, String description, double percent) {
|
||||
assertThat(EndingAchievement.isCandidate(achievement("A", name, description, percent))).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(delimiter = '|', nullValues = "-", textBlock = """
|
||||
The Feast Begins! | Finished tutorial. | 98.6
|
||||
True Fan I | Finish 5 sessions with a score of at least 1000 | 30.1
|
||||
Last Breath | Complete the game with 1 HP remaining (and no armor). | 5.9
|
||||
Honor and Glory | Complete the game after entering and achieving glory. | 14.6
|
||||
Super Fighting Robot | Complete the game on NORMAL difficulty. | 10.2
|
||||
Geminiphobia | Beat the game… twice. | 11.6
|
||||
Do You Want to Super Size | Beat the game on a Large Map. | 13.4
|
||||
The Alexman | Beat the game on the Warlord difficulty level. | 18.1
|
||||
In Storybook Endings | Lock up Mad Hatter in GCPD. | 20.0
|
||||
Neverending | Ascend the world 3 times | 40.0
|
||||
Victory | Complete the game | 0.5
|
||||
Victory | Complete the game | 95.0
|
||||
First Steps | - | 89.1
|
||||
Prologue Done | Finish the prologue of the game | 70.0
|
||||
Act One | Complete Act 1 of the campaign | 60.0
|
||||
First Level | Clear the first level of the game | 75.0
|
||||
Match Played | Complete a game | 75.0
|
||||
Chapter 1 | Complete chapter 1 of the story | 70.0
|
||||
The End of the Beginning | - | 65.0
|
||||
Dead End | - | 40.0
|
||||
Beginning of the End | - | 30.0
|
||||
""")
|
||||
void isCandidate_NoStoryEnd(String name, String description, double percent) {
|
||||
assertThat(EndingAchievement.isCandidate(achievement("A", name, description, percent))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isCandidate_UnknownRateIsNoCandidate() {
|
||||
assertThat(EndingAchievement.isCandidate(achievement("A", "Survivor", "Complete the game.", null))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void of_TheEasiestVariantWins() {
|
||||
List<GameAchievementSchema> schema = List.of(
|
||||
achievement("Game_CompleteApocolyptic", "Apocalyptic", "Complete the Game on Apocalyptic Difficulty", 6.6),
|
||||
achievement("Game_Complete", "Protector of Humanity", "Complete the Game on Any Difficulty Setting", 32.9),
|
||||
achievement("GAME_STORY", "Story", "Complete the story", 20.0),
|
||||
achievement("Other", "First Blood", "Kill an enemy", 90.0));
|
||||
|
||||
EndingAchievement ending = EndingAchievement.of(schema).orElseThrow();
|
||||
assertThat(ending.achievement().getApiName()).isEqualTo("Game_Complete");
|
||||
// Two candidates: a hint, never FINISHED by Core itself.
|
||||
assertThat(ending.certain()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void of_MoreThanThreeCandidatesIsAnOpenGame() {
|
||||
List<GameAchievementSchema> schema = List.of(
|
||||
achievement("A", "Egypt", "Beat the game on any difficulty setting as Ramesses II.", 10.0),
|
||||
achievement("B", "Rome", "Beat the game on any difficulty setting as Augustus.", 11.0),
|
||||
achievement("C", "China", "Beat the game on any difficulty setting as Wu Zetian.", 9.0),
|
||||
achievement("D", "Greece", "Beat the game on any difficulty setting as Alexander.", 12.0));
|
||||
|
||||
assertThat(EndingAchievement.of(schema)).isEmpty();
|
||||
}
|
||||
|
||||
// Three of the four leaders are under the 1 % floor; the family still shows an open game.
|
||||
@Test
|
||||
void of_SiblingsUnderTheRateFloorStillMakeAnOpenGame() {
|
||||
List<GameAchievementSchema> schema = List.of(
|
||||
achievement("A", "Egypt", "Beat the game on any difficulty setting as Ramesses II.", 5.0),
|
||||
achievement("B", "Rome", "Beat the game on any difficulty setting as Augustus.", 0.8),
|
||||
achievement("C", "China", "Beat the game on any difficulty setting as Wu Zetian.", 0.7),
|
||||
achievement("D", "Greece", "Beat the game on any difficulty setting as Alexander.", 0.5));
|
||||
|
||||
assertThat(EndingAchievement.of(schema).filter(EndingAchievement::certain)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void of_NoCandidateIsEmpty() {
|
||||
assertThat(EndingAchievement.of(List.of(achievement("A", "First Blood", "Kill an enemy", 90.0)))).isEmpty();
|
||||
assertThat(EndingAchievement.of(List.of())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void of_OneTextMatchIsCertain() {
|
||||
EndingAchievement ending = EndingAchievement.of(List.of(
|
||||
achievement("END", "Survivor", "Complete the game.", 19.2),
|
||||
achievement("KILL", "First Blood", "Kill an enemy", 90.0))).orElseThrow();
|
||||
|
||||
assertThat(ending.certain()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void of_ANameOnlyMatchIsNeverCertain() {
|
||||
EndingAchievement ending = EndingAchievement.of(List.of(achievement("ACH02", "The End of Fire", null, 21.4))).orElseThrow();
|
||||
|
||||
assertThat(ending.certain()).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package core_service.modules.library.domain;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class FinishedHintTest {
|
||||
|
||||
private static final int NORMAL = 1000;
|
||||
|
||||
private static GameAchievementSchema achievement(String apiName, String name, String description, double percent) {
|
||||
GameAchievementSchema row = new GameAchievementSchema(1L, apiName, name, description, null, null, false);
|
||||
row.setGlobalPercent(percent);
|
||||
return row;
|
||||
}
|
||||
|
||||
private static final List<GameAchievementSchema> WITH_ENDING = List.of(
|
||||
achievement("END", "Survivor", "Complete the game.", 19.2),
|
||||
achievement("KILL", "First Blood", "Kill an enemy", 90.0));
|
||||
private static final List<GameAchievementSchema> WITHOUT_ENDING = List.of(
|
||||
achievement("KILL", "First Blood", "Kill an enemy", 90.0),
|
||||
achievement("BOSS", "Big One", "Defeat a boss", 50.0));
|
||||
|
||||
/**
|
||||
* A PLAYING Steam game with visible stats, known time to beat and not endless.
|
||||
*/
|
||||
private static FinishedHint.Facts facts(List<GameAchievementSchema> schema, Set<String> unlocked, int playtime) {
|
||||
return new FinishedHint.Facts(GameStatus.PLAYING, GameSource.STEAM, schema, unlocked, true, playtime, NORMAL,
|
||||
false, null);
|
||||
}
|
||||
|
||||
private static Optional<FinishedHintType> type(FinishedHint.Facts facts) {
|
||||
return FinishedHint.of(facts).map(FinishedHint::type);
|
||||
}
|
||||
|
||||
@Test
|
||||
void endingUnlocked_IsTheEndingHintWithItsName() {
|
||||
assertThat(FinishedHint.of(facts(WITH_ENDING, Set.of("END"), 0)))
|
||||
.contains(new FinishedHint(FinishedHintType.ENDING, "Survivor"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void endingLocked_IsNoHintEvenAfterALongPlaytime() {
|
||||
assertThat(type(facts(WITH_ENDING, Set.of("KILL"), 5 * NORMAL))).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void noEnding_PlaytimeFromNinetyPercentOfTheNormalTime() {
|
||||
assertThat(type(facts(WITHOUT_ENDING, Set.of(), 900))).contains(FinishedHintType.PLAYTIME);
|
||||
assertThat(type(facts(WITHOUT_ENDING, Set.of(), 899))).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void statsHiddenBySteam_UsesThePlaytime() {
|
||||
FinishedHint.Facts hidden = new FinishedHint.Facts(GameStatus.PLAYING, GameSource.STEAM, WITH_ENDING, Set.of(),
|
||||
false, NORMAL, NORMAL, false, null);
|
||||
|
||||
assertThat(type(hidden)).contains(FinishedHintType.PLAYTIME);
|
||||
}
|
||||
|
||||
@Test
|
||||
void endlessOrUnknownGame_GetsNoPlaytimeHint() {
|
||||
FinishedHint.Facts endless = new FinishedHint.Facts(GameStatus.PLAYING, GameSource.STEAM, List.of(), Set.of(),
|
||||
true, 5 * NORMAL, NORMAL, true, null);
|
||||
FinishedHint.Facts unknown = new FinishedHint.Facts(GameStatus.PLAYING, GameSource.STEAM, List.of(), Set.of(),
|
||||
true, 5 * NORMAL, NORMAL, null, null);
|
||||
FinishedHint.Facts noTimeToBeat = new FinishedHint.Facts(GameStatus.PLAYING, GameSource.STEAM, List.of(), Set.of(),
|
||||
true, 5 * NORMAL, null, false, null);
|
||||
|
||||
assertThat(type(endless)).isEmpty();
|
||||
assertThat(type(unknown)).isEmpty();
|
||||
assertThat(type(noTimeToBeat)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyAchievementUnlocked_LeavesItToTheCompletedHint() {
|
||||
assertThat(type(facts(WITH_ENDING, Set.of("END", "KILL"), 5 * NORMAL))).isEmpty();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(value = GameStatus.class, names = {"FINISHED", "COMPLETED", "DROPPED"})
|
||||
void onlyBacklogAndPlayingGamesAreAsked(GameStatus status) {
|
||||
FinishedHint.Facts done = new FinishedHint.Facts(status, GameSource.STEAM, WITH_ENDING, Set.of("END"), true,
|
||||
5 * NORMAL, NORMAL, false, null);
|
||||
|
||||
assertThat(type(done)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void dismissedPlaytime_StillShowsTheEndingLater() {
|
||||
FinishedHint.Facts playtimeOnly = new FinishedHint.Facts(GameStatus.PLAYING, GameSource.STEAM, WITHOUT_ENDING,
|
||||
Set.of(), true, 5 * NORMAL, NORMAL, false, FinishedHintType.PLAYTIME);
|
||||
FinishedHint.Facts endingNow = new FinishedHint.Facts(GameStatus.PLAYING, GameSource.STEAM, WITH_ENDING,
|
||||
Set.of("END"), true, 5 * NORMAL, NORMAL, false, FinishedHintType.PLAYTIME);
|
||||
|
||||
assertThat(type(playtimeOnly)).isEmpty();
|
||||
assertThat(type(endingNow)).contains(FinishedHintType.ENDING);
|
||||
}
|
||||
|
||||
@Test
|
||||
void dismissedEnding_ShowsNothing() {
|
||||
FinishedHint.Facts facts = new FinishedHint.Facts(GameStatus.PLAYING, GameSource.STEAM, WITH_ENDING,
|
||||
Set.of("END"), true, 5 * NORMAL, NORMAL, false, FinishedHintType.ENDING);
|
||||
|
||||
assertThat(type(facts)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void gameAddedByHand_UsesOnlyTheTypedHours() {
|
||||
FinishedHint.Facts manual = new FinishedHint.Facts(GameStatus.BACKLOG, GameSource.MANUAL, List.of(), Set.of(),
|
||||
true, NORMAL, NORMAL, false, null);
|
||||
|
||||
assertThat(type(manual)).contains(FinishedHintType.PLAYTIME);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package core_service.modules.library.domain;
|
||||
|
||||
import core_service.shared.test.AbstractUnitTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -45,4 +47,25 @@ class MoodTest extends AbstractUnitTest {
|
||||
void of_NothingThatFits_HasNoMood() {
|
||||
assertThat(Mood.of(List.of("Racing", "Sport"), List.of("Science fiction"), List.of("Multiplayer"))).isEmpty();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(delimiter = '|', textBlock = """
|
||||
Simulator | - | -
|
||||
Strategy | Sandbox | -
|
||||
Adventure | Survival | -
|
||||
Strategy | 4X (explore, expand, exploit, and exterminate) | -
|
||||
RPG | - | Massively Multiplayer Online (MMO)
|
||||
""", nullValues = "-")
|
||||
void endless_GamesWithoutAnEnd(String genre, String theme, String mode) {
|
||||
assertThat(Mood.endless(listOf(genre), listOf(theme), listOf(mode))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void endless_StrategyWithACampaignIsNotEndless() {
|
||||
assertThat(Mood.endless(List.of("Real Time Strategy (RTS)", "Strategy"), List.of("Warfare"), List.of("Single player"))).isFalse();
|
||||
}
|
||||
|
||||
private static List<String> listOf(String value) {
|
||||
return value == null ? List.of() : List.of(value);
|
||||
}
|
||||
}
|
||||
@@ -26,12 +26,23 @@ class GameMoodsRepositoryTest extends AbstractPersistenceTest {
|
||||
gameMoodsRepository.deleteAll();
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsert_StoresTheEndlessFlag() {
|
||||
gameMoodsRepository.upsert(427520L, new String[]{"CHILL"}, true, Instant.now());
|
||||
entityManager.clear();
|
||||
assertThat(gameMoodsRepository.findById(427520L).orElseThrow().getEndless()).isTrue();
|
||||
|
||||
gameMoodsRepository.upsert(427520L, new String[]{"CHILL"}, false, Instant.now());
|
||||
entityManager.clear();
|
||||
assertThat(gameMoodsRepository.findById(427520L).orElseThrow().getEndless()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsert_StoresTheMoodsAndReplacesThemOnTheNextRefresh() {
|
||||
gameMoodsRepository.upsert(620L, new String[]{"CHILL", "COOP"}, Instant.now());
|
||||
gameMoodsRepository.upsert(620L, new String[]{"CHILL", "COOP"}, false, Instant.now());
|
||||
assertThat(gameMoodsRepository.findById(620L).orElseThrow().getMoods()).containsExactly("CHILL", "COOP");
|
||||
|
||||
gameMoodsRepository.upsert(620L, new String[0], Instant.now());
|
||||
gameMoodsRepository.upsert(620L, new String[0], false, Instant.now());
|
||||
// The native upsert passes the persistence context; read the row again.
|
||||
entityManager.clear();
|
||||
assertThat(gameMoodsRepository.findById(620L).orElseThrow().getMoods()).isEmpty();
|
||||
@@ -39,8 +50,8 @@ class GameMoodsRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
@Test
|
||||
void findFreshAppids_LeavesOutOldAndMissingRows() {
|
||||
gameMoodsRepository.upsert(620L, new String[]{"STORY"}, Instant.now());
|
||||
gameMoodsRepository.upsert(220L, new String[]{"ACTION"}, Instant.now().minus(Duration.ofDays(40)));
|
||||
gameMoodsRepository.upsert(620L, new String[]{"STORY"}, false, Instant.now());
|
||||
gameMoodsRepository.upsert(220L, new String[]{"ACTION"}, false, Instant.now().minus(Duration.ofDays(40)));
|
||||
|
||||
assertThat(gameMoodsRepository.findFreshAppids(List.of(620L, 220L, 700L), Instant.now().minus(Duration.ofDays(30))))
|
||||
.isEqualTo(Set.of(620L));
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package core_service.modules.library.service;
|
||||
|
||||
import core_service.modules.library.domain.GameAchievementSchema;
|
||||
import core_service.modules.library.domain.GameAchievementSchemaMeta;
|
||||
import core_service.modules.library.domain.UserGame;
|
||||
import core_service.modules.library.domain.UserGameAchievement;
|
||||
import core_service.modules.library.domain.UserGameAchievementMeta;
|
||||
import core_service.modules.library.repo.GameAchievementSchemaMetaRepository;
|
||||
import core_service.modules.library.repo.GameAchievementSchemaRepository;
|
||||
@@ -9,6 +11,7 @@ import core_service.modules.library.repo.UserGameAchievementMetaRepository;
|
||||
import core_service.modules.library.repo.UserGameAchievementRepository;
|
||||
import core_service.modules.library.repo.UserGameRepository;
|
||||
import core_service.modules.steam_adapter.api.dto.response.SteamPlayerAchievementsResponse;
|
||||
import core_service.modules.steam_adapter.api.dto.response.SteamSchemaForGameResponse;
|
||||
import core_service.modules.steam_adapter.service.SteamApiService;
|
||||
import core_service.shared.test.AbstractUnitTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -23,6 +26,7 @@ import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -184,6 +188,101 @@ class AchievementSyncServiceTest extends AbstractUnitTest {
|
||||
verify(steamApiService, never()).getPlayerAchievements(anyString(), anyLong());
|
||||
}
|
||||
|
||||
private static Optional<SteamSchemaForGameResponse.Game> steamSchema(String... apiNames) {
|
||||
List<SteamSchemaForGameResponse.Achievement> achievements = java.util.Arrays.stream(apiNames)
|
||||
.map(name -> new SteamSchemaForGameResponse.Achievement(name, name, "Complete the game.", 0, "icon", "gray"))
|
||||
.toList();
|
||||
return Optional.of(new SteamSchemaForGameResponse.Game("Game", "1",
|
||||
new SteamSchemaForGameResponse.AvailableGameStats(achievements)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<GameAchievementSchema> savedSchema() {
|
||||
ArgumentCaptor<List<GameAchievementSchema>> saved = ArgumentCaptor.forClass(List.class);
|
||||
verify(schemaRepository).saveAll(saved.capture());
|
||||
return saved.getValue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshSchemaNow_StoresTheGlobalRatesPerAchievement() throws Exception {
|
||||
when(steamApiService.getSchemaForGame(620L)).thenReturn(steamSchema("A", "B"));
|
||||
when(steamApiService.getGlobalAchievementPercentages(620L)).thenReturn(Map.of("A", 12.5));
|
||||
|
||||
service.refreshSchemaNow(620L);
|
||||
|
||||
List<GameAchievementSchema> rows = savedSchema();
|
||||
assertEquals(12.5, rows.get(0).getGlobalPercent());
|
||||
assertEquals(null, rows.get(1).getGlobalPercent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshSchemaNow_FailedRatesCall_KeepsTheStoredRates() throws Exception {
|
||||
GameAchievementSchema old = new GameAchievementSchema(620L, "A", "A", null, null, null, false);
|
||||
old.setGlobalPercent(30.0);
|
||||
when(steamApiService.getSchemaForGame(620L)).thenReturn(steamSchema("A"));
|
||||
when(steamApiService.getGlobalAchievementPercentages(620L)).thenReturn(Map.of());
|
||||
when(schemaRepository.findByAppid(620L)).thenReturn(List.of(old));
|
||||
|
||||
service.refreshSchemaNow(620L);
|
||||
|
||||
assertEquals(30.0, savedSchema().getFirst().getGlobalPercent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshSchemaNow_RateLimiterFull_StillStoresTheSchema() throws Exception {
|
||||
when(steamApiService.getSchemaForGame(620L)).thenReturn(steamSchema("A"));
|
||||
when(steamApiService.getGlobalAchievementPercentages(620L)).thenThrow(new IllegalStateException("limiter full"));
|
||||
|
||||
service.refreshSchemaNow(620L);
|
||||
|
||||
assertEquals("A", savedSchema().getFirst().getApiName());
|
||||
}
|
||||
|
||||
private void endingOf620(boolean unlocked) {
|
||||
GameAchievementSchema ending = new GameAchievementSchema(620L, "END", "Survivor", "Complete the game.", null, null, false);
|
||||
ending.setGlobalPercent(19.2);
|
||||
when(schemaRepository.findByAppid(620L)).thenReturn(List.of(ending));
|
||||
when(userAchievementRepository.findByUserIdAndAppid(USER_ID, 620L))
|
||||
.thenReturn(List.of(new UserGameAchievement(USER_ID, 620L, "END", unlocked, null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void finishIfEndingUnlocked_UnlockedEnding_FinishesAnUntouchedGame() {
|
||||
endingOf620(true);
|
||||
when(userGameRepository.finishIfUntouched(USER_ID, 620L)).thenReturn(1);
|
||||
|
||||
assertTrue(service.finishIfEndingUnlocked(USER_ID, 620L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void finishIfEndingUnlocked_LockedEnding_ChangesNothing() {
|
||||
endingOf620(false);
|
||||
|
||||
assertFalse(service.finishIfEndingUnlocked(USER_ID, 620L));
|
||||
verify(userGameRepository, never()).finishIfUntouched(anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void syncLibrary_UntouchedGameWithUnchangedPlaytime_IsCheckedForTheEnding() {
|
||||
library(List.of(game(620L, 120)), List.of(schema(620L, 1)), List.of(unlocks(620L, 1, 120)));
|
||||
endingOf620(true);
|
||||
|
||||
service.syncLibrary(USER_ID, STEAM_ID);
|
||||
|
||||
verify(userGameRepository).finishIfUntouched(USER_ID, 620L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void syncLibrary_StatusChosenByTheUser_IsNeverFinishedByCore() {
|
||||
UserGame chosen = game(620L, 120);
|
||||
chosen.setStatusSetByUser(true);
|
||||
library(List.of(chosen), List.of(schema(620L, 1)), List.of(unlocks(620L, 1, 120)));
|
||||
|
||||
service.syncLibrary(USER_ID, STEAM_ID);
|
||||
|
||||
verify(userGameRepository, never()).finishIfUntouched(anyLong(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshSchemaNow_EmptyAnswer_KeepsTheStoredSchema() throws Exception {
|
||||
when(steamApiService.getSchemaForGame(620L)).thenReturn(Optional.empty());
|
||||
|
||||
@@ -18,8 +18,10 @@ import java.util.Set;
|
||||
|
||||
import static org.mockito.AdditionalMatchers.aryEq;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyCollection;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
@@ -53,9 +55,21 @@ class MoodServiceTest extends AbstractUnitTest {
|
||||
|
||||
service.refresh(List.of(220L, 620L, 700L));
|
||||
|
||||
verify(gameMoodsRepository).upsert(eq(620L), aryEq(new String[]{"CHILL", "COOP"}), any(Instant.class));
|
||||
verify(gameMoodsRepository).upsert(eq(700L), aryEq(new String[0]), any(Instant.class));
|
||||
verify(gameMoodsRepository, never()).upsert(eq(220L), any(), any());
|
||||
verify(gameMoodsRepository).upsert(eq(620L), aryEq(new String[]{"CHILL", "COOP"}), eq(false), any(Instant.class));
|
||||
verify(gameMoodsRepository).upsert(eq(700L), aryEq(new String[0]), isNull(), any(Instant.class));
|
||||
verify(gameMoodsRepository, never()).upsert(eq(220L), any(), anyBoolean(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refresh_StoresWhetherTheGameHasNoEnd() {
|
||||
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||
when(gameMoodsRepository.findFreshAppids(anyCollection(), any())).thenReturn(Set.of());
|
||||
when(igdbApiService.getGameTags(Set.of(427520L))).thenReturn(Optional.of(Map.of(
|
||||
427520L, new IgdbApiService.GameTags(List.of("Simulator", "Strategy"), List.of("Sandbox"), List.of()))));
|
||||
|
||||
service.refresh(List.of(427520L));
|
||||
|
||||
verify(gameMoodsRepository).upsert(eq(427520L), any(), eq(true), any(Instant.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -67,7 +81,7 @@ class MoodServiceTest extends AbstractUnitTest {
|
||||
|
||||
service.refreshBacklogInBackground(7L);
|
||||
|
||||
verify(gameMoodsRepository).upsert(eq(620L), aryEq(new String[0]), any(Instant.class));
|
||||
verify(gameMoodsRepository).upsert(eq(620L), aryEq(new String[0]), isNull(), any(Instant.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -79,7 +93,7 @@ class MoodServiceTest extends AbstractUnitTest {
|
||||
|
||||
service.refresh(List.of(-119388L));
|
||||
|
||||
verify(gameMoodsRepository).upsert(eq(-119388L), aryEq(new String[]{"STORY"}), any(Instant.class));
|
||||
verify(gameMoodsRepository).upsert(eq(-119388L), aryEq(new String[]{"STORY"}), eq(false), any(Instant.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -90,7 +104,7 @@ class MoodServiceTest extends AbstractUnitTest {
|
||||
|
||||
service.refresh(List.of(620L));
|
||||
|
||||
verify(gameMoodsRepository, never()).upsert(any(), any(), any());
|
||||
verify(gameMoodsRepository, never()).upsert(any(), any(), anyBoolean(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -383,4 +383,29 @@ class SteamApiServiceTest {
|
||||
assertThatThrownBy(() -> steamApiService.getPlayerAchievements("76561197960435530", 620))
|
||||
.isInstanceOf(RestClientException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getGlobalAchievementPercentages_ReadsStringsAndNumbers() {
|
||||
server.expect(requestTo(containsString("/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v2/")))
|
||||
.andExpect(queryParam("gameid", "620"))
|
||||
.andRespond(withSuccess("""
|
||||
{"achievementpercentages":{"achievements":[
|
||||
{"name":"ACH.WAKE_UP","percent":"64.8"},
|
||||
{"name":"ACH.LASER","percent":62.7},
|
||||
{"name":"ACH.BROKEN"}
|
||||
]}}
|
||||
""", MediaType.APPLICATION_JSON));
|
||||
|
||||
assertThat(steamApiService.getGlobalAchievementPercentages(620))
|
||||
.containsOnly(Map.entry("ACH.WAKE_UP", 64.8), Map.entry("ACH.LASER", 62.7));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getGlobalAchievementPercentages_AnyFailureIsEmpty() {
|
||||
server.expect(requestTo(containsString("/GetGlobalAchievementPercentagesForApp/"))).andRespond(withStatus(HttpStatus.FORBIDDEN));
|
||||
server.expect(requestTo(containsString("/GetGlobalAchievementPercentagesForApp/"))).andRespond(withServerError());
|
||||
|
||||
assertThat(steamApiService.getGlobalAchievementPercentages(620)).isEmpty();
|
||||
assertThat(steamApiService.getGlobalAchievementPercentages(620)).isEmpty();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user