using Bunit; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using NSubstitute; using ScadaLink.CentralUI.Components.Audit; using ScadaLink.CentralUI.Services; using ScadaLink.Commons.Entities.Audit; using ScadaLink.Commons.Types.Audit; using ScadaLink.Commons.Types.Enums; namespace ScadaLink.CentralUI.Tests.Components.Audit; /// /// bUnit tests for (#23 M7-T3 / Bundle B). The grid /// renders 10 columns, paginates via keyset (passing the last row's /// (OccurredAtUtc, EventId) back to the service), raises a row-click callback /// that Bundle C wires to the drilldown drawer, and styles non-success status /// rows with an error-coded badge. /// public class AuditResultsGridTests : BunitContext { private readonly IAuditLogQueryService _service; private readonly List<(AuditLogQueryFilter Filter, AuditLogPaging? Paging)> _calls = new(); private static AuditEvent MakeEvent(DateTime occurredAtUtc, AuditStatus status, AuditChannel channel = AuditChannel.ApiOutbound, AuditKind kind = AuditKind.ApiCall, string? site = "plant-a") => new() { EventId = Guid.NewGuid(), OccurredAtUtc = occurredAtUtc, Channel = channel, Kind = kind, Status = status, SourceSiteId = site, Target = "demo-target", Actor = "tester", DurationMs = 42, HttpStatus = status == AuditStatus.Delivered ? 200 : 500, ErrorMessage = status == AuditStatus.Failed ? "boom — unreachable" : null, }; public AuditResultsGridTests() { _service = Substitute.For(); _service.DefaultPageSize.Returns(100); Services.AddSingleton(_service); } private void StubPage(IReadOnlyList rows) { _service.QueryAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(callInfo => { _calls.Add(((AuditLogQueryFilter)callInfo[0], (AuditLogPaging?)callInfo[1])); return Task.FromResult(rows); }); } [Fact] public void Render_TenColumns_FromStubService() { StubPage(new List { MakeEvent(DateTime.UtcNow.AddMinutes(-1), AuditStatus.Delivered), }); var cut = Render(p => p.Add(c => c.Filter, new AuditLogQueryFilter())); // 10 column headers per Component-AuditLog.md §10. var expectedHeaders = new[] { "OccurredAtUtc", "Site", "Channel", "Kind", "Status", "Target", "Actor", "DurationMs", "HttpStatus", "ErrorMessage", }; foreach (var header in expectedHeaders) { Assert.Contains($"data-test=\"col-header-{header}\"", cut.Markup); } } [Fact] public void Click_NextPage_CallsService_WithCursor_OfLastRow() { // First page: two rows, descending by OccurredAtUtc. The grid must pass the // LAST row (the older one) back as the keyset cursor for the next page. var first = MakeEvent(new DateTime(2026, 5, 20, 12, 0, 0, DateTimeKind.Utc), AuditStatus.Delivered); var second = MakeEvent(new DateTime(2026, 5, 20, 11, 30, 0, DateTimeKind.Utc), AuditStatus.Failed); StubPage(new[] { first, second }); var cut = Render(p => p.Add(c => c.Filter, new AuditLogQueryFilter())); cut.Find("[data-test=\"grid-next-page\"]").Click(); // Two service calls: initial + next. Assert.Equal(2, _calls.Count); var nextCall = _calls[1]; Assert.NotNull(nextCall.Paging); Assert.Equal(second.OccurredAtUtc, nextCall.Paging!.AfterOccurredAtUtc); Assert.Equal(second.EventId, nextCall.Paging.AfterEventId); } [Fact] public void Click_Row_RaisesOnRowSelected() { var target = MakeEvent(DateTime.UtcNow.AddMinutes(-5), AuditStatus.Delivered); StubPage(new[] { target }); AuditEvent? captured = null; var cut = Render(p => p .Add(c => c.Filter, new AuditLogQueryFilter()) .Add(c => c.OnRowSelected, EventCallback.Factory.Create(this, e => captured = e))); cut.Find($"[data-test=\"grid-row-{target.EventId}\"]").Click(); Assert.NotNull(captured); Assert.Equal(target.EventId, captured!.EventId); } [Fact] public void Status_FailedRow_HasErrorBadgeClass() { var failed = MakeEvent(DateTime.UtcNow.AddMinutes(-2), AuditStatus.Failed); var delivered = MakeEvent(DateTime.UtcNow.AddMinutes(-1), AuditStatus.Delivered); StubPage(new[] { delivered, failed }); var cut = Render(p => p.Add(c => c.Filter, new AuditLogQueryFilter())); // Failed badge => bg-danger (red). Delivered => bg-success (green). var failedBadge = cut.Find($"[data-test=\"status-badge-{failed.EventId}\"]"); Assert.Contains("bg-danger", failedBadge.GetAttribute("class") ?? string.Empty); var deliveredBadge = cut.Find($"[data-test=\"status-badge-{delivered.EventId}\"]"); Assert.Contains("bg-success", deliveredBadge.GetAttribute("class") ?? string.Empty); } }