Skip to content

Commit 459ee8b

Browse files
feat: Add Connected Players management with index view, controller, and JavaScript functionality
1 parent 8ab7f6d commit 459ee8b

6 files changed

Lines changed: 890 additions & 3 deletions

File tree

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
using System.Net;
2+
using System.Security.Claims;
3+
4+
using Microsoft.ApplicationInsights;
5+
using Microsoft.ApplicationInsights.Extensibility;
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Mvc;
8+
using Microsoft.AspNetCore.Mvc.ViewFeatures;
9+
using Microsoft.Extensions.Configuration;
10+
using Microsoft.Extensions.Logging;
11+
12+
using Moq;
13+
using MX.Api.Abstractions;
14+
using MX.Observability.ApplicationInsights.Auditing;
15+
using Newtonsoft.Json;
16+
17+
using XtremeIdiots.Portal.Repository.Abstractions.Constants.V1;
18+
using XtremeIdiots.Portal.Repository.Abstractions.Models.V1.ConnectedPlayers;
19+
using XtremeIdiots.Portal.Repository.Api.Client.V1;
20+
using XtremeIdiots.Portal.Web.Controllers;
21+
using XtremeIdiots.Portal.Web.ViewModels;
22+
23+
namespace XtremeIdiots.Portal.Web.Tests.Controllers;
24+
25+
public class ConnectedPlayersControllerTests
26+
{
27+
private readonly Mock<IRepositoryApiClient> mockRepositoryApiClient = new(MockBehavior.Default) { DefaultValue = DefaultValue.Mock };
28+
private readonly TelemetryClient telemetryClient = new(new TelemetryConfiguration());
29+
private readonly Mock<ILogger<ConnectedPlayersController>> mockLogger = new();
30+
private readonly Mock<IConfiguration> mockConfiguration = new();
31+
private readonly IAuditLogger auditLogger = new Mock<IAuditLogger>().Object;
32+
33+
private ConnectedPlayersController CreateSut(ClaimsPrincipal? user = null)
34+
{
35+
var controller = new ConnectedPlayersController(
36+
mockRepositoryApiClient.Object,
37+
telemetryClient,
38+
mockLogger.Object,
39+
mockConfiguration.Object,
40+
auditLogger);
41+
42+
var httpContext = new DefaultHttpContext
43+
{
44+
User = user ?? new ClaimsPrincipal(new ClaimsIdentity("TestAuth"))
45+
};
46+
47+
controller.ControllerContext = new ControllerContext { HttpContext = httpContext };
48+
controller.TempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
49+
50+
return controller;
51+
}
52+
53+
[Fact]
54+
public async Task Index_WhenApiSucceeds_ReturnsViewModel()
55+
{
56+
// 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+
69+
var sut = CreateSut(new ClaimsPrincipal(new ClaimsIdentity([
70+
new Claim(UserProfileClaimType.SeniorAdmin, "true")
71+
], "TestAuth")));
72+
73+
// Act
74+
var result = await sut.Index();
75+
76+
// Assert
77+
var viewResult = Assert.IsType<ViewResult>(result);
78+
var model = Assert.IsType<ConnectedPlayersAdminViewModel>(viewResult.Model);
79+
80+
Assert.Single(model.ConnectedPlayers);
81+
Assert.True(model.IsSeniorAdmin);
82+
Assert.Equal(1, model.TotalCount);
83+
}
84+
85+
[Fact]
86+
public async Task Index_WhenApiFails_RedirectsToErrorPage()
87+
{
88+
// 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+
93+
var sut = CreateSut(new ClaimsPrincipal(new ClaimsIdentity([
94+
new Claim(UserProfileClaimType.Moderator, GameType.CallOfDuty4.ToString())
95+
], "TestAuth")));
96+
97+
// Act
98+
var result = await sut.Index();
99+
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+
141+
// Assert
142+
var viewResult = Assert.IsType<ViewResult>(result);
143+
var model = Assert.IsType<ConnectedPlayersAdminViewModel>(viewResult.Model);
144+
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);
183+
}
184+
185+
[Fact]
186+
public async Task CreateManualLink_WhenNotSeniorAdmin_ReturnsForbid()
187+
{
188+
// Arrange
189+
var sut = CreateSut();
190+
191+
// Act
192+
var result = await sut.CreateManualLink(new CreateConnectedPlayerLinkInput
193+
{
194+
PlayerId = Guid.NewGuid(),
195+
UserProfileId = Guid.NewGuid()
196+
});
197+
198+
// Assert
199+
Assert.IsType<ForbidResult>(result);
200+
201+
mockRepositoryApiClient.Verify(x => x.ConnectedPlayers.V1.CreateConnectedPlayerLink(
202+
It.IsAny<CreateConnectedPlayerLinkDto>(),
203+
It.IsAny<CancellationToken>()), Times.Never);
204+
}
205+
206+
[Fact]
207+
public async Task CreateManualLink_WhenSeniorAdmin_CallsCreateConnectedPlayerLink()
208+
{
209+
// Arrange
210+
var playerId = Guid.NewGuid();
211+
var userProfileId = Guid.NewGuid();
212+
var actorProfileId = Guid.NewGuid();
213+
214+
mockRepositoryApiClient
215+
.Setup(x => x.ConnectedPlayers.V1.CreateConnectedPlayerLink(It.IsAny<CreateConnectedPlayerLinkDto>(), It.IsAny<CancellationToken>()))
216+
.ReturnsAsync(new ApiResult(HttpStatusCode.Created));
217+
218+
var sut = CreateSut(new ClaimsPrincipal(new ClaimsIdentity([
219+
new Claim(UserProfileClaimType.SeniorAdmin, "true"),
220+
new Claim(UserProfileClaimType.UserProfileId, actorProfileId.ToString())
221+
], "TestAuth")));
222+
223+
// Act
224+
var result = await sut.CreateManualLink(new CreateConnectedPlayerLinkInput
225+
{
226+
PlayerId = playerId,
227+
UserProfileId = userProfileId
228+
});
229+
230+
// Assert
231+
var redirect = Assert.IsType<RedirectToActionResult>(result);
232+
Assert.Equal(nameof(ConnectedPlayersController.Index), redirect.ActionName);
233+
234+
mockRepositoryApiClient.Verify(x => x.ConnectedPlayers.V1.CreateConnectedPlayerLink(
235+
It.Is<CreateConnectedPlayerLinkDto>(dto =>
236+
dto.PlayerId == playerId &&
237+
dto.UserProfileId == userProfileId &&
238+
dto.LinkedByUserProfileId == actorProfileId &&
239+
dto.LinkMethod == ConnectedPlayerLinkMethod.AdminForced),
240+
It.IsAny<CancellationToken>()), Times.Once);
241+
}
242+
243+
[Fact]
244+
public async Task CreateManualLink_WhenSeniorAdminMissingProfileId_ReturnsForbid()
245+
{
246+
// Arrange
247+
var sut = CreateSut(new ClaimsPrincipal(new ClaimsIdentity([
248+
new Claim(UserProfileClaimType.SeniorAdmin, "true")
249+
], "TestAuth")));
250+
251+
// Act
252+
var result = await sut.CreateManualLink(new CreateConnectedPlayerLinkInput
253+
{
254+
PlayerId = Guid.NewGuid(),
255+
UserProfileId = Guid.NewGuid()
256+
});
257+
258+
// Assert
259+
Assert.IsType<ForbidResult>(result);
260+
mockRepositoryApiClient.Verify(x => x.ConnectedPlayers.V1.CreateConnectedPlayerLink(
261+
It.IsAny<CreateConnectedPlayerLinkDto>(),
262+
It.IsAny<CancellationToken>()), Times.Never);
263+
}
264+
265+
[Fact]
266+
public async Task ForceUnlink_WhenNotSeniorAdmin_ReturnsForbid()
267+
{
268+
// Arrange
269+
var sut = CreateSut();
270+
271+
// Act
272+
var result = await sut.ForceUnlink(Guid.NewGuid());
273+
274+
// Assert
275+
Assert.IsType<ForbidResult>(result);
276+
mockRepositoryApiClient.Verify(x => x.ConnectedPlayers.V1.ForceUnlinkConnectedPlayer(
277+
It.IsAny<Guid>(),
278+
It.IsAny<ForceUnlinkConnectedPlayerDto>(),
279+
It.IsAny<CancellationToken>()), Times.Never);
280+
}
281+
282+
[Fact]
283+
public async Task ForceUnlink_WhenSeniorAdmin_CallsForceUnlink()
284+
{
285+
// Arrange
286+
var connectedPlayerProfileId = Guid.NewGuid();
287+
var actorProfileId = Guid.NewGuid();
288+
289+
mockRepositoryApiClient
290+
.Setup(x => x.ConnectedPlayers.V1.ForceUnlinkConnectedPlayer(It.IsAny<Guid>(), It.IsAny<ForceUnlinkConnectedPlayerDto>(), It.IsAny<CancellationToken>()))
291+
.ReturnsAsync(new ApiResult(HttpStatusCode.NoContent));
292+
293+
var sut = CreateSut(new ClaimsPrincipal(new ClaimsIdentity([
294+
new Claim(UserProfileClaimType.SeniorAdmin, "true"),
295+
new Claim(UserProfileClaimType.UserProfileId, actorProfileId.ToString())
296+
], "TestAuth")));
297+
298+
// Act
299+
var result = await sut.ForceUnlink(connectedPlayerProfileId);
300+
301+
// Assert
302+
var redirect = Assert.IsType<RedirectToActionResult>(result);
303+
Assert.Equal(nameof(ConnectedPlayersController.Index), redirect.ActionName);
304+
305+
mockRepositoryApiClient.Verify(x => x.ConnectedPlayers.V1.ForceUnlinkConnectedPlayer(
306+
connectedPlayerProfileId,
307+
It.Is<ForceUnlinkConnectedPlayerDto>(dto => dto.UnlinkedByUserProfileId == actorProfileId),
308+
It.IsAny<CancellationToken>()), Times.Once);
309+
}
310+
311+
[Fact]
312+
public async Task ForceUnlink_WhenSeniorAdminMissingProfileId_ReturnsForbid()
313+
{
314+
// Arrange
315+
var sut = CreateSut(new ClaimsPrincipal(new ClaimsIdentity([
316+
new Claim(UserProfileClaimType.SeniorAdmin, "true")
317+
], "TestAuth")));
318+
319+
// Act
320+
var result = await sut.ForceUnlink(Guid.NewGuid());
321+
322+
// Assert
323+
Assert.IsType<ForbidResult>(result);
324+
mockRepositoryApiClient.Verify(x => x.ConnectedPlayers.V1.ForceUnlinkConnectedPlayer(
325+
It.IsAny<Guid>(),
326+
It.IsAny<ForceUnlinkConnectedPlayerDto>(),
327+
It.IsAny<CancellationToken>()), Times.Never);
328+
}
329+
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+
}
351+
}

0 commit comments

Comments
 (0)