Public Access
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bd1eda0ec | ||
|
|
07d287cd43 | ||
|
|
5c0c0c6a78 |
No files matched your search
+10
-1
@@ -7,4 +7,13 @@ services:
|
||||
environment:
|
||||
- POSTGRES_DB=tbp_core
|
||||
- POSTGRES_USER=tbp_user
|
||||
- POSTGRES_PASSWORD=local_secret_password
|
||||
- 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
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "o1y9j9v6",
|
||||
"names": {
|
||||
"international": "Celeste"
|
||||
},
|
||||
"released": 2018,
|
||||
"weblink": "https://www.speedrun.com/celeste",
|
||||
"links": [
|
||||
{
|
||||
"rel": "self"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "y8dwozoj"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"response": {
|
||||
"players": [
|
||||
{
|
||||
"steamid": "76561198000000001",
|
||||
"personaname": "Dev",
|
||||
"profileurl": "https://steamcommunity.com/profiles/76561198000000001/",
|
||||
"avatarfull": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"response": {
|
||||
"items": [
|
||||
{
|
||||
"appid": 1030300,
|
||||
"date_added": 1735689600
|
||||
},
|
||||
{
|
||||
"appid": 1145350,
|
||||
"date_added": 1735776000
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"access_token": "local",
|
||||
"expires_in": 5000000,
|
||||
"token_type": "bearer"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"urlPath": "/igdb/external_games"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"bodyFileName": "igdb-external-games.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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"urlPath": "/igdb/games"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"jsonBody": []
|
||||
},
|
||||
"priority": 10
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"urlPath": "/speedrun/games"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"jsonBody": {
|
||||
"data": []
|
||||
}
|
||||
},
|
||||
"priority": 10
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"urlPath": "/speedrun/runs"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"jsonBody": {
|
||||
"data": []
|
||||
}
|
||||
},
|
||||
"priority": 10
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"urlPath": "/steam/ISteamUserStats/GetSchemaForGame/v2/"
|
||||
},
|
||||
"response": {
|
||||
"status": 400,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"jsonBody": {}
|
||||
},
|
||||
"priority": 10
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"urlPath": "/steam/IWishlistService/GetWishlist/v1/"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"bodyFileName": "steam-wishlist.json"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"urlPath": "/twitch/oauth2/token"
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"bodyFileName": "twitch-token.json"
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,10 @@ To Be Played is split into three repositories:
|
||||
- **Public profile data**: totals, status counts, games in progress and finished games with rating, perfect games
|
||||
and average completion. Readable only
|
||||
with a profile ticket issued by Auth. Never returns notes, priority or the Steam ID. `stats` holds only the stat
|
||||
groups the user shares (the ticket's `profileStats` claim, set in Auth); none by default.
|
||||
groups the user shares (the ticket's `profileStats` claim, derived in Auth from the profile tiles); none by
|
||||
default. A section the user
|
||||
hides (the ticket's `sections` claim: `SUMMARY`, `STATUS`, `PLAYING`, `FINISHED`, `STATS`) is null and runs no
|
||||
query; a ticket without the claim shows all.
|
||||
|
||||
## Authentication
|
||||
|
||||
@@ -264,7 +267,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
|
||||
|
||||
@@ -66,6 +66,10 @@ public class JwtService implements ProfileTicketVerifier {
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<String> names(List<?> claim) {
|
||||
return claim.stream().filter(String.class::isInstance).map(String.class::cast).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProfileTicket> verifyProfileTicket(String ticket) {
|
||||
try {
|
||||
@@ -75,10 +79,9 @@ public class JwtService implements ProfileTicketVerifier {
|
||||
}
|
||||
long userId = Long.parseLong(claims.get("profileUserId", String.class));
|
||||
// A ticket of an Auth from before the claim has none: then the profile shows no stats.
|
||||
Set<String> profileStats = claims.get("profileStats") instanceof List<?> groups
|
||||
? groups.stream().filter(String.class::isInstance).map(String.class::cast).collect(Collectors.toSet())
|
||||
: Set.of();
|
||||
return Optional.of(new ProfileTicket(userId, profileStats));
|
||||
Set<String> profileStats = claims.get("profileStats") instanceof List<?> groups ? names(groups) : Set.of();
|
||||
Set<String> sections = claims.get("sections") instanceof List<?> shown ? names(shown) : null;
|
||||
return Optional.of(new ProfileTicket(userId, profileStats, sections));
|
||||
} catch (Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -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<Long> ids = List.copyOf(igdbIds);
|
||||
Map<Long, TimeToBeat> 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<Long> batchIds = ids.subList(from, Math.min(from + BATCH, ids.size()));
|
||||
Set<Long> 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<Long, GameTags> 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<Long, Integer> 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<Long, List<Long>> 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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public class PublicProfileController {
|
||||
@GetMapping("/library")
|
||||
public ResponseEntity<?> getPublicLibrary(@RequestHeader(value = "X-Profile-Ticket", required = false) String ticket) {
|
||||
return (ticket == null ? Optional.<ProfileTicket>empty() : profileTicketVerifier.verifyProfileTicket(ticket))
|
||||
.<ResponseEntity<?>>map(t -> ResponseEntity.ok(queryService.getPublicLibrary(t.userId(), t.profileStats())))
|
||||
.<ResponseEntity<?>>map(t -> ResponseEntity.ok(queryService.getPublicLibrary(t.userId(), t.profileStats(), t.sections())))
|
||||
.orElseGet(() -> ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(Map.of("code", "INVALID_TICKET", "message", "Missing, invalid or expired profile ticket")));
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* What a public profile shows. Never notes, priority or anything that identifies the Steam account.
|
||||
* What a public profile shows. Never notes, priority or anything that identifies the Steam account. The fields of a
|
||||
* section the user hides ({@link core_service.modules.library.domain.ProfileSection}) are null.
|
||||
*
|
||||
* @param finishedGames FINISHED and COMPLETED; part of the summary, so it stays when the status counts are hidden
|
||||
* @param byStatus has every status, also those with 0 games
|
||||
* @param playing up to 8 games, most played first
|
||||
* @param finished up to 12 FINISHED or COMPLETED games, best rated first
|
||||
@@ -15,12 +17,13 @@ import java.util.Map;
|
||||
* @param stats only the stat groups the user shares, the others null; null when the user shares none
|
||||
*/
|
||||
public record PublicLibraryDto(
|
||||
long totalGames,
|
||||
long totalPlaytimeMinutes,
|
||||
Long totalGames,
|
||||
Long totalPlaytimeMinutes,
|
||||
Long finishedGames,
|
||||
Map<GameStatus, Long> byStatus,
|
||||
List<Game> playing,
|
||||
List<Game> finished,
|
||||
int perfectGames,
|
||||
Integer perfectGames,
|
||||
Double averageCompletion,
|
||||
UserStatsDto stats
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package core_service.modules.library.domain;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* The parts of the public profile a user can hide. Core sends no data for a hidden part.
|
||||
*/
|
||||
public enum ProfileSection {
|
||||
SUMMARY, STATUS, PLAYING, FINISHED, STATS;
|
||||
|
||||
/**
|
||||
* @param names from the profile ticket; null (a ticket from before the sections) shows all. Unknown names are
|
||||
* left out.
|
||||
*/
|
||||
public static Set<ProfileSection> shown(Collection<String> names) {
|
||||
Set<ProfileSection> sections = EnumSet.noneOf(ProfileSection.class);
|
||||
for (ProfileSection section : values()) {
|
||||
if (names == null || names.contains(section.name())) {
|
||||
sections.add(section);
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import core_service.modules.library.domain.GameSource;
|
||||
import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.GameTimeToBeat;
|
||||
import core_service.modules.library.domain.Mood;
|
||||
import core_service.modules.library.domain.ProfileSection;
|
||||
import core_service.modules.library.domain.StatGroup;
|
||||
import core_service.modules.library.domain.SteamGame;
|
||||
import core_service.modules.library.domain.UserGame;
|
||||
@@ -47,6 +48,7 @@ import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -212,27 +214,45 @@ public class LibraryQueryService {
|
||||
* @param profileStats the stat group names of the profile ticket
|
||||
*/
|
||||
@Public
|
||||
public PublicLibraryDto getPublicLibrary(Long userId, Collection<String> profileStats) {
|
||||
Map<GameStatus, Long> byStatus = userGameRepository.countEveryStatus(userId);
|
||||
List<UserGame> playing = userGameRepository.findListedByStatusIn(userId,
|
||||
List.of(GameStatus.PLAYING),
|
||||
PageRequest.of(0, PUBLIC_PLAYING, Sort.by(Sort.Direction.DESC, "playtimeMinutes").and(Sort.by("appid"))));
|
||||
List<UserGame> finished = userGameRepository.findListedByStatusIn(userId,
|
||||
List.of(GameStatus.FINISHED, GameStatus.COMPLETED),
|
||||
PageRequest.of(0, PUBLIC_FINISHED, Sort.by(Sort.Order.desc("rating").nullsLast()).and(Sort.by("appid"))));
|
||||
Map<Long, SteamGame> meta = steamGameRepository.findAllById(
|
||||
Stream.concat(playing.stream(), finished.stream()).map(UserGame::getAppid).toList())
|
||||
/**
|
||||
* @param sectionNames see {@link ProfileSection#shown}; a hidden section runs no query
|
||||
*/
|
||||
public PublicLibraryDto getPublicLibrary(Long userId, Collection<String> profileStats, Collection<String> sectionNames) {
|
||||
Set<ProfileSection> sections = ProfileSection.shown(sectionNames);
|
||||
boolean summary = sections.contains(ProfileSection.SUMMARY);
|
||||
Map<GameStatus, Long> byStatus = summary || sections.contains(ProfileSection.STATUS)
|
||||
? userGameRepository.countEveryStatus(userId) : null;
|
||||
List<UserGame> playing = sections.contains(ProfileSection.PLAYING)
|
||||
? userGameRepository.findListedByStatusIn(userId, List.of(GameStatus.PLAYING),
|
||||
PageRequest.of(0, PUBLIC_PLAYING, Sort.by(Sort.Direction.DESC, "playtimeMinutes").and(Sort.by("appid"))))
|
||||
: null;
|
||||
List<UserGame> finished = sections.contains(ProfileSection.FINISHED)
|
||||
? userGameRepository.findListedByStatusIn(userId, List.of(GameStatus.FINISHED, GameStatus.COMPLETED),
|
||||
PageRequest.of(0, PUBLIC_FINISHED, Sort.by(Sort.Order.desc("rating").nullsLast()).and(Sort.by("appid"))))
|
||||
: null;
|
||||
Map<Long, SteamGame> meta = steamGameRepository.findAllById(Stream.of(playing, finished)
|
||||
.filter(Objects::nonNull).flatMap(List::stream).map(UserGame::getAppid).toList())
|
||||
.stream().collect(Collectors.toMap(SteamGame::getAppid, g -> g));
|
||||
long total = byStatus.values().stream().mapToLong(Long::longValue).sum();
|
||||
List<AchievementProgress> started = startedGames(userGameRepository.findAchievementProgress(userId));
|
||||
Set<StatGroup> shared = StatGroup.shared(profileStats);
|
||||
return new PublicLibraryDto(total, userGameRepository.sumPlaytime(userId), byStatus,
|
||||
List<AchievementProgress> started = summary ? startedGames(userGameRepository.findAchievementProgress(userId)) : null;
|
||||
Set<StatGroup> shared = sections.contains(ProfileSection.STATS) ? StatGroup.shared(profileStats) : Set.of();
|
||||
return new PublicLibraryDto(
|
||||
summary ? byStatus.values().stream().mapToLong(Long::longValue).sum() : null,
|
||||
summary ? userGameRepository.sumPlaytime(userId) : null,
|
||||
summary ? byStatus.get(GameStatus.FINISHED) + byStatus.get(GameStatus.COMPLETED) : null,
|
||||
sections.contains(ProfileSection.STATUS) ? byStatus : null,
|
||||
toPublicGames(playing, meta), toPublicGames(finished, meta),
|
||||
perfectGames(started), averageCompletion(started),
|
||||
summary ? perfectGames(started) : null,
|
||||
summary ? averageCompletion(started) : null,
|
||||
shared.isEmpty() ? null : userStatsService.getStats(userId, shared));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null for a hidden section
|
||||
*/
|
||||
private static List<PublicLibraryDto.Game> toPublicGames(List<UserGame> games, Map<Long, SteamGame> meta) {
|
||||
if (games == null) {
|
||||
return null;
|
||||
}
|
||||
return games.stream()
|
||||
.map(ug -> new PublicLibraryDto.Game(ug.getAppid(),
|
||||
meta.containsKey(ug.getAppid()) ? meta.get(ug.getAppid()).getName() : null,
|
||||
|
||||
@@ -6,5 +6,6 @@ import java.util.Set;
|
||||
* A valid public-profile ticket from Auth.
|
||||
*
|
||||
* @param profileStats the names of the stat groups the user shows on the profile; empty for a ticket without them
|
||||
* @param sections the names of the profile sections the user shows; null for a ticket without them, which shows all
|
||||
*/
|
||||
public record ProfileTicket(long userId, Set<String> profileStats) {}
|
||||
public record ProfileTicket(long userId, Set<String> profileStats, Set<String> sections) {}
|
||||
@@ -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: "-"
|
||||
@@ -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<String, Class<?>> 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<Path> mappings;
|
||||
try (Stream<Path> 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<Path> files;
|
||||
try (Stream<Path> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ class JwtServiceTest extends AbstractUnitTest {
|
||||
|
||||
@Test
|
||||
void verifyProfileTicket_ReturnsTheUser() {
|
||||
assertEquals(Optional.of(new ProfileTicket(42L, Set.of())),
|
||||
assertEquals(Optional.of(new ProfileTicket(42L, Set.of(), null)),
|
||||
jwtService.verifyProfileTicket(signTicket("public-profile", "42", 60_000)));
|
||||
}
|
||||
|
||||
@@ -116,7 +116,16 @@ class JwtServiceTest extends AbstractUnitTest {
|
||||
.claim("profileStats", List.of("PILE", "TREND"))
|
||||
.expiration(new Date(System.currentTimeMillis() + 60_000)).signWith(signingKey, Jwts.SIG.EdDSA).compact();
|
||||
|
||||
assertEquals(Optional.of(new ProfileTicket(42L, Set.of("PILE", "TREND"))), jwtService.verifyProfileTicket(ticket));
|
||||
assertEquals(Optional.of(new ProfileTicket(42L, Set.of("PILE", "TREND"), null)), jwtService.verifyProfileTicket(ticket));
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyProfileTicket_ReadsTheShownSections() {
|
||||
String ticket = Jwts.builder().subject("public-profile").claim("typ", "public-profile").claim("profileUserId", "42")
|
||||
.claim("sections", List.of("PLAYING", "STATS"))
|
||||
.expiration(new Date(System.currentTimeMillis() + 60_000)).signWith(signingKey, Jwts.SIG.EdDSA).compact();
|
||||
|
||||
assertEquals(Optional.of(new ProfileTicket(42L, Set.of(), Set.of("PLAYING", "STATS"))), jwtService.verifyProfileTicket(ticket));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -59,6 +59,12 @@ class PublicProfileControllerTest extends AbstractIntegrationTest {
|
||||
.expiration(new Date(System.currentTimeMillis() + 60_000)).signWith(TEST_KEYS.getPrivate(), Jwts.SIG.EdDSA).compact();
|
||||
}
|
||||
|
||||
private static String ticket(long userId, List<String> profileStats, List<String> sections) {
|
||||
return Jwts.builder().subject("public-profile").claim("typ", "public-profile").claim("profileUserId", String.valueOf(userId))
|
||||
.claim("profileStats", profileStats).claim("sections", sections)
|
||||
.expiration(new Date(System.currentTimeMillis() + 60_000)).signWith(TEST_KEYS.getPrivate(), Jwts.SIG.EdDSA).compact();
|
||||
}
|
||||
|
||||
private UserGame game(long appid, int playtime, GameStatus status, Integer rating) {
|
||||
UserGame game = new UserGame(USER_ID, appid, playtime, Instant.now(), Instant.now());
|
||||
game.setStatus(status);
|
||||
@@ -94,6 +100,7 @@ class PublicProfileControllerTest extends AbstractIntegrationTest {
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.totalGames").value(3))
|
||||
.andExpect(jsonPath("$.totalPlaytimeMinutes").value(1260))
|
||||
.andExpect(jsonPath("$.finishedGames").value(2))
|
||||
.andExpect(jsonPath("$.byStatus.FINISHED").value(1))
|
||||
.andExpect(jsonPath("$.byStatus.COMPLETED").value(1))
|
||||
.andExpect(jsonPath("$.playing", hasSize(1)))
|
||||
@@ -142,6 +149,41 @@ class PublicProfileControllerTest extends AbstractIntegrationTest {
|
||||
.andExpect(jsonPath("$.stats.trend").value(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicLibrary_HiddenSections_SendNoData() throws Exception {
|
||||
mockMvc.perform(get("/api/public/profiles/library")
|
||||
.header("X-Profile-Ticket", ticket(USER_ID, List.of("TOP_GAMES"), List.of("PLAYING"))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.playing", hasSize(1)))
|
||||
.andExpect(jsonPath("$.totalGames").value(nullValue()))
|
||||
.andExpect(jsonPath("$.totalPlaytimeMinutes").value(nullValue()))
|
||||
.andExpect(jsonPath("$.finishedGames").value(nullValue()))
|
||||
.andExpect(jsonPath("$.perfectGames").value(nullValue()))
|
||||
.andExpect(jsonPath("$.byStatus").value(nullValue()))
|
||||
.andExpect(jsonPath("$.finished").value(nullValue()))
|
||||
.andExpect(jsonPath("$.stats").value(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicLibrary_SummaryWithoutStatus_KeepsTheFinishedCountButNoStatusCounts() throws Exception {
|
||||
mockMvc.perform(get("/api/public/profiles/library")
|
||||
.header("X-Profile-Ticket", ticket(USER_ID, List.of(), List.of("SUMMARY"))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.totalGames").value(3))
|
||||
.andExpect(jsonPath("$.finishedGames").value(2))
|
||||
.andExpect(jsonPath("$.byStatus").value(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicLibrary_OnlyStatusShown_HasTheCountsButNoTotals() throws Exception {
|
||||
mockMvc.perform(get("/api/public/profiles/library")
|
||||
.header("X-Profile-Ticket", ticket(USER_ID, List.of(), List.of("STATUS", "MONEY"))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.byStatus.FINISHED").value(1))
|
||||
.andExpect(jsonPath("$.totalGames").value(nullValue()))
|
||||
.andExpect(jsonPath("$.playing").value(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicLibrary_WithoutOrWithABadTicket_Is401() throws Exception {
|
||||
mockMvc.perform(get("/api/public/profiles/library")).andExpect(status().isUnauthorized());
|
||||
|
||||
Reference in new issue
Block a user