Skip to content

Commit 173e1c4

Browse files
feat: Implement tag filtering in Players Index and enhance related functionality
1 parent 8cb9cdf commit 173e1c4

10 files changed

Lines changed: 445 additions & 29 deletions

File tree

src/XtremeIdiots.Portal.Integrations.Forums/XtremeIdiots.Portal.Integrations.Forums.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<PackageReference Include="Azure.Identity" Version="1.17.1" />
1313
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.3" />
1414
<PackageReference Include="MX.InvisionCommunity.Api.Client" Version="1.0.18" />
15-
<PackageReference Include="XtremeIdiots.Portal.Repository.Api.Client.V1" Version="2.1.220" />
15+
<PackageReference Include="XtremeIdiots.Portal.Repository.Api.Client.V1" Version="2.1.249" />
1616
</ItemGroup>
1717

1818
</Project>
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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.Configuration;
10+
using Microsoft.Extensions.Logging;
11+
12+
using Moq;
13+
using MX.Api.Abstractions;
14+
using MX.GeoLocation.Api.Client.V1;
15+
using MX.Observability.ApplicationInsights.Auditing;
16+
using Newtonsoft.Json;
17+
using Newtonsoft.Json.Linq;
18+
19+
using XtremeIdiots.Portal.Repository.Abstractions.Constants.V1;
20+
using XtremeIdiots.Portal.Repository.Abstractions.Models.V1.Players;
21+
using XtremeIdiots.Portal.Repository.Api.Client.V1;
22+
using PlayersApiController = XtremeIdiots.Portal.Web.ApiControllers.PlayersController;
23+
24+
namespace XtremeIdiots.Portal.Web.Tests.ApiControllers;
25+
26+
public class PlayersControllerTests
27+
{
28+
private readonly Mock<IRepositoryApiClient> mockRepositoryApiClient = new();
29+
private readonly Mock<IGeoLocationApiClient> mockGeoLocationApiClient = new();
30+
private readonly TelemetryClient telemetryClient = new(new TelemetryConfiguration());
31+
private readonly Mock<ILogger<PlayersApiController>> mockLogger = new();
32+
private readonly Mock<IConfiguration> mockConfiguration = new();
33+
private readonly IAuditLogger auditLogger = new Mock<IAuditLogger>().Object;
34+
35+
private PlayersApiController CreateSut(ClaimsPrincipal? user = null)
36+
{
37+
var controller = new PlayersApiController(
38+
mockRepositoryApiClient.Object,
39+
mockGeoLocationApiClient.Object,
40+
telemetryClient,
41+
mockLogger.Object,
42+
mockConfiguration.Object,
43+
auditLogger);
44+
45+
var httpContext = new DefaultHttpContext
46+
{
47+
User = user ?? new ClaimsPrincipal(new ClaimsIdentity("TestAuth"))
48+
};
49+
50+
controller.ControllerContext = new ControllerContext { HttpContext = httpContext };
51+
return controller;
52+
}
53+
54+
[Fact]
55+
public async Task GetPlayersAjax_WithEmptyBody_ReturnsBadRequest()
56+
{
57+
// Arrange
58+
var sut = CreateSut();
59+
sut.HttpContext.Request.Body = new MemoryStream();
60+
61+
// Act
62+
var result = await sut.GetPlayersAjax(null, null, null, CancellationToken.None);
63+
64+
// Assert
65+
Assert.IsType<BadRequestObjectResult>(result);
66+
}
67+
68+
[Fact]
69+
public async Task GetPlayersAjax_ForwardsSelectedTagAndRequestsTagsEntityOptions()
70+
{
71+
// Arrange
72+
var selectedTagId = Guid.NewGuid();
73+
var player = CreatePlayerDtoWithTag();
74+
var apiResponse = new ApiResponse<CollectionModel<PlayerDto>>(new CollectionModel<PlayerDto>([player]))
75+
{
76+
Pagination = new ApiPagination(totalCount: 1, filteredCount: 1, skip: 0, top: 25)
77+
};
78+
79+
mockRepositoryApiClient
80+
.Setup(x => x.Players.V1.GetPlayers(
81+
GameType.CallOfDuty4,
82+
PlayersFilter.Tag,
83+
selectedTagId.ToString(),
84+
0,
85+
25,
86+
PlayersOrder.LastSeenDesc,
87+
PlayerEntityOptions.Tags))
88+
.ReturnsAsync(new ApiResult<CollectionModel<PlayerDto>>(HttpStatusCode.OK, apiResponse));
89+
90+
var request = new
91+
{
92+
draw = 1,
93+
start = 0,
94+
length = 25,
95+
columns = new[]
96+
{
97+
new { data = "username", name = "username", searchable = true, orderable = true, search = new { value = "", regex = false } },
98+
new { data = "tags", name = "tags", searchable = true, orderable = false, search = new { value = "", regex = false } },
99+
new { data = "ipAddress", name = "ipAddress", searchable = true, orderable = false, search = new { value = "", regex = false } },
100+
new { data = "guid", name = "guid", searchable = true, orderable = false, search = new { value = "", regex = false } },
101+
new { data = "firstSeen", name = "firstSeen", searchable = false, orderable = true, search = new { value = "", regex = false } },
102+
new { data = "lastSeen", name = "lastSeen", searchable = false, orderable = true, search = new { value = "", regex = false } }
103+
},
104+
order = new[] { new { column = 5, dir = "desc" } },
105+
search = new { value = "", regex = false }
106+
};
107+
108+
var body = JsonConvert.SerializeObject(request);
109+
var sut = CreateSut();
110+
sut.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
111+
112+
// Act
113+
var result = await sut.GetPlayersAjax(GameType.CallOfDuty4, PlayersFilter.UsernameAndGuid, selectedTagId, CancellationToken.None);
114+
115+
// Assert
116+
var ok = Assert.IsType<OkObjectResult>(result);
117+
var payload = JObject.Parse(JsonConvert.SerializeObject(ok.Value));
118+
var data = Assert.IsType<JArray>(payload["data"]);
119+
120+
Assert.Single(data);
121+
Assert.Equal("Trusted", data[0]?["tags"]?[0]?["name"]?.Value<string>());
122+
123+
mockRepositoryApiClient.Verify(x => x.Players.V1.GetPlayers(
124+
GameType.CallOfDuty4,
125+
PlayersFilter.Tag,
126+
selectedTagId.ToString(),
127+
0,
128+
25,
129+
PlayersOrder.LastSeenDesc,
130+
PlayerEntityOptions.Tags), Times.Once);
131+
}
132+
133+
[Fact]
134+
public async Task GetPlayersAjax_DefaultsFilter_WhenPlayersFilterNotProvided()
135+
{
136+
// Arrange
137+
var apiResponse = new ApiResponse<CollectionModel<PlayerDto>>(new CollectionModel<PlayerDto>([]))
138+
{
139+
Pagination = new ApiPagination(totalCount: 0, filteredCount: 0, skip: 0, top: 25)
140+
};
141+
142+
mockRepositoryApiClient
143+
.Setup(x => x.Players.V1.GetPlayers(
144+
null,
145+
PlayersFilter.UsernameAndGuid,
146+
string.Empty,
147+
0,
148+
25,
149+
PlayersOrder.LastSeenDesc,
150+
PlayerEntityOptions.Tags))
151+
.ReturnsAsync(new ApiResult<CollectionModel<PlayerDto>>(HttpStatusCode.OK, apiResponse));
152+
153+
var request = new
154+
{
155+
draw = 2,
156+
start = 0,
157+
length = 25,
158+
columns = new[]
159+
{
160+
new { data = "username", name = "username", searchable = true, orderable = true, search = new { value = "", regex = false } },
161+
new { data = "tags", name = "tags", searchable = true, orderable = false, search = new { value = "", regex = false } },
162+
new { data = "ipAddress", name = "ipAddress", searchable = true, orderable = false, search = new { value = "", regex = false } },
163+
new { data = "guid", name = "guid", searchable = true, orderable = false, search = new { value = "", regex = false } },
164+
new { data = "firstSeen", name = "firstSeen", searchable = false, orderable = true, search = new { value = "", regex = false } },
165+
new { data = "lastSeen", name = "lastSeen", searchable = false, orderable = true, search = new { value = "", regex = false } }
166+
},
167+
order = new[] { new { column = 5, dir = "desc" } },
168+
search = new { value = "", regex = false }
169+
};
170+
171+
var sut = CreateSut();
172+
sut.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(request)));
173+
174+
// Act
175+
var result = await sut.GetPlayersAjax(null, null, null, CancellationToken.None);
176+
177+
// Assert
178+
Assert.IsType<OkObjectResult>(result);
179+
mockRepositoryApiClient.Verify(x => x.Players.V1.GetPlayers(
180+
null,
181+
PlayersFilter.UsernameAndGuid,
182+
string.Empty,
183+
0,
184+
25,
185+
PlayersOrder.LastSeenDesc,
186+
PlayerEntityOptions.Tags), Times.Once);
187+
}
188+
189+
private static PlayerDto CreatePlayerDtoWithTag()
190+
{
191+
var json = JsonConvert.SerializeObject(new
192+
{
193+
PlayerId = Guid.NewGuid(),
194+
GameType = "CallOfDuty4",
195+
Username = "TestPlayer",
196+
Guid = "ABC-123",
197+
IpAddress = "",
198+
FirstSeen = DateTime.UtcNow.AddDays(-2),
199+
LastSeen = DateTime.UtcNow,
200+
Tags = new[]
201+
{
202+
new
203+
{
204+
PlayerTagId = Guid.NewGuid(),
205+
PlayerId = Guid.NewGuid(),
206+
TagId = Guid.NewGuid(),
207+
Tag = new
208+
{
209+
TagId = Guid.NewGuid(),
210+
Name = "Trusted",
211+
TagHtml = "<span class=\"badge bg-success\">Trusted</span>",
212+
UserDefined = true
213+
}
214+
}
215+
}
216+
});
217+
218+
return JsonConvert.DeserializeObject<PlayerDto>(json)!;
219+
}
220+
}

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

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66
using Microsoft.Extensions.Configuration;
77
using Microsoft.Extensions.Logging;
88
using Moq;
9+
using MX.Api.Abstractions;
910
using MX.GeoLocation.Api.Client.V1;
1011
using MX.Observability.ApplicationInsights.Auditing;
12+
using System.Net;
1113
using System.Security.Claims;
1214
using XtremeIdiots.Portal.Repository.Abstractions.Constants.V1;
15+
using XtremeIdiots.Portal.Repository.Abstractions.Models.V1.Tags;
1316
using XtremeIdiots.Portal.Repository.Api.Client.V1;
1417
using XtremeIdiots.Portal.Web.Controllers;
18+
using XtremeIdiots.Portal.Web.ViewModels;
1519

