feat(library): add hand-ordered backlog with next-up picker source
Build, Test & Push Core Service / quality-check (push) Successful in 1m25s
Build, Test & Push Core Service / docker (push) Successful in 1m29s

This commit is contained in:
lobo committed 2026-10-06 09:06:51 +02:00
1 parent 7b538b7f30
commit 7747bc7b8b
22 files changed
+754 -20

No files matched your search

@@ -232,6 +232,18 @@ class GameMergeFlowTest extends AbstractIntegrationTest {
"STARTED " + MANUAL, "STARTED " + STEAM);
}
@Test
void merge_BothInBacklog_KeptGameTakesTheBetterPosition() throws Exception {
manualRow(AUDITOR_ID, MANUAL, GameStatus.BACKLOG);
userGameRepository.save(new UserGame(AUDITOR_ID, STEAM_2, 0, Instant.now(), Instant.now()));
userGameRepository.save(new UserGame(AUDITOR_ID, STEAM, 0, Instant.now(), Instant.now()));
merge(Role.AUDITOR, MANUAL, STEAM).andExpect(status().isOk());
assertThat(jdbc.queryForList("select appid from backlog_order where user_id = ? order by position, appid",
Long.class, AUDITOR_ID)).containsExactly(STEAM, STEAM_2);
}
@Test
void userWithBothRows_KeepsTheSteamRowWithTheNewerHandSetFields() throws Exception {
UserGame steam = new UserGame(AUDITOR_ID, STEAM, 300, Instant.now(), Instant.now());
@@ -0,0 +1,171 @@
package core_service.modules.library.api;
import core_service.configuration.security.ClientHeaderFilter;
import core_service.modules.library.domain.GameStatus;
import core_service.modules.library.domain.SteamGame;
import core_service.modules.library.domain.UserGame;
import core_service.modules.library.repo.GameReleaseRepository;
import core_service.modules.library.repo.SteamGameRepository;
import core_service.modules.library.repo.UserGameRepository;
import core_service.shared.test.AbstractMockedLibraryServicesTest;
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.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.stream.Collectors;
import java.util.stream.LongStream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasSize;
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.
*/
@AutoConfigureMockMvc
class LibraryControllerBacklogOrderTest extends AbstractMockedLibraryServicesTest {
private static final long USER_ID = 1L;
@Autowired
private MockMvc mockMvc;
@Autowired
private SteamGameRepository steamGameRepository;
@Autowired
private UserGameRepository userGameRepository;
@Autowired
private GameReleaseRepository gameReleaseRepository;
@BeforeEach
void clean() {
gameReleaseRepository.deleteAll();
userGameRepository.deleteAll();
}
private void game(long userId, long appid, GameStatus status) {
steamGameRepository.save(new SteamGame(appid, "Game " + appid, null, Instant.now()));
UserGame game = new UserGame(userId, appid, 0, Instant.now(), Instant.now());
game.setStatus(status);
userGameRepository.save(game);
}
private ResultActions putOrder(String appidsJson) throws Exception {
return mockMvc.perform(put("/api/library/backlog").header(ClientHeaderFilter.CLIENT_HEADER, "test")
.contentType(MediaType.APPLICATION_JSON).content("{\"appids\":" + appidsJson + "}"));
}
@Test
void getBacklog_ReturnsTheGamesInOrder_AndLimitReturnsTheFirst() throws Exception {
game(USER_ID, 10, GameStatus.BACKLOG);
game(USER_ID, 20, GameStatus.BACKLOG);
game(USER_ID, 30, GameStatus.BACKLOG);
game(USER_ID, 40, GameStatus.PLAYING);
mockMvc.perform(get("/api/library/backlog"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.games[*].appid", contains(10, 20, 30)));
mockMvc.perform(get("/api/library/backlog").param("limit", "2"))
.andExpect(jsonPath("$.games[*].appid", contains(10, 20)));
}
@Test
void getBacklog_ComingGame_IsShownAndCanBeReordered_ReleasedLeavesItOut() throws Exception {
LocalDate berlinToday = LocalDate.now(ZoneId.of("Europe/Berlin"));
game(USER_ID, 10, GameStatus.BACKLOG);
game(USER_ID, 20, GameStatus.BACKLOG);
gameReleaseRepository.upsert(20L, berlinToday.plusDays(1), false, Instant.now());
putOrder("[20,10]").andExpect(status().isNoContent());
mockMvc.perform(get("/api/library/backlog"))
.andExpect(jsonPath("$.games[*].appid", contains(20, 10)))
.andExpect(jsonPath("$.games[0].coming").value(true));
mockMvc.perform(get("/api/library/backlog").param("released", "true"))
.andExpect(jsonPath("$.games[*].appid", contains(10)));
}
@Test
void putBacklog_ThenGet_ReturnsTheNewOrder() throws Exception {
game(USER_ID, 10, GameStatus.BACKLOG);
game(USER_ID, 20, GameStatus.BACKLOG);
game(USER_ID, 30, GameStatus.BACKLOG);
putOrder("[30,10,20]").andExpect(status().isNoContent());
mockMvc.perform(get("/api/library/backlog"))
.andExpect(jsonPath("$.games[*].appid", contains(30, 10, 20)));
}
@Test
void putBacklog_AnotherUsersAppid_DoesNotChangeThatUsersOrder() throws Exception {
game(USER_ID, 10, GameStatus.BACKLOG);
game(2L, 20, GameStatus.BACKLOG);
game(2L, 30, GameStatus.BACKLOG);
putOrder("[30,10,20]").andExpect(status().isNoContent());
mockMvc.perform(get("/api/library/backlog"))
.andExpect(jsonPath("$.games[*].appid", contains(10)));
assertThat(userGameRepository.findBacklogInOrder(2L, false, 10))
.extracting(UserGame::getAppid).containsExactly(20L, 30L);
}
@Test
void putBacklog_PartialList_LeavesTheMissingGamesAfterTheSentOnes() throws Exception {
game(USER_ID, 10, GameStatus.BACKLOG);
game(USER_ID, 20, GameStatus.BACKLOG);
game(USER_ID, 30, GameStatus.BACKLOG);
putOrder("[30]").andExpect(status().isNoContent());
mockMvc.perform(get("/api/library/backlog"))
.andExpect(jsonPath("$.games[*].appid", contains(30, 10, 20)));
}
@Test
void getBacklog_Empty_ReturnsNoGames() throws Exception {
mockMvc.perform(get("/api/library/backlog"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.games", hasSize(0)));
}
@Test
void putBacklog_NullElement_Is400() throws Exception {
putOrder("[10,null]").andExpect(status().isBadRequest());
}
@Test
void putBacklog_MoreThan20000Appids_Is400() throws Exception {
String tooMany = LongStream.rangeClosed(1, 20001).mapToObj(Long::toString)
.collect(Collectors.joining(",", "[", "]"));
putOrder(tooMany).andExpect(status().isBadRequest());
}
@Test
void getBacklog_GameSetToFinished_IsGone() throws Exception {
game(USER_ID, 10, GameStatus.BACKLOG);
game(USER_ID, 20, GameStatus.BACKLOG);
mockMvc.perform(put("/api/library/games/10/status").header(ClientHeaderFilter.CLIENT_HEADER, "test")
.contentType(MediaType.APPLICATION_JSON).content("{\"status\":\"FINISHED\"}"))
.andExpect(status().isOk());
mockMvc.perform(get("/api/library/backlog"))
.andExpect(jsonPath("$.games[*].appid", contains(20)));
}
}
@@ -1,5 +1,6 @@
package core_service.modules.library.api;
import core_service.configuration.security.ClientHeaderFilter;
import core_service.modules.library.domain.GamePriority;
import core_service.modules.library.domain.GameStatus;
import core_service.modules.library.domain.GameTimeToBeat;
@@ -11,24 +12,30 @@ import core_service.modules.library.repo.GameReleaseRepository;
import core_service.modules.library.repo.GameSpeedrunRepository;
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.UserGameRepository;
import core_service.shared.test.AbstractMockedLibraryServicesTest;
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.test.web.servlet.MockMvc;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Arrays;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
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;
@@ -63,9 +70,14 @@ class LibraryControllerPickerTest extends AbstractMockedLibraryServicesTest {
@Autowired
private GameReleaseRepository gameReleaseRepository;
@Autowired
private UserGameAchievementMetaRepository achievementMetaRepository;
@BeforeEach
void clean() {
gameReleaseRepository.deleteAll();
// A game with unlocked achievements is never coming; other test classes leave such rows.
achievementMetaRepository.deleteAll();
userGameRepository.deleteAll();
gameTimeToBeatRepository.deleteAll();
gameMoodsRepository.deleteAll();
@@ -306,4 +318,47 @@ class LibraryControllerPickerTest extends AbstractMockedLibraryServicesTest {
mockMvc.perform(get("/api/library/picker"))
.andExpect(jsonPath("$.games[*].appid", containsInAnyOrder(10, 20)));
}
private void putOrder(Long... appids) throws Exception {
mockMvc.perform(put("/api/library/backlog").header(ClientHeaderFilter.CLIENT_HEADER, "test")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"appids\":" + Arrays.toString(appids) + "}"))
.andExpect(status().isNoContent());
}
@Test
void picker_FourSlots_NextUpComesFromTopFiveWithReason() throws Exception {
for (long appid = 1; appid <= 7; appid++) {
game(appid, GameStatus.BACKLOG, null, null);
}
putOrder(7L, 6L, 5L, 4L, 3L, 2L, 1L);
mockMvc.perform(get("/api/library/picker").param("count", "4"))
.andExpect(jsonPath("$.games", hasSize(4)))
.andExpect(jsonPath("$.games[3].reason.source").value("NEXT_UP"))
.andExpect(jsonPath("$.games[3].appid", anyOf(is(7), is(6), is(5), is(4), is(3))));
}
@Test
void picker_NextUp_SkipsExcludedAndFilteredGames() throws Exception {
for (long appid = 1; appid <= 12; appid++) {
game(appid, GameStatus.BACKLOG, null, 60);
}
mockMvc.perform(get("/api/library/picker").param("count", "4").param("exclude", "1", "2", "3", "4", "5"))
.andExpect(jsonPath("$.games[3].reason.source").value("NEXT_UP"))
.andExpect(jsonPath("$.games[3].appid", anyOf(is(6), is(7), is(8), is(9), is(10))));
}
@Test
void picker_NextUp_NeverPicksAComingGame() throws Exception {
LocalDate berlinToday = LocalDate.now(ZoneId.of("Europe/Berlin"));
game(10L, GameStatus.BACKLOG, null, null);
game(20L, GameStatus.BACKLOG, null, null);
gameReleaseRepository.upsert(10L, berlinToday.plusDays(1), false, Instant.now());
mockMvc.perform(get("/api/library/picker").param("count", "4"))
.andExpect(jsonPath("$.games", hasSize(1)))
.andExpect(jsonPath("$.games[0].appid").value(20));
}
}
@@ -0,0 +1,36 @@
package core_service.modules.library.domain;
import core_service.shared.test.AbstractUnitTest;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
class BacklogOrderTest extends AbstractUnitTest {
@Test
void reorder_KeepsTheSentOrder() {
assertEquals(List.of(3L, 1L, 2L), BacklogOrder.reorder(List.of(1L, 2L, 3L), List.of(3L, 1L, 2L)));
}
@Test
void reorder_IgnoresUnknownAppids() {
assertEquals(List.of(2L, 1L), BacklogOrder.reorder(List.of(1L, 2L), List.of(99L, 2L, 1L, 98L)));
}
@Test
void reorder_KeepsADuplicateOnce() {
assertEquals(List.of(2L, 1L), BacklogOrder.reorder(List.of(1L, 2L), List.of(2L, 2L, 1L, 2L)));
}
@Test
void reorder_AppendsTheMissingInTheirOldOrder() {
assertEquals(List.of(4L, 1L, 2L, 3L, 5L), BacklogOrder.reorder(List.of(1L, 2L, 3L, 4L, 5L), List.of(4L)));
}
@Test
void reorder_EmptyWanted_KeepsCurrent() {
assertEquals(List.of(1L, 2L, 3L), BacklogOrder.reorder(List.of(1L, 2L, 3L), List.of()));
}
}
@@ -9,6 +9,7 @@ import java.util.Arrays;
import java.util.List;
import static core_service.modules.library.domain.PickSource.LONGEST;
import static core_service.modules.library.domain.PickSource.NEXT_UP;
import static core_service.modules.library.domain.PickSource.RANDOM;
import static core_service.modules.library.domain.PickSource.SIMILAR;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -34,7 +35,7 @@ class PickMixTest extends AbstractUnitTest {
@Test
void mix_ThreeSources_OneGameEachInSlotOrder() {
List<Pick> picks = PickMix.mix(games(1), games(2), games(3), 3);
List<Pick> picks = PickMix.mix(games(1), games(2), games(3), List.of(), 3);
assertEquals(List.of(SIMILAR, LONGEST, RANDOM), sources(picks));
assertEquals(List.of(1L, 2L, 3L), appids(picks));
@@ -42,15 +43,16 @@ class PickMixTest extends AbstractUnitTest {
@Test
void mix_MoreSlotsThanSources_RotationContinues() {
List<Pick> picks = PickMix.mix(games(1, 4, 7), games(2, 5), games(3, 6), 7);
List<Pick> picks = PickMix.mix(games(1, 5, 9), games(2, 6), games(3, 7), games(4, 8), 9);
assertEquals(List.of(SIMILAR, LONGEST, RANDOM, SIMILAR, LONGEST, RANDOM, SIMILAR), sources(picks));
assertEquals(List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L), appids(picks));
assertEquals(List.of(SIMILAR, LONGEST, RANDOM, NEXT_UP, SIMILAR, LONGEST, RANDOM, NEXT_UP, SIMILAR),
sources(picks));
assertEquals(List.of(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L), appids(picks));
}
@Test
void mix_SameGameFirstInAllLists_AppearsOnce() {
List<Pick> picks = PickMix.mix(games(1, 4), games(1, 5), games(1, 6), 3);
List<Pick> picks = PickMix.mix(games(1, 4), games(1, 5), games(1, 6), List.of(), 3);
assertEquals(List.of(1L, 5L, 6L), appids(picks));
assertEquals(List.of(SIMILAR, LONGEST, RANDOM), sources(picks));
@@ -58,7 +60,7 @@ class PickMixTest extends AbstractUnitTest {
@Test
void mix_EmptySimilar_SlotComesFromRandom() {
List<Pick> picks = PickMix.mix(List.of(), games(2), games(3, 4), 3);
List<Pick> picks = PickMix.mix(List.of(), games(2), games(3, 4), List.of(), 3);
assertEquals(List.of(RANDOM, LONGEST, RANDOM), sources(picks));
assertEquals(List.of(3L, 2L, 4L), appids(picks));
@@ -66,7 +68,7 @@ class PickMixTest extends AbstractUnitTest {
@Test
void mix_ListsShorterThanCount_ReturnsAllDistinctGames() {
List<Pick> picks = PickMix.mix(games(1), games(1, 2), games(2, 3), 10);
List<Pick> picks = PickMix.mix(games(1), games(1, 2), games(2, 3), List.of(), 10);
assertEquals(List.of(1L, 2L, 3L), appids(picks).stream().sorted().toList());
assertEquals(3, picks.size());
@@ -74,6 +76,22 @@ class PickMixTest extends AbstractUnitTest {
@Test
void mix_AllEmpty_ReturnsNothing() {
assertTrue(PickMix.mix(List.of(), List.of(), List.of(), 3).isEmpty());
assertTrue(PickMix.mix(List.of(), List.of(), List.of(), List.of(), 3).isEmpty());
}
@Test
void mix_FourSources_NextUpIsTheFourthSlot() {
List<Pick> picks = PickMix.mix(games(1), games(2), games(3), games(4), 4);
assertEquals(List.of(SIMILAR, LONGEST, RANDOM, NEXT_UP), sources(picks));
assertEquals(List.of(1L, 2L, 3L, 4L), appids(picks));
}
@Test
void mix_EmptyNextUp_SlotComesFromRandom() {
List<Pick> picks = PickMix.mix(games(1), games(2), games(3, 4), List.of(), 4);
assertEquals(List.of(SIMILAR, LONGEST, RANDOM, RANDOM), sources(picks));
assertEquals(List.of(1L, 2L, 3L, 4L), appids(picks));
}
}
@@ -0,0 +1,82 @@
package core_service.modules.library.repo;
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;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@Import(BacklogOrderRepository.class)
class BacklogOrderRepositoryTest extends AbstractPersistenceTest {
private static final long USER = 1L;
@Autowired
private BacklogOrderRepository backlogOrderRepository;
@Autowired
private UserGameRepository userGameRepository;
@Autowired
private JdbcTemplate jdbc;
@BeforeEach
void seed() {
jdbc.update("delete from game_link");
jdbc.update("delete from game_release");
jdbc.update("delete from user_game");
for (long appid : List.of(10L, 20L, 30L, 40L)) {
jdbc.update("insert into steam_game (appid, name) values (?, ?) on conflict do nothing", appid, "Game " + appid);
}
for (long appid : List.of(10L, 20L, 30L)) {
jdbc.update("insert into user_game (user_id, appid, status) values (?, ?, 'BACKLOG')", USER, appid);
}
}
private List<Long> backlog(boolean released) {
return userGameRepository.findBacklogInOrder(USER, released, Integer.MAX_VALUE).stream()
.map(UserGame::getAppid).toList();
}
@Test
void writeOrder_AfterLockOrder_GivesPositionsOneToN() {
assertThat(backlogOrderRepository.lockOrder(USER)).containsExactly(10L, 20L, 30L);
backlogOrderRepository.writeOrder(USER, List.of(30L, 10L, 20L));
assertThat(jdbc.queryForList("select position from backlog_order where user_id = ? order by position",
Long.class, USER)).containsExactly(1L, 2L, 3L);
assertThat(backlogOrderRepository.lockOrder(USER)).containsExactly(30L, 10L, 20L);
}
@Test
void findBacklogInOrder_FollowsThePosition_AndLimit() {
backlogOrderRepository.writeOrder(USER, List.of(30L, 10L, 20L));
assertThat(backlog(false)).containsExactly(30L, 10L, 20L);
assertThat(userGameRepository.findBacklogInOrder(USER, false, 2)).extracting(UserGame::getAppid)
.containsExactly(30L, 10L);
}
@Test
void findBacklogInOrder_LeavesOutALesserEditionOfALinkedSet() {
jdbc.update("insert into user_game (user_id, appid, status, playtime_minutes) values (?, 40, 'BACKLOG', 100)", USER);
jdbc.update("insert into game_link (appid, set_id) values (10, 1), (40, 1)");
assertThat(backlog(false)).containsExactly(20L, 30L, 40L);
}
@Test
void findBacklogInOrder_ReleasedLeavesOutAComingGame() {
jdbc.update("insert into game_release (appid, release_date, fetched_at) values (20, current_date + 30, now())");
assertThat(backlog(true)).containsExactly(10L, 30L);
assertThat(backlog(false)).containsExactly(10L, 20L, 30L);
}
}
@@ -0,0 +1,131 @@
package core_service.modules.library.repo;
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.jdbc.core.JdbcTemplate;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
/**
* The trigger of V34 that keeps backlog_order.
*/
class BacklogOrderTriggerTest extends AbstractPersistenceTest {
private static final long USER = 1L;
private static final long OTHER_USER = 2L;
@Autowired
private JdbcTemplate jdbc;
@BeforeEach
void seed() {
jdbc.update("delete from user_game");
for (long appid = 1; appid <= 300; appid++) {
jdbc.update("insert into steam_game (appid, name) values (?, ?) on conflict do nothing", appid, "Game " + appid);
}
}
private void own(long userId, long appid, String status) {
jdbc.update("insert into user_game (user_id, appid, status) values (?, ?, ?)", userId, appid, status);
}
private void setStatus(long appid, String status) {
jdbc.update("update user_game set status = ? where user_id = ? and appid = ?", status, USER, appid);
}
private List<Long> order(long userId) {
return jdbc.queryForList("select appid from backlog_order where user_id = ? order by position, appid",
Long.class, userId);
}
@Test
void insertedBacklogGame_GoesToTheBottom() {
own(USER, 1, "BACKLOG");
own(USER, 2, "BACKLOG");
own(OTHER_USER, 3, "BACKLOG");
assertThat(order(USER)).containsExactly(1L, 2L);
assertThat(order(OTHER_USER)).containsExactly(3L);
assertThat(jdbc.queryForObject("select position from backlog_order where user_id = ? and appid = 3",
Long.class, OTHER_USER)).isEqualTo(1L);
}
@Test
void insertedPlayingGame_HasNoRow() {
own(USER, 1, "PLAYING");
assertThat(order(USER)).isEmpty();
}
@Test
void leavingBacklog_DeletesTheRow_AndComingBackPutsItAtTheBottom() {
own(USER, 1, "BACKLOG");
own(USER, 2, "BACKLOG");
setStatus(1, "FINISHED");
assertThat(order(USER)).containsExactly(2L);
setStatus(1, "BACKLOG");
assertThat(order(USER)).containsExactly(2L, 1L);
}
@Test
void hideAndRemove_DeleteTheRow_AndUnhidePutsItAtTheBottom() {
own(USER, 1, "BACKLOG");
own(USER, 2, "BACKLOG");
own(USER, 3, "BACKLOG");
jdbc.update("update user_game set hidden_at = now() where user_id = ? and appid = 1", USER);
jdbc.update("update user_game set removed_at = now() where user_id = ? and appid = 2", USER);
assertThat(order(USER)).containsExactly(3L);
jdbc.update("update user_game set hidden_at = null where user_id = ? and appid = 1", USER);
assertThat(order(USER)).containsExactly(3L, 1L);
}
@Test
void updateWithoutStatusHideOrRemoveChange_KeepsThePosition() {
own(USER, 1, "BACKLOG");
own(USER, 2, "BACKLOG");
jdbc.update("update backlog_order set position = 5 where user_id = ? and appid = 1", USER);
jdbc.update("update user_game set status = 'BACKLOG', notes = 'x', hidden_at = null where user_id = ? and appid = 1",
USER);
assertThat(order(USER)).containsExactly(2L, 1L);
}
@Test
void manyNewGamesAfterAReorder_KeepTheFirstPositions() {
own(USER, 1, "BACKLOG");
own(USER, 2, "BACKLOG");
own(USER, 3, "BACKLOG");
jdbc.update("update backlog_order set position = 3 where user_id = ? and appid = 1", USER);
jdbc.update("update backlog_order set position = 2 where user_id = ? and appid = 2", USER);
jdbc.update("update backlog_order set position = 1 where user_id = ? and appid = 3", USER);
for (long appid = 100; appid < 300; appid++) {
own(USER, appid, "BACKLOG");
}
List<Long> expected = new ArrayList<>(List.of(3L, 2L, 1L));
for (long appid = 100; appid < 300; appid++) {
expected.add(appid);
}
assertThat(order(USER)).isEqualTo(expected);
}
@Test
void deletingTheGame_DeletesTheOrderRow() {
own(USER, 1, "BACKLOG");
jdbc.update("delete from user_game where user_id = ? and appid = 1", USER);
assertThat(order(USER)).isEmpty();
}
}