Skip to content

Commit 6e49e1c

Browse files
feat: Implement Connected Players API with AJAX support and update related tests
1 parent 8512f27 commit 6e49e1c

6 files changed

Lines changed: 436 additions & 230 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
using System.Net;
2+
using System.Security.Claims;
3+
using System.Text;
4+
5+
using Microsoft.ApplicationInsights;
6+
using Microsoft.ApplicationInsights.Extensibility;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Mvc;
9+
using Microsoft.Extensions.Caching.Memory;
10+
using Microsoft.Extensions.Configuration;
11+
using Microsoft.Extensions.Logging;
12+
13+
using Moq;
14+
using MX.Api.Abstractions;
15+
using MX.Observability.ApplicationInsights.Auditing;
16+
using Newtonsoft.Json;
17+
using Newtonsoft.Json.Linq;
18+
19+
using XtremeIdiots.Portal.Repository.Abstractions.Models.V1.ConnectedPlayers;
20+
using XtremeIdiots.Portal.Repository.Api.Client.V1;
21+
using ConnectedPlayersApiController = XtremeIdiots.Portal.Web.ApiControllers.ConnectedPlayersController;
22+
23+
namespace XtremeIdiots.Portal.Web.Tests.ApiControllers;
24+
25+
public class ConnectedPlayersControllerTests
26+
{
27+
private readonly Mock<IRepositoryApiClient> mockRepositoryApiClient = new();
28+
private readonly IMemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions());
29+
private readonly TelemetryClient telemetryClient = new(new TelemetryConfiguration());
30+
private readonly Mock<ILogger<ConnectedPlayersApiController>> mockLogger = new();
31+
private readonly Mock<IConfiguration> mockConfiguration = new();
32+
private readonly IAuditLogger auditLogger = new Mock<IAuditLogger>().Object;
33+
34+
private ConnectedPlayersApiController CreateSut(ClaimsPrincipal? user = null)
35+
{
36+
var controller = new ConnectedPlayersApiController(
37+
mockRepositoryApiClient.Object,
38+
memoryCache,
39+
telemetryClient,
40+
mockLogger.Object,
41+
mockConfiguration.Object,
42+
auditLogger);
43+
44+
var httpContext = new DefaultHttpContext
45+
{
46+
User = user ?? new ClaimsPrincipal(new ClaimsIdentity("TestAuth"))
47+
};
48+
49+
controller.ControllerContext = new ControllerContext { HttpContext = httpContext };
50+
51+
return controller;
52+
}
53+
54+
[Fact]
55+
public async Task GetConnectedPlayersAjax_WithEmptyBody_ReturnsBadRequest()
56+
{
57+
// Arrange
58+
var sut = CreateSut();
59+
sut.HttpContext.Request.Body = new MemoryStream();
60+
61+
// Act
62+
var result = await sut.GetConnectedPlayersAjax();
63+
64+
// Assert
65+
Assert.IsType<BadRequestObjectResult>(result);
66+
}
67+
68+
[Fact]
69+
public async Task GetConnectedPlayersAjax_WithValidBody_ReturnsDataTablePayload()
70+
{
71+
// Arrange
72+
var item = CreateConnectedPlayerDto();
73+
var apiResponse = new ApiResponse<CollectionModel<ConnectedPlayerDto>>(new CollectionModel<ConnectedPlayerDto>([item]))
74+
{
75+
Pagination = new ApiPagination(totalCount: 1, filteredCount: 1, skip: 0, top: 500)
76+
};
77+
78+
mockRepositoryApiClient
79+
.Setup(x => x.ConnectedPlayers.V1.GetConnectedPlayers(null, null, null, null, 0, 500, It.IsAny<CancellationToken>()))
80+
.ReturnsAsync(new ApiResult<CollectionModel<ConnectedPlayerDto>>(HttpStatusCode.OK, apiResponse));
81+
82+
var request = new
83+
{
84+
draw = 3,
85+
start = 0,
86+
length = 25,
87+
columns = new[]
88+
{
89+
new { data = "gameType", name = "gameType", searchable = true, orderable = true, search = new { value = "", regex = false } },
90+
new { data = "username", name = "username", searchable = true, orderable = true, search = new { value = "", regex = false } },
91+
new { data = "linkedAtUtc", name = "linkedAtUtc", searchable = false, orderable = true, search = new { value = "", regex = false } }
92+
},
93+
order = new[] { new { column = 2, dir = "desc" } },
94+
search = new { value = "", regex = false }
95+
};
96+
97+
var body = JsonConvert.SerializeObject(request);
98+
var sut = CreateSut();
99+
sut.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
100+
101+
// Act
102+
var result = await sut.GetConnectedPlayersAjax();
103+
104+
// Assert
105+
var ok = Assert.IsType<OkObjectResult>(result);
106+
var payload = JObject.Parse(JsonConvert.SerializeObject(ok.Value));
107+
108+
Assert.Equal(3, (payload["draw"] ?? payload["Draw"])?.Value<int>());
109+
Assert.Equal(1, payload["recordsTotal"]?.Value<int>());
110+
Assert.Equal(1, payload["recordsFiltered"]?.Value<int>());
111+
Assert.Equal(1, payload["data"]?.Count());
112+
}
113+
114+
[Fact]
115+
public async Task GetConnectedPlayersAjax_WithInvalidOrderColumnIndex_ReturnsBadRequest()
116+
{
117+
// Arrange
118+
var request = new
119+
{
120+
draw = 1,
121+
start = 0,
122+
length = 25,
123+
columns = new[]
124+
{
125+
new { data = "gameType", name = "gameType", searchable = true, orderable = true, search = new { value = "", regex = false } }
126+
},
127+
order = new[] { new { column = 5, dir = "desc" } },
128+
search = new { value = "", regex = false }
129+
};
130+
131+
var body = JsonConvert.SerializeObject(request);
132+
var sut = CreateSut();
133+
sut.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
134+
135+
// Act
136+
var result = await sut.GetConnectedPlayersAjax();
137+
138+
// Assert
139+
Assert.IsType<BadRequestObjectResult>(result);
140+
mockRepositoryApiClient.Verify(x => x.ConnectedPlayers.V1.GetConnectedPlayers(
141+
It.IsAny<Guid?>(),
142+
It.IsAny<Guid?>(),
143+
It.IsAny<XtremeIdiots.Portal.Repository.Abstractions.Constants.V1.GameType?>(),
144+
It.IsAny<bool?>(),
145+
It.IsAny<int>(),
146+
It.IsAny<int>(),
147+
It.IsAny<CancellationToken>()), Times.Never);
148+
}
149+
150+
private static ConnectedPlayerDto CreateConnectedPlayerDto()
151+
{
152+
var now = DateTime.UtcNow;
153+
154+
var json = JsonConvert.SerializeObject(new
155+
{
156+
ConnectedPlayerProfileId = Guid.NewGuid(),
157+
PlayerId = Guid.NewGuid(),
158+
UserProfileId = Guid.NewGuid(),
159+
GameType = "CallOfDuty4",
160+
Username = "PlayerOne",
161+
LinkMethod = "ActivationCode",
162+
LinkedAtUtc = now,
163+
LinkedByUserProfileId = Guid.NewGuid(),
164+
UnlinkedAtUtc = (DateTime?)null,
165+
UnlinkedByUserProfileId = (Guid?)null,
166+
IsActive = true
167+
});
168+
169+
return JsonConvert.DeserializeObject<ConnectedPlayerDto>(json)!;
170+
}
171+
}