1620
namespace XtremeIdiots.Portal.Web.Tests.Controllers;
1721

@@ -56,43 +60,73 @@ public void Constructor_WithValidDependencies_DoesNotThrow()
5660
}
5761

5862
[Fact]
59-
public async Task Index_ReturnsViewResult()
63+
public async Task Index_ReturnsViewResult_WithTagOptionsModel()
6064
{
6165
// Arrange
66+
SetupTagsResponse();
6267
var sut = CreateSut();
6368

6469
// Act
6570
var result = await sut.Index();
6671

6772
// Assert
68-
Assert.IsType<ViewResult>(result);
73+
var viewResult = Assert.IsType<ViewResult>(result);
74+
var model = Assert.IsType<PlayersIndexViewModel>(viewResult.Model);
75+
76+
Assert.Null(model.SelectedGameType);
77+
Assert.Single(model.Tags);
78+
Assert.Equal("Trusted", model.Tags[0].Name);
79+
80+
mockRepositoryApiClient.Verify(x => x.Tags.V1.GetTags(0, 100, It.IsAny<CancellationToken>()), Times.Once);
6981
}
7082

7183
[Fact]
7284
public async Task GameIndex_WithNullGameType_ReturnsViewResult()
7385
{
7486
// Arrange
87+
SetupTagsResponse();
7588
var sut = CreateSut();
7689

7790
// Act
7891
var result = await sut.GameIndex(null);
7992

8093
// Assert
81-
Assert.IsType<ViewResult>(result);
94+
var viewResult = Assert.IsType<ViewResult>(result);
95+
var model = Assert.IsType<PlayersIndexViewModel>(viewResult.Model);
96+
97+
Assert.Equal("Index", viewResult.ViewName);
98+
Assert.Null(model.SelectedGameType);
8299
}
83100

