Skip to content

Commit e0c1b01

Browse files
Merge remote-tracking branch 'upstream/master'
2 parents 7379290 + 93ac98a commit e0c1b01

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+508
-435
lines changed

.github/workflows/dotnet.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,11 @@ jobs:
1414
- name: Setup .NET
1515
uses: actions/setup-dotnet@v1
1616
with:
17-
dotnet-version: 6.0.x
17+
dotnet-version: 9.0.x
1818
- name: Restore dependencies
1919
run: dotnet restore
20-
- name: Build
21-
run: dotnet build --no-restore
2220
- name: Test
23-
run: dotnet test --no-build --verbosity normal
21+
run: dotnet test --verbosity normal --filter "Category!=SkipCi"
2422
env:
2523
IGDB_CLIENT_ID: ${{ secrets.IGDB_CLIENT_ID }}
2624
IGDB_CLIENT_SECRET: ${{ secrets.IGDB_CLIENT_SECRET }}

IGDB.Tests/AssemblyInfo.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using Xunit;
2+
3+
[assembly: CollectionBehavior(DisableTestParallelization = true)]

IGDB.Tests/Dumps.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,21 @@
77

88
namespace IGDB.Tests
99
{
10+
[Collection("/dumps")]
1011
public class Dumps
1112
{
1213
IGDBClient _api;
1314

1415
public Dumps()
1516
{
16-
_api = new IGDB.IGDBClient(
17+
_api = IGDBClient.CreateWithDefaults(
1718
Environment.GetEnvironmentVariable("IGDB_CLIENT_ID"),
1819
Environment.GetEnvironmentVariable("IGDB_CLIENT_SECRET")
1920
);
2021
}
2122

2223
[Fact]
24+
[Trait("Category", "SkipCi")]
2325
public async Task ShouldReturnDumpsList()
2426
{
2527
var dumps = await _api.GetDataDumpsAsync();
@@ -29,9 +31,10 @@ public async Task ShouldReturnDumpsList()
2931
}
3032

3133
[Fact]
34+
[Trait("Category", "SkipCi")]
3235
public async Task ShouldReturnGamesEndpointDump()
3336
{
34-
var gameDump = await _api.GetDataDumpEndpointAsync("games");
37+
var gameDump = await _api.GetDataDumpEndpointAsync(IGDBClient.Endpoints.Games);
3538

3639
Assert.NotNull(gameDump);
3740
Assert.NotNull(gameDump.S3Url);

IGDB.Tests/GameTimeToBeats.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Xunit;
4+
5+
namespace IGDB.Tests
6+
{
7+
[Collection("/game_time_to_beats")]
8+
public class GameTimeToBeats
9+
{
10+
IGDBClient _api;
11+
12+
public GameTimeToBeats()
13+
{
14+
_api = IGDBClient.CreateWithDefaults(
15+
Environment.GetEnvironmentVariable("IGDB_CLIENT_ID"),
16+
Environment.GetEnvironmentVariable("IGDB_CLIENT_SECRET")
17+
);
18+
}
19+
20+
[Fact]
21+
public async Task Should_Return_GameTimeToBeat_Data()
22+
{
23+
var games = await _api.QueryAsync<IGDB.Models.GameTimeToBeat>(IGDBClient.Endpoints.GameTimeToBeats, query: "fields *; where id = 3575;");
24+
Assert.NotNull(games);
25+
Assert.NotEmpty(games);
26+
Assert.Equal(3575, games[0].Id);
27+
Assert.Equal(201711, games[0].GameId);
28+
Assert.Equal(1, games[0].Count);
29+
Assert.Equal(14400, games[0].Hastily);
30+
Assert.Equal(28800, games[0].Normally);
31+
Assert.Equal(32400, games[0].Completely);
32+
}
33+
}
34+
}

IGDB.Tests/Games.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66

77
namespace IGDB.Tests
88
{
9+
[Collection("/games")]
910
public class Games
1011
{
1112
IGDBClient _api;
1213

1314
public Games()
1415
{
15-
_api = new IGDB.IGDBClient(
16+
_api = IGDBClient.CreateWithDefaults(
1617
Environment.GetEnvironmentVariable("IGDB_CLIENT_ID"),
1718
Environment.GetEnvironmentVariable("IGDB_CLIENT_SECRET")
1819
);
@@ -103,6 +104,38 @@ public async Task ShouldReturnResponseWithSingleGameExpandedCover()
103104
Assert.Equal(756, game.Cover.Value.Width);
104105
}
105106

107+
[Fact]
108+
public async Task ShouldReturnResponseWithExpandedTableEnumFieldsForAug2025Migration()
109+
{
110+
var games = await _api.QueryAsync<Game>(IGDBClient.Endpoints.Games, "fields id,game_type.type,game_status.status; where id = 52625;");
111+
112+
Assert.NotNull(games);
113+
114+
var game = games[0];
115+
116+
Assert.NotNull(game.GameStatus.Value);
117+
Assert.Equal("Cancelled", game.GameStatus.Value.Status);
118+
119+
Assert.NotNull(game.GameType.Value);
120+
Assert.Equal("Main Game", game.GameType.Value.Type);
121+
}
122+
123+
[Fact]
124+
public async Task ShouldReturnResponseWithNonExpandedTableEnumFieldsForAug2025Migration()
125+
{
126+
var games = await _api.QueryAsync<Game>(IGDBClient.Endpoints.Games, "fields id,game_type,game_status; where id = 52625;");
127+
128+
Assert.NotNull(games);
129+
130+
var game = games[0];
131+
132+
Assert.NotNull(game.GameStatus.Id);
133+
Assert.Equal(6, game.GameStatus.Id);
134+
135+
Assert.NotNull(game.GameType.Id);
136+
Assert.Equal(0, game.GameType.Id);
137+
}
138+
106139
[Fact]
107140
public async Task ShouldReturnResponseWithUnixTimestamp()
108141
{

IGDB.Tests/IGDB.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net6.0</TargetFramework>
4+
<TargetFramework>net9.0</TargetFramework>
55

66
<IsPackable>false</IsPackable>
77
</PropertyGroup>

IGDB.Tests/Platforms.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66

77
namespace IGDB.Tests
88
{
9+
[Collection("/platforms")]
910
public class Platforms
1011
{
1112
IGDBClient _api;
1213

1314
public Platforms()
1415
{
15-
_api = new IGDB.IGDBClient(
16+
_api = IGDBClient.CreateWithDefaults(
1617
Environment.GetEnvironmentVariable("IGDB_CLIENT_ID"),
1718
Environment.GetEnvironmentVariable("IGDB_CLIENT_SECRET")
1819
);

IGDB.Tests/PopScore.cs

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,54 @@
66

77
namespace IGDB.Tests
88
{
9+
[Collection("/popularity_types")]
910
public class PopScore
10-
{
11-
IGDBClient _api;
11+
{
12+
IGDBClient _api;
1213

13-
public PopScore()
14-
{
15-
_api = new IGDB.IGDBClient(
16-
Environment.GetEnvironmentVariable("IGDB_CLIENT_ID"),
17-
Environment.GetEnvironmentVariable("IGDB_CLIENT_SECRET")
18-
);
19-
}
14+
public PopScore()
15+
{
16+
_api = IGDBClient.CreateWithDefaults(
17+
Environment.GetEnvironmentVariable("IGDB_CLIENT_ID"),
18+
Environment.GetEnvironmentVariable("IGDB_CLIENT_SECRET")
19+
);
20+
}
2021

21-
[Fact]
22-
public async Task ShouldReturnAllPopularityTypes()
23-
{
24-
var popularityTypes = await _api.QueryAsync<PopularityType>(IGDBClient.Endpoints.PopularityTypes, "fields *;");
22+
[Fact]
23+
public async Task ShouldReturnAllPopularityTypes()
24+
{
25+
var popularityTypes = await _api.QueryAsync<PopularityType>(IGDBClient.Endpoints.PopularityTypes, "fields *;");
2526

26-
Assert.NotNull(popularityTypes);
27-
foreach (var popularityType in popularityTypes)
28-
{
29-
Assert.NotNull(popularityType.Checksum);
30-
Assert.NotNull(popularityType.CreatedAt);
31-
Assert.NotNull(popularityType.Name);
32-
Assert.NotNull(popularityType.PopularitySource);
33-
Assert.NotNull(popularityType.UpdatedAt);
34-
}
35-
}
27+
Assert.NotNull(popularityTypes);
28+
foreach (var popularityType in popularityTypes)
29+
{
30+
Assert.NotNull(popularityType.Checksum);
31+
Assert.NotNull(popularityType.CreatedAt);
32+
Assert.NotNull(popularityType.Name);
33+
Assert.NotNull(popularityType.ExternalPopularitySource.Id);
34+
Assert.NotNull(popularityType.UpdatedAt);
35+
}
36+
}
3637

37-
[Fact]
38-
public async Task ShouldReturnLimitedPopularityPrimitives()
39-
{
40-
var popularityPrimitives = await _api.QueryAsync<PopularityPrimitive>(
41-
IGDBClient.Endpoints.PopularityPrimitives,
42-
"fields *; limit 10;");
38+
[Fact]
39+
public async Task ShouldReturnLimitedPopularityPrimitives()
40+
{
41+
var popularityPrimitives = await _api.QueryAsync<PopularityPrimitive>(
42+
IGDBClient.Endpoints.PopularityPrimitives,
43+
"fields *; limit 10;");
4344

44-
Assert.NotNull(popularityPrimitives);
45-
Assert.True(popularityPrimitives.Length == 10);
45+
Assert.NotNull(popularityPrimitives);
46+
Assert.True(popularityPrimitives.Length == 10);
4647

47-
foreach (var popularityPrimitive in popularityPrimitives)
48-
{
49-
Assert.NotNull(popularityPrimitive.CalculatedAt);
50-
//Assert.NotNull(popularityPrimitive.Checksum);
51-
Assert.NotNull(popularityPrimitive.CreatedAt);
52-
Assert.NotNull(popularityPrimitive.PopularitySource);
53-
Assert.NotNull(popularityPrimitive.PopularityType);
54-
Assert.NotNull(popularityPrimitive.UpdatedAt);
55-
Assert.NotNull(popularityPrimitive.Value);
56-
}
57-
}
58-
}
48+
foreach (var popularityPrimitive in popularityPrimitives)
49+
{
50+
Assert.NotNull(popularityPrimitive.CalculatedAt);
51+
Assert.NotNull(popularityPrimitive.CreatedAt);
52+
Assert.NotNull(popularityPrimitive.ExternalPopularitySource.Id);
53+
Assert.NotNull(popularityPrimitive.PopularityType);
54+
Assert.NotNull(popularityPrimitive.UpdatedAt);
55+
Assert.NotNull(popularityPrimitive.Value);
56+
}
57+
}
58+
}
5959
}

IGDB.Tests/TokenHandling.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ public async Task ShouldHandleInvalidTokenAndRetryRequest()
1818
var invalidTokenClient = new IGDB.IGDBClient(
1919
Environment.GetEnvironmentVariable("IGDB_CLIENT_ID"),
2020
Environment.GetEnvironmentVariable("IGDB_CLIENT_SECRET"),
21-
tokenStore
21+
tokenStore,
22+
ApiPolicy.DefaultApiPolicy
2223
);
2324

2425
var games = await invalidTokenClient.QueryAsync<Game>("games");
@@ -34,7 +35,8 @@ public async Task ShouldHandleExpiredTokenAndAcquireNewOne()
3435
var client = new IGDB.IGDBClient(
3536
Environment.GetEnvironmentVariable("IGDB_CLIENT_ID"),
3637
Environment.GetEnvironmentVariable("IGDB_CLIENT_SECRET"),
37-
tokenStore
38+
tokenStore,
39+
ApiPolicy.DefaultApiPolicy
3840
);
3941

4042
await client.QueryAsync<Game>("games");
@@ -51,7 +53,8 @@ public async Task ShouldUseExistingTokenWhenNotExpired()
5153
var client = new IGDB.IGDBClient(
5254
Environment.GetEnvironmentVariable("IGDB_CLIENT_ID"),
5355
Environment.GetEnvironmentVariable("IGDB_CLIENT_SECRET"),
54-
tokenStore
56+
tokenStore,
57+
ApiPolicy.DefaultApiPolicy
5558
);
5659

5760
await client.QueryAsync<Game>("games");

IGDB/ApiPolicy.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
using System.Net.Http;
3+
using System.Threading.Tasks;
4+
using Polly;
5+
using Polly.Bulkhead;
6+
using Polly.RateLimit;
7+
8+
namespace IGDB
9+
{
10+
public static class ApiPolicy
11+
{
12+
private static readonly Random _jitter = new Random();
13+
private const int MaxRetries = 3;
14+
private const double RetryDelayBaseSeconds = 0.5;
15+
private const int MaxParallelization = 6;
16+
private const int MaxQueuingActions = 32;
17+
private const int MaxRateLimit = 4;
18+
private const int JitterMs = 500;
19+
private static readonly TimeSpan RateLimitPeriod = TimeSpan.FromMilliseconds(900);
20+
21+
/// <summary>
22+
/// Default API policy for handling HTTP requests to IGDB.
23+
///
24+
/// This policy includes:
25+
/// - Retry logic for rate limit and bulkhead exceptions with exponential backoff and jitter.
26+
/// - Bulkhead isolation to limit the number of concurrent requests.
27+
/// - Rate limiting to control the request rate.
28+
///
29+
/// The retry logic will attempt to retry up to 3 times with an exponential backoff strategy,
30+
/// adding a random jitter of up to ±500ms to avoid thundering herd problems.
31+
/// </summary>
32+
public static readonly IAsyncPolicy<HttpResponseMessage> DefaultApiPolicy
33+
= Policy.WrapAsync(
34+
Policy<HttpResponseMessage>.Handle<RateLimitRejectedException>()
35+
.Or<BulkheadRejectedException>()
36+
.WaitAndRetryAsync(
37+
retryCount: MaxRetries,
38+
sleepDurationProvider: (retryAttempt) =>
39+
{
40+
var backOff = TimeSpan.FromSeconds(RetryDelayBaseSeconds * Math.Pow(2, retryAttempt - 1));
41+
var jitter = TimeSpan.FromMilliseconds(_jitter.Next(-JitterMs, JitterMs));
42+
return backOff + jitter;
43+
},
44+
onRetry: (_, __, ___) => { }
45+
),
46+
Policy.BulkheadAsync<HttpResponseMessage>(maxParallelization: MaxParallelization, maxQueuingActions: MaxQueuingActions),
47+
Policy.RateLimitAsync<HttpResponseMessage>(MaxRateLimit, RateLimitPeriod)
48+
);
49+
}
50+
}

0 commit comments

Comments
 (0)