src/XtremeIdiots.Portal.Web.Tests/Controllers/ConnectedPlayersControllerTests.cs

Lines changed: 6 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
using Moq;
1313
using MX.Api.Abstractions;
1414
using MX.Observability.ApplicationInsights.Auditing;
15-
using Newtonsoft.Json;
1615

1716
using XtremeIdiots.Portal.Repository.Abstractions.Constants.V1;
1817
using XtremeIdiots.Portal.Repository.Abstractions.Models.V1.ConnectedPlayers;
@@ -54,18 +53,6 @@ private ConnectedPlayersController CreateSut(ClaimsPrincipal? user = null)
5453
public async Task Index_WhenApiSucceeds_ReturnsViewModel()
5554
{
5655
// Arrange
57-
var collection = new CollectionModel<ConnectedPlayerDto>([
58-
CreateConnectedPlayerDto(true)
59-
]);
60-
var apiResponse = new ApiResponse<CollectionModel<ConnectedPlayerDto>>(collection)
61-
{
62-
Pagination = new ApiPagination(totalCount: 1, filteredCount: 1, skip: 0, top: 500)
63-
};
64-
65-
mockRepositoryApiClient
66-
.Setup(x => x.ConnectedPlayers.V1.GetConnectedPlayers(null, null, null, null, 0, 500, It.IsAny<CancellationToken>()))
67-
.ReturnsAsync(new ApiResult<CollectionModel<ConnectedPlayerDto>>(HttpStatusCode.OK, apiResponse));
68-
6956
var sut = CreateSut(new ClaimsPrincipal(new ClaimsIdentity([
7057
new Claim(UserProfileClaimType.SeniorAdmin, "true")
7158
], "TestAuth")));
@@ -77,109 +64,29 @@ public async Task Index_WhenApiSucceeds_ReturnsViewModel()
7764
var viewResult = Assert.IsType<ViewResult>(result);
7865
var model = Assert.IsType<ConnectedPlayersAdminViewModel>(viewResult.Model);
7966

80-
Assert.Single(model.ConnectedPlayers);
67+
Assert.Empty(model.ConnectedPlayers);
8168
Assert.True(model.IsSeniorAdmin);
82-
Assert.Equal(1, model.TotalCount);
69+
Assert.Equal(0, model.TotalCount);
8370
}
8471