84101
[Fact]
85102
public async Task GameIndex_WithGameType_SetsViewDataAndReturnsViewResult()
86103
{
87104
// Arrange
105+
SetupTagsResponse();
88106
var sut = CreateSut();
89107

90108
// Act
91109
var result = await sut.GameIndex(GameType.CallOfDuty2);
92110

93111
// Assert
94112
var viewResult = Assert.IsType<ViewResult>(result);
113+
var model = Assert.IsType<PlayersIndexViewModel>(viewResult.Model);
114+
95115
Assert.Equal("Index", viewResult.ViewName);
96-
Assert.Equal(GameType.CallOfDuty2, sut.ViewData["GameType"]);
116+
Assert.Equal(GameType.CallOfDuty2, model.SelectedGameType);
117+
Assert.Single(model.Tags);
118+
}
119+
120+
private void SetupTagsResponse()
121+
{
122+
var tags = new CollectionModel<TagDto>([new TagDto { TagId = Guid.NewGuid(), Name = "Trusted", TagHtml = "<span class=\"badge bg-success\">Trusted</span>" }]);
123+
var response = new ApiResponse<CollectionModel<TagDto>>(tags)
124+
{
125+
Pagination = new ApiPagination(totalCount: 1, filteredCount: 1, skip: 0, top: 100)
126+
};
127+
128+
mockRepositoryApiClient
129+
.Setup(x => x.Tags.V1.GetTags(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
130+
.ReturnsAsync(new ApiResult<CollectionModel<TagDto>>(HttpStatusCode.OK, response));
97131
}
98132
}

src/XtremeIdiots.Portal.Web/ApiControllers/PlayersController.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ public class PlayersController(
3434
/// </summary>
3535
/// <param name="id">Optional game type filter</param>
3636
/// <param name="playersFilter">Filter type (UsernameAndGuid or IpAddress)</param>
37+
/// <param name="selectedTagId">Optional tag filter by tag ID</param>
3738
/// <param name="cancellationToken">Cancellation token for the async operation</param>
3839
/// <returns>JSON data for DataTable display</returns>
3940
[HttpPost("GetPlayersAjax/{id?}")]
4041
[ValidateAntiForgeryToken]
41-
public async Task<IActionResult> GetPlayersAjax(GameType? id, [FromQuery] PlayersFilter? playersFilter, CancellationToken cancellationToken = default)
42+
public async Task<IActionResult> GetPlayersAjax(GameType? id, [FromQuery] PlayersFilter? playersFilter, [FromQuery] Guid? selectedTagId, CancellationToken cancellationToken = default)
4243
{
4344
return await ExecuteWithErrorHandlingAsync(async () =>
4445
{
@@ -56,9 +57,17 @@ public async Task<IActionResult> GetPlayersAjax(GameType? id, [FromQuery] Player
5657
}
5758

5859
var order = GetPlayersOrderFromDataTable(model);
60+
var effectiveFilter = selectedTagId.HasValue ? PlayersFilter.Tag : filter;
61+
var filterString = selectedTagId.HasValue ? selectedTagId.Value.ToString() : model.Search?.Value;
5962

6063
var playerCollectionApiResponse = await repositoryApiClient.Players.V1.GetPlayers(
61-
id, filter, model.Search?.Value, model.Start, model.Length, order, PlayerEntityOptions.None).ConfigureAwait(false);
64+
id,
65+
effectiveFilter,
66+
filterString,
67+
model.Start,
68+
model.Length,
69+
order,
70+
PlayerEntityOptions.Tags).ConfigureAwait(false);
6271

6372
if (!playerCollectionApiResponse.IsSuccess || playerCollectionApiResponse.Result?.Data?.Items is null)
6473
{
@@ -80,6 +89,11 @@ public async Task<IActionResult> GetPlayersAjax(GameType? id, [FromQuery] Player
8089
player.Username,
8190
player.Guid,
8291
player.IpAddress,
92+
tags = (player.Tags ?? []).Select(t => new
93+
{
94+
t.TagId,
95+
name = t.Tag?.Name
96+
}),
8397
player.FirstSeen,
8498
player.LastSeen,
8599
intel.ProxyCheckRiskScore,

0 commit comments

Comments
 (0)