using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Uns; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Security.Auth; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Raw; /// /// R04 P6: the CSV import grid renders through Virtualize from a precomputed view, and the export is /// downloaded from an endpoint (bounded preview in the modal) instead of being embedded in the circuit. /// [Trait("Category", "Unit")] public sealed class RawCsvRenderingTests { private static RawCsvParseResult Result(params (int Row, bool Ok)[] rows) => new(["Name"], rows.Select(r => new RawCsvReviewRow { RowNumber = r.Row, Name = $"t{r.Row}", Error = r.Ok ? null : "bad", ImportRow = r.Ok ? new RawTagImportRow(null, new RawTagInput($"t{r.Row}", "Int32", Configuration.Enums.TagAccessLevel.Read, false, null, "{}")) : null, }).ToList(), null); [Fact] public void Review_view_counts_and_filters_once_per_change() { var view = new RawCsvReviewView(); view.Set(Result((1, true), (2, false), (3, true))); view.OkCount.ShouldBe(2); view.ErrorCount.ShouldBe(1); view.RemainingCount.ShouldBe(3); view.Remove(2); view.ErrorCount.ShouldBe(0); view.VisibleRows.Select(r => r.RowNumber).ShouldBe([1, 3]); view.CommitRows().Select(r => r.Tag.Name).ShouldBe(["t1", "t3"]); view.Set(null); view.RemainingCount.ShouldBe(0); // Positive control: a fresh set after a clear shows every row again (removals are forgotten). view.Set(Result((1, true), (2, false))); view.RemainingCount.ShouldBe(2); } [Fact] public void Preview_is_bounded_and_flags_truncation() { var csv = string.Concat(Enumerable.Range(0, 200).Select(i => $"row{i}\n")); var (preview, truncated) = RawTagCsvExport.Preview(csv, maxLines: 50); truncated.ShouldBeTrue(); preview.Split('\n', StringSplitOptions.RemoveEmptyEntries).Length.ShouldBe(50); var (all, cut) = RawTagCsvExport.Preview("a\nb\n", maxLines: 50); cut.ShouldBeFalse(); all.ShouldBe("a\nb\n"); } [Fact] public async Task Preview_build_maps_only_the_first_rows_but_counts_all() { // Review r0 M8: the modal maps only its preview's rows; the full count still comes back. var dbName = Uns.RawTreeTestDb.SeedFresh(); var reader = new RawTagCsvExportReader(Uns.RawTreeTestDb.Factory(dbName)); var full = await RawTagCsvExport.BuildAsync(reader, Uns.RawTreeTestDb.FolderedDeviceId, null, "Modbus", TestContext.Current.CancellationToken); var preview = await RawTagCsvExport.BuildAsync(reader, Uns.RawTreeTestDb.FolderedDeviceId, null, "Modbus", ct: TestContext.Current.CancellationToken, maxRows: 1); full!.RowCount.ShouldBe(2); Commons.Csv.CsvParser.Parse(full.Csv).Count.ShouldBe(3); // header + 2 (positive control) preview!.RowCount.ShouldBe(2); Commons.Csv.CsvParser.Parse(preview.Csv).Count.ShouldBe(2); // header + 1 } [Fact] public void Attachment_header_carries_an_rfc5987_name_for_non_ascii_ids() { var header = RawTagCsvExport.AttachmentHeader(RawTagCsvExport.BuildFileName("Prüfstand")); header.ShouldStartWith("attachment"); header.ShouldContain("filename*=UTF-8''tags-Pr%C3%BCfstand.csv"); // Positive control: an ASCII name is still a plain filename. RawTagCsvExport.AttachmentHeader("tags-dev1.csv").ShouldContain("filename=tags-dev1.csv"); } [Fact] public void Download_url_escapes_its_query_values() { RawTagCsvExport.DownloadUrl("dev 1&x", null, "Modbus") .ShouldBe("/api/raw/tags.csv?deviceId=dev%201%26x&driverType=Modbus"); RawTagCsvExport.DownloadUrl("", "TG/1", null).ShouldBe("/api/raw/tags.csv?tagGroupId=TG%2F1"); } [Fact] public void Download_endpoint_requires_the_same_ConfigEditor_policy_as_the_raw_page() { var builder = WebApplication.CreateBuilder(); builder.Services.AddDbContextFactory(o => o.UseInMemoryDatabase("csv-export-metadata")); builder.Services.AddScoped(); var app = builder.Build(); app.MapRawTagCsvExport(); var endpoint = ((IEndpointRouteBuilder)app).DataSources.SelectMany(d => d.Endpoints).OfType() .Single(e => e.RoutePattern.RawText == RawTagCsvExport.DownloadPath); endpoint.Metadata.GetMetadata().ShouldBeNull(); endpoint.Metadata.GetOrderedMetadata().Select(a => a.Policy) .ShouldContain(AdminUiPolicies.ConfigEditor); // Parity with the page that links to it. typeof(ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Raw.GlobalRaw) .GetCustomAttributes(typeof(AuthorizeAttribute), false).Cast() .Single().Policy.ShouldBe(AdminUiPolicies.ConfigEditor); } [Fact] public void Modals_no_longer_embed_the_csv_or_render_every_row() { var export = AdminUiSource.Read("Components", "Shared", "Raw", "RawCsvExportModal.razor"); export.ShouldNotContain("data:text/csv"); export.ShouldContain("RawTagCsvExport.Preview("); var import = AdminUiSource.Read("Components", "Shared", "Raw", "RawCsvImportModal.razor"); import.ShouldContain("