feat(audit): multi-value filters across ManagementService, CLI and Central UI
This commit is contained in:
@@ -367,6 +367,89 @@ public class AuditEndpointsTests
|
||||
Assert.Equal(AuditEndpoints.MaxPageSize, paging.PageSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseFilter_RepeatedParams_ParseIntoMultiValueLists()
|
||||
{
|
||||
// Repeated query params (channel=A&channel=B …) must widen to multi-value
|
||||
// filter lists — one element per supplied value.
|
||||
var query = new Microsoft.AspNetCore.Http.QueryCollection(
|
||||
new Dictionary<string, Microsoft.Extensions.Primitives.StringValues>
|
||||
{
|
||||
["channel"] = new[] { "ApiOutbound", "DbOutbound" },
|
||||
["kind"] = new[] { "ApiCall", "DbWrite" },
|
||||
["status"] = new[] { "Failed", "Parked" },
|
||||
["sourceSiteId"] = new[] { "plant-a", "plant-b" },
|
||||
});
|
||||
|
||||
var filter = AuditEndpoints.ParseFilter(query);
|
||||
|
||||
Assert.Equal(new[] { AuditChannel.ApiOutbound, AuditChannel.DbOutbound }, filter.Channels);
|
||||
Assert.Equal(new[] { AuditKind.ApiCall, AuditKind.DbWrite }, filter.Kinds);
|
||||
Assert.Equal(new[] { AuditStatus.Failed, AuditStatus.Parked }, filter.Statuses);
|
||||
Assert.Equal(new[] { "plant-a", "plant-b" }, filter.SourceSiteIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseFilter_SingleParam_ParsesIntoOneElementList()
|
||||
{
|
||||
// The single-valued contract still holds — one param yields a
|
||||
// one-element list, not a scalar.
|
||||
var query = new Microsoft.AspNetCore.Http.QueryCollection(
|
||||
new Dictionary<string, Microsoft.Extensions.Primitives.StringValues>
|
||||
{
|
||||
["channel"] = "ApiOutbound",
|
||||
["status"] = "Delivered",
|
||||
});
|
||||
|
||||
var filter = AuditEndpoints.ParseFilter(query);
|
||||
|
||||
Assert.Equal(new[] { AuditChannel.ApiOutbound }, filter.Channels);
|
||||
Assert.Equal(new[] { AuditStatus.Delivered }, filter.Statuses);
|
||||
Assert.Null(filter.Kinds);
|
||||
Assert.Null(filter.SourceSiteIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseFilter_UnparseableValuesInRepeatedSet_AreDroppedSilently()
|
||||
{
|
||||
// Lax-parse contract: an unrecognised enum name is dropped, the rest of
|
||||
// the repeated set survives — no 400, no whole-set drop.
|
||||
var query = new Microsoft.AspNetCore.Http.QueryCollection(
|
||||
new Dictionary<string, Microsoft.Extensions.Primitives.StringValues>
|
||||
{
|
||||
["channel"] = new[] { "ApiOutbound", "Bogus", "Notification" },
|
||||
["status"] = new[] { "Nonsense" },
|
||||
});
|
||||
|
||||
var filter = AuditEndpoints.ParseFilter(query);
|
||||
|
||||
Assert.Equal(new[] { AuditChannel.ApiOutbound, AuditChannel.Notification }, filter.Channels);
|
||||
// Every value unparseable → the dimension stays unconstrained (null).
|
||||
Assert.Null(filter.Statuses);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Query_RepeatedChannelParams_ReachRepositoryAsMultiValueFilter()
|
||||
{
|
||||
// End-to-end: a repeated channel= query param must surface at the
|
||||
// repository as a two-element Channels list.
|
||||
var (client, repo, host) = await BuildHostAsync(roles: new[] { "Audit" });
|
||||
using (host)
|
||||
{
|
||||
var response = await client.SendAsync(Get(
|
||||
"/api/audit/query?channel=ApiOutbound&channel=DbOutbound"));
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
||||
await repo.Received().QueryAsync(
|
||||
Arg.Is<AuditLogQueryFilter>(f =>
|
||||
f.Channels != null && f.Channels.Count == 2 &&
|
||||
f.Channels.Contains(AuditChannel.ApiOutbound) &&
|
||||
f.Channels.Contains(AuditChannel.DbOutbound)),
|
||||
Arg.Any<AuditLogPaging>(),
|
||||
Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsePaging_HalfSuppliedCursor_IsDropped()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user