using System.IO; using System.Reflection; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Admin.Tests; /// /// Regression for Admin-010 — admin-ui.md "Tech Stack" requires Bootstrap 5 /// "vendored under wwwroot/lib/bootstrap/" so the Admin app has no third-party /// runtime dependency and works in air-gapped fleet deployments. These tests /// guard against a future re-introduction of the cdn.jsdelivr.net references /// in App.razor. /// [Trait("Category", "Unit")] public sealed class BootstrapVendoringTests { [Fact] public void AppRazor_does_not_reference_a_remote_CDN_for_bootstrap() { var appRazor = File.ReadAllText(ResolveAdminPath("Components/App.razor")); appRazor.ShouldNotContain("cdn.jsdelivr.net", customMessage: "Admin-010: Bootstrap must be served from the vendored copy under wwwroot/lib/bootstrap/, not jsDelivr — air-gapped deployments cannot reach the public CDN."); appRazor.ShouldNotContain("cdnjs.cloudflare.com", customMessage: "Admin-010: third-party CDN references regress the vendoring requirement."); appRazor.ShouldNotContain("unpkg.com", customMessage: "Admin-010: third-party CDN references regress the vendoring requirement."); } [Fact] public void AppRazor_references_vendored_bootstrap_assets() { var appRazor = File.ReadAllText(ResolveAdminPath("Components/App.razor")); appRazor.ShouldContain("lib/bootstrap/css/bootstrap.min.css", customMessage: "App.razor must load the vendored Bootstrap stylesheet."); appRazor.ShouldContain("lib/bootstrap/js/bootstrap.bundle.min.js", customMessage: "App.razor must load the vendored Bootstrap JS bundle."); } [Fact] public void Vendored_bootstrap_assets_exist_under_wwwroot_lib_bootstrap() { var root = ResolveAdminPath("wwwroot/lib/bootstrap"); Directory.Exists(root).ShouldBeTrue($"expected vendored bootstrap directory at '{root}'"); File.Exists(Path.Combine(root, "css", "bootstrap.min.css")).ShouldBeTrue("bootstrap.min.css missing"); File.Exists(Path.Combine(root, "js", "bootstrap.bundle.min.js")).ShouldBeTrue("bootstrap.bundle.min.js missing"); // Sanity-check non-empty (a zero-byte placeholder would still pass File.Exists). new FileInfo(Path.Combine(root, "css", "bootstrap.min.css")).Length.ShouldBeGreaterThan(100_000); new FileInfo(Path.Combine(root, "js", "bootstrap.bundle.min.js")).Length.ShouldBeGreaterThan(50_000); } /// Resolve a path under the Admin source project from the test runner's bin folder. private static string ResolveAdminPath(string relative) { var asmDir = Path.GetDirectoryName(typeof(BootstrapVendoringTests).Assembly.Location)!; // tests/Server/ZB.MOM.WW.OtOpcUa.Admin.Tests/bin/Debug/net10.0 -> ../../../../../src/Server/... var repoRoot = Path.GetFullPath(Path.Combine(asmDir, "..", "..", "..", "..", "..", "..")); return Path.Combine(repoRoot, "src", "Server", "ZB.MOM.WW.OtOpcUa.Admin", relative.Replace('/', Path.DirectorySeparatorChar)); } }