Public Access
feat(picker): add FRIENDS row from followed users
Backlog games a followed user finished or played >= 5 h. One game per friend; strongest signal first (finished, then playtime). Requires an active follow of a public profile that shows SUMMARY or STATUS. Skips DROPPED games, blocked and deleted users, hidden and removed games.
This commit is contained in:
1 parent
117ff61730
commit
e87f40bba0
8 files changed
+481
-18
No files matched your search
+344
@@ -0,0 +1,344 @@
|
||||
package core_service.modules.library.api;
|
||||
|
||||
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.repo.GameReleaseRepository;
|
||||
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.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.hasItem;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* The FRIENDS picker row. The dev profile authenticates every request as user 1.
|
||||
*/
|
||||
@AutoConfigureMockMvc
|
||||
class LibraryControllerPickerFriendsTest extends AbstractMockedLibraryServicesTest {
|
||||
|
||||
private static final long USER_ID = 1L;
|
||||
private static final long MAX = 950_001L;
|
||||
private static final long ANNA = 950_002L;
|
||||
private static final long TOM = 950_003L;
|
||||
private static final long[] FRIENDS = {MAX, ANNA, TOM, 950_004L, 950_005L, 950_006L, 950_007L};
|
||||
private static final String ROW = "$.categories[?(@.source == 'FRIENDS')]";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@Autowired
|
||||
private SteamGameRepository steamGameRepository;
|
||||
|
||||
@Autowired
|
||||
private UserGameRepository userGameRepository;
|
||||
|
||||
@Autowired
|
||||
private GameTimeToBeatRepository gameTimeToBeatRepository;
|
||||
|
||||
@Autowired
|
||||
private GameReleaseRepository gameReleaseRepository;
|
||||
|
||||
@Autowired
|
||||
private UserGameAchievementMetaRepository achievementMetaRepository;
|
||||
|
||||
@BeforeEach
|
||||
void clean() {
|
||||
for (long id : FRIENDS) {
|
||||
jdbc.update("delete from follow where user_id = ? or followed_user_id = ?", id, id);
|
||||
jdbc.update("delete from public_user where user_id = ?", id);
|
||||
jdbc.update("delete from blocked_user where user_id = ?", id);
|
||||
jdbc.update("delete from deleted_user where user_id = ?", id);
|
||||
jdbc.update("delete from account_name where user_id = ?", id);
|
||||
}
|
||||
jdbc.update("delete from follow where user_id = ?", USER_ID);
|
||||
gameReleaseRepository.deleteAll();
|
||||
achievementMetaRepository.deleteAll();
|
||||
userGameRepository.deleteAll();
|
||||
gameTimeToBeatRepository.deleteAll();
|
||||
}
|
||||
|
||||
private void mine(long appid, GameStatus status, Integer normallyMinutes) {
|
||||
steamGameRepository.save(new SteamGame(appid, "Game " + appid, null, Instant.now()));
|
||||
save(USER_ID, appid, status, 0);
|
||||
if (normallyMinutes != null) {
|
||||
gameTimeToBeatRepository.save(new GameTimeToBeat(appid, null, normallyMinutes, null, Instant.now()));
|
||||
}
|
||||
}
|
||||
|
||||
private void friendGame(long userId, long appid, GameStatus status, int playtime) {
|
||||
steamGameRepository.save(new SteamGame(appid, "Game " + appid, null, Instant.now()));
|
||||
save(userId, appid, status, playtime);
|
||||
}
|
||||
|
||||
private void save(long userId, long appid, GameStatus status, int playtime) {
|
||||
UserGame game = new UserGame(userId, appid, playtime, Instant.now(), Instant.now());
|
||||
game.setStatus(status);
|
||||
userGameRepository.save(game);
|
||||
}
|
||||
|
||||
/** A public friend the caller follows; null sections show everything. */
|
||||
private void friend(long userId, String name, String[] sections) {
|
||||
jdbc.update("insert into account_name (user_id, display_name) values (?, ?)", userId, name);
|
||||
jdbc.update("insert into public_user (user_id, sections) values (?, ?)", userId, sections);
|
||||
jdbc.update("insert into follow (user_id, followed_user_id) values (?, ?)", USER_ID, userId);
|
||||
}
|
||||
|
||||
private ResultActions pick(String... params) throws Exception {
|
||||
var request = get("/api/library/picker").param("category", "FRIENDS");
|
||||
for (int i = 0; i < params.length; i += 2) {
|
||||
request.param(params[i], params[i + 1]);
|
||||
}
|
||||
return mockMvc.perform(request).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
private void expectNoRow() throws Exception {
|
||||
pick().andExpect(jsonPath("$.categories", hasSize(0)));
|
||||
}
|
||||
|
||||
private static String reason(long appid, String field) {
|
||||
return ROW + ".games[?(@.appid == " + appid + ")].reason." + field;
|
||||
}
|
||||
|
||||
@Test
|
||||
void friends_FinishedAndPlayedGames_WithReason() throws Exception {
|
||||
mine(10L, GameStatus.BACKLOG, null);
|
||||
mine(20L, GameStatus.BACKLOG, null);
|
||||
mine(30L, GameStatus.BACKLOG, null);
|
||||
friend(MAX, "Max", null);
|
||||
friend(ANNA, "Anna", null);
|
||||
friend(TOM, "Tom", null);
|
||||
friendGame(MAX, 10L, GameStatus.FINISHED, 100);
|
||||
friendGame(ANNA, 20L, GameStatus.PLAYING, 2400);
|
||||
friendGame(TOM, 30L, GameStatus.PLAYING, 60);
|
||||
|
||||
pick().andExpect(jsonPath(ROW + ".games[*].appid", contains(10, 20)))
|
||||
.andExpect(jsonPath(reason(10, "source"), contains("FRIENDS")))
|
||||
.andExpect(jsonPath(reason(10, "friendName"), contains("Max")))
|
||||
.andExpect(jsonPath(reason(10, "friendFinished"), contains(true)))
|
||||
.andExpect(jsonPath(reason(20, "friendName"), contains("Anna")))
|
||||
.andExpect(jsonPath(reason(20, "friendFinished"), contains(false)))
|
||||
.andExpect(jsonPath(reason(20, "friendPlaytimeMinutes"), contains(2400)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void friends_UsesTheCurrentDisplayName() throws Exception {
|
||||
mine(10L, GameStatus.BACKLOG, null);
|
||||
friend(MAX, "Max", null);
|
||||
friendGame(MAX, 10L, GameStatus.FINISHED, 100);
|
||||
jdbc.update("update account_name set display_name = 'Maximilian' where user_id = ?", MAX);
|
||||
|
||||
pick().andExpect(jsonPath(reason(10, "friendName"), contains("Maximilian")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void friends_NeverSendsAUserId() throws Exception {
|
||||
mine(10L, GameStatus.BACKLOG, null);
|
||||
friend(MAX, "Max", null);
|
||||
friendGame(MAX, 10L, GameStatus.FINISHED, 100);
|
||||
|
||||
String body = pick().andReturn().getResponse().getContentAsString();
|
||||
org.assertj.core.api.Assertions.assertThat(body).doesNotContain(String.valueOf(MAX)).doesNotContain("friendUserId");
|
||||
}
|
||||
|
||||
@Test
|
||||
void friends_OneGamePerFriend_StrongestFriendPerGame() throws Exception {
|
||||
mine(10L, GameStatus.BACKLOG, null);
|
||||
mine(20L, GameStatus.BACKLOG, null);
|
||||
mine(30L, GameStatus.BACKLOG, null);
|
||||
mine(40L, GameStatus.BACKLOG, null);
|
||||
friend(MAX, "Max", null);
|
||||
friend(ANNA, "Anna", null);
|
||||
friendGame(MAX, 10L, GameStatus.FINISHED, 900);
|
||||
friendGame(MAX, 20L, GameStatus.FINISHED, 100);
|
||||
friendGame(MAX, 30L, GameStatus.FINISHED, 50);
|
||||
friendGame(ANNA, 10L, GameStatus.PLAYING, 600);
|
||||
friendGame(ANNA, 40L, GameStatus.PLAYING, 400);
|
||||
|
||||
pick().andExpect(jsonPath(ROW + ".games[*].appid", contains(10, 40)))
|
||||
.andExpect(jsonPath(ROW + ".games[*].reason.friendName", contains("Max", "Anna")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void friends_ExactlyFiveHours_Counts() throws Exception {
|
||||
mine(10L, GameStatus.BACKLOG, null);
|
||||
mine(20L, GameStatus.BACKLOG, null);
|
||||
friend(MAX, "Max", null);
|
||||
friend(ANNA, "Anna", null);
|
||||
friendGame(MAX, 10L, GameStatus.PLAYING, 300);
|
||||
friendGame(ANNA, 20L, GameStatus.PLAYING, 299);
|
||||
|
||||
pick().andExpect(jsonPath(ROW + ".games[*].appid", contains(10)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void friends_Reroll_ExcludesShownAndEndsWhenNoneLeft() throws Exception {
|
||||
mine(10L, GameStatus.BACKLOG, null);
|
||||
mine(20L, GameStatus.BACKLOG, null);
|
||||
friend(MAX, "Max", null);
|
||||
friendGame(MAX, 10L, GameStatus.FINISHED, 900);
|
||||
friendGame(MAX, 20L, GameStatus.FINISHED, 100);
|
||||
|
||||
pick().andExpect(jsonPath(ROW + ".games[*].appid", contains(10)));
|
||||
pick("exclude", "10").andExpect(jsonPath(ROW + ".games[*].appid", contains(20)));
|
||||
pick("exclude", "10,20").andExpect(jsonPath("$.categories", hasSize(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void friends_RowSitsBetweenStudioAndLongest() throws Exception {
|
||||
mine(10L, GameStatus.BACKLOG, null);
|
||||
friend(MAX, "Max", null);
|
||||
friendGame(MAX, 10L, GameStatus.FINISHED, 100);
|
||||
|
||||
mockMvc.perform(get("/api/library/picker")).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.categories[*].source", contains("NEXT_UP", "FRIENDS", "LONGEST", "RANDOM")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void friends_NoFollows_LeavesOutTheRow() throws Exception {
|
||||
mine(10L, GameStatus.BACKLOG, null);
|
||||
|
||||
mockMvc.perform(get("/api/library/picker")).andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.categories[*].source", not(hasItem("FRIENDS"))));
|
||||
}
|
||||
|
||||
private enum Hidden { NOT_FOLLOWED, NOT_PUBLIC, BLOCKED, DELETED, ONLY_FOLLOWS_ME }
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(Hidden.class)
|
||||
void friends_NeverShows(Hidden case_) throws Exception {
|
||||
mine(10L, GameStatus.BACKLOG, null);
|
||||
jdbc.update("insert into account_name (user_id, display_name) values (?, 'Max')", MAX);
|
||||
if (case_ != Hidden.NOT_PUBLIC) {
|
||||
jdbc.update("insert into public_user (user_id) values (?)", MAX);
|
||||
}
|
||||
switch (case_) {
|
||||
case NOT_FOLLOWED, NOT_PUBLIC -> { }
|
||||
case BLOCKED -> jdbc.update("insert into blocked_user (user_id) values (?)", MAX);
|
||||
case DELETED -> jdbc.update("insert into deleted_user (user_id) values (?)", MAX);
|
||||
case ONLY_FOLLOWS_ME -> jdbc.update("insert into follow (user_id, followed_user_id) values (?, ?)", MAX, USER_ID);
|
||||
}
|
||||
if (case_ != Hidden.NOT_FOLLOWED && case_ != Hidden.ONLY_FOLLOWS_ME) {
|
||||
jdbc.update("insert into follow (user_id, followed_user_id) values (?, ?)", USER_ID, MAX);
|
||||
}
|
||||
friendGame(MAX, 10L, GameStatus.FINISHED, 2400);
|
||||
|
||||
expectNoRow();
|
||||
jdbc.update("delete from follow where user_id = ?", MAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
void friends_HiddenSections_ShownOnlyWithSummaryOrStatus() throws Exception {
|
||||
for (long appid = 10; appid <= 60; appid += 10) {
|
||||
mine(appid, GameStatus.BACKLOG, null);
|
||||
}
|
||||
friend(MAX, "Stats", new String[]{"STATS"});
|
||||
friend(ANNA, "Summary", new String[]{"SUMMARY"});
|
||||
friend(TOM, "FinishedOnly", new String[]{"FINISHED"});
|
||||
friend(950_004L, "All", null);
|
||||
friend(950_005L, "Status", new String[]{"STATUS"});
|
||||
friend(950_006L, "PlayingFinished", new String[]{"PLAYING", "FINISHED"});
|
||||
friendGame(MAX, 10L, GameStatus.FINISHED, 2400);
|
||||
friendGame(ANNA, 20L, GameStatus.FINISHED, 2400);
|
||||
friendGame(TOM, 30L, GameStatus.FINISHED, 2400);
|
||||
friendGame(950_004L, 40L, GameStatus.FINISHED, 2400);
|
||||
friendGame(950_005L, 50L, GameStatus.FINISHED, 2400);
|
||||
friendGame(950_006L, 60L, GameStatus.FINISHED, 2400);
|
||||
|
||||
pick().andExpect(jsonPath(ROW + ".games[*].appid", containsInAnyOrder(20, 40, 50)));
|
||||
pick("exclude", "20,40,50").andExpect(jsonPath("$.categories", hasSize(0)));
|
||||
for (long appid : new long[]{20, 40, 50}) {
|
||||
pick().andExpect(jsonPath(reason(appid, "friendFinished"), contains(true)))
|
||||
.andExpect(jsonPath(reason(appid, "friendPlaytimeMinutes"), contains(2400)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void friends_FinishedOnlyProfile_NeverShowsAFinishTheProfileDoesNotList() throws Exception {
|
||||
friend(MAX, "Max", new String[]{"FINISHED"});
|
||||
for (long appid = 101; appid <= 113; appid++) {
|
||||
friendGame(MAX, appid, GameStatus.FINISHED, 2400);
|
||||
}
|
||||
mine(113L, GameStatus.BACKLOG, null);
|
||||
|
||||
expectNoRow();
|
||||
}
|
||||
|
||||
@Test
|
||||
void friends_DroppedGame_NeverCounts() throws Exception {
|
||||
mine(10L, GameStatus.BACKLOG, null);
|
||||
friend(MAX, "Max", null);
|
||||
friendGame(MAX, 10L, GameStatus.DROPPED, 6000);
|
||||
|
||||
expectNoRow();
|
||||
}
|
||||
|
||||
@Test
|
||||
void friends_HiddenOrRemovedGameOfFriend_NeverCounts() throws Exception {
|
||||
mine(10L, GameStatus.BACKLOG, null);
|
||||
mine(20L, GameStatus.BACKLOG, null);
|
||||
friend(MAX, "Max", null);
|
||||
friendGame(MAX, 10L, GameStatus.FINISHED, 100);
|
||||
friendGame(MAX, 20L, GameStatus.FINISHED, 100);
|
||||
jdbc.update("update user_game set hidden_at = now() where user_id = ? and appid = 10", MAX);
|
||||
jdbc.update("update user_game set removed_at = now() where user_id = ? and appid = 20", MAX);
|
||||
|
||||
expectNoRow();
|
||||
}
|
||||
|
||||
@Test
|
||||
void friends_OnlyMyBacklog() throws Exception {
|
||||
mine(10L, GameStatus.PLAYING, null);
|
||||
mine(20L, GameStatus.BACKLOG, null);
|
||||
mine(30L, GameStatus.BACKLOG, null);
|
||||
friend(MAX, "Max", null);
|
||||
friendGame(MAX, 10L, GameStatus.FINISHED, 100);
|
||||
friendGame(MAX, 20L, GameStatus.FINISHED, 100);
|
||||
friendGame(MAX, 40L, GameStatus.FINISHED, 100);
|
||||
jdbc.update("update user_game set hidden_at = now() where user_id = ? and appid = 20", USER_ID);
|
||||
|
||||
expectNoRow();
|
||||
}
|
||||
|
||||
@Test
|
||||
void friends_BudgetAndReleasedOnly() throws Exception {
|
||||
mine(10L, GameStatus.BACKLOG, 3000);
|
||||
mine(20L, GameStatus.BACKLOG, 60);
|
||||
mine(30L, GameStatus.BACKLOG, 60);
|
||||
gameReleaseRepository.upsert(20L, LocalDate.now().plusDays(1), false, Instant.now());
|
||||
friend(MAX, "Max", null);
|
||||
friend(ANNA, "Anna", null);
|
||||
friend(TOM, "Tom", null);
|
||||
friendGame(MAX, 10L, GameStatus.FINISHED, 100);
|
||||
// No playtime: a game anybody played is never "coming" (coming_game view).
|
||||
friendGame(ANNA, 20L, GameStatus.FINISHED, 0);
|
||||
friendGame(TOM, 30L, GameStatus.FINISHED, 100);
|
||||
|
||||
pick("maxMinutes", "300").andExpect(jsonPath(ROW + ".games[*].appid", contains(30)))
|
||||
.andExpect(jsonPath(ROW + ".games", hasSize(1)));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user