refactor: item 29 cleanup

C14 C16 C17 C18 C20 A11 A15 X10 X12 X15: dead Steam count code, GameNames, steamIdText, one query per game for the finished estimate, no API versioning config, open-in-view off, CORS from APP_FRONTEND_URL, non-root image, prune only up to the highest known id.
This commit is contained in:
lobo committed 2026-09-30 15:00:47 +02:00
1 parent 504c935b27
commit 34d64e7215
23 files changed
+116 -188

No files matched your search

+4
View File
@@ -36,5 +36,9 @@ EXPOSE 8090
# Copy the compiled jar file from the builder stage
COPY --from=builder /app/target/*.jar app.jar
# No root in the container
RUN addgroup -S app && adduser -S app -G app
USER app
# Run the application
ENTRYPOINT ["java", "-jar", "app.jar"]
+12 -4
View File
@@ -76,9 +76,9 @@ To Be Played is split into three repositories:
## Authentication
Core never issues tokens. It verifies Auth's EdDSA (Ed25519) JWTs with public keys only. It loads them from Auth's
JWKS by the token's `kid` and loads again when a new `kid` shows up (at most every 10 seconds). Loaded keys stay
until Core restarts. `JWT_PUBLIC_KEY` is used only until the first JWKS load works. The token comes from Auth's
`tbp_access` cookie or a `Bearer` header, whichever verifies first. Every token names its kind in `aud`
JWKS by the token's `kid` and loads again when a new `kid` shows up (at most every 10 seconds). Each load replaces
the key set: a key that left the JWKS stops working. `JWT_PUBLIC_KEY` is used only until the first JWKS load works.
The token comes from Auth's `tbp_access` cookie or a `Bearer` header, whichever verifies first. Every token names its kind in `aud`
(`tbp-login`, `tbp-public-profile`, `tbp-internal`, with `iss=tbp-auth`); a login's `sub` is the user id. Core reads
`steamId` and the active `role` (missing or unknown = USER) from the claims and grants that role and every lower one
(`USER < AUDITOR < PLATFORM_ADMIN`). A token without `aud` comes from an Auth from before item 26 (kind in `typ`,
@@ -98,11 +98,19 @@ only guard.
Every service method a controller calls carries one role marker, on the method or on its class (the method wins):
`@RequiresRole(Role.X)`, `@Public` (no login, e.g. landing stats; the public profile checks its ticket) or
`@Internal` (only Auth's service token). A Spring Security method interceptor checks it while a request is served;
scheduled jobs and background work run without a user and are not checked. A denied call answers `401` without a
scheduled jobs and background work run without a user and are not checked. That stays on purpose: the sync
worker calls `SteamApiService` and the import worker `ManualGameService`, both `@RequiresRole(USER)`, without a
request. Auth denies instead, because it has no such caller. A denied call answers `401` without a
login, else `403`. The URL rules in `SecurityConfig` stay as the outer guard. ArchUnit fails the build for an
unmarked method, two markers, a controller calling an `@Async` method (the check would never run), and a service
method calling another service's method that needs more.
**Mirrored in Auth.** These files exist in both services with small differences; a change in one needs the same
change in the other: `ClientHeaderFilter`, `RoleAuthorizationManager`, `MethodSecurityConfig`, `Role`, `RequiresRole`,
`Public` (only Core has `Internal`), `UserPrincipal`, the login token check in `JwtService.parseLoginToken`, the
ArchUnit role rules (`RoleAnnotationRules`) and `src/test/resources/contract/auth-core-contract.json`
(`ToBePlayed-Deployment/scripts/check-contract.sh` compares the two).
## API
| Method | Path | Auth |
@@ -17,7 +17,7 @@ public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("steamGameCount", "publicStats", "steamPlayerSummary",
CaffeineCacheManager cacheManager = new CaffeineCacheManager("publicStats", "steamPlayerSummary",
"igdbSearch");
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
@@ -6,6 +6,7 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -90,13 +91,15 @@ public class AccountCleanupRepository {
}
/**
* Users that have rows in Core but are not in the list.
* Users that have rows in Core but are not in the list. Ids above the highest known one are left out: Auth's
* ids never come back, so such a user registered after Auth read the list.
*
* @param knownUserIds not empty
*/
public List<Long> findOrphans(Collection<Long> knownUserIds) {
String union = String.join(" union ", ORPHAN_SOURCES.stream().map(table -> "select user_id from " + table).toList());
return jdbc.queryForList("select user_id from (" + union + ") u where user_id not in (:ids) order by user_id",
ids(knownUserIds), Long.class);
return jdbc.queryForList("select user_id from (" + union + ") u where user_id not in (:ids) "
+ "and user_id < :highest order by user_id",
ids(knownUserIds).addValue("highest", Collections.max(knownUserIds)), Long.class);
}
}
@@ -57,6 +57,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Objects;
import java.util.Set;
@@ -87,11 +88,8 @@ public class LibraryController {
@GetMapping("/games-count")
public ResponseEntity<?> getGamesCount(@AuthenticationPrincipal UserPrincipal principal) {
Long userId = principal.getIdAsLong();
if (principal.steamId() != null) {
syncService.syncIfStale(userId, String.valueOf(principal.steamId()));
} else {
syncService.stopNightlySync(userId);
}
principal.steamIdText().ifPresentOrElse(steamId -> syncService.syncIfStale(userId, steamId),
() -> syncService.stopNightlySync(userId));
long count = queryService.countOwnedGames(userId);
return ResponseEntity.ok(Map.of("count", count, "hasSteamId", principal.steamId() != null));
}
@@ -108,9 +106,7 @@ public class LibraryController {
@RequestParam(defaultValue = "false") boolean hidden
) {
Long userId = principal.getIdAsLong();
if (principal.steamId() != null) {
syncService.syncIfStale(userId, String.valueOf(principal.steamId()));
}
principal.steamIdText().ifPresent(steamId -> syncService.syncIfStale(userId, steamId));
int safeSize = ALLOWED_SIZES.contains(size) ? size : DEFAULT_SIZE;
int safePage = Math.max(0, page);
@@ -132,11 +128,12 @@ public class LibraryController {
@PostMapping("/sync")
public ResponseEntity<?> forceSync(@AuthenticationPrincipal UserPrincipal principal) {
if (principal.steamId() == null) {
Optional<String> steamId = principal.steamIdText();
if (steamId.isEmpty()) {
return ResponseEntity.badRequest().body(new ErrorResponse("NO_STEAM_ID", "User has no Steam ID"));
}
try {
syncService.forceSync(principal.getIdAsLong(), String.valueOf(principal.steamId()));
syncService.forceSync(principal.getIdAsLong(), steamId.get());
return ResponseEntity.accepted().body(Map.of("status", "started"));
} catch (SyncCooldownException e) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
@@ -152,8 +149,7 @@ public class LibraryController {
public ResponseEntity<?> getGameDetail(@AuthenticationPrincipal UserPrincipal principal,
@PathVariable("appid") Long appid) {
Long userId = principal.getIdAsLong();
String steamId = principal.steamId() != null ? String.valueOf(principal.steamId()) : null;
return gameDetailService.getGameDetail(userId, steamId, appid)
return gameDetailService.getGameDetail(userId, principal.steamIdText().orElse(null), appid)
.<ResponseEntity<?>>map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(GAME_NOT_FOUND));
@@ -310,9 +306,7 @@ public class LibraryController {
int safeCount = Math.clamp(count, 1, MAX_PICKER_GAMES);
Integer budget = maxMinutes != null && maxMinutes > 0 ? maxMinutes : null;
Long userId = principal.getIdAsLong();
if (principal.steamId() != null) {
syncService.syncIfStale(userId, String.valueOf(principal.steamId()));
}
principal.steamIdText().ifPresent(steamId -> syncService.syncIfStale(userId, steamId));
return ResponseEntity.ok(queryService.pickBacklogGames(userId, budget, mood, excluded, safeCount));
}
@@ -0,0 +1,25 @@
package core_service.modules.library.domain;
import java.text.Normalizer;
import java.util.Locale;
/**
* Game names as the matches compare them (import lines, speedrun.com hits).
*/
public final class GameNames {
public static final String TRADEMARK_SIGNS = "[™®©]";
private GameNames() {
}
/**
* Lower case, without accents, ™ ® © and punctuation, words split by one space.
*/
public static String normalize(String name) {
// Before NFKD, which turns ™ into "TM".
String withoutMarks = name.replaceAll(TRADEMARK_SIGNS, "");
String plain = Normalizer.normalize(withoutMarks, Normalizer.Form.NFKD).replaceAll("\\p{M}", "");
return plain.toLowerCase(Locale.ROOT).replaceAll("[^\\p{L}\\p{N}]+", " ").trim();
}
}
@@ -4,11 +4,15 @@ import core_service.modules.library.domain.UserGameAchievement;
import core_service.modules.library.domain.UserGameAchievementId;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Collection;
import java.util.List;
public interface UserGameAchievementRepository extends JpaRepository<UserGameAchievement, UserGameAchievementId> {
List<UserGameAchievement> findByUserIdAndAppid(Long userId, Long appid);
List<UserGameAchievement> findByAppidAndApiNameAndUserIdInAndUnlockedTrueAndUnlockedAtNotNull(Long appid,
String apiName, Collection<Long> userIds);
void deleteByUserIdAndAppid(Long userId, Long appid);
}
@@ -10,6 +10,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -39,15 +40,17 @@ public class FinishedEstimateService {
for (Map.Entry<Long, List<FinishedWithoutDate>> app : byApp.entrySet()) {
Optional<EndingAchievement> ending = EndingAchievement.of(schemaRepository.findByAppid(app.getKey()));
if (ending.isEmpty()) continue;
String apiName = ending.get().achievement().getApiName();
// One query per game, not per user: most candidates never unlocked the ending and come back every night.
// With the user ids, the primary key (user_id, appid, api_name) serves it.
List<Long> userIds = app.getValue().stream().map(FinishedWithoutDate::userId).toList();
Map<Long, Instant> unlockByUser = userAchievementRepository
.findByAppidAndApiNameAndUserIdInAndUnlockedTrueAndUnlockedAtNotNull(app.getKey(),
ending.get().achievement().getApiName(), userIds).stream()
.collect(Collectors.toMap(UserGameAchievement::getUserId, UserGameAchievement::getUnlockedAt));
for (FinishedWithoutDate game : app.getValue()) {
Optional<UserGameAchievement> unlock = userAchievementRepository
.findByUserIdAndAppid(game.userId(), game.appid()).stream()
.filter(a -> a.isUnlocked() && a.getUnlockedAt() != null && a.getApiName().equals(apiName))
.findFirst();
if (unlock.isPresent()) {
dated += userGameRepository.setFinishedAtEstimate(game.userId(), game.appid(),
unlock.get().getUnlockedAt());
Instant unlockedAt = unlockByUser.get(game.userId());
if (unlockedAt != null) {
dated += userGameRepository.setFinishedAtEstimate(game.userId(), game.appid(), unlockedAt);
}
}
}
@@ -1,5 +1,6 @@
package core_service.modules.library.service;
import core_service.modules.library.domain.GameNames;
import core_service.modules.library.domain.GameImport;
import core_service.modules.library.domain.IgdbGame;
import core_service.modules.library.repo.GameImportRepository;
@@ -195,7 +196,7 @@ public class GameImportWorker {
* Case, accents, punctuation and marks like ™ ignored, as in the speedrun.com match.
*/
static boolean sameName(String a, String b) {
return SpeedrunMatch.normalize(a).equals(SpeedrunMatch.normalize(b));
return GameNames.normalize(a).equals(GameNames.normalize(b));
}
/**
@@ -212,11 +212,9 @@ public class LibraryQueryService {
/**
* @param profileStats the stat group names of the profile ticket
*/
@Public
/**
* @param sectionNames see {@link ProfileSection#shown}; a hidden section runs no query
*/
@Public
public PublicLibraryDto getPublicLibrary(Long userId, Collection<String> profileStats, Collection<String> sectionNames) {
Set<ProfileSection> sections = ProfileSection.shown(sectionNames);
boolean summary = sections.contains(ProfileSection.SUMMARY);
@@ -1,12 +1,11 @@
package core_service.modules.library.service;
import core_service.modules.library.domain.GameNames;
import core_service.modules.speedrun_adapter.service.SpeedrunApiService;
import java.text.Normalizer;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -21,7 +20,6 @@ final class SpeedrunMatch {
static final String WEBLINK_PREFIX = "https://www.speedrun.com/";
// speedrun.com tells remakes apart by the year: "Prey (2017)" next to "Prey".
private static final String TRADEMARK_SIGNS = "[™®©]";
private static final Pattern YEAR_SUFFIX = Pattern.compile("^(.*?)\\s*\\((\\d{4})\\)$");
// The column sizes of game_speedrun.
private static final int MAX_GAME_ID_LENGTH = 16;
@@ -64,24 +62,14 @@ final class SpeedrunMatch {
* The name for speedrun.com's search, which does not find "DOOM Eternal" for "DOOM Eternal™".
*/
static String searchText(String name) {
return name.replaceAll(TRADEMARK_SIGNS, "").trim();
}
/**
* Lower case, without accents, ™ ® © and punctuation, words split by one space.
*/
static String normalize(String name) {
// Before NFKD, which turns ™ into "TM".
String withoutMarks = name.replaceAll(TRADEMARK_SIGNS, "");
String plain = Normalizer.normalize(withoutMarks, Normalizer.Form.NFKD).replaceAll("\\p{M}", "");
return plain.toLowerCase(Locale.ROOT).replaceAll("[^\\p{L}\\p{N}]+", " ").trim();
return name.replaceAll(GameNames.TRADEMARK_SIGNS, "").trim();
}
private static NameAndYear split(String name) {
Matcher matcher = YEAR_SUFFIX.matcher(name.trim());
return matcher.matches()
? new NameAndYear(normalize(matcher.group(1)), Integer.parseInt(matcher.group(2)))
: new NameAndYear(normalize(name), null);
? new NameAndYear(GameNames.normalize(matcher.group(1)), Integer.parseInt(matcher.group(2)))
: new NameAndYear(GameNames.normalize(name), null);
}
private record NameAndYear(String name, Integer year) {}
@@ -1,30 +0,0 @@
package core_service.modules.steam_adapter.api;
import core_service.modules.steam_adapter.service.SteamApiService;
import core_service.shared.security.UserPrincipal;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/api/steam")
@RequiredArgsConstructor
public class SteamController {
private final SteamApiService steamApiService;
@GetMapping("/games-count")
public ResponseEntity<?> getGamesCount(@AuthenticationPrincipal UserPrincipal principal) {
if (principal.steamId() == null) {
return ResponseEntity.ok(Map.of("count", 0, "hasSteamId", false));
}
int count = steamApiService.getOwnedGamesCount(String.valueOf(principal.steamId()));
return ResponseEntity.ok(Map.of("count", count, "hasSteamId", true));
}
}
@@ -11,7 +11,6 @@ public record SteamOwnedGamesResponse(
@JsonIgnoreProperties(ignoreUnknown = true)
public record OwnedGamesResponse(
@JsonProperty("game_count") Integer gameCount,
@JsonProperty("games") List<Game> games
) {}
@@ -51,51 +51,6 @@ public class SteamApiService {
@Value("${app.steam.store-country:DE}")
private String storeCountry;
@Cacheable(value = "steamGameCount", key = "#steamId")
@RateLimiter(name = "steamApi")
@Retry(name = "steamApiOnce", fallbackMethod = "noGamesCount")
public int getOwnedGamesCount(String steamId) {
if (apiKey == null || apiKey.isEmpty() || apiKey.equals("${STEAM_API_KEY}")) {
log.error("Steam API Key is not configured correctly!");
return 0;
}
SteamOwnedGamesResponse response = steamRestClient.get()
.uri(uriBuilder -> uriBuilder
.path("/IPlayerService/GetOwnedGames/v1/")
.queryParam("key", apiKey)
.queryParam("steamid", steamId)
.queryParam("include_appinfo", "true")
.queryParam("include_played_free_games", "true")
.queryParam("skip_unvetted_apps", 0)
.queryParam("format", "json")
.build())
.retrieve()
.body(SteamOwnedGamesResponse.class);
if (response != null && response.response() != null) {
Integer count = response.response().gameCount();
if (count == null) {
if (response.response().games() != null) {
int size = response.response().games().size();
log.info("game_count was null, but games list has {} entries for SteamID: {}", size, steamId);
return size;
}
log.warn("Steam API returned an empty response object for SteamID: {}. Check if the ID is correct and 'Game details' are Public.", steamId);
return 0;
}
log.info("Successfully fetched {} games for SteamID: {}", count, steamId);
return count;
}
return 0;
}
// The fallbacks run after the last try; the callers keep their "Steam failed" rule.
private int noGamesCount(String steamId, Exception e) {
log.error("Error communicating with Steam API for game count of SteamID: {}", steamId, e);
return 0;
}
// Asked on every page load for the avatar in the header.
@Cacheable(value = "steamPlayerSummary", key = "#steamId", unless = "#result == null")
@RateLimiter(name = "steamApi")
@@ -1,6 +1,7 @@
package core_service.shared.security;
import java.security.Principal;
import java.util.Optional;
/**
* @param role the active role of this login ("view as"), set by Auth
@@ -20,6 +21,13 @@ public record UserPrincipal(
return id;
}
/**
* The Steam ID as the Steam API and user_steam_sync take it; empty, never "null", without one.
*/
public Optional<String> steamIdText() {
return Optional.ofNullable(steamId).map(String::valueOf);
}
public long getIdAsLong() {
return Long.parseLong(id);
}
+1 -2
View File
@@ -14,8 +14,7 @@ security:
jwks-url: "http://localhost:8080/.well-known/jwks.json"
app:
cors:
allowed-origins: "http://localhost:5173"
frontend-url: "http://localhost:5173"
steam:
api-key: local
base-url: "http://localhost:8089/steam"
+5 -8
View File
@@ -7,7 +7,7 @@ spring:
deserialization:
accept-float-as-int: false
jpa:
open-in-view: true
open-in-view: false
flyway:
enabled: true
baseline-on-migrate: true
@@ -15,12 +15,6 @@ spring:
threads:
virtual:
enabled: true
mvc:
apiversion:
use:
query-parameter: version
default: "1.0"
supported: 1.0, 2,0
server:
port: 8090
@@ -42,8 +36,11 @@ app:
# Filled from the CI run by the Dockerfile.
commit: ${APP_BUILD_COMMIT:dev}
time: ${APP_BUILD_TIME:}
# The frontend; FRONTEND_HOST in the Deployment compose file. The prod host as default: an image that starts before
# the compose file sets the env var keeps working.
frontend-url: ${APP_FRONTEND_URL:https://tbp.andreas-dahm.eu}
cors:
allowed-origins: "https://tbp.andreas-dahm.eu"
allowed-origins: ${app.frontend-url}
steam:
api-key: ${STEAM_API_KEY}
base-url: https://api.steampowered.com
@@ -111,10 +111,16 @@ class AccountCleanupRepositoryTest extends AbstractPersistenceTest {
void findOrphans_ReturnsUsersNotInTheList() {
repository.addDeleted(List.of(7L));
assertThat(repository.findOrphans(List.of(2L))).containsExactly(1L, 7L);
assertThat(repository.findOrphans(List.of(2L, 8L))).containsExactly(1L, 7L);
assertThat(repository.findOrphans(List.of(1L, 2L, 7L))).isEmpty();
}
// Auth reads its ids before the call: a user who registers in between has a higher id and is no orphan.
@Test
void findOrphans_SkipsUsersAboveTheHighestKnownId() {
assertThat(repository.findOrphans(List.of(1L))).isEmpty();
}
// A new table with user rows must be in the purge list, or "Delete my account" leaves its rows behind.
@Test
void tables_CoverEveryTableWithAUserId() {
@@ -0,0 +1,15 @@
package core_service.modules.library.domain;
import core_service.shared.test.AbstractUnitTest;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class GameNamesTest extends AbstractUnitTest {
@Test
void normalize_DropsCaseAccentsTrademarksAndPunctuation() {
assertThat(GameNames.normalize(" Pokémon™: Let's Go, Pikachu!® ")).isEqualTo("pokemon let s go pikachu");
assertThat(GameNames.normalize("The Witcher 3: Wild Hunt")).isEqualTo(GameNames.normalize("The Witcher® 3 - Wild Hunt"));
}
}
@@ -18,12 +18,6 @@ class SpeedrunMatchTest extends AbstractUnitTest {
private static final SpeedrunApiService.Game PREY_2006 = hit("prey", "Prey", 2006);
private static final SpeedrunApiService.Game PREY_2017 = hit("prey2017", "Prey (2017)", 2017);
@Test
void normalize_DropsCaseAccentsTrademarksAndPunctuation() {
assertThat(SpeedrunMatch.normalize(" Pokémon™: Let's Go, Pikachu!® ")).isEqualTo("pokemon let s go pikachu");
assertThat(SpeedrunMatch.normalize("The Witcher 3: Wild Hunt")).isEqualTo(SpeedrunMatch.normalize("The Witcher® 3 - Wild Hunt"));
}
@Test
void candidates_YearSuffixTellsTheRemakeApart() {
assertThat(SpeedrunMatch.candidates("Prey", Set.of(2017), List.of(PREY_2006, PREY_2017))).containsExactly(PREY_2017);
@@ -108,7 +108,7 @@ class SteamApiServiceCacheTest extends AbstractIntegrationTest {
@Test
void getOwnedGames_TransientSteamError_IsTriedAgain() {
SteamOwnedGamesResponse games = new SteamOwnedGamesResponse(new SteamOwnedGamesResponse.OwnedGamesResponse(1,
SteamOwnedGamesResponse games = new SteamOwnedGamesResponse(new SteamOwnedGamesResponse.OwnedGamesResponse(
List.of(new SteamOwnedGamesResponse.Game(220, "Half-Life 2", 60, null, true, 0))));
when(steamRestClient.get().uri(any(Function.class)).retrieve().body(SteamOwnedGamesResponse.class))
.thenAnswer(invocation -> {
@@ -45,27 +45,6 @@ class SteamApiServiceTest {
ReflectionTestUtils.setField(steamApiService, "apiKey", "test-api-key");
}
@Test
void getOwnedGamesCount_shouldReturnCount() {
String steamId = "76561197960435530";
String jsonResponse = """
{
"response": {
"game_count": 42
}
}
""";
server.expect(requestTo(containsString("/IPlayerService/GetOwnedGames/v1/")))
.andExpect(queryParam("steamid", steamId))
.andExpect(queryParam("key", "test-api-key"))
.andRespond(withSuccess(jsonResponse, MediaType.APPLICATION_JSON));
int result = steamApiService.getOwnedGamesCount(steamId);
assertThat(result).isEqualTo(42);
}
@Test
void getPlayerSummary_shouldReturnPlayer() {
String steamId = "76561197960435530";
@@ -135,15 +114,6 @@ class SteamApiServiceTest {
assertThat(steamApiService.getOwnedGames("76561197960435530")).hasSize(1);
}
@Test
void getOwnedGamesCount_shouldCountUnvettedApps() {
server.expect(requestTo(containsString("/IPlayerService/GetOwnedGames/v1/")))
.andExpect(queryParam("skip_unvetted_apps", "0"))
.andRespond(withSuccess("{\"response\":{\"game_count\":1}}", MediaType.APPLICATION_JSON));
assertThat(steamApiService.getOwnedGamesCount("76561197960435530")).isEqualTo(1);
}
@Test
void getOwnedGames_shouldReturnGamesEvenWithNullValues() {
String steamId = "76561197960435530";
@@ -1,13 +0,0 @@
package core_service.shared.test;
import org.junit.jupiter.api.Tag;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
/**
* Base class for WebMvc tests.
* Slices the web layer.
*/
@Tag("WebMvcTest")
@WebMvcTest
public abstract class AbstractWebMvcTest {
}