File: StaticWebAssets\GenerateStaticWebAssetsManifestTest.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 Moq;
 
namespace Microsoft.NET.Sdk.StaticWebAssets.Tests
{
    public class GenerateStaticWebAssetsManifestTest
    {
        public GenerateStaticWebAssetsManifestTest()
        {
            Directory.CreateDirectory(Path.Combine(TestContext.Current.TestExecutionDirectory, nameof(GenerateStaticWebAssetsManifestTest)));
            TempFilePath = Path.Combine(TestContext.Current.TestExecutionDirectory, nameof(GenerateStaticWebAssetsManifestTest), Guid.NewGuid().ToString("N") + ".json");
        }
 
        public string TempFilePath { get; }
 
        [Fact]
        public void CanGenerateEmptyManifest()
        {
            var errorMessages = new List<string>();
            var buildEngine = new Mock<IBuildEngine>();
            buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
                .Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message));
 
            // GetTempFilePath automatically creates the file, which interferes with the test.
            File.Delete(TempFilePath);
 
            var task = new GenerateStaticWebAssetsManifest
            {
                BuildEngine = buildEngine.Object,
                Assets = Array.Empty<ITaskItem>(),
                Endpoints = Array.Empty<ITaskItem>(),
                ReferencedProjectsConfigurations = Array.Empty<ITaskItem>(),
                DiscoveryPatterns = Array.Empty<ITaskItem>(),
                BasePath = "/",
                Source = "MyProject",
                ManifestType = "Build",
                Mode = "Default",
                ManifestPath = TempFilePath,
            };
 
            // Act
            var result = task.Execute();
 