8572
[Fact]
86-
public async Task Index_WhenApiFails_RedirectsToErrorPage()
73+
public async Task Index_WhenUserIsNotSeniorAdmin_ReturnsViewModelWithNoSeniorAdminFlag()
8774
{
8875
// Arrange
89-
mockRepositoryApiClient
90-
.Setup(x => x.ConnectedPlayers.V1.GetConnectedPlayers(null, null, null, null, 0, 500, It.IsAny<CancellationToken>()))
91-
.ReturnsAsync(new ApiResult<CollectionModel<ConnectedPlayerDto>>(HttpStatusCode.InternalServerError));
92-
9376
var sut = CreateSut(new ClaimsPrincipal(new ClaimsIdentity([
9477
new Claim(UserProfileClaimType.Moderator, GameType.CallOfDuty4.ToString())
9578
], "TestAuth")));
9679

9780
// Act
9881
var result = await sut.Index();
9982

100-
// Assert
101-
var redirect = Assert.IsType<RedirectToActionResult>(result);
102-
Assert.Equal(nameof(ErrorsController.Display), redirect.ActionName);
103-
Assert.Equal("Errors", redirect.ControllerName);
104-
}
105-
106-
[Fact]
107-
public async Task Index_WhenMultiplePages_AggregatesAllRows()
108-
{
109-
// Arrange
110-
var firstPageCollection = new CollectionModel<ConnectedPlayerDto>([
111-
CreateConnectedPlayerDto(true)
112-
]);
113-
var firstPageResponse = new ApiResponse<CollectionModel<ConnectedPlayerDto>>(firstPageCollection)
114-
{
115-
Pagination = new ApiPagination(totalCount: 2, filteredCount: 2, skip: 0, top: 500)
116-
};
117-
118-
var secondPageCollection = new CollectionModel<ConnectedPlayerDto>([
119-
CreateConnectedPlayerDto(false)
120-
]);
121-
var secondPageResponse = new ApiResponse<CollectionModel<ConnectedPlayerDto>>(secondPageCollection)
122-
{
123-
Pagination = new ApiPagination(totalCount: 2, filteredCount: 2, skip: 1, top: 500)
124-
};
125-
126-
mockRepositoryApiClient
127-
.Setup(x => x.ConnectedPlayers.V1.GetConnectedPlayers(null, null, null, null, 0, 500, It.IsAny<CancellationToken>()))
128-
.ReturnsAsync(new ApiResult<CollectionModel<ConnectedPlayerDto>>(HttpStatusCode.OK, firstPageResponse));
129-
130-
mockRepositoryApiClient
131-
.Setup(x => x.ConnectedPlayers.V1.GetConnectedPlayers(null, null, null, null, 1, 500, It.IsAny<CancellationToken>()))
132-
.ReturnsAsync(new ApiResult<CollectionModel<ConnectedPlayerDto>>(HttpStatusCode.OK, secondPageResponse));
133-
134-
var sut = CreateSut(new ClaimsPrincipal(new ClaimsIdentity([
135-
new Claim(UserProfileClaimType.SeniorAdmin, "true")
136-
], "TestAuth")));
137-
138-
// Act
139-
var result = await sut.Index();
140-
14183
// Assert
14284
var viewResult = Assert.IsType<ViewResult>(result);
14385
var model = Assert.IsType<ConnectedPlayersAdminViewModel>(viewResult.Model);
14486

145-
Assert.Equal(2, model.ConnectedPlayers.Count);
146-
Assert.Equal(2, model.TotalCount);
147-
148-
mockRepositoryApiClient.Verify(x => x.ConnectedPlayers.V1.GetConnectedPlayers(null, null, null, null, 0, 500, It.IsAny<CancellationToken>()), Times.Once);
149-
mockRepositoryApiClient.Verify(x => x.ConnectedPlayers.V1.GetConnectedPlayers(null, null, null, null, 1, 500, It.IsAny<CancellationToken>()), Times.Once);
150-
}
151-
152-
[Fact]
153-
public async Task Index_WhenSecondPageFails_RedirectsToErrorPage()
154-
{
155-
// Arrange
156-
var firstPageCollection = new CollectionModel<ConnectedPlayerDto>([
157-
CreateConnectedPlayerDto(true)
158-
]);
159-
var firstPageResponse = new ApiResponse<CollectionModel<ConnectedPlayerDto>>(firstPageCollection)
160-
{
161-
Pagination = new ApiPagination(totalCount: 2, filteredCount: 2, skip: 0, top: 500)
162-
};
163-
164-
mockRepositoryApiClient
165-
.Setup(x => x.ConnectedPlayers.V1.GetConnectedPlayers(null, null, null, null, 0, 500, It.IsAny<CancellationToken>()))
166-
.ReturnsAsync(new ApiResult<CollectionModel<ConnectedPlayerDto>>(HttpStatusCode.OK, firstPageResponse));
167-
168-
mockRepositoryApiClient
169-
.Setup(x => x.ConnectedPlayers.V1.GetConnectedPlayers(null, null, null, null, 1, 500, It.IsAny<CancellationToken>()))
170-
.ReturnsAsync(new ApiResult<CollectionModel<ConnectedPlayerDto>>(HttpStatusCode.InternalServerError));
171-
172-
var sut = CreateSut(new ClaimsPrincipal(new ClaimsIdentity([
173-
new Claim(UserProfileClaimType.SeniorAdmin, "true")
174-
], "TestAuth")));
175-
176-
// Act
177-
var result = await sut.Index();
178-
179-
// Assert
180-
var redirect = Assert.IsType<RedirectToActionResult>(result);
181-
Assert.Equal(nameof(ErrorsController.Display), redirect.ActionName);
182-
Assert.Equal("Errors", redirect.ControllerName);
87+
Assert.Empty(model.ConnectedPlayers);
88+
Assert.False(model.IsSeniorAdmin);
89+
Assert.Equal(0, model.TotalCount);
18390
}
18491

