File: StaticWebAssetEndpointsIntegrationTest.cs
Web Access
Project: ..\..\..\test\Microsoft.NET.Sdk.StaticWebAssets.Tests\Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj (Microsoft.NET.Sdk.StaticWebAssets.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System.Diagnostics.Eventing.Reader;
using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.StaticWebAssets.Tasks;
 
namespace Microsoft.NET.Sdk.StaticWebAssets.Tests;
 
public partial class StaticWebAssetEndpointsIntegrationTest(ITestOutputHelper log)
    : AspNetSdkBaselineTest(log, GenerateBaselines)
{
    [GeneratedRegex("""(?'project'[a-zA-Z0-9]+)(?:\.(?'fingerprint'[a-zA-Z0-9]*))?\.bundle\.scp\.css(?'compress'\.(?:gz|br))?$""")]
    private static partial Regex ProjectBundleRegex();
 
    [GeneratedRegex("""(?'project'[a-zA-Z0-9]+)(?:\.(?'fingerprint'[a-zA-Z0-9]*))?\.styles\.css(?'compress'\.(?:gz|br))?$""")]
    private static partial Regex AppBundleRegex();
 
    [Fact]
    public void Build_CreatesEndpointsForAssets()
    {
        ProjectDirectory = CreateAspNetSdkTestAsset("RazorComponentApp");
        var root = ProjectDirectory.TestRoot;
 
        var dir = Directory.CreateDirectory(Path.Combine(root, "wwwroot"));
        File.WriteAllText(Path.Combine(dir.FullName, "app.js"), "console.log('hello world!');");
 
        var build = CreateBuildCommand(ProjectDirectory);
        ExecuteCommand(build).Should().Pass();
 
        var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
        var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString();
 
        // GenerateStaticWebAssetsManifest should generate the manifest file.
        var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json");
        new FileInfo(path).Should().Exist();
        var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path));
 
        var endpoints = manifest.Endpoints;
        // blazor.server.js and blazor.web.js assets and endpoints are included automatically
        // based on the presence of .razor files in projects referencing the web SDK.
        // In the future we will filter these out based on whether the app references the Endpoints or the Server
        // assemblies, but for now, just account for them in the tests and ignore them.
        endpoints.Should().HaveCount(27);
        var appJsEndpoints = endpoints.Where(ep => ep.Route.EndsWith("app.js"));
        appJsEndpoints.Should().HaveCount(2);
        var appJsGzEndpoints = endpoints.Where(ep => ep.Route.EndsWith("app.js.gz"));
        appJsGzEndpoints.Should().HaveCount(1);
 
        // project bundle endpoints
        var bundleEndpoints = endpoints.Where(MatchUncompresedProjectBundlesNoFingerprint);
        bundleEndpoints.Should().HaveCount(2);
 
        var bundleGzEndpoints = endpoints.Where(MatchCompressedProjectBundlesNoFingerprint);
        bundleGzEndpoints.Should().HaveCount(1);
 
        var fingerprintedBundleGzEndpoints = endpoints.Where(MatchCompressedProjectBundlesWithFingerprint);
        fingerprintedBundleGzEndpoints.Should().HaveCount(1);
 
        var fingerprintedBundles = endpoints.Where(MatchUncompressedProjectBundlesWithFingerprint);
        fingerprintedBundles.Should().HaveCount(2);
 
        // app bundle endpoints
        var appBundleEndpoints = endpoints.Where(MatchUncompressedAppBundleNoFingerprint);
        appBundleEndpoints.Should().HaveCount(2);
 
        var appBundleGzEndpoints = endpoints.Where(MatchCompressedAppBundleNoFingerprint);
        appBundleGzEndpoints.Should().HaveCount(1);
 
        var fingerprintedAppBundle = endpoints.Where(MatchUncompressedAppBundleWithFingerprint);
        fingerprintedAppBundle.Should().HaveCount(2);
 
        var fingerprintedAppBundleGz = endpoints.Where(MatchCompressedAppBundleWithFingerprint);
        fingerprintedAppBundleGz.Should().HaveCount(1);
 
        AssertManifest(manifest, LoadBuildManifest());
    }
 
    private bool MatchUncompresedProjectBundlesNoFingerprint(StaticWebAssetEndpoint ep) => ProjectBundleRegex().Match(ep.Route) is
    {
        Success: true,
        Groups: [
            var _,
            { Name: "project", Value: "ComponentApp", Success: true, },
            { Name: "fingerprint", Value: "", Success: false },
            { Name: "compress", Value: "", Success: false }
        ]
    };
 
    private bool MatchCompressedProjectBundlesNoFingerprint(StaticWebAssetEndpoint ep) => ProjectBundleRegex().Match(ep.Route) is
    {
        Success: true,
        Groups: [
            var _,
            { Name: "project", Value: "ComponentApp", Success: true, },
            { Name: "fingerprint", Value: "", Success: false },
            { Name: "compress", Value: var compress, Success: true }
        ]
    } && (compress == ".gz" || compress == ".br");
 
    private bool MatchUncompressedProjectBundlesWithFingerprint(StaticWebAssetEndpoint ep) => ProjectBundleRegex().Match(ep.Route) is
    {
        Success: true,
        Groups: [
            var m,
            { Name: "project", Value: "ComponentApp", Success: true, },
            { Name: "fingerprint", Value: var fingerprint, Success: true },
            { Name: "compress", Value: "", Success: false }
        ]
    } && fingerprint == ep.EndpointProperties.Single(p => p.Name == "fingerprint").Value;
 
    private bool MatchCompressedProjectBundlesWithFingerprint(StaticWebAssetEndpoint ep) => ProjectBundleRegex().Match(ep.Route) is
    {
        Success: true,
        Groups: [
            var m,
            { Name: "project", Value: "ComponentApp", Success: true, },
            { Name: "fingerprint", Value: var fingerprint, Success: true },
            { Name: "compress", Value: var compress, Success: true }
        ]
    } && !string.IsNullOrWhiteSpace(fingerprint)
      && (compress == ".gz" || compress == ".br");
 
    private bool MatchUncompressedAppBundleNoFingerprint(StaticWebAssetEndpoint ep) => AppBundleRegex().Match(ep.Route) is
    {
        Success: true,
        Groups: [
            var _,
            { Name: "project", Value: "ComponentApp", Success: true, },
            { Name: "fingerprint", Value: "", Success: false },
            { Name: "compress", Value: "", Success: false }
        ]
    };
 
    private bool MatchCompressedAppBundleNoFingerprint(StaticWebAssetEndpoint ep) => AppBundleRegex().Match(ep.Route) is
    {
        Success: true,
        Groups: [
            var _,
            { Name: "project", Value: "ComponentApp", Success: true, },
            { Name: "fingerprint", Value: "", Success: false },
            { Name: "compress", Value: var compress, Success: true }
        ]
    } && (compress == ".gz" || compress == ".br");
 
    private bool MatchUncompressedAppBundleWithFingerprint(StaticWebAssetEndpoint ep) => AppBundleRegex().Match(ep.Route) is
    {
        Success: true,
        Groups: [
            var m,
            { Name: "project", Value: "ComponentApp", Success: true, },
            { Name: "fingerprint", Value: var fingerprint, Success: true },
            { Name: "compress", Value: "", Success: false }
        ]
    } && fingerprint == ep.EndpointProperties.Single(p => p.Name == "fingerprint").Value;
 
    private bool MatchCompressedAppBundleWithFingerprint(StaticWebAssetEndpoint ep) => AppBundleRegex().Match(ep.Route) is
    {
        Success: true,
        Groups: [
            var m,
            { Name: "project", Value: "ComponentApp", Success: true, },
            { Name: "fingerprint", Value: var fingerprint, Success: true },
            { Name: "compress", Value: var compress, Success: true }
        ]
    } && !string.IsNullOrWhiteSpace(fingerprint)
      && (compress == ".gz" || compress == ".br");
 
    [Fact]
    public void Publish_CreatesEndpointsForAssets()
    {
        ProjectDirectory = CreateAspNetSdkTestAsset("RazorComponentApp");
        var root = ProjectDirectory.TestRoot;
 
        var dir = Directory.CreateDirectory(Path.Combine(root, "wwwroot"));
        File.WriteAllText(Path.Combine(dir.FullName, "app.js"), "console.log('hello world!');");
 
        var publish = CreatePublishCommand(ProjectDirectory);
        ExecuteCommand(publish).Should().Pass();
 
        var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
        var outputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString();
 
        // GenerateStaticWebAssetsManifest should generate the manifest file.
        var path = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json");
        new FileInfo(path).Should().Exist();
        var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path));
 
        var endpoints = manifest.Endpoints;
 
        foreach (var endpoint in endpoints)
        {
            var contentLength = endpoint.ResponseHeaders.Single(rh => rh.Name == "Content-Length");
            var length = long.Parse(contentLength.Value, CultureInfo.InvariantCulture);
            var file = new FileInfo(endpoint.AssetFile);
            file.Should().Exist();
            file.Length.Should().Be(length, $"because {endpoint.Route} {file.FullName}");
        }
 
        // blazor.server.js and blazor.web.js assets and endpoints are included automatically
        // based on the presence of .razor files in projects referencing the web SDK.
        // In the future we will filter these out based on whether the app references the Endpoints or the Server
        // assemblies, but for now, just account for them in the tests and ignore them.
        endpoints.Should().HaveCount(45);
        var appJsEndpoints = endpoints.Where(ep => ep.Route.EndsWith("app.js"));
        appJsEndpoints.Should().HaveCount(3);
        var appJsGzEndpoints = endpoints.Where(ep => ep.Route.EndsWith("app.js.gz"));
        appJsGzEndpoints.Should().HaveCount(1);
        var appJsBrEndpoints = endpoints.Where(ep => ep.Route.EndsWith("app.js.br"));
        appJsBrEndpoints.Should().HaveCount(1);
 
        var uncompressedAppJsEndpoint = appJsEndpoints.Where(ep => ep.Selectors.Length == 0);
        uncompressedAppJsEndpoint.Should().HaveCount(1);
        uncompressedAppJsEndpoint.Single().ResponseHeaders.Select(h => h.Name).Should().BeEquivalentTo(
            [
                "Cache-Control",
                "Content-Length",
                "Content-Type",
                "ETag",
                "Last-Modified",
                "Vary",
            ]
        );
 
        var eTagHeader = uncompressedAppJsEndpoint.Single().ResponseHeaders.Single(h => h.Name == "ETag");
 
        var gzipCompressedAppJsEndpoint = appJsEndpoints.Where(ep => ep.Selectors.Length == 1 && ep.Selectors[0].Value == "gzip");
        gzipCompressedAppJsEndpoint.Should().HaveCount(1);
        gzipCompressedAppJsEndpoint.Single().ResponseHeaders.Select(h => h.Name).Should().BeEquivalentTo(
            [
                "Cache-Control",
                "Content-Length",
                "Content-Type",
                "ETag",
                "Last-Modified",
                "Content-Encoding",
                "Vary",
            ]
        );
 
        var brotliCompressedAppJsEndpoint = appJsEndpoints.Where(ep => ep.Selectors.Length == 1 && ep.Selectors[0].Value == "br");
        brotliCompressedAppJsEndpoint.Should().HaveCount(1);
        brotliCompressedAppJsEndpoint.Single().ResponseHeaders.Select(h => h.Name).Should().BeEquivalentTo(
            [
                "Cache-Control",
                "Content-Length",
                "Content-Type",
                "ETag",
                "Last-Modified",
                "Content-Encoding",
                "Vary",
            ]
        );
 
        var bundleEndpoints = endpoints.Where(MatchUncompresedProjectBundlesNoFingerprint);
        bundleEndpoints.Should().HaveCount(3);
        var bundleGzEndpoints = endpoints.Where(MatchCompressedProjectBundlesNoFingerprint).Where(ep => ep.Route.EndsWith(".gz"));
        bundleGzEndpoints.Should().HaveCount(1);
        var bundleBrEndpoints = endpoints.Where(MatchCompressedProjectBundlesNoFingerprint).Where(ep => ep.Route.EndsWith(".br"));
        bundleBrEndpoints.Should().HaveCount(1);
        var fingerprintedBundleGzEndpoints = endpoints.Where(MatchCompressedProjectBundlesWithFingerprint).Where(ep => ep.Route.EndsWith(".gz"));
        fingerprintedBundleGzEndpoints.Should().HaveCount(1);
        var fingerprintedBundleBrEndpoints = endpoints.Where(MatchCompressedProjectBundlesWithFingerprint).Where(ep => ep.Route.EndsWith(".br"));
        fingerprintedBundleBrEndpoints.Should().HaveCount(1);
 
        var fingerprintedBundleEndpoints = endpoints.Where(MatchUncompressedProjectBundlesWithFingerprint);
        fingerprintedBundleEndpoints.Should().HaveCount(3);
 
        var appBundleEndpoints = endpoints.Where(MatchUncompressedAppBundleNoFingerprint);
        appBundleEndpoints.Should().HaveCount(3);
        var appBundleGzEndpoints = endpoints.Where(MatchCompressedAppBundleNoFingerprint).Where(ep => ep.Route.EndsWith(".gz"));
        appBundleGzEndpoints.Should().HaveCount(1);
        var appBundleBrEndpoints = endpoints.Where(MatchCompressedAppBundleNoFingerprint).Where(ep => ep.Route.EndsWith(".br"));
        appBundleBrEndpoints.Should().HaveCount(1);
        var fingerprintedAppBundleGzEndpoints = endpoints.Where(MatchCompressedAppBundleWithFingerprint).Where(ep => ep.Route.EndsWith(".gz"));
        fingerprintedAppBundleGzEndpoints.Should().HaveCount(1);
        var fingerprintedAppBundleBrEndpoints = endpoints.Where(MatchCompressedAppBundleWithFingerprint).Where(ep => ep.Route.EndsWith(".br"));
        fingerprintedAppBundleBrEndpoints.Should().HaveCount(1);
 
        var fingerprintedAppBundleEndpoints = endpoints.Where(MatchUncompressedAppBundleWithFingerprint);
        fingerprintedAppBundleEndpoints.Should().HaveCount(3);
 
        AssertManifest(manifest, LoadPublishManifest());
    }
 
    [Fact]
    public void Publish_CreatesEndpointsForAssets_BuildAndPublish_Assets()
    {
        ProjectDirectory = CreateAspNetSdkTestAsset("RazorComponentApp")
            .WithProjectChanges(document =>
            {
                document.Root.AddFirst(
                    new XElement("ItemGroup",
                        new XElement("Content",
                            new XAttribute("Update", "wwwroot/app.js"),
                            new XAttribute("CopyToPublishDirectory", "Never")),
                        new XElement("Content",
                            new XAttribute("Update", "wwwroot/app.publish.js"),
                            new XAttribute("TargetPath", "wwwroot/app.js"),
                            new XAttribute("CopyToPublishDirectory", "PreserveNewest"))));
                var doc2 = document;
            });
        var root = ProjectDirectory.TestRoot;
 
        var dir = Directory.CreateDirectory(Path.Combine(root, "wwwroot"));
        File.WriteAllText(Path.Combine(dir.FullName, "app.js"), "console.log('hello world!');");
        File.WriteAllText(Path.Combine(dir.FullName, "app.publish.js"), "console.log('publish hello world!');");
 
        var publish = CreatePublishCommand(ProjectDirectory);
        ExecuteCommand(publish).Should().Pass();
 
        var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString();
        var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString();
 
        // GenerateStaticWebAssetsManifest should generate the manifest file.
        var path = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json");
        new FileInfo(path).Should().Exist();
        var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path));
        AssertManifest(buildManifest, LoadPublishManifest());
 
        var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.publish.json")));
 
        var endpoints = publishManifest.Endpoints;
 
        var appJsEndpoints = endpoints.Where(ep => ep.Route.EndsWith("app.js"));
        appJsEndpoints.Should().HaveCount(3);
 
        // There's only 1 uncompressed asset endpoint.
        var unCompressedAssetEndpoint = appJsEndpoints.Where(ep => ep.Selectors.Length == 0);
        unCompressedAssetEndpoint.Should().HaveCount(1);
 
        // The uncompressed asset endpoint is for the publish asset.
        var publishAsset = publishManifest.Assets.Where(a => a.Identity == unCompressedAssetEndpoint.Single().AssetFile);
        publishAsset.Should().HaveCount(1);
 
        // There is only 1 gzip asset endpoint.
        var appGzAssetEndpoint = appJsEndpoints.Where(ep => ep.Selectors.Length == 1 && ep.Selectors[0].Value == "gzip");
        appGzAssetEndpoint.Should().HaveCount(1);
 
        // The gzip asset endpoint is for the gzip compressed version of the publish asset.
        var publishGzAsset = publishManifest.Assets.Where(a => a.Identity == appGzAssetEndpoint.Single().AssetFile);
        publishGzAsset.Should().HaveCount(1);
        publishGzAsset.Single().RelatedAsset.Should().Be(publishAsset.Single().Identity);
 
        // There is only 1 br asset endpoint.
        var appBrAssetEndpoint = appJsEndpoints.Where(ep => ep.Selectors.Length == 1 && ep.Selectors[0].Value == "br");
        appBrAssetEndpoint.Should().HaveCount(1);
 
        // The br asset endpoint is for the br compressed version of the publish asset.
        var publishBrAsset = publishManifest.Assets.Where(a => a.Identity == appBrAssetEndpoint.Single().AssetFile);
        publishBrAsset.Should().HaveCount(1);
        publishBrAsset.Single().RelatedAsset.Should().Be(publishAsset.Single().Identity);
 
        // The compressed gzip and br assets are exposed with their extensions.
        var appJsGzEndpoints = endpoints.Where(ep => ep.Route.EndsWith("app.js.gz"));
        appJsGzEndpoints.Should().HaveCount(1);
 
        var appJsBrEndpoints = endpoints.Where(ep => ep.Route.EndsWith("app.js.br"));
        appJsBrEndpoints.Should().HaveCount(1);
 
        var bundleEndpoints = endpoints.Where(MatchUncompresedProjectBundlesNoFingerprint);
        bundleEndpoints.Should().HaveCount(3);
        var bundleGzEndpoints = endpoints.Where(MatchCompressedProjectBundlesNoFingerprint).Where(ep => ep.Route.EndsWith(".gz"));
        bundleGzEndpoints.Should().HaveCount(1);
        var bundleBrEndpoints = endpoints.Where(MatchCompressedProjectBundlesNoFingerprint).Where(ep => ep.Route.EndsWith(".br"));
        bundleBrEndpoints.Should().HaveCount(1);
        var fingerprintedBundleGzEndpoints = endpoints.Where(MatchCompressedProjectBundlesWithFingerprint).Where(ep => ep.Route.EndsWith(".gz"));
        fingerprintedBundleGzEndpoints.Should().HaveCount(1);
        var fingerprintedBundleBrEndpoints = endpoints.Where(MatchCompressedProjectBundlesWithFingerprint).Where(ep => ep.Route.EndsWith(".br"));
        fingerprintedBundleBrEndpoints.Should().HaveCount(1);
 
        var fingerprintedBundleEndpoints = endpoints.Where(MatchUncompressedProjectBundlesWithFingerprint);
        fingerprintedBundleEndpoints.Should().HaveCount(3);
 
        var appBundleEndpoints = endpoints.Where(MatchUncompressedAppBundleNoFingerprint);
        appBundleEndpoints.Should().HaveCount(3);
        var appBundleGzEndpoints = endpoints.Where(MatchCompressedAppBundleNoFingerprint).Where(ep => ep.Route.EndsWith(".gz"));
        appBundleGzEndpoints.Should().HaveCount(1);
        var appBundleBrEndpoints = endpoints.Where(MatchCompressedAppBundleNoFingerprint).Where(ep => ep.Route.EndsWith(".br"));
        appBundleBrEndpoints.Should().HaveCount(1);
        var fingerprintedAppBundleGzEndpoints = endpoints.Where(MatchCompressedAppBundleWithFingerprint).Where(ep => ep.Route.EndsWith(".gz"));
        fingerprintedAppBundleGzEndpoints.Should().HaveCount(1);
        var fingerprintedAppBundleBrEndpoints = endpoints.Where(MatchCompressedAppBundleWithFingerprint).Where(ep => ep.Route.EndsWith(".br"));
        fingerprintedAppBundleBrEndpoints.Should().HaveCount(1);
 
        var fingerprintedAppBundleEndpoints = endpoints.Where(MatchUncompressedAppBundleWithFingerprint);
        fingerprintedAppBundleEndpoints.Should().HaveCount(3);
 
        // blazor.server.js and blazor.web.js assets and endpoints are included automatically
        // based on the presence of .razor files in projects referencing the web SDK.
        // In the future we will filter these out based on whether the app references the Endpoints or the Server
        // assemblies, but for now, just account for them in the tests and ignore them.
        endpoints.Should().HaveCount(45);
 
        AssertManifest(publishManifest, LoadPublishManifest());
    }
 
    [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")]
    public void Build_EndpointManifest_ContainsEndpoints()
    {
        // Arrange
        var expectedExtensions = new[] { ".pdb", ".js", ".wasm" };
        var testAppName = "BlazorWasmWithLibrary";
        var testInstance = CreateAspNetSdkTestAsset(testAppName)
            .WithProjectChanges((p, doc) =>
            {
                if (Path.GetFileName(p) == "blazorwasm.csproj")
                {
                    var itemGroup = new XElement("PropertyGroup");
                    var serviceWorkerAssetsManifest = new XElement("ServiceWorkerAssetsManifest", "service-worker-assets.js");
                    var fingerprintAssets = new XElement("WasmFingerprintAssets", false);
                    itemGroup.Add(serviceWorkerAssetsManifest);
                    itemGroup.Add(fingerprintAssets);
                    itemGroup.Add(new XElement("WasmEnableHotReload", false));
                    doc.Root.Add(itemGroup);
                }
            });
 
        var buildCommand = CreateBuildCommand(testInstance, "blazorwasm");
        buildCommand.Execute("/bl").Should().Pass();
 
        var buildOutputDirectory = buildCommand.GetOutputDirectory(DefaultTfm).ToString();
        VerifyEndpointsCollection(buildOutputDirectory, "blazorwasm", readFromDevManifest: true);
    }
 
    [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")]
    public void BuildHosted_EndpointManifest_ContainsEndpoints()
    {
        // Arrange
        var testAppName = "BlazorHosted";
        var testInstance = CreateAspNetSdkTestAsset(testAppName)
            .WithProjectChanges((p, doc) =>
                {
                    if (Path.GetFileName(p) == "blazorwasm.csproj")
                    {
                        var itemGroup = new XElement("PropertyGroup");
                        var fingerprintAssets = new XElement("WasmFingerprintAssets", false);
                        itemGroup.Add(fingerprintAssets);
                        itemGroup.Add(new XElement("WasmEnableHotReload", false));
                        doc.Root.Add(itemGroup);
                    }
                });
 
        var buildCommand = CreateBuildCommand(testInstance, "blazorhosted");
        buildCommand.Execute()
            .Should().Pass();
 
        var buildOutputDirectory = OutputPathCalculator.FromProject(Path.Combine(testInstance.TestRoot, "blazorhosted")).GetOutputDirectory();
 
        VerifyEndpointsCollection(buildOutputDirectory, "blazorhosted", readFromDevManifest: true);
    }
 
    [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")]
    public void Publish_EndpointManifestContainsEndpoints()
    {
        // Arrange
        var testAppName = "BlazorWasmWithLibrary";
        var testInstance = CreateAspNetSdkTestAsset(testAppName)
            .WithProjectChanges((p, doc) =>
            {
                if (Path.GetFileName(p) == "blazorwasm.csproj")
                {
                    var itemGroup = new XElement("PropertyGroup");
                    var fingerprintAssets = new XElement("WasmFingerprintAssets", false);
                    itemGroup.Add(fingerprintAssets);
                    itemGroup.Add(new XElement("WasmEnableHotReload", false));
                    doc.Root.Add(itemGroup);
                }
            });
 
        var publishCommand = CreatePublishCommand(testInstance, "blazorwasm");
        publishCommand.Execute().Should().Pass();
 
        var publishOutputDirectory = publishCommand.GetOutputDirectory(DefaultTfm).ToString();
 
        VerifyEndpointsCollection(publishOutputDirectory, "blazorwasm");
    }
 
    [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")]
    public void PublishHosted_EndpointManifest_ContainsEndpoints()
    {
        // Arrange
        var testAppName = "BlazorHosted";
        var testInstance = CreateAspNetSdkTestAsset(testAppName)
            .WithProjectChanges((p, doc) =>
            {
                if (Path.GetFileName(p) == "blazorwasm.csproj")
                {
                    var itemGroup = new XElement("PropertyGroup");
                    var fingerprintAssets = new XElement("WasmFingerprintAssets", false);
                    itemGroup.Add(fingerprintAssets);
                    itemGroup.Add(new XElement("WasmEnableHotReload", false));
                    doc.Root.Add(itemGroup);
                }
            });
 
        var publishCommand = CreatePublishCommand(testInstance, "blazorhosted");
        publishCommand.Execute().Should().Pass();
 
        var publishOutputDirectory = publishCommand.GetOutputDirectory(DefaultTfm).ToString();
 
        VerifyEndpointsCollection(publishOutputDirectory, "blazorhosted");
    }
 
    // Makes several assertions about the endpoints we defined.
    // All assets have at least one endpoint.
    // No endpoint points to a non-existent asset
    // All compressed assets have 2 endpoints (one for the path with the extension, one for the path without the extension)
    // All uncompressed assets have 1 endpoint
    private static void VerifyEndpointsCollection(string outputDirectory, string projectName, bool readFromDevManifest = false)
    {
        var endpointsManifestFile = Path.Combine(outputDirectory, $"{projectName}.staticwebassets.endpoints.json");
 
        var endpoints = JsonSerializer.Deserialize<StaticWebAssetEndpointsManifest>(File.ReadAllText(endpointsManifestFile));
 
        var wwwrootFolderFiles = GetWwwrootFolderFiles(outputDirectory);
 
        var foundAssets = new HashSet<string>();
        var endpointsByAssetFile = endpoints.Endpoints.GroupBy(e => e.AssetFile).ToDictionary(g => g.Key, g => g.ToArray());
 
        foreach (var endpoint in endpoints.Endpoints)
        {
            wwwrootFolderFiles.Should().Contain(endpoint.AssetFile);
            foundAssets.Add(endpoint.AssetFile);
        }
 
        wwwrootFolderFiles.Should().BeEquivalentTo(foundAssets);
 
        foreach (var file in wwwrootFolderFiles)
        {
            endpointsByAssetFile.Should().ContainKey(file);
            if (file.EndsWith(".br") || file.EndsWith(".gz"))
            {
                endpointsByAssetFile[file].Should().HaveCount(2);
            }
            else if (endpointsByAssetFile[file].Length > 1)
            {
                endpointsByAssetFile[file].Where(e => e.EndpointProperties.Any(p => p.Name == "integrity")).Count().Should().Be(1);
                endpointsByAssetFile[file].Where(e => e.EndpointProperties.Length == 0).Count().Should().Be(1);
            }
            else
            {
                endpointsByAssetFile[file].Should().HaveCount(1);
            }
        }
 
        HashSet<string> GetWwwrootFolderFiles(string outputDirectory)
        {
            if (!readFromDevManifest)
            {
                return [.. Directory.GetFiles(Path.Combine(outputDirectory, "wwwroot"), "*", SearchOption.AllDirectories)
                        .Select(a => StaticWebAsset.Normalize(Path.GetRelativePath(Path.Combine(outputDirectory, "wwwroot"), a)))];
            }
            else
            {
                var staticWebAssetDevelopmentManifest = JsonSerializer.Deserialize<StaticWebAssetsDevelopmentManifest>(File.ReadAllText(Path.Combine(outputDirectory, $"{projectName}.staticwebassets.runtime.json")));
                var endpoints = new HashSet<string>();
 
                //Traverse the node tree and compute the paths for all assets
                Traverse(staticWebAssetDevelopmentManifest.Root, "", endpoints);
                return endpoints;
            }
        }
    }
 
    private static void Traverse(StaticWebAssetNode node, string pathSoFar, HashSet<string> endpoints)
    {
        if (node.Asset != null)
        {
            endpoints.Add(StaticWebAsset.Normalize(pathSoFar));
        }
        else
        {
            foreach (var child in node.Children)
            {
                Traverse(child.Value, Path.Combine(pathSoFar, child.Key), endpoints);
            }
        }
    }
 
    public class StaticWebAssetsDevelopmentManifest
    {
        public string[] ContentRoots { get; set; }
 
        public StaticWebAssetNode Root { get; set; }
    }
 
    public class StaticWebAssetPattern
    {
        public int ContentRootIndex { get; set; }
        public string Pattern { get; set; }
        public int Depth { get; set; }
    }
 
    public class StaticWebAssetMatch
    {
        public int ContentRootIndex { get; set; }
        public string SubPath { get; set; }
    }
 
    public class StaticWebAssetNode
    {
        public Dictionary<string, StaticWebAssetNode> Children { get; set; }
        public StaticWebAssetMatch Asset { get; set; }
        public StaticWebAssetPattern[] Patterns { get; set; }
    }
}