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:
lobo committed 2026-10-09 09:31:48 +02:00
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();
}
}