Skip to content

Commit d469246

Browse files
feat: Implement Live Status API with Table Storage integration
- Added ILiveStatusApi interface and its implementation in LiveStatusApi class. - Created DTOs for game server live status and player information. - Developed TableStorageLiveStatusStore for managing live status data in Azure Table Storage. - Introduced NullLiveStatusStore for fallback scenarios. - Implemented LiveStatusController to handle API requests for live status. - Updated ServiceCollectionExtensions to register new services. - Added unit tests for Live Status API functionality. - Enhanced Terraform scripts to provision necessary Azure resources for live status.
1 parent b2ca833 commit d469246

26 files changed

Lines changed: 710 additions & 9 deletions
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using MX.Api.Abstractions;
2+
3+
using XtremeIdiots.Portal.Repository.Abstractions.Models.V1.LiveStatus;
4+
using XtremeIdiots.Portal.Repository.Abstractions.Models.V1.Players;
5+
6+
namespace XtremeIdiots.Portal.Repository.Abstractions.Interfaces.V1;
7+
8+
public interface ILiveStatusApi
9+
{
10+
Task<ApiResult> SetGameServerLiveStatus(Guid gameServerId, SetGameServerLiveStatusDto dto, CancellationToken cancellationToken = default);
11+
Task<ApiResult<GameServerLiveStatusDto>> GetGameServerLiveStatus(Guid gameServerId, CancellationToken cancellationToken = default);
12+
Task<ApiResult<CollectionModel<GameServerLiveStatusDto>>> GetAllGameServerLiveStatuses(CancellationToken cancellationToken = default);
13+
Task<ApiResult<CollectionModel<LivePlayerDto>>> GetGameServerLivePlayers(Guid gameServerId, CancellationToken cancellationToken = default);
14+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.Text.Json.Serialization;
2+
3+
using Newtonsoft.Json;
4+
5+
namespace XtremeIdiots.Portal.Repository.Abstractions.Models.V1.LiveStatus
6+
{
7+
public record GameServerLiveStatusDto : IDto
8+
{
9+
[JsonProperty]
10+
public Guid ServerId { get; internal set; }
11+
12+
[JsonProperty]
13+
public string? Title { get; internal set; }
14+
15+
[JsonProperty]
16+
public string? Map { get; internal set; }
17+
18+
[JsonProperty]
19+
public string? Mod { get; internal set; }
20+
21+
[JsonProperty]
22+
public int MaxPlayers { get; internal set; }
23+
24+
[JsonProperty]
25+
public int CurrentPlayers { get; internal set; }
26+
27+
[JsonProperty]
28+
public DateTime? LastUpdated { get; internal set; }
29+
30+
[JsonProperty]
31+
public bool IsOnline { get; internal set; }
32+
33+
[Newtonsoft.Json.JsonIgnore]
34+
public Dictionary<string, string> TelemetryProperties => new()
35+
{
36+
{ nameof(ServerId), ServerId.ToString() },
37+
{ nameof(Title), Title ?? string.Empty }
38+
};
39+
}
40+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Newtonsoft.Json;
2+
3+
using XtremeIdiots.Portal.Repository.Abstractions.Models.V1.Players;
4+
5+
namespace XtremeIdiots.Portal.Repository.Abstractions.Models.V1.LiveStatus
6+
{
7+
public record SetGameServerLiveStatusDto : IDto
8+
{
9+
[JsonProperty]
10+
public string? Title { get; set; }
11+
12+
[JsonProperty]
13+
public string? Map { get; set; }
14+
15+
[JsonProperty]
16+
public string? Mod { get; set; }
17+
18+
[JsonProperty]
19+
public int MaxPlayers { get; set; }
20+
21+
[JsonProperty]
22+
public int CurrentPlayers { get; set; }
23+
24+
[JsonProperty]
25+
public List<CreateLivePlayerDto> Players { get; set; } = [];
26+
27+
[Newtonsoft.Json.JsonIgnore]
28+
public Dictionary<string, string> TelemetryProperties => new()
29+
{
30+
{ nameof(Title), Title ?? string.Empty },
31+
{ nameof(CurrentPlayers), CurrentPlayers.ToString() }
32+
};
33+
}
34+
}

src/XtremeIdiots.Portal.Repository.Api.Client.Testing/FakeRepositoryApiClient.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ public class FakeVersionedGameServerConfigurationsApi : IVersionedGameServerConf
166166
public IGameServerConfigurationsApi V1 { get; }
167167
}
168168

169+
public class FakeVersionedLiveStatusApi : IVersionedLiveStatusApi
170+
{
171+
public FakeVersionedLiveStatusApi(FakeLiveStatusApi v1) => V1 = v1;
172+
public ILiveStatusApi V1 { get; }
173+
}
174+
169175
/// <summary>
170176
/// In-memory fake of <see cref="IRepositoryApiClient"/> for unit and integration tests.
171177
/// Eliminates the need for nested mock hierarchies.
@@ -200,6 +206,7 @@ public class FakeRepositoryApiClient : IRepositoryApiClient
200206
public FakeDashboardApi DashboardApi { get; } = new();
201207
public FakeGlobalConfigurationsApi GlobalConfigurationsApi { get; } = new();
202208
public FakeGameServerConfigurationsApi GameServerConfigurationsApi { get; } = new();
209+
public FakeLiveStatusApi LiveStatusApi { get; } = new();
203210

204211
private readonly Lazy<FakeVersionedAdminActionsApi> _adminActions;
205212
private readonly Lazy<FakeVersionedBanFileMonitorsApi> _banFileMonitors;
@@ -228,6 +235,7 @@ public class FakeRepositoryApiClient : IRepositoryApiClient
228235
private readonly Lazy<FakeVersionedDashboardApi> _dashboard;
229236
private readonly Lazy<FakeVersionedGlobalConfigurationsApi> _globalConfigurations;
230237
private readonly Lazy<FakeVersionedGameServerConfigurationsApi> _gameServerConfigurations;
238+
private readonly Lazy<FakeVersionedLiveStatusApi> _liveStatus;
231239

232240
public FakeRepositoryApiClient()
233241
{
@@ -258,6 +266,7 @@ public FakeRepositoryApiClient()
258266
_dashboard = new Lazy<FakeVersionedDashboardApi>(() => new FakeVersionedDashboardApi(DashboardApi));
259267
_globalConfigurations = new Lazy<FakeVersionedGlobalConfigurationsApi>(() => new FakeVersionedGlobalConfigurationsApi(GlobalConfigurationsApi));
260268
_gameServerConfigurations = new Lazy<FakeVersionedGameServerConfigurationsApi>(() => new FakeVersionedGameServerConfigurationsApi(GameServerConfigurationsApi));
269+
_liveStatus = new Lazy<FakeVersionedLiveStatusApi>(() => new FakeVersionedLiveStatusApi(LiveStatusApi));
261270
}
262271

263272
public IVersionedAdminActionsApi AdminActions => _adminActions.Value;
@@ -287,6 +296,7 @@ public FakeRepositoryApiClient()
287296
public IVersionedDashboardApi Dashboard => _dashboard.Value;
288297
public IVersionedGlobalConfigurationsApi GlobalConfigurations => _globalConfigurations.Value;
289298
public IVersionedGameServerConfigurationsApi GameServerConfigurations => _gameServerConfigurations.Value;
299+
public IVersionedLiveStatusApi LiveStatus => _liveStatus.Value;
290300

291301
/// <summary>
292302
/// Resets all fakes to their initial state, clearing configured responses,
@@ -320,6 +330,7 @@ public FakeRepositoryApiClient Reset()
320330
DashboardApi.Reset();
321331
GlobalConfigurationsApi.Reset();
322332
GameServerConfigurationsApi.Reset();
333+
LiveStatusApi.Reset();
323334
return this;
324335
}
325336
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System.Collections.Concurrent;
2+
using System.Net;
3+
using MX.Api.Abstractions;
4+
using XtremeIdiots.Portal.Repository.Abstractions.Interfaces.V1;
5+
using XtremeIdiots.Portal.Repository.Abstractions.Models.V1.LiveStatus;
6+
using XtremeIdiots.Portal.Repository.Abstractions.Models.V1.Players;
7+
8+
namespace XtremeIdiots.Portal.Repository.Api.Client.Testing.Fakes;
9+
10+
public class FakeLiveStatusApi : ILiveStatusApi
11+
{
12+
private readonly ConcurrentDictionary<Guid, GameServerLiveStatusDto> _statuses = new();
13+
private readonly ConcurrentDictionary<Guid, List<LivePlayerDto>> _players = new();
14+
15+
public FakeLiveStatusApi AddStatus(GameServerLiveStatusDto status) { _statuses[status.ServerId] = status; return this; }
16+
public FakeLiveStatusApi AddPlayers(Guid serverId, List<LivePlayerDto> players) { _players[serverId] = players; return this; }
17+
public FakeLiveStatusApi Reset() { _statuses.Clear(); _players.Clear(); return this; }
18+
19+
public Task<ApiResult> SetGameServerLiveStatus(Guid gameServerId, SetGameServerLiveStatusDto dto, CancellationToken cancellationToken = default)
20+
{
21+
return Task.FromResult(new ApiResult(HttpStatusCode.OK, new ApiResponse()));
22+
}
23+
24+
public Task<ApiResult<GameServerLiveStatusDto>> GetGameServerLiveStatus(Guid gameServerId, CancellationToken cancellationToken = default)
25+
{
26+
if (_statuses.TryGetValue(gameServerId, out var status))
27+
{
28+
return Task.FromResult(new ApiResult<GameServerLiveStatusDto>(HttpStatusCode.OK, new ApiResponse<GameServerLiveStatusDto>(status)));
29+
}
30+
31+
return Task.FromResult(new ApiResult<GameServerLiveStatusDto>(HttpStatusCode.NotFound, new ApiResponse<GameServerLiveStatusDto>(new ApiError("NotFound", "Not found"))));
32+
}
33+
34+
public Task<ApiResult<CollectionModel<GameServerLiveStatusDto>>> GetAllGameServerLiveStatuses(CancellationToken cancellationToken = default)
35+
{
36+
var collection = new CollectionModel<GameServerLiveStatusDto> { Items = _statuses.Values.ToList() };
37+
return Task.FromResult(new ApiResult<CollectionModel<GameServerLiveStatusDto>>(HttpStatusCode.OK, new ApiResponse<CollectionModel<GameServerLiveStatusDto>>(collection)));
38+
}
39+
40+
public Task<ApiResult<CollectionModel<LivePlayerDto>>> GetGameServerLivePlayers(Guid gameServerId, CancellationToken cancellationToken = default)
41+
{
42+
var players = _players.TryGetValue(gameServerId, out var list) ? list : [];
43+
var collection = new CollectionModel<LivePlayerDto> { Items = players };
44+
return Task.FromResult(new ApiResult<CollectionModel<LivePlayerDto>>(HttpStatusCode.OK, new ApiResponse<CollectionModel<LivePlayerDto>>(collection)));
45+
}
46+
}