            // Assert
            result.Should().Be(true);
            var manifest = StaticWebAssetsManifest.FromJsonString(File.ReadAllText(TempFilePath));
            manifest.Should().NotBeNull();
            manifest.Assets.Should().BeNullOrEmpty();
            manifest.Endpoints.Should().BeNullOrEmpty();
            manifest.DiscoveryPatterns.Should().BeNullOrEmpty();
            manifest.ReferencedProjectsConfiguration.Should().BeNullOrEmpty();
            manifest.Version.Should().Be(1);
            manifest.Hash.Should().NotBeNullOrWhiteSpace();
            manifest.Mode.Should().Be("Default");
            manifest.ManifestType.Should().Be("Build");
            manifest.BasePath.Should().Be("/");
            manifest.Source.Should().Be("MyProject");
        }
 
        [Fact]
        public void GeneratesManifestWithAssets()
        {
            var errorMessages = new List<string>();
            var buildEngine = new Mock<IBuildEngine>();
            buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
                .Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message));
 
            // GetTempFilePath automatically creates the file, which interferes with the test.
            File.Delete(TempFilePath);
            var asset = CreateAsset(Path.Combine("wwwroot", "candidate.js"), "MyProject", "Computed", "candidate.js", "All", "All");
            var endpoint = CreateEndpoint(asset);
            var task = new GenerateStaticWebAssetsManifest
            {
                BuildEngine = buildEngine.Object,
                Assets = new[]
                {
                    asset.ToTaskItem()
                },
                Endpoints = [endpoint.ToTaskItem()],
                ReferencedProjectsConfigurations = Array.Empty<ITaskItem>(),
                DiscoveryPatterns = Array.Empty<ITaskItem>(),
                BasePath = "/",
                Source = "MyProject",
                ManifestType = "Build",
                Mode = "Default",
                ManifestPath = TempFilePath,
            };
 
            // Act
            var result = task.Execute();
 
            // Assert
            result.Should().Be(true);
            var manifest = StaticWebAssetsManifest.FromJsonString(File.ReadAllText(TempFilePath));
            manifest.Should().NotBeNull();
            manifest.Assets.Should().HaveCount(1);
            var newAsset = manifest.Assets[0];
            newAsset.Should().Be(asset);
            manifest.Endpoints.Should().HaveCount(1);
            var newEndpoint = manifest.Endpoints[0];
            newEndpoint.Should().Be(endpoint);
        }
 
        private static StaticWebAssetEndpoint CreateEndpoint(StaticWebAsset asset)
        {
            return new StaticWebAssetEndpoint
            {
                Route = asset.ComputeTargetPath("", '/'),
                AssetFile = asset.Identity,
                Selectors = [],
                EndpointProperties = [],
                ResponseHeaders =
                [
                    new()
                    {
                        Name = "Content-Type",
                        Value = "__content-type__"
                    },
                    new()
                    {
                        Name = "Content-Length",
                        Value = "__content-length__",
                    },
                    new()
                    {
                        Name = "ETag",
                        Value = "__etag__",
                    },
                    new()
                    {
                        Name = "Last-Modified",
                        Value = "__last-modified__"
                    }
                ]
            };
        }
 
        public static TheoryData<Action<StaticWebAsset>> GeneratesManifestFailsWhenInvalidAssetsAreProvidedData
        {
            get
            {
                var theoryData = new TheoryData<Action<StaticWebAsset>>
                {
                    a => a.SourceId = "",
                    a => a.SourceType = "",
                    a => a.RelativePath = "",
                    a => a.ContentRoot = "",
                    a => a.OriginalItemSpec = "",
                    a => a.AssetKind = "",
                    a => a.AssetRole = "",
                    a => a.AssetMode = "",
                    a =>
                    {
                        a.AssetRole = "Related";
                        a.RelatedAsset = "";
                    },
                    a =>
                    {
                        a.AssetRole = "Alternative";
                        a.RelatedAsset = "";
                    }
                };
 
                return theoryData;
            }
        }
 
        [Theory]
        [MemberData(nameof(GeneratesManifestFailsWhenInvalidAssetsAreProvidedData))]
        public void GeneratesManifestFailsWhenInvalidAssetsAreProvided(Action<StaticWebAsset> change)
        {
            var errorMessages = new List<string>();
            var buildEngine = new Mock<IBuildEngine>();
            buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
                .Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message));
 
            // GetTempFilePath automatically creates the file, which interferes with the test.
            File.Delete(TempFilePath);
            var asset = CreateAsset(Path.Combine("wwwroot", "candidate.js"), "MyProject", "Computed", "candidate.js", "All", "All");
            change(asset);
            var task = new GenerateStaticWebAssetsManifest
            {
                BuildEngine = buildEngine.Object,
                Assets = new[]
                {
                    asset.ToTaskItem()
                },
                Endpoints = Array.Empty<ITaskItem>(),
                ReferencedProjectsConfigurations = Array.Empty<ITaskItem>(),
                DiscoveryPatterns = Array.Empty<ITaskItem>(),
                BasePath = "/",
                Source = "MyProject",
                ManifestType = "Build",
                Mode = "Default",
                ManifestPath = TempFilePath,
            };
 
            // Act
            var result = task.Execute();
 
            // Assert
            result.Should().Be(false);
        }
 
        public static TheoryData<StaticWebAsset, StaticWebAsset> GeneratesManifestFailsWhenTwoAssetsEndUpOnTheSamePathData
        {
            get
            {
                var data = new TheoryData<StaticWebAsset, StaticWebAsset>
                {
                    // Duplicate assets
                    {
                        CreateAsset(Path.Combine("wwwroot", "candidate.js"), "MyProject", "Computed", "candidate.js", "All", "All"),
                        CreateAsset(Path.Combine("wwwroot", "candidate.js"), "MyProject", "Computed", "candidate.js", "All", "All")
                    },
 
                    // Conflicting Build asssets from different projects
                    {
                        CreateAsset(Path.Combine("wwwroot", "candidate.js"), "Package", "Package", "candidate.js", "All", "Build"),
                        CreateAsset(Path.Combine("wwwroot", "candidate.js"), "OtherProject", "Project", "candidate.js", "All", "Build")
                    },
 
                    // Conflicting Publish asssets from different projects
                    {
                        CreateAsset(Path.Combine("wwwroot", "candidate.js"), "Package", "Package", "candidate.js", "All", "Publish"),
                        CreateAsset(Path.Combine("wwwroot", "candidate.js"), "OtherProject", "Project", "candidate.js", "All", "Publish")
                    },
 
                    // Conflicting All asssets from different projects
                    {
                        CreateAsset(Path.Combine("wwwroot", "candidate.js"), "Package", "Package", "candidate.js", "All", "All"),
                        CreateAsset(Path.Combine("wwwroot", "candidate.js"), "OtherProject", "Project", "candidate.js", "All", "All")
                    },
 
                    // Assets with compatible kinds but from different projects
                    {
                        CreateAsset(Path.Combine("wwwroot", "candidate.js"), "MyProject", "Computed", "candidate.js", "All", "Build"),
                        CreateAsset(Path.Combine("wwwroot", "candidate.js"), "Other", "Project", "candidate.js", "All", "Publish")
                    }
                };
 
                return data;
            }
        }
 
        [Theory]
        [MemberData(nameof(GeneratesManifestFailsWhenTwoAssetsEndUpOnTheSamePathData))]
        public void GeneratesManifestFailsWhenTwoAssetsEndUpOnTheSamePath(StaticWebAsset first, StaticWebAsset second)
        {
            var errorMessages = new List<string>();
            var buildEngine = new Mock<IBuildEngine>();
            buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
                .Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message));
 
            // GetTempFilePath automatically creates the file, which interferes with the test.
            File.Delete(TempFilePath);
            var task = new GenerateStaticWebAssetsManifest
            {
                BuildEngine = buildEngine.Object,
                Assets = new[]
                {
                    first.ToTaskItem(),
                    second.ToTaskItem()
                },
                Endpoints = Array.Empty<ITaskItem>(),
                ReferencedProjectsConfigurations = Array.Empty<ITaskItem>(),
                DiscoveryPatterns = Array.Empty<ITaskItem>(),
                BasePath = "/",
                Source = "MyProject",
                ManifestType = "Build",
                Mode = "Default",
                ManifestPath = TempFilePath,
            };
 
            // Act
            var result = task.Execute();
 
            // Assert
            result.Should().Be(false);
        }
 
 
        [Fact]
        public void GeneratesManifestWithReferencedProjectConfigurations()
        {
            var errorMessages = new List<string>();
            var buildEngine = new Mock<IBuildEngine>();
            buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
                .Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message));
 
            // GetTempFilePath automatically creates the file, which interferes with the test.
            File.Delete(TempFilePath);
            var projectReference = CreateProjectReferenceConfiguration(2, "Other");
            var task = new GenerateStaticWebAssetsManifest
            {
                BuildEngine = buildEngine.Object,
                Assets = Array.Empty<ITaskItem>(),
                Endpoints = Array.Empty<ITaskItem>(),
                ReferencedProjectsConfigurations = new[] { projectReference.ToTaskItem() },
                DiscoveryPatterns = Array.Empty<ITaskItem>(),
                BasePath = "/",
                Source = "MyProject",
                ManifestType = "Build",
                Mode = "Default",
                ManifestPath = TempFilePath,
            };
 
            // Act
            var result = task.Execute();
 
            // Assert
            result.Should().Be(true);
            var manifest = StaticWebAssetsManifest.FromJsonString(File.ReadAllText(TempFilePath));
            manifest.Should().NotBeNull();
            manifest.ReferencedProjectsConfiguration.Should().HaveCount(1);
            var newProjectConfig = manifest.ReferencedProjectsConfiguration[0];
            newProjectConfig.Should().Be(projectReference);
        }
 
        [Fact]
        public void GeneratesManifestWithDiscoveryPatterns()
        {
            var errorMessages = new List<string>();
            var buildEngine = new Mock<IBuildEngine>();
            buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
                .Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message));
 
            // GetTempFilePath automatically creates the file, which interferes with the test.
            File.Delete(TempFilePath);
 
            var candidatePattern = CreatePatternCandidate(Path.Combine("MyProject", "wwwroot"), "base", "wwwroot/**", "MyProject");
            var task = new GenerateStaticWebAssetsManifest
            {
                BuildEngine = buildEngine.Object,
                Assets = Array.Empty<ITaskItem>(),
                Endpoints = Array.Empty<ITaskItem>(),
                ReferencedProjectsConfigurations = Array.Empty<ITaskItem>(),
                DiscoveryPatterns = new[] { candidatePattern.ToTaskItem() },
                BasePath = "/",
                Source = "MyProject",
                ManifestType = "Build",
                Mode = "Default",
                ManifestPath = TempFilePath,
            };
 
            // Act
            var result = task.Execute();
 
            // Assert
            result.Should().Be(true);
            var manifest = StaticWebAssetsManifest.FromJsonString(File.ReadAllText(TempFilePath));
            manifest.Should().NotBeNull();
            manifest.DiscoveryPatterns.Should().HaveCount(1);
            var newProjectConfig = manifest.DiscoveryPatterns[0];
            newProjectConfig.Should().Be(candidatePattern);
        }
 
        private static StaticWebAssetsManifest.ReferencedProjectConfiguration CreateProjectReferenceConfiguration(
            int version,
            string source,
            string publishTargets = "ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems",
            string additionalPublishProperties = ";",
            string additionalPublishPropertiesToRemove = ";WebPublishProfileFile",
            string buildTargets = "GetCurrentProjectBuildStaticWebAssetItems",
            string additionalBuildProperties = ";",
            string additionalBuildPropertiesToRemove = ";WebPublishProfileFile")
        {
            var result = new StaticWebAssetsManifest.ReferencedProjectConfiguration
            {
                Identity = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), $"{source}.csproj")),
                Version = version,
                Source = source,
                GetPublishAssetsTargets = publishTargets,
                AdditionalPublishProperties = additionalPublishProperties,
                AdditionalPublishPropertiesToRemove = additionalPublishPropertiesToRemove,
                GetBuildAssetsTargets = buildTargets,
                AdditionalBuildProperties = additionalBuildProperties,
                AdditionalBuildPropertiesToRemove = additionalBuildPropertiesToRemove
            };
 
            return result;
        }
 
        private static StaticWebAsset CreateAsset(
            string itemSpec,
            string sourceId,
            string sourceType,
            string relativePath,
            string assetKind,
            string assetMode,
            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,
                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",
                LastWriteTime = new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero),
                FileLength = 10,
            };
 
            result.ApplyDefaults();
            result.Normalize();
 
            return result;
        }
 
        private static StaticWebAssetsDiscoveryPattern CreatePatternCandidate(
            string name,
            string basePath,
            string pattern,
            string source)
        {
            var result = new StaticWebAssetsDiscoveryPattern()
            {
                Name = name,
                BasePath = basePath,
                ContentRoot = Directory.GetCurrentDirectory(),
                Pattern = pattern,
                Source = source
            };
 
            return result;
        }
    }
}