File: StaticWebAssets\GenerateStaticWebAssetEndpointsManifestTest.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.Text.Json;
using Microsoft.AspNetCore.StaticWebAssets.Tasks;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Moq;
 
namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets;
 
public class GenerateStaticWebAssetEndpointsManifestTest
{
    [Fact]
    public void GeneratesManifest_ForEndpointsWithTokens()
    {
        StaticWebAssetEndpoint[] expectedEndpoints =
        [
            new() {
                Route = "index.fingerprint.html",
                AssetFile = "index.html",
                Selectors = [],
                ResponseHeaders =
                [
                    new() {
                        Name = "Cache-Control",
                        Value = "max-age=31536000, immutable"
                    },
                    new() {
                        Name = "Content-Length",
                        Value = "10"
                    },
                    new() {
                        Name = "Content-Type",
                        Value = "text/html"
                    },
                    new() {
                        Name = "ETag",
                        Value = "\"integrity\""
                    },
                    new() {
                        Name = "Last-Modified",
                        Value = "Sat, 01 Jan 2000 00:00:01 GMT"
                    }
                ],
                EndpointProperties =
                [
                    new() {
                        Name = "fingerprint",
                        Value = "fingerprint"
                    },
                    new() {
                        Name = "integrity",
                        Value = "sha256-integrity"
                    },
                    new() {
                        Name = "label",
                        Value = "index.html"
                    }
                ]
            },
            new() {
                Route = "index.fingerprint.js",
                AssetFile = "index.fingerprint.js",
                Selectors = [],
                ResponseHeaders =
                [
                    new() {
                        Name = "Cache-Control",
                        Value = "max-age=31536000, immutable"
                    },
                    new() {
                        Name = "Content-Length",
                        Value = "10"
                    },
                    new() {
                        Name = "Content-Type",
                        Value = "text/javascript"
                    },
                    new() {
                        Name = "ETag",
                        Value = "\"integrity\""
                    },
                    new() {
                        Name = "Last-Modified",
                        Value = "Sat, 01 Jan 2000 00:00:01 GMT"
                    }
                ],
                EndpointProperties =
                [
                    new() {
                        Name = "fingerprint",
                        Value = "fingerprint"
                    },
                    new() {
                        Name = "integrity",
                        Value = "sha256-integrity"
                    },
                    new() {
                        Name = "label",
                        Value = "index.js"
                    }
                ]
            },
            new() {
                Route = "index.html",
                AssetFile = "index.html",
                Selectors = [],
                ResponseHeaders =
                [
                    new() {
                        Name = "Cache-Control",
                        Value = "no-cache"
                    },
                    new() {
                        Name = "Content-Length",
                        Value = "10"
                    },
                    new() {
                        Name = "Content-Type",
                        Value = "text/html"
                    },
                    new() {
                        Name = "ETag",
                        Value = "\"integrity\""
                    },
                    new() {
                        Name = "Last-Modified",
                        Value = "Sat, 01 Jan 2000 00:00:01 GMT"
                    }
                ],
                EndpointProperties = [
                    new() {
                        Name = "integrity",
                        Value = "sha256-integrity"
                    }]
            },
            new() {
                Route = "index.js",
                AssetFile = "index.fingerprint.js",
                Selectors = [],
                ResponseHeaders =
                [
                    new() {
                        Name = "Cache-Control",
                        Value = "no-cache"
                    },
                    new() {
                        Name = "Content-Length",
                        Value = "10"
                    },
                    new() {
                        Name = "Content-Type",
                        Value = "text/javascript"
                    },
                    new() {
                        Name = "ETag",
                        Value = "\"integrity\""
                    },
                    new() {
                        Name = "Last-Modified",
                        Value = "Sat, 01 Jan 2000 00:00:01 GMT"
                    }
                ],
                EndpointProperties = [
                    new() {
                        Name = "integrity",
                        Value = "sha256-integrity"
                    }]
            }
        ];
        Array.Sort(expectedEndpoints);
 
        var assets = new[]
        {
            CreateAsset("index.html", relativePath: "index#[.{fingerprint}]?.html"),
            CreateAsset("index.js", relativePath: "index#[.{fingerprint}]!.js"),
        };
        Array.Sort(assets, (l, r) => string.Compare(l.Identity, r.Identity, StringComparison.Ordinal));
 
        var endpoints = CreateEndpoints(assets);
 
        var path = Path.Combine(AppContext.BaseDirectory, Guid.NewGuid().ToString("N") + "endpoints.json");
 
        var task = new GenerateStaticWebAssetEndpointsManifest
        {
            Assets = assets.Select(a => a.ToTaskItem()).ToArray(),
            Endpoints = endpoints.Select(e => e.ToTaskItem()).ToArray(),
            ManifestType = "Build",
            Source = "MyApp",
            ManifestPath = path,
            BuildEngine = Mock.Of<IBuildEngine>()
        };
 
        try
        {
            // Act
            task.Execute();
 
            // Assert
            new FileInfo(path).Should().Exist();
            var manifest = File.ReadAllText(path);
            var json = JsonSerializer.Deserialize<StaticWebAssetEndpointsManifest>(manifest);
            json.Should().NotBeNull();
            json.Endpoints.Should().HaveCount(4);
            Array.Sort(json.Endpoints);
            json.Endpoints.Should().BeEquivalentTo(expectedEndpoints);
        }
        finally
        {
            if (File.Exists(path))
            {
                File.Delete(path);
            }
        }
    }
 