src/XtremeIdiots.Portal.Repository.Api.Client.Testing/ServiceCollectionExtensions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public static IServiceCollection AddFakeRepositoryApiClient(
5151
services.RemoveAll<IVersionedApiInfoApi>();
5252
services.RemoveAll<IApiHealthApi>();
5353
services.RemoveAll<IApiInfoApi>();
54+
services.RemoveAll<IVersionedLiveStatusApi>();
5455

5556
// Remove all actual API implementations
5657
services.RemoveAll<IAdminActionsApi>();
@@ -64,6 +65,7 @@ public static IServiceCollection AddFakeRepositoryApiClient(
6465
services.RemoveAll<IGameServersStatsApi>();
6566
services.RemoveAll<IGameTrackerBannerApi>();
6667
services.RemoveAll<ILivePlayersApi>();
68+
services.RemoveAll<ILiveStatusApi>();
6769
services.RemoveAll<IMapPacksApi>();
6870
services.RemoveAll<IMapRotationsApi>();
6971
services.RemoveAll<IDashboardApi>();
@@ -100,6 +102,7 @@ public static IServiceCollection AddFakeRepositoryApiClient(
100102
services.AddSingleton<IVersionedUserProfileApi>(fakeClient.UserProfiles);
101103
services.AddSingleton<IVersionedApiHealthApi>(fakeClient.ApiHealth);
102104
services.AddSingleton<IVersionedApiInfoApi>(fakeClient.ApiInfo);
105+
services.AddSingleton<IVersionedLiveStatusApi>(fakeClient.LiveStatus);
103106
services.AddSingleton<IApiHealthApi>(fakeClient.HealthApi);
104107
services.AddSingleton<IApiInfoApi>(fakeClient.InfoApi);
105108

@@ -114,6 +117,7 @@ public static IServiceCollection AddFakeRepositoryApiClient(
114117
services.AddSingleton<IGameServersStatsApi>(fakeClient.GameServersStatsApi);
115118
services.AddSingleton<IGameTrackerBannerApi>(fakeClient.GameTrackerBannerApi);
116119
services.AddSingleton<ILivePlayersApi>(fakeClient.LivePlayersApi);
120+
services.AddSingleton<ILiveStatusApi>(fakeClient.LiveStatusApi);
117121
services.AddSingleton<IMapPacksApi>(fakeClient.MapPacksApi);
118122
services.AddSingleton<IMapRotationsApi>(fakeClient.MapRotationsApi);
119123
services.AddSingleton<IDashboardApi>(fakeClient.DashboardApi);

src/XtremeIdiots.Portal.Repository.Api.Client.Tests.V1/RepositoryApiClientTests.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public class RepositoryApiClientTests
3232
private readonly Mock<IVersionedDashboardApi> _dashboard = new();
3333
private readonly Mock<IVersionedGlobalConfigurationsApi> _globalConfigurations = new();
3434
private readonly Mock<IVersionedGameServerConfigurationsApi> _gameServerConfigurations = new();
35+
private readonly Mock<IVersionedLiveStatusApi> _liveStatus = new();
3536

3637
private RepositoryApiClient CreateClient() => new(
3738
_adminActions.Object,
@@ -60,7 +61,8 @@ public class RepositoryApiClientTests
6061
_mapRotations.Object,
6162
_dashboard.Object,
6263
_globalConfigurations.Object,
63-
_gameServerConfigurations.Object);
64+
_gameServerConfigurations.Object,
65+
_liveStatus.Object);
6466

6567
[Fact]
6668
public void Constructor_StoresAllProperties()
@@ -94,6 +96,7 @@ public void Constructor_StoresAllProperties()
9496
Assert.Same(_dashboard.Object, client.Dashboard);
9597
Assert.Same(_globalConfigurations.Object, client.GlobalConfigurations);
9698
Assert.Same(_gameServerConfigurations.Object, client.GameServerConfigurations);
99+
Assert.Same(_liveStatus.Object, client.LiveStatus);
97100
}
98101

99102
[Fact]
@@ -127,7 +130,9 @@ public void Constructor_NoPropertyIsNull()
127130
Assert.NotNull(client.MapRotations);
128131
Assert.NotNull(client.GlobalConfigurations);
129132
Assert.NotNull(client.GameServerConfigurations);
133+
Assert.NotNull(client.LiveStatus);
130134
}
135+
131136
[Fact]
132137
public void Properties_ReturnCorrectTypes()
133138
{
@@ -159,6 +164,7 @@ public void Properties_ReturnCorrectTypes()
159164
Assert.IsAssignableFrom<IVersionedMapRotationsApi>(client.MapRotations);
160165
Assert.IsAssignableFrom<IVersionedGlobalConfigurationsApi>(client.GlobalConfigurations);
161166
Assert.IsAssignableFrom<IVersionedGameServerConfigurationsApi>(client.GameServerConfigurations);
167+
Assert.IsAssignableFrom<IVersionedLiveStatusApi>(client.LiveStatus);
162168
}
163169
}
164170
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using Microsoft.Extensions.Logging;
2+
3+
using MX.Api.Abstractions;
4+
using MX.Api.Client;
5+
using MX.Api.Client.Auth;
6+
using MX.Api.Client.Extensions;
7+
8+
using RestSharp;
9+
10+
using XtremeIdiots.Portal.Repository.Abstractions.Interfaces.V1;
11+
using XtremeIdiots.Portal.Repository.Abstractions.Models.V1.LiveStatus;
12+
using XtremeIdiots.Portal.Repository.Abstractions.Models.V1.Players;
13+
14+
namespace XtremeIdiots.Portal.Repository.Api.Client.V1;
15+
16+
public class LiveStatusApi : BaseApi<RepositoryApiClientOptions>, ILiveStatusApi
17+
{
18+
public LiveStatusApi(ILogger<BaseApi<RepositoryApiClientOptions>> logger, IApiTokenProvider apiTokenProvider, IRestClientService restClientService, RepositoryApiClientOptions options)
19+
: base(logger, apiTokenProvider, restClientService, options)
20+
{
21+
}
22+
23+
public async Task<ApiResult> SetGameServerLiveStatus(Guid gameServerId, SetGameServerLiveStatusDto dto, CancellationToken cancellationToken = default)
24+
{
25+
var request = await CreateRequestAsync($"v1/game-servers/{gameServerId}/live-status", Method.Put).ConfigureAwait(false);
26+
request.AddJsonBody(dto);
27+
28+
var response = await ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
29+
30+
return response.ToApiResult();
31+
}
32+
33+
public async Task<ApiResult<GameServerLiveStatusDto>> GetGameServerLiveStatus(Guid gameServerId, CancellationToken cancellationToken = default)
34+
{
35+
var request = await CreateRequestAsync($"v1/game-servers/{gameServerId}/live-status", Method.Get).ConfigureAwait(false);
36+
37+
var response = await ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
38+
39+
return response.ToApiResult<GameServerLiveStatusDto>();
40+
}
41+
42+
public async Task<ApiResult<CollectionModel<GameServerLiveStatusDto>>> GetAllGameServerLiveStatuses(CancellationToken cancellationToken = default)
43+
{
44+
var request = await CreateRequestAsync("v1/game-servers/live-status", Method.Get).ConfigureAwait(false);
45+
46+
var response = await ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
47+
48+
return response.ToApiResult<CollectionModel<GameServerLiveStatusDto>>();
49+
}
50+
51+
public async Task<ApiResult<CollectionModel<LivePlayerDto>>> GetGameServerLivePlayers(Guid gameServerId, CancellationToken cancellationToken = default)
52+
{
53+
var request = await CreateRequestAsync($"v1/game-servers/{gameServerId}/live-players", Method.Get).ConfigureAwait(false);
54+
55+
var response = await ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
56+
57+
return response.ToApiResult<CollectionModel<LivePlayerDto>>();
58+
}
59+
}

src/XtremeIdiots.Portal.Repository.Api.Client.V1/ApiVersionSelectorImplementations.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,4 +272,14 @@ public VersionedGameServerConfigurationsApi(IGameServerConfigurationsApi v1Api)
272272

273273
public IGameServerConfigurationsApi V1 { get; }
274274
}
275+
276+
public class VersionedLiveStatusApi : IVersionedLiveStatusApi
277+
{
278+
public VersionedLiveStatusApi(ILiveStatusApi v1Api)
279+
{
280+
V1 = v1Api;
281+
}
282+
283+
public ILiveStatusApi V1 { get; }
284+
}
275285
}

src/XtremeIdiots.Portal.Repository.Api.Client.V1/ApiVersionSelectors.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,9 @@ public interface IVersionedGameServerConfigurationsApi
137137
{
138138
IGameServerConfigurationsApi V1 { get; }
139139
}
140+
141+
public interface IVersionedLiveStatusApi
142+
{
143+
ILiveStatusApi V1 { get; }
144+
}
140145
}

0 commit comments

Comments
 (0)