diff --git a/src/ScadaLink.CentralUI/Audit/AuditExportEndpoints.cs b/src/ScadaLink.CentralUI/Audit/AuditExportEndpoints.cs index fdf34f6..4047763 100644 --- a/src/ScadaLink.CentralUI/Audit/AuditExportEndpoints.cs +++ b/src/ScadaLink.CentralUI/Audit/AuditExportEndpoints.cs @@ -116,10 +116,10 @@ public static class AuditExportEndpoints DateTime? toUtc = ParseUtcDate(query, "to"); return new AuditLogQueryFilter( - Channel: channel, - Kind: kind, - Status: status, - SourceSiteId: site, + Channels: channel is { } c ? new[] { c } : null, + Kinds: kind is { } k ? new[] { k } : null, + Statuses: status is { } s ? new[] { s } : null, + SourceSiteIds: site is { } siteId ? new[] { siteId } : null, Target: target, Actor: actor, CorrelationId: correlationId, diff --git a/src/ScadaLink.CentralUI/Components/Audit/AuditQueryModel.cs b/src/ScadaLink.CentralUI/Components/Audit/AuditQueryModel.cs index 6ed9e70..f902c90 100644 --- a/src/ScadaLink.CentralUI/Components/Audit/AuditQueryModel.cs +++ b/src/ScadaLink.CentralUI/Components/Audit/AuditQueryModel.cs @@ -114,10 +114,10 @@ public sealed class AuditQueryModel var (fromUtc, toUtc) = ResolveTimeWindow(utcNow); return new AuditLogQueryFilter( - Channel: Channels.Count > 0 ? Channels.First() : null, - Kind: Kinds.Count > 0 ? Kinds.First() : null, - Status: status, - SourceSiteId: SiteIdentifiers.Count > 0 ? SiteIdentifiers.First() : null, + Channels: Channels.Count > 0 ? new[] { Channels.First() } : null, + Kinds: Kinds.Count > 0 ? new[] { Kinds.First() } : null, + Statuses: status is { } s ? new[] { s } : null, + SourceSiteIds: SiteIdentifiers.Count > 0 ? new[] { SiteIdentifiers.First() } : null, Target: string.IsNullOrWhiteSpace(TargetSearch) ? null : TargetSearch.Trim(), Actor: string.IsNullOrWhiteSpace(ActorSearch) ? null : ActorSearch.Trim(), CorrelationId: null, diff --git a/src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor.cs b/src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor.cs index b3c05ff..9653150 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor.cs +++ b/src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor.cs @@ -129,9 +129,9 @@ public partial class AuditLogPage } _currentFilter = new AuditLogQueryFilter( - Channel: channel, - Status: status, - SourceSiteId: site, + Channels: channel is { } ch ? new[] { ch } : null, + Statuses: status is { } st ? new[] { st } : null, + SourceSiteIds: site is { } siteId ? new[] { siteId } : null, Target: target, Actor: actor, CorrelationId: correlationId); @@ -181,21 +181,24 @@ public partial class AuditLogPage } var parts = new List>(9); - if (filter.Channel is { } ch) + // Task 8: the filter dimensions are multi-value now; the export query-string + // is still single-value, so emit the first selected value of each list. + // Task 9 widens the query-string contract to carry the full set. + if (filter.Channels is { Count: > 0 } channels) { - parts.Add(new("channel", ch.ToString())); + parts.Add(new("channel", channels[0].ToString())); } - if (filter.Kind is { } kind) + if (filter.Kinds is { Count: > 0 } kinds) { - parts.Add(new("kind", kind.ToString())); + parts.Add(new("kind", kinds[0].ToString())); } - if (filter.Status is { } status) + if (filter.Statuses is { Count: > 0 } statuses) { - parts.Add(new("status", status.ToString())); + parts.Add(new("status", statuses[0].ToString())); } - if (!string.IsNullOrWhiteSpace(filter.SourceSiteId)) + if (filter.SourceSiteIds is { Count: > 0 } sourceSiteIds && !string.IsNullOrWhiteSpace(sourceSiteIds[0])) { - parts.Add(new("site", filter.SourceSiteId)); + parts.Add(new("site", sourceSiteIds[0])); } if (!string.IsNullOrWhiteSpace(filter.Target)) { diff --git a/src/ScadaLink.Commons/Types/Audit/AuditLogQueryFilter.cs b/src/ScadaLink.Commons/Types/Audit/AuditLogQueryFilter.cs index 4e2001a..5399f48 100644 --- a/src/ScadaLink.Commons/Types/Audit/AuditLogQueryFilter.cs +++ b/src/ScadaLink.Commons/Types/Audit/AuditLogQueryFilter.cs @@ -4,16 +4,20 @@ namespace ScadaLink.Commons.Types.Audit; /// /// Filter predicate for . -/// Any field left null means "do not constrain on that column". Time bounds -/// are half-open in the spec sense — is inclusive and -/// is inclusive of the upper bound; the repository SQL uses -/// >= / <= respectively. All filter fields are AND-combined. +/// Any field left null means "do not constrain on that column". The +/// , , and +/// dimensions are multi-value: a null OR empty +/// list means "do not constrain", and a non-empty list is OR-combined within the +/// dimension (translated to a SQL IN (…)). Time bounds are half-open in +/// the spec sense — is inclusive and is +/// inclusive of the upper bound; the repository SQL uses >= / <= +/// respectively. All filter dimensions are AND-combined with one another. /// public sealed record AuditLogQueryFilter( - AuditChannel? Channel = null, - AuditKind? Kind = null, - AuditStatus? Status = null, - string? SourceSiteId = null, + IReadOnlyList? Channels = null, + IReadOnlyList? Kinds = null, + IReadOnlyList? Statuses = null, + IReadOnlyList? SourceSiteIds = null, string? Target = null, string? Actor = null, Guid? CorrelationId = null, diff --git a/src/ScadaLink.ConfigurationDatabase/Repositories/AuditLogRepository.cs b/src/ScadaLink.ConfigurationDatabase/Repositories/AuditLogRepository.cs index f517a8e..4a6ce78 100644 --- a/src/ScadaLink.ConfigurationDatabase/Repositories/AuditLogRepository.cs +++ b/src/ScadaLink.ConfigurationDatabase/Repositories/AuditLogRepository.cs @@ -116,25 +116,28 @@ VALUES var query = _context.Set().AsNoTracking(); - if (filter.Channel is { } channel) + // Multi-value dimensions: a null OR empty list means "no constraint" + // (the { Count: > 0 } guard prevents an empty list collapsing to a + // WHERE 1=0). A non-empty list translates to a SQL IN (…) via EF Core's + // IReadOnlyList.Contains support — server-side, no client-eval. + if (filter.Channels is { Count: > 0 } channels) { - query = query.Where(e => e.Channel == channel); + query = query.Where(e => channels.Contains(e.Channel)); } - if (filter.Kind is { } kind) + if (filter.Kinds is { Count: > 0 } kinds) { - query = query.Where(e => e.Kind == kind); + query = query.Where(e => kinds.Contains(e.Kind)); } - if (filter.Status is { } status) + if (filter.Statuses is { Count: > 0 } statuses) { - query = query.Where(e => e.Status == status); + query = query.Where(e => statuses.Contains(e.Status)); } - if (!string.IsNullOrEmpty(filter.SourceSiteId)) + if (filter.SourceSiteIds is { Count: > 0 } sourceSiteIds) { - var siteId = filter.SourceSiteId; - query = query.Where(e => e.SourceSiteId == siteId); + query = query.Where(e => e.SourceSiteId != null && sourceSiteIds.Contains(e.SourceSiteId)); } if (!string.IsNullOrEmpty(filter.Target)) diff --git a/src/ScadaLink.ManagementService/AuditEndpoints.cs b/src/ScadaLink.ManagementService/AuditEndpoints.cs index 18217eb..0cf58c3 100644 --- a/src/ScadaLink.ManagementService/AuditEndpoints.cs +++ b/src/ScadaLink.ManagementService/AuditEndpoints.cs @@ -401,11 +401,13 @@ public static class AuditEndpoints correlationId = parsedCorr; } + var sourceSiteId = TrimToNullable(query, "sourceSiteId"); + return new AuditLogQueryFilter( - Channel: channel, - Kind: kind, - Status: status, - SourceSiteId: TrimToNullable(query, "sourceSiteId"), + Channels: channel is { } c ? new[] { c } : null, + Kinds: kind is { } k ? new[] { k } : null, + Statuses: status is { } s ? new[] { s } : null, + SourceSiteIds: sourceSiteId is { } site ? new[] { site } : null, Target: TrimToNullable(query, "target"), Actor: TrimToNullable(query, "actor"), CorrelationId: correlationId, diff --git a/tests/ScadaLink.AuditLog.Tests/Integration/DatabaseSyncEmissionEndToEndTests.cs b/tests/ScadaLink.AuditLog.Tests/Integration/DatabaseSyncEmissionEndToEndTests.cs index 5a73256..21910f7 100644 --- a/tests/ScadaLink.AuditLog.Tests/Integration/DatabaseSyncEmissionEndToEndTests.cs +++ b/tests/ScadaLink.AuditLog.Tests/Integration/DatabaseSyncEmissionEndToEndTests.cs @@ -214,7 +214,7 @@ public class DatabaseSyncEmissionEndToEndTests : TestKit, IClassFixture(f => - f.Channel == AuditChannel.ApiOutbound && - f.Kind == AuditKind.ApiCall && - f.Status == AuditStatus.Failed && - f.SourceSiteId == "plant-a" && + f.Channels != null && f.Channels.Count == 1 && f.Channels[0] == AuditChannel.ApiOutbound && + f.Kinds != null && f.Kinds.Count == 1 && f.Kinds[0] == AuditKind.ApiCall && + f.Statuses != null && f.Statuses.Count == 1 && f.Statuses[0] == AuditStatus.Failed && + f.SourceSiteIds != null && f.SourceSiteIds.Count == 1 && f.SourceSiteIds[0] == "plant-a" && f.Target == "PaymentApi" && f.Actor == "apikey-1" && f.CorrelationId == Guid.Parse(correlationId) && @@ -188,10 +188,10 @@ public class AuditExportEndpointsTests await repo.Received().QueryAsync( Arg.Is(f => - f.Channel == null && - f.Kind == null && - f.Status == null && - f.SourceSiteId == null && + f.Channels == null && + f.Kinds == null && + f.Statuses == null && + f.SourceSiteIds == null && f.Target == null && f.Actor == null && f.CorrelationId == null && @@ -216,7 +216,7 @@ public class AuditExportEndpointsTests _ = await response.Content.ReadAsStringAsync(); await repo.Received().QueryAsync( - Arg.Is(f => f.Channel == null), + Arg.Is(f => f.Channels == null), Arg.Any(), Arg.Any()); } diff --git a/tests/ScadaLink.CentralUI.Tests/Components/Audit/AuditFilterBarTests.cs b/tests/ScadaLink.CentralUI.Tests/Components/Audit/AuditFilterBarTests.cs index 282c49c..0c8c623 100644 --- a/tests/ScadaLink.CentralUI.Tests/Components/Audit/AuditFilterBarTests.cs +++ b/tests/ScadaLink.CentralUI.Tests/Components/Audit/AuditFilterBarTests.cs @@ -77,7 +77,9 @@ public class AuditFilterBarTests : BunitContext cut.Find("[data-test=\"filter-apply\"]").Click(); Assert.NotNull(captured); - Assert.Equal(AuditChannel.ApiOutbound, captured!.Channel); + // Task 8: the filter dimension is multi-value now; ToFilter still collapses + // the chip selection to a single-element list (Task 9 widens that). + Assert.Equal(new[] { AuditChannel.ApiOutbound }, captured!.Channels); Assert.Equal("Plant-A-OPC", captured.Target); } @@ -117,14 +119,14 @@ public class AuditFilterBarTests : BunitContext cut.Find("[data-test=\"filter-apply\"]").Click(); Assert.NotNull(captured); - // Single-value filter contract: Failed leads the non-success set. - Assert.Equal(AuditStatus.Failed, captured!.Status); + // Single-value collapse contract (Task 8): Failed leads the non-success set. + Assert.Equal(new[] { AuditStatus.Failed }, captured!.Statuses); // Now pin an explicit Status chip — Errors-only must yield (chip wins). cut.Find("[data-test=\"chip-status-Delivered\"]").Click(); cut.Find("[data-test=\"filter-apply\"]").Click(); - Assert.Equal(AuditStatus.Delivered, captured!.Status); + Assert.Equal(new[] { AuditStatus.Delivered }, captured!.Statuses); } [Fact] diff --git a/tests/ScadaLink.CentralUI.Tests/Pages/AuditLogPageExportUrlTests.cs b/tests/ScadaLink.CentralUI.Tests/Pages/AuditLogPageExportUrlTests.cs index 136a929..6fad432 100644 --- a/tests/ScadaLink.CentralUI.Tests/Pages/AuditLogPageExportUrlTests.cs +++ b/tests/ScadaLink.CentralUI.Tests/Pages/AuditLogPageExportUrlTests.cs @@ -36,10 +36,10 @@ public class AuditLogPageExportUrlTests { var corr = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); var filter = new AuditLogQueryFilter( - Channel: AuditChannel.ApiOutbound, - Kind: AuditKind.ApiCall, - Status: AuditStatus.Failed, - SourceSiteId: "plant-a", + Channels: new[] { AuditChannel.ApiOutbound }, + Kinds: new[] { AuditKind.ApiCall }, + Statuses: new[] { AuditStatus.Failed }, + SourceSiteIds: new[] { "plant-a" }, Target: "PaymentApi", Actor: "apikey-1", CorrelationId: corr, @@ -65,7 +65,7 @@ public class AuditLogPageExportUrlTests [Fact] public void BuildExportUrl_OnlyChannelSet_OmitsOtherParams() { - var filter = new AuditLogQueryFilter(Channel: AuditChannel.Notification); + var filter = new AuditLogQueryFilter(Channels: new[] { AuditChannel.Notification }); var url = AuditLogPage.BuildExportUrl(filter); diff --git a/tests/ScadaLink.CentralUI.Tests/Pages/AuditLogPageScaffoldTests.cs b/tests/ScadaLink.CentralUI.Tests/Pages/AuditLogPageScaffoldTests.cs index 955c314..f117d20 100644 --- a/tests/ScadaLink.CentralUI.Tests/Pages/AuditLogPageScaffoldTests.cs +++ b/tests/ScadaLink.CentralUI.Tests/Pages/AuditLogPageScaffoldTests.cs @@ -197,7 +197,8 @@ public class AuditLogPageScaffoldTests : BunitContext cut.WaitForAssertion(() => { _queryService.Received().QueryAsync( - Arg.Is(f => f.SourceSiteId == "plant-a"), + Arg.Is(f => + f.SourceSiteIds != null && f.SourceSiteIds.Count == 1 && f.SourceSiteIds[0] == "plant-a"), Arg.Any(), Arg.Any()); }); @@ -218,7 +219,8 @@ public class AuditLogPageScaffoldTests : BunitContext cut.WaitForAssertion(() => { _queryService.Received().QueryAsync( - Arg.Is(f => f.Status == AuditStatus.Failed), + Arg.Is(f => + f.Statuses != null && f.Statuses.Count == 1 && f.Statuses[0] == AuditStatus.Failed), Arg.Any(), Arg.Any()); }); diff --git a/tests/ScadaLink.CentralUI.Tests/Services/AuditLogQueryServiceTests.cs b/tests/ScadaLink.CentralUI.Tests/Services/AuditLogQueryServiceTests.cs index 3e86a0d..03e0e82 100644 --- a/tests/ScadaLink.CentralUI.Tests/Services/AuditLogQueryServiceTests.cs +++ b/tests/ScadaLink.CentralUI.Tests/Services/AuditLogQueryServiceTests.cs @@ -34,7 +34,7 @@ public class AuditLogQueryServiceTests public async Task QueryAsync_ForwardsFilterAndPaging_ToRepository() { var repo = Substitute.For(); - var filter = new AuditLogQueryFilter(Channel: AuditChannel.ApiOutbound); + var filter = new AuditLogQueryFilter(Channels: new[] { AuditChannel.ApiOutbound }); var paging = new AuditLogPaging(PageSize: 25); var expected = new List { @@ -179,7 +179,7 @@ public class AuditLogQueryServiceTests var scopeFactory = provider.GetRequiredService(); var sut = new AuditLogQueryService(scopeFactory, EmptyAggregator()); - var filter = new AuditLogQueryFilter(Channel: AuditChannel.ApiOutbound); + var filter = new AuditLogQueryFilter(Channels: new[] { AuditChannel.ApiOutbound }); // Fire two QueryAsync calls in parallel. With scope-per-query each gets a // fresh DbContext, so this completes cleanly; with a shared scoped context diff --git a/tests/ScadaLink.ConfigurationDatabase.Tests/Repositories/AuditLogRepositoryTests.cs b/tests/ScadaLink.ConfigurationDatabase.Tests/Repositories/AuditLogRepositoryTests.cs index 775fb2e..0ee14f0 100644 --- a/tests/ScadaLink.ConfigurationDatabase.Tests/Repositories/AuditLogRepositoryTests.cs +++ b/tests/ScadaLink.ConfigurationDatabase.Tests/Repositories/AuditLogRepositoryTests.cs @@ -91,7 +91,7 @@ public class AuditLogRepositoryTests : IClassFixture await repo.InsertIfNotExistsAsync(NewEvent(siteId, occurredAtUtc: t0.AddMinutes(20))); var rows = await repo.QueryAsync( - new AuditLogQueryFilter(SourceSiteId: siteId), + new AuditLogQueryFilter(SourceSiteIds: new[] { siteId }), new AuditLogPaging(PageSize: 10)); Assert.Equal(3, rows.Count); @@ -114,13 +114,116 @@ public class AuditLogRepositoryTests : IClassFixture await repo.InsertIfNotExistsAsync(NewEvent(siteId, occurredAtUtc: t0.AddMinutes(2), channel: AuditChannel.Notification)); var rows = await repo.QueryAsync( - new AuditLogQueryFilter(Channel: AuditChannel.Notification, SourceSiteId: siteId), + new AuditLogQueryFilter( + Channels: new[] { AuditChannel.Notification }, + SourceSiteIds: new[] { siteId }), new AuditLogPaging(PageSize: 10)); Assert.Equal(2, rows.Count); Assert.All(rows, r => Assert.Equal(AuditChannel.Notification, r.Channel)); } + [SkippableFact] + public async Task QueryAsync_FilterByMultipleChannels_ReturnsUnion() + { + Skip.IfNot(_fixture.Available, _fixture.SkipReason); + + var siteId = NewSiteId(); + await using var context = CreateContext(); + var repo = new AuditLogRepository(context); + + var t0 = new DateTime(2026, 5, 2, 14, 0, 0, DateTimeKind.Utc); + // One row per channel; the multi-value filter must return the union of + // ApiOutbound + Notification and exclude DbOutbound. + await repo.InsertIfNotExistsAsync(NewEvent(siteId, occurredAtUtc: t0, channel: AuditChannel.ApiOutbound)); + await repo.InsertIfNotExistsAsync(NewEvent(siteId, occurredAtUtc: t0.AddMinutes(1), channel: AuditChannel.Notification)); + await repo.InsertIfNotExistsAsync(NewEvent(siteId, occurredAtUtc: t0.AddMinutes(2), channel: AuditChannel.DbOutbound)); + + var rows = await repo.QueryAsync( + new AuditLogQueryFilter( + Channels: new[] { AuditChannel.ApiOutbound, AuditChannel.Notification }, + SourceSiteIds: new[] { siteId }), + new AuditLogPaging(PageSize: 10)); + + Assert.Equal(2, rows.Count); + Assert.All(rows, r => Assert.Contains(r.Channel, new[] { AuditChannel.ApiOutbound, AuditChannel.Notification })); + Assert.DoesNotContain(rows, r => r.Channel == AuditChannel.DbOutbound); + } + + [SkippableFact] + public async Task QueryAsync_FilterByMultipleStatuses_ReturnsUnion() + { + Skip.IfNot(_fixture.Available, _fixture.SkipReason); + + var siteId = NewSiteId(); + await using var context = CreateContext(); + var repo = new AuditLogRepository(context); + + var t0 = new DateTime(2026, 5, 2, 15, 0, 0, DateTimeKind.Utc); + // Failed + Parked are requested; Delivered must be excluded. + await repo.InsertIfNotExistsAsync(NewEvent(siteId, occurredAtUtc: t0, status: AuditStatus.Failed)); + await repo.InsertIfNotExistsAsync(NewEvent(siteId, occurredAtUtc: t0.AddMinutes(1), status: AuditStatus.Parked)); + await repo.InsertIfNotExistsAsync(NewEvent(siteId, occurredAtUtc: t0.AddMinutes(2), status: AuditStatus.Delivered)); + + var rows = await repo.QueryAsync( + new AuditLogQueryFilter( + Statuses: new[] { AuditStatus.Failed, AuditStatus.Parked }, + SourceSiteIds: new[] { siteId }), + new AuditLogPaging(PageSize: 10)); + + Assert.Equal(2, rows.Count); + Assert.All(rows, r => Assert.Contains(r.Status, new[] { AuditStatus.Failed, AuditStatus.Parked })); + Assert.DoesNotContain(rows, r => r.Status == AuditStatus.Delivered); + } + + [SkippableFact] + public async Task QueryAsync_FilterByMultipleSourceSiteIds_ReturnsUnion() + { + Skip.IfNot(_fixture.Available, _fixture.SkipReason); + + var siteA = NewSiteId(); + var siteB = NewSiteId(); + var siteC = NewSiteId(); + await using var context = CreateContext(); + var repo = new AuditLogRepository(context); + + var t0 = new DateTime(2026, 5, 2, 16, 0, 0, DateTimeKind.Utc); + await repo.InsertIfNotExistsAsync(NewEvent(siteA, occurredAtUtc: t0)); + await repo.InsertIfNotExistsAsync(NewEvent(siteB, occurredAtUtc: t0.AddMinutes(1))); + await repo.InsertIfNotExistsAsync(NewEvent(siteC, occurredAtUtc: t0.AddMinutes(2))); + + var rows = await repo.QueryAsync( + new AuditLogQueryFilter(SourceSiteIds: new[] { siteA, siteB }), + new AuditLogPaging(PageSize: 10)); + + Assert.Equal(2, rows.Count); + Assert.All(rows, r => Assert.Contains(r.SourceSiteId, new[] { siteA, siteB })); + Assert.DoesNotContain(rows, r => r.SourceSiteId == siteC); + } + + [SkippableFact] + public async Task QueryAsync_EmptyChannelList_DoesNotConstrain() + { + Skip.IfNot(_fixture.Available, _fixture.SkipReason); + + var siteId = NewSiteId(); + await using var context = CreateContext(); + var repo = new AuditLogRepository(context); + + var t0 = new DateTime(2026, 5, 2, 17, 0, 0, DateTimeKind.Utc); + await repo.InsertIfNotExistsAsync(NewEvent(siteId, occurredAtUtc: t0, channel: AuditChannel.ApiOutbound)); + await repo.InsertIfNotExistsAsync(NewEvent(siteId, occurredAtUtc: t0.AddMinutes(1), channel: AuditChannel.Notification)); + + // An empty Channels list must mean "no filter" — NOT WHERE 1=0. + var rows = await repo.QueryAsync( + new AuditLogQueryFilter( + Channels: Array.Empty(), + SourceSiteIds: new[] { siteId }), + new AuditLogPaging(PageSize: 10)); + + Assert.Equal(2, rows.Count); + } + [SkippableFact] public async Task QueryAsync_FilterBySourceSiteId() { @@ -137,7 +240,7 @@ public class AuditLogRepositoryTests : IClassFixture await repo.InsertIfNotExistsAsync(NewEvent(otherSiteId, occurredAtUtc: t0.AddMinutes(2))); var rows = await repo.QueryAsync( - new AuditLogQueryFilter(SourceSiteId: siteId), + new AuditLogQueryFilter(SourceSiteIds: new[] { siteId }), new AuditLogPaging(PageSize: 10)); Assert.Equal(2, rows.Count); @@ -160,7 +263,7 @@ public class AuditLogRepositoryTests : IClassFixture var rows = await repo.QueryAsync( new AuditLogQueryFilter( - SourceSiteId: siteId, + SourceSiteIds: new[] { siteId }, FromUtc: t0.AddMinutes(10), ToUtc: t0.AddHours(1)), new AuditLogPaging(PageSize: 10)); @@ -187,7 +290,7 @@ public class AuditLogRepositoryTests : IClassFixture } var page1 = await repo.QueryAsync( - new AuditLogQueryFilter(SourceSiteId: siteId), + new AuditLogQueryFilter(SourceSiteIds: new[] { siteId }), new AuditLogPaging(PageSize: 2)); Assert.Equal(2, page1.Count); @@ -196,7 +299,7 @@ public class AuditLogRepositoryTests : IClassFixture var cursor = page1[^1]; var page2 = await repo.QueryAsync( - new AuditLogQueryFilter(SourceSiteId: siteId), + new AuditLogQueryFilter(SourceSiteIds: new[] { siteId }), new AuditLogPaging( PageSize: 2, AfterOccurredAtUtc: cursor.OccurredAtUtc, @@ -208,7 +311,7 @@ public class AuditLogRepositoryTests : IClassFixture var cursor2 = page2[^1]; var page3 = await repo.QueryAsync( - new AuditLogQueryFilter(SourceSiteId: siteId), + new AuditLogQueryFilter(SourceSiteIds: new[] { siteId }), new AuditLogPaging( PageSize: 2, AfterOccurredAtUtc: cursor2.OccurredAtUtc, @@ -281,7 +384,7 @@ public class AuditLogRepositoryTests : IClassFixture await repo.InsertIfNotExistsAsync(e); } - var filter = new AuditLogQueryFilter(SourceSiteId: siteId); + var filter = new AuditLogQueryFilter(SourceSiteIds: new[] { siteId }); var page1 = await repo.QueryAsync(filter, new AuditLogPaging(PageSize: 2)); Assert.Equal(2, page1.Count);