    [Fact]
    public void ExcludesEndpoints_BasedOnExclusionPatterns()
    {
        // Arrange
        var assets = new[]
        {
            CreateAsset("index.html", relativePath: "index.html", basePath: "_content/MyApp"),
            CreateAsset("app.js", relativePath: "app.js", basePath: "_content/MyApp"),
            CreateAsset("styles.css", relativePath: "styles.css", basePath: "_content/OtherApp"),
        };
        Array.Sort(assets, (l, r) => string.Compare(l.Identity, r.Identity, StringComparison.Ordinal));
 
        var endpoints = CreateEndpoints(assets);
        var path = Path.Combine(AppContext.BaseDirectory, Guid.NewGuid().ToString("N") + "endpoints.json");
        var exclusionCachePath = Path.Combine(AppContext.BaseDirectory, Guid.NewGuid().ToString("N") + "exclusions.cache");
 
        var task = new GenerateStaticWebAssetEndpointsManifest
        {
            Assets = assets.Select(a => a.ToTaskItem()).ToArray(),
            Endpoints = endpoints.Select(e => e.ToTaskItem()).ToArray(),
            ManifestType = "Build",
            Source = "MyApp",
            ManifestPath = path,
            ExclusionPatterns = "**/*.js;**/*.html",
            ExclusionPatternsCacheFilePath = exclusionCachePath,
            BuildEngine = Mock.Of<IBuildEngine>()
        };
 
        try
        {
            // Act
            task.Execute();
 
            // Assert
            new FileInfo(path).Should().Exist();
            new FileInfo(exclusionCachePath).Should().Exist();
            
            var manifest = File.ReadAllText(path);
            var json = JsonSerializer.Deserialize<StaticWebAssetEndpointsManifest>(manifest);
            json.Should().NotBeNull();
            
            // Only styles.css endpoint should remain as others match _content/MyApp/**
            json.Endpoints.Should().HaveCount(1);
            json.Endpoints[0].Route.Should().Contain("styles.css");
        }
        finally
        {
            if (File.Exists(path))
            {
                File.Delete(path);
            }
 
            if (File.Exists(exclusionCachePath))
            {
                File.Delete(exclusionCachePath);
            }
        }
    }
 
    [Fact]
    public void SkipsRegeneration_WhenExclusionPatternsUnchanged()
    {
        // Arrange
        var assets = new[]
        {
            CreateAsset("index.html", relativePath: "index.html"),
        };
 
        var endpoints = CreateEndpoints(assets);
        var path = Path.Combine(AppContext.BaseDirectory, Guid.NewGuid().ToString("N") + "endpoints.json");
        var cachePath = Path.Combine(AppContext.BaseDirectory, Guid.NewGuid().ToString("N") + ".cache");
        var exclusionCachePath = Path.Combine(AppContext.BaseDirectory, Guid.NewGuid().ToString("N") + "exclusions.cache");
 
        // First run
        var task = new GenerateStaticWebAssetEndpointsManifest
        {
            Assets = assets.Select(a => a.ToTaskItem()).ToArray(),
            Endpoints = endpoints.Select(e => e.ToTaskItem()).ToArray(),
            ManifestType = "Build",
            Source = "MyApp",
            ManifestPath = path,
            CacheFilePath = cachePath,
            ExclusionPatterns = "test/**",
            ExclusionPatternsCacheFilePath = exclusionCachePath,
            BuildEngine = Mock.Of<IBuildEngine>()
        };
 
        try
        {
            // Act - First execution
            task.Execute();
            File.WriteAllText(cachePath, "cache"); // Simulate cache file
            var firstWriteTime = File.GetLastWriteTimeUtc(path);
 
            // Act - Second execution with same patterns
            Thread.Sleep(10); // Ensure time difference
            var task2 = new GenerateStaticWebAssetEndpointsManifest
            {
                Assets = assets.Select(a => a.ToTaskItem()).ToArray(),
                Endpoints = endpoints.Select(e => e.ToTaskItem()).ToArray(),
                ManifestType = "Build",
                Source = "MyApp",
                ManifestPath = path,
                CacheFilePath = cachePath,
                ExclusionPatterns = "test/**",
                ExclusionPatternsCacheFilePath = exclusionCachePath,
                BuildEngine = Mock.Of<IBuildEngine>()
            };
            task2.Execute();
 
            // Assert - File should not be regenerated
            var secondWriteTime = File.GetLastWriteTimeUtc(path);
            secondWriteTime.Should().Be(firstWriteTime);
        }
        finally
        {
            if (File.Exists(path))
            {
                File.Delete(path);
            }
 
            if (File.Exists(cachePath))
            {
                File.Delete(cachePath);
            }
 
            if (File.Exists(exclusionCachePath))
            {
                File.Delete(exclusionCachePath);
            }
        }
    }
 
