diff --git a/compose.yaml b/compose.yaml index 678feae..6dac42f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -7,4 +7,13 @@ services: environment: - POSTGRES_DB=tbp_core - POSTGRES_USER=tbp_user - - POSTGRES_PASSWORD=local_secret_password \ No newline at end of file + - POSTGRES_PASSWORD=local_secret_password + # Recorded answers of Steam, Twitch/IGDB and speedrun.com (local/wiremock). Starts only with the local profile. + wiremock: + image: wiremock/wiremock:3.9.1 + container_name: tbp_core_mocks_local + profiles: ["local"] + ports: + - "8089:8080" + volumes: + - ./local/wiremock:/home/wiremock diff --git a/local/wiremock/__files/igdb-external-games.json b/local/wiremock/__files/igdb-external-games.json new file mode 100644 index 0000000..5f1ce9c --- /dev/null +++ b/local/wiremock/__files/igdb-external-games.json @@ -0,0 +1,34 @@ +[ + { + "game": 72, + "uid": "620" + }, + { + "game": 113112, + "uid": "1145360" + }, + { + "game": 17000, + "uid": "413150" + }, + { + "game": 7046, + "uid": "427520" + }, + { + "game": 26226, + "uid": "504230" + }, + { + "game": 14593, + "uid": "367520" + }, + { + "game": 233, + "uid": "220" + }, + { + "game": 133236, + "uid": "892970" + } +] diff --git a/local/wiremock/__files/igdb-game-113112.json b/local/wiremock/__files/igdb-game-113112.json new file mode 100644 index 0000000..e60b67f --- /dev/null +++ b/local/wiremock/__files/igdb-game-113112.json @@ -0,0 +1,27 @@ +[ + { + "id": 113112, + "name": "Hades", + "first_release_date": 1600387200, + "cover": { + "image_id": "co1r7f" + }, + "platforms": [ + { + "name": "PC (Microsoft Windows)", + "abbreviation": "PC" + }, + { + "name": "Nintendo Switch", + "abbreviation": "Switch" + } + ], + "total_rating_count": 1500, + "external_games": [ + { + "external_game_source": 1, + "uid": "1145360" + } + ] + } +] diff --git a/local/wiremock/__files/igdb-game-119388.json b/local/wiremock/__files/igdb-game-119388.json new file mode 100644 index 0000000..68d3c77 --- /dev/null +++ b/local/wiremock/__files/igdb-game-119388.json @@ -0,0 +1,17 @@ +[ + { + "id": 119388, + "name": "The Legend of Zelda: Tears of the Kingdom", + "first_release_date": 1683849600, + "cover": { + "image_id": "co1r7f" + }, + "platforms": [ + { + "name": "Nintendo Switch", + "abbreviation": "Switch" + } + ], + "total_rating_count": 900 + } +] diff --git a/local/wiremock/__files/igdb-game-26226.json b/local/wiremock/__files/igdb-game-26226.json new file mode 100644 index 0000000..b8e6155 --- /dev/null +++ b/local/wiremock/__files/igdb-game-26226.json @@ -0,0 +1,27 @@ +[ + { + "id": 26226, + "name": "Celeste", + "first_release_date": 1516838400, + "cover": { + "image_id": "co1r7f" + }, + "platforms": [ + { + "name": "PC (Microsoft Windows)", + "abbreviation": "PC" + }, + { + "name": "Nintendo Switch", + "abbreviation": "Switch" + } + ], + "total_rating_count": 1300, + "external_games": [ + { + "external_game_source": 1, + "uid": "504230" + } + ] + } +] diff --git a/local/wiremock/__files/igdb-game-search.json b/local/wiremock/__files/igdb-game-search.json new file mode 100644 index 0000000..94c9823 --- /dev/null +++ b/local/wiremock/__files/igdb-game-search.json @@ -0,0 +1,67 @@ +[ + { + "id": 119388, + "name": "The Legend of Zelda: Tears of the Kingdom", + "first_release_date": 1683849600, + "cover": { + "image_id": "co1r7f" + }, + "platforms": [ + { + "name": "Nintendo Switch", + "abbreviation": "Switch" + } + ], + "total_rating_count": 900 + }, + { + "id": 113112, + "name": "Hades", + "first_release_date": 1600387200, + "cover": { + "image_id": "co1r7f" + }, + "platforms": [ + { + "name": "PC (Microsoft Windows)", + "abbreviation": "PC" + }, + { + "name": "Nintendo Switch", + "abbreviation": "Switch" + } + ], + "total_rating_count": 1500, + "external_games": [ + { + "external_game_source": 1, + "uid": "1145360" + } + ] + }, + { + "id": 26226, + "name": "Celeste", + "first_release_date": 1516838400, + "cover": { + "image_id": "co1r7f" + }, + "platforms": [ + { + "name": "PC (Microsoft Windows)", + "abbreviation": "PC" + }, + { + "name": "Nintendo Switch", + "abbreviation": "Switch" + } + ], + "total_rating_count": 1300, + "external_games": [ + { + "external_game_source": 1, + "uid": "504230" + } + ] + } +] diff --git a/local/wiremock/__files/igdb-game-years.json b/local/wiremock/__files/igdb-game-years.json new file mode 100644 index 0000000..4c7a023 --- /dev/null +++ b/local/wiremock/__files/igdb-game-years.json @@ -0,0 +1,38 @@ +[ + { + "id": 72, + "first_release_date": 1303171200 + }, + { + "id": 113112, + "first_release_date": 1600387200 + }, + { + "id": 17000, + "first_release_date": 1456444800 + }, + { + "id": 7046, + "first_release_date": 1600387200 + }, + { + "id": 26226, + "first_release_date": 1516838400 + }, + { + "id": 14593, + "first_release_date": 1487980800 + }, + { + "id": 233, + "first_release_date": 1100736000 + }, + { + "id": 133236, + "first_release_date": 1612828800 + }, + { + "id": 119388, + "first_release_date": 1683849600 + } +] diff --git a/local/wiremock/__files/igdb-tags.json b/local/wiremock/__files/igdb-tags.json new file mode 100644 index 0000000..c007c50 --- /dev/null +++ b/local/wiremock/__files/igdb-tags.json @@ -0,0 +1,257 @@ +[ + { + "id": 72, + "genres": [ + { + "name": "Puzzle" + }, + { + "name": "Platform" + }, + { + "name": "Shooter" + } + ], + "themes": [ + { + "name": "Science fiction" + }, + { + "name": "Comedy" + } + ], + "game_modes": [ + { + "name": "Single player" + }, + { + "name": "Co-operative" + } + ] + }, + { + "id": 113112, + "genres": [ + { + "name": "Role-playing (RPG)" + }, + { + "name": "Hack and slash/Beat 'em up" + }, + { + "name": "Indie" + } + ], + "themes": [ + { + "name": "Action" + }, + { + "name": "Fantasy" + } + ], + "game_modes": [ + { + "name": "Single player" + } + ] + }, + { + "id": 17000, + "genres": [ + { + "name": "Role-playing (RPG)" + }, + { + "name": "Simulator" + }, + { + "name": "Indie" + } + ], + "themes": [ + { + "name": "Sandbox" + } + ], + "game_modes": [ + { + "name": "Single player" + }, + { + "name": "Multiplayer" + }, + { + "name": "Co-operative" + } + ] + }, + { + "id": 7046, + "genres": [ + { + "name": "Simulator" + }, + { + "name": "Strategy" + }, + { + "name": "Indie" + } + ], + "themes": [ + { + "name": "Science fiction" + }, + { + "name": "Sandbox" + }, + { + "name": "Survival" + } + ], + "game_modes": [ + { + "name": "Single player" + }, + { + "name": "Multiplayer" + } + ] + }, + { + "id": 26226, + "genres": [ + { + "name": "Platform" + }, + { + "name": "Indie" + } + ], + "themes": [ + { + "name": "Action" + } + ], + "game_modes": [ + { + "name": "Single player" + } + ] + }, + { + "id": 14593, + "genres": [ + { + "name": "Platform" + }, + { + "name": "Adventure" + }, + { + "name": "Indie" + } + ], + "themes": [ + { + "name": "Action" + }, + { + "name": "Fantasy" + } + ], + "game_modes": [ + { + "name": "Single player" + } + ] + }, + { + "id": 233, + "genres": [ + { + "name": "Shooter" + } + ], + "themes": [ + { + "name": "Action" + }, + { + "name": "Science fiction" + } + ], + "game_modes": [ + { + "name": "Single player" + } + ] + }, + { + "id": 133236, + "genres": [ + { + "name": "Adventure" + }, + { + "name": "Indie" + }, + { + "name": "Simulator" + } + ], + "themes": [ + { + "name": "Action" + }, + { + "name": "Fantasy" + }, + { + "name": "Sandbox" + }, + { + "name": "Survival" + } + ], + "game_modes": [ + { + "name": "Single player" + }, + { + "name": "Multiplayer" + }, + { + "name": "Co-operative" + } + ] + }, + { + "id": 119388, + "genres": [ + { + "name": "Adventure" + }, + { + "name": "Role-playing (RPG)" + } + ], + "themes": [ + { + "name": "Action" + }, + { + "name": "Fantasy" + }, + { + "name": "Open world" + } + ], + "game_modes": [ + { + "name": "Single player" + } + ] + } +] diff --git a/local/wiremock/__files/igdb-time-to-beats.json b/local/wiremock/__files/igdb-time-to-beats.json new file mode 100644 index 0000000..7d9ac0e --- /dev/null +++ b/local/wiremock/__files/igdb-time-to-beats.json @@ -0,0 +1,58 @@ +[ + { + "game_id": 72, + "hastily": 21600, + "normally": 32400, + "completely": 79200, + "count": 100 + }, + { + "game_id": 113112, + "hastily": 72000, + "normally": 90000, + "completely": 342000, + "count": 100 + }, + { + "game_id": 17000, + "hastily": 144000, + "normally": 190800, + "completely": 576000, + "count": 100 + }, + { + "game_id": 7046, + "hastily": 108000, + "normally": 216000, + "completely": 900000, + "count": 100 + }, + { + "game_id": 26226, + "hastily": 28800, + "normally": 43200, + "completely": 144000, + "count": 100 + }, + { + "game_id": 14593, + "hastily": 97200, + "normally": 147600, + "completely": 226800, + "count": 100 + }, + { + "game_id": 233, + "hastily": 43200, + "normally": 50400, + "completely": 68400, + "count": 100 + }, + { + "game_id": 119388, + "hastily": 180000, + "normally": 216000, + "completely": 684000, + "count": 100 + } +] diff --git a/local/wiremock/__files/speedrun-games-celeste.json b/local/wiremock/__files/speedrun-games-celeste.json new file mode 100644 index 0000000..48f29d3 --- /dev/null +++ b/local/wiremock/__files/speedrun-games-celeste.json @@ -0,0 +1,17 @@ +{ + "data": [ + { + "id": "o1y9j9v6", + "names": { + "international": "Celeste" + }, + "released": 2018, + "weblink": "https://www.speedrun.com/celeste", + "links": [ + { + "rel": "self" + } + ] + } + ] +} diff --git a/local/wiremock/__files/speedrun-runs-celeste.json b/local/wiremock/__files/speedrun-runs-celeste.json new file mode 100644 index 0000000..c85860b --- /dev/null +++ b/local/wiremock/__files/speedrun-runs-celeste.json @@ -0,0 +1,7 @@ +{ + "data": [ + { + "id": "y8dwozoj" + } + ] +} diff --git a/local/wiremock/__files/steam-global-percentages-504230.json b/local/wiremock/__files/steam-global-percentages-504230.json new file mode 100644 index 0000000..b2e84c2 --- /dev/null +++ b/local/wiremock/__files/steam-global-percentages-504230.json @@ -0,0 +1,22 @@ +{ + "achievementpercentages": { + "achievements": [ + { + "name": "FIRST_STEPS", + "percent": 88.0 + }, + { + "name": "CRYSTAL_HEART", + "percent": 35.2 + }, + { + "name": "SUMMIT", + "percent": 30.6 + }, + { + "name": "FAREWELL", + "percent": 5.9 + } + ] + } +} diff --git a/local/wiremock/__files/steam-global-percentages-620.json b/local/wiremock/__files/steam-global-percentages-620.json new file mode 100644 index 0000000..1effc03 --- /dev/null +++ b/local/wiremock/__files/steam-global-percentages-620.json @@ -0,0 +1,22 @@ +{ + "achievementpercentages": { + "achievements": [ + { + "name": "ACH_WAKE_UP", + "percent": 92.1 + }, + { + "name": "ACH_SURVIVE_CONTAINMENT", + "percent": 70.4 + }, + { + "name": "ACH_CO_OP", + "percent": 22.5 + }, + { + "name": "ACH_END_SP", + "percent": 48.3 + } + ] + } +} diff --git a/local/wiremock/__files/steam-owned-games.json b/local/wiremock/__files/steam-owned-games.json new file mode 100644 index 0000000..e8ed248 --- /dev/null +++ b/local/wiremock/__files/steam-owned-games.json @@ -0,0 +1,71 @@ +{ + "response": { + "game_count": 8, + "games": [ + { + "appid": 620, + "name": "Portal 2", + "playtime_forever": 1450, + "playtime_2weeks": null, + "img_icon_url": "", + "has_community_visible_stats": true + }, + { + "appid": 1145360, + "name": "Hades", + "playtime_forever": 3200, + "playtime_2weeks": 240, + "img_icon_url": "", + "has_community_visible_stats": true + }, + { + "appid": 413150, + "name": "Stardew Valley", + "playtime_forever": 5400, + "playtime_2weeks": null, + "img_icon_url": "", + "has_community_visible_stats": true + }, + { + "appid": 427520, + "name": "Factorio", + "playtime_forever": 12000, + "playtime_2weeks": 600, + "img_icon_url": "", + "has_community_visible_stats": true + }, + { + "appid": 504230, + "name": "Celeste", + "playtime_forever": 800, + "playtime_2weeks": null, + "img_icon_url": "", + "has_community_visible_stats": true + }, + { + "appid": 367520, + "name": "Hollow Knight", + "playtime_forever": 0, + "playtime_2weeks": null, + "img_icon_url": "", + "has_community_visible_stats": true + }, + { + "appid": 220, + "name": "Half-Life 2", + "playtime_forever": 780, + "playtime_2weeks": null, + "img_icon_url": "", + "has_community_visible_stats": true + }, + { + "appid": 892970, + "name": "Valheim", + "playtime_forever": 90, + "playtime_2weeks": 90, + "img_icon_url": "", + "has_community_visible_stats": true + } + ] + } +} diff --git a/local/wiremock/__files/steam-player-achievements-504230.json b/local/wiremock/__files/steam-player-achievements-504230.json new file mode 100644 index 0000000..bbfd790 --- /dev/null +++ b/local/wiremock/__files/steam-player-achievements-504230.json @@ -0,0 +1,29 @@ +{ + "playerstats": { + "steamID": "76561198000000001", + "gameName": "Celeste", + "success": true, + "achievements": [ + { + "apiname": "FIRST_STEPS", + "achieved": 1, + "unlocktime": 1700000000 + }, + { + "apiname": "CRYSTAL_HEART", + "achieved": 1, + "unlocktime": 1700003600 + }, + { + "apiname": "SUMMIT", + "achieved": 0, + "unlocktime": 0 + }, + { + "apiname": "FAREWELL", + "achieved": 0, + "unlocktime": 0 + } + ] + } +} diff --git a/local/wiremock/__files/steam-player-achievements-620.json b/local/wiremock/__files/steam-player-achievements-620.json new file mode 100644 index 0000000..466a03b --- /dev/null +++ b/local/wiremock/__files/steam-player-achievements-620.json @@ -0,0 +1,29 @@ +{ + "playerstats": { + "steamID": "76561198000000001", + "gameName": "Portal 2", + "success": true, + "achievements": [ + { + "apiname": "ACH_WAKE_UP", + "achieved": 1, + "unlocktime": 1700000000 + }, + { + "apiname": "ACH_SURVIVE_CONTAINMENT", + "achieved": 1, + "unlocktime": 1700003600 + }, + { + "apiname": "ACH_CO_OP", + "achieved": 0, + "unlocktime": 0 + }, + { + "apiname": "ACH_END_SP", + "achieved": 1, + "unlocktime": 1700010800 + } + ] + } +} diff --git a/local/wiremock/__files/steam-player-summary.json b/local/wiremock/__files/steam-player-summary.json new file mode 100644 index 0000000..d800453 --- /dev/null +++ b/local/wiremock/__files/steam-player-summary.json @@ -0,0 +1,12 @@ +{ + "response": { + "players": [ + { + "steamid": "76561198000000001", + "personaname": "Dev", + "profileurl": "https://steamcommunity.com/profiles/76561198000000001/", + "avatarfull": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg" + } + ] + } +} diff --git a/local/wiremock/__files/steam-schema-504230.json b/local/wiremock/__files/steam-schema-504230.json new file mode 100644 index 0000000..1bd6f42 --- /dev/null +++ b/local/wiremock/__files/steam-schema-504230.json @@ -0,0 +1,42 @@ +{ + "game": { + "gameName": "Celeste", + "gameVersion": "1", + "availableGameStats": { + "achievements": [ + { + "name": "FIRST_STEPS", + "displayName": "First Steps", + "description": "Complete the Prologue.", + "hidden": 0, + "icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg", + "icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg" + }, + { + "name": "CRYSTAL_HEART", + "displayName": "Heart of the Mountain", + "description": "Collect a Crystal Heart.", + "hidden": 0, + "icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg", + "icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg" + }, + { + "name": "SUMMIT", + "displayName": "Summit", + "description": "Reach the summit and finish the story.", + "hidden": 0, + "icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg", + "icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg" + }, + { + "name": "FAREWELL", + "displayName": "Farewell", + "description": "Complete Chapter 9.", + "hidden": 0, + "icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg", + "icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/504230/capsule_184x69.jpg" + } + ] + } + } +} diff --git a/local/wiremock/__files/steam-schema-620.json b/local/wiremock/__files/steam-schema-620.json new file mode 100644 index 0000000..40937cd --- /dev/null +++ b/local/wiremock/__files/steam-schema-620.json @@ -0,0 +1,42 @@ +{ + "game": { + "gameName": "Portal 2", + "gameVersion": "1", + "availableGameStats": { + "achievements": [ + { + "name": "ACH_WAKE_UP", + "displayName": "Wake Up Call", + "description": "Survive the manual override of the relaxation center.", + "hidden": 0, + "icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg", + "icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg" + }, + { + "name": "ACH_SURVIVE_CONTAINMENT", + "displayName": "Pit Boss", + "description": "Show that pit who is boss.", + "hidden": 0, + "icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg", + "icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg" + }, + { + "name": "ACH_CO_OP", + "displayName": "Team Building", + "description": "Complete all test chambers in the Team Building co-op course.", + "hidden": 0, + "icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg", + "icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg" + }, + { + "name": "ACH_END_SP", + "displayName": "Lunacy", + "description": "Complete the single player campaign.", + "hidden": 0, + "icon": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg", + "icongray": "https://cdn.cloudflare.steamstatic.com/steam/apps/620/capsule_184x69.jpg" + } + ] + } + } +} diff --git a/local/wiremock/__files/steam-store-items.json b/local/wiremock/__files/steam-store-items.json new file mode 100644 index 0000000..f5d2f40 --- /dev/null +++ b/local/wiremock/__files/steam-store-items.json @@ -0,0 +1,34 @@ +{ + "response": { + "store_items": [ + { + "appid": 1030300, + "success": 1, + "name": "Hollow Knight: Silksong", + "best_purchase_option": { + "formatted_final_price": "19,50€", + "formatted_original_price": "19,50€", + "discount_pct": 0 + }, + "assets": { + "asset_url_format": "steam/apps/1030300/${FILENAME}", + "header": "header.jpg" + } + }, + { + "appid": 1145350, + "success": 1, + "name": "Hades II", + "best_purchase_option": { + "formatted_final_price": "22,49€", + "formatted_original_price": "29,99€", + "discount_pct": 25 + }, + "assets": { + "asset_url_format": "steam/apps/1145350/${FILENAME}", + "header": "header.jpg" + } + } + ] + } +} diff --git a/local/wiremock/__files/steam-wishlist.json b/local/wiremock/__files/steam-wishlist.json new file mode 100644 index 0000000..62d3d5e --- /dev/null +++ b/local/wiremock/__files/steam-wishlist.json @@ -0,0 +1,14 @@ +{ + "response": { + "items": [ + { + "appid": 1030300, + "date_added": 1735689600 + }, + { + "appid": 1145350, + "date_added": 1735776000 + } + ] + } +} diff --git a/local/wiremock/__files/twitch-token.json b/local/wiremock/__files/twitch-token.json new file mode 100644 index 0000000..9c7ba42 --- /dev/null +++ b/local/wiremock/__files/twitch-token.json @@ -0,0 +1,5 @@ +{ + "access_token": "local", + "expires_in": 5000000, + "token_type": "bearer" +} diff --git a/local/wiremock/mappings/igdb-external-games.json b/local/wiremock/mappings/igdb-external-games.json new file mode 100644 index 0000000..806fd62 --- /dev/null +++ b/local/wiremock/mappings/igdb-external-games.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "POST", + "urlPath": "/igdb/external_games" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "igdb-external-games.json" + } +} diff --git a/local/wiremock/mappings/igdb-game-113112.json b/local/wiremock/mappings/igdb-game-113112.json new file mode 100644 index 0000000..779338a --- /dev/null +++ b/local/wiremock/mappings/igdb-game-113112.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "POST", + "urlPath": "/igdb/games", + "bodyPatterns": [ + { + "contains": "where id = 113112;" + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "igdb-game-113112.json" + } +} diff --git a/local/wiremock/mappings/igdb-game-119388.json b/local/wiremock/mappings/igdb-game-119388.json new file mode 100644 index 0000000..b84111f --- /dev/null +++ b/local/wiremock/mappings/igdb-game-119388.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "POST", + "urlPath": "/igdb/games", + "bodyPatterns": [ + { + "contains": "where id = 119388;" + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "igdb-game-119388.json" + } +} diff --git a/local/wiremock/mappings/igdb-game-26226.json b/local/wiremock/mappings/igdb-game-26226.json new file mode 100644 index 0000000..cbeee83 --- /dev/null +++ b/local/wiremock/mappings/igdb-game-26226.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "POST", + "urlPath": "/igdb/games", + "bodyPatterns": [ + { + "contains": "where id = 26226;" + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "igdb-game-26226.json" + } +} diff --git a/local/wiremock/mappings/igdb-game-search.json b/local/wiremock/mappings/igdb-game-search.json new file mode 100644 index 0000000..bdf2201 --- /dev/null +++ b/local/wiremock/mappings/igdb-game-search.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "POST", + "urlPath": "/igdb/games", + "bodyPatterns": [ + { + "contains": "search \"" + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "igdb-game-search.json" + } +} diff --git a/local/wiremock/mappings/igdb-game-years.json b/local/wiremock/mappings/igdb-game-years.json new file mode 100644 index 0000000..4890a5d --- /dev/null +++ b/local/wiremock/mappings/igdb-game-years.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "POST", + "urlPath": "/igdb/games", + "bodyPatterns": [ + { + "contains": "fields first_release_date;" + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "igdb-game-years.json" + } +} diff --git a/local/wiremock/mappings/igdb-games-none.json b/local/wiremock/mappings/igdb-games-none.json new file mode 100644 index 0000000..6ba2699 --- /dev/null +++ b/local/wiremock/mappings/igdb-games-none.json @@ -0,0 +1,14 @@ +{ + "request": { + "method": "POST", + "urlPath": "/igdb/games" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": [] + }, + "priority": 10 +} diff --git a/local/wiremock/mappings/igdb-tags.json b/local/wiremock/mappings/igdb-tags.json new file mode 100644 index 0000000..59e649c --- /dev/null +++ b/local/wiremock/mappings/igdb-tags.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "POST", + "urlPath": "/igdb/games", + "bodyPatterns": [ + { + "contains": "genres.name" + } + ] + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "igdb-tags.json" + } +} diff --git a/local/wiremock/mappings/igdb-time-to-beats.json b/local/wiremock/mappings/igdb-time-to-beats.json new file mode 100644 index 0000000..ca2aa42 --- /dev/null +++ b/local/wiremock/mappings/igdb-time-to-beats.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "POST", + "urlPath": "/igdb/game_time_to_beats" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "igdb-time-to-beats.json" + } +} diff --git a/local/wiremock/mappings/speedrun-games-celeste.json b/local/wiremock/mappings/speedrun-games-celeste.json new file mode 100644 index 0000000..9d06294 --- /dev/null +++ b/local/wiremock/mappings/speedrun-games-celeste.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "GET", + "urlPath": "/speedrun/games", + "queryParameters": { + "name": { + "equalTo": "Celeste" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "speedrun-games-celeste.json" + } +} diff --git a/local/wiremock/mappings/speedrun-games-none.json b/local/wiremock/mappings/speedrun-games-none.json new file mode 100644 index 0000000..fb49fad --- /dev/null +++ b/local/wiremock/mappings/speedrun-games-none.json @@ -0,0 +1,16 @@ +{ + "request": { + "method": "GET", + "urlPath": "/speedrun/games" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "data": [] + } + }, + "priority": 10 +} diff --git a/local/wiremock/mappings/speedrun-runs-celeste.json b/local/wiremock/mappings/speedrun-runs-celeste.json new file mode 100644 index 0000000..4b78190 --- /dev/null +++ b/local/wiremock/mappings/speedrun-runs-celeste.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "GET", + "urlPath": "/speedrun/runs", + "queryParameters": { + "game": { + "equalTo": "o1y9j9v6" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "speedrun-runs-celeste.json" + } +} diff --git a/local/wiremock/mappings/speedrun-runs-none.json b/local/wiremock/mappings/speedrun-runs-none.json new file mode 100644 index 0000000..b4c114f --- /dev/null +++ b/local/wiremock/mappings/speedrun-runs-none.json @@ -0,0 +1,16 @@ +{ + "request": { + "method": "GET", + "urlPath": "/speedrun/runs" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "data": [] + } + }, + "priority": 10 +} diff --git a/local/wiremock/mappings/steam-global-percentages-504230.json b/local/wiremock/mappings/steam-global-percentages-504230.json new file mode 100644 index 0000000..a217f6e --- /dev/null +++ b/local/wiremock/mappings/steam-global-percentages-504230.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "GET", + "urlPath": "/steam/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v2/", + "queryParameters": { + "gameid": { + "equalTo": "504230" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "steam-global-percentages-504230.json" + } +} diff --git a/local/wiremock/mappings/steam-global-percentages-620.json b/local/wiremock/mappings/steam-global-percentages-620.json new file mode 100644 index 0000000..d68f4f7 --- /dev/null +++ b/local/wiremock/mappings/steam-global-percentages-620.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "GET", + "urlPath": "/steam/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v2/", + "queryParameters": { + "gameid": { + "equalTo": "620" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "steam-global-percentages-620.json" + } +} diff --git a/local/wiremock/mappings/steam-global-percentages-none.json b/local/wiremock/mappings/steam-global-percentages-none.json new file mode 100644 index 0000000..f70c552 --- /dev/null +++ b/local/wiremock/mappings/steam-global-percentages-none.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "GET", + "urlPath": "/steam/ISteamUserStats/GetGlobalAchievementPercentagesForApp/v2/" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "achievementpercentages": { + "achievements": [] + } + } + }, + "priority": 10 +} diff --git a/local/wiremock/mappings/steam-owned-games.json b/local/wiremock/mappings/steam-owned-games.json new file mode 100644 index 0000000..46c7a36 --- /dev/null +++ b/local/wiremock/mappings/steam-owned-games.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "urlPath": "/steam/IPlayerService/GetOwnedGames/v1/" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "steam-owned-games.json" + } +} diff --git a/local/wiremock/mappings/steam-player-achievements-504230.json b/local/wiremock/mappings/steam-player-achievements-504230.json new file mode 100644 index 0000000..1a05217 --- /dev/null +++ b/local/wiremock/mappings/steam-player-achievements-504230.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "GET", + "urlPath": "/steam/ISteamUserStats/GetPlayerAchievements/v1/", + "queryParameters": { + "appid": { + "equalTo": "504230" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "steam-player-achievements-504230.json" + } +} diff --git a/local/wiremock/mappings/steam-player-achievements-620.json b/local/wiremock/mappings/steam-player-achievements-620.json new file mode 100644 index 0000000..04f2dba --- /dev/null +++ b/local/wiremock/mappings/steam-player-achievements-620.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "GET", + "urlPath": "/steam/ISteamUserStats/GetPlayerAchievements/v1/", + "queryParameters": { + "appid": { + "equalTo": "620" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "steam-player-achievements-620.json" + } +} diff --git a/local/wiremock/mappings/steam-player-achievements-none.json b/local/wiremock/mappings/steam-player-achievements-none.json new file mode 100644 index 0000000..7cdf89b --- /dev/null +++ b/local/wiremock/mappings/steam-player-achievements-none.json @@ -0,0 +1,19 @@ +{ + "request": { + "method": "GET", + "urlPath": "/steam/ISteamUserStats/GetPlayerAchievements/v1/" + }, + "response": { + "status": 400, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "playerstats": { + "error": "Requested app has no stats", + "success": false + } + } + }, + "priority": 10 +} diff --git a/local/wiremock/mappings/steam-player-summary.json b/local/wiremock/mappings/steam-player-summary.json new file mode 100644 index 0000000..152f1f5 --- /dev/null +++ b/local/wiremock/mappings/steam-player-summary.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "urlPath": "/steam/ISteamUser/GetPlayerSummaries/v0002/" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "steam-player-summary.json" + } +} diff --git a/local/wiremock/mappings/steam-schema-504230.json b/local/wiremock/mappings/steam-schema-504230.json new file mode 100644 index 0000000..95d5e66 --- /dev/null +++ b/local/wiremock/mappings/steam-schema-504230.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "GET", + "urlPath": "/steam/ISteamUserStats/GetSchemaForGame/v2/", + "queryParameters": { + "appid": { + "equalTo": "504230" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "steam-schema-504230.json" + } +} diff --git a/local/wiremock/mappings/steam-schema-620.json b/local/wiremock/mappings/steam-schema-620.json new file mode 100644 index 0000000..cd53855 --- /dev/null +++ b/local/wiremock/mappings/steam-schema-620.json @@ -0,0 +1,18 @@ +{ + "request": { + "method": "GET", + "urlPath": "/steam/ISteamUserStats/GetSchemaForGame/v2/", + "queryParameters": { + "appid": { + "equalTo": "620" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "steam-schema-620.json" + } +} diff --git a/local/wiremock/mappings/steam-schema-none.json b/local/wiremock/mappings/steam-schema-none.json new file mode 100644 index 0000000..8b2586a --- /dev/null +++ b/local/wiremock/mappings/steam-schema-none.json @@ -0,0 +1,14 @@ +{ + "request": { + "method": "GET", + "urlPath": "/steam/ISteamUserStats/GetSchemaForGame/v2/" + }, + "response": { + "status": 400, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": {} + }, + "priority": 10 +} diff --git a/local/wiremock/mappings/steam-store-items.json b/local/wiremock/mappings/steam-store-items.json new file mode 100644 index 0000000..b8e533b --- /dev/null +++ b/local/wiremock/mappings/steam-store-items.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "urlPath": "/steam/IStoreBrowseService/GetItems/v1/" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "steam-store-items.json" + } +} diff --git a/local/wiremock/mappings/steam-wishlist.json b/local/wiremock/mappings/steam-wishlist.json new file mode 100644 index 0000000..64a77cf --- /dev/null +++ b/local/wiremock/mappings/steam-wishlist.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "GET", + "urlPath": "/steam/IWishlistService/GetWishlist/v1/" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "steam-wishlist.json" + } +} diff --git a/local/wiremock/mappings/twitch-token.json b/local/wiremock/mappings/twitch-token.json new file mode 100644 index 0000000..c9cd673 --- /dev/null +++ b/local/wiremock/mappings/twitch-token.json @@ -0,0 +1,13 @@ +{ + "request": { + "method": "POST", + "urlPath": "/twitch/oauth2/token" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFileName": "twitch-token.json" + } +} diff --git a/readme.md b/readme.md index 0513069..ba1c603 100644 --- a/readme.md +++ b/readme.md @@ -264,7 +264,46 @@ a second row when IGDB learns its Steam appid later and the user owns it on Stea ## Run locally -Needs Java 25 and Docker. +### Local development without keys (profile `local`) + +The whole stack runs on your machine with no API keys and no calls to Steam, Twitch/IGDB or speedrun.com. Needs +Java 25, Node 24 and Docker. Clone `ToBePlayed-Auth`, `ToBePlayed-Core` and `ToBePlayed-Frontend` next to each +other, then start them in this order (Core also starts without Auth and finds it later): + +1. **Auth**: in `ToBePlayed-Auth`, `./mvnw spring-boot:run -Dspring-boot.run.profiles=local` + (IDE: active profile `local`). Port 8080. +2. **Core**: in `ToBePlayed-Core`, `./mvnw spring-boot:run -Dspring-boot.run.profiles=local`. Port 8090. +3. **Frontend**: in `ToBePlayed-Frontend`, `npm install`, then `npm run dev`, and open `http://localhost:5173` + (`localhost`, not `127.0.0.1`, so the browser sends the login cookies to every local port). The development + values must be `VITE_API_URL=http://localhost:8080` and `VITE_CORE_API_URL=http://localhost:8090`. +4. **Log in** with `dev@localhost` / `password123`. On the dashboard, start a Steam sync: the library fills with 8 + games, achievements, time to beat, moods, a wishlist and one speedrun game from recorded answers. + +What the `local` profile does: + +- Spring Boot's Docker Compose support starts each service's `compose.yaml`: PostgreSQL for Auth (port 5432) and + for Core (port 5433), and in Core a WireMock container (port 8089, compose profile `local`) that answers in + place of Steam, Twitch/IGDB and speedrun.com from `ToBePlayed-Core/local/wiremock`. +- Auth makes a new signing key at each start (`LocalKeysEnvironmentPostProcessor`, only in this profile), so no + key is in the repo. After an Auth restart (also a devtools restart), log in again. +- Never use `local` on a server: it adds an admin with a public password. Auth refuses `local` together with + `prod`. +- Auth adds the dev user (Flyway callback in `db/local`): role `PLATFORM_ADMIN`, so user, auditor and admin pages + can all be tried ("View as" in the menu switches down), and a linked Steam ID that the recorded Steam answers + belong to. The invite code for more users is `local`. +- Core checks the login with Auth's local JWKS, like in production. +- The browser still loads game pictures from the Steam and IGDB image servers; offline they stay gray. +- "Sign in through Steam" still opens the real Steam page (no key needed). The dev user is already linked. +- A request to WireMock that has no recording answers 404 and shows up in Core's log. + +To change the recorded answers, edit the JSON in `local/wiremock/mappings` (which request) and +`local/wiremock/__files` (the answer), then restart Core (it restarts the container). `LocalMocksParseTest` checks +that every answer still fits the adapters' response types. `http://localhost:8089/__admin/requests` lists what Core +asked. + +### With the real services (profile `dev`) + +Needs Java 25, Docker and your own keys. ```bash ./mvnw spring-boot:run -Dspring-boot.run.profiles=dev diff --git a/src/main/java/core_service/modules/igdb_adapter/service/IgdbApiService.java b/src/main/java/core_service/modules/igdb_adapter/service/IgdbApiService.java index c178818..5664d20 100644 --- a/src/main/java/core_service/modules/igdb_adapter/service/IgdbApiService.java +++ b/src/main/java/core_service/modules/igdb_adapter/service/IgdbApiService.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Comparator; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -275,13 +276,14 @@ public class IgdbApiService { List ids = List.copyOf(igdbIds); Map result = new HashMap<>(); for (int from = 0; from < ids.size(); from += BATCH) { - String gameIds = ids.subList(from, Math.min(from + BATCH, ids.size())).stream() - .map(String::valueOf).collect(Collectors.joining(",")); + List batchIds = ids.subList(from, Math.min(from + BATCH, ids.size())); + Set batch = new HashSet<>(batchIds); + String gameIds = batchIds.stream().map(String::valueOf).collect(Collectors.joining(",")); IgdbResponses.TimeToBeat[] times = query("/game_time_to_beats", "fields game_id,hastily,normally,completely,count; where game_id = (" + gameIds + "); limit " + LIMIT + ";", IgdbResponses.TimeToBeat[].class); for (IgdbResponses.TimeToBeat time : times) { - if (time.gameId() != null) { + if (time.gameId() != null && batch.contains(time.gameId())) { result.putIfAbsent(time.gameId(), new TimeToBeat(minutes(time.hastily()), minutes(time.normally()), minutes(time.completely()))); } @@ -353,8 +355,10 @@ public class IgdbApiService { "fields genres.name,themes.name,game_modes.name; where id = (" + gameIds + "); limit " + LIMIT + ";", IgdbResponses.GameTags[].class); Map result = new HashMap<>(); + // Only the asked ids, in this and the other lookups: callers index their maps by them, and an answer can + // hold more games. for (IgdbResponses.GameTags game : games) { - if (game.id() != null) { + if (game.id() != null && igdbIds.contains(game.id())) { result.put(game.id(), new GameTags(names(game.genres()), names(game.themes()), names(game.gameModes()))); } } @@ -371,7 +375,7 @@ public class IgdbApiService { IgdbResponses.Game[].class); Map result = new HashMap<>(); for (IgdbResponses.Game game : games) { - if (game.id() != null && game.firstReleaseDate() != null) { + if (game.id() != null && game.firstReleaseDate() != null && igdbIds.contains(game.id())) { result.put(game.id(), year(game.firstReleaseDate())); } } @@ -397,7 +401,8 @@ public class IgdbApiService { IgdbResponses.ExternalGame[].class); Map> steamByIgdb = new HashMap<>(); for (IgdbResponses.ExternalGame external : externals) { - if (external.game() != null && external.uid() != null && external.uid().matches("\\d+")) { + if (external.game() != null && external.uid() != null && external.uid().matches("\\d+") + && steamAppids.contains(Long.parseLong(external.uid()))) { steamByIgdb.computeIfAbsent(external.game(), game -> new ArrayList<>()).add(Long.parseLong(external.uid())); } } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..10b6c5d --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,31 @@ +# Local development without keys or third parties: start with the profile "local" (alone), after Auth's local +# profile. Postgres and WireMock (local/wiremock) come from compose.yaml. See the readme, "Run locally". +spring: + docker: + compose: + profiles: + active: local + # Also when Postgres still runs from a dev run; else WireMock would not start. + start: + skip: never + +security: + jwt: + jwks-url: "http://localhost:8080/.well-known/jwks.json" + +app: + cors: + allowed-origins: "http://localhost:5173" + steam: + api-key: local + base-url: "http://localhost:8089/steam" + igdb: + client-id: local + client-secret: local + base-url: "http://localhost:8089/igdb" + token-base-url: "http://localhost:8089/twitch" + speedrun: + base-url: "http://localhost:8089/speedrun" + library: + # The image repair checks the real Steam CDN. + image-repair-cron: "-" diff --git a/src/test/java/core_service/configuration/LocalMocksParseTest.java b/src/test/java/core_service/configuration/LocalMocksParseTest.java new file mode 100644 index 0000000..ec032cb --- /dev/null +++ b/src/test/java/core_service/configuration/LocalMocksParseTest.java @@ -0,0 +1,104 @@ +package core_service.configuration; + +import core_service.modules.igdb_adapter.api.dto.response.IgdbResponses; +import core_service.modules.speedrun_adapter.api.dto.response.SpeedrunResponses; +import core_service.modules.steam_adapter.api.dto.response.SteamGlobalAchievementPercentagesResponse; +import core_service.modules.steam_adapter.api.dto.response.SteamOwnedGamesResponse; +import core_service.modules.steam_adapter.api.dto.response.SteamPlayerAchievementsResponse; +import core_service.modules.steam_adapter.api.dto.response.SteamPlayerSummaryResponse; +import core_service.modules.steam_adapter.api.dto.response.SteamSchemaForGameResponse; +import core_service.modules.steam_adapter.api.dto.response.SteamStoreItemsResponse; +import core_service.modules.steam_adapter.api.dto.response.SteamWishlistResponse; +import core_service.shared.test.AbstractUnitTest; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.json.JsonMapper; + +import java.io.IOException; +import java.lang.reflect.RecordComponent; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The recorded answers of the local profile (local/wiremock) still fit the adapters' response types. + */ +class LocalMocksParseTest extends AbstractUnitTest { + + private static final Path FILES = Path.of("local/wiremock/__files"); + private static final Path MAPPINGS = Path.of("local/wiremock/mappings"); + + // By file name prefix; no prefix starts another one. + private static final Map> TYPES = Map.ofEntries( + Map.entry("steam-owned-games", SteamOwnedGamesResponse.class), + Map.entry("steam-player-summary", SteamPlayerSummaryResponse.class), + Map.entry("steam-wishlist", SteamWishlistResponse.class), + Map.entry("steam-store-items", SteamStoreItemsResponse.class), + Map.entry("steam-schema-", SteamSchemaForGameResponse.class), + Map.entry("steam-global-percentages-", SteamGlobalAchievementPercentagesResponse.class), + Map.entry("steam-player-achievements-", SteamPlayerAchievementsResponse.class), + Map.entry("twitch-token", IgdbResponses.Token.class), + Map.entry("igdb-external-games", IgdbResponses.ExternalGame[].class), + Map.entry("igdb-time-to-beats", IgdbResponses.TimeToBeat[].class), + Map.entry("igdb-tags", IgdbResponses.GameTags[].class), + Map.entry("igdb-game-", IgdbResponses.Game[].class), + Map.entry("speedrun-games-", SpeedrunResponses.Games.class), + Map.entry("speedrun-runs-", SpeedrunResponses.Runs.class)); + + private final JsonMapper jsonMapper = JsonMapper.builder().build(); + + @Test + void everyMapping_PointsToAnExistingAnswer() throws IOException { + List mappings; + try (Stream listed = Files.list(MAPPINGS)) { + mappings = listed.sorted().toList(); + } + assertThat(mappings).isNotEmpty(); + + for (Path mapping : mappings) { + JsonNode response = jsonMapper.readTree(mapping.toFile()).path("response"); + assertThat(response.has("bodyFileName") || response.has("jsonBody")).as(mapping.toString()).isTrue(); + if (response.has("bodyFileName")) { + assertThat(FILES.resolve(response.get("bodyFileName").asString())).as(mapping.toString()).exists(); + } + } + } + + // Every top-level field of an answer, and the first field (the id) of every list element, is not null. + private static void assertFilled(String name, Object parsed) throws ReflectiveOperationException { + if (parsed instanceof Object[] elements) { + assertThat(elements).as(name).isNotEmpty(); + for (Object element : elements) { + RecordComponent id = element.getClass().getRecordComponents()[0]; + assertThat(id.getAccessor().invoke(element)).as(name + " " + id.getName()).isNotNull(); + } + return; + } + for (RecordComponent component : parsed.getClass().getRecordComponents()) { + assertThat(component.getAccessor().invoke(parsed)).as(name + " " + component.getName()).isNotNull(); + } + } + + @Test + void everyRecordedAnswer_ParsesIntoItsResponseType() throws IOException, ReflectiveOperationException { + List files; + try (Stream listed = Files.list(FILES)) { + files = listed.sorted().toList(); + } + assertThat(files).isNotEmpty(); + + for (Path file : files) { + String name = file.getFileName().toString(); + Class type = TYPES.entrySet().stream() + .filter(entry -> name.startsWith(entry.getKey())) + .map(Map.Entry::getValue) + .findFirst() + .orElseThrow(() -> new AssertionError("No response type for " + name)); + assertFilled(name, jsonMapper.readValue(file.toFile(), type)); + } + } +} diff --git a/src/test/java/core_service/modules/igdb_adapter/service/IgdbApiServiceTest.java b/src/test/java/core_service/modules/igdb_adapter/service/IgdbApiServiceTest.java index 5f88adc..57fa15e 100644 --- a/src/test/java/core_service/modules/igdb_adapter/service/IgdbApiServiceTest.java +++ b/src/test/java/core_service/modules/igdb_adapter/service/IgdbApiServiceTest.java @@ -127,6 +127,38 @@ class IgdbApiServiceTest extends AbstractUnitTest { assertThat(tags.get(620L).themes()).contains("Horror"); } + @Test + void getTimeToBeat_AnswersForAppsAndGamesNotAskedFor_AreLeftOut() { + expectToken(); + igdb.expect(requestTo(containsString("/external_games"))) + .andRespond(withSuccess("[{\"game\": 72, \"uid\": \"620\"}, {\"game\": 73, \"uid\": \"999\"}]", + MediaType.APPLICATION_JSON)); + igdb.expect(requestTo(containsString("/game_time_to_beats"))) + .andRespond(withSuccess("[{\"game_id\": 72, \"normally\": 3600}, {\"game_id\": 73, \"normally\": 7200}]", + MediaType.APPLICATION_JSON)); + igdb.expect(requestTo(containsString("/game_time_to_beats"))) + .andRespond(withSuccess("[{\"game_id\": 1025, \"normally\": 3600}, {\"game_id\": 1026, \"normally\": 7200}]", + MediaType.APPLICATION_JSON)); + + assertThat(service.getTimeToBeat(List.of(620L)).orElseThrow()).containsOnlyKeys(620L); + assertThat(service.getTimeToBeatByGameIds(List.of(1025L)).orElseThrow()).containsOnlyKeys(1025L); + } + + @Test + void getGameTagsAndYears_AnswerWithAGameNotAskedFor_KeepTheAskedGames() { + expectToken(); + for (int call = 0; call < 2; call++) { + igdb.expect(requestTo(containsString("/external_games"))) + .andRespond(withSuccess("[{\"game\": 72, \"uid\": \"620\"}]", MediaType.APPLICATION_JSON)); + igdb.expect(requestTo(containsString("/games"))) + .andRespond(withSuccess("[{\"id\": 72, \"first_release_date\": 1149120000, \"themes\": [{\"name\": \"Horror\"}]}," + + " {\"id\": 999, \"first_release_date\": 1493942400}]", MediaType.APPLICATION_JSON)); + } + + assertThat(service.getGameTags(List.of(620L)).orElseThrow().get(620L).themes()).contains("Horror"); + assertThat(service.getReleaseYears(List.of(620L))).contains(Map.of(620L, Set.of(2006))); + } + @Test void getReleaseYears_SteamAppLinkedToTwoIgdbGames_GetsBothYears() { expectToken();