Public Access
feat(library): add question queue for open game decisions
Add open questions per game: Start it?, Finished?, Dropped?, Rate it?. A "No" is saved in user_game_question (V41) and snoozes the question. Account cleanup clears the new table too. Refs Gitea #32.
This commit is contained in:
1 parent
0ddb345a41
commit
6cd92f3e83
18 files changed
+934
-2
No files matched your search
@@ -15,7 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
@Import(AccountCleanupRepository.class)
|
||||
class AccountCleanupRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
private static final List<String> USER_TABLES = List.of("user_game_status_change", "user_game_achievement",
|
||||
private static final List<String> USER_TABLES = List.of("user_game_question", "user_game_status_change", "user_game_achievement",
|
||||
"user_game_achievement_meta", "user_game_playtime_day", "wishlist_game", "backlog_snapshot",
|
||||
"sync_change", "sync_result", "user_steam_sync", "game_merge_undo_user", "user_psn_trophy", "user_psn_trophy_set", "user_game", "game_link_request", "account_name", "game_import", "reward_ledger",
|
||||
"reward_equipped", "user_psn_account", "listed_user", "public_user", "follow", "follow_digest", "inbox_note");
|
||||
@@ -35,6 +35,7 @@ class AccountCleanupRepositoryTest extends AbstractPersistenceTest {
|
||||
for (long userId : List.of(1L, 2L)) {
|
||||
// The V14 trigger adds the user_game_status_change row.
|
||||
jdbc.update("insert into user_game (user_id, appid, status) values (?, 220, 'BACKLOG')", userId);
|
||||
jdbc.update("insert into user_game_question (user_id, appid, question, playtime_minutes) values (?, 220, 'START', 0)", userId);
|
||||
jdbc.update("insert into user_game_achievement (user_id, appid, api_name) values (?, 220, 'ACH_1')", userId);
|
||||
jdbc.update("insert into user_game_achievement_meta (user_id, appid) values (?, 220)", userId);
|
||||
jdbc.update("insert into user_game_playtime_day (user_id, day, appid, minutes) values (?, current_date, 220, 30)", userId);
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package core_service.modules.library.api;
|
||||
|
||||
import core_service.configuration.security.ClientHeaderFilter;
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/** The dev profile authenticates every request as user 1. */
|
||||
@AutoConfigureMockMvc
|
||||
class QuestionFlowTest extends AbstractIntegrationTest {
|
||||
|
||||
private static final long USER_ID = 1L;
|
||||
private static final long BACKLOG_GAME = 11L;
|
||||
private static final long PLAYING_GAME = 12L;
|
||||
private static final long FINISHED_GAME = 13L;
|
||||
private static final long HIDDEN_GAME = 14L;
|
||||
private static final long COMPLETED_GAME = 15L;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
for (String table : List.of("user_game_question", "user_game_playtime_day", "game_time_to_beat", "game_mood",
|
||||
"game_link", "user_game")) {
|
||||
jdbc.update("delete from " + table);
|
||||
}
|
||||
for (long appid : List.of(BACKLOG_GAME, PLAYING_GAME, FINISHED_GAME, HIDDEN_GAME, COMPLETED_GAME)) {
|
||||
jdbc.update("insert into steam_game (appid, name) values (?, ?) on conflict do nothing", appid, "Game " + appid);
|
||||
}
|
||||
game(BACKLOG_GAME, "BACKLOG", 60);
|
||||
game(PLAYING_GAME, "PLAYING", 700);
|
||||
game(FINISHED_GAME, "FINISHED", 100);
|
||||
game(COMPLETED_GAME, "COMPLETED", 100);
|
||||
game(HIDDEN_GAME, "BACKLOG", 60);
|
||||
jdbc.update("update user_game set hidden_at = now() where appid = ?", HIDDEN_GAME);
|
||||
}
|
||||
|
||||
private void game(long appid, String status, int minutes) {
|
||||
jdbc.update("insert into user_game (user_id, appid, status, playtime_minutes) values (?, ?, ?, ?)",
|
||||
USER_ID, appid, status, minutes);
|
||||
}
|
||||
|
||||
private ResultActions answer(long appid, String body) throws Exception {
|
||||
return mockMvc.perform(post("/api/library/questions/" + appid).header(ClientHeaderFilter.CLIENT_HEADER, "test")
|
||||
.contentType(MediaType.APPLICATION_JSON).content(body));
|
||||
}
|
||||
|
||||
private String statusOf(long appid) {
|
||||
return jdbc.queryForObject("select status from user_game where appid = ?", String.class, appid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void list_HasOneCardPerOpenGameInAskOrder_AndTheCountMatches() throws Exception {
|
||||
mockMvc.perform(get("/api/library/questions"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.results.length()").value(4))
|
||||
.andExpect(jsonPath("$.results[0].appid").value(BACKLOG_GAME))
|
||||
.andExpect(jsonPath("$.results[0].question").value("START"))
|
||||
.andExpect(jsonPath("$.results[1].appid").value(PLAYING_GAME))
|
||||
.andExpect(jsonPath("$.results[1].question").value("FINISHED"))
|
||||
.andExpect(jsonPath("$.results[2].question").value("RATE"));
|
||||
mockMvc.perform(get("/api/library/questions/count")).andExpect(jsonPath("$.count").value(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
void finishedYes_SetsTheStatusLikeTheDetailPage_AndAsksToRateNext() throws Exception {
|
||||
answer(PLAYING_GAME, "{\"question\":\"FINISHED\",\"yes\":true}")
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.next.question").value("RATE"));
|
||||
|
||||
assertThat(statusOf(PLAYING_GAME)).isEqualTo("FINISHED");
|
||||
assertThat(jdbc.queryForObject("select status_set_by_user from user_game where appid = ?", Boolean.class,
|
||||
PLAYING_GAME)).isTrue();
|
||||
assertThat(jdbc.queryForObject("""
|
||||
select count(*) from user_game_status_change
|
||||
where appid = ? and to_status = 'FINISHED' and by_user""", Integer.class, PLAYING_GAME)).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void startYes_SetsPlaying_AndHasNoNextQuestion() throws Exception {
|
||||
answer(BACKLOG_GAME, "{\"question\":\"START\",\"yes\":true}")
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.next").doesNotExist());
|
||||
|
||||
assertThat(statusOf(BACKLOG_GAME)).isEqualTo("PLAYING");
|
||||
}
|
||||
|
||||
@Test
|
||||
void startNo_RemovesTheCard_AndItComesBackAfterFiveMoreHours() throws Exception {
|
||||
answer(BACKLOG_GAME, "{\"question\":\"START\",\"yes\":false}").andExpect(status().isOk());
|
||||
|
||||
mockMvc.perform(get("/api/library/questions/count")).andExpect(jsonPath("$.count").value(3));
|
||||
jdbc.update("update user_game set playtime_minutes = 359 where appid = ?", BACKLOG_GAME);
|
||||
mockMvc.perform(get("/api/library/questions/count")).andExpect(jsonPath("$.count").value(3));
|
||||
jdbc.update("update user_game set playtime_minutes = 360 where appid = ?", BACKLOG_GAME);
|
||||
mockMvc.perform(get("/api/library/questions/count")).andExpect(jsonPath("$.count").value(4));
|
||||
assertThat(statusOf(BACKLOG_GAME)).isEqualTo("BACKLOG");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rateYes_SavesTheRating_AlsoForACompletedGame() throws Exception {
|
||||
answer(FINISHED_GAME, "{\"question\":\"RATE\",\"yes\":true,\"rating\":4}").andExpect(status().isOk());
|
||||
answer(COMPLETED_GAME, "{\"question\":\"RATE\",\"yes\":true,\"rating\":5}").andExpect(status().isOk());
|
||||
|
||||
assertThat(jdbc.queryForObject("select rating from user_game where appid = ?", Integer.class, FINISHED_GAME)).isEqualTo(4);
|
||||
assertThat(jdbc.queryForObject("select rating from user_game where appid = ?", Integer.class, COMPLETED_GAME)).isEqualTo(5);
|
||||
assertThat(statusOf(FINISHED_GAME)).isEqualTo("FINISHED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rateSkip_NeverAsksAgain() throws Exception {
|
||||
answer(FINISHED_GAME, "{\"question\":\"RATE\",\"yes\":false}").andExpect(status().isOk());
|
||||
|
||||
mockMvc.perform(get("/api/library/questions/count")).andExpect(jsonPath("$.count").value(3));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rateYesWithoutRating_Is400() throws Exception {
|
||||
answer(FINISHED_GAME, "{\"question\":\"RATE\",\"yes\":true}")
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("INVALID_ANSWER"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void badBody_Is400() throws Exception {
|
||||
answer(PLAYING_GAME, "{\"question\":\"FINISHED\"}").andExpect(status().isBadRequest());
|
||||
answer(PLAYING_GAME, "{\"yes\":true}").andExpect(status().isBadRequest());
|
||||
answer(FINISHED_GAME, "{\"question\":\"RATE\",\"yes\":true,\"rating\":6}").andExpect(status().isBadRequest());
|
||||
|
||||
assertThat(statusOf(PLAYING_GAME)).isEqualTo("PLAYING");
|
||||
}
|
||||
|
||||
@Test
|
||||
void staleQuestion_Is409_AndChangesNothing() throws Exception {
|
||||
answer(PLAYING_GAME, "{\"question\":\"START\",\"yes\":true}")
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value("QUESTION_GONE"));
|
||||
answer(PLAYING_GAME, "{\"question\":\"FINISHED\",\"yes\":true}").andExpect(status().isOk());
|
||||
answer(PLAYING_GAME, "{\"question\":\"FINISHED\",\"yes\":true}").andExpect(status().isConflict());
|
||||
|
||||
assertThat(statusOf(PLAYING_GAME)).isEqualTo("FINISHED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void linkedSetPlayedYesterdayOnTheOtherEdition_IsNotAskedDropped() throws Exception {
|
||||
jdbc.update("delete from user_game");
|
||||
for (long appid : List.of(21L, 22L)) {
|
||||
jdbc.update("insert into steam_game (appid, name) values (?, ?) on conflict do nothing", appid, "Game " + appid);
|
||||
}
|
||||
jdbc.update("insert into game_time_to_beat (appid, normally_minutes, fetched_at) values (21, 6000, now()), (22, 6000, now())");
|
||||
game(21L, "PLAYING", 1800);
|
||||
game(22L, "PLAYING", 300);
|
||||
jdbc.update("update user_game set first_seen_at = now() - interval '1 year' where appid in (21, 22)");
|
||||
jdbc.update("update user_game set last_played_at = now() - interval '1 day' where appid = 22");
|
||||
jdbc.update("update user_game_status_change set changed_at = now() - interval '1 year' where appid in (21, 22)");
|
||||
jdbc.update("insert into user_game_playtime_day (user_id, day, appid, minutes) values (?, current_date - 200, 21, 60)", USER_ID);
|
||||
jdbc.update("insert into game_link (appid, set_id) values (21, 1), (22, 1)");
|
||||
|
||||
mockMvc.perform(get("/api/library/questions/count")).andExpect(jsonPath("$.count").value(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hiddenOrUnknownGame_Is404() throws Exception {
|
||||
answer(HIDDEN_GAME, "{\"question\":\"START\",\"yes\":true}").andExpect(status().isNotFound());
|
||||
answer(999L, "{\"question\":\"START\",\"yes\":true}").andExpect(status().isNotFound());
|
||||
|
||||
assertThat(statusOf(HIDDEN_GAME)).isEqualTo("BACKLOG");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
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.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class OpenQuestionTest {
|
||||
|
||||
private static final ZoneId ZONE = ZoneId.of("Europe/Berlin");
|
||||
private static final Instant NOW = Instant.parse("2026-10-09T12:00:00Z");
|
||||
private static final Instant LONG_AGO = NOW.minus(400, ChronoUnit.DAYS);
|
||||
|
||||
/** A game with a calm default: playing 1 h, played lately, rated. */
|
||||
private static final class Game {
|
||||
GameStatus status = GameStatus.PLAYING;
|
||||
Integer rating = 3;
|
||||
int playtime = 60;
|
||||
int playtime2Weeks = 30;
|
||||
Integer normally = null;
|
||||
Boolean endless = null;
|
||||
LocalDate lastPlayDay = null;
|
||||
Instant lastPlayedAt = null;
|
||||
Instant lastStatusChange = LONG_AGO;
|
||||
Instant firstSeenAt = LONG_AGO;
|
||||
Integer startAskedAt = null;
|
||||
Integer finishedAskedAt = null;
|
||||
Instant droppedAnsweredAt = null;
|
||||
boolean rateSkipped = false;
|
||||
|
||||
QuestionFacts facts() {
|
||||
return new QuestionFacts(1L, "Game", null, status, rating, playtime, playtime2Weeks, normally, endless,
|
||||
lastPlayDay, lastPlayedAt, lastStatusChange, firstSeenAt, startAskedAt, finishedAskedAt,
|
||||
droppedAnsweredAt, rateSkipped);
|
||||
}
|
||||
}
|
||||
|
||||
private static Question askIdle(UnaryOperator<Game> setup) {
|
||||
return ask(g -> {
|
||||
g.playtime2Weeks = 0;
|
||||
return setup.apply(g);
|
||||
});
|
||||
}
|
||||
|
||||
private static Question ask(UnaryOperator<Game> setup) {
|
||||
return OpenQuestion.of(setup.apply(new Game()).facts(), NOW, ZONE).map(OpenQuestion::question).orElse(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_PlayedBacklogGame() {
|
||||
assertThat(ask(g -> { g.status = GameStatus.BACKLOG; g.playtime = 1; return g; })).isEqualTo(Question.START);
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_NotAskedForAnUnplayedGame() {
|
||||
assertThat(ask(g -> { g.status = GameStatus.BACKLOG; g.playtime = 0; return g; })).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void start_SnoozedUntilFiveMoreHours() {
|
||||
assertThat(ask(g -> { g.status = GameStatus.BACKLOG; g.playtime = 100; g.startAskedAt = 100 - 299; return g; }))
|
||||
.isNull();
|
||||
assertThat(ask(g -> { g.status = GameStatus.BACKLOG; g.playtime = 400; g.startAskedAt = 100; return g; }))
|
||||
.isEqualTo(Question.START);
|
||||
assertThat(ask(g -> { g.status = GameStatus.BACKLOG; g.playtime = 399; g.startAskedAt = 100; return g; }))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void finished_UsesTimeToBeatWhenKnown() {
|
||||
assertThat(ask(g -> { g.playtime = 1999; g.normally = 2000; return g; })).isNull();
|
||||
assertThat(ask(g -> { g.playtime = 2000; g.normally = 2000; return g; })).isEqualTo(Question.FINISHED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void finished_FallsBackToTenHoursWithoutTimeToBeat() {
|
||||
assertThat(ask(g -> { g.playtime = 599; return g; })).isNull();
|
||||
assertThat(ask(g -> { g.playtime = 600; return g; })).isEqualTo(Question.FINISHED);
|
||||
assertThat(ask(g -> { g.playtime = 600; g.normally = 0; return g; })).isEqualTo(Question.FINISHED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void finished_AlsoForBacklogGame_ButNeverForFinishedDroppedOrCompleted() {
|
||||
assertThat(ask(g -> { g.status = GameStatus.BACKLOG; g.playtime = 700; g.startAskedAt = 600; return g; }))
|
||||
.isEqualTo(Question.FINISHED);
|
||||
assertThat(ask(g -> { g.status = GameStatus.DROPPED; g.playtime = 700; return g; })).isNull();
|
||||
assertThat(ask(g -> { g.status = GameStatus.COMPLETED; g.playtime = 700; g.rating = 5; return g; })).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void finished_EndlessGameNeverAsks_ButUnknownOrFalseDoes() {
|
||||
assertThat(ask(g -> { g.playtime = 700; g.endless = true; return g; })).isNull();
|
||||
assertThat(ask(g -> { g.playtime = 700; g.endless = false; return g; })).isEqualTo(Question.FINISHED);
|
||||
assertThat(ask(g -> { g.playtime = 700; g.endless = null; return g; })).isEqualTo(Question.FINISHED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void finished_SnoozedUntilFiveMoreHours() {
|
||||
assertThat(ask(g -> { g.playtime = 899; g.finishedAskedAt = 600; return g; })).isNull();
|
||||
assertThat(ask(g -> { g.playtime = 900; g.finishedAskedAt = 600; return g; })).isEqualTo(Question.FINISHED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void endlessGame_StillGetsDropped() {
|
||||
assertThat(askIdle(g -> { g.playtime = 700; g.endless = true; return g; })).isEqualTo(Question.DROPPED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void dropped_AfterNinetyIdleDays() {
|
||||
assertThat(askIdle(g -> { g.lastPlayedAt = NOW.minus(89, ChronoUnit.DAYS); return g; })).isNull();
|
||||
assertThat(askIdle(g -> { g.lastPlayedAt = NOW.minus(91, ChronoUnit.DAYS); return g; })).isEqualTo(Question.DROPPED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void dropped_PlaytimeInTheLastTwoWeeksBlocks() {
|
||||
assertThat(ask(g -> g)).isNull();
|
||||
assertThat(askIdle(g -> g)).isEqualTo(Question.DROPPED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void dropped_EachActivitySourceCounts() {
|
||||
Instant recent = NOW.minus(10, ChronoUnit.DAYS);
|
||||
assertThat(askIdle(g -> { g.lastPlayDay = LocalDate.of(2026, 10, 1); return g; })).isNull();
|
||||
assertThat(askIdle(g -> { g.lastPlayedAt = recent; return g; })).isNull();
|
||||
assertThat(askIdle(g -> { g.lastStatusChange = recent; return g; })).isNull();
|
||||
assertThat(askIdle(g -> { g.firstSeenAt = recent; return g; })).isNull();
|
||||
assertThat(askIdle(g -> { g.droppedAnsweredAt = recent; return g; })).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void dropped_AnswerNoRestartsTheNinetyDays() {
|
||||
assertThat(askIdle(g -> { g.droppedAnsweredAt = NOW.minus(91, ChronoUnit.DAYS); return g; })).isEqualTo(Question.DROPPED);
|
||||
assertThat(askIdle(g -> { g.droppedAnsweredAt = NOW.minus(89, ChronoUnit.DAYS); return g; })).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void dropped_ResultCarriesTheLastActivity() {
|
||||
Game game = new Game();
|
||||
game.playtime2Weeks = 0;
|
||||
game.lastPlayedAt = NOW.minus(120, ChronoUnit.DAYS);
|
||||
QuestionFacts facts = game.facts();
|
||||
|
||||
assertThat(OpenQuestion.of(facts, NOW, ZONE).orElseThrow().lastActivity()).isEqualTo(facts.lastPlayedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void dropped_OnlyForPlayingGames() {
|
||||
assertThat(ask(g -> { g.status = GameStatus.BACKLOG; g.playtime = 0; return g; })).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(value = GameStatus.class, names = {"FINISHED", "COMPLETED"})
|
||||
void rate_UnratedFinishedGame(GameStatus status) {
|
||||
assertThat(ask(g -> { g.status = status; g.rating = null; return g; })).isEqualTo(Question.RATE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rate_NotWhenRatedOrSkipped() {
|
||||
assertThat(ask(g -> { g.status = GameStatus.FINISHED; return g; })).isNull();
|
||||
assertThat(ask(g -> { g.status = GameStatus.FINISHED; g.rating = null; g.rateSkipped = true; return g; })).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void priority_StartBeforeFinishedBeforeDroppedBeforeRate() {
|
||||
assertThat(ask(g -> { g.status = GameStatus.BACKLOG; g.playtime = 700; return g; })).isEqualTo(Question.START);
|
||||
assertThat(ask(g -> { g.playtime = 700; return g; })).isEqualTo(Question.FINISHED);
|
||||
assertThat(ask(g -> { g.status = GameStatus.FINISHED; g.rating = null; g.playtime = 700; return g; }))
|
||||
.isEqualTo(Question.RATE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualGameWithoutPlaytime_GetsOnlyDroppedOrRate() {
|
||||
assertThat(askIdle(g -> { g.playtime = 0; return g; })).isEqualTo(Question.DROPPED);
|
||||
assertThat(ask(g -> { g.status = GameStatus.FINISHED; g.rating = null; g.playtime = 0; return g; }))
|
||||
.isEqualTo(Question.RATE);
|
||||
assertThat(ask(g -> { g.status = GameStatus.BACKLOG; g.playtime = 0; return g; })).isNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package core_service.modules.library.repo;
|
||||
|
||||
import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.Question;
|
||||
import core_service.modules.library.domain.QuestionFacts;
|
||||
import core_service.shared.test.AbstractPersistenceTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Import(QuestionRepository.class)
|
||||
class QuestionRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
private static final long USER = 1L;
|
||||
private static final long OTHER_USER = 2L;
|
||||
|
||||
@Autowired
|
||||
private QuestionRepository repository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
void seed() {
|
||||
for (String table : List.of("user_game_question", "user_game_playtime_day", "game_link", "game_mood",
|
||||
"game_time_to_beat", "user_game")) {
|
||||
jdbc.update("delete from " + table);
|
||||
}
|
||||
for (long appid : List.of(10L, 20L, 30L, 40L, 50L)) {
|
||||
jdbc.update("insert into steam_game (appid, name, image_url) values (?, ?, ?) on conflict do nothing",
|
||||
appid, "Game " + appid, "img" + appid);
|
||||
}
|
||||
game(USER, 10, "PLAYING", 120);
|
||||
}
|
||||
|
||||
private void game(long userId, long appid, String status, int minutes) {
|
||||
jdbc.update("insert into user_game (user_id, appid, status, playtime_minutes) values (?, ?, ?, ?)",
|
||||
userId, appid, status, minutes);
|
||||
}
|
||||
|
||||
private QuestionFacts only(long appid) {
|
||||
List<QuestionFacts> facts = repository.facts(USER, appid);
|
||||
assertThat(facts).hasSize(1);
|
||||
return facts.get(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void facts_ReadTheGameItsPlaytimeDayAndTheStatusChange() {
|
||||
jdbc.update("insert into user_game_playtime_day (user_id, day, appid, minutes) values (?, '2026-05-01', 10, 30)", USER);
|
||||
jdbc.update("insert into user_game_playtime_day (user_id, day, appid, minutes) values (?, '2026-06-02', 10, 30)", USER);
|
||||
|
||||
QuestionFacts facts = only(10);
|
||||
|
||||
assertThat(facts.name()).isEqualTo("Game 10");
|
||||
assertThat(facts.imageUrl()).isEqualTo("img10");
|
||||
assertThat(facts.status()).isEqualTo(GameStatus.PLAYING);
|
||||
assertThat(facts.playtimeMinutes()).isEqualTo(120);
|
||||
assertThat(facts.lastPlayDay()).isEqualTo(LocalDate.of(2026, 6, 2));
|
||||
assertThat(facts.lastStatusChange()).isNotNull();
|
||||
assertThat(facts.firstSeenAt()).isNotNull();
|
||||
assertThat(facts.normallyMinutes()).isNull();
|
||||
assertThat(facts.endless()).isNull();
|
||||
assertThat(facts.rateSkipped()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void facts_ReadTimeToBeatAndEndless() {
|
||||
jdbc.update("insert into game_time_to_beat (appid, normally_minutes, fetched_at) values (10, 900, now())");
|
||||
jdbc.update("insert into game_mood (appid, moods, checked_at, endless) values (10, '{}', now(), true)");
|
||||
|
||||
QuestionFacts facts = only(10);
|
||||
|
||||
assertThat(facts.normallyMinutes()).isEqualTo(900);
|
||||
assertThat(facts.endless()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void facts_ReadTheSnoozes() {
|
||||
repository.snooze(USER, 10, Question.START, 100);
|
||||
repository.snooze(USER, 10, Question.FINISHED, 700);
|
||||
repository.snooze(USER, 10, Question.DROPPED, 120);
|
||||
repository.snooze(USER, 10, Question.RATE, 120);
|
||||
|
||||
QuestionFacts facts = only(10);
|
||||
|
||||
assertThat(facts.startAskedAt()).isEqualTo(100);
|
||||
assertThat(facts.finishedAskedAt()).isEqualTo(700);
|
||||
assertThat(facts.droppedAnsweredAt()).isNotNull();
|
||||
assertThat(facts.rateSkipped()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void facts_OnlyTheCountedGamesOfTheUser() {
|
||||
game(USER, 20, "BACKLOG", 0);
|
||||
game(USER, 30, "BACKLOG", 0);
|
||||
game(OTHER_USER, 40, "BACKLOG", 0);
|
||||
jdbc.update("update user_game set hidden_at = now() where user_id = ? and appid = 20", USER);
|
||||
jdbc.update("update user_game set removed_at = now() where user_id = ? and appid = 30", USER);
|
||||
|
||||
assertThat(repository.facts(USER, null)).extracting(QuestionFacts::appid).containsExactly(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void facts_LinkedSetGivesOnlyTheCountedEdition() {
|
||||
game(USER, 20, "BACKLOG", 0);
|
||||
jdbc.update("insert into game_link (appid, set_id) values (10, 1), (20, 1)");
|
||||
|
||||
assertThat(repository.facts(USER, null)).extracting(QuestionFacts::appid).containsExactly(10L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void facts_AppidFilter() {
|
||||
game(USER, 20, "BACKLOG", 0);
|
||||
|
||||
assertThat(repository.facts(USER, 20L)).extracting(QuestionFacts::appid).containsExactly(20L);
|
||||
assertThat(repository.facts(USER, null)).hasSize(2);
|
||||
assertThat(repository.facts(USER, 99L)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void snooze_OverwritesPlaytimeAndTime() {
|
||||
repository.snooze(USER, 10, Question.START, 100);
|
||||
jdbc.update("update user_game_question set answered_at = now() - interval '1 day'");
|
||||
|
||||
repository.snooze(USER, 10, Question.START, 400);
|
||||
|
||||
assertThat(jdbc.queryForList("select playtime_minutes from user_game_question", Integer.class)).containsExactly(400);
|
||||
assertThat(jdbc.queryForObject("select answered_at > now() - interval '1 hour' from user_game_question", Boolean.class))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void snooze_GoesWithTheGameRow() {
|
||||
repository.snooze(USER, 10, Question.START, 100);
|
||||
|
||||
jdbc.update("update user_game set appid = 50 where appid = 10");
|
||||
assertThat(jdbc.queryForList("select appid from user_game_question", Long.class)).containsExactly(50L);
|
||||
|
||||
jdbc.update("delete from user_game where appid = 50");
|
||||
assertThat(jdbc.queryForObject("select count(*) from user_game_question", Integer.class)).isZero();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user