    [Fact]
    public void RegeneratesManifest_WhenExclusionPatternsChange()
    {
        // Arrange
        var assets = new[]
        {
            CreateAsset("index.html", relativePath: "index.html"),
        };
 
        var endpoints = CreateEndpoints(assets);
        var endpointsManifestPath = Path.Combine(AppContext.BaseDirectory, Guid.NewGuid().ToString("N") + ".endpoints.json");
        var manifestPath = Path.Combine(AppContext.BaseDirectory, Guid.NewGuid().ToString("N") + ".cache");
        var exclusionCachePath = Path.Combine(AppContext.BaseDirectory, Guid.NewGuid().ToString("N") + ".exclusions.cache");
 
        // First run
        var task = new GenerateStaticWebAssetEndpointsManifest
        {
            Assets = assets.Select(a => a.ToTaskItem()).ToArray(),
            Endpoints = endpoints.Select(e => e.ToTaskItem()).ToArray(),
            ManifestType = "Build",
            Source = "MyApp",
            ManifestPath = endpointsManifestPath,
            CacheFilePath = manifestPath,
            ExclusionPatterns = "test/**",
            ExclusionPatternsCacheFilePath = exclusionCachePath,
            BuildEngine = Mock.Of<IBuildEngine>()
        };
 
        try
        {
            File.WriteAllText(manifestPath, "manifest");
            Thread.Sleep(10);
 
            // Act - First execution
            task.Execute();
            var firstWriteTime = File.GetLastWriteTimeUtc(endpointsManifestPath);
 
            // Act - Second execution with different patterns
            Thread.Sleep(10); // Ensure time difference
            var task2 = new GenerateStaticWebAssetEndpointsManifest
            {
                Assets = assets.Select(a => a.ToTaskItem()).ToArray(),
                Endpoints = endpoints.Select(e => e.ToTaskItem()).ToArray(),
                ManifestType = "Build",
                Source = "MyApp",
                ManifestPath = endpointsManifestPath,
                CacheFilePath = manifestPath,
                ExclusionPatterns = "different/**;pattern/**",
                ExclusionPatternsCacheFilePath = exclusionCachePath,
                BuildEngine = Mock.Of<IBuildEngine>()
            };
            task2.Execute();
 
            // Assert - File should be regenerated
            var secondWriteTime = File.GetLastWriteTimeUtc(endpointsManifestPath);
            secondWriteTime.Should().BeAfter(firstWriteTime);
            
            // Verify cache file was updated
            var cacheContent = File.ReadAllText(exclusionCachePath);
            cacheContent.Should().Contain("different/**");
            cacheContent.Should().Contain("pattern/**");
        }
        finally
        {
            if (File.Exists(endpointsManifestPath))
            {
                File.Delete(endpointsManifestPath);
            }
 
            if (File.Exists(manifestPath))
            {
                File.Delete(manifestPath);
            }
 
            if (File.Exists(exclusionCachePath))
            {
                File.Delete(exclusionCachePath);
            }
        }
    }
 
    private StaticWebAssetEndpoint[] CreateEndpoints(StaticWebAsset[] assets)
    {
        var defineStaticWebAssetEndpoints = new DefineStaticWebAssetEndpoints
        {
            CandidateAssets = assets.Select(a => a.ToTaskItem()).ToArray(),
            ExistingEndpoints = [],
            ContentTypeMappings = []
        };
        defineStaticWebAssetEndpoints.BuildEngine = Mock.Of<IBuildEngine>();
 
        defineStaticWebAssetEndpoints.Execute();
        return StaticWebAssetEndpoint.FromItemGroup(defineStaticWebAssetEndpoints.Endpoints);
    }
 
    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 = new DateTime(2000, 1, 1, 0, 0, 1)
        };
 
        result.ApplyDefaults();
        result.Normalize();
 
        return result;
    }
}