From 2e3896b00ddf3370e216199280a6492ac25206ff Mon Sep 17 00:00:00 2001 From: Lobo Date: Sun, 19 Apr 2026 15:15:15 +0200 Subject: [PATCH] Add owned-games DTO and full profile support Introduce SteamOwnedGamesResponse and SteamProfileAndGamesResponse DTOs and extend SteamApiService with getOwnedGames(...) and getFullPlayerProfile(...) to fetch and combine player summary and owned games. Update SteamTestController to return the combined profile/games response. Remove redundant Steam service URL entries from application.yml. Add SteamApiServiceTest to cover player summary, owned games (including null fields), and the combined profile flow; service falls back to empty lists on errors. --- .../api/SteamTestController.java | 8 +- .../dto/response/SteamOwnedGamesResponse.java | 20 +++ .../SteamProfileAndGamesResponse.java | 9 + .../service/SteamApiService.java | 40 +++++ src/main/resources/application.yml | 2 - .../service/SteamApiServiceTest.java | 170 ++++++++++++++++++ 6 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 src/main/java/core_service/modules/steam_adapter/api/dto/response/SteamOwnedGamesResponse.java create mode 100644 src/main/java/core_service/modules/steam_adapter/api/dto/response/SteamProfileAndGamesResponse.java create mode 100644 src/test/java/core_service/modules/steam_adapter/service/SteamApiServiceTest.java diff --git a/src/main/java/core_service/modules/steam_adapter/api/SteamTestController.java b/src/main/java/core_service/modules/steam_adapter/api/SteamTestController.java index ab967d0..993e254 100644 --- a/src/main/java/core_service/modules/steam_adapter/api/SteamTestController.java +++ b/src/main/java/core_service/modules/steam_adapter/api/SteamTestController.java @@ -1,6 +1,6 @@ package core_service.modules.steam_adapter.api; -import core_service.modules.steam_adapter.api.dto.response.SteamPlayerSummaryResponse; +import core_service.modules.steam_adapter.api.dto.response.SteamProfileAndGamesResponse; import core_service.modules.steam_adapter.service.SteamApiService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -21,10 +21,10 @@ public class SteamTestController { @GetMapping("/{steamId}") public ResponseEntity testSteamConnection(@PathVariable String steamId) { - Optional playerOpt = steamApiService.getPlayerSummary(steamId); + Optional profileOpt = steamApiService.getFullPlayerProfile(steamId); - if (playerOpt.isPresent()) { - return ResponseEntity.ok(playerOpt.get()); + if (profileOpt.isPresent()) { + return ResponseEntity.ok(profileOpt.get()); } else { return ResponseEntity.notFound().build(); } diff --git a/src/main/java/core_service/modules/steam_adapter/api/dto/response/SteamOwnedGamesResponse.java b/src/main/java/core_service/modules/steam_adapter/api/dto/response/SteamOwnedGamesResponse.java new file mode 100644 index 0000000..e49ee95 --- /dev/null +++ b/src/main/java/core_service/modules/steam_adapter/api/dto/response/SteamOwnedGamesResponse.java @@ -0,0 +1,20 @@ +package core_service.modules.steam_adapter.api.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +public record SteamOwnedGamesResponse(OwnedGamesResponse response) { + + public record OwnedGamesResponse( + @JsonProperty("game_count") int gameCount, + List games + ) {} + + public record Game( + Integer appid, + String name, + @JsonProperty("playtime_forever") Integer playtimeForever, + @JsonProperty("img_icon_url") String imgIconUrl, + @JsonProperty("has_community_visible_stats") Boolean hasCommunityVisibleStats + ) {} +} diff --git a/src/main/java/core_service/modules/steam_adapter/api/dto/response/SteamProfileAndGamesResponse.java b/src/main/java/core_service/modules/steam_adapter/api/dto/response/SteamProfileAndGamesResponse.java new file mode 100644 index 0000000..314d9ba --- /dev/null +++ b/src/main/java/core_service/modules/steam_adapter/api/dto/response/SteamProfileAndGamesResponse.java @@ -0,0 +1,9 @@ +package core_service.modules.steam_adapter.api.dto.response; + +import java.util.List; + +public record SteamProfileAndGamesResponse( + SteamPlayerSummaryResponse.Player player, + List games +) { +} diff --git a/src/main/java/core_service/modules/steam_adapter/service/SteamApiService.java b/src/main/java/core_service/modules/steam_adapter/service/SteamApiService.java index 1dfb1a3..3eee7b1 100644 --- a/src/main/java/core_service/modules/steam_adapter/service/SteamApiService.java +++ b/src/main/java/core_service/modules/steam_adapter/service/SteamApiService.java @@ -1,12 +1,16 @@ package core_service.modules.steam_adapter.service; +import core_service.modules.steam_adapter.api.dto.response.SteamOwnedGamesResponse; import core_service.modules.steam_adapter.api.dto.response.SteamPlayerSummaryResponse; +import core_service.modules.steam_adapter.api.dto.response.SteamProfileAndGamesResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestClient; +import java.util.Collections; +import java.util.List; import java.util.Optional; @Slf4j @@ -19,6 +23,16 @@ public class SteamApiService { @Value("${app.steam.api-key}") private String apiKey; + public Optional getFullPlayerProfile(String steamId) { + var playerOpt = getPlayerSummary(steamId); + if (playerOpt.isEmpty()) { + return Optional.empty(); + } + + var games = getOwnedGames(steamId); + return Optional.of(new SteamProfileAndGamesResponse(playerOpt.get(), games)); + } + public Optional getPlayerSummary(String steamId) { try { log.info("Fetching Steam profile for SteamID: {}", steamId); @@ -42,4 +56,30 @@ public class SteamApiService { return Optional.empty(); } + + public List getOwnedGames(String steamId) { + try { + log.info("Fetching owned games for SteamID: {}", steamId); + + SteamOwnedGamesResponse response = steamRestClient.get() + .uri(uriBuilder -> uriBuilder + .path("/IPlayerService/GetOwnedGames/v0001/") + .queryParam("key", apiKey) + .queryParam("steamid", steamId) + .queryParam("include_appinfo", 1) + .queryParam("include_played_free_games", 1) + .build()) + .retrieve() + .body(SteamOwnedGamesResponse.class); + + if (response != null && response.response() != null && response.response().games() != null) { + return response.response().games(); + } + + } catch (Exception e) { + log.error("Error communicating with Steam API for games of SteamID: {}", steamId, e); + } + + return Collections.emptyList(); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c49daff..7268cb0 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -43,6 +43,4 @@ app: steam: api-key: ${STEAM_API_KEY} base-url: https://api.steampowered.com - playerService-url: https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/ - storeService-url: https://api.steampowered.com/IStoreService/GetAppList/v1/ imageService-url: https://cdn.cloudflare.steamstatic.com/steam/apps/ \ No newline at end of file diff --git a/src/test/java/core_service/modules/steam_adapter/service/SteamApiServiceTest.java b/src/test/java/core_service/modules/steam_adapter/service/SteamApiServiceTest.java new file mode 100644 index 0000000..62a69b4 --- /dev/null +++ b/src/test/java/core_service/modules/steam_adapter/service/SteamApiServiceTest.java @@ -0,0 +1,170 @@ +package core_service.modules.steam_adapter.service; + +import core_service.modules.steam_adapter.api.dto.response.SteamOwnedGamesResponse; +import core_service.modules.steam_adapter.api.dto.response.SteamPlayerSummaryResponse; +import core_service.modules.steam_adapter.api.dto.response.SteamProfileAndGamesResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestClient; + +import java.util.List; +import java.util.Optional; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.queryParam; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +class SteamApiServiceTest { + + private SteamApiService steamApiService; + private MockRestServiceServer server; + + @BeforeEach + void setUp() { + RestClient.Builder builder = RestClient.builder(); + server = MockRestServiceServer.bindTo(builder).build(); + steamApiService = new SteamApiService(builder.build()); + ReflectionTestUtils.setField(steamApiService, "apiKey", "test-api-key"); + } + + @Test + void getPlayerSummary_shouldReturnPlayer() { + String steamId = "76561197960435530"; + String jsonResponse = """ + { + "response": { + "players": [ + { + "steamid": "76561197960435530", + "personaname": "TestUser", + "profileurl": "http://steamcommunity.com/id/testuser/", + "avatarfull": "http://media.steampowered.com/avatar.jpg" + } + ] + } + } + """; + + server.expect(requestTo(containsString("/ISteamUser/GetPlayerSummaries/v0002/"))) + .andExpect(queryParam("steamids", steamId)) + .andExpect(queryParam("key", "test-api-key")) + .andRespond(withSuccess(jsonResponse, MediaType.APPLICATION_JSON)); + + Optional result = steamApiService.getPlayerSummary(steamId); + + assertThat(result).isPresent(); + assertThat(result.get().personaname()).isEqualTo("TestUser"); + } + + @Test + void getOwnedGames_shouldReturnGames() { + String steamId = "76561197960435530"; + String jsonResponse = """ + { + "response": { + "game_count": 1, + "games": [ + { + "appid": 10, + "name": "Counter-Strike", + "playtime_forever": 32, + "img_icon_url": "icon_url", + "has_community_visible_stats": true + } + ] + } + } + """; + + server.expect(requestTo(containsString("/IPlayerService/GetOwnedGames/v0001/"))) + .andExpect(queryParam("steamid", steamId)) + .andExpect(queryParam("key", "test-api-key")) + .andRespond(withSuccess(jsonResponse, MediaType.APPLICATION_JSON)); + + List result = steamApiService.getOwnedGames(steamId); + + assertThat(result).hasSize(1); + assertThat(result.get(0).name()).isEqualTo("Counter-Strike"); + } + + @Test + void getOwnedGames_shouldReturnGamesEvenWithNullValues() { + String steamId = "76561197960435530"; + String jsonResponse = """ + { + "response": { + "game_count": 1, + "games": [ + { + "appid": 10, + "name": "Counter-Strike" + } + ] + } + } + """; + + server.expect(requestTo(containsString("/IPlayerService/GetOwnedGames/v0001/"))) + .andRespond(withSuccess(jsonResponse, MediaType.APPLICATION_JSON)); + + List result = steamApiService.getOwnedGames(steamId); + + assertThat(result).hasSize(1); + assertThat(result.get(0).name()).isEqualTo("Counter-Strike"); + assertThat(result.get(0).playtimeForever()).isNull(); + assertThat(result.get(0).hasCommunityVisibleStats()).isNull(); + } + + @Test + void getFullPlayerProfile_shouldReturnCombinedResult() { + String steamId = "76561197960435530"; + String summaryJson = """ + { + "response": { + "players": [ + { + "steamid": "76561197960435530", + "personaname": "TestUser", + "profileurl": "url", + "avatarfull": "avatar" + } + ] + } + } + """; + String gamesJson = """ + { + "response": { + "game_count": 1, + "games": [ + { + "appid": 10, + "name": "Counter-Strike", + "playtime_forever": 32, + "img_icon_url": "icon_url", + "has_community_visible_stats": true + } + ] + } + } + """; + + server.expect(requestTo(containsString("/ISteamUser/GetPlayerSummaries/v0002/"))) + .andRespond(withSuccess(summaryJson, MediaType.APPLICATION_JSON)); + + server.expect(requestTo(containsString("/IPlayerService/GetOwnedGames/v0001/"))) + .andRespond(withSuccess(gamesJson, MediaType.APPLICATION_JSON)); + + Optional result = steamApiService.getFullPlayerProfile(steamId); + + assertThat(result).isPresent(); + assertThat(result.get().player().personaname()).isEqualTo("TestUser"); + assertThat(result.get().games()).hasSize(1); + assertThat(result.get().games().get(0).name()).isEqualTo("Counter-Strike"); + } +}