refactor(audit): consolidate query-param parsers; widen CLI export to multi-value
This commit is contained in:
@@ -63,8 +63,8 @@ public class AuditExportCommandTests
|
||||
Until = "2026-05-20T12:00:00Z",
|
||||
Format = "jsonl",
|
||||
Output = "/tmp/x",
|
||||
Channel = "Notification",
|
||||
Site = "site-9",
|
||||
Channel = new[] { "Notification" },
|
||||
Site = new[] { "site-9" },
|
||||
};
|
||||
var qs = AuditExportHelpers.BuildQueryString(args, now);
|
||||
var parsed = HttpUtility.ParseQueryString(qs.TrimStart('?'));
|
||||
@@ -76,6 +76,90 @@ public class AuditExportCommandTests
|
||||
Assert.Equal("2026-05-20T12:00:00.0000000+00:00", parsed["toUtc"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildQueryString_MultiValueFilters_EmitOneKeyPerValue()
|
||||
{
|
||||
var now = DateTimeOffset.Parse("2026-05-20T12:00:00Z");
|
||||
var args = new AuditExportArgs
|
||||
{
|
||||
Since = "1h",
|
||||
Until = "2026-05-20T12:00:00Z",
|
||||
Format = "csv",
|
||||
Output = "/tmp/x",
|
||||
Channel = new[] { "ApiOutbound", "DbOutbound" },
|
||||
Kind = new[] { "ApiCall", "DbWrite" },
|
||||
Status = new[] { "Failed", "Parked" },
|
||||
Site = new[] { "site-1", "site-2" },
|
||||
};
|
||||
var qs = AuditExportHelpers.BuildQueryString(args, now);
|
||||
var parsed = HttpUtility.ParseQueryString(qs.TrimStart('?'));
|
||||
|
||||
Assert.Equal(new[] { "ApiOutbound", "DbOutbound" }, parsed.GetValues("channel"));
|
||||
Assert.Equal(new[] { "ApiCall", "DbWrite" }, parsed.GetValues("kind"));
|
||||
Assert.Equal(new[] { "Failed", "Parked" }, parsed.GetValues("status"));
|
||||
Assert.Equal(new[] { "site-1", "site-2" }, parsed.GetValues("sourceSiteId"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildQueryString_OmitsUnsetMultiValueFilters()
|
||||
{
|
||||
var now = DateTimeOffset.Parse("2026-05-20T12:00:00Z");
|
||||
var args = new AuditExportArgs
|
||||
{
|
||||
Since = "1h",
|
||||
Until = "0h",
|
||||
Format = "csv",
|
||||
Output = "/tmp/x",
|
||||
};
|
||||
var qs = AuditExportHelpers.BuildQueryString(args, now);
|
||||
var parsed = HttpUtility.ParseQueryString(qs.TrimStart('?'));
|
||||
|
||||
Assert.Null(parsed["channel"]);
|
||||
Assert.Null(parsed["kind"]);
|
||||
Assert.Null(parsed["status"]);
|
||||
Assert.Null(parsed["sourceSiteId"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Export_MultipleChannelValues_SingleToken_AreAccepted()
|
||||
{
|
||||
// AllowMultipleArgumentsPerToken: --channel A B parses as two values.
|
||||
var root = AuditCommandTestHarness.BuildRoot();
|
||||
var parse = root.Parse(new[]
|
||||
{
|
||||
"audit", "export", "--since", "1h", "--until", "0h",
|
||||
"--format", "csv", "--output", "/tmp/out.csv",
|
||||
"--channel", "ApiOutbound", "DbOutbound",
|
||||
});
|
||||
Assert.Empty(parse.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Export_MultipleChannelValues_RepeatedFlag_AreAccepted()
|
||||
{
|
||||
var root = AuditCommandTestHarness.BuildRoot();
|
||||
var parse = root.Parse(new[]
|
||||
{
|
||||
"audit", "export", "--since", "1h", "--until", "0h",
|
||||
"--format", "csv", "--output", "/tmp/out.csv",
|
||||
"--channel", "ApiOutbound", "--channel", "Notification",
|
||||
});
|
||||
Assert.Empty(parse.Errors);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Export_MultiValueChannel_WithOneInvalidName_FailsFast()
|
||||
{
|
||||
// AcceptOnlyFromAmong validates EACH value of the multi-value option.
|
||||
var root = AuditCommandTestHarness.BuildRoot();
|
||||
var (exit, _, err) = AuditCommandTestHarness.Invoke(
|
||||
root, "audit", "export", "--since", "1h", "--until", "0h",
|
||||
"--format", "csv", "--output", "/tmp/out.csv",
|
||||
"--channel", "ApiOutbound", "OutboundApi");
|
||||
Assert.NotEqual(0, exit);
|
||||
Assert.NotEqual("", err);
|
||||
}
|
||||
|
||||
// ---- Streaming export to file -----------------------------------------
|
||||
|
||||
private sealed class BodyHandler : HttpMessageHandler
|
||||
|
||||
@@ -76,7 +76,7 @@ public class AuditQueryCommandTests
|
||||
Assert.Equal("ApiCallCached", parsed["kind"]);
|
||||
Assert.Equal("Delivered", parsed["status"]);
|
||||
Assert.Equal("site-1", parsed["sourceSiteId"]);
|
||||
// --instance was dropped: AuditLogQueryFilter has no instance column.
|
||||
// The CLI audit query has no --instance flag, so no instance param is emitted.
|
||||
Assert.Null(parsed["instance"]);
|
||||
Assert.Equal("weather-api", parsed["target"]);
|
||||
Assert.Equal("multi-role", parsed["actor"]);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using ScadaLink.Commons.Types.Audit;
|
||||
using ScadaLink.Commons.Types.Enums;
|
||||
|
||||
namespace ScadaLink.Commons.Tests.Types;
|
||||
|
||||
/// <summary>
|
||||
/// Audit Log #23 (M8): tests for the shared lax multi-value query-param parsers
|
||||
/// used by the ManagementService + CentralUI audit endpoints and the
|
||||
/// <c>AuditLogPage</c> drill-in parser. The contract under test: parse each
|
||||
/// repeated value independently, silently drop unparseable/blank elements, and
|
||||
/// collapse an empty result to <c>null</c>.
|
||||
/// </summary>
|
||||
public class AuditQueryParamParsersTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParseEnumList_NullInput_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseEnumList<AuditChannel>(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseEnumList_EmptyInput_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseEnumList<AuditChannel>(Array.Empty<string?>()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseEnumList_AllValuesValid_ParsesEverything()
|
||||
{
|
||||
var result = AuditQueryParamParsers.ParseEnumList<AuditChannel>(
|
||||
new[] { "ApiOutbound", "DbOutbound" });
|
||||
Assert.Equal(new[] { AuditChannel.ApiOutbound, AuditChannel.DbOutbound }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseEnumList_IsCaseInsensitive()
|
||||
{
|
||||
var result = AuditQueryParamParsers.ParseEnumList<AuditChannel>(new[] { "apioutbound" });
|
||||
Assert.Equal(new[] { AuditChannel.ApiOutbound }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseEnumList_DropsUnparseableElement_KeepsTheRest()
|
||||
{
|
||||
var result = AuditQueryParamParsers.ParseEnumList<AuditChannel>(
|
||||
new[] { "ApiOutbound", "NotAChannel", "Notification" });
|
||||
Assert.Equal(new[] { AuditChannel.ApiOutbound, AuditChannel.Notification }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseEnumList_AllValuesUnparseable_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseEnumList<AuditStatus>(new[] { "Bogus", "" }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseStringList_NullInput_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseStringList(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseStringList_TrimsValuesAndDropsBlanks()
|
||||
{
|
||||
var result = AuditQueryParamParsers.ParseStringList(
|
||||
new[] { " site-1 ", "", " ", "site-2", null });
|
||||
Assert.Equal(new[] { "site-1", "site-2" }, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseStringList_AllBlank_ReturnsNull()
|
||||
{
|
||||
Assert.Null(AuditQueryParamParsers.ParseStringList(new[] { "", " ", null }));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user