18592
[Fact]
@@ -327,25 +234,4 @@ public async Task ForceUnlink_WhenSeniorAdminMissingProfileId_ReturnsForbid()
327234
It.IsAny<CancellationToken>()), Times.Never);
328235
}
329236

330-
private static ConnectedPlayerDto CreateConnectedPlayerDto(bool isActive)
331-
{
332-
var now = DateTime.UtcNow;
333-
334-
var json = JsonConvert.SerializeObject(new
335-
{
336-
ConnectedPlayerProfileId = Guid.NewGuid(),
337-
PlayerId = Guid.NewGuid(),
338-
UserProfileId = Guid.NewGuid(),
339-
GameType = GameType.CallOfDuty4,
340-
Username = "TestPlayer",
341-
LinkMethod = ConnectedPlayerLinkMethod.ActivationCode,
342-
LinkedAtUtc = now,
343-
LinkedByUserProfileId = Guid.NewGuid(),
344-
UnlinkedAtUtc = isActive ? (DateTime?)null : now.AddMinutes(1),
345-
UnlinkedByUserProfileId = isActive ? (Guid?)null : Guid.NewGuid(),
346-
IsActive = isActive
347-
});
348-
349-
return JsonConvert.DeserializeObject<ConnectedPlayerDto>(json)!;
350-
}
351237
}

0 commit comments

Comments
 (0)