File: StaticWebAssets\UpdateStaticWebAssetEndpointsTest.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 Microsoft.AspNetCore.StaticWebAssets.Tasks;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Moq;
 
namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets;
 
public class UpdateStaticWebAssetEndpointsTest
{
    [Fact]
    public void CanUpdateEndpoint_AppendResponseHeaders()
    {
        // Arrrange
        var assets = new[] {
            CreateAsset("index.html", relativePath: "index#[.{fingerprint}]?.html"),
            CreateAsset("index.js", relativePath: "index#[.{fingerprint}]?.js"),
            CreateAsset("index.css", relativePath: "index#[.{fingerprint}]?.css"),
            CreateAsset("other.html", relativePath: "other#[.{fingerprint}]?.html"),
            CreateAsset("other.js", relativePath: "other#[.{fingerprint}]?.js"),
            CreateAsset("other.css", relativePath: "other#[.{fingerprint}]?.css"),
        };
        Array.Sort(assets, (l, r) => string.Compare(l.Identity, r.Identity, StringComparison.Ordinal));
 
        var endpoints = CreateEndpoints(assets);
        var fingerprintedEndpoints = endpoints.Where(e => e.EndpointProperties.Any(p => string.Equals(p.Name, "fingerprint", StringComparison.Ordinal))).ToArray();
        foreach (var endpoint in fingerprintedEndpoints)
        {
            endpoint.ResponseHeaders = endpoint.ResponseHeaders.Where(h => !string.Equals(h.Name, "Cache-Control", StringComparison.Ordinal)).ToArray();
        }
 
        var filterStaticWebAssetEndpoints = new UpdateStaticWebAssetEndpoints
        {
            EndpointsToUpdate = fingerprintedEndpoints.Select(e => e.ToTaskItem()).ToArray(),
            AllEndpoints = endpoints.Select(e => e.ToTaskItem()).ToArray(),
            Operations =
            [
                CreateOperation("Append", "Header", "Cache-Control", "immutable")
            ],
            BuildEngine = Mock.Of<IBuildEngine>()
        };
 
        // Act
        var result = filterStaticWebAssetEndpoints.Execute();
        result.Should().BeTrue();
 
        // Assert
        var updatedEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterStaticWebAssetEndpoints.UpdatedEndpoints);
        updatedEndpoints.Should().HaveCount(fingerprintedEndpoints.Length);
        foreach (var updatedEndpoint in updatedEndpoints)
        {
            updatedEndpoint.ResponseHeaders.Should().ContainSingle(h => string.Equals(h.Name, "Cache-Control", StringComparison.Ordinal) && string.Equals(h.Value, "immutable"));
        }
    }
 
    [Fact]
    public void CanUpdateEndpoint_RemoveResponseHeaders()
    {
        // Arrrange
        var assets = new[] {
            CreateAsset("index.html", relativePath: "index#[.{fingerprint}]?.html"),
            CreateAsset("index.js", relativePath: "index#[.{fingerprint}]?.js"),
            CreateAsset("index.css", relativePath: "index#[.{fingerprint}]?.css"),
            CreateAsset("other.html", relativePath: "other#[.{fingerprint}]?.html"),
            CreateAsset("other.js", relativePath: "other#[.{fingerprint}]?.js"),
            CreateAsset("other.css", relativePath: "other#[.{fingerprint}]?.css"),
        };
        Array.Sort(assets, (l, r) => string.Compare(l.Identity, r.Identity, StringComparison.Ordinal));
 
        var endpoints = CreateEndpoints(assets);
        var fingerprintedEndpoints = endpoints.Where(e => e.EndpointProperties.Any(p => string.Equals(p.Name, "fingerprint", StringComparison.Ordinal))).ToArray();
 
        var filterStaticWebAssetEndpoints = new UpdateStaticWebAssetEndpoints
        {
            EndpointsToUpdate = fingerprintedEndpoints.Select(e => e.ToTaskItem()).ToArray(),
            AllEndpoints = endpoints.Select(e => e.ToTaskItem()).ToArray(),
            Operations =
            [
                CreateOperation("Remove", "Header", "Cache-Control", null)
            ],
            BuildEngine = Mock.Of<IBuildEngine>()
        };
 
        // Act
        var result = filterStaticWebAssetEndpoints.Execute();
        result.Should().BeTrue();
 
        // Assert
        var updatedEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterStaticWebAssetEndpoints.UpdatedEndpoints);
        updatedEndpoints.Should().HaveCount(fingerprintedEndpoints.Length);
        foreach (var updatedEndpoint in updatedEndpoints)
        {
            updatedEndpoint.ResponseHeaders.Should().NotContain(h => string.Equals(h.Name, "Cache-Control", StringComparison.Ordinal));
        }
    }
 
    [Fact]
    public void CanUpdateEndpoint_RemoveAllResponseHeaders()
    {
        // Arrrange
        var assets = new[] {
            CreateAsset("index.html", relativePath: "index#[.{fingerprint}]?.html"),
            CreateAsset("index.js", relativePath: "index#[.{fingerprint}]?.js"),
            CreateAsset("index.css", relativePath: "index#[.{fingerprint}]?.css"),
            CreateAsset("other.html", relativePath: "other#[.{fingerprint}]?.html"),
            CreateAsset("other.js", relativePath: "other#[.{fingerprint}]?.js"),
            CreateAsset("other.css", relativePath: "other#[.{fingerprint}]?.css"),
        };
        Array.Sort(assets, (l, r) => string.Compare(l.Identity, r.Identity, StringComparison.Ordinal));
 
        var endpoints = CreateEndpoints(assets);
        var fingerprintedEndpoints = endpoints.Where(e => e.EndpointProperties.Any(p => string.Equals(p.Name, "fingerprint", StringComparison.Ordinal))).ToArray();
        foreach (var endpoint in fingerprintedEndpoints)
        {
            endpoint.ResponseHeaders = [.. endpoint.ResponseHeaders, new StaticWebAssetEndpointResponseHeader { Name = "ETag", Value = "W/\"integrity\"" }];
        }
 
        var filterStaticWebAssetEndpoints = new UpdateStaticWebAssetEndpoints
        {
            EndpointsToUpdate = fingerprintedEndpoints.Select(e => e.ToTaskItem()).ToArray(),
            AllEndpoints = endpoints.Select(e => e.ToTaskItem()).ToArray(),
            Operations =
            [
                CreateOperation("RemoveAll", "Header", "ETag", null)
            ],
            BuildEngine = Mock.Of<IBuildEngine>()
        };
 
        // Act
        var result = filterStaticWebAssetEndpoints.Execute();
        result.Should().BeTrue();
 
        // Assert
        var updatedEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterStaticWebAssetEndpoints.UpdatedEndpoints);
        updatedEndpoints.Should().HaveCount(fingerprintedEndpoints.Length);
        foreach (var updatedEndpoint in updatedEndpoints)
        {
            updatedEndpoint.ResponseHeaders.Should().NotContain(h => string.Equals(h.Name, "ETag", StringComparison.Ordinal));
        }
    }
 
    [Fact]
    public void CanUpdateEndpoint_RemoveAllResponseHeadersWithValue()
    {
        // Arrrange
        var assets = new[] {
            CreateAsset("index.html", relativePath: "index#[.{fingerprint}]?.html"),
            CreateAsset("index.js", relativePath: "index#[.{fingerprint}]?.js"),
            CreateAsset("index.css", relativePath: "index#[.{fingerprint}]?.css"),
            CreateAsset("other.html", relativePath: "other#[.{fingerprint}]?.html"),
            CreateAsset("other.js", relativePath: "other#[.{fingerprint}]?.js"),
            CreateAsset("other.css", relativePath: "other#[.{fingerprint}]?.css"),
        };
        Array.Sort(assets, (l, r) => string.Compare(l.Identity, r.Identity, StringComparison.Ordinal));
 
        var endpoints = CreateEndpoints(assets);
        var fingerprintedEndpoints = endpoints.Where(e => e.EndpointProperties.Any(p => string.Equals(p.Name, "fingerprint", StringComparison.Ordinal))).ToArray();
        foreach (var endpoint in fingerprintedEndpoints)
        {
            endpoint.ResponseHeaders = [.. endpoint.ResponseHeaders, new StaticWebAssetEndpointResponseHeader { Name = "ETag", Value = "W/\"integrity\"" }];
        }
 
        var filterStaticWebAssetEndpoints = new UpdateStaticWebAssetEndpoints
        {
            EndpointsToUpdate = fingerprintedEndpoints.Select(e => e.ToTaskItem()).ToArray(),
            AllEndpoints = endpoints.Select(e => e.ToTaskItem()).ToArray(),
            Operations =
            [
                CreateOperation("RemoveAll", "Header", "ETag", "W/\"integrity\"")
            ],
            BuildEngine = Mock.Of<IBuildEngine>()
        };
 
        // Act
        var result = filterStaticWebAssetEndpoints.Execute();
        result.Should().BeTrue();
 
        // Assert
        var updatedEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterStaticWebAssetEndpoints.UpdatedEndpoints);
        updatedEndpoints.Should().HaveCount(fingerprintedEndpoints.Length);
        foreach (var updatedEndpoint in updatedEndpoints)
        {
            updatedEndpoint.ResponseHeaders.Should().ContainSingle(h => string.Equals(h.Name, "ETag", StringComparison.Ordinal) && string.Equals(h.Value, "\"integrity\"", StringComparison.Ordinal));
            updatedEndpoint.ResponseHeaders.Should().NotContain(h => string.Equals(h.Name, "ETag", StringComparison.Ordinal) && string.Equals(h.Value, "W/\"integrity\"", StringComparison.Ordinal));
        }
    }
 
    [Fact]
    public void CanUpdateEndpoint_ReplaceResponseHeaders()
    {
        // Arrrange
        var assets = new[] {
            CreateAsset("index.html", relativePath: "index#[.{fingerprint}]?.html"),
            CreateAsset("index.js", relativePath: "index#[.{fingerprint}]?.js"),
            CreateAsset("index.css", relativePath: "index#[.{fingerprint}]?.css"),
            CreateAsset("other.html", relativePath: "other#[.{fingerprint}]?.html"),
            CreateAsset("other.js", relativePath: "other#[.{fingerprint}]?.js"),
            CreateAsset("other.css", relativePath: "other#[.{fingerprint}]?.css"),
        };
        Array.Sort(assets, (l, r) => string.Compare(l.Identity, r.Identity, StringComparison.Ordinal));
 
        var endpoints = CreateEndpoints(assets);
        var fingerprintedEndpoints = endpoints.Where(e => e.EndpointProperties.Any(p => string.Equals(p.Name, "fingerprint", StringComparison.Ordinal))).ToArray();
 
        var filterStaticWebAssetEndpoints = new UpdateStaticWebAssetEndpoints
        {
            EndpointsToUpdate = endpoints.Select(e => e.ToTaskItem()).ToArray(),
            AllEndpoints = endpoints.Select(e => e.ToTaskItem()).ToArray(),
            Operations =
            [
                CreateOperation("Replace", "Header", "Cache-Control", "max-age=31536000, immutable", "immutable")
            ],
            BuildEngine = Mock.Of<IBuildEngine>()
        };
 
        // Act
        var result = filterStaticWebAssetEndpoints.Execute();
        result.Should().BeTrue();
 
        // Assert
        var updatedEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterStaticWebAssetEndpoints.UpdatedEndpoints);
        updatedEndpoints.Should().HaveCount(fingerprintedEndpoints.Length);
        foreach (var updatedEndpoint in updatedEndpoints)
        {
            updatedEndpoint.ResponseHeaders.Should().ContainSingle(h => string.Equals(h.Name, "Cache-Control", StringComparison.Ordinal) && string.Equals(h.Value, "immutable"));
        }
    }
 
    [Fact]
    public void CanUpdateEndpoint_RetainsNonModifiedEndpointsWithSameRoute()
    {
        // Arrrange
        var assets = new[] {
            CreateAsset("index.html", relativePath: "index#[.{fingerprint}]?.html"),
            CreateAsset("index.js", relativePath: "index#[.{fingerprint}]?.js"),
            CreateAsset("index.css", relativePath: "index#[.{fingerprint}]?.css"),
            CreateAsset("other.html", relativePath: "other#[.{fingerprint}]?.html"),
            CreateAsset("other.js", relativePath: "other#[.{fingerprint}]?.js"),
            CreateAsset("other.css", relativePath: "other#[.{fingerprint}]?.css"),
        };
        Array.Sort(assets, (l, r) => string.Compare(l.Identity, r.Identity, StringComparison.Ordinal));
 
        var endpoints = CreateEndpoints(assets);
        var fingerprintedEndpoints = endpoints.Where(e => e.EndpointProperties.Any(p => string.Equals(p.Name, "fingerprint", StringComparison.Ordinal))).ToArray();
 
        var unmodifiedEndpoint = new StaticWebAssetEndpoint
        {
            Route = fingerprintedEndpoints[0].Route,
            AssetFile = fingerprintedEndpoints[0].AssetFile + ".gz",
            Selectors = [new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip" }],
            ResponseHeaders = [.. fingerprintedEndpoints[0].ResponseHeaders],
            EndpointProperties = [.. fingerprintedEndpoints[0].EndpointProperties]
        };
 
        endpoints = [..endpoints, unmodifiedEndpoint];
 
        foreach (var endpoint in fingerprintedEndpoints)
        {
            endpoint.ResponseHeaders = endpoint.ResponseHeaders.Where(h => !string.Equals(h.Name, "Cache-Control", StringComparison.Ordinal)).ToArray();
        }
 
        var filterStaticWebAssetEndpoints = new UpdateStaticWebAssetEndpoints
        {
            EndpointsToUpdate = fingerprintedEndpoints.Select(e => e.ToTaskItem()).ToArray(),
            AllEndpoints = endpoints.Select(e => e.ToTaskItem()).ToArray(),
            Operations =
            [
                CreateOperation("Append", "Header", "Cache-Control", "immutable")
            ],
            BuildEngine = Mock.Of<IBuildEngine>()
        };
 
        // Act
        var result = filterStaticWebAssetEndpoints.Execute();
        result.Should().BeTrue();
 
        // Assert
        var updatedEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterStaticWebAssetEndpoints.UpdatedEndpoints);
        updatedEndpoints.Should().HaveCount(fingerprintedEndpoints.Length + 1);
        var updatedUnmodifiedEndpoint = updatedEndpoints.Where(e => e.AssetFile.EndsWith(".gz"));
        updatedUnmodifiedEndpoint.Should().HaveCount(1);
 
        var updatedModifiedEndpoints = updatedEndpoints.Where(e => !e.AssetFile.EndsWith(".gz"));
        updatedModifiedEndpoints.Should().HaveCount(fingerprintedEndpoints.Length);
        foreach (var updatedEndpoint in updatedModifiedEndpoints)
        {
            updatedEndpoint.ResponseHeaders.Should().ContainSingle(h => string.Equals(h.Name, "Cache-Control", StringComparison.Ordinal) && string.Equals(h.Value, "immutable"));
        }
    }
 
    private static ITaskItem CreateOperation(string type, string target, string name, string value, string newValue = null)
    {
        return new TaskItem(type, new Dictionary<string, string>
        {
            { "UpdateTarget", target },
            { "Name", name },
            { "Value", value },
            { "NewValue", newValue }
        });
    }
 
    private StaticWebAssetEndpoint[] CreateEndpoints(StaticWebAsset[] assets)
    {
        var defineStaticWebAssetEndpoints = new DefineStaticWebAssetEndpoints
        {
            CandidateAssets = assets.Select(a => a.ToTaskItem()).ToArray(),
            ExistingEndpoints = [],
            ContentTypeMappings =
            [
                CreateContentMapping("*.html", "text/html"),
                CreateContentMapping("*.js", "application/javascript"),
                CreateContentMapping("*.css", "text/css"),
            ]
        };
        defineStaticWebAssetEndpoints.BuildEngine = Mock.Of<IBuildEngine>();
 
        defineStaticWebAssetEndpoints.Execute();
        return StaticWebAssetEndpoint.FromItemGroup(defineStaticWebAssetEndpoints.Endpoints);
    }
 
    private static TaskItem CreateContentMapping(string pattern, string contentType)
    {
        return new TaskItem(contentType, new Dictionary<string, string>
        {
            { "Pattern", pattern },
            { "Priority", "0" }
        });
    }
 
 
    private static StaticWebAsset CreateAsset(
        string itemSpec,
        string sourceId = "MyApp",
        string sourceType = "Discovered",
        string relativePath = null,
        string assetKind = "All",
        string assetMode = "All",
        string basePath = "base",
        string assetRole = "Primary",
        string relatedAsset = "",
        string assetTraitName = "",
        string assetTraitValue = "",
        string copyToOutputDirectory = "Never",
        string copytToPublishDirectory = "PreserveNewest")
    {
        var result = new StaticWebAsset()
        {
            Identity = Path.GetFullPath(itemSpec),
            SourceId = sourceId,
            SourceType = sourceType,
            ContentRoot = Directory.GetCurrentDirectory(),
            BasePath = basePath,
            RelativePath = relativePath ?? itemSpec,
            AssetKind = assetKind,
            AssetMode = assetMode,
            AssetRole = assetRole,
            AssetMergeBehavior = StaticWebAsset.MergeBehaviors.PreferTarget,
            AssetMergeSource = "",
            RelatedAsset = relatedAsset,
            AssetTraitName = assetTraitName,
            AssetTraitValue = assetTraitValue,
            CopyToOutputDirectory = copyToOutputDirectory,
            CopyToPublishDirectory = copytToPublishDirectory,
            OriginalItemSpec = itemSpec,
            // Add these to avoid accessing the disk to compute them
            Integrity = "integrity",
            Fingerprint = "fingerprint",
            FileLength = 10,
            LastWriteTime = DateTimeOffset.UtcNow,
        };
 
        result.ApplyDefaults();
        result.Normalize();
 
        return result;
    }
}