From 90685f3ba69a13876aa7367e8f91e9718337a82b Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 18 Jun 2026 11:09:03 +0200 Subject: [PATCH 1/5] Migrate Microsoft.NET.Sdk.StaticWebAssets.Tests to MSTest.Sdk on MTP Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AspNetSdkBaselineTest.cs | 637 ++- .../AssetGroupsIntegrationTest.cs | 424 +- ...cWebAssetsTargetPathsMultiThreadingTest.cs | 217 +- .../ComputeStaticWebAssetsTargetPathsTest.cs | 288 +- .../DeferredAssetGroupsIntegrationTest.cs | 348 +- .../FrameworkAssetsIntegrationTest.cs | 436 ++- .../FrameworkAssetsP2PIntegrationTest.cs | 284 +- .../GroupedFrameworkAssetsIntegrationTest.cs | 232 +- ...NuGetPackageFolderAspNetSdkBaselineTest.cs | 44 +- .../JsModulesIntegrationTest.cs | 726 +++- .../LegacyStaticWebAssetsV1IntegrationTest.cs | 268 +- ...osoft.NET.Sdk.StaticWebAssets.Tests.csproj | 5 +- .../ScopedCssIntegrationTests.cs | 1790 ++++++++- .../StaticWebAssetEndpointsIntegrationTest.cs | 1337 ++++++- .../ApplyCompressionNegotiationTest.cs | 3420 ++++++++++++++++- .../StaticWebAssets/ApplyCssScopesTest.cs | 790 +++- .../AssetGroupFilteringTest.cs | 1854 ++++++++- .../StaticWebAssets/AssetToCompressTest.cs | 362 +- .../StaticWebAssets/ComputeCssScopesTests.cs | 280 +- ...erenceStaticWebAssetsMultiThreadingTest.cs | 230 +- ...ndpointsForReferenceStaticWebAssetsTest.cs | 454 ++- ...ComputeReferenceStaticWebAssetItemsTest.cs | 1024 ++++- ...uteStaticWebAssetsForCurrentProjectTest.cs | 660 +++- .../StaticWebAssets/ConcatenateFilesTest.cs | 760 +++- .../ContentTypeProviderTests.cs | 482 ++- .../DefineStaticWebAssetEndpointsTest.cs | 1786 ++++++++- .../DiscoverDefaultScopedCssItemsTests.cs | 214 +- ...erPrecompressedAssetsMultiThreadingTest.cs | 212 +- .../DiscoverPrecompressedAssetsTest.cs | 236 +- .../DiscoverStaticWebAssetsTest.cs | 2007 +++++++++- .../FilterStaticWebAssetEndpointsTest.cs | 648 +++- .../FilterStaticWebAssetGroupsTest.cs | 646 +++- .../FingerprintPatternMatcherTest.cs | 280 +- .../GeneratePackageAssetsManifestFileTest.cs | 792 +++- .../GeneratePackageAssetsTargetsFileTest.cs | 272 +- ...rateStaticWebAssetEndpointsManifestTest.cs | 1000 ++++- ...ateStaticWebAssetEndpointsPropsFileTest.cs | 442 ++- ...eStaticWebAssetsDevelopmentManifestTest.cs | 1564 +++++++- .../GenerateStaticWebAssetsManifestTest.cs | 930 ++++- .../GenerateStaticWebAssetsPropsFileTest.cs | 1786 ++++++++- .../GenerateV1StaticWebAssetsManifestTest.cs | 562 ++- .../Globbing/PathTokenizerTest.cs | 348 +- ...icWebAssetGlobMatcherTest.Compatibility.cs | 765 +++- .../Globbing/StaticWebAssetGlobMatcherTest.cs | 1250 +++++- ...nfigurationPropertiesMultiThreadingTest.cs | 268 +- .../MergeConfigurationPropertiesTest.cs | 614 ++- .../OverrideHtmlAssetPlaceholdersTest.cs | 672 +++- .../ReadPackageAssetsManifestTest.cs | 1008 ++++- .../ReadStaticWebAssetsManifestFileTest.cs | 776 +++- .../ResolveAllScopedCssAssetsTest.cs | 256 +- .../ResolveCompressedAssetsTest.cs | 808 +++- ...tedStaticWebAssetEndpointsForAssetsTest.cs | 612 ++- .../StaticWebAssets/RewriteCssTest.cs | 892 ++++- .../StaticWebAssetEndpointTest.cs | 168 +- .../StaticWebAssetPathPatternTest.cs | 1640 +++++++- .../StaticWebAssetTaskEnvironmentTests.cs | 540 ++- .../StaticWebAssets/StaticWebAssetTest.cs | 999 ++++- ...eratePackagePropsFileMultiThreadingTest.cs | 126 +- ...icWebAssetsGeneratePackagePropsFileTest.cs | 104 +- ...ateExternallyDefinedStaticWebAssetsTest.cs | 1074 +++++- .../UpdatePackageStaticWebAssetsTest.cs | 1470 ++++++- .../UpdateStaticWebAssetEndpointsTest.cs | 778 +++- .../ValidateStaticWebAssetsUniquePathsTest.cs | 446 ++- .../StaticWebAssetsBaselineComparer.cs | 524 ++- .../StaticWebAssetsBaselineFactory.cs | 489 ++- ...aticWebAssetsCompressionIntegrationTest.cs | 427 +- .../StaticWebAssetsCrossTargetingTests.cs | 228 +- .../StaticWebAssetsDesignTimeTest.cs | 336 +- .../StaticWebAssetsFingerprintingTest.cs | 552 ++- .../StaticWebAssetsIntegrationTest.cs | 2005 +++++++++- .../StaticWebAssetsPackIntegrationTest.cs | 2808 +++++++++++++- .../TheoryData.cs | 21 + 72 files changed, 52331 insertions(+), 1402 deletions(-) create mode 100644 test/Microsoft.NET.Sdk.StaticWebAssets.Tests/TheoryData.cs diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AspNetSdkBaselineTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AspNetSdkBaselineTest.cs index 74e16f6b04bd..0f2898d99d87 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AspNetSdkBaselineTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AspNetSdkBaselineTest.cs @@ -1,326 +1,943 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Reflection; + + using System.Runtime.CompilerServices; + + using System.Text.Json; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { - [Trait("AspNetCore", "BaselineTest")] + + + [TestCategory("BaselineTest")] + [TestProperty("AspNetCore", "BaselineTest")] public class AspNetSdkBaselineTest : AspNetSdkTest + + { + + private static readonly JsonSerializerOptions BaselineSerializationOptions = new() { WriteIndented = true }; + + private readonly StaticWebAssetsBaselineComparer _comparer; + + private readonly StaticWebAssetsBaselineFactory _baselineFactory; + + + + private string _baselinesFolder; + + + + #if GENERATE_SWA_BASELINES + + public static bool GenerateBaselines = true; + + #else + + public static bool GenerateBaselines = bool.TryParse(Environment.GetEnvironmentVariable("ASPNETCORE_TEST_BASELINES"), out var result) && result; + + #endif + + + + private bool _generateBaselines = GenerateBaselines; - public AspNetSdkBaselineTest(ITestOutputHelper log) : base(log) + public AspNetSdkBaselineTest() + { - TestAssembly = Assembly.GetCallingAssembly(); + + TestAssembly = GetType().Assembly; + var testAssemblyMetadata = TestAssembly.GetCustomAttributes(); + RuntimeVersion = testAssemblyMetadata.SingleOrDefault(a => a.Key == "NetCoreAppRuntimePackageVersion").Value; + DefaultPackageVersion = testAssemblyMetadata.SingleOrDefault(a => a.Key == "DefaultTestBaselinePackageVersion").Value; + _comparer = CreateBaselineComparer(); + _baselineFactory = CreateBaselineFactory(); + } + + + protected void EnsureLocalPackagesExists() + + { + + var packTransitiveDependency = CreatePackCommand(ProjectDirectory, "RazorPackageLibraryTransitiveDependency"); + + ExecuteCommand(packTransitiveDependency).Should().Pass(); + + + + var packDirectDependency = CreatePackCommand(ProjectDirectory, "RazorPackageLibraryDirectDependency"); + + ExecuteCommand(packDirectDependency).Should().Pass(); - } - public AspNetSdkBaselineTest(ITestOutputHelper log, bool generateBaselines) : this(log) - { - _generateBaselines = generateBaselines; - _comparer = CreateBaselineComparer(); + } + + + public TestAsset ProjectDirectory { get; set; } + + + + public string RuntimeVersion { get; set; } + + + + public string DefaultPackageVersion { get; set; } + + + + public string BaselinesFolder => + + _baselinesFolder ??= ComputeBaselineFolder(); + + + + protected Assembly TestAssembly { get; } + + + + protected virtual StaticWebAssetsBaselineComparer CreateBaselineComparer() => StaticWebAssetsBaselineComparer.Instance; + + + + private static StaticWebAssetsBaselineFactory CreateBaselineFactory() => StaticWebAssetsBaselineFactory.Instance; + + + + protected virtual string ComputeBaselineFolder() => + + Path.Combine(SdkTestContext.GetRepoRoot() ?? AppContext.BaseDirectory, "test", "Microsoft.NET.Sdk.StaticWebAssets.Tests", "StaticWebAssetsBaselines"); + + + + protected virtual string EmbeddedResourcePrefix => string.Join('.', "Microsoft.NET.Sdk.StaticWebAssets.Tests", "StaticWebAssetsBaselines"); + + + + public StaticWebAssetsManifest LoadBuildManifest(string suffix = "", [CallerMemberName] string name = "") + + { + + if (_generateBaselines) + + { + + return default; + + } + + else + + { + + using var stream = GetManifestEmbeddedResource(suffix, name, "Build"); + + var manifest = StaticWebAssetsManifest.FromStream(stream); + + return manifest; + + } + + } + + + + public StaticWebAssetsManifest LoadPublishManifest(string suffix = "", [CallerMemberName] string name = "") + + { + + if (_generateBaselines) + + { + + return default; + + } + + else + + { + + using var stream = GetManifestEmbeddedResource(suffix, name, "Publish"); + + var manifest = StaticWebAssetsManifest.FromStream(stream); + + return manifest; + + } + + } + + + + protected void AssertBuildAssets( + + StaticWebAssetsManifest manifest, + + string outputFolder, + + string intermediateOutputPath, + + string suffix = "", + + [CallerMemberName] string name = "") + + { + + var fileEnumerationOptions = new EnumerationOptions { RecurseSubdirectories = true }; + + var wwwRootFolder = Path.Combine(outputFolder, "wwwroot"); + + var wwwRootFiles = Directory.Exists(wwwRootFolder) ? + + Directory.GetFiles(wwwRootFolder, "*", fileEnumerationOptions) : + + []; + + + + var computedFiles = manifest.Assets + + .Where(a => a.SourceType is StaticWebAsset.SourceTypes.Computed && + + a.AssetKind is not StaticWebAsset.AssetKinds.Publish); + + + + // We keep track of assets that need to be copied to the output folder. + + // In addition to that, we copy assets that are defined somewhere different + + // from their content root folder when the content root does not match the output folder. + + // We do this to allow copying things like Publish assets to temporary locations during the + + // build process if they are later on going to be transformed. + + var copyToOutputDirectoryAssets = manifest.Assets.Where(a => a.ShouldCopyToOutputDirectory()).ToArray(); + + var temporaryAsssets = manifest.Assets + + .Where(a => + + !a.HasContentRoot(Path.Combine(outputFolder, "wwwroot")) && + + File.Exists(a.Identity) && + + !File.Exists(Path.Combine(a.ContentRoot, a.RelativePath)) && + + a.AssetTraitName != "Content-Encoding").ToArray(); + + + + var copyToOutputDirectoryFiles = copyToOutputDirectoryAssets + + .Select(a => Path.GetFullPath(Path.Combine(outputFolder, "wwwroot", a.RelativePath))) + + .Concat(temporaryAsssets + + .Select(a => Path.GetFullPath(Path.Combine(a.ContentRoot, a.RelativePath)))) + + .ToArray(); + + + + var existingFiles = _baselineFactory.TemplatizeExpectedFiles( + + wwwRootFiles + + .Concat(computedFiles.Select(a => a.Identity)) + + .Concat(copyToOutputDirectoryFiles) + + .Distinct() + + .OrderBy(f => f, StringComparer.Ordinal) - .ToArray(), + + + .ToArray(), + + GetNuGetCachePath() ?? SdkTestContext.Current.NuGetCachePath, + + ProjectDirectory.TestRoot, + + intermediateOutputPath, + + outputFolder).ToArray(); + + + + + + if (!_generateBaselines) + + { + + var expected = LoadExpectedFilesBaseline(manifest.ManifestType, suffix, name) + + .OrderBy(f => f, StringComparer.Ordinal); + + + + AssertFilesCore(existingFiles, expected); + + } + + else + + { + + File.WriteAllText( + + GetExpectedFilesPath(suffix, name, manifest.ManifestType), + + JsonSerializer.Serialize(existingFiles, BaselineSerializationOptions)); + + } + + } + + + + private static void AssertFilesCore(IEnumerable existingFiles, IEnumerable expected) + + { + + var existingSet = new HashSet(existingFiles); + + var expectedSet = new HashSet(expected); + + var different = new HashSet(existingFiles); + + + + different.SymmetricExceptWith(expectedSet); + + + + var messages = new List(); + + if (existingSet.Count < expectedSet.Count) + + { + + messages.Add("The build produced less files than expected."); + + } + + else if (expectedSet.Count < existingSet.Count) + + { + + messages.Add("The build produced more files than expected."); + + } + + else if (different.Count > 0) + + { + + messages.Add("The build produced different files than expected."); + + } + + + + ComputeDifferences(expectedSet, different, messages); + + string.Join(Environment.NewLine, messages).Should().BeEmpty(); + + + + static void ComputeDifferences(HashSet existingSet, HashSet different, List messages) + + { + + foreach (var file in different) + + { + + if (existingSet.Contains(file)) + + { + + messages.Add($"The file '{file}' is not in the baseline."); + + } + + else + + { + + messages.Add($"The file '{file}' is missing from the build."); + + } + + } + + } + + } + + + + protected void AssertPublishAssets( + + StaticWebAssetsManifest manifest, + + string publishFolder, + + string intermediateOutputPath, + + string suffix = "", + + [CallerMemberName] string name = "") + + { + + var fileEnumerationOptions = new EnumerationOptions { RecurseSubdirectories = true }; + + string wwwRootFolder = Path.Combine(publishFolder, "wwwroot"); + + var wwwRootFiles = Directory.Exists(wwwRootFolder) ? + + Directory.GetFiles(wwwRootFolder, "*", fileEnumerationOptions) + + .Select(f => _baselineFactory.TemplatizeFilePath(f, null, null, intermediateOutputPath, publishFolder, null)) : + + []; + + + + // Computed publish assets must exist on disk (we do this check to quickly identify when something is not being + + // generated vs when its being copied to the wrong place) + + var computedFiles = manifest.Assets + + .Where(a => a.SourceType is StaticWebAsset.SourceTypes.Computed && + + a.AssetKind is not StaticWebAsset.AssetKinds.Build); + + + + // For assets that are copied to the publish folder, the path is always based on + + // the wwwroot folder, the relative path and the base path for project or package + + // assets. + + var copyToPublishDirectoryFiles = manifest.Assets + + .Where(a => !string.Equals(a.SourceId, manifest.Source, StringComparison.Ordinal) || + + !string.Equals(a.AssetMode, StaticWebAsset.AssetModes.Reference)) + + .Select(a => Path.Combine(wwwRootFolder, a.ComputeTargetPath("", Path.DirectorySeparatorChar))); + + + + var existingFiles = _baselineFactory.TemplatizeExpectedFiles( + + [.. wwwRootFiles + + .Concat(computedFiles.Select(a => a.Identity)) + + .Concat(copyToPublishDirectoryFiles) + + .Distinct() + + .OrderBy(f => f, StringComparer.Ordinal)], + + GetNuGetCachePath() ?? SdkTestContext.Current.NuGetCachePath, + + ProjectDirectory.TestRoot, + + intermediateOutputPath, + + publishFolder); + + + + if (!_generateBaselines) + + { + + var expected = LoadExpectedFilesBaseline(manifest.ManifestType, suffix, name); + + existingFiles.Should().BeEquivalentTo(expected); + + } + + else + + { + + File.WriteAllText( + + GetExpectedFilesPath(suffix, name, manifest.ManifestType), + + JsonSerializer.Serialize(existingFiles, BaselineSerializationOptions)); + + } + + } + + + + public string[] LoadExpectedFilesBaseline( + + string type, + + string suffix, + + string name) + + { + + if (!_generateBaselines) + + { + + using var filesBaselineStream = GetExpectedFilesEmbeddedResource(suffix, name, type); + + return JsonSerializer.Deserialize(filesBaselineStream); + + } + + else + + { + + return []; + + } + + } + + + + internal void AssertManifest( + + StaticWebAssetsManifest actual, + + StaticWebAssetsManifest expected, + + string suffix = "", + + string runtimeIdentifier = null, + + [CallerMemberName] string name = "") + + { + + if (!_generateBaselines) + + { + + // We are going to compare the generated manifest with the current manifest. + + // For that, we "templatize" the current manifest to avoid issues with hashes, versions, etc. + + _baselineFactory.ToTemplate( + + actual, + + ProjectDirectory.Path, + + GetNuGetCachePath() ?? SdkTestContext.Current.NuGetCachePath, + + runtimeIdentifier); + + + + _comparer.AssertManifest(expected, actual); + + } + + else + + { + + var template = Templatize(actual, ProjectDirectory.Path, GetNuGetCachePath() ?? SdkTestContext.Current.NuGetCachePath, runtimeIdentifier); + + if (!Directory.Exists(Path.Combine(BaselinesFolder))) + + { + + Directory.CreateDirectory(Path.Combine(BaselinesFolder)); + + } + + + + File.WriteAllText(GetManifestPath(suffix, name, actual.ManifestType), template); + + } + + } + + + + private string GetManifestPath(string suffix, string name, string manifestType) + + => Path.Combine(BaselinesFolder, $"{name}{(!string.IsNullOrEmpty(suffix) ? $"_{suffix}" : "")}.{manifestType}.staticwebassets.json"); + + + + private Stream GetManifestEmbeddedResource(string suffix, string name, string manifestType) + + => TestAssembly.GetManifestResourceStream(string.Join('.', EmbeddedResourcePrefix, $"{name}{(!string.IsNullOrEmpty(suffix) ? $"_{suffix}" : "")}.{manifestType}.staticwebassets.json")); + + + + + + private string GetExpectedFilesPath(string suffix, string name, string manifestType) + + => Path.Combine(BaselinesFolder, $"{name}{(!string.IsNullOrEmpty(suffix) ? $"_{suffix}" : "")}.{manifestType}.files.json"); + + + + private Stream GetExpectedFilesEmbeddedResource(string suffix, string name, string manifestType) + + => TestAssembly.GetManifestResourceStream(string.Join('.', EmbeddedResourcePrefix, $"{name}{(!string.IsNullOrEmpty(suffix) ? $"_{suffix}" : "")}.{manifestType}.files.json")); + + + + private string Templatize(StaticWebAssetsManifest manifest, string projectRoot, string restorePath, string runtimeIdentifier) + + { + + _baselineFactory.ToTemplate(manifest, projectRoot, restorePath, runtimeIdentifier); + + return JsonSerializer.Serialize(manifest, BaselineSerializationOptions); + + } + + } + + } diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AssetGroupsIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AssetGroupsIntegrationTest.cs index 2de1933ee3f4..e9ffd750e2b4 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AssetGroupsIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AssetGroupsIntegrationTest.cs @@ -1,204 +1,612 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.IO.Compression; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { - public class AssetGroupsIntegrationTest(ITestOutputHelper log) - : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, nameof(AssetGroupsIntegrationTest)) + + + [TestClass] + + public class AssetGroupsIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest + { - [Fact] + + protected override string RestoreNugetPackagePath => nameof(AssetGroupsIntegrationTest); + + + [TestMethod] + + public void Pack_NupkgContains_GroupedStaticWebAssets() + + { + + var packagePath = PackIdentityUILib("Pack_Nupkg"); + + + + new FileInfo(packagePath).Should().Exist(); + + + + using var archive = ZipFile.OpenRead(packagePath); + + var entryNames = archive.Entries.Select(e => e.FullName).ToList(); + + + + var expectedPatterns = new[] + + { + + "staticwebassets/V4/css/site.css", + + "staticwebassets/V4/js/site.js", + + "staticwebassets/V5/css/site.css", + + "staticwebassets/V5/js/site.js", + + "build/IdentityUILib.PackageAssets.json", + + "build/Microsoft.AspNetCore.StaticWebAssets.targets", + + "build/StaticWebAssets.Groups.targets", + + "build/IdentityUILib.targets", + + "buildMultiTargeting/IdentityUILib.targets", + + "buildTransitive/IdentityUILib.targets", + + }; + + + + foreach (var pattern in expectedPatterns) + + { + + entryNames.Should().Contain( + + e => e.Replace('\\', '/').EndsWith(pattern, StringComparison.OrdinalIgnoreCase), + + $"nupkg should contain entry matching '{pattern}'"); + + } + + } - [Fact] + + + + + [TestMethod] + + public void Pack_PropsFile_ContainsAssetGroups_Metadata() + + { + + var packagePath = PackIdentityUILib("Pack_Props"); + + + + new FileInfo(packagePath).Should().Exist(); + + + + using var archive = ZipFile.OpenRead(packagePath); + + var manifestEntry = archive.Entries.FirstOrDefault( + + e => e.FullName.Equals("build/IdentityUILib.PackageAssets.json", StringComparison.OrdinalIgnoreCase)); + + + + manifestEntry.Should().NotBeNull("the nupkg should contain a PackageAssets.json manifest file"); + + + + using var stream = manifestEntry.Open(); + + using var reader = new StreamReader(stream); + + var manifestContent = reader.ReadToEnd(); + + + + // V5 assets should carry AssetGroups metadata containing BootstrapVersion=V5 + + manifestContent.Should().Contain("BootstrapVersion=V5", + + "V5 assets should have AssetGroups metadata with BootstrapVersion=V5"); + + + + // V4 assets should carry AssetGroups metadata containing BootstrapVersion=V4 + + manifestContent.Should().Contain("BootstrapVersion=V4", + + "V4 assets should have AssetGroups metadata with BootstrapVersion=V4"); + + } - [Fact] + + + + + [TestMethod] + + public void Build_ConsumerDefault_ExcludesGroupedAssets() + + { + + var manifest = BuildConsumer("Build_Default", "IdentityUIConsumer"); + + + + // The library's StaticWebAssets.Groups.props defaults IdentityUIFrameworkVersion=V5, + + // so V5 is selected automatically and V4 is excluded. + + var v4Assets = manifest.Assets + + .Where(a => (a.AssetGroups ?? "").Contains("V4")) + + .ToList(); + + + + var v5PrimaryAssets = manifest.Assets + + .Where(a => (a.AssetGroups ?? "").Contains("V5") && a.AssetRole == "Primary") + + .ToList(); + + + + v4Assets.Should().BeEmpty("V4 grouped assets should be excluded when the default selects V5"); + + v5PrimaryAssets.Should().HaveCountGreaterThan(1, + + "V5 should be the default group — at least css/site.css and js/site.js expected"); + + } - [Fact] + + + + + [TestMethod] + + public void Build_ConsumerV4_IncludesOnlyV4Assets() + + { + + var manifest = BuildConsumer("Build_V4", "IdentityUIConsumerV4"); + + + + var includedAssets = manifest.Assets + + .Where(a => (a.AssetGroups ?? "").Contains("V4") && a.AssetRole == "Primary") + + .ToList(); + + + + includedAssets.Should().HaveCountGreaterThan(1, + + "V4 assets should be included when consumer selects V4 — at least css/site.css and js/site.js"); + + + + var excludedAssets = manifest.Assets + + .Where(a => (a.AssetGroups ?? "").Contains("V5")) + + .ToList(); + + + + excludedAssets.Should().BeEmpty( + + "V5 assets should be excluded when consumer only selects V4"); + + + + var includedAssetFiles = new HashSet(includedAssets.Select(a => a.Identity)); + + var includedEndpoints = manifest.Endpoints + + ?.Where(e => includedAssetFiles.Contains(e.AssetFile)) + + .ToList(); + + + + includedEndpoints.Should().NotBeNull().And.HaveCountGreaterThan(1, + + "endpoints should exist for V4 assets — at least one per asset"); + + + + includedEndpoints.Should().AllSatisfy(e => + + e.Route.Should().NotContain("V4/", + + "file-only segment (~) should be excluded from endpoint routes")); + + } - [Fact] + + + + + [TestMethod] + + public void Build_ConsumerV5_IncludesOnlyV5Assets() + + { + + var manifest = BuildConsumer("Build_V5", "IdentityUIConsumerV5"); + + + + var includedAssets = manifest.Assets + + .Where(a => (a.AssetGroups ?? "").Contains("V5") && a.AssetRole == "Primary") + + .ToList(); + + + + includedAssets.Should().HaveCountGreaterThan(1, + + "V5 assets should be included when consumer selects V5 — at least css/site.css and js/site.js"); + + + + var excludedAssets = manifest.Assets + + .Where(a => (a.AssetGroups ?? "").Contains("V4")) + + .ToList(); + + + + excludedAssets.Should().BeEmpty( + + "V4 assets should be excluded when consumer only selects V5"); + + + + var includedAssetFiles = new HashSet(includedAssets.Select(a => a.Identity)); + + var includedEndpoints = manifest.Endpoints + + ?.Where(e => includedAssetFiles.Contains(e.AssetFile)) + + .ToList(); + + + + includedEndpoints.Should().NotBeNull().And.HaveCountGreaterThan(1, + + "endpoints should exist for V5 assets — at least one per asset"); + + + + includedEndpoints.Should().AllSatisfy(e => + + e.Route.Should().NotContain("V5/", + + "file-only segment (~) should be excluded from endpoint routes")); + + } + + + + // Clear the cached package so NuGet re-extracts from the freshly-packed nupkg. + + private void ClearCachedPackage(string packageId) + + { + + var cached = Path.Combine(GetNuGetCachePath(), packageId); + + if (Directory.Exists(cached)) + + { + + Directory.Delete(cached, recursive: true); + + } + + } + + + + private string PackIdentityUILib(string identifier) + + { + + var testAsset = "AssetGroupsSample"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset, identifier: identifier); + + + + var pack = CreatePackCommand(ProjectDirectory, "IdentityUILib"); + + ExecuteCommand(pack).Should().Pass(); + + + + return Path.Combine( + + ProjectDirectory.TestRoot, + + "TestPackages", + + "IdentityUILib.1.0.0.nupkg"); + + } + + + + private StaticWebAssetsManifest BuildConsumer(string identifier, string consumerProject) + + { + + var testAsset = "AssetGroupsSample"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset, identifier: identifier); + + + + var pack = CreatePackCommand(ProjectDirectory, "IdentityUILib"); + + ExecuteCommand(pack).Should().Pass(); + + ClearCachedPackage("identityuilib"); + + + + var restore = CreateRestoreCommand(ProjectDirectory, consumerProject); + + ExecuteCommand(restore).Should().Pass(); + + + + var build = CreateBuildCommand(ProjectDirectory, consumerProject); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var manifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + new FileInfo(manifestPath).Should().Exist(); + + + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(manifestPath)); + + manifest.Should().NotBeNull(); + + return manifest; + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsMultiThreadingTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsMultiThreadingTest.cs index 41d1fcff1b07..a60977797cfe 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsMultiThreadingTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsMultiThreadingTest.cs @@ -1,108 +1,311 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; -[CollectionDefinition("ProcessState", DisableParallelization = true)] -public class ProcessStateCollection -{ -} -[Collection("ProcessState")] + + + +[DoNotParallelize] + + +[TestClass] + public class ComputeStaticWebAssetsTargetPathsMultiThreadingTest + + { - [Fact] + + + [TestMethod] + + public void ResolvesRelativeContentRootAgainstTaskEnvironmentProjectDirectoryNotProcessCurrentDirectory() + + { + + var testRoot = Path.Combine(AppContext.BaseDirectory, nameof(ComputeStaticWebAssetsTargetPathsMultiThreadingTest), Guid.NewGuid().ToString("N")); + + var projectDir = Path.Combine(testRoot, "project"); + + var spawnDir = Path.Combine(testRoot, "decoy", "spawn"); + + Directory.CreateDirectory(projectDir); + + Directory.CreateDirectory(spawnDir); + + + + const string relativeContentRoot = "wwwroot"; + + + + var projectAbsoluteContentRoot = Path.GetFullPath(Path.Combine(projectDir, relativeContentRoot)); + + var spawnAbsoluteContentRoot = Path.GetFullPath(Path.Combine(spawnDir, relativeContentRoot)); + + projectAbsoluteContentRoot.Should().NotBe(spawnAbsoluteContentRoot, + + "the test setup must place project and decoy in different parents so a relative path resolves differently against each"); + + + + var originalCurrentDirectory = Directory.GetCurrentDirectory(); + + try + + { + + Directory.SetCurrentDirectory(spawnDir); + + + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeStaticWebAssetsTargetPaths + + { + + BuildEngine = buildEngine.Object, + + TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir), + + Assets = [CreateCandidateWithRelativeContentRoot(Path.Combine(relativeContentRoot, "candidate.js"), relativeContentRoot)], + + PathPrefix = "wwwroot", + + }; + + + + var result = task.Execute(); + + + + result.Should().Be(true, "the task must run to completion when TaskEnvironment.ProjectDirectory differs from the process CWD"); + + errorMessages.Should().BeEmpty(); + + task.AssetsWithTargetPath.Should().ContainSingle(); + + + + // The relative ContentRoot must be absolutized against the task's ProjectDirectory, + + // not the process current working directory (the decoy). This is the multithreaded-safe behavior. + + var contentRoot = task.AssetsWithTargetPath[0].GetMetadata("ContentRoot"); + + contentRoot.Should().StartWith(projectAbsoluteContentRoot); + + contentRoot.Should().NotStartWith(spawnAbsoluteContentRoot); + + + + // The computed TargetPath is unaffected by the ContentRoot resolution. + + task.AssetsWithTargetPath[0].GetMetadata("TargetPath").Should().Be(Path.Combine("wwwroot", "candidate.js")); + + } + + finally + + { + + Directory.SetCurrentDirectory(originalCurrentDirectory); + + if (Directory.Exists(testRoot)) + + { + + try { Directory.Delete(testRoot, recursive: true); } catch { } + + } + + } + + } + + + + private static ITaskItem CreateCandidateWithRelativeContentRoot(string itemSpec, string relativeContentRoot) + + { + + // Intentionally skips Normalize() so the relative ContentRoot reaches the task unmodified. + + var result = new StaticWebAsset() + + { + + Identity = Path.GetFullPath(itemSpec), + + SourceId = "MyPackage", + + SourceType = "Discovered", + + ContentRoot = relativeContentRoot, + + BasePath = "base", + + RelativePath = "candidate.js", + + AssetKind = "All", + + AssetMode = "All", + + AssetRole = "Primary", + + RelatedAsset = "", + + AssetTraitName = "", + + AssetTraitValue = "", + + CopyToOutputDirectory = "", + + CopyToPublishDirectory = "", + + OriginalItemSpec = itemSpec, + + // Add these to avoid accessing the disk to compute them + + Integrity = "integrity", + + Fingerprint = "fingerprint", + + FileLength = 10, + + LastWriteTime = DateTime.UtcNow, + + }; + + + + return result.ToTaskItem(); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsTest.cs index 8bf1bafb89dd..24ca68de7bec 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsTest.cs @@ -2,139 +2,421 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System; + + using System.Collections.Generic; + + using System.Linq; + + using System.Text; + + using System.Threading.Tasks; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + + +[TestClass] + public class ComputeStaticWebAssetsTargetPathsTest + + { - [Fact] + + + [TestMethod] + + public void IncludesFingerprintInFileWhenPreferred() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + + + var task = new ComputeStaticWebAssetsTargetPaths + + { + + BuildEngine = buildEngine.Object, + + Assets = [CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate#[.{fingerprint}]!.js", "All", "All", fingerprint: "1234asdf")], + + PathPrefix = "wwwroot", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.AssetsWithTargetPath.Should().ContainSingle(); + + var asset = task.AssetsWithTargetPath[0]; + + asset.Should().NotBeNull(); + + asset.GetMetadata("TargetPath").Should().Be(Path.Combine("wwwroot", "candidate.1234asdf.js")); + + } - [Fact] + + + + + [TestMethod] + + public void IncludesFingerprintInFileWhenRequired() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + + + var task = new ComputeStaticWebAssetsTargetPaths + + { + + BuildEngine = buildEngine.Object, + + Assets = [CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate#[.{fingerprint}].js", "All", "All", fingerprint: "1234asdf")], + + PathPrefix = "wwwroot", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.AssetsWithTargetPath.Should().ContainSingle(); + + var asset = task.AssetsWithTargetPath[0]; + + asset.Should().NotBeNull(); + + asset.GetMetadata("TargetPath").Should().Be(Path.Combine("wwwroot", "candidate.1234asdf.js")); + + } - [Fact] + + + + + [TestMethod] + + public void DoesNotIncludeFingerprintInFileWhenNotPreferred() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + + + var task = new ComputeStaticWebAssetsTargetPaths + + { + + BuildEngine = buildEngine.Object, + + Assets = [CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate#[.{fingerprint}]?.js", "All", "All", fingerprint: "1234asdf")], + + PathPrefix = "wwwroot", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.AssetsWithTargetPath.Should().ContainSingle(); + + var asset = task.AssetsWithTargetPath[0]; + + asset.Should().NotBeNull(); + + asset.GetMetadata("TargetPath").Should().Be(Path.Combine("wwwroot", "candidate.js")); + + } + + + + private static ITaskItem CreateCandidate( + + string itemSpec, + + string sourceId, + + string sourceType, + + string relativePath, + + string assetKind, + + string assetMode, + + string fingerprint = null) + + { + + var result = new StaticWebAsset() + + { + + Identity = Path.GetFullPath(itemSpec), + + SourceId = sourceId, + + SourceType = sourceType, + + ContentRoot = Directory.GetCurrentDirectory(), + + BasePath = "base", + + RelativePath = relativePath, + + AssetKind = assetKind, + + AssetMode = assetMode, + + AssetRole = "Primary", + + RelatedAsset = "", + + AssetTraitName = "", + + AssetTraitValue = "", + + CopyToOutputDirectory = "", + + CopyToPublishDirectory = "", + + OriginalItemSpec = itemSpec, + + // Add these to avoid accessing the disk to compute them + + Integrity = "integrity", + + Fingerprint = fingerprint ?? "fingerprint", + + FileLength = 10, + + LastWriteTime = DateTime.UtcNow, + + }; + + + + result.ApplyDefaults(); + + result.Normalize(); + + + + return result.ToTaskItem(); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/DeferredAssetGroupsIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/DeferredAssetGroupsIntegrationTest.cs index 52cd072926b7..106f71f47b94 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/DeferredAssetGroupsIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/DeferredAssetGroupsIntegrationTest.cs @@ -1,169 +1,509 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Text.Json; + + using System.Xml.Linq; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { - public class DeferredAssetGroupsIntegrationTest(ITestOutputHelper log) : AspNetSdkTest(log) + + + [TestClass] + + public class DeferredAssetGroupsIntegrationTest : AspNetSdkTest + + { - [Fact] + + + [TestMethod] + + public void Build_DeferredGroupEnabled_IncludesGroupedAssetAndEndpoints() + + { + + var intermediateOutputPath = BuildWithDeferredGroup(enableBlazorGroup: "enabled"); + + var manifest = LoadBuildManifest(intermediateOutputPath); + + var endpoints = LoadEndpointsManifest(intermediateOutputPath); + + + + // Primary asset should be present + + var primaryAsset = manifest.Assets + + .Where(a => a.RelativePath == "deferred.blazor.js" && a.AssetRole == "Primary") + + .ToList(); + + primaryAsset.Should().ContainSingle("deferred.blazor.js primary asset should be present when BlazorGroup=enabled"); + + + + // Compressed alternatives should be present + + var compressedAssets = manifest.Assets + + .Where(a => a.RelativePath.StartsWith("deferred.blazor.js.") && a.AssetRole == "Alternative" && a.AssetTraitName == "Content-Encoding") + + .ToList(); + + compressedAssets.Should().NotBeEmpty("compressed alternatives (gzip/brotli) should exist for deferred.blazor.js"); + + + + // Uncompressed endpoint (no selector) + + var primaryEndpoints = endpoints + + .Where(e => e.Route.EndsWith("deferred.blazor.js") && e.Selectors.Length == 0) + + .ToList(); + + primaryEndpoints.Should().ContainSingle("an uncompressed endpoint should exist for deferred.blazor.js"); + + + + // Compressed endpoints (with Content-Encoding selector on the same route) + + var compressedEndpoints = endpoints + + .Where(e => e.Route.EndsWith("deferred.blazor.js") && e.Selectors.Length == 1 && e.Selectors[0].Name == "Content-Encoding") + + .ToList(); + + compressedEndpoints.Should().NotBeEmpty("compressed endpoints with Content-Encoding selector should exist for deferred.blazor.js"); + + + + // Direct .gz/.br route endpoints + + var directCompressedEndpoints = endpoints + + .Where(e => e.Route.EndsWith("deferred.blazor.js.gz") || e.Route.EndsWith("deferred.blazor.js.br")) + + .ToList(); + + directCompressedEndpoints.Should().NotBeEmpty("direct .gz/.br route endpoints should exist for deferred.blazor.js"); + + + + // Existing non-grouped assets should be unaffected + + var existingEndpoints = endpoints + + .Where(e => e.Route.Contains("project-transitive-dep.js")) + + .ToList(); + + existingEndpoints.Should().NotBeEmpty("endpoints for existing non-grouped assets should be unaffected"); + + } - [Fact] + + + + + [TestMethod] + + public void Build_DeferredGroupDisabled_ExcludesGroupedAssetAndEndpoints() + + { + + var intermediateOutputPath = BuildWithDeferredGroup(enableBlazorGroup: "disabled"); + + var manifest = LoadBuildManifest(intermediateOutputPath); + + var endpoints = LoadEndpointsManifest(intermediateOutputPath); + + + + // Build manifest retains all assets (unfiltered) — the primary asset is still there + + var primaryAsset = manifest.Assets + + .Where(a => a.RelativePath == "deferred.blazor.js" && a.AssetRole == "Primary") + + .ToList(); + + primaryAsset.Should().ContainSingle("build manifest retains all variants; deferred.blazor.js should still be present"); + + + + // But all deferred.blazor.js endpoints should be excluded from the endpoints manifest + + var deferredEndpoints = endpoints + + .Where(e => e.Route.Contains("deferred.blazor.js")) + + .ToList(); + + deferredEndpoints.Should().BeEmpty("no endpoints should exist for deferred.blazor.js when BlazorGroup=disabled"); + + + + // That includes direct .gz/.br routes + + var directCompressedEndpoints = endpoints + + .Where(e => e.Route.EndsWith("deferred.blazor.js.gz") || e.Route.EndsWith("deferred.blazor.js.br")) + + .ToList(); + + directCompressedEndpoints.Should().BeEmpty("no compressed route endpoints should exist for excluded deferred.blazor.js"); + + + + // Existing non-grouped assets should be unaffected + + var existingEndpoints = endpoints + + .Where(e => e.Route.Contains("project-transitive-dep.js")) + + .ToList(); + + existingEndpoints.Should().NotBeEmpty("endpoints for existing non-grouped assets should be unaffected"); + + } + + + + private string BuildWithDeferredGroup(string enableBlazorGroup) + + { + + var testAsset = "RazorAppWithP2PReference"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, identifier: enableBlazorGroup) + + .WithProjectChanges((path, document) => + + { + + if (Path.GetFileName(path) == "ClassLibrary.csproj") + + { + + document.Root.Add( + + new XElement("ItemGroup", + + new XElement("StaticWebAssetGroupDefinition", + + new XAttribute("Include", "BlazorGroup"), + + new XAttribute("Value", "enabled"), + + new XAttribute("Order", "0"), + + new XAttribute("SourceId", "ClassLibrary"), + + new XAttribute("IncludePattern", "deferred.blazor.js")))); + + document.Root.Add( + + new XElement("Import", + + new XAttribute("Project", "StaticWebAssets.Groups.targets"))); + + } + + + + if (Path.GetFileName(path) == "AppWithP2PReference.csproj") + + { + + document.Root.Add( + + new XElement("Import", + + new XAttribute("Project", @"..\ClassLibrary\StaticWebAssets.Groups.targets"))); + + } + + }); + + + + var classLibDir = Path.Combine(projectDirectory.TestRoot, "ClassLibrary"); + + + + File.WriteAllText( + + Path.Combine(classLibDir, "wwwroot", "deferred.blazor.js"), + + "console.log('deferred blazor');"); + + + + File.WriteAllText( + + Path.Combine(classLibDir, "StaticWebAssets.Groups.targets"), + + $""" + + + + + + {enableBlazorGroup} + + + + $(FilterDeferredStaticWebAssetGroupsDependsOn); + + ResolveDeferredBlazorGroup + + + + + + + + + + + + + + + + + + + + + + + + + + """); + + + + var build = CreateBuildCommand(projectDirectory, "AppWithP2PReference"); + + ExecuteCommand(build).Should().Pass(); + + + + return build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + } + + + + private static StaticWebAssetsManifest LoadBuildManifest(string intermediateOutputPath) + + { + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + return StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + } + + + + private static StaticWebAssetEndpoint[] LoadEndpointsManifest(string intermediateOutputPath) + + { + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.endpoints.json"); + + var manifest = JsonSerializer.Deserialize( + + File.ReadAllBytes(path), + + StaticWebAssetsJsonSerializerContext.Default.StaticWebAssetEndpointsManifest); + + return manifest?.Endpoints ?? []; + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsIntegrationTest.cs index 2c87c4b69f0e..8ae2207bd148 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsIntegrationTest.cs @@ -1,209 +1,627 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; +using Microsoft.NET.TestFramework.Commands; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.IO.Compression; + + using System.Text.Json; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { - public class FrameworkAssetsIntegrationTest(ITestOutputHelper log) - : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, nameof(FrameworkAssetsIntegrationTest)) + + + [TestClass] + + public class FrameworkAssetsIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest + { - [Fact] + + protected override string RestoreNugetPackagePath => nameof(FrameworkAssetsIntegrationTest); + + + [TestMethod] + + public void Pack_PropsFile_ContainsFrameworkSourceType_ForMatchedAssets() + + { + + var testAsset = "FrameworkAssetsSample"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var pack = CreatePackCommand(ProjectDirectory, "FrameworkAssetsLib"); + + ExecuteCommand(pack).Should().Pass(); + + + + var packagePath = Path.Combine( + + ProjectDirectory.TestRoot, + + "TestPackages", + + "FrameworkAssetsLib.1.0.0.nupkg"); + + + + new FileInfo(packagePath).Should().Exist(); + + + + // Extract the JSON manifest from the nupkg and verify SourceType + + using var archive = ZipFile.OpenRead(packagePath); + + var manifestEntry = archive.Entries.FirstOrDefault( + + e => e.FullName.Equals("build/FrameworkAssetsLib.PackageAssets.json", StringComparison.OrdinalIgnoreCase)); + + + + manifestEntry.Should().NotBeNull("the nupkg should contain a PackageAssets.json file"); + + + + using var stream = manifestEntry.Open(); + + var manifest = JsonSerializer.Deserialize(stream, + + StaticWebAssetsJsonSerializerContext.Default.StaticWebAssetPackageManifest); + + + + manifest.Should().NotBeNull(); + + manifest.Assets.Should().NotBeEmpty(); + + + + // JS files should be marked as Framework + + manifest.Assets.Values.Should().Contain( + + a => a.SourceType == "Framework", + + "JS assets matching the FrameworkPattern should have SourceType=Framework"); + + + + // CSS files should remain as Package + + manifest.Assets.Values.Should().Contain( + + a => a.SourceType == "Package", + + "CSS assets not matching the FrameworkPattern should have SourceType=Package"); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_NupkgContains_ExpectedStaticWebAssets() + + { + + var testAsset = "FrameworkAssetsSample"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var pack = CreatePackCommand(ProjectDirectory, "FrameworkAssetsLib"); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var packagePath = Path.Combine( + + ProjectDirectory.TestRoot, + + "TestPackages", + + "FrameworkAssetsLib.1.0.0.nupkg"); + + + + result.Should().NuPkgContainsPatterns( + + packagePath, + + filePatterns: new[] + + { + + Path.Combine("staticwebassets", "js", "framework.js"), + + Path.Combine("staticwebassets", "css", "site.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), + + Path.Combine("build", "FrameworkAssetsLib.targets"), + + Path.Combine("build", "FrameworkAssetsLib.PackageAssets.json"), + + Path.Combine("buildMultiTargeting", "FrameworkAssetsLib.targets"), + + Path.Combine("buildTransitive", "FrameworkAssetsLib.targets"), + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Build_Consumer_MaterializesFrameworkAssets() + + { + + var (intermediateOutputPath, _) = BuildFrameworkAssetsConsumer(); + + var manifest = LoadBuildManifest(intermediateOutputPath); + + + + // The framework JS asset should be materialized (SourceType changed from Framework to Discovered) + + var frameworkAssets = manifest.Assets + + .Where(a => a.RelativePath.Contains("framework.js")) + + .ToList(); + + + + frameworkAssets.Should().HaveCountGreaterThan(0, "framework.js should appear in the build manifest"); + + + + // After materialization, the framework asset should have SourceType=Discovered + + // and be under the fx/ intermediate directory + + var materializedAsset = frameworkAssets + + .FirstOrDefault(a => a.Identity.Contains(Path.Combine("fx", "FrameworkAssetsLib"))); + + + + materializedAsset.Should().NotBeNull( + + "framework.js should be materialized under the fx/FrameworkAssetsLib directory"); + + materializedAsset.SourceType.Should().Be("Discovered"); + + materializedAsset.AssetMode.Should().Be("CurrentProject"); + + + + // The CSS asset should remain as a regular Package asset (not materialized) + + var cssAssets = manifest.Assets + + .Where(a => a.RelativePath.Contains("site.css")) + + .ToList(); + + + + cssAssets.Should().HaveCountGreaterThan(0, "site.css should appear in the build manifest"); + + cssAssets.Should().OnlyContain(a => a.SourceType == "Package", + + "CSS assets should remain as Package type since they don't match the FrameworkPattern"); + + } - [Fact] + + + + + [TestMethod] + + public void Build_Consumer_MaterializedFrameworkAsset_FileExistsOnDisk() + + { + + var (intermediateOutputPath, _) = BuildFrameworkAssetsConsumer(); + + + + // The materialized file should exist on disk under the staticwebassets/fx directory + + var fxDir = Path.Combine(intermediateOutputPath, "staticwebassets", "fx", "FrameworkAssetsLib"); + + var materializedFile = Directory.GetFiles(fxDir, "framework.js", SearchOption.AllDirectories); + + + + materializedFile.Should().HaveCount(1, "framework.js should be materialized exactly once"); + + File.ReadAllText(materializedFile[0]).Should().Contain("framework", + + "materialized file should contain the original framework.js content"); + + } - [Fact] + + + + + [TestMethod] + + public void Build_Consumer_EndpointsRemapped_ForFrameworkAssets() + + { + + var (intermediateOutputPath, _) = BuildFrameworkAssetsConsumer(); + + var manifest = LoadBuildManifest(intermediateOutputPath); + + + + // Check that the framework asset in the manifest has been remapped to the materialized path + + var frameworkAssets = manifest.Assets + + .Where(a => a.RelativePath.Contains("framework.js") + + && a.Identity.Contains(Path.Combine("staticwebassets", "fx", "FrameworkAssetsLib"))) + + .ToList(); + + + + frameworkAssets.Should().HaveCountGreaterThan(0, + + "the manifest should contain a materialized framework asset under staticwebassets/fx/"); + + + + // Endpoints for the route should exist (some may be compressed variants) + + var fxEndpoints = manifest.Endpoints + + ?.Where(e => e.Route.Contains("framework.js")) + + .ToList(); + + + + fxEndpoints.Should().NotBeNull().And.HaveCountGreaterThan(0, + + "there should be at least one endpoint for framework.js"); + + + + // At least one endpoint should reference the materialized asset (not all will — compressed endpoints point elsewhere) + + var endpointsPointingToMaterialized = fxEndpoints + + .Where(e => e.AssetFile.Contains(Path.Combine("staticwebassets", "fx", "FrameworkAssetsLib"))) + + .ToList(); + + + + endpointsPointingToMaterialized.Should().HaveCountGreaterThan(0, + + "at least one endpoint for framework.js should point to the materialized file path under staticwebassets/fx/"); + + } - [Fact] + + + + + [TestMethod] + + public void Build_Consumer_IsIncremental() + + { + + var (intermediateOutputPath, _) = BuildFrameworkAssetsConsumer(); + + + + var fxDir = Path.Combine(intermediateOutputPath, "staticwebassets", "fx", "FrameworkAssetsLib"); + + var materializedFile = Directory.GetFiles(fxDir, "framework.js", SearchOption.AllDirectories).Single(); + + var firstWriteTime = File.GetLastWriteTimeUtc(materializedFile); + + + + // Second build — should be incremental (file not re-copied) + + var build2 = CreateBuildCommand(ProjectDirectory, "FrameworkAssetsConsumer"); + + ExecuteCommand(build2).Should().Pass(); + + + + var secondWriteTime = File.GetLastWriteTimeUtc(materializedFile); + + secondWriteTime.Should().Be(firstWriteTime, + + "framework asset should not be re-copied on incremental build"); + + } + + + + private (string intermediateOutputPath, BuildCommand build) BuildFrameworkAssetsConsumer() + + { + + var testAsset = "FrameworkAssetsSample"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var pack = CreatePackCommand(ProjectDirectory, "FrameworkAssetsLib"); + + ExecuteCommand(pack).Should().Pass(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "FrameworkAssetsConsumer"); + + ExecuteCommand(restore).Should().Pass(); + + + + var build = CreateBuildCommand(ProjectDirectory, "FrameworkAssetsConsumer"); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + return (intermediateOutputPath, build); + + } + + + + private StaticWebAssetsManifest LoadBuildManifest(string intermediateOutputPath) + + { + + var manifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(manifestPath)); + + manifest.Should().NotBeNull(); + + return manifest; + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsP2PIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsP2PIntegrationTest.cs index 03f64c4ec58e..c43587b5dbb0 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsP2PIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsP2PIntegrationTest.cs @@ -1,135 +1,407 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Xml.Linq; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { - public class FrameworkAssetsP2PIntegrationTest(ITestOutputHelper log) : AspNetSdkTest(log) + + + [TestClass] + + public class FrameworkAssetsP2PIntegrationTest : AspNetSdkTest + + { - [Fact] + + + [TestMethod] + + public void Build_Consumer_MaterializesFrameworkAssetsFromProjectReference() + + { + + var intermediateOutputPath = BuildConsumerWithFrameworkPattern(); + + var manifest = LoadBuildManifest(intermediateOutputPath); + + + + // The JS assets matching the FrameworkPattern should be materialized under fx/ + + var materializedAssets = manifest.Assets + + .Where(a => a.RelativePath.Contains(".js") + + && a.Identity.Contains(Path.Combine("fx", "ClassLibrary"))) + + .ToList(); + + + + materializedAssets.Should().NotBeEmpty( + + "JS assets matching FrameworkPattern should be materialized under the fx/ directory"); + + + + foreach (var asset in materializedAssets) + + { + + // SourceType should be Discovered (changed from Framework during materialization) + + asset.SourceType.Should().Be("Discovered", + + $"materialized framework asset {asset.RelativePath} should have SourceType=Discovered"); + + + + // SourceId should be updated to the consuming project's PackageId + + asset.SourceId.Should().Be("AppWithP2PReference", + + $"materialized framework asset {asset.RelativePath} should have SourceId updated to the consumer"); + + + + // BasePath should be the consumer's base path ("/" for a web app) + + asset.BasePath.Should().Be("/", + + $"materialized framework asset {asset.RelativePath} should have BasePath updated to the consumer"); + + + + // AssetMode should be CurrentProject + + asset.AssetMode.Should().Be("CurrentProject", + + $"materialized framework asset {asset.RelativePath} should have AssetMode=CurrentProject"); + + } + + } - [Fact] + + + + + [TestMethod] + + public void Build_Consumer_NonMatchingAssetsRemainUnchanged() + + { + + var intermediateOutputPath = BuildConsumerWithFrameworkPattern(); + + var manifest = LoadBuildManifest(intermediateOutputPath); + + + + // CSS assets from ClassLibrary should remain as Project type (they don't match **/*.js) + + var cssAssets = manifest.Assets + + .Where(a => a.RelativePath.Contains(".css") && a.SourceId == "ClassLibrary") + + .ToList(); + + + + cssAssets.Should().NotBeEmpty("CSS assets from ClassLibrary should be present"); + + cssAssets.Should().OnlyContain(a => a.SourceType == "Project", + + "CSS assets not matching FrameworkPattern should remain as Project type"); + + } - [Fact] + + + + + [TestMethod] + + public void Build_Consumer_MaterializedFrameworkAssetFilesExistOnDisk() + + { + + var intermediateOutputPath = BuildConsumerWithFrameworkPattern(); + + + + var fxDir = Path.Combine(intermediateOutputPath, "staticwebassets", "fx", "ClassLibrary"); + + Directory.Exists(fxDir).Should().BeTrue("the fx/ClassLibrary directory should be created"); + + + + var materializedFiles = Directory.GetFiles(fxDir, "*.js", SearchOption.AllDirectories); + + materializedFiles.Should().NotBeEmpty("JS framework assets should be copied to the fx/ directory"); + + } - [Fact] + + + + + [TestMethod] + + public void Build_Consumer_EndpointsExistForMaterializedFrameworkAssets() + + { + + var intermediateOutputPath = BuildConsumerWithFrameworkPattern(); + + var manifest = LoadBuildManifest(intermediateOutputPath); + + + + var materializedAssets = manifest.Assets + + .Where(a => a.RelativePath.Contains(".js") + + && a.Identity.Contains(Path.Combine("fx", "ClassLibrary"))) + + .ToList(); + + + + materializedAssets.Should().NotBeEmpty(); + + + + // Each materialized asset should have at least one endpoint referencing it + + foreach (var asset in materializedAssets) + + { + + var matchingEndpoints = manifest.Endpoints + + ?.Where(e => string.Equals(e.AssetFile, asset.Identity, StringComparison.OrdinalIgnoreCase)) + + .ToList(); + + + + matchingEndpoints.Should().NotBeNullOrEmpty( + + $"materialized framework asset {asset.RelativePath} should have at least one endpoint"); + + + + // Endpoint routes should NOT contain the library's base path — they should + + // reflect the consumer's base path (which is "/" for a web app). + + foreach (var ep in matchingEndpoints) + + { + + ep.Route.Should().NotContain("_content/ClassLibrary", + + "endpoint route should not retain the library's base path after materialization"); + + } + + } + + } + + + + private string BuildConsumerWithFrameworkPattern() + + { + + var projectDirectory = CreateAspNetSdkTestAsset("RazorAppWithP2PReference") + + .WithProjectChanges((path, document) => + + { + + if (Path.GetFileName(path) == "ClassLibrary.csproj") + + { + + // Add FrameworkPattern to mark all .js files as framework assets + + var propertyGroup = document.Root.Descendants("TargetFramework").First().Parent; + + propertyGroup.Add( + + new XElement("StaticWebAssetFrameworkPattern", "**/*.js")); + + } + + }); + + + + var build = CreateBuildCommand(projectDirectory, "AppWithP2PReference"); + + ExecuteCommand(build).Should().Pass(); + + + + return build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + } + + + + private static StaticWebAssetsManifest LoadBuildManifest(string intermediateOutputPath) + + { + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + return StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/GroupedFrameworkAssetsIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/GroupedFrameworkAssetsIntegrationTest.cs index a1f9ce7d7aa4..d2fc5409aed8 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/GroupedFrameworkAssetsIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/GroupedFrameworkAssetsIntegrationTest.cs @@ -1,110 +1,330 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Xml.Linq; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { - public class GroupedFrameworkAssetsIntegrationTest(ITestOutputHelper log) - : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, nameof(GroupedFrameworkAssetsIntegrationTest)) + + + [TestClass] + + public class GroupedFrameworkAssetsIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest + { + + protected override string RestoreNugetPackagePath => nameof(GroupedFrameworkAssetsIntegrationTest); + + // Regression coverage for the blazor.webassembly.js 404. A package ships an asset that is both a + + // framework asset and a member of a group (as Microsoft.AspNetCore.Components.WebAssembly does for + + // blazor.webassembly.js). The scenario is Package -> Library -> App: + + // * The group is opt-in via the IncludeGroupedFrameworkAssets property set by the library, so + + // inclusion is conditional. + + // * When the library enables the group, the framework asset materializes into the library under the + + // library base path (_content/) with its AssetGroups cleared. If AssetGroups is not + + // cleared during materialization, downstream endpoint generation skips the asset and it 404s. + + // * The materialized framework asset is a current-project asset of the library, so the app (which + + // does not enable the group) does not include it at the root. - [Theory] - [InlineData(true)] - [InlineData(false)] + + + [TestMethod] + + + [DataRow(true)] + + + [DataRow(false)] + + public void Build_PackageToLibraryToApp_GroupedFrameworkAsset_IsConditionalAndMaterializedIntoLibrary(bool includeGroupedFrameworkAssets) + + { + + ProjectDirectory = CreateAspNetSdkTestAsset("GroupedFrameworkAssetsSample", identifier: includeGroupedFrameworkAssets.ToString()) + + .WithProjectChanges((path, document) => + + { + + if (Path.GetFileName(path) == "GroupedFrameworkLibrary.csproj") + + { + + // Only the library opts into the group, so the app does not re-include the asset. + + var propertyGroup = document.Root.Descendants("TargetFramework").First().Parent; + + propertyGroup.Add( + + new XElement("IncludeGroupedFrameworkAssets", includeGroupedFrameworkAssets.ToString())); + + } + + }); + + + + var pack = CreatePackCommand(ProjectDirectory, "GroupedFrameworkPackage"); + + ExecuteCommand(pack).Should().Pass(); + + ClearCachedPackage("groupedframeworkpackage"); + + + + var build = CreateBuildCommand(ProjectDirectory, "GroupedFrameworkApp"); + + ExecuteCommand(build).Should().Pass(); + + + + var libraryManifest = LoadBuildManifest( + + Path.Combine(ProjectDirectory.TestRoot, "GroupedFrameworkLibrary", "obj", "Debug", DefaultTfm)); + + var appManifest = LoadBuildManifest(build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString()); + + + + // The materialized asset lives under fx\ (the originating framework + + // package), but is owned by the consuming library (SourceId, BasePath are remapped). + + var libraryMaterializedJs = libraryManifest.Assets + + .Where(a => a.RelativePath.Contains(".js") + + && a.Identity.Contains(Path.Combine("fx", "GroupedFrameworkPackage"))) + + .ToList(); + + + + if (includeGroupedFrameworkAssets) + + { + + libraryMaterializedJs.Should().NotBeEmpty( + + "the grouped JS framework asset should be materialized into the library when the group is enabled"); + + + + foreach (var asset in libraryMaterializedJs) + + { + + // Materialized into the library, under the library base path. + + asset.SourceId.Should().Be("GroupedFrameworkLibrary", + + $"materialized framework asset {asset.RelativePath} should belong to the library"); + + asset.BasePath.Should().Be("_content/GroupedFrameworkLibrary", + + $"materialized framework asset {asset.RelativePath} should be under the library base path"); + + + + // AssetGroups must be cleared, otherwise endpoint generation skips the asset (the 404). + + asset.AssetGroups.Should().BeNullOrEmpty( + + $"materialized framework asset {asset.RelativePath} must have its AssetGroups cleared"); + + } + + + + // The framework asset is a current-project asset of the library, so the app does not + + // include it at the root. + + appManifest.Assets + + .Where(a => a.RelativePath.Contains("feature") && a.BasePath == "/") + + .Should().BeEmpty("the app should not include the library's framework asset at the root"); + + } + + else + + { + + // The group is not enabled, so the grouped framework asset is excluded entirely. + + libraryMaterializedJs.Should().BeEmpty( + + "the grouped JS framework asset should be excluded when the group is not enabled"); + + } + + } + + + + private static StaticWebAssetsManifest LoadBuildManifest(string intermediateOutputPath) + + { + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + new FileInfo(path).Should().Exist(); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + manifest.Should().NotBeNull(); + + return manifest; + + } + + + + // Clear the cached package so NuGet re-extracts from the freshly-packed nupkg. + + private void ClearCachedPackage(string packageId) + + { + + var cached = Path.Combine(GetNuGetCachePath(), packageId); + + if (Directory.Exists(cached)) + + { + + Directory.Delete(cached, recursive: true); + + } + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/IsolatedNuGetPackageFolderAspNetSdkBaselineTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/IsolatedNuGetPackageFolderAspNetSdkBaselineTest.cs index 528f4606a602..6bcf96ff6b33 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/IsolatedNuGetPackageFolderAspNetSdkBaselineTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/IsolatedNuGetPackageFolderAspNetSdkBaselineTest.cs @@ -1,26 +1,50 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + { - [Trait("AspNetCore", "NugetIsolation")] - [Trait("AspNetCore", "BaselineTest")] - public class IsolatedNuGetPackageFolderAspNetSdkBaselineTest : AspNetSdkBaselineTest + + [TestCategory("NugetIsolation")] + [TestCategory("BaselineTest")] + [TestProperty("AspNetCore", "NugetIsolation")] + + [TestProperty("AspNetCore", "BaselineTest")] + + public abstract class IsolatedNuGetPackageFolderAspNetSdkBaselineTest : AspNetSdkBaselineTest + { - private readonly string _cachePath; - public IsolatedNuGetPackageFolderAspNetSdkBaselineTest(ITestOutputHelper log, string restoreNugetPackagePath) : base(log) - { - _cachePath = Path.GetFullPath(Path.Combine(SdkTestContext.Current.TestExecutionDirectory, Shorten(restoreNugetPackagePath))); - } + protected abstract string RestoreNugetPackagePath { get; } + + + + private string? _cachePath; + + + + protected override string GetNuGetCachePath() => + + _cachePath ??= Path.GetFullPath(Path.Combine(SdkTestContext.Current.TestExecutionDirectory, Shorten(RestoreNugetPackagePath))); + + private static string Shorten(string restoreNugetPackagePath) => + restoreNugetPackagePath + .Replace("IntegrationTest", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("Tests", string.Empty, StringComparison.OrdinalIgnoreCase); - protected override string GetNuGetCachePath() => _cachePath; } -} +} diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/JsModulesIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/JsModulesIntegrationTest.cs index d96dab0d59e5..465ae5302515 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/JsModulesIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/JsModulesIntegrationTest.cs @@ -2,346 +2,1046 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { - public class JsModulesIntegrationTest(ITestOutputHelper log) : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, nameof(JsModulesIntegrationTest)) + + + [TestClass] + + public class JsModulesIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest + { - [Fact] + + protected override string RestoreNugetPackagePath => nameof(JsModulesIntegrationTest); + + + [TestMethod] + + public void Build_NoOps_WhenJsModulesIsDisabled() + + { + + var testAsset = "RazorComponentApp"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + Directory.CreateDirectory(Path.Combine(projectDirectory.TestRoot, "wwwroot")); + + File.WriteAllText(Path.Combine(projectDirectory.TestRoot, "wwwroot", "ComponentApp.lib.module.js"), "console.log('Hello world!');"); + + + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build, "/p:JsModulesEnabled=false").Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + + + new FileInfo(Path.Combine(intermediateOutputPath, "jsmodules", "jsmodules.build.manifest.json")).Should().NotExist(); + + } - [Fact] + + + + + [TestMethod] + + public void Build_GeneratesManifestWhenItFindsALibrary() + + { + + var testAsset = "RazorComponentApp"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset) + + .WithProjectChanges(p => { + + var fingerprintContent = p.Descendants() + + .SingleOrDefault(e => e.Name.LocalName == "StaticWebAssetsFingerprintContent"); + + fingerprintContent.Value = "true"; + + }); + + + + Directory.CreateDirectory(Path.Combine(projectDirectory.TestRoot, "wwwroot")); + + File.WriteAllText(Path.Combine(projectDirectory.TestRoot, "wwwroot", "ComponentApp.lib.module.js"), "console.log('Hello world!');"); + + + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + + + var file = new FileInfo(Path.Combine(intermediateOutputPath, "jsmodules", "jsmodules.build.manifest.json")); + + file.Should().Exist(); + + file.Should().Match("""ComponentApp\.[a-zA-Z-0-9]{10}\.lib\.module\.js"""); + + } - [Fact] + + + + + [TestMethod] + + public void Build_DiscoversJsModulesBasedOnPatterns() + + { + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + // Components + + CreateFile("", ProjectDirectory.TestRoot, "Components", "Pages", "Counter.razor.js"); + + + + // MVC | Razor pages + + CreateFile("", ProjectDirectory.TestRoot, "Pages", "Index.cshtml"); + + CreateFile("", ProjectDirectory.TestRoot, "Pages", "Index.cshtml.js"); + + + + var build = CreateBuildCommand(ProjectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(finalPath)); + + AssertManifest( + + buildManifest, + + LoadBuildManifest()); + + + + buildManifest.Should().NotBeNull(); + + buildManifest.DiscoveryPatterns.Should().BeEmpty(); + + + + AssertBuildAssets( + + buildManifest, + + outputPath, + + intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void Publish_PublishesJsModuleBundleBundleToTheRightLocation() + + { + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) + + .WithProjectChanges(p => { + + var fingerprintContent = p.Descendants() + + .SingleOrDefault(e => e.Name.LocalName == "StaticWebAssetsFingerprintContent"); + + fingerprintContent.Value = "true"; + + }); + + Directory.CreateDirectory(Path.Combine(ProjectDirectory.TestRoot, "wwwroot")); + + File.WriteAllText(Path.Combine(ProjectDirectory.TestRoot, "wwwroot", "ComponentApp.lib.module.js"), "console.log('Hello world!');"); + + + + var publish = CreatePublishCommand(ProjectDirectory); + + var publishResult = ExecuteCommand(publish); + + publishResult.Should().Pass(); + + + + var outputPath = publish.GetOutputDirectory(DefaultTfm).ToString(); + + var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + new FileInfo(path).Should().Exist(); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + AssertManifest(manifest, LoadPublishManifest()); + + + + AssertPublishAssets( + + manifest, + + outputPath, + + intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void Publish_DoesNotPublishAnyFile_WhenThereAreNoJsModulesFiles() + + { + + var testAsset = "RazorComponentApp"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var publish = CreatePublishCommand(projectDirectory); + + ExecuteCommand(publish).Should().Pass(); + + + + var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "ComponentApp.lib.module.js")).Should().NotExist(); + + new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "ComponentApp.modules.json")).Should().NotExist(); + + } - [Fact] + + + + + [TestMethod] + + public void Does_Nothing_WhenThereAreNoJsModulesFiles() + + { + + var testAsset = "RazorComponentApp"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + + + var file = new FileInfo(Path.Combine(intermediateOutputPath, "jsmodules", "jsmodules.build.manifest.json")); + + file.Should().NotExist(); + + } - [Fact] + + + + + [TestMethod] + + public void Build_JsModules_IsIncremental() + + { + + // Arrange + + var thumbprintLookup = new Dictionary(); + + + + var testAsset = "RazorComponentApp"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + Directory.CreateDirectory(Path.Combine(projectDirectory.TestRoot, "wwwroot")); + + File.WriteAllText(Path.Combine(projectDirectory.TestRoot, "wwwroot", "ComponentApp.lib.module.js"), "console.log('Hello world!');"); + + + + // Act & Assert 1 + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + var directoryPath = Path.Combine(intermediateOutputPath, "jsmodules"); + + + + var files = Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories); + + foreach (var file in files) + + { + + var thumbprint = FileThumbPrint.Create(file); + + thumbprintLookup[file] = thumbprint; + + } + + + + // Act & Assert 2 + + for (var i = 0; i < 2; i++) + + { + + build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + foreach (var file in files) + + { + + var thumbprint = FileThumbPrint.Create(file); - Assert.Equal(thumbprintLookup[file], thumbprint); + + + Assert.AreEqual(thumbprintLookup[file], thumbprint); + + } + + } + + } + + + + private void CreateFile(string content, params string[] path) + + { + + Directory.CreateDirectory(Path.Combine(path[..^1].Prepend(ProjectDirectory.TestRoot).ToArray())); + + File.WriteAllText(Path.Combine(path.Prepend(ProjectDirectory.TestRoot).ToArray()), content); + + } + + } - public class JsModulesPackagesIntegrationTest(ITestOutputHelper log) : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, nameof(JsModulesPackagesIntegrationTest)) + + [TestClass] + + + + + public class JsModulesPackagesIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest + { - [Fact] + + protected override string RestoreNugetPackagePath => nameof(JsModulesPackagesIntegrationTest); + + + [TestMethod] + + public void BuildProjectWithReferences_IncorporatesInitializersFromClassLibraries() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + CreateFile("console.log('Hello world AnotherClassLib')", "AnotherClassLib", "wwwroot", "AnotherClassLib.lib.module.js"); + + CreateFile("console.log('Hello world ClassLibrary')", "ClassLibrary", "wwwroot", "ClassLibrary.lib.module.js"); + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(finalPath)); + + AssertManifest( + + manifest, + + LoadBuildManifest()); + + + + AssertBuildAssets( + + manifest, + + outputPath, + + intermediateOutputPath); + + + + var file = new FileInfo(Path.Combine(intermediateOutputPath, "jsmodules", "jsmodules.build.manifest.json")); + + file.Should().Exist(); + + file.Should().Contain("_content/AnotherClassLib/AnotherClassLib.lib.module.js"); + + file.Should().Contain("_content/ClassLibrary/ClassLibrary.lib.module.js"); + + } - [Fact] + + + + + [TestMethod] + + public void PublishProjectWithReferences_IncorporatesInitializersFromClassLibrariesAndPublishesAssetsToTheRightLocation() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + CreateFile("console.log('Hello world AnotherClassLib')", "AnotherClassLib", "wwwroot", "AnotherClassLib.lib.module.js"); + + + + // Notice that it does not follow the pattern $(PackageId).lib.module.js + + CreateFile("console.log('Hello world ClassLibrary')", "ClassLibrary", "wwwroot", "AnotherClassLib.lib.module.js"); + + + + var publish = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(publish).Should().Pass(); + + + + var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var outputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + AssertManifest( + + buildManifest, + + LoadBuildManifest()); + + + + var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(finalPath)); + + AssertManifest( + + publishManifest, + + LoadPublishManifest()); + + + + AssertPublishAssets( + + publishManifest, + + outputPath, + + intermediateOutputPath); + + + + var file = new FileInfo(Path.Combine(outputPath, "wwwroot", "AppWithPackageAndP2PReference.modules.json")); + + file.Should().Exist(); + + file.Should().Contain("_content/AnotherClassLib/AnotherClassLib.lib.module.js"); + + file.Should().NotContain("_content/ClassLibrary/AnotherClassLib.lib.module.js"); + + } - [Fact] + + + + + [TestMethod] + + public void PublishProjectWithReferences_DifferentBuildAndPublish_LibraryInitializers() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + CreateFile("console.log('Hello world AnotherClassLib publish')", "AnotherClassLib", "wwwroot", "AnotherClassLib.lib.module.js"); + + CreateFile("console.log('Hello world AnotherClassLib')", "AnotherClassLib", "wwwroot", "AnotherClassLib.lib.module.build.js"); + + ProjectDirectory.WithProjectChanges((project, document) => + + { + + if (project.EndsWith("AnotherClassLib.csproj")) + + { + + document.Root.Add(new XElement("ItemGroup", + + new XElement("Content", + + new XAttribute("Update", "wwwroot\\AnotherClassLib.lib.module.build.js"), + + new XAttribute("CopyToPublishDirectory", "Never"), + + new XAttribute("TargetPath", "wwwroot\\AnotherClassLib.lib.module.js")))); + + } + + }); + + var publish = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(publish).Should().Pass(); + + + + var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var outputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); ; + + var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + + + var initializers = buildManifest.Assets.Where(a => a.RelativePath == "AnotherClassLib.lib.module.js"); + + initializers.Should().HaveCount(1); + + initializers.Should().Contain(a => a.IsBuildOnly()); + + + + AssertManifest( + + buildManifest, + + LoadBuildManifest()); + + + + var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(finalPath)); + + AssertManifest( + + manifest, + + LoadPublishManifest()); + + + + AssertBuildAssets( + + manifest, + + outputPath, + + intermediateOutputPath); + + + + var modulesManifest = new FileInfo(Path.Combine(outputPath, "wwwroot", "AppWithPackageAndP2PReference.modules.json")); + + modulesManifest.Should().Exist(); + + modulesManifest.Should().Contain("_content/AnotherClassLib/AnotherClassLib.lib.module.js"); + + modulesManifest.Should().NotContain("_content/ClassLibrary/AnotherClassLib.lib.module.js"); + + + + var moduleFile = new FileInfo(Path.Combine(outputPath, "wwwroot", "_content", "AnotherClassLib", "AnotherClassLib.lib.module.js")); + + moduleFile.Should().Exist(); + + moduleFile.Should().Contain("console.log('Hello world AnotherClassLib publish')"); + + } + + + + private void CreateFile(string content, params string[] path) + + { + + Directory.CreateDirectory(Path.Combine(path[..^1].Prepend(ProjectDirectory.TestRoot).ToArray())); + + File.WriteAllText(Path.Combine(path.Prepend(ProjectDirectory.TestRoot).ToArray()), content); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/LegacyStaticWebAssetsV1IntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/LegacyStaticWebAssetsV1IntegrationTest.cs index 9089ed148d04..4d4fe9a2997f 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/LegacyStaticWebAssetsV1IntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/LegacyStaticWebAssetsV1IntegrationTest.cs @@ -2,131 +2,389 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.StaticWebAssets.Tasks; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + -[assembly:CollectionBehavior(DisableTestParallelization = true)] namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { - public class LegacyStaticWebAssetsV1IntegrationTest(ITestOutputHelper log) - : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, nameof(LegacyStaticWebAssetsV1IntegrationTest)) + + + [TestClass] + + public class LegacyStaticWebAssetsV1IntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest + { - [Fact] + + protected override string RestoreNugetPackagePath => nameof(LegacyStaticWebAssetsV1IntegrationTest); + + + [TestMethod] + + public void PublishProjectWithReferences_WorksWithStaticWebAssetsV1ClassLibraries() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) + + .WithProjectChanges((project, document) => + + { + + if (Path.GetFileName(project) == "AnotherClassLib.csproj") + + { + + document.Descendants("TargetFramework").Single().ReplaceNodes("netstandard2.1"); + + document.Descendants("FrameworkReference").Single().Remove(); + + document.Descendants("PropertyGroup").First().Add(new XElement("RazorLangVersion", "3.0")); + + } + + if (Path.GetFileName(project) == "ClassLibrary.csproj") + + { + + document.Descendants("TargetFramework").Single().ReplaceNodes("netstandard2.0"); + + document.Descendants("FrameworkReference").Single().Remove(); + + document.Descendants("PropertyGroup").First().Add(new XElement("RazorLangVersion", "3.0")); + + } + + }); + + + + // We are deleting Views and Components because we are only interested in the static web assets behavior for this test + + // and this makes it easier to validate the test. + + Directory.Delete(Path.Combine(ProjectDirectory.TestRoot, "AnotherClassLib", "Views"), recursive: true); + + Directory.Delete(Path.Combine(ProjectDirectory.TestRoot, "ClassLibrary", "Views"), recursive: true); + + Directory.Delete(Path.Combine(ProjectDirectory.TestRoot, "ClassLibrary", "Components"), recursive: true); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + var publish = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(publish).Should().Pass(); + + + + var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var publishPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + // GenerateStaticWebAssetsManifest should generate the manifest file. + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + new FileInfo(path).Should().Exist(); + + AssertManifest( + + StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)), + + LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(publishPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().NotExist(); + + + + // GenerateStaticWebAssetsPublishManifest should generate the publish manifest file. + + var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + new FileInfo(path).Should().Exist(); + + var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); + + AssertManifest( + + publishManifest, + + LoadPublishManifest()); + + + + AssertPublishAssets( + + publishManifest, + + publishPath, + + intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void BuildProjectWithReferences_WorksWithStaticWebAssetsV1ClassLibraries() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) + + .WithProjectChanges((project, document) => + + { + + if (Path.GetFileName(project) == "AnotherClassLib.csproj") + + { + + document.Descendants("TargetFramework").Single().ReplaceNodes("netstandard2.1"); + + document.Descendants("FrameworkReference").Single().Remove(); + + document.Descendants("PropertyGroup").First().Add(new XElement("RazorLangVersion", "3.0")); + + } + + if (Path.GetFileName(project) == "ClassLibrary.csproj") + + { + + document.Descendants("TargetFramework").Single().ReplaceNodes("netstandard2.0"); + + document.Descendants("FrameworkReference").Single().Remove(); + + document.Descendants("PropertyGroup").First().Add(new XElement("RazorLangVersion", "3.0")); + + } + + }); + + + + // We are deleting Views and Components because we are only interested in the static web assets behavior for this test + + // and this makes it easier to validate the test. + + Directory.Delete(Path.Combine(ProjectDirectory.TestRoot, "AnotherClassLib", "Views"), recursive: true); + + Directory.Delete(Path.Combine(ProjectDirectory.TestRoot, "ClassLibrary", "Views"), recursive: true); + + Directory.Delete(Path.Combine(ProjectDirectory.TestRoot, "ClassLibrary", "Components"), recursive: true); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + 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.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + AssertManifest( + + manifest, + + LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + + + AssertBuildAssets( + + manifest, + + outputPath, + + intermediateOutputPath); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj index 377507009b2d..fb9d01da5b8c 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj @@ -1,4 +1,4 @@ - + false @@ -10,7 +10,6 @@ - Exe testSdkStaticWebAssets @@ -44,7 +43,7 @@ - + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs index f12e024fb662..d1df366e1bc8 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs @@ -1,783 +1,2353 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Text.Json; + + using System.Text.RegularExpressions; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { - public class ScopedCssIntegrationTest(ITestOutputHelper log) - : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, nameof(ScopedCssIntegrationTest)) + + + [TestClass] + + public class ScopedCssIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest + { - [Fact] + + protected override string RestoreNugetPackagePath => nameof(ScopedCssIntegrationTest); + + + [TestMethod] + + public void Build_NoOps_WhenScopedCssIsDisabled() + + { + + var testAsset = "RazorComponentApp"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build, "/p:ScopedCssEnabled=false").Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css")).Should().NotExist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "FetchData.razor.rz.scp.css")).Should().NotExist(); + + } - [Fact] + + + + + [TestMethod] + + public void Build_NoOps_ForMvcApp_WhenScopedCssIsDisabled() + + { + + var testAsset = "RazorSimpleMvc"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build, "/p:ScopedCssEnabled=false").Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Index.cshtml.rz.scp.css")).Should().NotExist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Contact.cshtml.rz.scp.css")).Should().NotExist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "SimpleMvc.styles.css")).Should().NotExist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "About.cshtml.rz.scp.css")).Should().NotExist(); + + } - [Fact] + + + + + [TestMethod] + + public void CanDisableDefaultDiscoveryConvention() + + { + + var testAsset = "RazorComponentApp"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build, "/p:EnableDefaultScopedCssItems=false").Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css")).Should().NotExist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "FetchData.razor.rz.scp.css")).Should().NotExist(); + + } - [CoreMSBuildOnlyFact] + + + + + [TestMethod] + + [CoreMSBuildOnly] + + public void CanOverrideScopeIdentifiers() + + { + + var testAsset = "RazorComponentApp"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset) + + .WithProjectChanges(project => + + { + + var ns = project.Root.Name.Namespace; + + var itemGroup = new XElement(ns + "ItemGroup"); + + var element = new XElement("ScopedCssInput", new XAttribute("Include", @"Styles\Pages\Counter.css")); + + element.Add(new XElement("RazorComponent", @"Components\Pages\Counter.razor")); + + element.Add(new XElement("CssScope", "b-overridden")); + + itemGroup.Add(element); + + project.Root.Add(itemGroup); + + }); + + + + var stylesFolder = Path.Combine(projectDirectory.Path, "Styles", "Pages"); + + Directory.CreateDirectory(stylesFolder); + + var styles = Path.Combine(stylesFolder, "Counter.css"); + + File.Move(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css"), styles); + + + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build, "/p:EnableDefaultScopedCssItems=false", "/p:EmitCompilerGeneratedFiles=true").Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + + + var scoped = Path.Combine(intermediateOutputPath, "scopedcss", "Styles", "Pages", "Counter.rz.scp.css"); + + new FileInfo(scoped).Should().Exist(); + + new FileInfo(scoped).Should().Contain("b-overridden"); + + var generated = Path.Combine(intermediateOutputPath, "generated", "Microsoft.CodeAnalysis.Razor.Compiler", "Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator", "Components", "Pages", "Counter_razor.g.cs"); + + new FileInfo(generated).Should().Exist(); + + new FileInfo(generated).Should().Contain("b-overridden"); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist(); + + } - [Fact] + + + + + [TestMethod] + + public void Build_GeneratesTransformedFilesAndBundle_ForComponentsWithScopedCss() + + { + + var testAsset = "RazorComponentApp"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().Exist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().Exist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css")).Should().Exist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "projectbundle", "ComponentApp.bundle.scp.css")).Should().Exist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "FetchData.razor.rz.scp.css")).Should().NotExist(); + + } - [Fact] + + + + + [TestMethod] + + public void Build_GeneratesTransformedFilesAndBundle_ForViewsWithScopedCss() + + { + + var testAsset = "RazorSimpleMvc"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - var build = CreateBuildCommand(projectDirectory); - ExecuteCommand(build).Should().Pass(); - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Index.cshtml.rz.scp.css")).Should().Exist(); - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Contact.cshtml.rz.scp.css")).Should().Exist(); - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "SimpleMvc.styles.css")).Should().Exist(); - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "projectbundle", "SimpleMvc.bundle.scp.css")).Should().Exist(); - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "About.cshtml.rz.scp.css")).Should().Exist(); - } - [Fact] - public void Build_ScopedCssFiles_ContainsUniqueScopesPerFile() - { - var testAsset = "RazorComponentApp"; - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - var generatedCounter = Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css"); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + + + + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Index.cshtml.rz.scp.css")).Should().Exist(); + + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Contact.cshtml.rz.scp.css")).Should().Exist(); + + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "SimpleMvc.styles.css")).Should().Exist(); + + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "projectbundle", "SimpleMvc.bundle.scp.css")).Should().Exist(); + + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "About.cshtml.rz.scp.css")).Should().Exist(); + + + } + + + + + + [TestMethod] + + + public void Build_ScopedCssFiles_ContainsUniqueScopesPerFile() + + + { + + + var testAsset = "RazorComponentApp"; + + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + + + var build = CreateBuildCommand(projectDirectory); + + + ExecuteCommand(build).Should().Pass(); + + + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + + + + + var generatedCounter = Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css"); + + new FileInfo(generatedCounter).Should().Exist(); + + var generatedIndex = Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css"); + + new FileInfo(generatedIndex).Should().Exist(); + + var counterContent = File.ReadAllText(generatedCounter); + + var indexContent = File.ReadAllText(generatedIndex); + + + + var counterScopeMatch = Regex.Match(counterContent, ".*button\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); - Assert.True(counterScopeMatch.Success, "Couldn't find a scope id in the generated Counter scoped css file."); + + + Assert.IsTrue(counterScopeMatch.Success, "Couldn't find a scope id in the generated Counter scoped css file."); + + var counterScopeId = counterScopeMatch.Groups[1].Captures[0].Value; + + + + var indexScopeMatch = Regex.Match(indexContent, ".*h1\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); - Assert.True(indexScopeMatch.Success, "Couldn't find a scope id in the generated Index scoped css file."); + + + Assert.IsTrue(indexScopeMatch.Success, "Couldn't find a scope id in the generated Index scoped css file."); + + var indexScopeId = indexScopeMatch.Groups[1].Captures[0].Value; - Assert.NotEqual(counterScopeId, indexScopeId); + + + + + Assert.AreNotEqual(counterScopeId, indexScopeId); + + } - [Fact] + + + + + [TestMethod] + + public void Build_ScopedCssViews_ContainsUniqueScopesPerView() + + { + + var testAsset = "RazorSimpleMvc"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + + + var generatedIndex = Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Index.cshtml.rz.scp.css"); + + new FileInfo(generatedIndex).Should().Exist(); + + var generatedAbout = Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "About.cshtml.rz.scp.css"); + + new FileInfo(generatedAbout).Should().Exist(); + + var generatedContact = Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Contact.cshtml.rz.scp.css"); + + new FileInfo(generatedContact).Should().Exist(); + + var indexContent = File.ReadAllText(generatedIndex); + + var aboutContent = File.ReadAllText(generatedAbout); + + var contactContent = File.ReadAllText(generatedContact); + + + + var indexScopeMatch = Regex.Match(indexContent, ".*p\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); - Assert.True(indexScopeMatch.Success, "Couldn't find a scope id in the generated Index scoped css file."); + + + Assert.IsTrue(indexScopeMatch.Success, "Couldn't find a scope id in the generated Index scoped css file."); + + var indexScopeId = indexScopeMatch.Groups[1].Captures[0].Value; + + + + var aboutScopeMatch = Regex.Match(aboutContent, ".*h2\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); - Assert.True(aboutScopeMatch.Success, "Couldn't find a scope id in the generated About scoped css file."); + + + Assert.IsTrue(aboutScopeMatch.Success, "Couldn't find a scope id in the generated About scoped css file."); + + var aboutScopeId = aboutScopeMatch.Groups[1].Captures[0].Value; + + + + var contactScopeMatch = Regex.Match(contactContent, ".*a\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); - Assert.True(contactScopeMatch.Success, "Couldn't find a scope id in the generated Contact scoped css file."); + + + Assert.IsTrue(contactScopeMatch.Success, "Couldn't find a scope id in the generated Contact scoped css file."); + + var contactScopeId = contactScopeMatch.Groups[1].Captures[0].Value; - Assert.NotEqual(indexScopeId, aboutScopeId); - Assert.NotEqual(indexScopeId, contactScopeId); - Assert.NotEqual(aboutScopeId, contactScopeId); + + + + + Assert.AreNotEqual(indexScopeId, aboutScopeId); + + + Assert.AreNotEqual(indexScopeId, contactScopeId); + + + Assert.AreNotEqual(aboutScopeId, contactScopeId); + + } - [Fact] + + + + + [TestMethod] + + public void Build_WorksWhenViewsAndComponentsArePartOfTheSameProject_ContainsUniqueScopesPerFile() + + { + + var testAsset = "RazorMvcWithComponents"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + + + var generatedIndex = Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Index.cshtml.rz.scp.css"); + + new FileInfo(generatedIndex).Should().Exist(); + + + + var generatedCounter = Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Counter.razor.rz.scp.css"); + + new FileInfo(generatedCounter).Should().Exist(); + + + + var indexContent = File.ReadAllText(generatedIndex); + + var counterContent = File.ReadAllText(generatedCounter); + + + + var indexScopeMatch = Regex.Match(indexContent, ".*p\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); - Assert.True(indexScopeMatch.Success, "Couldn't find a scope id in the generated Index scoped css file."); + + + Assert.IsTrue(indexScopeMatch.Success, "Couldn't find a scope id in the generated Index scoped css file."); + + var indexScopeId = indexScopeMatch.Groups[1].Captures[0].Value; + + + + var counterScopeMatch = Regex.Match(counterContent, ".*div\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); - Assert.True(counterScopeMatch.Success, "Couldn't find a scope id in the generated Counter scoped css file."); + + + Assert.IsTrue(counterScopeMatch.Success, "Couldn't find a scope id in the generated Counter scoped css file."); + + var counterScopeId = counterScopeMatch.Groups[1].Captures[0].Value; - Assert.NotEqual(indexScopeId, counterScopeId); - } - [Fact] - public void Publish_PublishesScopedCssBundleToTheRightLocation() + + + + Assert.AreNotEqual(indexScopeId, counterScopeId); + + + } + + + + + + [TestMethod] + + + public void Publish_PublishesScopedCssBundleToTheRightLocation() + + + { + + + var testAsset = "RazorComponentApp"; + + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + + + var publish = CreatePublishCommand(projectDirectory); + + + ExecuteCommand(publish).Should().Pass(); + + + + + + var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + + + new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "ComponentApp.styles.css")).Should().Exist(); + + + new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist(); + + + new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); + + + } + + + + + + [TestMethod] + + + public void Publish_NoBuild_PublishesBundleToTheRightLocation() + + + { + + + var testAsset = "RazorComponentApp"; + + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + + + var build = CreateBuildCommand(projectDirectory); + + + var buildResult = ExecuteCommand(build); + + + buildResult.Should().Pass(); + + + + + + var publish = CreatePublishCommand(projectDirectory); + + + ExecuteCommand(publish, "/p:NoBuild=true").Should().Pass(); + + + + + + var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + + + new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "ComponentApp.styles.css")).Should().Exist(); + + + new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist(); + + + new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); + + + } + + + + + + [TestMethod] + + + public void Publish_DoesNotPublishAnyFile_WhenThereAreNoScopedCssFiles() + + + { + + + var testAsset = "RazorComponentApp"; + + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + + + File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css")); + + + File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Index.razor.css")); + + + + + + var publish = CreatePublishCommand(projectDirectory); + + + ExecuteCommand(publish).Should().Pass(); + + + + + + var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + + + new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "_framework", "scoped.styles.css")).Should().NotExist(); + + + } + + + + + + [TestMethod] + + + public void Publish_Publishes_IndividualScopedCssFiles_WhenNoBundlingIsEnabled() + + { - var testAsset = "RazorComponentApp"; - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - var publish = CreatePublishCommand(projectDirectory); - ExecuteCommand(publish).Should().Pass(); - var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + var testAsset = "RazorComponentApp"; - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "ComponentApp.styles.css")).Should().Exist(); - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist(); - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); - } - [Fact] - public void Publish_NoBuild_PublishesBundleToTheRightLocation() - { - var testAsset = "RazorComponentApp"; var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - var build = CreateBuildCommand(projectDirectory); - var buildResult = ExecuteCommand(build); - buildResult.Should().Pass(); + + + var publish = CreatePublishCommand(projectDirectory); - ExecuteCommand(publish, "/p:NoBuild=true").Should().Pass(); - var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "ComponentApp.styles.css")).Should().Exist(); - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist(); - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); - } + ExecuteCommand(publish, "/p:DisableScopedCssBundling=true").Should().Pass(); + - [Fact] - public void Publish_DoesNotPublishAnyFile_WhenThereAreNoScopedCssFiles() - { - var testAsset = "RazorComponentApp"; - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css")); - File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Index.razor.css")); - var publish = CreatePublishCommand(projectDirectory); - ExecuteCommand(publish).Should().Pass(); var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "_framework", "scoped.styles.css")).Should().NotExist(); - } - [Fact] - public void Publish_Publishes_IndividualScopedCssFiles_WhenNoBundlingIsEnabled() - { - var testAsset = "RazorComponentApp"; - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - var publish = CreatePublishCommand(projectDirectory); - ExecuteCommand(publish, "/p:DisableScopedCssBundling=true").Should().Pass(); - var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "ComponentApp.styles.css")).Should().NotExist(); + + + + new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "Components", "Pages", "Index.razor.rz.scp.css")).Should().Exist(); + + new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().Exist(); + + } - [CoreMSBuildOnlyFact] + + + + + [TestMethod] + + [CoreMSBuildOnly] + + public void Build_RemovingScopedCssAndBuilding_UpdatesGeneratedCodeAndBundle() + + { + + var testAsset = "RazorComponentApp"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build, "/p:EmitCompilerGeneratedFiles=true").Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().Exist(); + + var generatedBundle = Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css"); + + new FileInfo(generatedBundle).Should().Exist(); + + var generatedProjectBundle = Path.Combine(intermediateOutputPath, "scopedcss", "projectbundle", "ComponentApp.bundle.scp.css"); + + new FileInfo(generatedProjectBundle).Should().Exist(); + + var generatedCounter = Path.Combine(intermediateOutputPath, "generated", "Microsoft.CodeAnalysis.Razor.Compiler", "Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator", "Components", "Pages", "Counter_razor.g.cs"); + + new FileInfo(generatedCounter).Should().Exist(); + + + + var componentThumbprint = FileThumbPrint.Create(generatedCounter); + + var bundleThumbprint = FileThumbPrint.Create(generatedBundle); + + + + File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css")); + + + + build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build, "/p:EmitCompilerGeneratedFiles=true").Should().Pass(); + + + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); + + new FileInfo(generatedCounter).Should().Exist(); + + + + var newComponentThumbprint = FileThumbPrint.Create(generatedCounter); + + var newBundleThumbprint = FileThumbPrint.Create(generatedBundle); - Assert.NotEqual(componentThumbprint, newComponentThumbprint); - Assert.NotEqual(bundleThumbprint, newBundleThumbprint); + + + + + Assert.AreNotEqual(componentThumbprint, newComponentThumbprint); + + + Assert.AreNotEqual(bundleThumbprint, newBundleThumbprint); + + } - [Fact] + + + + + [TestMethod] + + public void Does_Nothing_WhenThereAreNoScopedCssFiles() + + { + + var testAsset = "RazorComponentApp"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css")); + + File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Index.razor.css")); + + + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "_framework", "scoped.styles.css")).Should().NotExist(); + + } - [Fact] + + + + + [TestMethod] + + public void Build_ScopedCssTransformation_AndBundling_IsIncremental() + + { + + // Arrange + + var thumbprintLookup = new Dictionary(); + + + + var testAsset = "RazorComponentApp"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + // Act & Assert 1 + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + var directoryPath = Path.Combine(intermediateOutputPath, "scopedcss"); + + + + var files = Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories); + + foreach (var file in files) + + { + + var thumbprint = FileThumbPrint.Create(file); + + thumbprintLookup[file] = thumbprint; + + } + + + + // Act & Assert 2 + + for (var i = 0; i < 2; i++) + + { + + build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + foreach (var file in files) + + { + + var thumbprint = FileThumbPrint.Create(file); - Assert.Equal(thumbprintLookup[file], thumbprint); + + + Assert.AreEqual(thumbprintLookup[file], thumbprint); + + } + + } + + } + + + + // Regression test for https://github.com/dotnet/sdk/issues/50646 - [Fact] + + + [TestMethod] + + public void Build_RegeneratesScopedCss_WhenCssScopeMetadataChanges() + + { + + // Arrange + + var testAsset = "RazorComponentApp"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + // Act 1: First build without custom scope + + var build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); + + var scopedCssFile = Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css"); + + var bundleFile = Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css"); + + + + new FileInfo(scopedCssFile).Should().Exist(); + + new FileInfo(bundleFile).Should().Exist(); + + + + // Get initial thumbprints + + var initialScopedCssThumbprint = FileThumbPrint.Create(scopedCssFile); + + var initialBundleThumbprint = FileThumbPrint.Create(bundleFile); + + + + // Verify initial build uses auto-generated scope (starts with 'b-') + + var initialContent = File.ReadAllText(scopedCssFile); + + initialContent.Should().MatchRegex(@"\[b-[a-z0-9]+\]"); + + + + // Act 2: Add custom CssScope metadata to the project + + File.WriteAllText( + + Path.Combine(projectDirectory.Path, "Directory.Build.targets"), + + """ + + + + + + + + my-custom-scope + + + + + + + + """); + + + + build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + // Assert: Files should be regenerated with the new scope + + var newScopedCssThumbprint = FileThumbPrint.Create(scopedCssFile); + + var newBundleThumbprint = FileThumbPrint.Create(bundleFile); - Assert.NotEqual(initialScopedCssThumbprint, newScopedCssThumbprint); - Assert.NotEqual(initialBundleThumbprint, newBundleThumbprint); + + + + + Assert.AreNotEqual(initialScopedCssThumbprint, newScopedCssThumbprint); + + + Assert.AreNotEqual(initialBundleThumbprint, newBundleThumbprint); + + + + // Verify the new content uses the custom scope + + var newContent = File.ReadAllText(scopedCssFile); + + newContent.Should().Contain("[my-custom-scope]"); + + newContent.Should().NotMatchRegex(@"\[b-[a-z0-9]+\]"); + + + + // Act 3: Change the custom scope to a different value + + File.WriteAllText( + + Path.Combine(projectDirectory.Path, "Directory.Build.targets"), + + """ + + + + + + + + my-updated-scope + + + + + + + + """); + + + + build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + // Assert: Files should be regenerated again with the updated scope + + var updatedScopedCssThumbprint = FileThumbPrint.Create(scopedCssFile); + + var updatedBundleThumbprint = FileThumbPrint.Create(bundleFile); - Assert.NotEqual(newScopedCssThumbprint, updatedScopedCssThumbprint); - Assert.NotEqual(newBundleThumbprint, updatedBundleThumbprint); + + + + + Assert.AreNotEqual(newScopedCssThumbprint, updatedScopedCssThumbprint); + + + Assert.AreNotEqual(newBundleThumbprint, updatedBundleThumbprint); + + + + // Verify the content uses the updated scope + + var updatedContent = File.ReadAllText(scopedCssFile); + + updatedContent.Should().Contain("[my-updated-scope]"); + + updatedContent.Should().NotContain("[my-custom-scope]"); + + + + // Act 4: Verify that building again without changes doesn't regenerate + + var finalScopedCssThumbprint = FileThumbPrint.Create(scopedCssFile); + + var finalBundleThumbprint = FileThumbPrint.Create(bundleFile); + + + + build = CreateBuildCommand(projectDirectory); + + ExecuteCommand(build).Should().Pass(); - Assert.Equal(finalScopedCssThumbprint, FileThumbPrint.Create(scopedCssFile)); - Assert.Equal(finalBundleThumbprint, FileThumbPrint.Create(bundleFile)); + + + + + Assert.AreEqual(finalScopedCssThumbprint, FileThumbPrint.Create(scopedCssFile)); + + + Assert.AreEqual(finalBundleThumbprint, FileThumbPrint.Create(bundleFile)); + + } + + + + // This test verifies if the targets that VS calls to update scoped css works to update these files - [Fact] + + + [TestMethod] + + public void RegeneratingScopedCss_ForProject() + + { + + // Arrange + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var build = CreateBuildCommand(ProjectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var bundlePath = Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css"); + + + + new FileInfo(bundlePath).Should().Exist(); + + + + // Make an edit + + var scopedCssFile = Path.Combine(ProjectDirectory.TestRoot, "Components", "Pages", "Index.razor.css"); + + File.WriteAllLines(scopedCssFile, File.ReadAllLines(scopedCssFile).Concat(["body { background-color: orangered; }"])); + + + + build = CreateBuildCommand(ProjectDirectory); + + ExecuteCommand(build, "/t:UpdateStaticWebAssetsDesignTime").Should().Pass(); + + + + // Verify the generated file contains newly added css + + AssertFileContains(bundlePath, "background-color: orangered"); + + + + // Verify that CSS edits continue to apply after new JS modules are added to the project + + // https://github.com/dotnet/aspnetcore/issues/57599 + + var collocatedJsFile = Path.Combine(ProjectDirectory.TestRoot, "Components", "Pages", "Index.razor.js"); + + File.WriteAllLines(collocatedJsFile, ["console.log('Hello, world!');"]); + + File.WriteAllLines(scopedCssFile, File.ReadAllLines(scopedCssFile).Concat(["h1 { color: purple; }"])); + + + + build = CreateBuildCommand(ProjectDirectory); + + ExecuteCommand(build, "/t:UpdateStaticWebAssetsDesignTime").Should().Pass(); + + + + // Verify the generated file contains newly added css + + AssertFileContains(bundlePath, "color: purple"); + + + + static void AssertFileContains(string fileName, string content) + + { + + var fileInfo = new FileInfo(fileName); + + fileInfo.Should().Exist(); + + fileInfo.ReadAllText().Should().Contain(content); + + } + + } + + } - public class ScopedCssCompatibilityIntegrationTest(ITestOutputHelper log) - : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, Path.Combine(nameof(ScopedCssCompatibilityIntegrationTest), ".nuget")) + + [TestClass] + + + + + public class ScopedCssCompatibilityIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest + { - [Fact] + + protected override string RestoreNugetPackagePath => Path.Combine(nameof(ScopedCssCompatibilityIntegrationTest), ".nuget"); + + + [TestMethod] + + public void ScopedCss_IsBackwardsCompatible_WithPreviousVersions() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) + + .WithProjectChanges((project, document) => + + { + + if (Path.GetFileName(project) == "AnotherClassLib.csproj") + + { + + document.Descendants("TargetFramework").Single().ReplaceNodes("net5.0"); + + } + + if (Path.GetFileName(project) == "ClassLibrary.csproj") + + { + + document.Descendants("TargetFramework").Single().ReplaceNodes("net5.0"); + + } + + }); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + AssertManifest( + + manifest, + + LoadBuildManifest()); + + + + AssertBuildAssets( + + manifest, + + outputPath, + + intermediateOutputPath); + + + + var appBundle = new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "AppWithPackageAndP2PReference.styles.css")); + + appBundle.Should().Exist(); + + + + appBundle.Should().Contain("_content/ClassLibrary/ClassLibrary.bundle.scp.css"); + + appBundle.Should().Match(""".*_content/RazorPackageLibraryDirectDependency/RazorPackageLibraryDirectDependency\.[a-zA-Z0-9]+\.bundle\.scp\.css.*"""); + + } - [Fact] + + + + + [TestMethod] + + public void ScopedCss_PublishIsBackwardsCompatible_WithPreviousVersions() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) + + .WithProjectChanges((project, document) => + + { + + if (Path.GetFileName(project) == "AnotherClassLib.csproj") + + { + + document.Descendants("TargetFramework").Single().ReplaceNodes("net5.0"); + + } + + if (Path.GetFileName(project) == "ClassLibrary.csproj") + + { + + document.Descendants("TargetFramework").Single().ReplaceNodes("net5.0"); + + } + + }); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + var build = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(build, "/bl").Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + new FileInfo(finalPath).Should().Exist(); + + var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"))); + + AssertManifest( + + publishManifest, + + LoadPublishManifest()); + + + + AssertPublishAssets( + + publishManifest, + + outputPath, + + intermediateOutputPath); + + + + var appBundle = new FileInfo(Path.Combine(outputPath, "wwwroot", "AppWithPackageAndP2PReference.styles.css")); + + appBundle.Should().Exist(); + + + + appBundle.Should().Contain("_content/ClassLibrary/ClassLibrary.bundle.scp.css"); + + appBundle.Should().Match("""_content/RazorPackageLibraryDirectDependency/RazorPackageLibraryDirectDependency\.[a-zA-Z0-9]+\.bundle\.scp\.css"""); + + } + + } - public class ScopedCssPackageReferences(ITestOutputHelper log) - : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, Path.Combine(nameof(ScopedCssPackageReferences), ".nuget")) + + [TestClass] + + + + + public class ScopedCssPackageReferences : IsolatedNuGetPackageFolderAspNetSdkBaselineTest + { - [Fact] + + protected override string RestoreNugetPackagePath => Path.Combine(nameof(ScopedCssPackageReferences), ".nuget"); + + + [TestMethod] + + public void BuildProjectWithReferences_CorrectlyBundlesScopedCssFiles() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); - var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - ExecuteCommand(build).Should().Pass(); - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + + ExecuteCommand(build).Should().Pass(); + + + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + + var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + AssertManifest( + + buildManifest, + + LoadBuildManifest()); + + + + AssertBuildAssets( + + buildManifest, + + outputPath, + + intermediateOutputPath); + + + + var appBundle = new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "AppWithPackageAndP2PReference.styles.css")); + + appBundle.Should().Exist(); + + + + appBundle.Should().Match(""".*_content/RazorPackageLibraryDirectDependency/RazorPackageLibraryDirectDependency\.[a-zA-Z0-9]+\.bundle\.scp\.css.*"""); + + appBundle.Should().Match(""".*_content/ClassLibrary/ClassLibrary\.[a-zA-Z0-9]+\.bundle\.scp\.css.*"""); + + } + + + + // Regression test for https://github.com/dotnet/aspnetcore/issues/37592 - [Fact] + + + [TestMethod] + + public void RegeneratingScopedCss_ForProjectWithReferences() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var scopedCssFile = Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "Index.razor.css"); + + File.WriteAllText(scopedCssFile, "/* Empty css */"); + + File.WriteAllText(Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "Index.razor"), "This is a test razor component."); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var bundlePath = Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "AppWithPackageAndP2PReference.styles.css"); + + + + new FileInfo(bundlePath).Should().Exist(); + + + + // Make an edit to a scoped css file + + File.WriteAllLines(scopedCssFile, File.ReadAllLines(scopedCssFile).Concat(new[] { "body { background-color: orangered; }" })); + + + + build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(build, "/t:UpdateStaticWebAssetsDesignTime").Should().Pass(); + + + + var fileInfo = new FileInfo(bundlePath); + + fileInfo.Should().Exist(); + + // Verify the generated file contains newly added css + + var text = fileInfo.ReadAllText(); + + text.Should().Contain("background-color: orangered"); + + text.Should().MatchRegex(""".*@import '_content/ClassLibrary/ClassLibrary\.[a-zA-Z0-9]+\.bundle\.scp\.css.*"""); + + } - [Fact] + + + + + [TestMethod] + + public void Build_GeneratesUrlEncodedLinkHeaderForNonAsciiProjectName() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + // Rename the ClassLibrary project to have non-ASCII characters + + var originalLibPath = Path.Combine(ProjectDirectory.Path, "AnotherClassLib"); + + var newLibPath = Path.Combine(ProjectDirectory.Path, "项目"); + + Directory.Move(originalLibPath, newLibPath); + + + + // Update the project file to set the assembly name and package ID + + var libProjectFile = Path.Combine(newLibPath, "AnotherClassLib.csproj"); + + var newLibProjectFile = Path.Combine(newLibPath, "项目.csproj"); + + File.Move(libProjectFile, newLibProjectFile); + + + + // Add assembly name property to ensure consistent naming + + var libProjectContent = File.ReadAllText(newLibProjectFile); + + // Find the first PropertyGroup closing tag and replace it + + var targetPattern = ""; + + var replacement = " 项目\n 项目\n "; + + var index = libProjectContent.IndexOf(targetPattern); + + if (index >= 0) + + { + + libProjectContent = libProjectContent.Substring(0, index) + replacement + libProjectContent.Substring(index + targetPattern.Length); + + } + + File.WriteAllText(newLibProjectFile, libProjectContent); + + + + // Update the main project to reference the renamed library + + var mainProjectFile = Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "AppWithPackageAndP2PReference.csproj"); + + var mainProjectContent = File.ReadAllText(mainProjectFile); + + mainProjectContent = mainProjectContent.Replace(@"..\AnotherClassLib\AnotherClassLib.csproj", @"..\项目\项目.csproj"); + + File.WriteAllText(mainProjectFile, mainProjectContent); + + + + // Ensure library has scoped CSS + + var libCssFile = Path.Combine(newLibPath, "Views", "Shared", "Index.cshtml.css"); + + if (!File.Exists(libCssFile)) + + { + + Directory.CreateDirectory(Path.GetDirectoryName(libCssFile)); + + File.WriteAllText(libCssFile, ".test { color: red; }"); + + } + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + + + // Check that the staticwebassets.build.endpoints.json file contains URL-encoded characters + + var endpointsFile = Path.Combine(intermediateOutputPath, "staticwebassets.build.endpoints.json"); + + new FileInfo(endpointsFile).Should().Exist(); + + + + var endpointsContent = File.ReadAllText(endpointsFile); + + var json = JsonSerializer.Deserialize(endpointsContent, new JsonSerializerOptions(JsonSerializerDefaults.Web)); + + + + var styles = json.Endpoints.Where(e => e.Route.EndsWith("styles.css")); + + + + foreach (var styleEndpoint in styles) + + { + + styleEndpoint.ResponseHeaders.Should().Contain(h => h.Name.Equals("Link", StringComparison.OrdinalIgnoreCase) && h.Value.Contains("%E9%A1%B9%E7%9B%AE")); + + } + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetEndpointsIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetEndpointsIntegrationTest.cs index da086c81cb7e..7eece0c1b1f7 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetEndpointsIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetEndpointsIntegrationTest.cs @@ -2,650 +2,1963 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + 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) + + + + +[TestClass] + +public partial class StaticWebAssetEndpointsIntegrationTest : AspNetSdkBaselineTest + + { + + [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] + + + + + [TestMethod] + + 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(39); + + 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] + + + + + [TestMethod] + + 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(65); + + 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] + + + + + [TestMethod] + + 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(65); + + + + AssertManifest(publishManifest, LoadPublishManifest()); + + } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + + + + + [TestMethod] + + [RequiresMSBuildVersion("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")] + + + + + [TestMethod] + + [RequiresMSBuildVersion("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")] + + + + + [TestMethod] + + [RequiresMSBuildVersion("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")] + + + + + [TestMethod] + + [RequiresMSBuildVersion("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"); + + } - [Fact] + + + + + [TestMethod] + + public void Build_DefaultDocumentAndSpaFallback_CreatesAdditionalEndpoints() + + { + + ProjectDirectory = CreateAspNetSdkTestAsset("RazorComponentApp") + + .WithProjectChanges(document => + + { + + document.Root.AddFirst( + + new XElement("PropertyGroup", + + new XElement("StaticWebAssetDefaultDocumentEnabled", "true"), + + new XElement("StaticWebAssetSpaFallbackEnabled", "true"))); + + }); + + var root = ProjectDirectory.TestRoot; - var dir = Directory.CreateDirectory(Path.Combine(root, "wwwroot")); - File.WriteAllText(Path.Combine(dir.FullName, "index.html"), "Hello"); + + + + + var dir = Directory.CreateDirectory(Path.Combine(root, "wwwroot")); + + + File.WriteAllText(Path.Combine(dir.FullName, "index.html"), "Hello"); + + + + var build = CreateBuildCommand(ProjectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + new FileInfo(path).Should().Exist(); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + + + var endpoints = manifest.Endpoints; + + + + // There should be endpoints for index.html (original + fingerprinted + default document + spa fallback) + + var indexHtmlEndpoints = endpoints.Where(ep => ep.AssetFile.Contains("index.html") && !ep.AssetFile.Contains(".gz") && !ep.AssetFile.Contains(".br")); + + + + // Original index.html endpoint + + indexHtmlEndpoints.Should().Contain(e => e.Route == "index.html"); + + + + // SPA fallback endpoint with catch-all route and max int order + + var fallback = endpoints.FirstOrDefault(e => e.Route == "{**fallback:nonfile}"); + + fallback.Should().NotBeNull(); + + fallback.Order.Should().Be("2147483647"); + + + + AssertManifest(manifest, LoadBuildManifest()); + + } + + + + // 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(File.ReadAllText(endpointsManifestFile)); + + + + var wwwrootFolderFiles = GetWwwrootFolderFiles(outputDirectory); + + + + var foundAssets = new HashSet(); + + 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().HaveCountGreaterThanOrEqualTo(2); + + } + + else if (endpointsByAssetFile[file].Length > 1) + + { + + endpointsByAssetFile[file].Where(e => e.EndpointProperties.Any(p => p.Name == "integrity")).Count().Should().BeGreaterThanOrEqualTo(1); + + } + + else + + { + + endpointsByAssetFile[file].Should().HaveCount(1); + + } + + } + + + + HashSet 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(File.ReadAllText(Path.Combine(outputDirectory, $"{projectName}.staticwebassets.runtime.json"))); + + var endpoints = new HashSet(); + + + + //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 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 Children { get; set; } + + public StaticWebAssetMatch Asset { get; set; } + + public StaticWebAssetPattern[] Patterns { get; set; } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCompressionNegotiationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCompressionNegotiationTest.cs index 9e0eca9e4a7e..b26dbddabf4a 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCompressionNegotiationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCompressionNegotiationTest.cs @@ -1,1699 +1,5099 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Globalization; + + 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; + + + + +[TestClass] + public class ApplyCompressionNegotiationTest + + { - [Fact] + + + [TestMethod] + + public void AppliesContentNegotiationRules_ForExistingAssets() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ApplyCompressionNegotiation + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = + + [ + + CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "original-fingerprint", + + "original", + + fileLength: 20 + + ), + + CreateCandidate( + + Path.Combine("compressed", "candidate.js.gz"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "compressed-fingerprint", + + "compressed", + + Path.Combine("wwwroot", "candidate.js"), + + "Content-Encoding", + + "gzip", + + 9 + + ) + + ], + + CandidateEndpoints = + + [ + + CreateCandidateEndpoint( + + "candidate.js", + + Path.Combine("wwwroot", "candidate.js"), + + CreateHeaders("text/javascript", [("Content-Length", "20")])), + + + + CreateCandidateEndpoint( + + "candidate.js.gz", + + Path.Combine("compressed", "candidate.js.gz"), + + CreateHeaders("text/javascript", [("Content-Length", "9")])) + + ], + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); + + endpoints.Should().BeEquivalentTo((StaticWebAssetEndpoint[])[ + + new () + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Length", Value = "9" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [ new () { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], + + }, + + new () + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Length", Value = "20" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" }, + + ], + + EndpointProperties = [], + + Selectors = [], + + }, + + new () + + { + + Route = "candidate.js.gz", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Length", Value = "9" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [] + + } + + ]); + + } - [Fact] + + + + + [TestMethod] + + public void AppliesContentNegotiationRules_ForExistingAssets_WithFingerprints() + + { + + var now = DateTime.Now; + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + List candidateAssets = [ + + CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate#[.{fingerprint}]?.js", + + "All", + + "All", + + "original-fingerprint", + + "original", + + fileLength: 20, + + lastModified: now + + ) + + ]; + + + + var compressedTask = new ResolveCompressedAssets + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [.. candidateAssets], + + Formats = "gzip;brotli", + + IncludePatterns = "*.js", + + OutputPath = AppContext.BaseDirectory + + }; + + compressedTask.Execute().Should().BeTrue(); + + + + var compressedAssets = compressedTask.AssetsToCompress; + + compressedAssets[0].SetMetadata(nameof(StaticWebAsset.Fingerprint), "gzip"); + + compressedAssets[0].SetMetadata(nameof(StaticWebAsset.Integrity), "compressed-gzip"); + + compressedAssets[0].SetMetadata(nameof(StaticWebAsset.FileLength), "9"); + + compressedAssets[1].SetMetadata(nameof(StaticWebAsset.Fingerprint), "brotli"); + + compressedAssets[1].SetMetadata(nameof(StaticWebAsset.Integrity), "compressed-brotli"); + + compressedAssets[1].SetMetadata(nameof(StaticWebAsset.FileLength), "7"); + + candidateAssets.AddRange(compressedAssets); + + var expectedName = Path.GetFileNameWithoutExtension(compressedAssets[0].ItemSpec); + + var defineStaticAssetEndpointsTask = new DefineStaticWebAssetEndpoints + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [.. candidateAssets], + + ExistingEndpoints = [], + + ContentTypeMappings = [] + + }; + + defineStaticAssetEndpointsTask.Execute().Should().BeTrue(); + + var compressed = defineStaticAssetEndpointsTask.Endpoints; + + + + var task = new ApplyCompressionNegotiation + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [.. candidateAssets], + + CandidateEndpoints = compressed, + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); + + var expectedEndpoints = new StaticWebAssetEndpoint[] + + { + + new() + + { + + Route = "candidate.fingerprint.js", + + AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.br"), + + Selectors = [ + + new () + + { + + Name = "Content-Encoding", + + Value = "br", + + Quality = "0.125000000000" + + } + + ], + + ResponseHeaders = [ new () + + { + + Name = "Cache-Control", + + Value = "max-age=31536000, immutable" + + }, + + new () + + { + + Name = "Content-Encoding", + + Value = "br" + + }, + + new () + + { + + Name = "Content-Length", + + Value = "7" + + }, + + new () + + { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new () + + { + + Name = "ETag", + + Value = "\u0022compressed-brotli\u0022" + + }, + + new () + + { + + Name = "Last-Modified", + + Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) + + }, + + new () + + { + + Name = "Vary", + + Value = "Accept-Encoding" + + } + + ], + + EndpointProperties = [ + + new () + + { + + Name = "fingerprint", + + Value = "fingerprint" + + }, + + new () + + { + + Name = "integrity", + + Value = "sha256-original" + + }, + + new () + + { + + Name = "label", + + Value = "candidate.js" + + } + + ] + + }, + + new() + + { + + Route = "candidate.fingerprint.js", + + AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.gz"), + + Selectors = [ + + new () + + { + + Name = "Content-Encoding", + + Value = "gzip", + + Quality = "0.100000000000" + + } + + ], + + ResponseHeaders = [ new () + + { + + Name = "Cache-Control", + + Value = "max-age=31536000, immutable" + + }, + + new () + + { + + Name = "Content-Encoding", + + Value = "gzip" + + }, + + new () + + { + + Name = "Content-Length", + + Value = "9" + + }, + + new () + + { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new () + + { + + Name = "ETag", + + Value = "\u0022compressed-gzip\u0022" + + }, + + new () + + { + + Name = "Last-Modified", + + Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) + + }, + + new () + + { + + Name = "Vary", + + Value = "Accept-Encoding" + + } + + ], + + EndpointProperties = [ + + new () + + { + + Name = "fingerprint", + + Value = "fingerprint" + + }, + + new () + + { + + Name = "integrity", + + Value = "sha256-original" + + }, + + new () + + { + + Name = "label", + + Value = "candidate.js" + + } + + ] + + }, + + new() + + { + + Route = "candidate.fingerprint.js", + + AssetFile = Path.Combine(AppContext.BaseDirectory, "wwwroot", "candidate.js"), + + ResponseHeaders = [ new () + + { + + Name = "Cache-Control", + + Value = "max-age=31536000, immutable" + + }, + + new () + + { + + Name = "Content-Length", + + Value = "20" + + }, + + new () + + { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new () + + { + + Name = "ETag", + + Value = "\u0022original\u0022" + + }, + + new () + + { + + Name = "Last-Modified", + + Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) + + }, + + new () + + { + + Name = "Vary", + + Value = "Accept-Encoding" + + } + + ], + + EndpointProperties = [ + + new () + + { + + Name = "fingerprint", + + Value = "fingerprint" + + }, + + new () + + { + + Name = "integrity", + + Value = "sha256-original" + + }, + + new () + + { + + Name = "label", + + Value = "candidate.js" + + } + + ] + + }, + + new() + + { + + Route = "candidate.fingerprint.js.br", + + AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.br"), + + ResponseHeaders = [ new () + + { + + Name = "Cache-Control", + + Value = "max-age=31536000, immutable" + + }, + + new () + + { + + Name = "Content-Encoding", + + Value = "br" + + }, + + new () + + { + + Name = "Content-Length", + + Value = "7" + + }, + + new () + + { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new () + + { + + Name = "ETag", + + Value = "\u0022compressed-brotli\u0022" + + }, + + new () + + { + + Name = "Last-Modified", + + Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) + + }, + + new () + + { + + Name = "Vary", + + Value = "Accept-Encoding" + + } + + ], + + EndpointProperties = [ + + new () + + { + + Name = "fingerprint", + + Value = "fingerprint" + + }, + + new () + + { + + Name = "integrity", + + Value = "sha256-compressed-brotli" + + }, + + new () + + { + + Name = "label", + + Value = "candidate.js.br" + + } + + ] + + }, + + new() + + { + + Route = "candidate.fingerprint.js.gz", + + AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.gz"), + + ResponseHeaders = [ new () + + { + + Name = "Cache-Control", + + Value = "max-age=31536000, immutable" + + }, + + new () + + { + + Name = "Content-Encoding", + + Value = "gzip" + + }, + + new () + + { + + Name = "Content-Length", + + Value = "9" + + }, + + new () + + { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new () + + { + + Name = "ETag", + + Value = "\u0022compressed-gzip\u0022" + + }, + + new () + + { + + Name = "Last-Modified", + + Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) + + }, + + new () + + { + + Name = "Vary", + + Value = "Accept-Encoding" + + } + + ], + + EndpointProperties = [ + + new () + + { + + Name = "fingerprint", + + Value = "fingerprint" + + }, + + new () + + { + + Name = "integrity", + + Value = "sha256-compressed-gzip" + + }, + + new () + + { + + Name = "label", + + Value = "candidate.js.gz" + + } + + ] + + }, + + new() + + { + + Route = "candidate.js", + + AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.br"), + + Selectors = [ + + new () + + { + + Name = "Content-Encoding", + + Value = "br", + + Quality = "0.125000000000" + + } + + ], + + ResponseHeaders = [ new () + + { + + Name = "Cache-Control", + + Value = "no-cache" + + }, + + new () + + { + + Name = "Content-Encoding", + + Value = "br" + + }, + + new () + + { + + Name = "Content-Length", + + Value = "7" + + }, + + new () + + { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new () + + { + + Name = "ETag", + + Value = "\u0022compressed-brotli\u0022" + + }, + + new () + + { + + Name = "Last-Modified", + + Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) + + }, + + new () + + { + + Name = "Vary", + + Value = "Accept-Encoding" + + } + + ], + + EndpointProperties = [ + + new () + + { + + Name = "integrity", + + Value = "sha256-original" + + } + + ] + + }, + + new() + + { + + Route = "candidate.js", + + AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.gz"), + + Selectors = [ + + new () + + { + + Name = "Content-Encoding", + + Value = "gzip", + + Quality = "0.100000000000" + + } + + ], + + ResponseHeaders = [ new () + + { + + Name = "Cache-Control", + + Value = "no-cache" + + }, + + new () + + { + + Name = "Content-Encoding", + + Value = "gzip" + + }, + + new () + + { + + Name = "Content-Length", + + Value = "9" + + }, + + new () + + { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new () + + { + + Name = "ETag", + + Value = "\u0022compressed-gzip\u0022" + + }, + + new () + + { + + Name = "Last-Modified", + + Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) + + }, + + new () + + { + + Name = "Vary", + + Value = "Accept-Encoding" + + } + + ], + + EndpointProperties = [ + + new () + + { + + Name = "integrity", + + Value = "sha256-original" + + } + + ] + + }, + + new() + + { + + Route = "candidate.js", + + AssetFile = Path.Combine(AppContext.BaseDirectory, "wwwroot", "candidate.js"), + + ResponseHeaders = [ new () + + { + + Name = "Cache-Control", + + Value = "no-cache" + + }, + + new () + + { + + Name = "Content-Length", + + Value = "20" + + }, + + new () + + { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new () + + { + + Name = "ETag", + + Value = "\u0022original\u0022" + + }, + + new () + + { + + Name = "Last-Modified", + + Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) + + }, + + new () + + { + + Name = "Vary", + + Value = "Accept-Encoding" + + } + + ], + + EndpointProperties = [ + + new () + + { + + Name = "integrity", + + Value = "sha256-original" + + } + + ] + + }, + + new() + + { + + Route = "candidate.js.br", + + AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.br"), + + ResponseHeaders = [ new () + + { + + Name = "Cache-Control", + + Value = "no-cache" + + }, + + new () + + { + + Name = "Content-Encoding", + + Value = "br" + + }, + + new () + + { + + Name = "Content-Length", + + Value = "7" + + }, + + new () + + { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new () + + { + + Name = "ETag", + + Value = "\u0022compressed-brotli\u0022" + + }, + + new () + + { + + Name = "Last-Modified", + + Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) + + }, + + new () + + { + + Name = "Vary", + + Value = "Accept-Encoding" + + } + + ], + + EndpointProperties = [ + + new () + + { + + Name = "integrity", + + Value = "sha256-compressed-brotli" + + } + + ] + + }, + + new() + + { + + Route = "candidate.js.gz", + + AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.gz"), + + ResponseHeaders = [ new () { + + Name = "Cache-Control", + + Value = "no-cache" + + }, + + new () { + + Name = "Content-Encoding", + + Value = "gzip" + + }, + + new () { + + Name = "Content-Length", + + Value = "9" + + }, + + new () { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new () { + + Name = "ETag", + + Value = "\u0022compressed-gzip\u0022" + + }, + + new () { + + Name = "Last-Modified", + + Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) + + }, + + new () { + + Name = "Vary", + + Value = "Accept-Encoding" + + } + + ], + + EndpointProperties = [ + + new () { + + Name = "integrity", + + Value = "sha256-compressed-gzip" + + } + + ] + + } + + }; + + + + endpoints.Should().BeEquivalentTo(expectedEndpoints); + + } - [Fact] + + + + + [TestMethod] + + public void AppliesContentNegotiationRules_ToAllRelatedAssetEndpoints() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ApplyCompressionNegotiation + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = + + [ + + CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "original-fingerprint", + + "original", + + fileLength: 20 + + ), + + CreateCandidate( + + Path.Combine("compressed", "candidate.js.gz"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "compressed-fingerprint", + + "compressed", + + Path.Combine("wwwroot", "candidate.js"), + + "Content-Encoding", + + "gzip", + + fileLength: 9 + + ) + + ], + + CandidateEndpoints = + + [ + + CreateCandidateEndpoint( + + "candidate.js", + + Path.Combine("wwwroot", "candidate.js"), + + CreateHeaders("text/javascript")), + + + + CreateCandidateEndpoint( + + "candidate.fingerprint.js", + + Path.Combine("wwwroot", "candidate.js"), + + CreateHeaders("text/javascript")), + + + + CreateCandidateEndpoint( + + "candidate.js.gz", + + Path.Combine("compressed", "candidate.js.gz"), + + CreateHeaders("text/javascript")) + + ], + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); + + endpoints.Should().BeEquivalentTo((StaticWebAssetEndpoint[])[ + + new () + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [ new () { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], + + }, + + new () + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [], + + }, + + new () + + { + + Route = "candidate.fingerprint.js", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [ new () { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], + + }, + + new () + + { + + Route = "candidate.fingerprint.js", + + AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [], + + }, + + new () + + { + + Route = "candidate.js.gz", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [] + + } + + ]); + + } - [Fact] + + + + + [TestMethod] + + public void AppliesContentNegotiationRules_IgnoresAlreadyProcessedEndpoints() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ApplyCompressionNegotiation + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = + + [ + + CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "original-fingerprint", + + "original" + + ), + + CreateCandidate( + + Path.Combine("compressed", "candidate.js.gz"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "compressed-fingerprint", + + "compressed", + + Path.Combine("wwwroot", "candidate.js"), + + "Content-Encoding", + + "gzip" + + ) + + ], + + CandidateEndpoints = new StaticWebAssetEndpoint[] + + { + + new() + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new (){ Name = "Content-Type", Value = "text/javascript" }, + + new (){ Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], + + }, + + new() + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), + + ResponseHeaders = + + [ + + new (){ Name = "Content-Type", Value = "text/javascript" } + + ], + + EndpointProperties = [], + + Selectors = [], + + }, + + new() + + { + + Route = "candidate.fingerprint.js", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new (){ Name = "Content-Encoding", Value = "gzip" }, + + new (){ Name = "Content-Type", Value = "text/javascript" }, + + new (){ Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [ new () { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], + + }, + + new() + + { + + Route = "candidate.fingerprint.js", + + AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Type", Value = "text/javascript" } + + ], + + EndpointProperties = [], + + Selectors = [], + + }, + + new() + + { + + Route = "candidate.js.gz", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [] + + } + + }.Select(e => e.ToTaskItem()).ToArray(), + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); + + endpoints.Should().BeEquivalentTo([ + + new StaticWebAssetEndpoint + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], + + }, + + new StaticWebAssetEndpoint + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [], + + }, + + new StaticWebAssetEndpoint + + { + + Route = "candidate.fingerprint.js", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], + + }, + + new StaticWebAssetEndpoint + + { + + Route = "candidate.fingerprint.js", + + AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [], + + }, + + new StaticWebAssetEndpoint + + { + + Route = "candidate.js.gz", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [] + + } + + ]); + + } - [Fact] + + + + + [TestMethod] + + public void AppliesContentNegotiationRules_ProcessesNewCompressedFormatsWhenAvailable() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ApplyCompressionNegotiation + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = + + [ + + CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "original-fingerprint", + + "original", + + fileLength: 20 + + ), + + CreateCandidate( + + Path.Combine("compressed", "candidate.js.gz"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "compressed-gzip", + + "compressed", + + Path.Combine("wwwroot", "candidate.js"), + + "Content-Encoding", + + "gzip", + + fileLength: 9 + + ), + + CreateCandidate( + + Path.Combine("compressed", "candidate.js.br"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "compressed-brotli", + + "compressed", + + Path.Combine("wwwroot", "candidate.js"), + + "Content-Encoding", + + "br", + + fileLength: 9 + + ) + + ], + + CandidateEndpoints = new StaticWebAssetEndpoint[] + + { + + new() + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new (){ Name = "Content-Type", Value = "text/javascript" }, + + new (){ Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], + + }, + + new() + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), + + ResponseHeaders = + + [ + + new (){ Name = "Content-Type", Value = "text/javascript" } + + ], + + EndpointProperties = [], + + Selectors = [], + + }, + + new() + + { + + Route = "candidate.fingerprint.js", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new (){ Name = "Content-Encoding", Value = "gzip" }, + + new (){ Name = "Content-Type", Value = "text/javascript" }, + + new (){ Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [ new () { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], + + }, + + new() + + { + + Route = "candidate.fingerprint.js", + + AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Type", Value = "text/javascript" } + + ], + + EndpointProperties = [], + + Selectors = [], + + }, + + new() + + { + + Route = "candidate.js.gz", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [] + + }, + + new() + + { + + Route = "candidate.js.br", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.br")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Type", Value = "text/javascript" }, + + ], + + EndpointProperties = [], + + Selectors = [] + + } + + }.Select(e => e.ToTaskItem()).ToArray(), + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); + + endpoints.Should().BeEquivalentTo([ + + new StaticWebAssetEndpoint + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], + + }, + + new StaticWebAssetEndpoint + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.br")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "br" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "br", Quality = "0.100000000000" } ], + + }, + + new StaticWebAssetEndpoint + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [], + + }, + + new StaticWebAssetEndpoint + + { + + Route = "candidate.fingerprint.js", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], + + }, + + new StaticWebAssetEndpoint + + { + + Route = "candidate.fingerprint.js", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.br")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "br" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "br", Quality = "0.100000000000" } ], + + }, + + new StaticWebAssetEndpoint + + { + + Route = "candidate.fingerprint.js", + + AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [], + + }, + + new StaticWebAssetEndpoint + + { + + Route = "candidate.js.gz", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [] + + }, + + new StaticWebAssetEndpoint + + { + + Route = "candidate.js.br", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.br")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "br" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [] + + } + + ]); + + } - [Fact] + + + + + [TestMethod] + + public void AppliesContentNegotiationRules_AddsVaryHeaderToEndpointsWithSameRouteButDifferentAssets() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ApplyCompressionNegotiation + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = + + [ + + CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "original-fingerprint", + + "original", + + fileLength: 20 + + ), + + CreateCandidate( + + Path.Combine("compressed", "candidate.js.gz"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "compressed-fingerprint", + + "compressed", + + Path.Combine("wwwroot", "candidate.js"), + + "Content-Encoding", + + "gzip", + + 9 + + ), + + // This represents a different asset (e.g., a publish asset) that shares the same route + + // but wasn't part of the compression processing + + CreateCandidate( + + Path.Combine("publish", "candidate.js"), + + "PublishPackage", + + "Discovered", + + "candidate.js", + + "Publish", + + "All", + + "publish-fingerprint", + + "publish", + + fileLength: 18 + + ) + + ], + + CandidateEndpoints = + + [ + + CreateCandidateEndpoint( + + "candidate.js", + + Path.Combine("wwwroot", "candidate.js"), + + CreateHeaders("text/javascript", [("Content-Length", "20")])), + + + + CreateCandidateEndpoint( + + "candidate.js.gz", + + Path.Combine("compressed", "candidate.js.gz"), + + CreateHeaders("text/javascript", [("Content-Length", "9")])), + + + + // This endpoint shares the route but points to a different asset + + CreateCandidateEndpoint( + + "candidate.js", + + Path.Combine("publish", "candidate.js"), + + CreateHeaders("text/javascript", [("Content-Length", "18")])) + + ], + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); + + endpoints.Should().BeEquivalentTo((StaticWebAssetEndpoint[])[ + + new () + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Length", Value = "9" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [ new () { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], + + }, + + new () + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Length", Value = "20" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" }, + + ], + + EndpointProperties = [], + + Selectors = [], + + }, + + new () + + { + + Route = "candidate.js.gz", + + AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Encoding", Value = "gzip" }, + + new () { Name = "Content-Length", Value = "9" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" } + + ], + + EndpointProperties = [], + + Selectors = [] + + }, + + new () + + { + + Route = "candidate.js", + + AssetFile = Path.GetFullPath(Path.Combine("publish", "candidate.js")), + + ResponseHeaders = + + [ + + new () { Name = "Content-Length", Value = "18" }, + + new () { Name = "Content-Type", Value = "text/javascript" }, + + new () { Name = "Vary", Value = "Accept-Encoding" }, + + ], + + EndpointProperties = [], + + Selectors = [], + + } + + ]); + + } + + + + private static StaticWebAssetEndpointResponseHeader[] CreateHeaders(string contentType, params (string name, string value)[] AdditionalHeaders) + + { + + return + + [ + + new StaticWebAssetEndpointResponseHeader { + + Name = "Content-Type", + + Value = contentType + + }, + + ..(AdditionalHeaders ?? []).Select(h => new StaticWebAssetEndpointResponseHeader { Name = h.name, Value = h.value }) + + ]; + + } + + + + private static ITaskItem CreateCandidate( + + string itemSpec, + + string sourceId, + + string sourceType, + + string relativePath, + + string assetKind, + + string assetMode, + + string fingerprint = "", + + string integrity = "", + + string relatedAsset = "", + + string assetTraitName = "", + + string assetTraitValue = "", + + long fileLength = 9, + + DateTimeOffset? lastModified = null) + + { + + lastModified ??= new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero); + + var result = new StaticWebAsset() + + { + + Identity = Path.GetFullPath(itemSpec), + + SourceId = sourceId, + + SourceType = sourceType, + + ContentRoot = Directory.GetCurrentDirectory(), + + BasePath = "base", + + RelativePath = relativePath, + + AssetKind = assetKind, + + AssetMode = assetMode, + + AssetRole = "Primary", + + RelatedAsset = relatedAsset, + + AssetTraitName = assetTraitName, + + AssetTraitValue = assetTraitValue, + + CopyToOutputDirectory = "", + + CopyToPublishDirectory = "", + + OriginalItemSpec = itemSpec, + + // Add these to avoid accessing the disk to compute them + + Integrity = integrity, + + Fingerprint = "fingerprint", + + FileLength = fileLength, + + LastWriteTime = lastModified.Value, + + }; + + + + result.ApplyDefaults(); + + result.Normalize(); + + + + return result.ToTaskItem(); + + } + + + + private static ITaskItem CreateCandidateEndpoint( + + string route, + + string assetFile, + + StaticWebAssetEndpointResponseHeader[] responseHeaders = null, + + StaticWebAssetEndpointSelector[] responseSelector = null, + + StaticWebAssetEndpointProperty[] properties = null) + + { + + return new StaticWebAssetEndpoint + + { + + Route = route, + + AssetFile = Path.GetFullPath(assetFile), + + ResponseHeaders = responseHeaders ?? [], + + EndpointProperties = properties ?? [], + + Selectors = responseSelector ?? [] + + }.ToTaskItem(); + + } - [Fact] + + + + + [TestMethod] + + public void AppliesContentNegotiationRules_AttachesWeakETagAsResponseHeader() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ApplyCompressionNegotiation + + { + + BuildEngine = buildEngine.Object, + + AttachWeakETagToCompressedAssets = "ResponseHeader", + + CandidateAssets = + + [ + + CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "original-fingerprint", + + "original", + + fileLength: 20 + + ), + + CreateCandidate( + + Path.Combine("compressed", "candidate.js.gz"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "compressed-fingerprint", + + "compressed", + + Path.Combine("wwwroot", "candidate.js"), + + "Content-Encoding", + + "gzip", + + 9 + + ) + + ], + + CandidateEndpoints = + + [ + + CreateCandidateEndpoint( + + "candidate.js", + + Path.Combine("wwwroot", "candidate.js"), + + CreateHeaders("text/javascript", [("Content-Length", "20"), ("ETag", "\"original-etag\"")])), + + + + CreateCandidateEndpoint( + + "candidate.js.gz", + + Path.Combine("compressed", "candidate.js.gz"), + + CreateHeaders("text/javascript", [("Content-Length", "9"), ("ETag", "\"compressed-etag\"")])) + + ], + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); + + + + // The compressed endpoint for the original route should have the weak ETag from the original + + var compressedEndpoint = endpoints.FirstOrDefault(e => e.Route == "candidate.js" && e.AssetFile.Contains("candidate.js.gz")); + + compressedEndpoint.Should().NotBeNull(); + + compressedEndpoint.ResponseHeaders.Should().Contain(h => h.Name == "ETag" && h.Value == "W/\"original-etag\""); + + } - [Fact] + + + + + [TestMethod] + + public void AppliesContentNegotiationRules_AttachesWeakETagAsEndpointProperty() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ApplyCompressionNegotiation + + { + + BuildEngine = buildEngine.Object, + + AttachWeakETagToCompressedAssets = "EndpointProperty", + + CandidateAssets = + + [ + + CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "original-fingerprint", + + "original", + + fileLength: 20 + + ), + + CreateCandidate( + + Path.Combine("compressed", "candidate.js.gz"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "compressed-fingerprint", + + "compressed", + + Path.Combine("wwwroot", "candidate.js"), + + "Content-Encoding", + + "gzip", + + 9 + + ) + + ], + + CandidateEndpoints = + + [ + + CreateCandidateEndpoint( + + "candidate.js", + + Path.Combine("wwwroot", "candidate.js"), + + CreateHeaders("text/javascript", [("Content-Length", "20"), ("ETag", "\"original-etag\"")])), + + + + CreateCandidateEndpoint( + + "candidate.js.gz", + + Path.Combine("compressed", "candidate.js.gz"), + + CreateHeaders("text/javascript", [("Content-Length", "9"), ("ETag", "\"compressed-etag\"")])) + + ], + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); + + + + // The compressed endpoint for the original route should have the original-resource property + + var compressedEndpoint = endpoints.FirstOrDefault(e => e.Route == "candidate.js" && e.AssetFile.Contains("candidate.js.gz")); + + compressedEndpoint.Should().NotBeNull(); + + compressedEndpoint.EndpointProperties.Should().Contain(p => p.Name == "original-resource" && p.Value == "\"original-etag\""); + + } - [Fact] + + + + + [TestMethod] + + public void AppliesContentNegotiationRules_DoesNotAttachETagWhenModeIsEmpty() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ApplyCompressionNegotiation + + { + + BuildEngine = buildEngine.Object, + + AttachWeakETagToCompressedAssets = "", // Empty string should not attach ETag + + CandidateAssets = + + [ + + CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "original-fingerprint", + + "original", + + fileLength: 20 + + ), + + CreateCandidate( + + Path.Combine("compressed", "candidate.js.gz"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "compressed-fingerprint", + + "compressed", + + Path.Combine("wwwroot", "candidate.js"), + + "Content-Encoding", + + "gzip", + + 9 + + ) + + ], + + CandidateEndpoints = + + [ + + CreateCandidateEndpoint( + + "candidate.js", + + Path.Combine("wwwroot", "candidate.js"), + + CreateHeaders("text/javascript", [("Content-Length", "20"), ("ETag", "\"original-etag\"")])), + + + + CreateCandidateEndpoint( + + "candidate.js.gz", + + Path.Combine("compressed", "candidate.js.gz"), + + CreateHeaders("text/javascript", [("Content-Length", "9"), ("ETag", "\"compressed-etag\"")])) + + ], + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); + + + + // The compressed endpoint for the original route should not have weak ETag or original-resource property + + var compressedEndpoint = endpoints.FirstOrDefault(e => e.Route == "candidate.js" && e.AssetFile.Contains("candidate.js.gz")); + + compressedEndpoint.Should().NotBeNull(); + + compressedEndpoint.ResponseHeaders.Should().NotContain(h => h.Name == "ETag" && h.Value.StartsWith("W/")); + + compressedEndpoint.EndpointProperties.Should().NotContain(p => p.Name == "original-resource"); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCssScopesTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCssScopesTest.cs index db19972aa054..6b43265bcc98 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCssScopesTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCssScopesTest.cs @@ -1,379 +1,1139 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.StaticWebAssets.Tasks; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { + + + [TestClass] + public class ApplyAllCssScopesTest + + { - [Fact] + + + [TestMethod] + + public void ApplyAllCssScopes_AppliesScopesToRazorComponentFiles() + + { + + // Arrange + + var taskInstance = new ApplyCssScopes() + + { + + RazorComponents = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.razor"), + + new TaskItem("TestFiles/Pages/Index.razor"), + + }, + + RazorGenerate = Array.Empty(), + + ScopedCss = new[] + + { + + new TaskItem("TestFiles/Pages/Index.razor.css", new Dictionary { ["CssScope"] = "index-scope" }), + + new TaskItem("TestFiles/Pages/Counter.razor.css", new Dictionary { ["CssScope"] = "counter-scope" }), + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.RazorComponentsWithScopes.Should().HaveCount(2); + + taskInstance.RazorComponentsWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/Index.razor" && rcws.GetMetadata("CssScope") == "index-scope"); + + taskInstance.RazorComponentsWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/Counter.razor" && rcws.GetMetadata("CssScope") == "counter-scope"); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyAllCssScopes_AppliesScopesToRazorViewFiles() + + { + + // Arrange + + var taskInstance = new ApplyCssScopes() + + { + + RazorGenerate = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.cshtml"), + + new TaskItem("TestFiles/Pages/Index.cshtml"), + + }, + + RazorComponents = Array.Empty(), + + ScopedCss = new[] + + { + + new TaskItem("TestFiles/Pages/Index.cshtml.css", new Dictionary { ["CssScope"] = "index-scope" }), + + new TaskItem("TestFiles/Pages/Counter.cshtml.css", new Dictionary { ["CssScope"] = "counter-scope" }), + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.RazorGenerateWithScopes.Should().HaveCount(2); + + taskInstance.RazorGenerateWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/Index.cshtml" && rcws.GetMetadata("CssScope") == "index-scope"); + + taskInstance.RazorGenerateWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/Counter.cshtml" && rcws.GetMetadata("CssScope") == "counter-scope"); + + } - [Fact] + + + + + [TestMethod] + + public void DoesNotApplyCssScopes_ToRazorComponentsWithoutAssociatedFiles() + + { + + // Arrange + + var taskInstance = new ApplyCssScopes() + + { + + RazorComponents = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.razor"), + + new TaskItem("TestFiles/Pages/Index.razor"), + + new TaskItem("TestFiles/Pages/FetchData.razor"), + + }, + + RazorGenerate = Array.Empty(), + + ScopedCss = new[] + + { + + new TaskItem("TestFiles/Pages/Index.razor.css", new Dictionary { ["CssScope"] = "index-scope" }), + + new TaskItem("TestFiles/Pages/Counter.razor.css", new Dictionary { ["CssScope"] = "counter-scope" }) + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert - Assert.True(result); + + + Assert.IsTrue(result); + + result.Should().BeTrue(); + + taskInstance.RazorComponentsWithScopes.Should().NotContain(rcws => rcws.ItemSpec == "TestFiles/Pages/Fetchdata.razor"); + + } - [Fact] + + + + + [TestMethod] + + public void DoesNotApplyCssScopes_ToRazorViewsWithoutAssociatedFiles() + + { + + // Arrange + + var taskInstance = new ApplyCssScopes() + + { + + RazorGenerate = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.cshtml"), + + new TaskItem("TestFiles/Pages/Index.cshtml"), + + new TaskItem("TestFiles/Pages/FetchData.cshtml"), + + }, + + RazorComponents = Array.Empty(), + + ScopedCss = new[] + + { + + new TaskItem("TestFiles/Pages/Index.cshtml.css", new Dictionary { ["CssScope"] = "index-scope" }), + + new TaskItem("TestFiles/Pages/Counter.cshtml.css", new Dictionary { ["CssScope"] = "counter-scope" }) + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert - Assert.True(result); + + + Assert.IsTrue(result); + + result.Should().BeTrue(); + + taskInstance.RazorGenerateWithScopes.Should().NotContain(rcws => rcws.ItemSpec == "TestFiles/Pages/Fetchdata.razor"); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyAllCssScopes_FailsWhenTheScopedCss_DoesNotMatchTheRazorComponent() + + { + + // Arrange + + var taskInstance = new ApplyCssScopes + + { + + RazorComponents = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.razor"), + + new TaskItem("TestFiles/Pages/Index.razor"), + + }, + + RazorGenerate = Array.Empty(), + + ScopedCss = new[] + + { + + new TaskItem("TestFiles/Pages/Index.razor.css", new Dictionary { ["CssScope"] = "index-scope" }), + + new TaskItem("TestFiles/Pages/Counter.razor.css", new Dictionary { ["CssScope"] = "counter-scope" }), + + new TaskItem("TestFiles/Pages/Profile.razor.css", new Dictionary { ["CssScope"] = "profile-scope" }), + + }, + + BuildEngine = Mock.Of() + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyAllCssScopes_FailsWhenTheScopedCss_DoesNotMatchTheRazorView() + + { + + // Arrange + + var taskInstance = new ApplyCssScopes + + { + + RazorGenerate = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.cshtml"), + + new TaskItem("TestFiles/Pages/Index.cshtml"), + + }, + + RazorComponents = Array.Empty(), + + ScopedCss = new[] + + { + + new TaskItem("TestFiles/Pages/Index.cshtml.css", new Dictionary { ["CssScope"] = "index-scope" }), + + new TaskItem("TestFiles/Pages/Counter.cshtml.css", new Dictionary { ["CssScope"] = "counter-scope" }), + + new TaskItem("TestFiles/Pages/Profile.cshtml.css", new Dictionary { ["CssScope"] = "profile-scope" }), + + }, + + BuildEngine = Mock.Of() + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + } - [Fact] + + + + + [TestMethod] + + public void ScopedCssCanDefineAssociatedRazorComponentFile() + + { + + // Arrange + + var taskInstance = new ApplyCssScopes() + + { + + RazorComponents = new[] + + { + + new TaskItem("TestFiles/Pages/FetchData.razor") + + }, + + RazorGenerate = Array.Empty(), + + ScopedCss = new[] + + { + + new TaskItem("TestFiles/Pages/Profile.razor.css", new Dictionary + + { + + ["CssScope"] = "fetchdata-scope", + + ["RazorComponent"] = "TestFiles/Pages/FetchData.razor" + + }) + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.RazorComponentsWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/FetchData.razor" && rcws.GetMetadata("CssScope") == "fetchdata-scope"); + + } - [Fact] + + + + + [TestMethod] + + public void ScopedCssCanDefineAssociatedRazorGenerateFile() + + { + + // Arrange + + var taskInstance = new ApplyCssScopes() + + { + + RazorGenerate = new[] + + { + + new TaskItem("TestFiles/Pages/FetchData.cshtml") + + }, + + RazorComponents = Array.Empty(), + + ScopedCss = new[] + + { + + new TaskItem("TestFiles/Pages/Profile.cshtml.css", new Dictionary + + { + + ["CssScope"] = "fetchdata-scope", + + ["View"] = "TestFiles/Pages/FetchData.cshtml" + + }) + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.RazorGenerateWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/FetchData.cshtml" && rcws.GetMetadata("CssScope") == "fetchdata-scope"); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyAllCssScopes_FailsWhenMultipleScopedCssFiles_MatchTheSameRazorComponent() + + { + + // Arrange + + var taskInstance = new ApplyCssScopes + + { + + RazorComponents = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.razor"), + + new TaskItem("TestFiles/Pages/Index.razor"), + + }, + + RazorGenerate = Array.Empty(), + + ScopedCss = new[] + + { + + new TaskItem("TestFiles/Pages/Index.razor.css", new Dictionary { ["CssScope"] = "index-scope" }), + + new TaskItem("TestFiles/Pages/Counter.razor.css", new Dictionary { ["CssScope"] = "counter-scope" }), + + new TaskItem("TestFiles/Pages/Profile.razor.css", new Dictionary + + { + + ["CssScope"] = "conflict-scope", + + ["RazorComponent"] = "TestFiles/Pages/Index.razor" + + }), + + }, + + BuildEngine = Mock.Of() + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyAllCssScopes_FailsWhenMultipleScopedCssFiles_MatchTheSameRazorView() + + { + + // Arrange + + var taskInstance = new ApplyCssScopes + + { + + RazorGenerate = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.cshtml"), + + new TaskItem("TestFiles/Pages/Index.cshtml"), + + }, + + RazorComponents = Array.Empty(), + + ScopedCss = new[] + + { + + new TaskItem("TestFiles/Pages/Index.cshtml.css", new Dictionary { ["CssScope"] = "index-scope" }), + + new TaskItem("TestFiles/Pages/Counter.cshtml.css", new Dictionary { ["CssScope"] = "counter-scope" }), + + new TaskItem("TestFiles/Pages/Profile.cshtml.css", new Dictionary + + { + + ["CssScope"] = "conflict-scope", + + ["View"] = "TestFiles/Pages/Index.cshtml" + + }), + + }, + + BuildEngine = Mock.Of() + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyAllCssScopes_AppliesScopesToRazorComponentAndViewFiles() + + { + + // Arrange + + var taskInstance = new ApplyCssScopes() + + { + + RazorComponents = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.razor"), + + new TaskItem("TestFiles/Pages/Index.razor"), + + }, + + RazorGenerate = new[] + + { + + new TaskItem("TestFiles/Pages/Home.cshtml"), + + new TaskItem("TestFiles/Pages/_Host.cshtml"), + + }, + + ScopedCss = new[] + + { + + new TaskItem("TestFiles/Pages/Home.cshtml.css", new Dictionary { ["CssScope"] = "home-scope" }), + + new TaskItem("TestFiles/Pages/_Host.cshtml.css", new Dictionary { ["CssScope"] = "_host-scope" }), + + new TaskItem("TestFiles/Pages/Index.razor.css", new Dictionary { ["CssScope"] = "index-scope" }), + + new TaskItem("TestFiles/Pages/Counter.razor.css", new Dictionary { ["CssScope"] = "counter-scope" }), + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.RazorComponentsWithScopes.Should().HaveCount(2); + + taskInstance.RazorComponentsWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/Index.razor" && rcws.GetMetadata("CssScope") == "index-scope"); + + taskInstance.RazorComponentsWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/Counter.razor" && rcws.GetMetadata("CssScope") == "counter-scope"); + + + + taskInstance.RazorGenerateWithScopes.Should().HaveCount(2); + + taskInstance.RazorGenerateWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/Home.cshtml" && rcws.GetMetadata("CssScope") == "home-scope"); + + taskInstance.RazorGenerateWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/_Host.cshtml" && rcws.GetMetadata("CssScope") == "_host-scope"); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyAllCssScopes_ScopedCssComponentsDontMatchWithScopedCssViewStylesAndViceversa() + + { + + // Arrange + + var taskInstance = new ApplyCssScopes + + { + + RazorComponents = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.razor"), + + new TaskItem("TestFiles/Pages/Index.razor"), + + }, + + RazorGenerate = new[] + + { + + new TaskItem("TestFiles/Pages/Home.cshtml"), + + new TaskItem("TestFiles/Pages/_Host.cshtml"), + + }, + + ScopedCss = new[] + + { + + new TaskItem("TestFiles/Pages/Home.razor.css", new Dictionary { ["CssScope"] = "home-scope" }), + + new TaskItem("TestFiles/Pages/_Host.razor.css", new Dictionary { ["CssScope"] = "_host-scope" }), + + new TaskItem("TestFiles/Pages/Index.cshtml.css", new Dictionary { ["CssScope"] = "index-scope" }), + + new TaskItem("TestFiles/Pages/Counter.cshtml.css", new Dictionary { ["CssScope"] = "counter-scope" }), + + }, + + BuildEngine = Mock.Of() + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetGroupFilteringTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetGroupFilteringTest.cs index 79a5f1f1a6c4..3f95e7909509 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetGroupFilteringTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetGroupFilteringTest.cs @@ -1,899 +1,2699 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + /// + + /// Unit tests for asset-group filtering logic across UpdatePackageStaticWebAssets, + + /// UpdateExternallyDefinedStaticWebAssets, ComputeReferenceStaticWebAssetItems and + + /// DefineStaticWebAssets.ApplyGroupDefinitions. + + /// + + +[TestClass] + public class AssetGroupFilteringTest : IDisposable + + { + + private readonly string _tempDir; + + private readonly Mock _buildEngine; + + private readonly List _errorMessages; + + private readonly List _logMessages; + + + + public AssetGroupFilteringTest() + + { + + _tempDir = Path.Combine(Path.GetTempPath(), "AssetGroupTest_" + Guid.NewGuid().ToString("N")); + + Directory.CreateDirectory(_tempDir); + + + + _errorMessages = new List(); + + _logMessages = new List(); + + _buildEngine = new Mock(); + + _buildEngine.Setup(e => e.ProjectFileOfTaskNode).Returns(Path.Combine(_tempDir, "test.csproj")); + + _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => _errorMessages.Add(args.Message)); + + _buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => _logMessages.Add(args.Message)); + + } + + + + public void Dispose() + + { + + if (Directory.Exists(_tempDir)) + + { + + try { Directory.Delete(_tempDir, recursive: true); } catch { } + + } + + } - [Fact] + + + + + [TestMethod] + + public void UpdateExternal_AssetWithGroups_MatchingDeclaration_IsIncluded() + + { + + var file = CreateTempFile("ext", "site.css", "body{}"); + + var asset = CreateExternalAssetWithGroups(file, "IdentityUI", "css/site.css", "BootstrapVersion=V5"); + + + + var task = new UpdateExternallyDefinedStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + Endpoints = Array.Empty(), + + StaticWebAssetGroups = new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V5", + + ["SourceId"] = "IdentityUI" + + }) + + } + + }; + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + task.UpdatedAssets.Should().HaveCount(1); + + } - [Fact] + + + + + [TestMethod] + + public void UpdateExternal_AssetWithGroups_NoDeclarations_IsExcluded() + + { + + var file = CreateTempFile("ext", "site.css", "body{}"); + + var asset = CreateExternalAssetWithGroups(file, "IdentityUI", "css/site.css", "BootstrapVersion=V5"); + + + + var task = new UpdateExternallyDefinedStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + Endpoints = Array.Empty(), + + // No StaticWebAssetGroups + + }; + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + task.UpdatedAssets.Should().HaveCount(0, "grouped assets should be excluded when no declarations exist"); + + } - [Fact] + + + + + [TestMethod] + + public void UpdateExternal_MultiGroup_PartialMatch_IsExcluded() + + { + + var file = CreateTempFile("ext", "site.css", "body{}"); + + var asset = CreateExternalAssetWithGroups(file, "IdentityUI", "css/site.css", "BootstrapVersion=V5;DebugAssets=true"); + + + + var task = new UpdateExternallyDefinedStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + Endpoints = Array.Empty(), + + StaticWebAssetGroups = new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V5", + + ["SourceId"] = "IdentityUI" + + }) + + } + + }; + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + task.UpdatedAssets.Should().HaveCount(0, "AND-matching requires all entries satisfied"); + + } - [Fact] + + + + + [TestMethod] + + public void UpdateExternal_CascadingExclusion_RelatedAssetExcludedWithPrimary() + + { + + var primaryFile = CreateTempFile("ext", "css", "site.css", "body{}"); + + var relatedFile = CreateTempFile("ext", "css", "site.css.gz", "compressed"); + + + + var primary = CreateExternalAssetWithGroups(primaryFile, "IdentityUI", "css/site.css", "BootstrapVersion=V5"); + + var related = CreateExternalAsset(relatedFile, "IdentityUI", "css/site.css.gz"); + + related.SetMetadata("AssetRole", "Alternative"); + + related.SetMetadata("AssetTraitName", "Content-Encoding"); + + related.SetMetadata("AssetTraitValue", "gzip"); + + related.SetMetadata("RelatedAsset", Path.GetFullPath(primaryFile)); + + + + var task = new UpdateExternallyDefinedStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { primary, related }, + + Endpoints = Array.Empty(), + + // No declarations → primary excluded → related cascades + + }; + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + task.UpdatedAssets.Should().HaveCount(0, "related asset should cascade-exclude with primary"); + + } - [Fact] + + + + + [TestMethod] + + public void UpdateExternal_EndpointFiltering_ExcludedAssetEndpointsRemoved() + + { + + var includedFile = CreateTempFile("ext", "app.js", "var x;"); + + var excludedFile = CreateTempFile("ext2", "site.css", "body{}"); + + + + var includedAsset = CreateExternalAsset(includedFile, "SomeLib", "app.js"); + + var excludedAsset = CreateExternalAssetWithGroups(excludedFile, "IdentityUI", "css/site.css", "BootstrapVersion=V5"); + + + + var includedEndpoint = new TaskItem("app.js", new Dictionary + + { + + ["AssetFile"] = Path.GetFullPath(includedFile), + + ["Route"] = "app.js", + + ["Selectors"] = "[]", + + ["EndpointProperties"] = "[]", + + ["ResponseHeaders"] = "[]", + + }); + + var excludedEndpoint = new TaskItem("css/site.css", new Dictionary + + { + + ["AssetFile"] = Path.GetFullPath(excludedFile), + + ["Route"] = "css/site.css", + + ["Selectors"] = "[]", + + ["EndpointProperties"] = "[]", + + ["ResponseHeaders"] = "[]", + + }); + + + + var task = new UpdateExternallyDefinedStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { includedAsset, excludedAsset }, + + Endpoints = new[] { includedEndpoint, excludedEndpoint }, + + // No declarations → grouped asset excluded + + }; + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + task.UpdatedEndpoints.Should().HaveCount(1, "only endpoints for included assets should remain"); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeReference_GroupedFrameworkAsset_PreservesSourceType() + + { + + // Two grouped framework assets at the same target path + + var asset1 = CreateReferenceAsset("item1.css", "FrameworkLib", "Framework", "css/site.css", "All", "All", "BootstrapVersion=V4"); + + var asset2 = CreateReferenceAsset("item2.css", "FrameworkLib", "Framework", "css/site.css", "All", "All", "BootstrapVersion=V5"); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = _buildEngine.Object, + + Source = "FrameworkLib", + + Assets = new[] { asset1, asset2 }, + + Patterns = Array.Empty(), + + AssetKind = "Build", + + ProjectMode = "Default", + + UpdateSourceType = true + + }; + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + // Both should be included (distinct groups) + + task.StaticWebAssets.Should().HaveCount(2); + + // And both should STILL be Framework, not overwritten to Project + + foreach (var asset in task.StaticWebAssets) + + { + + asset.GetMetadata("SourceType").Should().Be("Framework", + + "grouped framework assets should preserve SourceType=Framework"); + + } + + } - [Fact] + + + + + [TestMethod] + + public void ComputeReference_NonGroupedFrameworkAsset_PreservesSourceType() + + { + + var asset = CreateReferenceAsset("framework.js", "FrameworkLib", "Framework", "js/framework.js", "All", "All"); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = _buildEngine.Object, + + Source = "FrameworkLib", + + Assets = new[] { asset }, + + Patterns = Array.Empty(), + + AssetKind = "Build", + + ProjectMode = "Default", + + UpdateSourceType = true + + }; + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + task.StaticWebAssets.Should().HaveCount(1); + + task.StaticWebAssets[0].GetMetadata("SourceType").Should().Be("Framework", + + "non-grouped framework assets should also preserve SourceType=Framework"); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_SameOrderSameSourceId_ProducesError() + + { + + var file = CreateTempFile("wwwroot", "V5", "css", "site.css", "body{}"); + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] { CreateCandidateAssetItem(file) }, + + new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V5", + + ["Order"] = "0", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "V5/**", + + ["RelativePathPattern"] = "V5/**" + + }), + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V4", + + ["Order"] = "0", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "V4/**", + + ["RelativePathPattern"] = "V4/**" + + }) + + }, + + sourceId: "MyProject", + + basePath: "_content/myproject"); + + + + var result = task.Execute(); + + + + result.Should().BeFalse("same Order + same SourceId should produce an error"); + + _errorMessages.Should().ContainMatch("*same Order*"); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_DifferentOrderSameSourceId_NoError() + + { + + var file = CreateTempFile("wwwroot", "V5", "css", "site.css", "body{}"); + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] { CreateCandidateAssetItem(file) }, + + new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V5", + + ["Order"] = "0", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "V5/**", + + ["RelativePathPattern"] = "V5/**" + + }), + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V4", + + ["Order"] = "1", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "V4/**", + + ["RelativePathPattern"] = "V4/**" + + }) + + }, + + sourceId: "MyProject", + + basePath: "_content/myproject"); + + + + var result = task.Execute(); + + + + result.Should().BeTrue("different Orders should not trigger the same-Order-same-SourceId validation"); + + _errorMessages.Should().BeEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_MissingValue_ProducesError() + + { + + var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body{}"); + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] { CreateCandidateAssetItem(file) }, + + new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + // No "Value" + + ["Order"] = "0", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "V4/**", + + }) + + }); + + + + var result = task.Execute(); + + + + result.Should().BeFalse(); + + _errorMessages.Should().ContainSingle(m => m.Contains("missing required metadata 'Value'")); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_MissingSourceId_ProducesError() + + { + + var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body{}"); + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] { CreateCandidateAssetItem(file) }, + + new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V4", + + ["Order"] = "0", + + // No "SourceId" + + ["IncludePattern"] = "V4/**", + + }) + + }); + + + + var result = task.Execute(); + + + + result.Should().BeFalse(); + + _errorMessages.Should().ContainSingle(m => m.Contains("missing required metadata 'SourceId'")); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_InvalidOrder_ProducesError() + + { + + var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body{}"); + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] { CreateCandidateAssetItem(file) }, + + new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V4", + + ["Order"] = "not-a-number", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "V4/**", + + }) + + }); + + + + var result = task.Execute(); + + + + result.Should().BeFalse(); + + _errorMessages.Should().ContainSingle(m => m.Contains("invalid or missing 'Order'")); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_MissingIncludePattern_ProducesError() + + { + + var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body{}"); + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] { CreateCandidateAssetItem(file) }, + + new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V4", + + ["Order"] = "0", + + ["SourceId"] = "IdentityUI", + + // No "IncludePattern" + + }) + + }); + + + + var result = task.Execute(); + + + + result.Should().BeFalse(); + + _errorMessages.Should().ContainSingle(m => m.Contains("missing required metadata 'IncludePattern'")); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_SourceIdMismatch_DefinitionsIgnored() + + { + + var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body-v4{}"); + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] { CreateCandidateAssetItem(file) }, + + new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V4", + + ["Order"] = "0", + + ["SourceId"] = "OtherLib", + + ["IncludePattern"] = "V4/**", + + ["RelativePathPattern"] = "V4/**", + + ["ContentRootSuffix"] = "V4" + + }) + + }); + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + _errorMessages.Should().BeEmpty(); + + task.Assets.Should().HaveCount(1); + + + + var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); + + asset.AssetGroups.Should().BeNullOrEmpty("definitions with mismatched SourceId should not apply"); + + asset.RelativePath.Should().Contain("V4", "RelativePath should not be transformed"); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_MultipleContentRootSuffix_Compose() + + { + + var file = CreateTempFile("wwwroot", "shared", "site.css", "body{}"); + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] { CreateCandidateAssetItem(file) }, + + new ITaskItem[] + + { + + new TaskItem("GroupA", new Dictionary + + { + + ["Value"] = "V1", + + ["Order"] = "0", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "**", + + ["ContentRootSuffix"] = "suffixA" + + }), + + new TaskItem("GroupB", new Dictionary + + { + + ["Value"] = "V2", + + ["Order"] = "1", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "**", + + ["ContentRootSuffix"] = "suffixB" + + }) + + }); + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); + + asset.ContentRoot.Should().Contain("suffixA"); + + asset.ContentRoot.Should().Contain("suffixB"); + + // Suffixes compose in order: suffixA/suffixB + + asset.ContentRoot.Should().Contain(Path.Combine("suffixA", "suffixB")); + + } - [Theory] - [InlineData(new[] { "BootstrapVersion=V4", "BootstrapVersion=V5" }, true)] - [InlineData(new[] { "BootstrapVersion=V4", "" }, false)] - [InlineData(new[] { "BootstrapVersion=V5", "BootstrapVersion=V5" }, false)] + + + + + [TestMethod] + + + [DataRow(new[] { "BootstrapVersion=V4", "BootstrapVersion=V5" }, true)] + + + [DataRow(new[] { "BootstrapVersion=V4", "" }, false)] + + + [DataRow(new[] { "BootstrapVersion=V5", "BootstrapVersion=V5" }, false)] + + public void AllAssetsHaveDistinctGroups_ReturnsExpectedResult(string[] groups, bool expected) + + { + + var assets = groups.Select((g, i) => CreateStaticWebAsset($"{(char)('a' + i)}.css", g)).ToList(); + + var groupSet = new HashSet(StringComparer.Ordinal); + + StaticWebAsset.AllAssetsHaveDistinctGroups(assets, groupSet).Should().Be(expected); + + } + + + + private string CreateTempFile(params string[] pathParts) + + { + + var content = pathParts[^1]; + + var segments = pathParts[..^1]; + + + + var dir = Path.Combine(new[] { _tempDir }.Concat(segments[..^1]).ToArray()); + + Directory.CreateDirectory(dir); + + var filePath = Path.Combine(dir, segments[^1]); + + File.WriteAllText(filePath, content); + + return filePath; + + } + + + + private ITaskItem CreateExternalAsset(string filePath, string sourceId, string relativePath) + + { + + var contentRoot = Path.GetDirectoryName(filePath) + Path.DirectorySeparatorChar; + + return new TaskItem(filePath, new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = sourceId, + + ["ContentRoot"] = contentRoot, + + ["BasePath"] = "", + + ["RelativePath"] = relativePath, + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["CopyToOutputDirectory"] = "PreserveNewest", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["OriginalItemSpec"] = filePath, + + }); + + } + + + + private ITaskItem CreateExternalAssetWithGroups(string filePath, string sourceId, string relativePath, string assetGroups) + + { + + var item = CreateExternalAsset(filePath, sourceId, relativePath); + + item.SetMetadata("AssetGroups", assetGroups); + + return item; + + } + + + + private static ITaskItem CreateReferenceAsset( + + string itemSpec, + + string sourceId, + + string sourceType, + + string relativePath, + + string assetKind, + + string assetMode, + + string assetGroups = null) + + { + + var result = new StaticWebAsset() + + { + + Identity = Path.GetFullPath(itemSpec), + + SourceId = sourceId, + + SourceType = sourceType, + + ContentRoot = Directory.GetCurrentDirectory(), + + BasePath = "base", + + RelativePath = relativePath, + + AssetKind = assetKind, + + AssetMode = assetMode, + + AssetRole = "Primary", + + RelatedAsset = "", + + AssetTraitName = "", + + AssetTraitValue = "", + + CopyToOutputDirectory = "", + + CopyToPublishDirectory = "", + + OriginalItemSpec = itemSpec, + + Integrity = "integrity", + + Fingerprint = "fingerprint", + + FileLength = 10, + + LastWriteTime = DateTime.UtcNow, + + }; + + + + if (!string.IsNullOrEmpty(assetGroups)) + + { + + result.AssetGroups = assetGroups; + + } + + + + result.ApplyDefaults(); + + result.Normalize(); + + + + return result.ToTaskItem(); + + } + + + + private static StaticWebAsset CreateStaticWebAsset(string identity, string assetGroups) + + { + + var asset = new StaticWebAsset + + { + + Identity = Path.GetFullPath(identity), + + SourceId = "TestLib", + + SourceType = "Package", + + ContentRoot = Directory.GetCurrentDirectory(), + + BasePath = "base", + + RelativePath = identity, + + AssetKind = "All", + + AssetMode = "All", + + AssetRole = "Primary", + + RelatedAsset = "", + + AssetTraitName = "", + + AssetTraitValue = "", + + CopyToOutputDirectory = "", + + CopyToPublishDirectory = "", + + OriginalItemSpec = identity, + + Integrity = "integrity", + + Fingerprint = "fingerprint", + + FileLength = 10, + + LastWriteTime = DateTime.UtcNow, + + AssetGroups = assetGroups, + + }; + + + + asset.ApplyDefaults(); + + asset.Normalize(); + + return asset; + + } + + + + private ITaskItem CreateCandidateAssetItem(string file, string integrity = "integrity", string fingerprint = "fingerprint", string fileLength = "10") + + { + + return new TaskItem(file, new Dictionary + + { + + ["RelativePath"] = "", + + ["TargetPath"] = "", + + ["Link"] = "", + + ["CopyToOutputDirectory"] = "", + + ["CopyToPublishDirectory"] = "", + + ["Integrity"] = integrity, + + ["Fingerprint"] = fingerprint, + + ["LastWriteTime"] = DateTime.UtcNow.ToString(StaticWebAsset.DateTimeAssetFormat), + + ["FileLength"] = fileLength, + + }); + + } + + + + private DefineStaticWebAssets CreateDefineStaticWebAssetsTask( + + ITaskItem[] candidates, + + ITaskItem[] groupDefs, + + string sourceId = "IdentityUI", + + string contentRoot = null, + + string basePath = "Identity") + + { + + return new DefineStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + TestResolveFileDetails = (_, _) => (null, 10, new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero)), + + CandidateAssets = candidates, + + RelativePathPattern = "wwwroot/**", + + SourceType = "Discovered", + + SourceId = sourceId, + + ContentRoot = contentRoot ?? Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar, + + BasePath = basePath, + + StaticWebAssetGroupDefinitions = groupDefs + + }; + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_ContentRootSuffix_AdjustsContentRoot() + + { + + var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body-v4{}"); + + var wwwrootPath = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] { CreateCandidateAssetItem(file) }, + + new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V4", + + ["Order"] = "0", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "V4/**", + + ["RelativePathPattern"] = "V4/**", + + ["ContentRootSuffix"] = "V4" + + }) + + }); + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + _errorMessages.Should().BeEmpty(); + + task.Assets.Should().HaveCount(1); + + + + var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); + + asset.RelativePath.Should().Be("css/site.css", "RelativePathPattern stripped the V4/ prefix"); + + asset.ContentRoot.Should().Be(wwwrootPath + "V4" + Path.DirectorySeparatorChar, + + "ContentRootSuffix appended V4 to wwwroot path"); + + asset.AssetGroups.Should().Contain("BootstrapVersion=V4"); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_PatternOnly_NoAutoFileOnlyToken() + + { + + var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body-v4{}"); + + var wwwrootPath = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] { CreateCandidateAssetItem(file) }, + + new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V4", + + ["Order"] = "0", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "V4/**", + + ["RelativePathPattern"] = "V4/**" + + // No RelativePathPrefix, no ContentRootSuffix + + }) + + }); + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + _errorMessages.Should().BeEmpty(); + + task.Assets.Should().HaveCount(1); + + + + var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); + + asset.RelativePath.Should().Be("css/site.css", + + "RelativePathPattern stripped V4/ prefix — no auto-injection of file-only ~ token"); + + asset.RelativePath.Should().NotContain("~", "SDK must not auto-inject file-only tokens"); + + asset.ContentRoot.Should().Be(wwwrootPath, "ContentRoot unchanged when no ContentRootSuffix"); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_RelativePathPrefix_FileOnlyToken() + + { + + var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body-v4{}"); + + var wwwrootPath = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] { CreateCandidateAssetItem(file) }, + + new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V4", + + ["Order"] = "0", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "V4/**", + + ["RelativePathPattern"] = "V4/**", + + ["RelativePathPrefix"] = "#[{BootstrapVersion}/]~" + + }) + + }); + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + _errorMessages.Should().BeEmpty(); + + task.Assets.Should().HaveCount(1); + + + + var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); + + asset.RelativePath.Should().Be("#[{BootstrapVersion}/]~css/site.css", + + "pattern strips V4/, prefix prepends file-only token expression"); + + asset.ContentRoot.Should().Be(wwwrootPath, "ContentRoot unchanged when no ContentRootSuffix"); + + asset.AssetGroups.Should().Contain("BootstrapVersion=V4"); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_RelativePathPrefix_LiteralPrepend() + + { + + var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body-v4{}"); + + var wwwrootPath = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] { CreateCandidateAssetItem(file) }, + + new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V4", + + ["Order"] = "0", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "V4/**", + + ["RelativePathPattern"] = "V4/**", + + ["RelativePathPrefix"] = "shared/" + + }) + + }); + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + _errorMessages.Should().BeEmpty(); + + task.Assets.Should().HaveCount(1); + + + + var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); + + asset.RelativePath.Should().Be("shared/css/site.css", + + "pattern strips V4/, prefix prepends literal 'shared/'"); + + asset.ContentRoot.Should().Be(wwwrootPath, "ContentRoot unchanged when no ContentRootSuffix"); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_AllThreeOrthogonal() + + { + + var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body-v4{}"); + + var wwwrootPath = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] { CreateCandidateAssetItem(file) }, + + new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V4", + + ["Order"] = "0", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "V4/**", + + ["RelativePathPattern"] = "V4/**", + + ["RelativePathPrefix"] = "shared/", + + ["ContentRootSuffix"] = "V4" + + }) + + }); + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + _errorMessages.Should().BeEmpty(); + + task.Assets.Should().HaveCount(1); + + + + var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); + + asset.RelativePath.Should().Be("shared/css/site.css", + + "pattern strips V4/, prefix prepends 'shared/'"); + + asset.ContentRoot.Should().Be(wwwrootPath + "V4" + Path.DirectorySeparatorChar, + + "ContentRootSuffix applied independently"); + + asset.AssetGroups.Should().Contain("BootstrapVersion=V4"); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_RelativePathPrefix_WithoutPattern_PrependsToOriginalPath() + + { + + var file = CreateTempFile("wwwroot", "css", "site.css", "body{}"); + + var wwwrootPath = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] { CreateCandidateAssetItem(file) }, + + new ITaskItem[] + + { + + new TaskItem("Theme", new Dictionary + + { + + ["Value"] = "Default", + + ["Order"] = "0", + + ["SourceId"] = "MyLib", + + ["IncludePattern"] = "**", + + ["RelativePathPrefix"] = "lib/" + + // No RelativePathPattern — no stripping, just prepend + + }) + + }, + + sourceId: "MyLib", + + basePath: "mylib"); + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + _errorMessages.Should().BeEmpty(); + + task.Assets.Should().HaveCount(1); + + + + var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); + + asset.RelativePath.Should().Be("lib/css/site.css", + + "no pattern stripping, but prefix 'lib/' prepended to original path"); + + asset.AssetGroups.Should().Contain("Theme=Default"); + + } - [Fact] + + + + + [TestMethod] + + public void ApplyGroupDefinitions_ContentRootSuffix_MultipleGroups_EachGetsOwnContentRoot() + + { + + var fileV4 = CreateTempFile("wwwroot", "V4", "css", "site.css", "body-v4{}"); + + var fileV5 = CreateTempFile("wwwroot", "V5", "css", "site.css", "body-v5{}"); + + var wwwrootPath = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; + + + + var task = CreateDefineStaticWebAssetsTask( + + new[] + + { + + CreateCandidateAssetItem(fileV4, integrity: "integrity-v4", fingerprint: "fingerprint-v4"), + + CreateCandidateAssetItem(fileV5, integrity: "integrity-v5", fingerprint: "fingerprint-v5", fileLength: "11") + + }, + + new ITaskItem[] + + { + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V5", + + ["Order"] = "0", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "V5/**", + + ["RelativePathPattern"] = "V5/**", + + ["ContentRootSuffix"] = "V5" + + }), + + new TaskItem("BootstrapVersion", new Dictionary + + { + + ["Value"] = "V4", + + ["Order"] = "1", + + ["SourceId"] = "IdentityUI", + + ["IncludePattern"] = "V4/**", + + ["RelativePathPattern"] = "V4/**", + + ["ContentRootSuffix"] = "V4" + + }) + + }); + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + _errorMessages.Should().BeEmpty(); + + task.Assets.Should().HaveCount(2); + + + + var assets = task.Assets.Select(a => StaticWebAsset.FromTaskItem(a)).ToList(); + + var v4Asset = assets.Single(a => a.AssetGroups.Contains("BootstrapVersion=V4")); + + var v5Asset = assets.Single(a => a.AssetGroups.Contains("BootstrapVersion=V5")); + + + + v4Asset.ContentRoot.Should().Be(wwwrootPath + "V4" + Path.DirectorySeparatorChar, + + "V4 asset gets its own ContentRoot with V4 suffix"); + + v4Asset.RelativePath.Should().Be("css/site.css"); + + + + v5Asset.ContentRoot.Should().Be(wwwrootPath + "V5" + Path.DirectorySeparatorChar, + + "V5 asset gets its own ContentRoot with V5 suffix"); + + v5Asset.RelativePath.Should().Be("css/site.css"); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetToCompressTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetToCompressTest.cs index c32b7b8dd3f7..60f5a6d6a4b1 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetToCompressTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetToCompressTest.cs @@ -1,173 +1,521 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks.Utils; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + + + + +[TestClass] + public class AssetToCompressTest : IDisposable + + { + + private readonly string _testDirectory; + + private readonly string _testFilePath; + + private readonly Mock _buildEngine; + + private readonly TaskLoggingHelper _log; + + private readonly List _errorMessages; + + private readonly List _logMessages; + + + + public AssetToCompressTest() + + { + + _testDirectory = Path.Combine(SdkTestContext.Current.TestExecutionDirectory, nameof(AssetToCompressTest), Guid.NewGuid().ToString("N")); + + Directory.CreateDirectory(_testDirectory); + + _testFilePath = Path.Combine(_testDirectory, "test-asset.js"); + + File.WriteAllText(_testFilePath, "// test content"); + + + + _errorMessages = new List(); + + _logMessages = new List(); + + _buildEngine = new Mock(); + + _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => _errorMessages.Add(args.Message)); + + _buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => _logMessages.Add(args.Message)); + + + + var dummyTask = new Mock(); + + dummyTask.Setup(t => t.BuildEngine).Returns(_buildEngine.Object); + + _log = new TaskLoggingHelper(dummyTask.Object); + + } + + + + public void Dispose() + + { + + if (Directory.Exists(_testDirectory)) + + { + + Directory.Delete(_testDirectory, recursive: true); + + } + + } - [Fact] + + + + + [TestMethod] + + public void TryFindInputFilePath_UsesRelatedAsset_WhenFileExists() + + { + + // Arrange + + var assetToCompress = new TaskItem("test.js.gz"); + + assetToCompress.SetMetadata("RelatedAsset", _testFilePath); + + assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", "some-other-path.js"); + + + + // Act + + var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); + + + + // Assert + + result.Should().BeTrue(); + + fullPath.Should().Be(_testFilePath); + + _errorMessages.Should().BeEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void TryFindInputFilePath_FallsBackToRelatedAssetOriginalItemSpec_WhenRelatedAssetDoesNotExist() + + { + + // Arrange + + var assetToCompress = new TaskItem("test.js.gz"); + + assetToCompress.SetMetadata("RelatedAsset", Path.Combine(_testDirectory, "non-existent.js")); + + assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", _testFilePath); + + + + // Act + + var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); + + + + // Assert + + result.Should().BeTrue(); + + fullPath.Should().Be(_testFilePath); + + _errorMessages.Should().BeEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void TryFindInputFilePath_ReturnsError_WhenNeitherPathExists() + + { + + // Arrange + + var nonExistentPath1 = Path.Combine(_testDirectory, "non-existent1.js"); + + var nonExistentPath2 = Path.Combine(_testDirectory, "non-existent2.js"); + + var assetToCompress = new TaskItem("test.js.gz"); + + assetToCompress.SetMetadata("RelatedAsset", nonExistentPath1); + + assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", nonExistentPath2); + + + + // Act + + var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); + + + + // Assert + + result.Should().BeFalse(); + + fullPath.Should().BeNull(); + + _errorMessages.Should().ContainSingle(); + + _errorMessages[0].Should().Contain("can not be found"); + + _errorMessages[0].Should().Contain(nonExistentPath1); + + _errorMessages[0].Should().Contain(nonExistentPath2); + + } - [Fact] + + + + + [TestMethod] + + public void TryFindInputFilePath_PrefersRelatedAsset_OverRelatedAssetOriginalItemSpec_WhenBothExist() + + { + + // Arrange - create two files to simulate the scenario where both metadata values point to existing files + + var relatedAssetPath = Path.Combine(_testDirectory, "correct-asset.js"); + + var originalItemSpecPath = Path.Combine(_testDirectory, "project-file.esproj"); + + File.WriteAllText(relatedAssetPath, "// correct JavaScript content"); + + File.WriteAllText(originalItemSpecPath, ""); + + + + var assetToCompress = new TaskItem("test.js.gz"); + + assetToCompress.SetMetadata("RelatedAsset", relatedAssetPath); + + assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", originalItemSpecPath); + + + + // Act + + var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); + + + + // Assert - should prefer RelatedAsset (the actual JavaScript file) over RelatedAssetOriginalItemSpec (the esproj file) + + result.Should().BeTrue(); + + fullPath.Should().Be(relatedAssetPath); + + fullPath.Should().NotBe(originalItemSpecPath); + + _errorMessages.Should().BeEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void TryFindInputFilePath_HandlesEmptyRelatedAsset_AndUsesRelatedAssetOriginalItemSpec() + + { + + // Arrange + + var assetToCompress = new TaskItem("test.js.gz"); + + assetToCompress.SetMetadata("RelatedAsset", ""); + + assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", _testFilePath); + + + + // Act + + var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); + + + + // Assert + + result.Should().BeTrue(); + + fullPath.Should().Be(_testFilePath); + + _errorMessages.Should().BeEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void TryFindInputFilePath_HandlesEsprojScenario_WhereOriginalItemSpecPointsToProjectFile() + + { + + // Arrange - simulate the esproj bug scenario where RelatedAssetOriginalItemSpec + + // incorrectly points to the .esproj project file instead of the actual JS asset + + var esprojFile = Path.Combine(_testDirectory, "MyProject.esproj"); + + var actualJsFile = Path.Combine(_testDirectory, "dist", "app.min.js"); + + + + Directory.CreateDirectory(Path.GetDirectoryName(actualJsFile)); + + File.WriteAllText(esprojFile, ""); + + File.WriteAllText(actualJsFile, "// actual JavaScript content"); + + + + var assetToCompress = new TaskItem(Path.Combine(_testDirectory, "compressed", "app.min.js.gz")); + + // RelatedAsset should contain the correct path to the actual JS file + + assetToCompress.SetMetadata("RelatedAsset", actualJsFile); + + // RelatedAssetOriginalItemSpec may incorrectly point to .esproj due to esproj SDK bug + + assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", esprojFile); + + + + // Act + + var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); + + + + // Assert - should use RelatedAsset (correct JS file) not RelatedAssetOriginalItemSpec (esproj file) + + result.Should().BeTrue(); + + fullPath.Should().Be(actualJsFile); + + fullPath.Should().NotBe(esprojFile); + + _errorMessages.Should().BeEmpty(); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeCssScopesTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeCssScopesTests.cs index 98dd095c3dbb..bf3726bf10a5 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeCssScopesTests.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeCssScopesTests.cs @@ -1,133 +1,401 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Text.RegularExpressions; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Utilities; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + + namespace Microsoft.NET.Sdk.Razor.Test + + { + + + [TestClass] + public class ComputeCssScopesTests + + { - [Fact] + + + [TestMethod] + + public void ComputesScopes_ComputesUniqueScopes_ForCssFiles() + + { + + // Arrange + + var taskInstance = new ComputeCssScope() + + { + + ScopedCssInput = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.razor.css"), + + new TaskItem("TestFiles/Pages/Index.razor.css"), + + new TaskItem("TestFiles/Pages/Profile.razor.css"), + + }, + + TargetName = "Test" + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().Be(true); + + taskInstance.ScopedCss.Select(s => s.GetMetadata("CssScope")).Should().OnlyContain(item => + + !string.IsNullOrEmpty(item) && new Regex("b-[a-z0-9]+").IsMatch(item)); + + + + taskInstance.ScopedCss.Select(s => s.GetMetadata("CssScope")).Should().HaveCount(3).And.OnlyHaveUniqueItems(); + + } - [Fact] + + + + + [TestMethod] + + public void ComputesScopes_ScopeVariesByTargetName() + + { + + // Arrange + + var taskInstance = new ComputeCssScope() + + { + + ScopedCssInput = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.razor.css"), + + new TaskItem("TestFiles/Pages/Index.razor.css"), + + new TaskItem("TestFiles/Pages/Profile.razor.css"), + + }, + + TargetName = "Test" + + }; + + + + // Act + + taskInstance.Execute(); + + var existing = taskInstance.ScopedCss.Select(s => s.GetMetadata("CssScope")).ToArray(); + + + + taskInstance.TargetName = "AnotherLibrary"; + + var result = taskInstance.Execute(); + + + + // Assert + + taskInstance.ScopedCss.Should().OnlyContain(newScoped => !existing.Contains(newScoped.GetMetadata("ScopedCss"))); + + } - [Fact] + + + + + [TestMethod] + + public void ComputesScopes_IsDeterministic() + + { + + // Arrange + + var taskInstance = new ComputeCssScope() + + { + + ScopedCssInput = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.razor.css"), + + new TaskItem("TestFiles/Pages/Index.razor.css"), + + new TaskItem("TestFiles/Pages/Profile.razor.css"), + + }, + + TargetName = "Test" + + }; + + + + // Act + + taskInstance.Execute(); + + var existing = taskInstance.ScopedCss.Select(s => s.GetMetadata("CssScope")).OrderBy(id => id).ToArray(); + + + + var result = taskInstance.Execute(); + + + + // Assert + + var computed = taskInstance.ScopedCss.Select(newScoped => newScoped.GetMetadata("CssScope")).OrderBy(id => id).ToArray(); + + computed.Should().Equal(existing); + + } - [Fact] + + + + + [TestMethod] + + public void ComputesScopes_VariesByPath() + + { + + // Arrange + + var taskInstance = new ComputeCssScope() + + { + + ScopedCssInput = new[] + + { + + new TaskItem("TestFiles/Pages/Index.razor.css"), + + new TaskItem("TestFiles/Index.razor.css"), + + }, + + TargetName = "Test" + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.ScopedCss.Should().HaveCount(2); + + taskInstance.ScopedCss[0].GetMetadata("CssScope").Should().NotBe(taskInstance.ScopedCss[1].GetMetadata("CssScope")); + + } - [Fact] + + + + + [TestMethod] + + public void ComputesScopes_PreservesUserDefinedScopes() + + { + + // Arrange + + var taskInstance = new ComputeCssScope() + + { + + ScopedCssInput = new[] + + { + + new TaskItem("TestFiles/Pages/Index.razor.css", new Dictionary{ ["CssScope"] = "b-predefined" }), }, + + TargetName = "Test" + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.ScopedCss.Should().ContainSingle(scopedCss => scopedCss.GetMetadata("CssScope") == "b-predefined"); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsMultiThreadingTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsMultiThreadingTest.cs index dcb3f60196f7..8f6edab5bf33 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsMultiThreadingTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsMultiThreadingTest.cs @@ -1,112 +1,338 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + + + + +[TestClass] + public class ComputeEndpointsForReferenceStaticWebAssetsMultiThreadingTest + + { - [Fact] + + + [TestMethod] + + public void ProducesCorrectEndpointsWhenTaskEnvironmentProjectDirectoryDiffersFromProcessCurrentDirectory() + + { + + var testRoot = Path.Combine(AppContext.BaseDirectory, nameof(ComputeEndpointsForReferenceStaticWebAssetsMultiThreadingTest), Guid.NewGuid().ToString("N")); + + var projectDir = Path.Combine(testRoot, "project"); + + var spawnDir = Path.Combine(testRoot, "decoy", "spawn"); + + Directory.CreateDirectory(projectDir); + + Directory.CreateDirectory(spawnDir); + + + + const string relativeContentRoot = "wwwroot"; + + + + var projectAbsoluteContentRoot = Path.GetFullPath(Path.Combine(projectDir, relativeContentRoot)); + + var spawnAbsoluteContentRoot = Path.GetFullPath(Path.Combine(spawnDir, relativeContentRoot)); + + projectAbsoluteContentRoot.Should().NotBe(spawnAbsoluteContentRoot, + + "the test setup must place project and decoy in different parents so a relative path resolves differently against each"); + + + + var assetIdentity = Path.Combine(projectAbsoluteContentRoot, "candidate.js"); + + + + var originalCurrentDirectory = Directory.GetCurrentDirectory(); + + try + + { + + Directory.SetCurrentDirectory(spawnDir); + + + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeEndpointsForReferenceStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir), + + Assets = new[] { CreateAssetItemWithRelativeContentRoot(assetIdentity, relativeContentRoot, basePath: "base") }, + + CandidateEndpoints = new[] { CreateCandidateEndpoint(route: "candidate.js", assetFile: assetIdentity) } + + }; + + + + var result = task.Execute(); + + + + result.Should().BeTrue("the task must run to completion when TaskEnvironment.ProjectDirectory differs from the process CWD"); + + errorMessages.Should().BeEmpty(); + + task.Endpoints.Should().ContainSingle(); + + + + // The route is re-rooted under the asset's BasePath — proves the endpoint + + // matched the asset in the dictionary (assets[endpoint.AssetFile] succeeded) + + // and the BasePath-application branch ran. + + task.Endpoints[0].ItemSpec.Should().Be("base/candidate.js"); + + + + // AssetFile is passed through unchanged from the input endpoint. + + task.Endpoints[0].GetMetadata("AssetFile").Should().Be(assetIdentity); + + } + + finally + + { + + Directory.SetCurrentDirectory(originalCurrentDirectory); + + if (Directory.Exists(testRoot)) + + { + + Directory.Delete(testRoot, recursive: true); + + } + + } + + } + + + + private static ITaskItem CreateAssetItemWithRelativeContentRoot(string identity, string relativeContentRoot, string basePath) + + { + + var asset = new StaticWebAsset + + { + + Identity = identity, + + SourceId = "MyPackage", + + SourceType = StaticWebAsset.SourceTypes.Discovered, + + ContentRoot = relativeContentRoot, + + BasePath = basePath, + + RelativePath = Path.GetFileName(identity), + + AssetKind = StaticWebAsset.AssetKinds.All, + + AssetMode = StaticWebAsset.AssetModes.All, + + AssetRole = StaticWebAsset.AssetRoles.Primary, + + RelatedAsset = "", + + AssetTraitName = "", + + AssetTraitValue = "", + + CopyToOutputDirectory = "", + + CopyToPublishDirectory = "", + + OriginalItemSpec = identity, + + Integrity = "integrity", + + Fingerprint = "fingerprint", + + LastWriteTime = DateTime.UtcNow, + + FileLength = 10, + + }; + + + + return asset.ToTaskItem(); + + } + + + + private static ITaskItem CreateCandidateEndpoint(string route, string assetFile) + + { + + return new StaticWebAssetEndpoint + + { + + Route = route, + + AssetFile = assetFile, + + EndpointProperties = [], + + }.ToTaskItem(); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsTest.cs index e76a6966a269..f7abdf300736 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsTest.cs @@ -1,220 +1,662 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + + + + +[TestClass] + public class ComputeEndpointsForReferenceStaticWebAssetsTest + + { - [Fact] + + + [TestMethod] + + public void IncludesEndpointsForAssetsFromCurrentProject() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeEndpointsForReferenceStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + Assets = [CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All")], + + CandidateEndpoints = [CreateCandidateEndpoint("candidate.js", Path.Combine("wwwroot", "candidate.js"))] + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.Endpoints.Should().ContainSingle(); + + task.Endpoints[0].ItemSpec.Should().Be("base/candidate.js"); + + task.Endpoints[0].GetMetadata("AssetFile").Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + } - [Fact] + + + + + [TestMethod] + + public void UpdatesLabelAsNecessary_ForChosenEndpoints() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeEndpointsForReferenceStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + Assets = [CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All")], + + CandidateEndpoints = [CreateCandidateEndpoint("candidate.js", Path.Combine("wwwroot", "candidate.js"), addLabel: true)] + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.Endpoints.Should().ContainSingle(); + + task.Endpoints[0].ItemSpec.Should().Be("base/candidate.js"); + + task.Endpoints[0].GetMetadata("AssetFile").Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + var properties = StaticWebAssetEndpointProperty.FromMetadataValue(task.Endpoints[0].GetMetadata("EndpointProperties")); + + properties.Should().ContainSingle(); + + properties[0].Name.Should().Be("label"); + + properties[0].Value.Should().Be("base/label-value"); + + } - [Fact] + + + + + [TestMethod] + + public void FiltersOutEndpointsForAssetsNotFound() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeEndpointsForReferenceStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + Assets = [CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All")], + + CandidateEndpoints = [ + + CreateCandidateEndpoint("candidate.js", Path.Combine("wwwroot", "candidate.js")), + + CreateCandidateEndpoint("package.js", Path.Combine("..", "_content", "package-id", "package.js")) + + ] + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.Endpoints.Where(e => e != null).Should().ContainSingle(); + + task.Endpoints[0].ItemSpec.Should().Be("base/candidate.js"); + + task.Endpoints[0].GetMetadata("AssetFile").Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + } - [Fact] + + + + + [TestMethod] + + public void AppliesBasePathWhenRouteStartsWithBasePathButNotAsPathSegment() + + { + + // This test verifies the fix for a bug where routes like "App1.styles.css" + + // were incorrectly skipped because they start with the BasePath "App1". + + // The correct behavior is that the base path should only be considered + + // "already applied" if the route starts with "App1/" (as a path segment), + + // not just any string starting with "App1". + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + // Create an asset with BasePath "App1" and a route "App1.styles.css" + + // The route starts with "App1" but NOT "App1/", so the base path should still be applied + + var task = new ComputeEndpointsForReferenceStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + Assets = [CreateCandidate( + + Path.Combine("obj", "scopedcss", "bundle", "App1.styles.css"), + + "App1", + + "Project", + + "App1.styles.css", + + "All", + + "CurrentProject", + + basePath: "App1")], + + CandidateEndpoints = [CreateCandidateEndpoint("App1.styles.css", Path.Combine("obj", "scopedcss", "bundle", "App1.styles.css"))] + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.Endpoints.Should().ContainSingle(); + + // The route should be "App1/App1.styles.css", not just "App1.styles.css" + + task.Endpoints[0].ItemSpec.Should().Be("App1/App1.styles.css"); + + } - [Fact] + + + + + [TestMethod] + + public void SkipsBasePathApplicationWhenRouteAlreadyHasBasePathAsPathSegment() + + { + + // This test verifies that routes already starting with "BasePath/" are correctly skipped + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeEndpointsForReferenceStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + Assets = [CreateCandidate( + + Path.Combine("wwwroot", "css", "app.css"), + + "App1", + + "Discovered", + + "css/app.css", + + "All", + + "All", + + basePath: "App1")], + + // Route already has the base path as a path segment + + CandidateEndpoints = [CreateCandidateEndpoint("App1/css/app.css", Path.Combine("wwwroot", "css", "app.css"))] + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.Endpoints.Should().ContainSingle(); + + // Should remain "App1/css/app.css", not become "App1/App1/css/app.css" + + task.Endpoints[0].ItemSpec.Should().Be("App1/css/app.css"); + + } + + + + private static ITaskItem CreateCandidate( + + string itemSpec, + + string sourceId, + + string sourceType, + + string relativePath, + + string assetKind, + + string assetMode, + + string basePath = "base") + + { + + var result = new StaticWebAsset() + + { + + Identity = Path.GetFullPath(itemSpec), + + SourceId = sourceId, + + SourceType = sourceType, + + ContentRoot = Directory.GetCurrentDirectory(), + + BasePath = basePath, + + RelativePath = relativePath, + + AssetKind = assetKind, + + AssetMode = assetMode, + + AssetRole = "Primary", + + RelatedAsset = "", + + AssetTraitName = "", + + AssetTraitValue = "", + + CopyToOutputDirectory = "", + + CopyToPublishDirectory = "", + + OriginalItemSpec = itemSpec, + + // Add these to avoid accessing the disk to compute them + + Integrity = "integrity", + + Fingerprint = "fingerprint", + + LastWriteTime = DateTime.UtcNow, + + FileLength = 10, + + }; + + + + result.ApplyDefaults(); + + result.Normalize(); + + + + return result.ToTaskItem(); + + } + + + + private static ITaskItem CreateCandidateEndpoint(string route, string assetFile, bool addLabel = false) + + { + + return new StaticWebAssetEndpoint + + { + + Route = route, + + AssetFile = Path.GetFullPath(assetFile), + + EndpointProperties = addLabel + + ? [new StaticWebAssetEndpointProperty { Name = "label", Value = "label-value" }] + + : [], + + }.ToTaskItem(); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeReferenceStaticWebAssetItemsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeReferenceStaticWebAssetItemsTest.cs index cea416a71f0b..c3f1f1bb6584 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeReferenceStaticWebAssetItemsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeReferenceStaticWebAssetItemsTest.cs @@ -1,491 +1,1475 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { + + + [TestClass] + public class ComputeReferenceStaticWebAssetItemsTest + + { - [Fact] + + + [TestMethod] + + public void IncludesAssetsFromCurrentProjectAsReferencedAssets() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All") }, + + Patterns = new ITaskItem[] { }, + + AssetKind = "Build", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(1); + + } - [Fact] + + + + + [TestMethod] + + public void IncludesPatternsFromCurrentProject() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All") }, + + Patterns = new[] { CreatePatternCandidate("MyPackage\\wwwroot", "base", Directory.GetCurrentDirectory(), "wwwroot\\**", "MyPackage") }, + + AssetKind = "Build", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.DiscoveryPatterns.Should().HaveCount(1); + + } - [Fact] + + + + + [TestMethod] + + public void FiltersPatternsFromReferencedProjects() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All") }, + + Patterns = new[] { CreatePatternCandidate("Other\\wwwroot", "base", Directory.GetCurrentDirectory(), "wwwroot\\**", "Other") }, + + AssetKind = "Build", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.DiscoveryPatterns.Should().HaveCount(0); + + } - [Fact] + + + + + [TestMethod] + + public void PrefersSpecificKindAssetsOverAllKindAssets() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] + + { + + CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All"), + + CreateCandidate(Path.Combine("wwwroot", "candidate.other.js"), "MyPackage", "Discovered", "candidate.js", "Build", "All") + + }, + + Patterns = new ITaskItem[] { }, + + AssetKind = "Build", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(1); + + task.StaticWebAssets[0].ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.other.js"))); + + } - [Fact] + + + + + [TestMethod] + + public void AllAssetGetsIgnoredWhenBuildAndPublishAssetsAreDefined() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] + + { + + CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All"), + + CreateCandidate(Path.Combine("wwwroot", "candidate.other.js"), "MyPackage", "Discovered", "candidate.js", "Build", "All"), + + CreateCandidate(Path.Combine("wwwroot", "candidate.publish.js"), "MyPackage", "Discovered", "candidate.js", "Publish", "All") + + }, + + Patterns = new ITaskItem[] { }, + + AssetKind = "Build", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(1); + + task.StaticWebAssets[0].ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.other.js"))); + + } - [Theory] - [InlineData("Build", "Publish")] - [InlineData("Publish", "Build")] + + + + + [TestMethod] + + + [DataRow("Build", "Publish")] + + + [DataRow("Publish", "Build")] + + public void FiltersAssetsForOppositeKind(string assetKind, string manifestKind) + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", assetKind, "All") }, + + Patterns = new ITaskItem[] { }, + + AssetKind = manifestKind, + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(0); + + } - [Fact] + + + + + [TestMethod] + + public void FiltersCurrentProjectOnlyAssetsInDefaultMode() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "CurrentProject") }, + + Patterns = new ITaskItem[] { }, + + AssetKind = "Default", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(0); + + } - [Fact] + + + + + [TestMethod] + + public void IncludesReferenceAssetsInDefaultMode() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "Reference") }, + + Patterns = new ITaskItem[] { }, + + AssetKind = "Default", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(1); + + } - [Fact] + + + + + [TestMethod] + + public void IncludesCurrentProjectAssetsInRootMode() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "CurrentProject") }, + + Patterns = new ITaskItem[] { }, + + AssetKind = "Default", + + ProjectMode = "Root" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(1); + + } - [Fact] + + + + + [TestMethod] + + public void FiltersReferenceOnlyAssetsInRootMode() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "Reference") }, + + Patterns = new ITaskItem[] { }, + + AssetKind = "Default", + + ProjectMode = "Root" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(0); + + } - [Fact] + + + + + [TestMethod] + + public void FiltersAssetsFromOtherProjects() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "Other", "Project", "candidate.js", "All", "All") }, + + Patterns = new ITaskItem[] { }, + + AssetKind = "Build", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(0); + + } - [Fact] + + + + + [TestMethod] + + public void FiltersAssetsFromPackages() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "Other", "Package", "candidate.js", "All", "All") }, + + Patterns = new ITaskItem[] { }, + + AssetKind = "Build", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(0); + + } - [Fact] + + + + + [TestMethod] + + public void AppliesFrameworkPatternToDiscoveredAssets() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] + + { + + CreateCandidate(Path.Combine("wwwroot", "framework.js"), "MyPackage", "Discovered", "framework.js", "All", "All"), + + CreateCandidate(Path.Combine("wwwroot", "app.css"), "MyPackage", "Discovered", "app.css", "All", "All") + + }, + + Patterns = new ITaskItem[] { }, + + AssetKind = "Build", + + ProjectMode = "Default", + + FrameworkPattern = "**/*.js" + + }; - // Act - var result = task.Execute(); + + + + + // Act + + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(2); + + task.StaticWebAssets[0].GetMetadata("SourceType").Should().Be("Framework"); + + task.StaticWebAssets[1].GetMetadata("SourceType").Should().Be("Project"); + + } - [Fact] + + + + + [TestMethod] + + public void FrameworkPatternDoesNotAffectNonDiscoveredAssets() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Project", "candidate.js", "All", "All") }, + + Patterns = new ITaskItem[] { }, + + AssetKind = "Build", + + ProjectMode = "Default", + + UpdateSourceType = false, + + FrameworkPattern = "**/*.js" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(1); + + task.StaticWebAssets[0].GetMetadata("SourceType").Should().Be("Project"); + + } - [Fact] + + + + + [TestMethod] + + public void PreservesAssetGroupsOnFrameworkAssets() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var asset = CreateCandidate(Path.Combine("wwwroot", "framework.js"), "MyPackage", "Framework", "framework.js", "All", "All"); + + asset.SetMetadata("AssetGroups", "MyGroup"); + + + + var task = new ComputeReferenceStaticWebAssetItems + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { asset }, + + Patterns = new ITaskItem[] { }, + + AssetKind = "Build", + + ProjectMode = "Default", + + UpdateSourceType = true + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert — groups are preserved here so FilterByGroup can evaluate them; + + // clearing happens later during MaterializeFrameworkAsset. + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(1); + + task.StaticWebAssets[0].GetMetadata("SourceType").Should().Be("Framework"); + + task.StaticWebAssets[0].GetMetadata("AssetGroups").Should().Be("MyGroup"); + + } + + + + private static ITaskItem CreateCandidate( + + string itemSpec, + + string sourceId, + + string sourceType, + + string relativePath, + + string assetKind, + + string assetMode) + + { + + var result = new StaticWebAsset() + + { + + Identity = Path.GetFullPath(itemSpec), + + SourceId = sourceId, + + SourceType = sourceType, + + ContentRoot = Directory.GetCurrentDirectory(), + + BasePath = "base", + + RelativePath = relativePath, + + AssetKind = assetKind, + + AssetMode = assetMode, + + AssetRole = "Primary", + + RelatedAsset = "", + + AssetTraitName = "", + + AssetTraitValue = "", + + CopyToOutputDirectory = "", + + CopyToPublishDirectory = "", + + OriginalItemSpec = itemSpec, + + // Add these to avoid accessing the disk to compute them + + Integrity = "integrity", + + Fingerprint = "fingerprint", + + FileLength = 10, + + LastWriteTime = DateTime.UtcNow, + + }; + + + + result.ApplyDefaults(); + + result.Normalize(); + + + + return result.ToTaskItem(); + + } + + + + private static ITaskItem CreatePatternCandidate( + + string name, + + string basePath, + + string contentRoot, + + string pattern, + + string source) + + { + + var result = new StaticWebAssetsDiscoveryPattern() + + { + + Name = name, + + BasePath = basePath, + + ContentRoot = contentRoot, + + Pattern = pattern, + + Source = source + + }; + + + + return result.ToTaskItem(); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeStaticWebAssetsForCurrentProjectTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeStaticWebAssetsForCurrentProjectTest.cs index 3ffa48fa7ced..b1136ff2be8d 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeStaticWebAssetsForCurrentProjectTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeStaticWebAssetsForCurrentProjectTest.cs @@ -1,316 +1,950 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Text.Json; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { + + + [TestClass] + public class ComputeStaticWebAssetsForCurrentProjectTest + + { - [Fact] + + + [TestMethod] + + public void IncludesAssetsFromCurrentProject() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeStaticWebAssetsForCurrentProject + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "All") }, + + AssetKind = "Build", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(1); + + } - [Fact] + + + + + [TestMethod] + + public void PrefersSpecificKindAssetsOverAllKindAssets() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeStaticWebAssetsForCurrentProject + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] + + { + + CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All"), + + CreateCandidate(Path.Combine("wwwroot", "candidate.other.js"), "MyPackage", "Discovered", "candidate.js", "Build", "All") + + }, + + AssetKind = "Build", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(1); + + task.StaticWebAssets[0].ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.other.js"))); + + } - [Fact] + + + + + [TestMethod] + + public void AllAssetGetsIgnoredWhenBuildAndPublishAssetsAreDefined() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeStaticWebAssetsForCurrentProject + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] + + { + + CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All"), + + CreateCandidate(Path.Combine("wwwroot", "candidate.other.js"), "MyPackage", "Discovered", "candidate.js", "Build", "All"), + + CreateCandidate(Path.Combine("wwwroot", "candidate.publish.js"), "MyPackage", "Discovered", "candidate.js", "Publish", "All") + + }, + + AssetKind = "Build", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(1); + + task.StaticWebAssets[0].ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.other.js"))); + + } - [Theory] - [InlineData("Build", "Publish")] - [InlineData("Publish", "Build")] + + + + + [TestMethod] + + + [DataRow("Build", "Publish")] + + + [DataRow("Publish", "Build")] + + public void FiltersAssetsForOppositeKind(string assetKind, string manifestKind) + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeStaticWebAssetsForCurrentProject + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", assetKind, "All") }, + + AssetKind = manifestKind, + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(0); + + } - [Fact] + + + + + [TestMethod] + + public void IncludesCurrentProjectOnlyAssetsInDefaultMode() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeStaticWebAssetsForCurrentProject + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "CurrentProject") }, + + AssetKind = "Default", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(1); + + } - [Fact] + + + + + [TestMethod] + + public void FiltersReferenceAssetsInDefaultMode() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeStaticWebAssetsForCurrentProject + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "Reference") }, + + AssetKind = "Default", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(0); + + } - [Fact] + + + + + [TestMethod] + + public void IncludesCurrentProjectAssetsInRootMode() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeStaticWebAssetsForCurrentProject + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "CurrentProject") }, + + AssetKind = "Default", + + ProjectMode = "Root" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(1); + + } - [Fact] + + + + + [TestMethod] + + public void FiltersReferenceOnlyAssetsInRootMode() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeStaticWebAssetsForCurrentProject + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "Reference") }, + + AssetKind = "Default", + + ProjectMode = "Root" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(0); + + } - [Fact] + + + + + [TestMethod] + + public void IncludesAssetsFromOtherProjects() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeStaticWebAssetsForCurrentProject + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "Other", "Project", "candidate.js", "All", "All") }, + + AssetKind = "Build", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(1); + + } - [Fact] + + + + + [TestMethod] + + public void IncludesAssetsFromPackages() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ComputeStaticWebAssetsForCurrentProject + + { + + BuildEngine = buildEngine.Object, + + Source = "MyPackage", + + Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "Other", "Package", "candidate.js", "All", "All") }, + + AssetKind = "Build", + + ProjectMode = "Default" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.StaticWebAssets.Should().HaveCount(1); + + } + + + + private static ITaskItem CreateCandidate( + + string itemSpec, + + string sourceId, + + string sourceType, + + string relativePath, + + string assetKind, + + string assetMode) + + { + + var result = new StaticWebAsset() + + { + + Identity = Path.GetFullPath(itemSpec), + + SourceId = sourceId, + + SourceType = sourceType, + + ContentRoot = Directory.GetCurrentDirectory(), + + BasePath = "base", + + RelativePath = relativePath, + + AssetKind = assetKind, + + AssetMode = assetMode, + + AssetRole = "Primary", + + RelatedAsset = "", + + AssetTraitName = "", + + AssetTraitValue = "", + + CopyToOutputDirectory = "", + + CopyToPublishDirectory = "", + + OriginalItemSpec = itemSpec, + + // Add these to avoid accessing the disk to compute them + + Integrity = "integrity", + + Fingerprint = "fingerprint", + + LastWriteTime = DateTime.UtcNow, + + FileLength = 10, + + }; + + + + result.ApplyDefaults(); + + result.Normalize(); + + + + return result.ToTaskItem(); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ConcatenateFilesTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ConcatenateFilesTest.cs index 71c9af0dda70..fd41edc17719 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ConcatenateFilesTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ConcatenateFilesTest.cs @@ -1,353 +1,1061 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.StaticWebAssets.Tasks; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + + namespace Microsoft.NET.Sdk.Razor.Test + + { + + + [TestClass] + public class ConcatenateCssFilesTest + + { + public TestContext TestContext { get; set; } = null!; + + private static readonly string BundleContent = + + @"/* _content/Test/TestFiles/Generated/Counter.razor.rz.scp.css */ + + .counter { + + font-size: 2rem; + + } + + /* _content/Test/TestFiles/Generated/Index.razor.rz.scp.css */ + + .index { + + font-weight: bold; + + } + + "; + + + + private static readonly string BundleWithImportsContent = """ + + @import '_content/Test/TestFiles/Generated/lib.bundle.scp.css'; + + @import '_content/Test/TestFiles/Generated/package.bundle.scp.css'; + + + + /* _content/Test/TestFiles/Generated/Counter.razor.rz.scp.css */ + + .counter { + + font-size: 2rem; + + } + + /* _content/Test/TestFiles/Generated/Index.razor.rz.scp.css */ + + .index { + + font-weight: bold; + + } + + """; + + + + private static readonly string UpdatedBundleContent = + + @"/* _content/Test/TestFiles/Generated/Counter.razor.rz.scp.css */ + + .counter { + + font-size: 2rem; + + } + + /* _content/Test/TestFiles/Generated/FetchData.razor.rz.scp.css */ + + .fetchData { + + font-family: Helvetica; + + } + + /* _content/Test/TestFiles/Generated/Index.razor.rz.scp.css */ + + .index { + + font-weight: bold; + + } + + "; - [Fact] + + + + + [TestMethod] + + public void BundlesScopedCssFiles_ProducesEmpyBundleIfNoFilesAvailable() + + { + + // Arrange + + var expectedFile = Path.Combine(Directory.GetCurrentDirectory(), $"{Guid.NewGuid():N}.css"); + + var taskInstance = new ConcatenateCssFiles() + + { + + ScopedCssFiles = Array.Empty(), + + ProjectBundles = Array.Empty(), + + OutputFile = expectedFile + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + File.Exists(expectedFile).Should().BeTrue(); + + File.ReadAllText(expectedFile).Should().BeEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void BundlesScopedCssFiles_ProducesBundle() + + { + + // Arrange + + var expectedFile = Path.Combine(Directory.GetCurrentDirectory(), $"{Guid.NewGuid():N}.css"); + + var taskInstance = new ConcatenateCssFiles() + + { + + ScopedCssFiles = new[] + + { + + CreateStaticAsset( + + "TestFiles/Generated/Counter.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/Counter.razor.rz.scp.css"), + + CreateStaticAsset( + + "TestFiles/Generated/Index.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/Index.razor.rz.scp.css"), + + }, + + ProjectBundles = Array.Empty(), + + OutputFile = expectedFile + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + File.Exists(expectedFile).Should().BeTrue(); + + + + var actualContents = File.ReadAllText(expectedFile); + + actualContents.Should().Contain(BundleContent); + + } + + + + private static TaskItem CreateEndpoint(string route) => + + new TaskItem(route); + + + + private static TaskItem CreateStaticAsset(string identity, string basePath, string relativePath) => + + new TaskItem( + + identity, + + new Dictionary + + { + + ["BasePath"] = basePath, + + ["RelativePath"] = relativePath, + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "staticwebassets"), + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["OriginalItemSpec"] = identity, + + ["Fingerprint"] = $"{Path.GetFileNameWithoutExtension(identity)}-fingerprint", + + ["Integrity"] = $"{Path.GetFileNameWithoutExtension(identity)}-integrity", + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }); - [Fact] + + + + + [TestMethod] + + public void BundlesScopedCssFiles_IncludesOtherBundles() + + { + + // Arrange + + var expectedFile = Path.Combine(Directory.GetCurrentDirectory(), $"{Guid.NewGuid():N}.css"); + + var taskInstance = new ConcatenateCssFiles() + + { + + ScopedCssFiles = new[] + + { + + CreateStaticAsset( + + "TestFiles/Generated/Counter.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/Counter.razor.rz.scp.css"), + + CreateStaticAsset( + + "TestFiles/Generated/Index.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/Index.razor.rz.scp.css"), + + }, + + ProjectBundles = new[] + + { + + CreateEndpoint("_content/Test/TestFiles/Generated/lib.bundle.scp.css"), + + CreateEndpoint("_content/Test/TestFiles/Generated/package.bundle.scp.css"), + + }, + + ScopedCssBundleBasePath = "/", + + OutputFile = expectedFile + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + File.Exists(expectedFile).Should().BeTrue(); + + + + var actualContents = File.ReadAllText(expectedFile); + + actualContents.Should().Contain(BundleWithImportsContent); + + } - [Theory] - [InlineData("", "", "TestFiles/Generated/lib.bundle.scp.css")] - [InlineData("/", "/", "TestFiles/Generated/lib.bundle.scp.css")] - [InlineData("app", "_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - [InlineData("app", "/_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - [InlineData("app", "/_content/", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - [InlineData("/app", "_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - [InlineData("/app", "/_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - [InlineData("/app", "/_content/", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - [InlineData("app/", "_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - [InlineData("app/", "/_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - [InlineData("app/", "/_content/", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - [InlineData("/company/app/", "_content", "../../_content/TestFiles/Generated/lib.bundle.scp.css")] - [InlineData("/company/app/", "/_content", "../../_content/TestFiles/Generated/lib.bundle.scp.css")] - [InlineData("/company/app/", "/_content/", "../../_content/TestFiles/Generated/lib.bundle.scp.css")] + + + + + [TestMethod] + + + [DataRow("", "", "TestFiles/Generated/lib.bundle.scp.css")] + + + [DataRow("/", "/", "TestFiles/Generated/lib.bundle.scp.css")] + + + [DataRow("app", "_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] + + + [DataRow("app", "/_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] + + + [DataRow("app", "/_content/", "../_content/TestFiles/Generated/lib.bundle.scp.css")] + + + [DataRow("/app", "_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] + + + [DataRow("/app", "/_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] + + + [DataRow("/app", "/_content/", "../_content/TestFiles/Generated/lib.bundle.scp.css")] + + + [DataRow("app/", "_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] + + + [DataRow("app/", "/_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] + + + [DataRow("app/", "/_content/", "../_content/TestFiles/Generated/lib.bundle.scp.css")] + + + [DataRow("/company/app/", "_content", "../../_content/TestFiles/Generated/lib.bundle.scp.css")] + + + [DataRow("/company/app/", "/_content", "../../_content/TestFiles/Generated/lib.bundle.scp.css")] + + + [DataRow("/company/app/", "/_content/", "../../_content/TestFiles/Generated/lib.bundle.scp.css")] + + public void BundlesScopedCssFiles_HandlesBasePathCombinationsCorrectly(string finalBasePath, string libraryBasePath, string expectedImport) + + { + + // Arrange + + var expectedContent = BundleWithImportsContent + + .Replace("_content/Test/TestFiles/Generated/lib.bundle.scp.css", expectedImport) + + .Replace("@import '_content/Test/TestFiles/Generated/package.bundle.scp.css';", "") + + .Replace("\r\n", "\n") + + .Replace("\n\n", "\n"); + + + + var expectedFile = Path.Combine(Directory.GetCurrentDirectory(), $"{Guid.NewGuid():N}.css"); + + var taskInstance = new ConcatenateCssFiles() + + { + + ScopedCssFiles = new[] + + { + + CreateStaticAsset( + + "TestFiles/Generated/Counter.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/Counter.razor.rz.scp.css"), + + CreateStaticAsset( + + "TestFiles/Generated/Index.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/Index.razor.rz.scp.css"), + + }, + + ProjectBundles = new[] + + { + + CreateEndpoint(StaticWebAsset.CombineNormalizedPaths("",libraryBasePath,"TestFiles/Generated/lib.bundle.scp.css", '/')) + + }, + + ScopedCssBundleBasePath = finalBasePath, + + OutputFile = expectedFile + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + File.Exists(expectedFile).Should().BeTrue(); - var actualContents = File.ReadAllText(expectedFile); - actualContents.Should().BeVisuallyEquivalentTo(expectedContent); - } - [Fact] + + + + var actualContents = File.ReadAllText(expectedFile); + + + actualContents.Should().BeVisuallyEquivalentTo(expectedContent); + + + } + + + + + + [TestMethod] + + public void BundlesScopedCssFiles_BundlesFilesInOrder() + + { + + // Arrange + + var expectedFile = Path.Combine(Directory.GetCurrentDirectory(), $"{Guid.NewGuid():N}.css"); + + var taskInstance = new ConcatenateCssFiles() + + { + + ScopedCssFiles = new[] + + { + + CreateStaticAsset( + + "TestFiles/Generated/Index.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/Index.razor.rz.scp.css"), + + CreateStaticAsset( + + "TestFiles/Generated/Counter.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/Counter.razor.rz.scp.css") + + }, + + ProjectBundles = Array.Empty(), + + OutputFile = expectedFile + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + File.Exists(expectedFile).Should().BeTrue(); + + + + var actualContents = File.ReadAllText(expectedFile); + + actualContents.Should().Contain(BundleContent); + + } - [Fact] + + + + + [TestMethod] + + public void BundlesScopedCssFiles_DoesNotOverrideBundleForSameContents() + + { + + // Arrange + + var expectedFile = Path.Combine(Directory.GetCurrentDirectory(), $"{Guid.NewGuid():N}.css"); + + var taskInstance = new ConcatenateCssFiles() + + { + + ScopedCssFiles = new[] + + { + + CreateStaticAsset( + + "TestFiles/Generated/Index.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/Index.razor.rz.scp.css"), + + CreateStaticAsset( + + "TestFiles/Generated/Counter.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/Counter.razor.rz.scp.css") + + }, + + ProjectBundles = Array.Empty(), + + OutputFile = expectedFile + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + var lastModified = File.GetLastWriteTimeUtc(expectedFile); + + + + taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + File.Exists(expectedFile).Should().BeTrue(); + + var actualContents = File.ReadAllText(expectedFile); + + actualContents.Should().Contain(BundleContent); + + + + lastModified.Should().BeSameDateAs(File.GetLastWriteTimeUtc(expectedFile)); + + } - [Fact] + + + + + [TestMethod] + + public async System.Threading.Tasks.Task BundlesScopedCssFiles_UpdatesBundleWhenContentsChange() + + { + + // Arrange + + var expectedFile = Path.Combine(Directory.GetCurrentDirectory(), $"{Guid.NewGuid():N}.css"); + + var taskInstance = new ConcatenateCssFiles() + + { + + ScopedCssFiles = new[] + + { + + CreateStaticAsset( + + "TestFiles/Generated/Index.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/Index.razor.rz.scp.css"), + + CreateStaticAsset( + + "TestFiles/Generated/Counter.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/Counter.razor.rz.scp.css") + + }, + + ProjectBundles = Array.Empty(), + + OutputFile = expectedFile + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + var lastModified = File.GetLastWriteTimeUtc(expectedFile); + + + + taskInstance.ScopedCssFiles = new[] + + { + + CreateStaticAsset( + + "TestFiles/Generated/Index.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/Index.razor.rz.scp.css"), + + CreateStaticAsset( + + "TestFiles/Generated/Counter.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/Counter.razor.rz.scp.css"), + + CreateStaticAsset( + + "TestFiles/Generated/FetchData.razor.rz.scp.css", + + "_content/Test/", + + "TestFiles/Generated/FetchData.razor.rz.scp.css"), + + }; - await System.Threading.Tasks.Task.Delay(1000, TestContext.Current.CancellationToken); + + + + + await System.Threading.Tasks.Task.Delay(1000, TestContext.CancellationToken); + + taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + File.Exists(expectedFile).Should().BeTrue(); + + var actualContents = File.ReadAllText(expectedFile); + + + + actualContents.Should().Contain(UpdatedBundleContent); + + lastModified.Should().NotBe(File.GetLastWriteTimeUtc(expectedFile)); + + } + + } + + } + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ContentTypeProviderTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ContentTypeProviderTests.cs index 88805b73dad2..91b66bc42d6e 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ContentTypeProviderTests.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ContentTypeProviderTests.cs @@ -1,206 +1,620 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; + + + + +[TestClass] + public class ContentTypeProviderTests + + { + + private readonly TaskLoggingHelper _log = new TestTaskLoggingHelper(); - [Fact] + + + + + [TestMethod] + + public void GetContentType_ReturnsTextPlainForTextFiles() + + { + + // Arrange + + var provider = new ContentTypeProvider([]); + + + + // Act + + var contentType = provider.ResolveContentTypeMapping(CreateContext("Fake-License.txt"), _log); + + + + // Assert - Assert.Equal("text/plain", contentType.MimeType); + + + Assert.AreEqual("text/plain", contentType.MimeType); + + } - [Fact] + + + + + [TestMethod] + + public void GetContentType_ReturnsMappingForRelativePath() + + { + + // Arrange + + var provider = new ContentTypeProvider([]); + + + + // Act + + var contentType = provider.ResolveContentTypeMapping(CreateContext("Components/Pages/Counter.razor.js"), _log); + + + + // Assert - Assert.Equal("text/javascript", contentType.MimeType); + + + Assert.AreEqual("text/javascript", contentType.MimeType); + + } + + + + private StaticWebAssetGlobMatcher.MatchContext CreateContext(string v) + + { + + var ctx = StaticWebAssetGlobMatcher.CreateMatchContext(); + + ctx.SetPathAndReinitialize(v); + + return ctx; + + } + + + + // wwwroot\exampleJsInterop.js.gz - [Fact] + + + + + [TestMethod] + + public void GetContentType_ReturnsMappingForCompressedRelativePath() + + { + + // Arrange + + var provider = new ContentTypeProvider([]); + + + + // Act + + var contentType = provider.ResolveContentTypeMapping(CreateContext("wwwroot/exampleJsInterop.js.gz"), _log); + + + + // Assert - Assert.Equal("text/javascript", contentType.MimeType); + + + Assert.AreEqual("text/javascript", contentType.MimeType); + + } - [Fact] + + + + + [TestMethod] + + public void GetContentType_HandlesFingerprintedPaths() + + { + + // Arrange + + var provider = new ContentTypeProvider([]); + + // Act + + var contentType = provider.ResolveContentTypeMapping(CreateContext("_content/RazorPackageLibraryDirectDependency/RazorPackageLibraryDirectDependency#[.{fingerprint}].bundle.scp.css.gz"), _log); + + // Assert - Assert.Equal("text/css", contentType.MimeType); + + + Assert.AreEqual("text/css", contentType.MimeType); + + } - [Fact] + + + + + [TestMethod] + + public void GetContentType_ReturnsDefaultForUnknownMappings() + + { + + // Arrange + + var provider = new ContentTypeProvider([]); + + + + // Act + + var contentType = provider.ResolveContentTypeMapping(CreateContext("something.unknown"), _log); + + + + // Assert - Assert.Null(contentType.MimeType); + + + Assert.IsNull(contentType.MimeType); + + } - [Theory] - [InlineData("something.unknown.gz", "application/x-gzip")] - [InlineData("something.unknown.br", "application/octet-stream")] + + + + + [TestMethod] + + + [DataRow("something.unknown.gz", "application/x-gzip")] + + + [DataRow("something.unknown.br", "application/octet-stream")] + + public void GetContentType_ReturnsGzipOrBrotliForUnknownCompressedMappings(string path, string expectedMapping) + + { + + // Arrange + + var provider = new ContentTypeProvider([]); + + + + // Act + + var contentType = provider.ResolveContentTypeMapping(CreateContext(path), _log); + + + + // Assert - Assert.Equal(expectedMapping, contentType.MimeType); + + + Assert.AreEqual(expectedMapping, contentType.MimeType); + + } - [Theory] - [InlineData("Fake-License.txt.gz")] - [InlineData("Fake-License.txt.br")] + + + + + [TestMethod] + + + [DataRow("Fake-License.txt.gz")] + + + [DataRow("Fake-License.txt.br")] + + public void GetContentType_ReturnsTextPlainForCompressedTextFiles(string path) + + { + + // Arrange + + var provider = new ContentTypeProvider([]); + + + + // Act + + var contentType = provider.ResolveContentTypeMapping(CreateContext(path), _log); + + + + // Assert - Assert.Equal("text/plain", contentType.MimeType); + + + Assert.AreEqual("text/plain", contentType.MimeType); + + } - [Fact] + + + + + [TestMethod] + + public void GetContentType_CustomMappingOverridesBuiltInMapping() + + { + + // Arrange + + var customMapping = new ContentTypeMapping("text/html", "no-store, must-revalidate, no-cache", "*.html", 2); + + var provider = new ContentTypeProvider([customMapping]); + + + + // Act + + var contentType = provider.ResolveContentTypeMapping(CreateContext("index.html"), _log); + + + + // Assert - Assert.Equal("text/html", contentType.MimeType); - Assert.Equal("no-store, must-revalidate, no-cache", contentType.Cache); - Assert.Equal("*.html", contentType.Pattern); - Assert.Equal(2, contentType.Priority); + + + Assert.AreEqual("text/html", contentType.MimeType); + + + Assert.AreEqual("no-store, must-revalidate, no-cache", contentType.Cache); + + + Assert.AreEqual("*.html", contentType.Pattern); + + + Assert.AreEqual(2, contentType.Priority); + + } - [Fact] + + + + + [TestMethod] + + public void GetContentType_CustomMappingOverridesBuiltInMappingForCompressedFiles() + + { + + // Arrange + + var customMapping = new ContentTypeMapping("text/html", "no-store, must-revalidate, no-cache", "*.html", 2); + + var provider = new ContentTypeProvider([customMapping]); + + + + // Act + + var contentType = provider.ResolveContentTypeMapping(CreateContext("index.html.gz"), _log); + + + + // Assert - Assert.Equal("text/html", contentType.MimeType); - Assert.Equal("no-store, must-revalidate, no-cache", contentType.Cache); - Assert.Equal("*.html", contentType.Pattern); - Assert.Equal(2, contentType.Priority); + + + Assert.AreEqual("text/html", contentType.MimeType); + + + Assert.AreEqual("no-store, must-revalidate, no-cache", contentType.Cache); + + + Assert.AreEqual("*.html", contentType.Pattern); + + + Assert.AreEqual(2, contentType.Priority); + + } - [Fact] + + + + + [TestMethod] + + public void GetContentType_CustomJavaScriptMappingOverridesBuiltIn() + + { + + // Arrange + + var customMapping = new ContentTypeMapping("text/javascript", "max-age=3600", "*.js", 3); + + var provider = new ContentTypeProvider([customMapping]); + + + + // Act + + var contentType = provider.ResolveContentTypeMapping(CreateContext("app.js"), _log); + + + + // Assert - Assert.Equal("text/javascript", contentType.MimeType); - Assert.Equal("max-age=3600", contentType.Cache); - Assert.Equal("*.js", contentType.Pattern); - Assert.Equal(3, contentType.Priority); + + + Assert.AreEqual("text/javascript", contentType.MimeType); + + + Assert.AreEqual("max-age=3600", contentType.Cache); + + + Assert.AreEqual("*.js", contentType.Pattern); + + + Assert.AreEqual(3, contentType.Priority); + + } + + + + private class TestTaskLoggingHelper : TaskLoggingHelper + + { + + public TestTaskLoggingHelper() : base(new TestTask()) + + { + + } + + + + private class TestTask : ITask + + { + + public IBuildEngine BuildEngine { get; set; } = new TestBuildEngine(); + + public ITaskHost HostObject { get; set; } = new TestTaskHost(); + + + + public bool Execute() => true; + + } + + + + private class TestBuildEngine : IBuildEngine + + { + + public bool ContinueOnError => true; + + + + public int LineNumberOfTaskNode => 0; + + + + public int ColumnNumberOfTaskNode => 0; + + + + public string ProjectFileOfTaskNode => "test.csproj"; + + + + public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => true; + + + + public void LogCustomEvent(CustomBuildEventArgs e) { } + + public void LogErrorEvent(BuildErrorEventArgs e) { } + + public void LogMessageEvent(BuildMessageEventArgs e) { } + + public void LogWarningEvent(BuildWarningEventArgs e) { } + + } + + + + private class TestTaskHost : ITaskHost + + { + + public object HostObject { get; set; } = new object(); + + } + + } + + + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DefineStaticWebAssetEndpointsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DefineStaticWebAssetEndpointsTest.cs index 86c36fc1a773..384524f6c86e 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DefineStaticWebAssetEndpointsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DefineStaticWebAssetEndpointsTest.cs @@ -1,875 +1,2627 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Diagnostics.Metrics; + + using System.Diagnostics; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + using NuGet.Packaging.Core; + + using System.Net; + + using System.Globalization; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + + + + +[TestClass] + public class DefineStaticWebAssetEndpointsTest + + { - [Theory] - [InlineData(StaticWebAsset.SourceTypes.Discovered)] - [InlineData(StaticWebAsset.SourceTypes.Computed)] + + + [TestMethod] + + + [DataRow(StaticWebAsset.SourceTypes.Discovered)] + + + [DataRow(StaticWebAsset.SourceTypes.Computed)] + + public void DefinesEndpointsForAssets(string sourceType) + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + + + var task = new DefineStaticWebAssetEndpoints + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + sourceType, + + "candidate.js", + + "All", + + "All", + + fileLength: 10, + + lastWriteTime: lastWrite)], + + ExistingEndpoints = [], + + ContentTypeMappings = [CreateContentMapping("**/*.js", "text/javascript")], + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); + + endpoints.Should().ContainSingle(); + + var endpoint = endpoints[0]; + + + + endpoint.Route.Should().Be("candidate.js"); + + endpoint.AssetFile.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + endpoint.ResponseHeaders.Should().BeEquivalentTo( + + [ + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Cache-Control", + + Value = "no-cache" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Content-Length", + + Value = "10" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "ETag", + + Value = "\"integrity\"" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Last-Modified", + + Value = "Thu, 15 Nov 1990 00:00:00 GMT" + + } + + ]); + + } - [Fact] + + + + + [TestMethod] + + public void CanDefineFingerprintedEndpoints() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + + + var task = new DefineStaticWebAssetEndpoints + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate#[.{fingerprint}]?.js", + + "All", + + "All", + + fingerprint: "1234asdf", + + integrity: "asdf1234", + + lastWriteTime: lastWrite)], + + ExistingEndpoints = [], + + ContentTypeMappings = [CreateContentMapping("**/*.js", "text/javascript")], + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); + + endpoints.Length.Should().Be(2); + + var endpoint = endpoints[0]; + + + + endpoint.Route.Should().Be("candidate.1234asdf.js"); + + endpoint.AssetFile.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + endpoint.EndpointProperties.Should().BeEquivalentTo([ + + new StaticWebAssetEndpointProperty + + { + + Name = "fingerprint", + + Value = "1234asdf" + + }, + + new StaticWebAssetEndpointProperty + + { + + Name = "integrity", + + Value = "sha256-asdf1234" + + }, + + new StaticWebAssetEndpointProperty + + { + + Name = "label", + + Value = "candidate.js" + + } + + ]); + + endpoint.ResponseHeaders.Should().BeEquivalentTo( + + [ + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Content-Length", + + Value = "10" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "ETag", + + Value = "\"asdf1234\"" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Last-Modified", + + Value = "Thu, 15 Nov 1990 00:00:00 GMT" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Cache-Control", + + Value = "max-age=31536000, immutable" + + } + + ]); + + + + var otherEndpoint = endpoints[1]; + + otherEndpoint.Route.Should().Be("candidate.js"); + + otherEndpoint.AssetFile.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + otherEndpoint.ResponseHeaders.Should().BeEquivalentTo( + + [ + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Cache-Control", + + Value = "no-cache" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Content-Length", + + Value = "10" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "ETag", + + Value = "\"asdf1234\"" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Last-Modified", + + Value = "Thu, 15 Nov 1990 00:00:00 GMT" + + } + + ]); + + } - [Fact] + + + + + [TestMethod] + + public void CanDefineFingerprintedEndpoints_WithEmbeddedFingerprint() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + + + var task = new DefineStaticWebAssetEndpoints + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate#[.{fingerprint=yolo}]?.js", + + "All", + + "All", + + fingerprint: "1234asdf", + + integrity: "asdf1234", + + lastWriteTime : lastWrite)], + + ExistingEndpoints = [], + + ContentTypeMappings = [CreateContentMapping("**/*.js", "text/javascript")], + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); + + endpoints.Length.Should().Be(2); + + var endpoint = endpoints[1]; + + + + endpoint.Route.Should().Be("candidate.yolo.js"); + + endpoint.AssetFile.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + endpoint.EndpointProperties.Should().BeEquivalentTo([ + + new StaticWebAssetEndpointProperty + + { + + Name = "fingerprint", + + Value = "yolo" + + }, + + new StaticWebAssetEndpointProperty + + { + + Name = "integrity", + + Value = "sha256-asdf1234" + + }, + + new StaticWebAssetEndpointProperty + + { + + Name = "label", + + Value = "candidate.js" + + } + + ]); + + endpoint.ResponseHeaders.Should().BeEquivalentTo( + + [ + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Content-Length", + + Value = "10" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "ETag", + + Value = "\"asdf1234\"" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Last-Modified", + + Value = "Thu, 15 Nov 1990 00:00:00 GMT" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Cache-Control", + + Value = "max-age=31536000, immutable" + + } + + ]); + + + + var otherEndpoint = endpoints[0]; + + otherEndpoint.Route.Should().Be("candidate.js"); + + otherEndpoint.AssetFile.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + otherEndpoint.ResponseHeaders.Should().BeEquivalentTo( + + [ + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Cache-Control", + + Value = "no-cache" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Content-Length", + + Value = "10" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "ETag", + + Value = "\"asdf1234\"" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Last-Modified", + + Value = "Thu, 15 Nov 1990 00:00:00 GMT" + + } + + ]); + + } - [Fact] + + + + + [TestMethod] + + public void DoesNotDefineNewEndpointsWhenAnExistingEndpointAlreadyExists() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + var headers = new StaticWebAssetEndpointResponseHeader[] + + { + + new() { + + Name = "Content-Length", + + Value = "10" + + }, + + new() { + + Name = "Content-Type", + + Value = "text/javascript" + + }, + + new() { + + Name = "ETag", + + Value = "integrity" + + }, + + new() { + + Name = "Last-Modified", + + Value = "Thu, 15 Nov 1990 00:00:00 GMT" + + } + + }; + + + + var task = new DefineStaticWebAssetEndpoints + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + lastWriteTime : lastWrite)], + + ExistingEndpoints = [ + + CreateCandidateEndpoint( + + "candidate.js", + + Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), + + headers)], + + ContentTypeMappings = [CreateContentMapping("**/*.js", "text/javascript")], + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); + + endpoints.Should().BeEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void ResolvesContentType_ForCompressedAssets() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + + + var task = new DefineStaticWebAssetEndpoints + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [ + + new TaskItem( + + Path.Combine(AppContext.BaseDirectory, "Client", "obj", "Debug", "net6.0", "compressed", "rdfmaxp4ta-43emfwee4b.gz"), + + new Dictionary + + { + + ["RelativePath"] = "_framework/dotnet.timezones.blat.gz", + + ["BasePath"] = "/", + + ["AssetMode"] = "All", + + ["AssetKind"] = "Build", + + ["SourceId"] = "BlazorWasmHosted60.Client", + + ["CopyToOutputDirectory"] = "PreserveNewest", + + ["Fingerprint"] = "3ji2l2o1xa", + + ["RelatedAsset"] = Path.Combine(AppContext.BaseDirectory, "Client", "bin", "Debug", "net6.0", "wwwroot", "_framework", "dotnet.timezones.blat"), + + ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "Client", "obj", "Debug", "net6.0", "compressed"), + + ["SourceType"] = "Computed", + + ["Integrity"] = "TwfyUDDMyF5dWUB2oRhrZaTk8sEa9o8ezAlKdxypsX4=", + + ["AssetRole"] = "Alternative", + + ["AssetTraitValue"] = "gzip", + + ["AssetTraitName"] = "Content-Encoding", + + ["OriginalItemSpec"] = Path.Combine("D:", "work", "dotnet-sdk", "artifacts", "tmp", "Release", "testing", "Publish60Host---0200F604", "Client", "bin", "Debug", "net6.0", "wwwroot", "_framework", "dotnet.timezones.blat"), + + ["CopyToPublishDirectory"] = "Never", + + ["FileLength"] = "10", + + ["LastWriteTime"] = lastWrite.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) + + }) + + ], + + ExistingEndpoints = [], + + ContentTypeMappings = [], + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); + + endpoints.Length.Should().Be(1); + + var endpoint = endpoints[0]; + + endpoint.ResponseHeaders.Should().ContainSingle(h => h.Name == "Content-Type" && h.Value == "application/x-gzip"); + + } - [Fact] + + + + + [TestMethod] + + public void ResolvesContentType_ForFingerprintedAssets() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + + + var task = new DefineStaticWebAssetEndpoints + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [ + + new TaskItem( + + Path.Combine(AppContext.BaseDirectory, "Client", "obj", "Debug", "net6.0", "compressed", "rdfmaxp4ta-43emfwee4b.gz"), + + new Dictionary + + { + + ["RelativePath"] = "RazorPackageLibraryDirectDependency.iiugt355ct.bundle.scp.css.gz", + + ["BasePath"] = "_content/RazorPackageLibraryDirectDependency", + + ["AssetMode"] = "Reference", + + ["AssetKind"] = "All", + + ["SourceId"] = "RazorPackageLibraryDirectDependency", + + ["CopyToOutputDirectory"] = "Never", + + ["Fingerprint"] = "olx7vzw7zz", + + ["RelatedAsset"] = Path.Combine(AppContext.BaseDirectory, "Client", "obj", "Debug", "net6.0", "compressed", "RazorPackageLibraryDirectDependency.iiugt355ct.bundle.scp.css"), + + ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "Client", "obj", "Debug", "net6.0", "compressed"), + + ["SourceType"] = "Package", + + ["Integrity"] = "JK/W3g5zqZGxAM7zbv/pJ3ngpJheT01SXQ+NofKgQcc=", + + ["AssetRole"] = "Alternative", + + ["AssetTraitValue"] = "gzip", + + ["AssetTraitName"] = "Content-Encoding", + + ["OriginalItemSpec"] = Path.Combine(AppContext.BaseDirectory, "Client", "obj", "Debug", "net6.0", "compressed", "RazorPackageLibraryDirectDependency.iiugt355ct.bundle.scp.css"), + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["FileLength"] = "10", + + ["LastWriteTime"] = lastWrite.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) + + }) + + ], + + ExistingEndpoints = [], + + ContentTypeMappings = [], + + }; + + + + // Act + + var result = task.Execute(); + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); + + endpoints.Length.Should().Be(1); + + var endpoint = endpoints[0]; + + endpoint.ResponseHeaders.Should().ContainSingle(h => h.Name == "Content-Type" && h.Value == "text/css"); + + } - [Fact] + + + + + [TestMethod] + + public void Produces_TheExpectedEndpoint_ForExternalAssets() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + + + var assetIdentity = Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"); + + var task = new DefineStaticWebAssetEndpoints + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [ + + new TaskItem( + + assetIdentity, + + new Dictionary + + { + + ["RelativePath"] = "assets/index-#[{fingerprint}].css", + + ["BasePath"] = "", + + ["AssetMode"] = "All", + + ["AssetKind"] = "Publish", + + ["SourceId"] = "MyProject", + + ["CopyToOutputDirectory"] = "PreserveNewest", + + ["RelatedAsset"] = "", + + ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "dist"), + + ["SourceType"] = "Discovered", + + ["AssetRole"] = "Primary", + + ["AssetTraitValue"] = "", + + ["AssetTraitName"] = "", + + ["Integrity"] = "asdf1234", + + ["Fingerprint"] = "C5tBAdQX", + + ["OriginalItemSpec"] = assetIdentity, + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["FileLength"] = "10", + + ["LastWriteTime"] = lastWrite.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) + + }), + + ], + + ExistingEndpoints = [], + + ContentTypeMappings = [CreateContentMapping("**/*.css", "text/css")], + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); + + endpoints.Length.Should().Be(1); + + var endpoint = endpoints[0]; + + + + endpoint.Route.Should().Be("assets/index-C5tBAdQX.css"); + + endpoint.AssetFile.Should().Be(assetIdentity); + + endpoint.EndpointProperties.Should().BeEquivalentTo([ + + new StaticWebAssetEndpointProperty + + { + + Name = "fingerprint", + + Value = "C5tBAdQX" + + }, + + new StaticWebAssetEndpointProperty + + { + + Name = "integrity", + + Value = "sha256-asdf1234" + + }, + + new StaticWebAssetEndpointProperty + + { + + Name = "label", + + Value = "assets/index-.css" + + } + + ]); + + endpoint.ResponseHeaders.Should().BeEquivalentTo( + + [ + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Content-Length", + + Value = "10" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Content-Type", + + Value = "text/css" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "ETag", + + Value = "\"asdf1234\"" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Last-Modified", + + Value = "Thu, 15 Nov 1990 00:00:00 GMT" + + }, + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Cache-Control", + + Value = "max-age=31536000, immutable" + + } + + ]); + + } + + + + private static ITaskItem CreateCandidate( + + string itemSpec, + + string sourceId, + + string sourceType, + + string relativePath, + + string assetKind, + + string assetMode, + + string fingerprint = null, + + string integrity = null, + + long fileLength = 10, + + DateTimeOffset? lastWriteTime = null) + + { + + lastWriteTime ??= DateTimeOffset.UtcNow; + + var result = new StaticWebAsset() + + { + + Identity = Path.GetFullPath(itemSpec), + + SourceId = sourceId, + + SourceType = sourceType, + + ContentRoot = Directory.GetCurrentDirectory(), + + BasePath = "base", + + RelativePath = relativePath, + + AssetKind = assetKind, + + AssetMode = assetMode, + + AssetRole = "Primary", + + RelatedAsset = "", + + AssetTraitName = "", + + AssetTraitValue = "", + + CopyToOutputDirectory = "", + + CopyToPublishDirectory = "", + + OriginalItemSpec = itemSpec, + + // Add these to avoid accessing the disk to compute them + + Integrity = integrity ?? "integrity", + + Fingerprint = fingerprint ?? "fingerprint", + + FileLength = fileLength, + + LastWriteTime = lastWriteTime.Value, + + }; + + + + result.ApplyDefaults(); + + result.Normalize(); + + + + return result.ToTaskItem(); + + } + + + + private static TaskItem CreateContentMapping(string pattern, string contentType) + + { + + return new TaskItem(contentType, new Dictionary + + { + + { "Pattern", pattern }, + + { "Priority", "0" } + + }); + + } + + + + private static ITaskItem CreateCandidateEndpoint( + + string route, + + string assetFile, + + StaticWebAssetEndpointResponseHeader[] responseHeaders = null, + + StaticWebAssetEndpointSelector[] responseSelector = null, + + StaticWebAssetEndpointProperty[] properties = null) + + { + + return new StaticWebAssetEndpoint + + { + + Route = route, + + AssetFile = Path.GetFullPath(assetFile), + + ResponseHeaders = responseHeaders ?? [], + + EndpointProperties = properties ?? [], + + Selectors = responseSelector ?? [] + + }.ToTaskItem(); + + } + + + + private static TaskItem CreateAdditionalEndpointDefinition(string name, string pattern, string replacement, string order = "") + + { + + return new TaskItem(name, new Dictionary + + { + + { "Pattern", pattern }, + + { "Replacement", replacement }, + + { "Order", order } + + }); + + } - [Theory] - [InlineData("index.html", "index.html", "/")] - [InlineData("admin/index.html", "admin/index.html", "admin")] + + + + + [TestMethod] + + + [DataRow("index.html", "index.html", "/")] + + + [DataRow("admin/index.html", "admin/index.html", "admin")] + + public void AdditionalEndpointDefinitions_DefaultDocument_CreatesEndpointWithCapturedStem( + + string relativeSubPath, string expectedOriginalRoute, string expectedAdditionalRoute) + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + var physicalPath = Path.Combine(new[] { "wwwroot" }.Concat(relativeSubPath.Split('/')).ToArray()); + + + + var task = new DefineStaticWebAssetEndpoints + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [CreateCandidate( + + physicalPath, + + "MyPackage", + + "Discovered", + + relativeSubPath, + + "All", + + "All", + + fileLength: 100, + + lastWriteTime: lastWrite)], + + ExistingEndpoints = [], + + ContentTypeMappings = [CreateContentMapping("**/*.html", "text/html")], + + AdditionalEndpointDefinitions = [ + + CreateAdditionalEndpointDefinition("DefaultDocument", "**/index.html", "") + + ], + + }; + + + + var result = task.Execute(); + + + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); + + // Should have original endpoint + additional endpoint + + endpoints.Length.Should().Be(2); + + + + var original = endpoints.First(e => e.Route == expectedOriginalRoute); + + original.Should().NotBeNull(); + + original.Order.Should().BeNullOrEmpty(); + + + + var additional = endpoints.First(e => e.Route != expectedOriginalRoute); + + additional.Route.Should().Be(expectedAdditionalRoute); + + additional.AssetFile.Should().Be(original.AssetFile); + + additional.ResponseHeaders.Should().BeEquivalentTo(original.ResponseHeaders); + + } - [Fact] + + + + + [TestMethod] + + public void AdditionalEndpointDefinitions_SpaFallback_CreatesEndpointWithFallbackRoute() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + + + var task = new DefineStaticWebAssetEndpoints + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [CreateCandidate( + + Path.Combine("wwwroot", "index.html"), + + "MyPackage", + + "Discovered", + + "index.html", + + "All", + + "All", + + fileLength: 100, + + lastWriteTime: lastWrite)], + + ExistingEndpoints = [], + + ContentTypeMappings = [CreateContentMapping("**/*.html", "text/html")], + + AdditionalEndpointDefinitions = [ + + CreateAdditionalEndpointDefinition("SpaFallback", "index.html", "{**fallback:nonfile}", "2147483647") + + ], + + }; + + + + var result = task.Execute(); + + + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); + + endpoints.Length.Should().Be(2); + + + + var original = endpoints.First(e => e.Route == "index.html"); + + original.Should().NotBeNull(); + + original.Order.Should().BeNullOrEmpty(); + + + + var fallback = endpoints.First(e => e.Route != "index.html"); + + fallback.Route.Should().Be("{**fallback:nonfile}"); + + fallback.AssetFile.Should().Be(original.AssetFile); + + fallback.Order.Should().Be("2147483647"); + + fallback.ResponseHeaders.Should().BeEquivalentTo(original.ResponseHeaders); + + } - [Fact] + + + + + [TestMethod] + + public void AdditionalEndpointDefinitions_DoesNotMatchNonMatchingRoutes() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + + + var task = new DefineStaticWebAssetEndpoints + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [CreateCandidate( + + Path.Combine("wwwroot", "app.js"), + + "MyPackage", + + "Discovered", + + "app.js", + + "All", + + "All", + + fileLength: 50, + + lastWriteTime: lastWrite)], + + ExistingEndpoints = [], + + ContentTypeMappings = [CreateContentMapping("**/*.js", "text/javascript")], + + AdditionalEndpointDefinitions = [ + + CreateAdditionalEndpointDefinition("DefaultDocument", "**/index.html", ""), + + CreateAdditionalEndpointDefinition("SpaFallback", "index.html", "{**fallback:nonfile}", "2147483647") + + ], + + }; + + + + var result = task.Execute(); + + + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); + + // Only the original endpoint, no additional ones + + endpoints.Should().ContainSingle(); + + endpoints[0].Route.Should().Be("app.js"); + + } - [Fact] + + + + + [TestMethod] + + public void AdditionalEndpointDefinitions_BothRules_CreateMultipleAdditionalEndpoints() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + + + var task = new DefineStaticWebAssetEndpoints + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [CreateCandidate( + + Path.Combine("wwwroot", "index.html"), + + "MyPackage", + + "Discovered", + + "index.html", + + "All", + + "All", + + fileLength: 100, + + lastWriteTime: lastWrite)], + + ExistingEndpoints = [], + + ContentTypeMappings = [CreateContentMapping("**/*.html", "text/html")], + + AdditionalEndpointDefinitions = [ + + CreateAdditionalEndpointDefinition("DefaultDocument", "**/index.html", ""), + + CreateAdditionalEndpointDefinition("SpaFallback", "index.html", "{**fallback:nonfile}", "2147483647") + + ], + + }; + + + + var result = task.Execute(); + + + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); + + // Original + DefaultDocument + SpaFallback + + endpoints.Length.Should().Be(3); + + + + endpoints.Should().Contain(e => e.Route == "index.html"); + + endpoints.Should().Contain(e => e.Route == "/"); + + endpoints.Should().Contain(e => e.Route == "{**fallback:nonfile}" && e.Order == "2147483647"); + + } - [Fact] + + + + + [TestMethod] + + public void AdditionalEndpointDefinitions_EmptyArray_NoAdditionalEndpoints() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); + + + + var task = new DefineStaticWebAssetEndpoints + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [CreateCandidate( + + Path.Combine("wwwroot", "index.html"), + + "MyPackage", + + "Discovered", + + "index.html", + + "All", + + "All", + + fileLength: 100, + + lastWriteTime: lastWrite)], + + ExistingEndpoints = [], + + ContentTypeMappings = [CreateContentMapping("**/*.html", "text/html")], + + AdditionalEndpointDefinitions = [], + + }; + + + + var result = task.Execute(); + + + + result.Should().Be(true); + + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); + + endpoints.Should().ContainSingle(); + + endpoints[0].Route.Should().Be("index.html"); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverDefaultScopedCssItemsTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverDefaultScopedCssItemsTests.cs index e4e9a5c914ff..3c46ee0e1bfe 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverDefaultScopedCssItemsTests.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverDefaultScopedCssItemsTests.cs @@ -1,101 +1,305 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.StaticWebAssets.Tasks; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + using Microsoft.Build.Utilities; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + + namespace Microsoft.NET.Sdk.Razor.Test + + { + + + [TestClass] + public class DiscoverDefaultScopedCssItemsTests + + { - [Fact] + + + [TestMethod] + + public void DiscoversScopedCssFiles_BasedOnTheirExtension() + + { + + // Arrange + + var taskInstance = new DiscoverDefaultScopedCssItems() + + { + + Content = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.razor.css"), + + new TaskItem("TestFiles/Pages/Index.razor.css"), + + new TaskItem("TestFiles/Pages/Profile.razor.css"), + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.DiscoveredScopedCssInputs.Should().HaveCount(3); + + } - [Fact] + + + + + [TestMethod] + + public void DoesNotDiscoversScopedCssFilesForViews_IfFeatureIsUnsupported() + + { + + // Arrange + + var taskInstance = new DiscoverDefaultScopedCssItems() + + { + + Content = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.cshtml.css"), + + new TaskItem("TestFiles/Pages/Index.cshtml.css"), + + new TaskItem("TestFiles/Pages/Profile.cshtml.css"), + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.DiscoveredScopedCssInputs.Should().BeEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void DiscoversScopedCssFilesForViews_BasedOnTheirExtension() + + { + + // Arrange + + var taskInstance = new DiscoverDefaultScopedCssItems() + + { + + SupportsScopedCshtmlCss = true, + + Content = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.cshtml.css"), + + new TaskItem("TestFiles/Pages/Index.cshtml.css"), + + new TaskItem("TestFiles/Pages/Profile.cshtml.css"), + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.DiscoveredScopedCssInputs.Should().HaveCount(3); + + } - [Fact] + + + + + [TestMethod] + + public void DiscoversScopedCssFilesForViews_SkipsFilesWithScopedAttributeWithAFalseValue() + + { + + // Arrange + + var taskInstance = new DiscoverDefaultScopedCssItems() + + { + + SupportsScopedCshtmlCss = true, + + Content = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.cshtml.css"), + + new TaskItem("TestFiles/Pages/Index.cshtml.css"), + + new TaskItem("TestFiles/Pages/Profile.cshtml.css", new Dictionary{ ["Scoped"] = "false" }), + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.DiscoveredScopedCssInputs.Should().HaveCount(2); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsMultiThreadingTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsMultiThreadingTest.cs index 50b116d075b8..d8ef85b4a7a9 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsMultiThreadingTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsMultiThreadingTest.cs @@ -1,103 +1,311 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + + + + +[TestClass] + public class DiscoverPrecompressedAssetsMultiThreadingTest + + { - [Fact] + + + [TestMethod] + + public void ResolvesContentRootRelativeToTaskEnvironmentProjectDirectory_NotProcessCurrentDirectory() + + { + + var testRoot = Path.Combine(AppContext.BaseDirectory, nameof(DiscoverPrecompressedAssetsMultiThreadingTest), Guid.NewGuid().ToString("N")); + + var projectDir = Path.Combine(testRoot, "project"); + + var spawnDir = Path.Combine(testRoot, "decoy", "spawn"); + + Directory.CreateDirectory(projectDir); + + Directory.CreateDirectory(spawnDir); + + + + const string relativeContentRoot = "wwwroot"; + + var expectedContentRoot = Path.GetFullPath(Path.Combine(projectDir, relativeContentRoot)) + Path.DirectorySeparatorChar; + + var decoyContentRoot = Path.GetFullPath(Path.Combine(spawnDir, relativeContentRoot)) + Path.DirectorySeparatorChar; + + expectedContentRoot.Should().NotBe(decoyContentRoot, + + "the test setup must place project and decoy in different parents so the migration is actually exercised"); + + + + var baseIdentity = Path.Combine(projectDir, "wwwroot", "js", "site.js"); + + var compressedIdentity = baseIdentity + ".gz"; + + + + var originalCurrentDirectory = Directory.GetCurrentDirectory(); + + try + + { + + Directory.SetCurrentDirectory(spawnDir); + + + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new DiscoverPrecompressedAssets + + { + + BuildEngine = buildEngine.Object, + + TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir), + + CandidateAssets = + + [ + + CreateCandidate(baseIdentity, relativePath: "js/site.js", relativeContentRoot), + + CreateCandidate(compressedIdentity, relativePath: "js/site.js.gz", relativeContentRoot), + + ], + + }; + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + errorMessages.Should().BeEmpty(); + + task.DiscoveredCompressedAssets.Should().ContainSingle(); + + + + var discovered = task.DiscoveredCompressedAssets[0]; + + discovered.GetMetadata("ContentRoot").Should().Be(expectedContentRoot, + + "ContentRoot must be absolutized against TaskEnvironment.ProjectDirectory, not the process CWD"); + + discovered.GetMetadata("ContentRoot").Should().NotBe(decoyContentRoot); + + discovered.GetMetadata("RelatedAsset").Should().Be(baseIdentity); + + } + + finally + + { + + Directory.SetCurrentDirectory(originalCurrentDirectory); + + if (Directory.Exists(testRoot)) + + { + + Directory.Delete(testRoot, recursive: true); + + } + + } + + } + + + + private static ITaskItem CreateCandidate(string identity, string relativePath, string contentRoot) + + { + + var asset = new StaticWebAsset + + { + + Identity = identity, + + RelativePath = relativePath, + + BasePath = "_content/Test", + + AssetMode = StaticWebAsset.AssetModes.All, + + AssetKind = StaticWebAsset.AssetKinds.All, + + AssetMergeSource = string.Empty, + + SourceId = "Test", + + CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, + + Fingerprint = "fingerprint", + + RelatedAsset = string.Empty, + + ContentRoot = contentRoot, + + SourceType = StaticWebAsset.SourceTypes.Discovered, + + Integrity = "integrity", + + AssetRole = StaticWebAsset.AssetRoles.Primary, + + AssetMergeBehavior = string.Empty, + + AssetTraitValue = string.Empty, + + AssetTraitName = string.Empty, + + OriginalItemSpec = identity, + + CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest, + + FileLength = 10, + + LastWriteTime = DateTime.UtcNow, + + }; + + return asset.ToTaskItem(); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs index d9b5e6cc44d6..4b69e72ec96e 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs @@ -1,115 +1,347 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + + + + +[TestClass] + public class DiscoverPrecompressedAssetsTest + + { + + public string ItemSpec { get; } + + + + public string OriginalItemSpec { get; } + + + + public string OutputBasePath { get; } + + + + public DiscoverPrecompressedAssetsTest() + + { + + OutputBasePath = Path.Combine(SdkTestContext.Current.TestExecutionDirectory, nameof(ResolveCompressedAssetsTest)); + + ItemSpec = Path.Combine(OutputBasePath, Guid.NewGuid().ToString("N") + ".tmp"); + + OriginalItemSpec = Path.Combine(OutputBasePath, Guid.NewGuid().ToString("N") + ".tmp"); + + } - [Fact] + + + + + [TestMethod] + + public void DiscoversPrecompressedAssetsCorrectly() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var uncompressedCandidate = new StaticWebAsset + + { + + Identity = Path.Combine(Environment.CurrentDirectory, "wwwroot", "js", "site.js"), + + RelativePath = "js/site#[.{fingerprint}]?.js", + + BasePath = "_content/Test", + + AssetMode = StaticWebAsset.AssetModes.All, + + AssetKind = StaticWebAsset.AssetKinds.All, + + AssetMergeSource = string.Empty, + + SourceId = "Test", + + CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, + + Fingerprint = "uncompressed", + + RelatedAsset = string.Empty, + + ContentRoot = Path.Combine(Environment.CurrentDirectory,"wwwroot"), + + SourceType = StaticWebAsset.SourceTypes.Discovered, + + Integrity = "uncompressed-integrity", + + AssetRole = StaticWebAsset.AssetRoles.Primary, + + AssetMergeBehavior = string.Empty, + + AssetTraitValue = string.Empty, + + AssetTraitName = string.Empty, + + OriginalItemSpec = Path.Combine("wwwroot", "js", "site.js"), + + CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest, + + FileLength = 10, + + LastWriteTime = DateTime.UtcNow + + }; + + + + var compressedCandidate = new StaticWebAsset + + { + + Identity = Path.Combine(Environment.CurrentDirectory, "wwwroot", "js", "site.js.gz"), + + RelativePath = "js/site.js#[.{fingerprint}]?.gz", + + BasePath = "_content/Test", + + AssetMode = StaticWebAsset.AssetModes.All, + + AssetKind = StaticWebAsset.AssetKinds.All, + + AssetMergeSource = string.Empty, + + SourceId = "Test", + + CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, + + Fingerprint = "compressed", + + RelatedAsset = string.Empty, + + ContentRoot = Path.Combine(Environment.CurrentDirectory, "wwwroot"), + + SourceType = StaticWebAsset.SourceTypes.Discovered, + + Integrity = "compressed-integrity", + + AssetRole = StaticWebAsset.AssetRoles.Primary, + + AssetMergeBehavior = string.Empty, + + AssetTraitValue = string.Empty, + + AssetTraitName = string.Empty, + + OriginalItemSpec = Path.Combine("wwwroot", "js", "site.js.gz"), + + CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest, + + FileLength = 10, + + LastWriteTime = DateTime.UtcNow + + }; + + + + var task = new DiscoverPrecompressedAssets + + { + + CandidateAssets = [uncompressedCandidate.ToTaskItem(), compressedCandidate.ToTaskItem()], + + BuildEngine = buildEngine.Object + + }; + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + task.DiscoveredCompressedAssets.Should().ContainSingle(); + + var asset = task.DiscoveredCompressedAssets[0]; + + asset.ItemSpec.Should().Be(compressedCandidate.Identity); + + asset.GetMetadata("RelatedAsset").Should().Be(uncompressedCandidate.Identity); + + asset.GetMetadata("OriginalItemSpec").Should().Be(uncompressedCandidate.Identity); + + asset.GetMetadata("RelativePath").Should().Be("js/site#[.{fingerprint=uncompressed}]?.js.gz"); + + asset.GetMetadata("AssetRole").Should().Be("Alternative"); + + asset.GetMetadata("AssetTraitName").Should().Be("Content-Encoding"); + + asset.GetMetadata("AssetTraitValue").Should().Be("gzip"); + + asset.GetMetadata("Fingerprint").Should().Be("compressed"); + + asset.GetMetadata("Integrity").Should().Be("compressed-integrity"); + + asset.GetMetadata("CopyToPublishDirectory").Should().Be("PreserveNewest"); + + asset.GetMetadata("CopyToOutputDirectory").Should().Be("Never"); + + asset.GetMetadata("AssetMergeSource").Should().Be(string.Empty); + + asset.GetMetadata("AssetMergeBehavior").Should().Be(string.Empty); + + asset.GetMetadata("AssetKind").Should().Be("All"); + + asset.GetMetadata("AssetMode").Should().Be("All"); + + asset.GetMetadata("SourceId").Should().Be("Test"); + + asset.GetMetadata("SourceType").Should().Be("Discovered"); + + asset.GetMetadata("ContentRoot").Should().Be(Path.Combine(Environment.CurrentDirectory, $"wwwroot{Path.DirectorySeparatorChar}")); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverStaticWebAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverStaticWebAssetsTest.cs index f252022d1338..93cfdd321591 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverStaticWebAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverStaticWebAssetsTest.cs @@ -1,925 +1,2778 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { + + + [TestClass] + public class DiscoverStaticWebAssetsTest + + { + + private readonly Func _testResolveFileDetails = + + (string identity, string originalItemSpec) => (null, 10, new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero)); - [Fact] + + + + + [TestMethod] + + public void DiscoversMatchingAssetsBasedOnPattern() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + TestResolveFileDetails = _testResolveFileDetails, + + CandidateAssets = + + [ + + CreateCandidate(Path.Combine("wwwroot", "candidate.js")) + + ], + + RelativePathPattern = "wwwroot\\**", + + SourceType = "Discovered", + + SourceId = "MyProject", + + ContentRoot = "wwwroot", + + BasePath = "_content/Path" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); + + task.Assets.Length.Should().Be(1); + + var asset = task.Assets[0]; + + asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().Be("MyProject"); + + asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().Be("Discovered"); + + asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar); + + asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Path"); + + asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("candidate.js"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().Be("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().Be("Primary"); + + asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); + + asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", "candidate.js")); + + } - [Theory] - [InlineData("index.js", "index#[.{fingerprint}]?.js", "")] - [InlineData("css/site.css", "css/site#[.{fingerprint}]!.css", "#[.{fingerprint}]!")] + + + + + [TestMethod] + + + [DataRow("index.js", "index#[.{fingerprint}]?.js", "")] + + + [DataRow("css/site.css", "css/site#[.{fingerprint}]!.css", "#[.{fingerprint}]!")] + + public void FingerprintsContentWhenEnabled(string file, string expectedRelativePath, string expression) + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + TestResolveFileDetails = _testResolveFileDetails, + + CandidateAssets = + + [ + + CreateCandidate(Path.Combine("wwwroot", file)) + + ], + + RelativePathPattern = "wwwroot\\**", + + FingerprintCandidates = true, + + SourceType = "Discovered", + + SourceId = "MyProject", + + ContentRoot = "wwwroot", + + BasePath = "_content/Path" + + }; + + if (!string.IsNullOrEmpty(expression)) + + { + + task.FingerprintPatterns = [new TaskItem("CssFile", new Dictionary { ["Pattern"] = "*.css", ["Expression"] = expression })]; + + } + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); + + task.Assets.Length.Should().Be(1); + + var asset = task.Assets[0]; + + asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", file))); + + asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().Be("MyProject"); + + asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().Be("Discovered"); + + asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar); + + asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Path"); + + asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be(expectedRelativePath); + + asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().Be("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().Be("Primary"); + + asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); + + asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", file)); + + } - [Theory] - [InlineData("index.js")] - [InlineData("css/site.js")] + + + + + [TestMethod] + + + [DataRow("index.js")] + + + [DataRow("css/site.js")] + + public void DoesNotFingerprintsContentWhenNotEnabled(string candidate) + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + TestResolveFileDetails = _testResolveFileDetails, + + CandidateAssets = + + [ + + CreateCandidate(Path.Combine("wwwroot", candidate.Replace('/', Path.DirectorySeparatorChar))) + + ], + + RelativePathPattern = "wwwroot\\**", + + FingerprintCandidates = false, + + SourceType = "Discovered", + + SourceId = "MyProject", + + ContentRoot = "wwwroot", + + BasePath = "_content/Path" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); + + task.Assets.Length.Should().Be(1); + + var asset = task.Assets[0]; + + asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", candidate))); + + asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().Be("MyProject"); + + asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().Be("Discovered"); + + asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar); + + asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Path"); + + asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be(candidate); + + asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().Be("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().Be("Primary"); + + asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); + + asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", Path.Combine(candidate.Split('/')))); + + } - [Theory] - [InlineData("candidate.lib.module.js", "candidate#[.{fingerprint}]?.lib.module.js", "")] - [InlineData("library.candidate.lib.module.js", "library.candidate#[.{fingerprint}]!.lib.module.js", "#[.{fingerprint}]!")] + + + + + [TestMethod] + + + [DataRow("candidate.lib.module.js", "candidate#[.{fingerprint}]?.lib.module.js", "")] + + + [DataRow("library.candidate.lib.module.js", "library.candidate#[.{fingerprint}]!.lib.module.js", "#[.{fingerprint}]!")] + + public void FingerprintsContentUsingPatternsWhenMoreThanOneExtension(string fileName, string expectedRelativePath, string expression) + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + TestResolveFileDetails = _testResolveFileDetails, + + CandidateAssets = + + [ + + CreateCandidate(Path.Combine("wwwroot", fileName)) + + ], + + FingerprintPatterns = [new TaskItem("JsModule", new Dictionary { ["Pattern"] = "*.lib.module.js", ["Expression"] = expression })], + + FingerprintCandidates = true, + + RelativePathPattern = "wwwroot\\**", + + SourceType = "Discovered", + + SourceId = "MyProject", + + ContentRoot = "wwwroot", + + BasePath = "_content/Path" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); + + task.Assets.Length.Should().Be(1); + + var asset = task.Assets[0]; + + asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", fileName))); + + asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().Be("MyProject"); + + asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().Be("Discovered"); + + asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar); + + asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Path"); + + asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be(expectedRelativePath); + + asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().Be("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().Be("Primary"); + + asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); + + asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", fileName)); + + } - [Fact] - [Trait("Category", "FingerprintIdentity")] + + + + + [TestMethod] + + + [TestCategory("FingerprintIdentity")] + + public void ComputesIdentity_UsingFingerprintPattern_ForComputedAssets_WhenIdentityNeedsComputation() + + { + + // Arrange: simulate a packaged asset (outside content root) with a RelativePath inside the app + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + // Create a physical file to allow fingerprint computation (tests override ResolveFileDetails returning null file otherwise) + + var tempRoot = Path.Combine(Path.GetTempPath(), "swafp_identity_test"); + + var nugetPackagePath = Path.Combine(tempRoot, "microsoft.aspnetcore.components.webassembly", "10.0.0-rc.1.25451.107", "build", "net10.0"); + + Directory.CreateDirectory(nugetPackagePath); + + var assetFileName = "blazor.webassembly.js"; + + var assetFullPath = Path.Combine(nugetPackagePath, assetFileName); + + File.WriteAllText(assetFullPath, "console.log('test');"); + + // Relative path provided by the item (pre-fingerprinting) + + var relativePath = Path.Combine("_framework", assetFileName).Replace('\\', '/'); + + var contentRoot = Path.Combine("bin", "Release", "net10.0", "wwwroot"); + + + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + // Use default file resolution so the file we created is used for hashing. + + TestResolveFileDetails = null, + + CandidateAssets = + + [ + + new TaskItem(assetFullPath, new Dictionary + + { + + ["RelativePath"] = relativePath + + }) + + ], + + // No RelativePathPattern, we trigger the branch that synthesizes identity under content root. + + FingerprintPatterns = [ new TaskItem("Js", new Dictionary{{"Pattern","*.js"},{"Expression","#[.{fingerprint}]!"}})], + + FingerprintCandidates = true, + + SourceType = "Computed", + + SourceId = "Client", + + ContentRoot = contentRoot, + + BasePath = "/", + + AssetKind = StaticWebAsset.AssetKinds.All, + + AssetTraitName = "WasmResource", + + AssetTraitValue = "boot" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue($"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); + + task.Assets.Length.Should().Be(1); + + var asset = task.Assets[0]; + + + + // RelativePath should still contain the hard fingerprint pattern placeholder (not expanded yet) + + asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("_framework/blazor.webassembly#[.{fingerprint}]!.js"); + + + + // Identity must contain the ACTUAL fingerprint value in the file name (placeholder expanded) + + var actualFingerprint = asset.GetMetadata(nameof(StaticWebAsset.Fingerprint)); + + actualFingerprint.Should().NotBeNullOrEmpty(); + + var expectedIdentity = Path.GetFullPath(Path.Combine(contentRoot, "_framework", $"blazor.webassembly.{actualFingerprint}.js")); + + asset.ItemSpec.Should().Be(expectedIdentity); + + } - [Fact] + + + + + [TestMethod] + + public void RespectsItemRelativePathWhenExplicitlySpecified() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + TestResolveFileDetails = _testResolveFileDetails, + + CandidateAssets = + + [ + + CreateCandidate(Path.Combine("wwwroot", "candidate.js"), relativePath: "subdir/candidate.js") + + ], + + RelativePathPattern = "wwwroot\\**", + + SourceType = "Discovered", + + SourceId = "MyProject", + + ContentRoot = "wwwroot", + + BasePath = "_content/Path" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); + + task.Assets.Length.Should().Be(1); + + var asset = task.Assets[0]; + + asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().Be("MyProject"); + + asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().Be("Discovered"); + + asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar); + + asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Path"); + + asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("subdir/candidate.js"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().Be("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().Be("Primary"); + + asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); + + asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", "candidate.js")); + + } - [Fact] + + + + + [TestMethod] + + public void UsesTargetPathWhenFound() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + TestResolveFileDetails = _testResolveFileDetails, + + CandidateAssets = + + [ + + CreateCandidate(Path.Combine("wwwroot", "candidate.js"), targetPath: Path.Combine("wwwroot", "subdir", "candidate.publish.js")) + + ], + + RelativePathPattern = "wwwroot\\**", + + SourceType = "Discovered", + + SourceId = "MyProject", + + ContentRoot = "wwwroot", + + BasePath = "_content/Path" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); + + task.Assets.Length.Should().Be(1); + + var asset = task.Assets[0]; + + asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().Be("MyProject"); + + asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().Be("Discovered"); + + asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar); + + asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Path"); + + asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("subdir/candidate.publish.js"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().Be("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().Be("Primary"); + + asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); + + asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", "candidate.js")); + + } - [Fact] + + + + + [TestMethod] + + public void UsesLinkPathWhenFound() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + TestResolveFileDetails = _testResolveFileDetails, + + CandidateAssets = + + [ + + CreateCandidate(Path.Combine("wwwroot", "candidate.js"), link: Path.Combine("wwwroot", "subdir", "candidate.link.js")) + + ], + + RelativePathPattern = "wwwroot\\**", + + SourceType = "Discovered", + + SourceId = "MyProject", + + ContentRoot = "wwwroot", + + BasePath = "_content/Path" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); + + task.Assets.Length.Should().Be(1); + + var asset = task.Assets[0]; + + asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().Be("MyProject"); + + asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().Be("Discovered"); + + asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar); + + asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Path"); + + asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("subdir/candidate.link.js"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().Be("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().Be("Primary"); + + asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().Be(""); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); + + asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", "candidate.js")); + + } - [Fact] + + + + + [TestMethod] + + public void AutomaticallyDetectsAssetKindWhenMultipleAssetsTargetTheSameRelativePath() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + TestResolveFileDetails = _testResolveFileDetails, + + CandidateAssets = + + [ + + CreateCandidate(Path.Combine("wwwroot", "candidate.js"), copyToPublishDirectory: "Never"), + + CreateCandidate(Path.Combine("wwwroot", "candidate.publish.js"), relativePath: "candidate.js") + + ], + + RelativePathPattern = "wwwroot\\**", + + SourceType = "Discovered", + + SourceId = "MyProject", + + ContentRoot = "wwwroot", + + BasePath = "_content/Path" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); + + task.Assets.Length.Should().Be(2); + + var buildAsset = task.Assets.Single(a => a.ItemSpec == Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + var publishAsset = task.Assets.Single(a => a.ItemSpec == Path.GetFullPath(Path.Combine("wwwroot", "candidate.publish.js"))); + + buildAsset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + buildAsset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("Build"); + + buildAsset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); + + buildAsset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("Never"); + + + + publishAsset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.publish.js"))); + + publishAsset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("Publish"); + + publishAsset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); + + publishAsset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); + + } - [Theory] - [InlineData("Never", "Never", "Build", "Never", "Never", "Build")] - [InlineData("PreserveNewest", "PreserveNewest", "All", "PreserveNewest", "PreserveNewest", "All")] - [InlineData("Always", "Always", "All", "Always", "Always", "All")] - [InlineData("Never", "Always", "All", "Never", "Always", "All")] - [InlineData("Always", "Never", "Build", "Always", "Never", "Build")] + + + + + [TestMethod] + + + [DataRow("Never", "Never", "Build", "Never", "Never", "Build")] + + + [DataRow("PreserveNewest", "PreserveNewest", "All", "PreserveNewest", "PreserveNewest", "All")] + + + [DataRow("Always", "Always", "All", "Always", "Always", "All")] + + + [DataRow("Never", "Always", "All", "Never", "Always", "All")] + + + [DataRow("Always", "Never", "Build", "Always", "Never", "Build")] + + public void FailsDiscoveringAssetsWhenThereIsAConflict( + + string copyToOutputDirectoryFirst, + + string copyToPublishDirectoryFirst, + + string firstKind, + + string copyToOutputDirectorySecond, + + string copyToPublishDirectorySecond, + + string secondKind) + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + TestResolveFileDetails = _testResolveFileDetails, + + CandidateAssets = + + [ + + CreateCandidate( + + Path.Combine("wwwroot","candidate.js"), + + copyToOutputDirectory: copyToOutputDirectoryFirst, + + copyToPublishDirectory: copyToPublishDirectoryFirst), + + + + CreateCandidate( + + Path.Combine("wwwroot","candidate.publish.js"), + + relativePath: "candidate.js", + + copyToOutputDirectory: copyToOutputDirectorySecond, + + copyToPublishDirectory: copyToPublishDirectorySecond) + + ], + + RelativePathPattern = "wwwroot\\**", + + SourceType = "Discovered", + + SourceId = "MyProject", + + ContentRoot = "wwwroot", + + BasePath = "_content/Path" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(false); + + errorMessages.Count.Should().Be(1); + + errorMessages[0].Should().Be($@"Two assets found targeting the same path with incompatible asset kinds: + + '{Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))}' with kind '{firstKind}' + + '{Path.GetFullPath(Path.Combine("wwwroot", "candidate.publish.js"))}' with kind '{secondKind}' + + for path 'candidate.js'"); + + } - [Theory] - [InlineData("\\_content\\Path\\", "_content/Path")] - [InlineData("\\_content\\Path", "_content/Path")] - [InlineData("_content\\Path", "_content/Path")] - [InlineData("/_content/Path/", "_content/Path")] - [InlineData("/_content/Path", "_content/Path")] - [InlineData("_content/Path", "_content/Path")] - [InlineData("\\_content/Path\\", "_content/Path")] - [InlineData("/_content\\Path/", "_content/Path")] - [InlineData("", "/")] - [InlineData("/", "/")] - [InlineData("\\", "/")] + + + + + [TestMethod] + + + [DataRow("\\_content\\Path\\", "_content/Path")] + + + [DataRow("\\_content\\Path", "_content/Path")] + + + [DataRow("_content\\Path", "_content/Path")] + + + [DataRow("/_content/Path/", "_content/Path")] + + + [DataRow("/_content/Path", "_content/Path")] + + + [DataRow("_content/Path", "_content/Path")] + + + [DataRow("\\_content/Path\\", "_content/Path")] + + + [DataRow("/_content\\Path/", "_content/Path")] + + + [DataRow("", "/")] + + + [DataRow("/", "/")] + + + [DataRow("\\", "/")] + + public void NormalizesBasePath(string givenPath, string expectedPath) + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + TestResolveFileDetails = _testResolveFileDetails, + + CandidateAssets = + + [ + + CreateCandidate("wwwroot\\candidate.js") + + ], + + RelativePathPattern = "wwwroot\\**", + + SourceType = "Discovered", + + SourceId = "MyProject", + + ContentRoot = "wwwroot", + + BasePath = givenPath + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); + + task.Assets.Length.Should().Be(1); + + var asset = task.Assets[0]; + + asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be(expectedPath); + + } + + + + public static TheoryData NormalizesContentRootData + + { + + get + + { + + var currentPath = Path.GetFullPath("."); + + var result = new TheoryData + + { + + { "wwwroot", Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar }, + + { currentPath + Path.DirectorySeparatorChar + "wwwroot" + Path.DirectorySeparatorChar + "subdir", Path.GetFullPath("wwwroot/subdir") + Path.DirectorySeparatorChar }, + + { currentPath + Path.DirectorySeparatorChar + "wwwroot" + Path.DirectorySeparatorChar + "subdir" + Path.DirectorySeparatorChar, Path.GetFullPath("wwwroot/subdir") + Path.DirectorySeparatorChar }, + + { currentPath + Path.DirectorySeparatorChar + "wwwroot" + Path.DirectorySeparatorChar + "subdir" + Path.AltDirectorySeparatorChar, Path.GetFullPath("wwwroot/subdir") + Path.DirectorySeparatorChar }, + + { currentPath + Path.AltDirectorySeparatorChar + "wwwroot" + Path.AltDirectorySeparatorChar + "subdir", Path.GetFullPath("wwwroot/subdir") + Path.DirectorySeparatorChar }, + + { currentPath + Path.DirectorySeparatorChar + "wwwroot" + Path.AltDirectorySeparatorChar + "subdir", Path.GetFullPath("wwwroot/subdir") + Path.DirectorySeparatorChar }, + + { currentPath + Path.AltDirectorySeparatorChar + "wwwroot" + Path.DirectorySeparatorChar + "subdir", Path.GetFullPath("wwwroot/subdir") + Path.DirectorySeparatorChar } + + }; + + return result; + + } + + } - [Theory] - [MemberData(nameof(NormalizesContentRootData))] + + + + + [TestMethod] + + + [DynamicData(nameof(NormalizesContentRootData))] + + public void NormalizesContentRoot(string contentRoot, string expected) + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + TestResolveFileDetails = _testResolveFileDetails, + + CandidateAssets = + + [ + + CreateCandidate("wwwroot\\candidate.js") + + ], + + RelativePathPattern = "wwwroot\\**", + + SourceType = "Discovered", + + SourceId = "MyProject", + + ContentRoot = contentRoot, + + BasePath = "base" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); + + task.Assets.Length.Should().Be(1); + + var asset = task.Assets[0]; + + asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); + + asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(expected); + + } - [Fact] + + + + + [TestMethod] + + public void DefineStaticWebAssetsCache_UpToDate() + + { + + // Arrange + + var (cache, inputHashes) = SetupCache([], []); + + // Assert + + cache.Update([], [], [], inputHashes); + + + + // Assert - Assert.True(cache.IsUpToDate()); + + + Assert.IsTrue(cache.IsUpToDate()); + + } - [Fact] + + + + + [TestMethod] + + public void DefineStaticWebAssetsCache_UpToDate_WithAssets() + + { + + // Arrange + + var (cache, inputHashes) = SetupCache(["input1"], ["input1"]); + + + + // Act + + cache.Update([], [], [], inputHashes); - // Assert - Assert.True(cache.IsUpToDate()); - } - [Theory] - [InlineData(UpdatedHash.GlobalProperties)] - [InlineData(UpdatedHash.FingerprintPatterns)] - [InlineData(UpdatedHash.Overrides)] - public void DefineStaticWebAssetsCache_Recomputes_All_WhenPropertiesChange(UpdatedHash updated) - { - // Arrange - var (cache, inputHashes) = SetupCache(["input1", "input2"], ["input1", "input2"]); + + + + // Assert + + + Assert.IsTrue(cache.IsUpToDate()); + + + } + + + + + + [TestMethod] + + + [DataRow(UpdatedHash.GlobalProperties)] + + + [DataRow(UpdatedHash.FingerprintPatterns)] + + + [DataRow(UpdatedHash.Overrides)] + + + public void DefineStaticWebAssetsCache_Recomputes_All_WhenPropertiesChange(UpdatedHash updated) + + + { + + + // Arrange + + + var (cache, inputHashes) = SetupCache(["input1", "input2"], ["input1", "input2"]); + + + + // Act + + switch (updated) + + { + + case UpdatedHash.GlobalProperties: + + cache.Update([1], [], [], inputHashes); + + break; + + case UpdatedHash.FingerprintPatterns: + + cache.Update([], [1], [], inputHashes); + + break; + + case UpdatedHash.Overrides: + + cache.Update([], [], [1], inputHashes); + + break; + + } - Assert.False(cache.IsUpToDate()); - Assert.Same(inputHashes, cache.OutOfDateInputs()); - Assert.Empty(cache.CachedAssets); - Assert.Empty(cache.CachedCopyCandidates); + + + + + Assert.IsFalse(cache.IsUpToDate()); + + + Assert.AreSame(inputHashes, cache.OutOfDateInputs()); + + + Assert.IsEmpty(cache.CachedAssets); + + + Assert.IsEmpty(cache.CachedCopyCandidates); + + } - [Fact] + + + + + [TestMethod] + + public void DefineStaticWebAssetsCache_PartialUpdate_WhenOnlySome_InputsChange() + + { + + // Arrange + + var (cache, inputHashes) = SetupCache(["input1"], ["input2"], appendCachedToInputHashes: true); + + var cachedAsset = cache.CachedAssets.Values.Single(); + + + + // Act + + cache.Update([], [], [], inputHashes); + + + + // Assert - Assert.False(cache.IsUpToDate()); - Assert.NotSame(inputHashes, cache.OutOfDateInputs()); - var input1 = Assert.Single(cache.OutOfDateInputs()); + + + Assert.IsFalse(cache.IsUpToDate()); + + + Assert.AreNotSame(inputHashes, cache.OutOfDateInputs()); + + + var input1 = Assert.ContainsSingle(cache.OutOfDateInputs()); + + var ouput = cache.GetComputedOutputs(); - var input2 = Assert.Single(ouput.Assets); + + + var input2 = Assert.ContainsSingle(ouput.Assets); + + } - [Fact] + + + + + [TestMethod] + + public void DefineStaticWebAssetsCache_PartialUpdate_NewAssetsCanBeAddedToTheCache() + + { + + // Arrange + + var (cache, inputHashes) = SetupCache(["input1"], ["input2"], appendCachedToInputHashes: true); + + cache.Update([], [], [], inputHashes); + + + + // Act + + var newAssetItem = inputHashes["input1"]; + + var newAsset = new StaticWebAsset { Identity = newAssetItem.ItemSpec }; + + cache.AppendAsset("input1", newAsset, newAssetItem); + + + + // Assert - Assert.False(cache.IsUpToDate()); - Assert.NotSame(inputHashes, cache.OutOfDateInputs()); - var input1 = Assert.Single(cache.OutOfDateInputs()); + + + Assert.IsFalse(cache.IsUpToDate()); + + + Assert.AreNotSame(inputHashes, cache.OutOfDateInputs()); + + + var input1 = Assert.ContainsSingle(cache.OutOfDateInputs()); + + Assert.Contains("input1", cache.CachedAssets.Keys); + + + + var ouput = cache.GetComputedOutputs(); - Assert.Equal(2, ouput.Assets.Count); - Assert.Equal("input2", ouput.Assets[0].ItemSpec); - Assert.Equal("input1", ouput.Assets[1].ItemSpec); + + + Assert.HasCount(2, ouput.Assets); + + + Assert.AreEqual("input2", ouput.Assets[0].ItemSpec); + + + Assert.AreEqual("input1", ouput.Assets[1].ItemSpec); + + } - [Fact] + + + + + [TestMethod] + + public void DefineStaticWebAssetsCache_CanRoundtripManifest() + + { + + var manifestPath = Path.Combine(Environment.CurrentDirectory, "CanRoundtripManifest.json"); + + if (File.Exists(manifestPath)) + + { + + File.Delete(manifestPath); + + } + + try + + { + + var (cache, inputHashes) = SetupCache([], [], appendCachedToInputHashes: true, manifestPath: manifestPath); + + + + var cachedAsset = CreateCandidate(Path.Combine(Environment.CurrentDirectory, "Input2.txt"), "Input2.txt"); + + cache.InputHashes = ["input2"]; + + cache.CachedAssets["input2"] = new StaticWebAsset { Identity = cachedAsset.ItemSpec, RelativePath = "Input2.txt" }; + + inputHashes["input2"] = cachedAsset; + + + + var newAsset = CreateCandidate(Path.Combine(Environment.CurrentDirectory, "Input1.txt"), "Input1.txt"); + + inputHashes["input1"] = newAsset; + + + + cache.Update([], [], [], inputHashes); + + cache.AppendAsset("input1", new StaticWebAsset { Identity = newAsset.ItemSpec, RelativePath = "Input1.txt" }, newAsset); + + cache.WriteCacheManifest(); + + + + var otherManifest = DefineStaticWebAssets.DefineStaticWebAssetsCache.ReadOrCreateCache(CreateLogger(), manifestPath); - Assert.Equal(cache.InputHashes, otherManifest.InputHashes); - Assert.Equal(cache.CachedAssets.Count, otherManifest.CachedAssets.Count); - Assert.Equal(cache.CachedAssets["input2"].Identity, otherManifest.CachedAssets["input2"].Identity); - Assert.Equal(cache.CachedAssets["input2"].RelativePath, otherManifest.CachedAssets["input2"].RelativePath); - Assert.Equal(cache.CachedAssets["input1"].Identity, otherManifest.CachedAssets["input1"].Identity); - Assert.Equal(cache.CachedAssets["input1"].RelativePath, otherManifest.CachedAssets["input1"].RelativePath); + + + otherManifest.InputHashes.Should().BeEquivalentTo(cache.InputHashes); + + + Assert.HasCount(cache.CachedAssets.Count, otherManifest.CachedAssets); + + + Assert.AreEqual(cache.CachedAssets["input2"].Identity, otherManifest.CachedAssets["input2"].Identity); + + + Assert.AreEqual(cache.CachedAssets["input2"].RelativePath, otherManifest.CachedAssets["input2"].RelativePath); + + + Assert.AreEqual(cache.CachedAssets["input1"].Identity, otherManifest.CachedAssets["input1"].Identity); + + + Assert.AreEqual(cache.CachedAssets["input1"].RelativePath, otherManifest.CachedAssets["input1"].RelativePath); + + } + + finally + + { + + File.Delete(manifestPath); + + } + + } - [Fact] + + + + + [TestMethod] + + public void ComputesRelativePath_ForDiscoveredAssetsWithFullPath() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + buildEngine.SetupGet(e => e.ProjectFileOfTaskNode) + + .Returns(Path.Combine(Environment.CurrentDirectory, "Debug", "TestProject.csproj")); + + + + var debugDir = Path.Combine(Environment.CurrentDirectory, "Debug", "wwwroot"); + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [ + + new TaskItem(Path.Combine(debugDir, "Microsoft.AspNetCore.Components.CustomElements.lib.module.js"), + + new Dictionary{ ["Integrity"] = "integrity", ["Fingerprint"] = "fingerprint"}), + + new TaskItem(Path.Combine(debugDir, "Microsoft.AspNetCore.Components.CustomElements.lib.module.js.map"), + + new Dictionary{ ["Integrity"] = "integrity", ["Fingerprint"] = "fingerprint"}) + + ], + + RelativePathPattern = "wwwroot/**", + + SourceType = "Discovered", + + SourceId = "Microsoft.AspNetCore.Components.CustomElements", + + ContentRoot = debugDir, + + BasePath = "_content/Microsoft.AspNetCore.Components.CustomElements", + + TestResolveFileDetails = _testResolveFileDetails, + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue($"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); + + task.Assets.Length.Should().Be(2); + + task.Assets[0].GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("Microsoft.AspNetCore.Components.CustomElements.lib.module.js"); + + task.Assets[0].GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Microsoft.AspNetCore.Components.CustomElements"); + + task.Assets[1].GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("Microsoft.AspNetCore.Components.CustomElements.lib.module.js.map"); + + task.Assets[1].GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Microsoft.AspNetCore.Components.CustomElements"); + + } - [Fact] + + + + + [TestMethod] + + public void ComputesRelativePath_WorksForItemsWithRelativePaths() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + buildEngine.SetupGet(e => e.ProjectFileOfTaskNode) + + .Returns(Path.Combine(Environment.CurrentDirectory, "Debug", "TestProject.csproj")); + + + + var debugDir = Path.Combine(Environment.CurrentDirectory, "Debug", "wwwroot"); + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [ + + new TaskItem(Path.Combine("wwwroot", "Microsoft.AspNetCore.Components.CustomElements.lib.module.js"), + + new Dictionary{ ["Integrity"] = "integrity", ["Fingerprint"] = "fingerprint"}), + + new TaskItem(Path.Combine("wwwroot", "Microsoft.AspNetCore.Components.CustomElements.lib.module.js.map"), + + new Dictionary{ ["Integrity"] = "integrity", ["Fingerprint"] = "fingerprint"}) + + ], + + RelativePathPattern = "wwwroot/**", + + SourceType = "Discovered", + + SourceId = "Microsoft.AspNetCore.Components.CustomElements", + + ContentRoot = debugDir, + + BasePath = "_content/Microsoft.AspNetCore.Components.CustomElements", + + TestResolveFileDetails = _testResolveFileDetails, + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue($"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); + + task.Assets.Length.Should().Be(2); + + task.Assets[0].GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("Microsoft.AspNetCore.Components.CustomElements.lib.module.js"); + + task.Assets[0].GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Microsoft.AspNetCore.Components.CustomElements"); + + task.Assets[1].GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("Microsoft.AspNetCore.Components.CustomElements.lib.module.js.map"); + + task.Assets[1].GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Microsoft.AspNetCore.Components.CustomElements"); + + } - [LinuxOnlyFact] + + + + + [TestMethod] + [PlatformSpecific(TestPlatforms.Linux)] + + public void ComputesRelativePath_ForAssets_ExplicitPaths() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + buildEngine.SetupGet(e => e.ProjectFileOfTaskNode) + + .Returns("/home/user/work/Repo/Project/Project.csproj"); + + + + var task = new DefineStaticWebAssets + + { + + BuildEngine = buildEngine.Object, + + CandidateAssets = [ + + new TaskItem("/home/user/work/Repo/Project/Components/Dropdown/Dropdown.razor.js", + + new Dictionary{ ["Integrity"] = "integrity", ["Fingerprint"] = "fingerprint"}), + + ], + + RelativePathPattern = "**", + + SourceType = "Discovered", + + SourceId = "Project", + + ContentRoot = "/home/user/work/Repo/Project", + + BasePath = "_content/Project", + + TestResolveFileDetails = _testResolveFileDetails, + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue($"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); + + task.Assets.Length.Should().Be(1); + + task.Assets[0].GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("Components/Dropdown/Dropdown.razor.js"); + + task.Assets[0].GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Project"); + + task.Assets[0].GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be("/home/user/work/Repo/Project/"); + + } + + + + private static TaskLoggingHelper CreateLogger() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + var loggingHelper = new TaskLoggingHelper(buildEngine.Object, "DefineStaticWebAssets"); + + return loggingHelper; + + } + + + + private (DefineStaticWebAssets.DefineStaticWebAssetsCache cache, Dictionary inputHashes) SetupCache( + + string[] newAssets, + + string[] cached, + + bool appendCachedToInputHashes = false, + + string manifestPath = null) + + { + + var loggingHelper = CreateLogger(); + + var cache = DefineStaticWebAssets.DefineStaticWebAssetsCache.ReadOrCreateCache(loggingHelper, manifestPath); + + cache.InputHashes = [.. cached]; + + cache.CachedAssets = cached.ToDictionary(c => c, c => new StaticWebAsset { Identity = c }); + + + + return (cache, newAssets.Concat(appendCachedToInputHashes ? cached : []).ToDictionary(c => c, c => new TaskItem(c) as ITaskItem)); + + } + + + + public enum UpdatedHash + + { + + GlobalProperties, + + FingerprintPatterns, + + Overrides + + } + + + + private static ITaskItem CreateCandidate( + + string itemSpec, + + string relativePath = null, + + string targetPath = null, + + string link = null, + + string copyToOutputDirectory = null, + + string copyToPublishDirectory = null) + + { + + return new TaskItem(itemSpec, new Dictionary + + { + + ["RelativePath"] = relativePath ?? "", + + ["TargetPath"] = targetPath ?? "", + + ["Link"] = link ?? "", + + ["CopyToOutputDirectory"] = copyToOutputDirectory ?? "", + + ["CopyToPublishDirectory"] = copyToPublishDirectory ?? "", + + // Add these to avoid accessing the disk to compute them + + ["Integrity"] = "integrity", + + ["Fingerprint"] = "fingerprint", + + ["LastWriteTime"] = DateTime.UtcNow.ToString(StaticWebAsset.DateTimeAssetFormat), + + ["FileLength"] = "10", + + }); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetEndpointsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetEndpointsTest.cs index 2cf9ee746d4e..d6f8aae97dde 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetEndpointsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetEndpointsTest.cs @@ -1,316 +1,950 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; + + +[TestClass] + public class FilterStaticWebAssetEndpointsTest + + { - [Fact] + + + [TestMethod] + + public void CanFilterEndpoints_ByAssetFile() + + { + + 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 expectedEndpoints = new[] { + + endpoints[0], //index.css + + endpoints[1], //index.fingerprint.css + + endpoints[8], // other.html + + endpoints[10] // other.fingerprint.html + + }; + + Array.Sort(expectedEndpoints); + + var filterEndpointsTask = new FilterStaticWebAssetEndpoints() + + { + + Endpoints = endpoints.Select(endpoints => endpoints.ToTaskItem()).ToArray(), + + Assets = + + [ + + // index.css + + assets[0].ToTaskItem(), + + // other.html + + assets[4].ToTaskItem(), + + ], + + }; + + + + // Act + + var result = filterEndpointsTask.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var filteredEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterEndpointsTask.FilteredEndpoints); + + Array.Sort(filteredEndpoints); + + filteredEndpoints.Should().HaveCount(4); + + filteredEndpoints.Should().BeEquivalentTo(expectedEndpoints); + + } - [Fact] + + + + + [TestMethod] + + public void CanFilterEndpoints_ByProperty() + + { + + 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 filterEndpointsTask = new FilterStaticWebAssetEndpoints() + + { + + Endpoints = endpoints.Select(endpoints => endpoints.ToTaskItem()).ToArray(), + + Filters = [ + + new TaskItem("Property", new Dictionary{ + + ["Name"] = "fingerprint" + + }) + + ], + + BuildEngine = Mock.Of() + + }; + + + + // Act + + var result = filterEndpointsTask.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var filteredEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterEndpointsTask.FilteredEndpoints); + + Array.Sort(filteredEndpoints); + + filteredEndpoints.Should().HaveCount(6); + + filteredEndpoints.Should().AllSatisfy(e => e.EndpointProperties.Should().ContainSingle(p => p.Name == "fingerprint")); + + } - [Fact] + + + + + [TestMethod] + + public void CanFilterEndpoints_ByResponseHeader() + + { + + 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 filterEndpointsTask = new FilterStaticWebAssetEndpoints() + + { + + Endpoints = endpoints.Select(endpoints => endpoints.ToTaskItem()).ToArray(), + + Filters = [ + + new TaskItem("Header", new Dictionary{ + + ["Name"] = "Content-Type", + + ["Value"] = "text/html" + + }) + + ], + + BuildEngine = Mock.Of() + + }; + + + + // Act + + var result = filterEndpointsTask.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var filteredEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterEndpointsTask.FilteredEndpoints); + + Array.Sort(filteredEndpoints); + + filteredEndpoints.Should().HaveCount(4); + + filteredEndpoints.Should().AllSatisfy(e => e.ResponseHeaders.Should().ContainSingle(p => p.Name == "Content-Type" && p.Value == "text/html")); + + } - [Fact] + + + + + [TestMethod] + + public void CanFilterEndpoints_Standalone() + + { + + var assets = new[] { + + CreateAsset("index.html", relativePath: "index#[.{fingerprint}]?.html"), + + CreateAsset("other.js", relativePath: "other#[.{fingerprint}]!.js"), + + }; + + Array.Sort(assets, (l, r) => string.Compare(l.Identity, r.Identity, StringComparison.Ordinal)); + + + + var endpoints = CreateEndpoints(assets); + + var filterEndpointsTask = new FilterStaticWebAssetEndpoints() + + { + + Endpoints = endpoints.Select(endpoints => endpoints.ToTaskItem()).ToArray(), + + Assets = [.. assets.Select(a => a.ToTaskItem())], + + Filters = [ + + new TaskItem("Standalone", new Dictionary{ }) + + ], + + BuildEngine = Mock.Of() + + }; + + + + // Act + + var result = filterEndpointsTask.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var filteredEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterEndpointsTask.FilteredEndpoints); + + Array.Sort(filteredEndpoints); + + filteredEndpoints.Should().HaveCount(2); + + filteredEndpoints.Where(e => e.Route == "index.html").Should().ContainSingle(); + + filteredEndpoints.Where(e => e.Route == "other.fingerprint.js").Should().ContainSingle(); + + } - [Fact] + + + + + [TestMethod] + + public void CanFilterEndpoints_BySelector() + + { + + 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); + + endpoints[0].Selectors = [new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip" }]; + + var filterEndpointsTask = new FilterStaticWebAssetEndpoints() + + { + + Endpoints = endpoints.Select(endpoints => endpoints.ToTaskItem()).ToArray(), + + Filters = [ + + new TaskItem("Selector", new Dictionary{ + + ["Name"] = "Content-Encoding", + + ["Value"] = "gzip" + + }) + + ], + + BuildEngine = Mock.Of() + + }; + + + + // Act + + var result = filterEndpointsTask.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var filteredEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterEndpointsTask.FilteredEndpoints); + + Array.Sort(filteredEndpoints); + + filteredEndpoints.Should().ContainSingle(); + + filteredEndpoints[0].Route.Should().Be(endpoints[0].Route); + + } - [Fact] + + + + + [TestMethod] + + public void CanFilterEndpoints_ByMultipleCriteria() + + { + + 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 filterEndpointsTask = new FilterStaticWebAssetEndpoints() + + { + + Endpoints = endpoints.Select(endpoints => endpoints.ToTaskItem()).ToArray(), + + Filters = [ + + new TaskItem("Header", new Dictionary{ + + ["Name"] = "Content-Type", + + ["Value"] = "text/html" + + }), + + new TaskItem("Property", new Dictionary{ + + ["Name"] = "fingerprint" + + }) + + ], + + BuildEngine = Mock.Of() + + }; + + + + // Act + + var result = filterEndpointsTask.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var filteredEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterEndpointsTask.FilteredEndpoints); + + Array.Sort(filteredEndpoints); + + filteredEndpoints.Should().HaveCount(2); + + filteredEndpoints.Should().AllSatisfy(e => e.ResponseHeaders.Should().ContainSingle(p => p.Name == "Content-Type" && p.Value == "text/html")); + + filteredEndpoints.Should().AllSatisfy(e => e.EndpointProperties.Should().ContainSingle(p => p.Name == "fingerprint")); + + } + + + + 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(); + + + + defineStaticWebAssetEndpoints.Execute(); + + return StaticWebAssetEndpoint.FromItemGroup(defineStaticWebAssetEndpoints.Endpoints); + + } + + + + private static TaskItem CreateContentMapping(string pattern, string contentType) + + { + + return new TaskItem(contentType, new Dictionary + + { + + { "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 = DateTime.UtcNow, + + }; + + + + result.ApplyDefaults(); + + result.Normalize(); + + + + return result; + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetGroupsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetGroupsTest.cs index fdf35b4dc2e3..4001a52b6ffa 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetGroupsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetGroupsTest.cs @@ -1,312 +1,938 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + +[TestClass] + public class FilterStaticWebAssetGroupsTest : IDisposable + + { + + private readonly string _tempDir; + + private readonly Mock _buildEngine; + + private readonly List _errorMessages; + + private readonly List _logMessages; + + + + public FilterStaticWebAssetGroupsTest() + + { + + _tempDir = Path.Combine(Path.GetTempPath(), "FilterGroups_" + Guid.NewGuid().ToString("N")); + + Directory.CreateDirectory(_tempDir); + + + + _errorMessages = new List(); + + _logMessages = new List(); + + _buildEngine = new Mock(); + + _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => _errorMessages.Add(args.Message)); + + _buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => _logMessages.Add(args.Message)); + + } + + + + public void Dispose() + + { + + if (Directory.Exists(_tempDir)) + + { + + try { Directory.Delete(_tempDir, recursive: true); } catch { } + + } + + } - [Fact] + + + + + [TestMethod] + + public void ConcreteGroupSatisfied_AssetIncluded() + + { + + var asset1 = CreateAssetItem("app.js", "MyLib", ""); + + var asset2 = CreateAssetItem("site.css", "MyLib", "BootstrapVersion=V5"); + + + + var endpoint1 = CreateEndpointItem("app.js", asset1.ItemSpec); + + var endpoint2 = CreateEndpointItem("site.css", asset2.ItemSpec); + + + + var (task, result) = ExecuteFilterTask( + + new[] { asset1, asset2 }, + + new[] { endpoint1, endpoint2 }, + + new[] { CreateGroup("BootstrapVersion", "V5", "MyLib") }); + + + + result.Should().BeTrue(); + + task.FilteredAssets.Should().HaveCount(2, "all assets should pass through when groups are satisfied"); + + task.SurvivingEndpoints.Should().HaveCount(2); + + } - [Fact] + + + + + [TestMethod] + + public void ConcreteGroupUnsatisfied_AssetExcluded() + + { + + var asset = CreateAssetItem("server.js", "MyLib", "ServerRendering=true"); + + var endpoint = CreateEndpointItem("server.js", asset.ItemSpec); + + + + var (task, result) = ExecuteFilterTask( + + new[] { asset }, + + new[] { endpoint }, + + new[] { CreateGroup("ServerRendering", "false", "MyLib") }); // Value doesn't match + + + + result.Should().BeTrue(); + + task.FilteredAssets.Where(a => a != null).Should().HaveCount(0, "asset with unsatisfied group should be excluded"); + + task.SurvivingEndpoints.Should().HaveCount(0, "endpoint for excluded asset should be removed"); + + } - [Fact] + + + + + [TestMethod] + + public void DeferredGroupInFinalPass_Errors() + + { + + var asset = CreateAssetItem("server.js", "MyLib", "ServerRendering=true"); + + var endpoint = CreateEndpointItem("server.js", asset.ItemSpec); + + + + // SkipDeferred defaults to false (final pass) + + var (_, result) = ExecuteFilterTask( + + new[] { asset }, + + new[] { endpoint }, + + new[] { CreateGroup("ServerRendering", "true", "MyLib", deferred: true) }); + + + + result.Should().BeFalse("deferred groups in the final pass should produce an error"); + + _errorMessages.Should().ContainSingle() + + .Which.Should().Contain("Deferred"); + + } - [Fact] + + + + + [TestMethod] + + public void SkipDeferred_DeferredGroupsSkipped_AssetPassesThrough() + + { + + var asset = CreateAssetItem("server.js", "MyLib", "ServerRendering=true"); + + var endpoint = CreateEndpointItem("server.js", asset.ItemSpec); + + + + var (task, result) = ExecuteFilterTask( + + new[] { asset }, + + new[] { endpoint }, + + new[] { CreateGroup("ServerRendering", "false", "MyLib", deferred: true) }, // Would fail if evaluated + + skipDeferred: true); + + + + result.Should().BeTrue(); + + task.FilteredAssets.Should().HaveCount(1, "deferred groups should be skipped during pre-filter"); + + task.SurvivingEndpoints.Should().HaveCount(1); + + } - [Fact] + + + + + [TestMethod] + + public void CascadingExclusion_RelatedAssetsExcludedWithPrimary() + + { + + var primary = CreateAssetItem("server.js", "MyLib", "ServerRendering=true"); + + var related = CreateRelatedAssetItem("server.js.gz", "server.js.gz", "MyLib", primary); + + + + var primaryEndpoint = CreateEndpointItem("server.js", primary.ItemSpec); + + var relatedEndpoint = CreateEndpointItem("server.js.gz", related.ItemSpec); + + + + var (task, result) = ExecuteFilterTask( + + new[] { primary, related }, + + new[] { primaryEndpoint, relatedEndpoint }, + + new[] { CreateGroup("ServerRendering", "false", "MyLib") }); // Not satisfied + + + + result.Should().BeTrue(); + + task.FilteredAssets.Where(a => a != null).Should().HaveCount(0, "both primary and related should be excluded via cascading"); + + task.SurvivingEndpoints.Should().HaveCount(0, "endpoints for both excluded assets should be removed"); + + } - [Fact] + + + + + [TestMethod] + + public void CascadingExclusion_RelatedAssetPathResolvedAgainstTaskEnvironment() + + { + + var primary = CreateAssetItem("server.js", "MyLib", "ServerRendering=true"); + + var related = CreateRelatedAssetItem("server.js.gz", "server.js.gz", "MyLib", primary, relatedAsset: "server.js"); + + + + var primaryEndpoint = CreateEndpointItem("server.js", primary.ItemSpec); + + var relatedEndpoint = CreateEndpointItem("server.js.gz", related.ItemSpec); + + + + var (task, result) = ExecuteFilterTask( + + new[] { primary, related }, + + new[] { primaryEndpoint, relatedEndpoint }, + + new[] { CreateGroup("ServerRendering", "false", "MyLib") }, + + taskEnvironment: TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(_tempDir)); + + + + result.Should().BeTrue(); + + task.FilteredAssets.Where(a => a != null).Should().HaveCount(0, "the related asset path should resolve against the project directory"); + + task.SurvivingEndpoints.Should().HaveCount(0); + + } - [Fact] + + + + + [TestMethod] + + public void EndpointsFiltered_ForExcludedAssets() + + { + + var includedAsset = CreateAssetItem("app.js", "MyLib", ""); + + var excludedAsset = CreateAssetItem("server.js", "MyLib", "ServerRendering=true"); + + + + var includedEndpoint = CreateEndpointItem("app.js", includedAsset.ItemSpec); + + var excludedEndpoint = CreateEndpointItem("server.js", excludedAsset.ItemSpec); + + + + var (task, result) = ExecuteFilterTask( + + new[] { includedAsset, excludedAsset }, + + new[] { includedEndpoint, excludedEndpoint }, + + new[] { CreateGroup("ServerRendering", "false", "MyLib") }); + + + + result.Should().BeTrue(); + + var nonNullAssets = task.FilteredAssets.Where(a => a != null).ToArray(); + + nonNullAssets.Should().HaveCount(1); + + nonNullAssets[0].ItemSpec.Should().Be(includedAsset.ItemSpec); + + task.SurvivingEndpoints.Should().HaveCount(1); + + task.SurvivingEndpoints[0].ItemSpec.Should().Be("app.js"); + + } - [Fact] + + + + + [TestMethod] + + public void SkipDeferred_NonDeferredGroupsStillEvaluated() + + { + + // An asset has both a non-deferred group requirement and a deferred group requirement. + + // In SkipDeferred mode, only the non-deferred group is evaluated. + + var asset = CreateAssetItem("site.css", "MyLib", "BootstrapVersion=V5;ServerRendering=true"); + + + + var endpoint = CreateEndpointItem("site.css", asset.ItemSpec); + + + + var (task, result) = ExecuteFilterTask( + + new[] { asset }, + + new[] { endpoint }, + + new[] + + { + + CreateGroup("BootstrapVersion", "V5", "MyLib"), + + CreateGroup("ServerRendering", "true", "MyLib", deferred: true) + + }, + + skipDeferred: true); + + + + result.Should().BeTrue(); + + task.FilteredAssets.Should().HaveCount(1, + + "non-deferred BootstrapVersion is satisfied; deferred ServerRendering is skipped"); + + } - [Fact] + + + + + [TestMethod] + + public void NullStaticWebAssetGroups_PassesThrough() + + { + + var asset = CreateAssetItem("app.js", "MyLib", ""); + + var endpoint = CreateEndpointItem("app.js", asset.ItemSpec); + + + + var (task, result) = ExecuteFilterTask( + + new[] { asset }, + + new[] { endpoint }); + + + + result.Should().BeTrue(); + + task.FilteredAssets.Should().HaveCount(1); + + task.SurvivingEndpoints.Should().HaveCount(1); + + } + + + + private static ITaskItem CreateGroup(string name, string value, string sourceId, bool deferred = false) + + { + + var dict = new Dictionary + + { + + ["Value"] = value, + + ["SourceId"] = sourceId, + + }; + + if (deferred) + + dict["Deferred"] = "true"; + + return new TaskItem(name, dict); + + } + + + + private (FilterStaticWebAssetGroups Task, bool Result) ExecuteFilterTask( + + ITaskItem[] assets, + + ITaskItem[] endpoints, + + ITaskItem[] groups = null, + + bool skipDeferred = false, + + TaskEnvironment taskEnvironment = null) + + { + + var task = new FilterStaticWebAssetGroups + + { + + BuildEngine = _buildEngine.Object, + + TaskEnvironment = taskEnvironment ?? TaskEnvironment.Fallback, + + Assets = assets, + + Endpoints = endpoints, + + SkipDeferred = skipDeferred, + + StaticWebAssetGroups = groups, + + }; + + var result = task.Execute(); + + return (task, result); + + } + + + + private ITaskItem CreateRelatedAssetItem(string fileName, string relativePath, string sourceId, ITaskItem primaryAsset, string traitName = "Content-Encoding", string traitValue = "gzip", string relatedAsset = null) + + { + + var filePath = Path.Combine(_tempDir, fileName); + + if (!File.Exists(filePath)) + + { + + File.WriteAllText(filePath, "content-" + fileName); + + } + + + + return new TaskItem(filePath, new Dictionary + + { + + ["SourceType"] = "Package", + + ["SourceId"] = sourceId, + + ["ContentRoot"] = _tempDir + Path.DirectorySeparatorChar, + + ["BasePath"] = "_content/" + sourceId.ToLowerInvariant(), + + ["RelativePath"] = relativePath, + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Alternative", + + ["RelatedAsset"] = relatedAsset ?? primaryAsset.ItemSpec, + + ["AssetTraitName"] = traitName, + + ["AssetTraitValue"] = traitValue, + + ["AssetGroups"] = "", + + ["Fingerprint"] = "test", + + ["Integrity"] = "sha256-test", + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["OriginalItemSpec"] = filePath, + + }); + + } + + + + private ITaskItem CreateAssetItem(string fileName, string sourceId, string assetGroups) + + { + + var filePath = Path.Combine(_tempDir, fileName); + + if (!File.Exists(filePath)) + + { + + File.WriteAllText(filePath, "content-" + fileName); + + } + + + + return new TaskItem(filePath, new Dictionary + + { + + ["SourceType"] = "Package", + + ["SourceId"] = sourceId, + + ["ContentRoot"] = _tempDir + Path.DirectorySeparatorChar, + + ["BasePath"] = "_content/" + sourceId.ToLowerInvariant(), + + ["RelativePath"] = fileName, + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["AssetGroups"] = assetGroups, + + ["Fingerprint"] = "test", + + ["Integrity"] = "sha256-test", + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["OriginalItemSpec"] = filePath, + + }); + + } + + + + private static ITaskItem CreateEndpointItem(string route, string assetFile) + + { + + return new TaskItem(route, new Dictionary + + { + + ["AssetFile"] = assetFile, + + ["Selectors"] = "[]", + + ["ResponseHeaders"] = "[]", + + ["EndpointProperties"] = "[]", + + }); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FingerprintPatternMatcherTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FingerprintPatternMatcherTest.cs index 964c3086cce6..026c45c12ccc 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FingerprintPatternMatcherTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FingerprintPatternMatcherTest.cs @@ -1,128 +1,386 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; + + + + +[TestClass] + public class FingerprintPatternMatcherTest + + { + + private readonly TaskLoggingHelper _log = new TestTaskLoggingHelper(); - [Fact] + + + + + [TestMethod] + + public void AppendFingerprintPattern_AlreadyContainsFingerprint_ReturnsIdentity() + + { + + // Arrange + + var relativePath = "test#[.{fingerprint}].txt"; + + + + // Act + + var result = new FingerprintPatternMatcher(_log, []).AppendFingerprintPattern(CreateMatchContext(relativePath), "Identity"); + + + + // Assert - Assert.Equal(relativePath, result); + + + Assert.AreEqual(relativePath, result); + + } - [Fact] + + + + + [TestMethod] + + public void AppendFingerprintPattern_AppendsPattern_AtTheEndOfTheFileName() + + { + + // Arrange + + var relativePath = Path.Combine("folder", "test.txt"); + + var expected = Path.Combine("folder", "test#[.{fingerprint}]?.txt"); + + + + // Act + + var result = new FingerprintPatternMatcher(_log, []).AppendFingerprintPattern(CreateMatchContext(relativePath), "Identity"); + + + + // Assert - Assert.Equal(expected, result); + + + Assert.AreEqual(expected, result); + + } - [Fact] + + + + + [TestMethod] + + public void AppendFingerprintPattern_AppendsPattern_AtTheEndOfTheFileName_WhenFileNameContainsDots() + + { + + // Arrange + + var relativePath = Path.Combine("folder", "test.v1.txt"); + + var expected = Path.Combine("folder", "test.v1#[.{fingerprint}]?.txt"); + + // Act + + var result = new FingerprintPatternMatcher(_log, []).AppendFingerprintPattern(CreateMatchContext(relativePath), "Identity"); + + // Assert - Assert.Equal(expected, result); + + + Assert.AreEqual(expected, result); + + } - [Fact] + + + + + [TestMethod] + + public void AppendFingerprintPattern_AppendsPattern_AtTheEndOfTheFileName_WhenFileDoesNotHaveExtension() + + { + + // Arrange + + var relativePath = Path.Combine("folder", "README"); + + var expected = Path.Combine("folder", "README#[.{fingerprint}]?"); + + // Act + + var result = new FingerprintPatternMatcher(_log, []).AppendFingerprintPattern(CreateMatchContext(relativePath), "Identity"); + + // Assert - Assert.Equal(expected, result); + + + Assert.AreEqual(expected, result); + + } - [Fact] + + + + + [TestMethod] + + public void AppendFingerprintPattern_AppendsPattern_AtTheRightLocation_WhenACustomPatternIsProvided() + + { + + // Arrange + + var relativePath = Path.Combine("folder", "test.bundle.scp.css"); + + var expected = Path.Combine("folder", "test#[.{fingerprint}]!.bundle.scp.css"); + + + + // Act + + var result = new FingerprintPatternMatcher( + + _log, + + [new TaskItem("ScopedCSS", new Dictionary { ["Pattern"] = "*.bundle.scp.css", ["Expression"] = "#[.{fingerprint}]!" })]) + + .AppendFingerprintPattern(CreateMatchContext(relativePath), "Identity"); + + + + // Assert - Assert.Equal(expected, result); + + + Assert.AreEqual(expected, result); + + } + + + + private StaticWebAssetGlobMatcher.MatchContext CreateMatchContext(string path) + + { + + var context = new StaticWebAssetGlobMatcher.MatchContext(); + + context.SetPathAndReinitialize(path); + + return context; + + } + + + + private class TestTaskLoggingHelper : TaskLoggingHelper + + { + + public TestTaskLoggingHelper() : base(new TestTask()) + + { + + } + + + + private class TestTask : ITask + + { + + public IBuildEngine BuildEngine { get; set; } = new TestBuildEngine(); + + public ITaskHost HostObject { get; set; } = new TestTaskHost(); + + + + public bool Execute() => true; + + } + + + + private class TestBuildEngine : IBuildEngine + + { + + public bool ContinueOnError => true; + + + + public int LineNumberOfTaskNode => 0; + + + + public int ColumnNumberOfTaskNode => 0; + + + + public string ProjectFileOfTaskNode => "test.csproj"; + + + + public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => true; + + + + public void LogCustomEvent(CustomBuildEventArgs e) { } + + public void LogErrorEvent(BuildErrorEventArgs e) { } + + public void LogMessageEvent(BuildMessageEventArgs e) { } + + public void LogWarningEvent(BuildWarningEventArgs e) { } + + } + + + + private class TestTaskHost : ITaskHost + + { + + public object HostObject { get; set; } = new object(); + + } + + } + + + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsManifestFileTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsManifestFileTest.cs index 1840d0fe9c66..74d939ea87d8 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsManifestFileTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsManifestFileTest.cs @@ -1,385 +1,1157 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Text.Json; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + +[TestClass] + public class GeneratePackageAssetsManifestFileTest : IDisposable + + { + + private readonly string _tempDir; + + private readonly Mock _buildEngine; + + private readonly List _errorMessages; + + + + public GeneratePackageAssetsManifestFileTest() + + { + + _tempDir = Path.Combine(Path.GetTempPath(), "GenPkgManifest_" + Guid.NewGuid().ToString("N")); + + Directory.CreateDirectory(_tempDir); + + + + _errorMessages = new List(); + + _buildEngine = new Mock(); + + _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => _errorMessages.Add(args.Message)); + + _buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())); + + } + + + + public void Dispose() + + { + + if (Directory.Exists(_tempDir)) + + { + + try { Directory.Delete(_tempDir, recursive: true); } catch { } + + } + + } - [Fact] + + + + + [TestMethod] + + public void EmptyAssets_DoesNotGenerateManifestFile() + + { + + var manifestPath = Path.Combine(_tempDir, "empty.json"); + + + + var task = new GeneratePackageAssetsManifestFile + + { + + BuildEngine = _buildEngine.Object, + + StaticWebAssets = Array.Empty(), + + StaticWebAssetEndpoints = Array.Empty(), + + TargetManifestPath = manifestPath, + + }; + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + File.Exists(manifestPath).Should().BeFalse(); + + } - [Fact] + + + + + [TestMethod] + + public void Assets_SerializedWithCorrectPackagePaths() + + { + + var file = CreateTempFile("wwwroot", "css", "site.css", "body{}"); + + var contentRoot = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; + + + + var asset = CreateAsset(file, contentRoot, "css/site#[.{fingerprint}]?.css", "abc123"); + + + + var task = CreateManifestTask(new[] { asset.ToTaskItem() }); + + task.Execute().Should().BeTrue(); + + var manifest = DeserializeManifest(task.TargetManifestPath); + + + + manifest.Assets.Should().HaveCount(1); + + var manifestAsset = manifest.Assets.Values.Single(); + + var packagePath = manifest.Assets.Keys.Single(); + + + + // Discovered assets don't include BasePath in the target path + + packagePath.Should().EndWith("css/site.css"); + + manifestAsset.RelativePath.Should().Be("css/site#[.{fingerprint}]?.css"); + + manifestAsset.AssetRole.Should().Be("Primary"); + + } - [Fact] + + + + + [TestMethod] + + public void RelatedAsset_RemappedToPackageRelativePath() + + { + + var primaryFile = CreateTempFile("wwwroot", "css", "site.css", "body{}"); + + var relatedFile = CreateTempFile("wwwroot", "css", "site.css.gz", "compressed"); + + var contentRoot = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; + + + + var primary = CreateAsset(primaryFile, contentRoot, "css/site.css", "abc"); + + + + var related = CreateAsset(relatedFile, contentRoot, "css/site.css.gz", "def"); + + related.AssetRole = "Alternative"; + + related.RelatedAsset = primaryFile; + + related.AssetTraitName = "Content-Encoding"; + + related.AssetTraitValue = "gzip"; + + + + var task = CreateManifestTask( + + new[] { primary.ToTaskItem(), related.ToTaskItem() }); + + task.Execute().Should().BeTrue(); + + var manifest = DeserializeManifest(task.TargetManifestPath); + + + + manifest.Assets.Should().HaveCount(2); + + + + var relatedAsset = manifest.Assets.Values.First(a => a.AssetRole == "Alternative"); + + // The RelatedAsset should be remapped from the absolute path to a package-relative path + + relatedAsset.RelatedAsset.Should().NotBe(primaryFile); + + relatedAsset.RelatedAsset.Should().NotBeNullOrEmpty(); + + // It should match the primary's PackagePath + + var primaryAssetPath = manifest.Assets.First(kvp => kvp.Value.AssetRole == "Primary").Key; + + relatedAsset.RelatedAsset.Should().Be(primaryAssetPath); + + } - [Fact] + + + + + [TestMethod] + + public void Endpoints_AssetFileRemappedToPackageRelativePath() + + { + + var file = CreateTempFile("wwwroot", "js", "app.js", "var x;"); + + var contentRoot = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; + + + + var asset = CreateAsset(file, contentRoot, "js/app.js", "abc"); + + + + var endpoint = new StaticWebAssetEndpoint + + { + + Route = "_content/mylib/js/app.js", + + AssetFile = file, + + Selectors = [], + + ResponseHeaders = [new() { Name = "Content-Type", Value = "text/javascript" }], + + EndpointProperties = [], + + }; + + + + var task = CreateManifestTask( + + new[] { asset.ToTaskItem() }, + + StaticWebAssetEndpoint.ToTaskItems(new[] { endpoint })); + + task.Execute().Should().BeTrue(); + + var manifest = DeserializeManifest(task.TargetManifestPath); + + + + manifest.Endpoints.Should().HaveCount(1); + + var ep = manifest.Endpoints[0]; + + // AssetFile should be remapped from absolute to package-relative + + ep.AssetFile.Should().NotBe(file); + + ep.AssetFile.Should().Be(manifest.Assets.Keys.Single()); + + } - [Fact] + + + + + [TestMethod] + + public void FrameworkPattern_TagsMatchingAssetsAsFramework() + + { + + var fwFile = CreateTempFile("wwwroot", "js", "framework.js", "fw"); + + var nonFwFile = CreateTempFile("wwwroot", "js", "app.js", "app"); + + var contentRoot = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; + + + + var fwAsset = CreateAsset(fwFile, contentRoot, "js/framework.js", "abc"); + + var nonFwAsset = CreateAsset(nonFwFile, contentRoot, "js/app.js", "def"); + + + + var task = CreateManifestTask( + + new[] { fwAsset.ToTaskItem(), nonFwAsset.ToTaskItem() }, + + frameworkPattern: "js/framework*"); + + task.Execute().Should().BeTrue(); + + var manifest = DeserializeManifest(task.TargetManifestPath); + + + + manifest.Assets.Should().HaveCount(2); + + + + var fwManifestAsset = manifest.Assets.Values.First(a => a.RelativePath == "js/framework.js"); + + fwManifestAsset.SourceType.Should().Be("Framework"); + + + + var nonFwManifestAsset = manifest.Assets.Values.First(a => a.RelativePath == "js/app.js"); + + nonFwManifestAsset.SourceType.Should().Be("Package"); + + } - [Fact] + + + + + [TestMethod] + + public void AssetGroups_PreservedInManifest() + + { + + var file = CreateTempFile("wwwroot", "css", "site.css", "body{}"); + + var contentRoot = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; + + + + var asset = CreateAsset(file, contentRoot, "css/site.css", "abc"); + + asset.AssetGroups = "BootstrapVersion=V5"; + + + + var task = CreateManifestTask(new[] { asset.ToTaskItem() }); + + task.Execute().Should().BeTrue(); + + var manifest = DeserializeManifest(task.TargetManifestPath); + + + + manifest.Assets.Should().HaveCount(1); + + manifest.Assets.Values.Single().AssetGroups.Should().Be("BootstrapVersion=V5"); + + } - [Fact] + + + + + [TestMethod] + + public void RelatedAsset_Unmapped_ProducesError() + + { + + // Symmetric to Endpoints_UnmappedAssetFile_ProducesError: exercises the + + // GeneratePackageAssetsManifestFile.cs error branch for RelatedAsset that + + // can't be remapped to a package-relative path (i.e., points outside the + + // packaged asset set). Without this test the RelatedAsset error branch is + + // unexercised by automated tests. + + var primaryFile = CreateTempFile("wwwroot", "css", "site.css", "body{}"); + + var relatedFile = CreateTempFile("wwwroot", "css", "site.css.gz", "gz"); + + var contentRoot = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; + + + + var primary = CreateAsset(primaryFile, contentRoot, "css/site.css", "abc"); + + + + var related = CreateAsset(relatedFile, contentRoot, "css/site.css.gz", "def"); + + related.AssetRole = "Alternative"; + + related.AssetTraitName = "Content-Encoding"; + + related.AssetTraitValue = "gzip"; + + // RelatedAsset points to a file that is NOT in the StaticWebAssets input set, + + // so it has no entry in identityToPackagePath and cannot be remapped. + + related.RelatedAsset = Path.Combine(_tempDir, "nonexistent", "primary.css"); + + + + var task = CreateManifestTask( + + new[] { primary.ToTaskItem(), related.ToTaskItem() }); + + var result = task.Execute(); + + + + result.Should().BeFalse(); + + _errorMessages.Should().ContainSingle(m => + + m.Contains("could not be mapped to a package-relative path") && + + m.Contains("RelatedAsset")); + + File.Exists(task.TargetManifestPath).Should().BeFalse( + + "the manifest must not be written when a referential integrity error is detected"); + + } - [Fact] + + + + + [TestMethod] + + public void RoundTrip_GenerateThenRead_RelatedAssetResolvesToConsumerAbsolutePath() + + { + + // End-to-end cross-boundary test. Proves the contract between + + // GeneratePackageAssetsManifestFile (producer) and ReadPackageAssetsManifest + + // (consumer): an absolute build-time RelatedAsset is remapped to a + + // package-relative form on the producer side, then re-resolved to an + + // absolute path under the consumer's packageRoot on the consumer side. + + // Two distinct directory trees stand in for producer and consumer machines. + + var primaryFile = CreateTempFile("source", "wwwroot", "css", "site.css", "body{}"); + + var relatedFile = CreateTempFile("source", "wwwroot", "css", "site.css.gz", "gz"); + + var contentRoot = Path.Combine(_tempDir, "source", "wwwroot") + Path.DirectorySeparatorChar; + + + + var primary = CreateAsset(primaryFile, contentRoot, "css/site.css", "abc"); + + var related = CreateAsset(relatedFile, contentRoot, "css/site.css.gz", "def"); + + related.AssetRole = "Alternative"; + + related.AssetTraitName = "Content-Encoding"; + + related.AssetTraitValue = "gzip"; + + related.RelatedAsset = primaryFile; + + + + // Producer side: write the manifest at the layout ReadPackageAssetsManifest expects: + + // packageRoot/build/.PackageAssets.json + + var packageRoot = Path.Combine(_tempDir, "packages", "MyLib"); + + var buildDir = Path.Combine(packageRoot, "build"); + + Directory.CreateDirectory(buildDir); + + var manifestPath = Path.Combine(buildDir, "MyLib.PackageAssets.json"); + + + + var generateTask = new GeneratePackageAssetsManifestFile + + { + + BuildEngine = _buildEngine.Object, + + StaticWebAssets = new[] { primary.ToTaskItem(), related.ToTaskItem() }, + + StaticWebAssetEndpoints = Array.Empty(), + + TargetManifestPath = manifestPath, + + }; + + generateTask.Execute().Should().BeTrue(); + + File.Exists(manifestPath).Should().BeTrue(); + + + + // Consumer side: feed the producer's manifest into ReadPackageAssetsManifest + + // pretending we're on a different machine (different packageRoot than the + + // producer's contentRoot). The whole point of producer-side package-relative + + // remap is that the consumer can re-anchor without knowing the producer's CWD. + + var consumerContentRoot = Path.Combine(packageRoot, "staticwebassets") + Path.DirectorySeparatorChar; + + var manifestItem = new TaskItem(manifestPath, new Dictionary + + { + + ["SourceId"] = "MyLib", + + ["ContentRoot"] = consumerContentRoot, + + ["PackageRoot"] = packageRoot, + + }); + + + + var readTask = new ReadPackageAssetsManifest + + { + + BuildEngine = _buildEngine.Object, + + PackageManifests = new[] { manifestItem }, + + StaticWebAssetGroups = Array.Empty(), + + IntermediateOutputPath = Path.Combine(_tempDir, "obj"), + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + readTask.Execute().Should().BeTrue(); + + readTask.Assets.Should().HaveCount(2); + + + + var emittedRelated = readTask.Assets.Single(a => a.GetMetadata("AssetRole") == "Alternative"); + + var emittedPrimary = readTask.Assets.Single(a => a.GetMetadata("AssetRole") == "Primary"); + + + + // The producer's contentRoot is _tempDir/source/wwwroot — the consumer must + + // not see any leakage of it. RelatedAsset on the consumer side must be the + + // primary's Identity (absolute path under the consumer's packageRoot). + + emittedRelated.GetMetadata("RelatedAsset").Should().Be(emittedPrimary.ItemSpec, + + "consumer's RelatedAsset must equal primary's Identity after package-root re-resolution"); + + emittedRelated.GetMetadata("RelatedAsset").Should().StartWith(packageRoot, + + "RelatedAsset must be re-anchored to the consumer's packageRoot, not the producer's contentRoot"); + + emittedRelated.GetMetadata("RelatedAsset").Should().NotContain( + + Path.Combine(_tempDir, "source"), + + "no producer-side build-time path may leak through the manifest to the consumer"); + + } - [Fact] + + + + + [TestMethod] + + public void Endpoints_UnmappedAssetFile_ProducesError() + + { + + var file = CreateTempFile("wwwroot", "js", "app.js", "var x;"); + + var contentRoot = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; + + + + var asset = CreateAsset(file, contentRoot, "js/app.js", "abc"); + + + + var endpoint = new StaticWebAssetEndpoint + + { + + Route = "_content/mylib/js/missing.js", + + AssetFile = Path.Combine(_tempDir, "nonexistent", "missing.js"), + + Selectors = [], + + ResponseHeaders = [], + + EndpointProperties = [], + + }; + + + + var task = CreateManifestTask( + + new[] { asset.ToTaskItem() }, + + StaticWebAssetEndpoint.ToTaskItems(new[] { endpoint })); + + var result = task.Execute(); + + + + result.Should().BeFalse(); + + _errorMessages.Should().ContainSingle(m => m.Contains("could not be mapped to a package-relative path")); + + } + + + + private GeneratePackageAssetsManifestFile CreateManifestTask( + + ITaskItem[] assets, + + ITaskItem[] endpoints = null, + + string frameworkPattern = null) + + { + + var task = new GeneratePackageAssetsManifestFile + + { + + BuildEngine = _buildEngine.Object, + + StaticWebAssets = assets, + + StaticWebAssetEndpoints = endpoints ?? Array.Empty(), + + TargetManifestPath = Path.Combine(_tempDir, "manifest.json"), + + }; + + + + if (frameworkPattern != null) + + task.FrameworkPattern = frameworkPattern; + + + + return task; + + } + + + + private static StaticWebAssetPackageManifest DeserializeManifest(string manifestPath) + + { + + return JsonSerializer.Deserialize( + + File.ReadAllBytes(manifestPath), + + StaticWebAssetsJsonSerializerContext.Default.StaticWebAssetPackageManifest); + + } + + + + private string CreateTempFile(params string[] pathParts) + + { + + var content = pathParts[^1]; + + var segments = pathParts[..^1]; + + + + var dir = Path.Combine(new[] { _tempDir }.Concat(segments[..^1]).ToArray()); + + Directory.CreateDirectory(dir); + + var filePath = Path.Combine(dir, segments[^1]); + + File.WriteAllText(filePath, content); + + return filePath; + + } + + + + private StaticWebAsset CreateAsset(string filePath, string contentRoot, string relativePath, string fingerprint) + + { + + var asset = new StaticWebAsset + + { + + Identity = filePath, + + SourceType = "Discovered", + + SourceId = "MyLib", + + ContentRoot = contentRoot, + + BasePath = "_content/mylib", + + RelativePath = relativePath, + + AssetKind = "All", + + AssetMode = "All", + + AssetRole = "Primary", + + RelatedAsset = "", + + AssetTraitName = "", + + AssetTraitValue = "", + + CopyToOutputDirectory = "Never", + + CopyToPublishDirectory = "PreserveNewest", + + OriginalItemSpec = filePath, + + Fingerprint = fingerprint, + + Integrity = "sha256-" + fingerprint, + + FileLength = 6, + + LastWriteTime = DateTime.UtcNow, + + }; + + asset.ApplyDefaults(); + + asset.Normalize(); + + return asset; + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsTargetsFileTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsTargetsFileTest.cs index 62c9d1363b72..58e2e7d0546a 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsTargetsFileTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsTargetsFileTest.cs @@ -1,131 +1,395 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Xml.Linq; + + using Microsoft.Build.Framework; + + using Moq; + + + + namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + +[TestClass] + public class GeneratePackageAssetsTargetsFileTest : IDisposable + + { + + private readonly string _tempDir; + + private readonly Mock _buildEngine; + + private readonly List _errorMessages; + + + + public GeneratePackageAssetsTargetsFileTest() + + { + + _tempDir = Path.Combine(Path.GetTempPath(), "GenPkgTargets_" + Guid.NewGuid().ToString("N")); + + Directory.CreateDirectory(_tempDir); + + + + _errorMessages = new List(); + + _buildEngine = new Mock(); + + _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => _errorMessages.Add(args.Message)); + + _buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())); + + } + + + + public void Dispose() + + { + + if (Directory.Exists(_tempDir)) + + { + + try { Directory.Delete(_tempDir, recursive: true); } catch { } + + } + + } - [Fact] + + + + + [TestMethod] + + public void GeneratesValidXml_WithStaticWebAssetPackageManifestItem() + + { + + var task = CreateTask(); + + + + task.Execute().Should().BeTrue(); + + File.Exists(task.TargetFilePath).Should().BeTrue(); + + + + var doc = XDocument.Load(task.TargetFilePath); + + var root = doc.Root; + + root.Should().NotBeNull(); + + root.Name.LocalName.Should().Be("Project"); + + + + var itemGroup = root.Element("ItemGroup"); + + itemGroup.Should().NotBeNull(); + + + + var manifestItem = itemGroup.Element("StaticWebAssetPackageManifest"); + + manifestItem.Should().NotBeNull(); + + + + var includeAttr = manifestItem.Attribute("Include"); + + includeAttr.Should().NotBeNull(); + + includeAttr.Value.Should().Contain("MyLib.PackageAssets.json"); + + + + var sourceIdElement = manifestItem.Element("SourceId"); + + sourceIdElement.Should().NotBeNull(); + + sourceIdElement.Value.Should().Be("MyLib"); + + + + var contentRootElement = manifestItem.Element("ContentRoot"); + + contentRootElement.Should().NotBeNull(); + + contentRootElement.Value.Should().Contain("staticwebassets"); + + + + var packageRootElement = manifestItem.Element("PackageRoot"); + + packageRootElement.Should().NotBeNull(); + + } - [Fact] + + + + + [TestMethod] + + public void Incremental_FileNotRewritten_WhenContentUnchanged() + + { + + var task = CreateTask(); + + + + // First write + + task.Execute().Should().BeTrue(); + + var firstWriteTime = File.GetLastWriteTimeUtc(task.TargetFilePath); + + var firstContent = File.ReadAllText(task.TargetFilePath); + + + + // Advance the file timestamp so we can detect if it gets rewritten + + File.SetLastWriteTimeUtc(task.TargetFilePath, firstWriteTime.AddSeconds(10)); + + firstWriteTime = File.GetLastWriteTimeUtc(task.TargetFilePath); + + + + // Second write with same inputs + + var task2 = CreateTask(); + + task2.Execute().Should().BeTrue(); + + var secondWriteTime = File.GetLastWriteTimeUtc(task2.TargetFilePath); + + var secondContent = File.ReadAllText(task2.TargetFilePath); + + + + // Content should be identical + + secondContent.Should().Be(firstContent); + + // File should not have been rewritten (timestamp preserved) + + secondWriteTime.Should().Be(firstWriteTime); + + } - [Fact] + + + + + [TestMethod] + + public void CustomPackagePathPrefix_ReflectedInContentRoot() + + { + + var task = CreateTask(packagePathPrefix: "custom/assets"); + + + + task.Execute().Should().BeTrue(); + + var manifestItem = LoadManifestItem(task.TargetFilePath); + + + + manifestItem.Element("ContentRoot").Value.Should().Contain("custom"); + + } + + + + private GeneratePackageAssetsTargetsFile CreateTask(string packagePathPrefix = null) + + { + + var task = new GeneratePackageAssetsTargetsFile + + { + + BuildEngine = _buildEngine.Object, + + PackageId = "MyLib", + + TargetFilePath = Path.Combine(_tempDir, "MyLib.targets"), + + ManifestFileName = "MyLib.PackageAssets.json", + + }; + + if (packagePathPrefix != null) + + { + + task.PackagePathPrefix = packagePathPrefix; + + } + + return task; + + } + + + + private static XElement LoadManifestItem(string targetFilePath) + + { + + var doc = XDocument.Load(targetFilePath); + + return doc.Root.Element("ItemGroup").Element("StaticWebAssetPackageManifest"); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsManifestTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsManifestTest.cs index a278a732193a..65678deb71b5 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsManifestTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsManifestTest.cs @@ -1,494 +1,1484 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + 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; + + + + +[TestClass] + public class GenerateStaticWebAssetEndpointsManifestTest + + { - [Fact] + + + [TestMethod] + + 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() + + }; + + + + try + + { + + // Act + + task.Execute(); + + + + // Assert + + new FileInfo(path).Should().Exist(); + + var manifest = File.ReadAllText(path); + + var json = JsonSerializer.Deserialize(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] + + + + + [TestMethod] + + 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() + + }; + + + + try + + { + + // Act + + task.Execute(); + + + + // Assert + + new FileInfo(path).Should().Exist(); + + new FileInfo(exclusionCachePath).Should().Exist(); + + + + var manifest = File.ReadAllText(path); + + var json = JsonSerializer.Deserialize(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] + + + + + [TestMethod] + + 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() + + }; + + + + 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() + + }; + + 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] + + + + + [TestMethod] + + 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() + + }; + + + + 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() + + }; + + 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(); + + + + 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; + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsPropsFileTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsPropsFileTest.cs index 90135f0c14ca..0dd8f9337e7b 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsPropsFileTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsPropsFileTest.cs @@ -1,217 +1,653 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Net; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + using NuGet.ContentModel; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; + + + + +[TestClass] + public class GenerateStaticWebAssetEndpointsPropsFileTest + + { - [Fact] + + + [TestMethod] + + public void Generates_ValidEndpointsDefinitions() + + { + + // Arrange + + var file = Path.GetTempFileName(); + + var expectedDocument = """ + + + + + + + + $([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)..\staticwebassets\js\sample.js)) + + + + + + + + + + + + + + """; + + + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new GenerateStaticWebAssetEndpointsPropsFile + + { + + BuildEngine = buildEngine.Object, + + StaticWebAssets = + + [ + + CreateStaticWebAsset( + + Path.Combine("wwwroot","js","sample.js"), + + "MyLibrary", + + "Discovered", + + Path.Combine("js", "sample.js"), + + "All", + + "All") + + ], + + StaticWebAssetEndpoints = + + [ + + CreateStaticWebAssetEndpoint( + + Path.Combine("js", "sample.js"), + + Path.GetFullPath(Path.Combine("wwwroot","js","sample.js")), + + [ + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Content-Length", + + Value = "10" + + } + + ], + + [ + + new StaticWebAssetEndpointSelector + + { + + Name = "Content-Encoding", + + Value = "gzip", + + Quality = "0.1" + + } + + ], + + [ + + new StaticWebAssetEndpointProperty + + { + + Name = "integrity", + + Value = "__integrity__" + + } + + ]) + + ], + + PackagePathPrefix = "staticwebassets", + + TargetPropsFilePath = file + + }; + + + + // Act + + try + + { + + var result = task.Execute(); + + + + result.Should().BeTrue(); + + new FileInfo(file).Should().Exist(); + + var document = File.ReadAllText(file); + + document.Should().BeVisuallyEquivalentTo(expectedDocument); + + } + + finally + + { + + if (File.Exists(file)) + + { + + try + + { + + File.Delete(file); + + } + + catch + + { + + } + + } + + } + + } - [Fact] + + + + + [TestMethod] + + public void Fails_WhenEndpointWithoutAssetExists() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new GenerateStaticWebAssetEndpointsPropsFile + + { + + BuildEngine = buildEngine.Object, + + StaticWebAssets = [], + + StaticWebAssetEndpoints = + + [ + + CreateStaticWebAssetEndpoint( + + Path.Combine("js", "sample.js").Replace('\\', '/'), + + Path.GetFullPath(Path.Combine("wwwroot","js","sample.js")), + + [ + + new StaticWebAssetEndpointResponseHeader + + { + + Name = "Content-Length", + + Value = "10" + + } + + ], + + [ + + new StaticWebAssetEndpointSelector + + { + + Name = "Content-Encoding", + + Value = "gzip", + + Quality = "0.1" + + } + + ], + + [ + + new StaticWebAssetEndpointProperty + + { + + Name = "integrity", + + Value = "__integrity__" + + } + + ]) + + ], + + PackagePathPrefix = "staticwebassets", + + TargetPropsFilePath = Path.GetTempFileName(), + + }; + + + + // Act + + var result = task.Execute(); + + + + result.Should().BeFalse(); + + errorMessages.Should().ContainSingle(); + + errorMessages[0].Should().Be($"""The asset file '{Path.GetFullPath(Path.Combine("wwwroot", "js", "sample.js"))}' specified in the endpoint '{Path.Combine("js","sample.js").Replace('\\', '/')}' does not exist."""); + + } + + + + private static ITaskItem CreateStaticWebAsset( + + string itemSpec, + + string sourceId, + + string sourceType, + + string relativePath, + + string assetKind, + + string assetMode) + + { + + var result = new StaticWebAsset() + + { + + Identity = Path.GetFullPath(itemSpec), + + SourceId = sourceId, + + SourceType = sourceType, + + ContentRoot = Directory.GetCurrentDirectory(), + + BasePath = "base", + + RelativePath = relativePath, + + AssetKind = assetKind, + + AssetMode = assetMode, + + AssetRole = "Primary", + + RelatedAsset = "", + + AssetTraitName = "", + + AssetTraitValue = "", + + CopyToOutputDirectory = "", + + CopyToPublishDirectory = "", + + OriginalItemSpec = itemSpec, + + // Add these to avoid accessing the disk to compute them + + Integrity = "integrity", + + Fingerprint = "fingerprint", + + FileLength = 10, + + LastWriteTime = DateTime.UtcNow, + + }; + + + + result.ApplyDefaults(); + + result.Normalize(); + + + + return result.ToTaskItem(); + + } + + + + private static ITaskItem CreateStaticWebAssetEndpoint( + + string route, + + string assetFile, + + StaticWebAssetEndpointResponseHeader[] responseHeaders = null, + + StaticWebAssetEndpointSelector[] responseSelector = null, + + StaticWebAssetEndpointProperty[] properties = null) + + { + + return new StaticWebAssetEndpoint + + { + + Route = route, + + AssetFile = Path.GetFullPath(assetFile), + + ResponseHeaders = responseHeaders ?? [], + + EndpointProperties = properties ?? [], + + Selectors = responseSelector ?? [] + + }.ToTaskItem(); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsDevelopmentManifestTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsDevelopmentManifestTest.cs index e2afd6e0097d..6bae1d09f433 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsDevelopmentManifestTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsDevelopmentManifestTest.cs @@ -1,755 +1,2267 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Moq; + + using static Microsoft.AspNetCore.StaticWebAssets.Tasks.GenerateStaticWebAssetsDevelopmentManifest; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { + + + [TestClass] + public class GenerateStaticWebAssetsDevelopmentManifestTest + + { - [Fact] + + + [TestMethod] + + public void SkipsManifestGenerationWhen_ThereAreNoAssetsNorDiscoveryPatterns() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + Assets = Array.Empty(), + + DiscoveryPatterns = Array.Empty() + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + messages.Should().HaveCount(1); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_IncludesBuildAssets() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + ("index.html", CreateMatchNode(0, "index.html"))), + + Environment.CurrentDirectory); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + }; + + + + var assets = new[] { CreateAsset("index.html", "index.html", assetKind: StaticWebAsset.AssetKinds.Build) }; + + var patterns = Array.Empty(); + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } - [Theory] - [InlineData("#[.{fingerprint}]?", "index.html", "optional.html")] - [InlineData("#[.{fingerprint}]!", "index.fingerprint.html", "preferred.html")] - [InlineData("#[.{fingerprint}]", "index.fingerprint.html", "required.html")] + + + + + [TestMethod] + + + [DataRow("#[.{fingerprint}]?", "index.html", "optional.html")] + + + [DataRow("#[.{fingerprint}]!", "index.fingerprint.html", "preferred.html")] + + + [DataRow("#[.{fingerprint}]", "index.fingerprint.html", "required.html")] + + public void ComputeDevelopmentManifest_ReplacesAssetTokens(string fingerprintExpression, string path, string fileName) + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + (path, CreateMatchNode(0, path))), + + Environment.CurrentDirectory); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + }; + + + + var assets = new[] { CreateAsset(fileName, $"index{fingerprintExpression}.html", assetKind: StaticWebAsset.AssetKinds.All) }; + + var patterns = Array.Empty(); + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } - [Theory] - [InlineData("#[.{fingerprint}]?", "index.html", "optional.html")] - [InlineData("#[.{fingerprint}]!", "index.fingerprint.html", "preferred.html")] - [InlineData("#[.{fingerprint}]", "index.fingerprint.html", "required.html")] + + + + + [TestMethod] + + + [DataRow("#[.{fingerprint}]?", "index.html", "optional.html")] + + + [DataRow("#[.{fingerprint}]!", "index.fingerprint.html", "preferred.html")] + + + [DataRow("#[.{fingerprint}]", "index.fingerprint.html", "required.html")] + + public void ComputeDevelopmentManifest_ReplacesAssetTokens_FileExists(string fingerprintExpression, string path, string subPath) + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + (path, CreateMatchNode(0, subPath))), + + Environment.CurrentDirectory); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + }; + + + + var assets = new[] { CreateAsset(subPath, $"index{fingerprintExpression}.html", assetKind: StaticWebAsset.AssetKinds.All) }; + + var patterns = Array.Empty(); + + + + var fileName = Path.Combine(Environment.CurrentDirectory, subPath); + + try + + { + + File.WriteAllText(fileName, "content"); + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } + + finally + + { + + if (File.Exists(fileName)) + + { + + File.Delete(fileName); + + } + + } + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_UsesIdentitySubpath_WhenFileExists_AndContentRoot_IsPrefix() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + ("_framework", + + CreateIntermediateNode( + + ("dotnet.native.fingerprint.js.gz", CreateMatchNode(0, "blob-hash.gz"))))), + + Environment.CurrentDirectory); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + }; + + + + var fileName = Path.Combine(Environment.CurrentDirectory, "blob-hash.gz"); + + try + + { + + File.WriteAllText(fileName, "content"); + + var assets = new[] { CreateAsset( + + fileName, + + $$"""_framework/dotnet.native#[.{fingerprint}]!.js.gz""", + + contentRoot: Environment.CurrentDirectory, + + assetKind: StaticWebAsset.AssetKinds.All) }; + + var patterns = Array.Empty(); + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } + + finally + + { + + if (File.Exists(fileName)) + + { + + File.Delete(fileName); + + } + + } + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_UsesRelativePath_ReplacesAssetTokens_WhenFileDoesNotExist_AtIdentity() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + ("_framework", + + CreateIntermediateNode( + + ("dotnet.native.fingerprint.js", CreateMatchNode(0, "_framework/dotnet.native.fingerprint.js"))))), + + Environment.CurrentDirectory); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + }; + + + + var fileName = Path.Combine(Environment.CurrentDirectory, "dotnet.native.js"); + + var assets = new[] { CreateAsset( + + fileName, + + $$"""_framework/dotnet.native#[.{fingerprint}]!.js""", + + contentRoot: Environment.CurrentDirectory, + + assetKind: StaticWebAsset.AssetKinds.All) }; + + var patterns = Array.Empty(); + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_IncludesAllAssets() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + ("index.html", CreateMatchNode(0, "index.html"))), + + Environment.CurrentDirectory); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + }; + + + + var assets = new[] { CreateAsset("index.html", "index.html", assetKind: StaticWebAsset.AssetKinds.All) }; + + var patterns = Array.Empty(); + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_ExcludesPublishAssets() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode()); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + }; + + + + var assets = new[] { CreateAsset("index.html", "index.html", assetKind: StaticWebAsset.AssetKinds.Publish) }; + + var patterns = Array.Empty(); + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_ExcludesReferenceAssets() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode()); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + Source = "CurrentProjectId" + + }; + + + + var assets = new[] { CreateAsset("index.html", "index.html", assetMode: StaticWebAsset.AssetModes.Reference) }; + + var patterns = Array.Empty(); + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_PrefersBuildAssetsOverAllAssets() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + ("index.html", CreateMatchNode(0, "index.build.html"))), + + Environment.CurrentDirectory); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + Source = "CurrentProjectId" + + }; + + + + var assets = new[] { + + CreateAsset("index.build.html", "index.html", assetKind: StaticWebAsset.AssetKinds.Build), + + CreateAsset("index.html", "index.html", assetKind: StaticWebAsset.AssetKinds.All) + + }; + + var patterns = Array.Empty(); + + + + var fileName = Path.Combine(Environment.CurrentDirectory, "index.build.html"); + + try + + { + + File.WriteAllText(fileName, "content"); + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } + + finally + + { + + if (File.Exists(fileName)) + + { + + File.Delete(fileName); + + } + + } + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_UsesIdentityWhenContentRootStartsByIdentity() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var filePath = Path.Combine("some", "subfolder", "index.build.html"); + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + ("index.html", CreateMatchNode(0, StaticWebAsset.Normalize(filePath)))), + + Environment.CurrentDirectory); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + Source = "CurrentProjectId" + + }; + + + + var assets = new[] { + + CreateAsset(filePath, "index.html"), + + }; + + var patterns = Array.Empty(); + + + + try + + { + + Directory.CreateDirectory(Path.GetDirectoryName(filePath)); + + File.WriteAllText(filePath, "content"); + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } + + finally + + { + + if (File.Exists(filePath)) + + { + + File.Delete(filePath); + + } + + } + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_UsesRelativePathContentRootDoesNotStartByIdentity() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + ("index.html", CreateMatchNode(0, "index.html"))), + + Path.GetFullPath(Path.Combine("bin", "debug", "wwwroot"))); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + Source = "CurrentProjectId" + + }; + + + + var assets = new[] { + + CreateAsset(Path.Combine("some", "subfolder", "index.build.html"), "index.html", contentRoot: Path.Combine("bin", "debug", "wwwroot")), + + }; + + var patterns = Array.Empty(); + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_MapsPatternsFromCurrentProject() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode() + + .AddPatterns((0, "**", 0)), + + Path.GetFullPath("wwwroot")); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + Source = "CurrentProjectId" + + }; + + + + var assets = Array.Empty(); + + var patterns = new[] { CreatePattern() }; + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_MapsPatternsFromOtherProjects() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + ("_other", CreateIntermediateNode( + + ("_project", CreateIntermediateNode().AddPatterns((0, "**", 2)))))), + + Path.GetFullPath("wwwroot")); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + Source = "CurrentProjectId" + + }; + + + + var assets = Array.Empty(); + + var patterns = new[] { CreatePattern(basePath: "_other/_project", source: "OtherProject") }; + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_CanMapMultiplePatternsOnSameNode() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + ("_other", CreateIntermediateNode( + + ("_project", CreateIntermediateNode().AddPatterns( + + (0, "*.js", 2), + + (0, "*.css", 2)))))), + + Path.GetFullPath("wwwroot")); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + Source = "CurrentProjectId" + + }; + + + + var assets = Array.Empty(); + + var patterns = new[] + + { + + CreatePattern(basePath: "_other/_project", source: "OtherProject", pattern: "*.js"), + + CreatePattern(basePath: "_other/_project", source: "OtherProject", pattern: "*.css") + + }; + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_CanMapMultiplePatternsOnSameNodeWithDifferentContentRoots() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + ("_other", CreateIntermediateNode( + + ("_project", CreateIntermediateNode().AddPatterns( + + (0, "*.css", 2), + + (1, "*.js", 2)))))), + + Path.GetFullPath("wwwroot"), + + Path.GetFullPath("styles")); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + Source = "CurrentProjectId" + + }; + + + + var assets = Array.Empty(); + + var patterns = new[] + + { + + CreatePattern(basePath: "_other/_project", source: "OtherProject", pattern: "*.js"), + + CreatePattern(basePath: "_other/_project", source: "OtherProject", pattern: "*.css", contentRoot: Path.GetFullPath("styles")) + + }; + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_MultipleAssetsSameContentRoot() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + ("css", CreateIntermediateNode(("site.css", CreateMatchNode(0, "css/site.css")))), + + ("js", CreateIntermediateNode(("index.js", CreateMatchNode(0, "js/index.js"))))), + + Environment.CurrentDirectory); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + Source = "CurrentProjectId" + + }; + + + + var assets = new[] + + { + + CreateAsset(Path.Combine(Environment.CurrentDirectory, "css", "site.css"), "css/site.css"), + + CreateAsset(Path.Combine(Environment.CurrentDirectory, "js", "index.js"), "js/index.js") + + }; + + + + var patterns = Array.Empty(); + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_DifferentCasingEndUpInDifferentNodes() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + ("css", CreateIntermediateNode(("site.css", CreateMatchNode(0, "css/site.css")))), + + ("CSS", CreateIntermediateNode(("site.css", CreateMatchNode(0, "CSS/site.css"))))), + + Environment.CurrentDirectory); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + Source = "CurrentProjectId" + + }; + + + + var assets = new[] + + { + + CreateAsset(Path.Combine(Environment.CurrentDirectory, "css", "site.css"), "css/site.css"), + + CreateAsset(Path.Combine(Environment.CurrentDirectory, "CSS", "site.css"), "CSS/site.css"), + + }; + + + + var patterns = Array.Empty(); + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeDevelopmentManifest_UsesBasePathForAssetsFromDifferentProjects() + + { + + // Arrange + + var messages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => messages.Add(args.Message)); + + + + var expectedManifest = CreateExpectedManifest( + + CreateIntermediateNode( + + ("css", CreateIntermediateNode(("site.css", CreateMatchNode(0, "css/site.css")))), + + ("_content", CreateIntermediateNode( + + ("OtherProject", CreateIntermediateNode( + + ("CSS", CreateIntermediateNode(("site.css", CreateMatchNode(1, "CSS/site.css"))))))))), + + Environment.CurrentDirectory, + + Path.GetFullPath("otherProject")); + + + + var task = new GenerateStaticWebAssetsDevelopmentManifest() + + { + + BuildEngine = buildEngine.Object, + + Source = "CurrentProjectId" + + }; + + + + var assets = new[] + + { + + CreateAsset(Path.Combine(Environment.CurrentDirectory, "css", "site.css"), "css/site.css"), + + CreateAsset( + + Path.Combine(Environment.CurrentDirectory, "CSS", "site.css"), + + "CSS/site.css", + + basePath: "_content/OtherProject", + + sourceType: "Project", + + contentRoot: Path.GetFullPath("otherProject")), + + }; + + + + var patterns = Array.Empty(); + + + + // Act + + var manifest = task.ComputeDevelopmentManifest(assets, patterns); + + + + // Assert + + manifest.Should().BeEquivalentTo(expectedManifest); + + } + + + + private static StaticWebAssetsDiscoveryPattern CreatePattern( + + string name = null, + + string contentRoot = null, + + string pattern = null, + + string basePath = null, + + string source = null) => + + new() + + { + + Name = name ?? "CurrentProjectId\\wwwroot", + + Pattern = pattern ?? "**", + + BasePath = basePath ?? "_content/CurrentProjectId", + + Source = source ?? "CurrentProjectId", + + ContentRoot = StaticWebAsset.NormalizeContentRootPath(contentRoot ?? Path.Combine(Environment.CurrentDirectory, "wwwroot")) + + }; + + + + private static StaticWebAssetsDevelopmentManifest CreateExpectedManifest(StaticWebAssetNode root, params string[] contentRoots) + + { + + return new StaticWebAssetsDevelopmentManifest() + + { + + ContentRoots = contentRoots.Select(cr => StaticWebAsset.NormalizeContentRootPath(cr)).ToArray(), + + Root = root + + }; + + } + + + + private static StaticWebAssetNode CreateIntermediateNode(params (string key, StaticWebAssetNode node)[] children) => new() + + { + + Children = children.Length == 0 ? null : children.ToDictionary(pair => pair.key, pair => pair.node) + + }; + + + + private static StaticWebAssetNode CreateMatchNode(int index, string subpath) => new() + + { + + Asset = new StaticWebAssetMatch { ContentRootIndex = index, SubPath = subpath } + + }; + + + + private static StaticWebAsset CreateAsset( + + string identity, + + string relativePath, + + string assetKind = default, + + string assetMode = default, + + string sourceId = default, + + string sourceType = default, + + string basePath = default, + + string contentRoot = default) + + { + + return new StaticWebAsset() + + { + + Identity = Path.GetFullPath(identity), + + SourceId = sourceId ?? "CurrentProjectId", + + SourceType = sourceType ?? StaticWebAsset.SourceTypes.Discovered, + + BasePath = basePath ?? "_content/Base", + + RelativePath = relativePath, + + AssetKind = assetKind ?? StaticWebAsset.AssetKinds.All, + + AssetMode = assetMode ?? StaticWebAsset.AssetModes.All, + + AssetRole = StaticWebAsset.AssetRoles.Primary, + + Fingerprint = "fingerprint", + + ContentRoot = StaticWebAsset.NormalizeContentRootPath(contentRoot ?? Environment.CurrentDirectory), + + OriginalItemSpec = identity + + }; + + } + + } + + + + internal static class StaticWebAssetNodeTestExtensions + + { + + public static StaticWebAssetNode AddPatterns(this StaticWebAssetNode node, params (int contentRoot, string pattern, int depth)[] patterns) + + { + + node.Patterns = patterns.Select(p => new StaticWebAssetPattern { ContentRootIndex = p.contentRoot, Pattern = p.pattern, Depth = p.depth }).ToArray(); + + return node; + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsManifestTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsManifestTest.cs index 63899799ee8e..1ebf7a66c8c4 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsManifestTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsManifestTest.cs @@ -1,455 +1,1367 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { + + + [TestClass] + public class GenerateStaticWebAssetsManifestTest + + { + + public GenerateStaticWebAssetsManifestTest() + + { + + Directory.CreateDirectory(Path.Combine(SdkTestContext.Current.TestExecutionDirectory, nameof(GenerateStaticWebAssetsManifestTest))); + + TempFilePath = Path.Combine(SdkTestContext.Current.TestExecutionDirectory, nameof(GenerateStaticWebAssetsManifestTest), Guid.NewGuid().ToString("N") + ".json"); + + } + + + + public string TempFilePath { get; } - [Fact] + + + + + [TestMethod] + + public void CanGenerateEmptyManifest() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(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(), + + Endpoints = Array.Empty(), + + ReferencedProjectsConfigurations = Array.Empty(), + + DiscoveryPatterns = Array.Empty(), + + 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] + + + + + [TestMethod] + + public void GeneratesManifestWithAssets() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(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(), + + DiscoveryPatterns = Array.Empty(), + + 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> GeneratesManifestFailsWhenInvalidAssetsAreProvidedData + + { + + get + + { + + var theoryData = new TheoryData> + + { + + 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))] + + + + + [TestMethod] + + + [DynamicData(nameof(GeneratesManifestFailsWhenInvalidAssetsAreProvidedData))] + + public void GeneratesManifestFailsWhenInvalidAssetsAreProvided(Action change) + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(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(), + + ReferencedProjectsConfigurations = Array.Empty(), + + DiscoveryPatterns = Array.Empty(), + + BasePath = "/", + + Source = "MyProject", + + ManifestType = "Build", + + Mode = "Default", + + ManifestPath = TempFilePath, + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(false); + + } + + + + public static TheoryData GeneratesManifestFailsWhenTwoAssetsEndUpOnTheSamePathData + + { + + get + + { + + var data = new TheoryData + + { + + // 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))] + + + + + [TestMethod] + + + [DynamicData(nameof(GeneratesManifestFailsWhenTwoAssetsEndUpOnTheSamePathData))] + + public void GeneratesManifestFailsWhenTwoAssetsEndUpOnTheSamePath(StaticWebAsset first, StaticWebAsset second) + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(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(), + + ReferencedProjectsConfigurations = Array.Empty(), + + DiscoveryPatterns = Array.Empty(), + + BasePath = "/", + + Source = "MyProject", + + ManifestType = "Build", + + Mode = "Default", + + ManifestPath = TempFilePath, + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(false); + + } - [Fact] + + + + + + + [TestMethod] + + public void GeneratesManifestWithReferencedProjectConfigurations() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(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(), + + Endpoints = Array.Empty(), + + ReferencedProjectsConfigurations = new[] { projectReference.ToTaskItem() }, + + DiscoveryPatterns = Array.Empty(), + + 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] + + + + + [TestMethod] + + public void GeneratesManifestWithDiscoveryPatterns() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(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(), + + Endpoints = Array.Empty(), + + ReferencedProjectsConfigurations = Array.Empty(), + + 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; + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsPropsFileTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsPropsFileTest.cs index ccbcefc18867..b216474546b5 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsPropsFileTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsPropsFileTest.cs @@ -1,874 +1,2624 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + using NuGet.Packaging.Core; + + + + namespace Microsoft.NET.Sdk.Razor.Test + + { + + + [TestClass] + public class GenerateStaticWebAssetsPropsFileTest + + { - [Fact] + + + [TestMethod] + + public void Fails_WhenStaticWebAsset_DoesNotContainSourceType() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new GenerateStaticWebAssetsPropsFile + + { + + BuildEngine = buildEngine.Object, + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary + + { + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = Path.Combine("js", "sample.js"), + + }) + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + var expectedError = $"Missing required metadata 'SourceType' for '{Path.Combine("wwwroot", "js", "sample.js")}'."; + + errorMessages.Should().ContainSingle(message => message == expectedError); + + } - [Fact] + + + + + [TestMethod] + + public void Fails_WhenStaticWebAsset_DoesNotContainSourceId() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new GenerateStaticWebAssetsPropsFile + + { + + BuildEngine = buildEngine.Object, + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = Path.Combine("js", "sample.js"), + + }) + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + var expectedError = $"Missing required metadata 'SourceId' for '{Path.Combine("wwwroot", "js", "sample.js")}'."; + + errorMessages.Should().ContainSingle(message => message == expectedError); + + } - [Fact] + + + + + [TestMethod] + + public void Fails_WhenStaticWebAsset_DoesNotContainContentRoot() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new GenerateStaticWebAssetsPropsFile + + { + + BuildEngine = buildEngine.Object, + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = Path.Combine("js", "sample.js"), + + }) + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + var expectedError = $"Missing required metadata 'ContentRoot' for '{Path.Combine("wwwroot", "js", "sample.js")}'."; + + errorMessages.Should().ContainSingle(message => message == expectedError); + + } - [Fact] + + + + + [TestMethod] + + public void Fails_WhenStaticWebAsset_DoesNotContainBasePath() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new GenerateStaticWebAssetsPropsFile + + { + + BuildEngine = buildEngine.Object, + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["RelativePath"] = Path.Combine("js", "sample.js"), + + }) + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + var expectedError = $"Missing required metadata 'BasePath' for '{Path.Combine("wwwroot", "js", "sample.js")}'."; + + errorMessages.Should().ContainSingle(message => message == expectedError); + + } - [Fact] + + + + + [TestMethod] + + public void Fails_WhenStaticWebAsset_DoesNotContainRelativePath() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new GenerateStaticWebAssetsPropsFile + + { + + BuildEngine = buildEngine.Object, + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }) + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + var expectedError = $"Missing required metadata 'RelativePath' for '{Path.Combine("wwwroot", "js", "sample.js")}'."; + + errorMessages.Should().ContainSingle(message => message == expectedError); + + } - [Fact] + + + + + [TestMethod] + + public void Fails_WhenStaticWebAsset_HasInvalidSourceType() + + { + + // Arrange + + + + var expectedError = $"Static web asset '{Path.Combine("wwwroot", "css", "site.css")}' has invalid source type 'Package'."; + + + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new GenerateStaticWebAssetsPropsFile + + { + + BuildEngine = buildEngine.Object, + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = Path.Combine("js", "sample.js"), + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }), + + CreateItem(Path.Combine("wwwroot","css","site.css"), new Dictionary + + { + + ["SourceType"] = "Package", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = Path.Combine("css", "site.css"), + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }) + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + errorMessages.Should().ContainSingle(message => message == expectedError); + + } - [Fact] + + + + + [TestMethod] + + public void Fails_WhenStaticWebAsset_HaveDifferentSourceId() + + { + + // Arrange + + var expectedError = "Static web assets have different 'SourceId' metadata values " + + + "'MyLibrary' and 'MyLibrary2' " + + + $"for '{Path.Combine("wwwroot", "js", "sample.js")}' and '{Path.Combine("wwwroot", "css", "site.css")}'."; + + + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new GenerateStaticWebAssetsPropsFile + + { + + BuildEngine = buildEngine.Object, + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = Path.Combine("js", "sample.js"), + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }), + + CreateItem(Path.Combine("wwwroot","css","site.css"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary2", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = Path.Combine("css", "site.css"), + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }) + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + errorMessages.Should().ContainSingle(message => message == expectedError); + + } - [Fact] + + + + + [TestMethod] + + public void WritesPropsFile_WhenThereIsAtLeastOneStaticAsset() + + { + + // Arrange + + var file = Path.GetTempFileName(); + + var expectedDocument = @" + + + + + + Package + + MyLibrary + + $(MSBuildThisFileDirectory)..\staticwebassets\ + + _content/mylibrary + + js/sample.js + + All + + All + + Primary + + + + + + + + sample-fingerprint + + sample-integrity + + Never + + PreserveNewest + + 10 + + Thu, 15 Nov 1990 00:00:00 GMT + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\js\sample.js')) + + + + + + "; + + + + try + + { + + var buildEngine = new Mock(); + + + + var task = new GenerateStaticWebAssetsPropsFile + + { + + BuildEngine = buildEngine.Object, + + TargetPropsFilePath = file, + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = Path.Combine("js", "sample.js").Replace("\\","/"), + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["Fingerprint"] = "sample-fingerprint", + + ["Integrity"] = "sample-integrity", + + ["OriginalItemSpec"] = Path.Combine("wwwroot","js","sample.js"), + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["FileLength"] = "10", + + ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) + + }), + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var document = File.ReadAllText(file); + + document.Should().Contain(expectedDocument); + + } + + finally + + { + + if (File.Exists(file)) + + { + + File.Delete(file); + + } + + } + + } - [Fact] + + + + + [TestMethod] + + public void WritesIndividualItems_WithTheirRespectiveBaseAndRelativePaths() + + { + + // Arrange + + var file = Path.GetTempFileName(); + + var expectedDocument = @" + + + + + + Package + + MyLibrary + + $(MSBuildThisFileDirectory)..\staticwebassets\ + + / + + App.styles.css + + All + + All + + Primary + + + + + + + + styles-fingerprint + + styles-integrity + + Never + + PreserveNewest + + 10 + + Thu, 15 Nov 1990 00:00:00 GMT + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\App.styles.css')) + + + + + + Package + + MyLibrary + + $(MSBuildThisFileDirectory)..\staticwebassets\ + + _content/mylibrary + + js/sample.js + + All + + All + + Primary + + + + + + + + sample-fingerprint + + sample-integrity + + Never + + PreserveNewest + + 10 + + Thu, 15 Nov 1990 00:00:00 GMT + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\js\sample.js')) + + + + + + "; + + + + try + + { + + var buildEngine = new Mock(); + + + + var task = new GenerateStaticWebAssetsPropsFile + + { + + BuildEngine = buildEngine.Object, + + TargetPropsFilePath = file, + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = Path.Combine("js", "sample.js").Replace("\\","/"), + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["OriginalItemSpec"] = Path.Combine("wwwroot","js","sample.js"), + + ["Fingerprint"] = "sample-fingerprint", + + ["Integrity"] = "sample-integrity", + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["FileLength"] = "10", + + ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) + + }), + + CreateItem(Path.Combine("wwwroot","App.styles.css"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "/", + + ["RelativePath"] = "App.styles.css", + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["OriginalItemSpec"] = Path.Combine("wwwroot","App.styles.css"), + + ["Fingerprint"] = "styles-fingerprint", + + ["Integrity"] = "styles-integrity", + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["FileLength"] = "10", + + ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) + + }), + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert - Assert.True(result); + + + Assert.IsTrue(result); + + var document = File.ReadAllText(file); - Assert.Equal(expectedDocument, document, ignoreLineEndingDifferences: true); + + + Assert.AreEqual(expectedDocument.ReplaceLineEndings(), document.ReplaceLineEndings()); + + } + + finally + + { + + if (File.Exists(file)) + + { + + File.Delete(file); + + } + + } + + } - [Fact] + + + + + [TestMethod] + + public void WritesFrameworkSourceType_WhenAssetMatchesFrameworkPattern() + + { + + // Arrange + + var file = Path.GetTempFileName(); + + var expectedDocument = @" + + + + + + Package + + MyLibrary + + $(MSBuildThisFileDirectory)..\staticwebassets\ + + _content/mylibrary + + css/site.css + + All + + All + + Primary + + + + + + + + css-fingerprint + + css-integrity + + Never + + PreserveNewest + + 10 + + Thu, 15 Nov 1990 00:00:00 GMT + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\css\site.css')) + + + + + + Framework + + MyLibrary + + $(MSBuildThisFileDirectory)..\staticwebassets\ + + _content/mylibrary + + js/framework.js + + All + + All + + Primary + + + + + + + + js-fingerprint + + js-integrity + + Never + + PreserveNewest + + 10 + + Thu, 15 Nov 1990 00:00:00 GMT + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\js\framework.js')) + + + + + + "; + + + + try + + { + + var buildEngine = new Mock(); + + + + var task = new GenerateStaticWebAssetsPropsFile + + { + + BuildEngine = buildEngine.Object, + + TargetPropsFilePath = file, + + FrameworkPattern = "**/*.js", + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","js","framework.js"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = "js/framework.js", + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["Fingerprint"] = "js-fingerprint", + + ["Integrity"] = "js-integrity", + + ["OriginalItemSpec"] = Path.Combine("wwwroot","js","framework.js"), + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["FileLength"] = "10", + + ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) + + }), + + CreateItem(Path.Combine("wwwroot","css","site.css"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = "css/site.css", + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["Fingerprint"] = "css-fingerprint", + + ["Integrity"] = "css-integrity", + + ["OriginalItemSpec"] = Path.Combine("wwwroot","css","site.css"), + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["FileLength"] = "10", + + ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) + + }), + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert - Assert.True(result); + + + Assert.IsTrue(result); + + var document = File.ReadAllText(file); - Assert.Equal(expectedDocument, document, ignoreLineEndingDifferences: true); + + + Assert.AreEqual(expectedDocument.ReplaceLineEndings(), document.ReplaceLineEndings()); + + } + + finally + + { + + if (File.Exists(file)) + + { + + File.Delete(file); + + } + + } + + } - [Fact] + + + + + [TestMethod] + + public void WritesAllAsPackage_WhenFrameworkPatternIsNull() + + { + + // Arrange + + var file = Path.GetTempFileName(); + + + + try + + { + + var buildEngine = new Mock(); + + + + var task = new GenerateStaticWebAssetsPropsFile + + { + + BuildEngine = buildEngine.Object, + + TargetPropsFilePath = file, + + FrameworkPattern = null, + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","js","app.js"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = "js/app.js", + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["Fingerprint"] = "fp", + + ["Integrity"] = "int", + + ["OriginalItemSpec"] = Path.Combine("wwwroot","js","app.js"), + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["FileLength"] = "10", + + ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) + + }), + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var document = File.ReadAllText(file); + + document.Should().Contain("Package"); + + document.Should().NotContain("Framework"); + + } + + finally + + { + + if (File.Exists(file)) + + { + + File.Delete(file); + + } + + } + + } - [Fact] + + + + + [TestMethod] + + public void WritesFrameworkSourceType_WithMultiplePatterns() + + { + + // Arrange + + var file = Path.GetTempFileName(); + + + + try + + { + + var buildEngine = new Mock(); + + + + var task = new GenerateStaticWebAssetsPropsFile + + { + + BuildEngine = buildEngine.Object, + + TargetPropsFilePath = file, + + FrameworkPattern = "**/*.js;**/*.css", + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","js","app.js"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = "js/app.js", + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["Fingerprint"] = "fp1", + + ["Integrity"] = "int1", + + ["OriginalItemSpec"] = Path.Combine("wwwroot","js","app.js"), + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["FileLength"] = "10", + + ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) + + }), + + CreateItem(Path.Combine("wwwroot","css","site.css"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = "css/site.css", + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["Fingerprint"] = "fp2", + + ["Integrity"] = "int2", + + ["OriginalItemSpec"] = Path.Combine("wwwroot","css","site.css"), + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["FileLength"] = "10", + + ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) + + }), + + CreateItem(Path.Combine("wwwroot","images","logo.png"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = "images/logo.png", + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["Fingerprint"] = "fp3", + + ["Integrity"] = "int3", + + ["OriginalItemSpec"] = Path.Combine("wwwroot","images","logo.png"), + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["FileLength"] = "10", + + ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) + + }), + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var document = File.ReadAllText(file); + + // JS and CSS files should be Framework, PNG should be Package + + var lines = document.Split('\n').Select(l => l.Trim()).ToList(); + + var sourceTypeLines = lines.Where(l => l.StartsWith("")).ToList(); + + // Order is: css/site.css, images/logo.png, js/app.js (sorted by BasePath then RelativePath) + + sourceTypeLines.Should().HaveCount(3); + + sourceTypeLines[0].Should().Be("Framework"); // css/site.css + + sourceTypeLines[1].Should().Be("Package"); // images/logo.png + + sourceTypeLines[2].Should().Be("Framework"); // js/app.js + + } + + finally + + { + + if (File.Exists(file)) + + { + + File.Delete(file); + + } + + } + + } - [Fact] + + + + + [TestMethod] + + public void WritesAllAsPackage_WhenFrameworkPatternMatchesNothing() + + { + + // Arrange + + var file = Path.GetTempFileName(); + + + + try + + { + + var buildEngine = new Mock(); + + + + var task = new GenerateStaticWebAssetsPropsFile + + { + + BuildEngine = buildEngine.Object, + + TargetPropsFilePath = file, + + FrameworkPattern = "**/*.wasm", + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","js","app.js"), new Dictionary + + { + + ["SourceType"] = "Discovered", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", + + ["BasePath"] = "_content/mylibrary", + + ["RelativePath"] = "js/app.js", + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["Fingerprint"] = "fp", + + ["Integrity"] = "int", + + ["OriginalItemSpec"] = Path.Combine("wwwroot","js","app.js"), + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["FileLength"] = "10", + + ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) + + }), + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var document = File.ReadAllText(file); + + document.Should().Contain("Package"); + + document.Should().NotContain("Framework"); + + } + + finally + + { + + if (File.Exists(file)) + + { + + File.Delete(file); + + } + + } + + } + + + + private static TaskItem CreateItem( + + string spec, + + IDictionary metadata) + + { + + var result = new TaskItem(spec); + + foreach (var (key, value) in metadata) + + { + + result.SetMetadata(key, value); + + } + + + + return result; + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateV1StaticWebAssetsManifestTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateV1StaticWebAssetsManifestTest.cs index cf209c460cc0..f65a05b9f6be 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateV1StaticWebAssetsManifestTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateV1StaticWebAssetsManifestTest.cs @@ -1,273 +1,821 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.NET.Sdk.Razor.Test + + { + + + [TestClass] + public class GenerateV1StaticWebAssetsManifestTest + + { - [Fact] + + + [TestMethod] + + public void ReturnsError_WhenBasePathIsMissing() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new GenerateV1StaticWebAssetsManifest + + { + + BuildEngine = buildEngine.Object, + + ContentRootDefinitions = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot", "sample.js"), new Dictionary + + { + + ["ContentRoot"] = "/" + + }) + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + var expectedError = $"Missing required metadata 'BasePath' for '{Path.Combine("wwwroot", "sample.js")}'."; + + errorMessages.Should().ContainSingle(message => message == expectedError); + + } - [Fact] + + + + + [TestMethod] + + public void ReturnsError_WhenContentRootIsMissing() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new GenerateV1StaticWebAssetsManifest + + { + + BuildEngine = buildEngine.Object, + + ContentRootDefinitions = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary + + { + + ["BasePath"] = "MyLibrary" + + }) + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + var expectedError = $"Missing required metadata 'ContentRoot' for '{Path.Combine("wwwroot", "sample.js")}'."; + + errorMessages.Should().ContainSingle(message => message == expectedError); + + } - [Fact] + + + + + [TestMethod] + + public void AllowsMultipleContentRootsWithSameBasePath_ForTheSameSourceId() + + { + + // Arrange + + var file = Path.GetTempFileName(); + + var expectedDocument = $@" + + + + + + "; + + + + var buildEngine = new Mock(); + + + + var task = new GenerateV1StaticWebAssetsManifest + + { + + BuildEngine = buildEngine.Object, + + ContentRootDefinitions = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary + + { + + ["BasePath"] = "Blazor.Client", + + ["ContentRoot"] = Path.Combine(".", "nuget","Blazor.Client"), + + ["SourceId"] = "Blazor.Client" + + }), + + CreateItem(Path.Combine("wwwroot", "otherLib.js"), new Dictionary + + { + + ["BasePath"] = "Blazor.Client", + + ["ContentRoot"] = Path.Combine(".", "nuget", "bin","debug","netstandard2.1"), + + ["SourceId"] = "Blazor.Client" + + }) + + }, + + TargetManifestPath = file + + }; + + + + try + + { + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var document = File.ReadAllText(file); + + document.Should().Contain(expectedDocument); + + } + + finally + + { + + if (File.Exists(file)) + + { + + File.Delete(file); + + } + + } + + } - [Fact] + + + + + [TestMethod] + + public void Generates_EmptyManifest_WhenNoItems_Passed() + + { + + // Arrange + + var file = Path.GetTempFileName(); + + var expectedDocument = @""; + + + + try + + { + + var buildEngine = new Mock(); + + + + var task = new GenerateV1StaticWebAssetsManifest + + { + + BuildEngine = buildEngine.Object, + + ContentRootDefinitions = new TaskItem[] { }, + + TargetManifestPath = file + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var document = File.ReadAllText(file); + + document.Should().Contain(expectedDocument); + + } + + finally + + { + + if (File.Exists(file)) + + { + + File.Delete(file); + + } + + } + + } - [Fact] + + + + + [TestMethod] + + public void Generates_Manifest_WhenContentRootsAvailable() + + { + + // Arrange + + var file = Path.GetTempFileName(); + + var expectedDocument = $@" + + + + "; + + + + try + + { + + var buildEngine = new Mock(); + + + + var task = new GenerateV1StaticWebAssetsManifest + + { + + BuildEngine = buildEngine.Object, + + ContentRootDefinitions = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary + + { + + ["BasePath"] = "MyLibrary", + + ["ContentRoot"] = Path.Combine(".", "nuget", "MyLibrary", "razorContent"), + + ["SourceId"] = "MyLibrary" + + }), + + }, + + TargetManifestPath = file + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var document = File.ReadAllText(file); + + document.Should().Contain(expectedDocument); + + } + + finally + + { + + if (File.Exists(file)) + + { + + File.Delete(file); + + } + + } + + } - [Fact] + + + + + [TestMethod] + + public void SkipsAdditionalElements_WithSameBasePathAndSameContentRoot() + + { + + // Arrange + + var file = Path.GetTempFileName(); + + var expectedDocument = $@" + + + + "; + + + + try + + { + + var buildEngine = new Mock(); + + + + var task = new GenerateV1StaticWebAssetsManifest + + { + + BuildEngine = buildEngine.Object, + + ContentRootDefinitions = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary + + { + + // Base path needs to be normalized to '/' as it goes in the url + + ["BasePath"] = "Base\\MyLibrary", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = Path.Combine(".", "nuget", "MyLibrary", "razorContent") + + }), + + // Comparisons are case insensitive + + CreateItem(Path.Combine("wwwroot, site.css"), new Dictionary + + { + + ["BasePath"] = "Base\\MyLIBRARY", + + ["SourceId"] = "MyLibrary", + + ["ContentRoot"] = Path.Combine(".", "nuget", "MyLIBRARY", "razorContent") + + }), + + }, + + TargetManifestPath = file + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var document = File.ReadAllText(file); + + document.Should().Contain(expectedDocument); + + } + + finally + + { + + if (File.Exists(file)) + + { + + File.Delete(file); + + } + + } + + } + + + + private static TaskItem CreateItem( + + string spec, + + IDictionary metadata) + + { + + var result = new TaskItem(spec); + + foreach (var (key, value) in metadata) + + { + + result.SetMetadata(key, value); + + } + + + + return result; + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/PathTokenizerTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/PathTokenizerTest.cs index ed77b13265c1..d2e232176c09 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/PathTokenizerTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/PathTokenizerTest.cs @@ -2,132 +2,400 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + using System.Collections.Generic; + + using System.Linq; + + using System.Text; + + using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + + namespace Microsoft.AspNetCore.StaticWebAssets.Tasks.Test; + + + + +[TestClass] + public class PathTokenizerTest + + { - [Fact] + + + [TestMethod] + + public void RootSeparator_ProducesEmptySegment() + + { + + var path = "/a/b/c"; + + var tokenizer = new PathTokenizer(path.AsMemory().Span); + + var segments = new List(); + + var collection = tokenizer.Fill(segments); - Assert.Equal("a", collection[0]); - Assert.Equal("b", collection[1]); - Assert.Equal("c", collection[2]); + + + Assert.AreEqual("a", collection[0].ToString()); + + + Assert.AreEqual("b", collection[1].ToString()); + + + Assert.AreEqual("c", collection[2].ToString()); + + } - [Fact] + + + + + [TestMethod] + + public void NonRootSeparator_ProducesInitialSegment() + + { + + var path = "a/b/c"; + + var tokenizer = new PathTokenizer(path.AsMemory().Span); + + var segments = new List(); + + var collection = tokenizer.Fill(segments); - Assert.Equal("a", collection[0]); - Assert.Equal("b", collection[1]); - Assert.Equal("c", collection[2]); + + + Assert.AreEqual("a", collection[0].ToString()); + + + Assert.AreEqual("b", collection[1].ToString()); + + + Assert.AreEqual("c", collection[2].ToString()); + + } - [Fact] + + + + + [TestMethod] + + public void NonRootSeparator_MatchesMultipleCharacters() + + { + + var path = "aa/b/c"; + + var tokenizer = new PathTokenizer(path.AsMemory().Span); + + var segments = new List(); + + var collection = tokenizer.Fill(segments); - Assert.Equal("aa", collection[0]); - Assert.Equal("b", collection[1]); - Assert.Equal("c", collection[2]); + + + Assert.AreEqual("aa", collection[0].ToString()); + + + Assert.AreEqual("b", collection[1].ToString()); + + + Assert.AreEqual("c", collection[2].ToString()); + + } - [Fact] + + + + + [TestMethod] + + public void NonRootSeparator_HandlesConsecutivePathSeparators() + + { + + var path = "aa//b/c"; + + var tokenizer = new PathTokenizer(path.AsMemory().Span); + + var segments = new List(); + + var collection = tokenizer.Fill(segments); - Assert.Equal("aa", collection[0]); - Assert.Equal("b", collection[1]); - Assert.Equal("c", collection[2]); + + + Assert.AreEqual("aa", collection[0].ToString()); + + + Assert.AreEqual("b", collection[1].ToString()); + + + Assert.AreEqual("c", collection[2].ToString()); + + } - [Fact] + + + + + [TestMethod] + + public void NonRootSeparator_HandlesFinalPathSeparator() + + { + + var path = "aa/b/c/"; + + var tokenizer = new PathTokenizer(path.AsMemory().Span); + + var segments = new List(); + + var collection = tokenizer.Fill(segments); - Assert.Equal("aa", collection[0]); - Assert.Equal("b", collection[1]); - Assert.Equal("c", collection[2]); + + + Assert.AreEqual("aa", collection[0].ToString()); + + + Assert.AreEqual("b", collection[1].ToString()); + + + Assert.AreEqual("c", collection[2].ToString()); + + } - [Fact] + + + + + [TestMethod] + + public void NonRootSeparator_HandlesAlternativePathSeparators() + + { + + var path = "aa\\b\\c\\"; + + var tokenizer = new PathTokenizer(path.AsMemory().Span); + + var segments = new List(); + + var collection = tokenizer.Fill(segments); - Assert.Equal("aa", collection[0]); - Assert.Equal("b", collection[1]); - Assert.Equal("c", collection[2]); + + + Assert.AreEqual("aa", collection[0].ToString()); + + + Assert.AreEqual("b", collection[1].ToString()); + + + Assert.AreEqual("c", collection[2].ToString()); + + } - [Fact] + + + + + [TestMethod] + + public void NonRootSeparator_HandlesMixedPathSeparators() + + { + + var path = "aa/b\\c/"; + + var tokenizer = new PathTokenizer(path.AsMemory().Span); + + var segments = new List(); + + var collection = tokenizer.Fill(segments); - Assert.Equal("aa", collection[0]); - Assert.Equal("b", collection[1]); - Assert.Equal("c", collection[2]); + + + Assert.AreEqual("aa", collection[0].ToString()); + + + Assert.AreEqual("b", collection[1].ToString()); + + + Assert.AreEqual("c", collection[2].ToString()); + + } - [Fact] + + + + + [TestMethod] + + public void Ignores_EmpySegments() + + { + + var path = "aa//b//c"; + + var tokenizer = new PathTokenizer(path.AsMemory().Span); + + var segments = new List(); + + var collection = tokenizer.Fill(segments); - Assert.Equal("aa", collection[0]); - Assert.Equal("b", collection[1]); - Assert.Equal("c", collection[2]); + + + Assert.AreEqual("aa", collection[0].ToString()); + + + Assert.AreEqual("b", collection[1].ToString()); + + + Assert.AreEqual("c", collection[2].ToString()); + + } - [Fact] + + + + + [TestMethod] + + public void Ignores_DotSegments() + + { + + var path = "./aa/./b/./c/."; + + var tokenizer = new PathTokenizer(path.AsMemory().Span); + + var segments = new List(); + + var collection = tokenizer.Fill(segments); - Assert.Equal("aa", collection[0]); - Assert.Equal("b", collection[1]); - Assert.Equal("c", collection[2]); + + + Assert.AreEqual("aa", collection[0].ToString()); + + + Assert.AreEqual("b", collection[1].ToString()); + + + Assert.AreEqual("c", collection[2].ToString()); + + } - [Fact] + + + + + [TestMethod] + + public void Ignores_DotDotSegments() + + { + + var path = "../aa/../b/../c/.."; + + var tokenizer = new PathTokenizer(path.AsMemory().Span); + + var segments = new List(); + + var collection = tokenizer.Fill(segments); - Assert.Equal("aa", collection[0]); - Assert.Equal("b", collection[1]); - Assert.Equal("c", collection[2]); + + + Assert.AreEqual("aa", collection[0].ToString()); + + + Assert.AreEqual("b", collection[1].ToString()); + + + Assert.AreEqual("c", collection[2].ToString()); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.Compatibility.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.Compatibility.cs index f68ea9169c4f..eee33c68b996 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.Compatibility.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.Compatibility.cs @@ -1,302 +1,915 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; +// Licensed to the .NET Foundation under one or more agreements. + + +// The .NET Foundation licenses this file to you under the MIT license. + + + + + namespace Microsoft.AspNetCore.StaticWebAssets.Tasks.Test; + + + + public partial class StaticWebAssetGlobMatcherTest + + { - [Fact] + + + [TestMethod] + + public void MatchingFileIsFound() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("alpha.txt"); + + var globMatcher = matcher.Build(); + + + + var match = globMatcher.Match("alpha.txt"); - Assert.True(match.IsMatch); - Assert.Equal("alpha.txt", match.Pattern); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("alpha.txt", match.Pattern); + + } - [Fact] + + + + + [TestMethod] + + public void MismatchedFileIsIgnored() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("alpha.txt"); + + var globMatcher = matcher.Build(); + + + + var match = globMatcher.Match("omega.txt"); - Assert.False(match.IsMatch); + + + Assert.IsFalse(match.IsMatch); + + } - [Fact] + + + + + [TestMethod] + + public void FolderNamesAreTraversed() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("beta/alpha.txt"); + + var globMatcher = matcher.Build(); + + + + var match = globMatcher.Match("beta/alpha.txt"); - Assert.True(match.IsMatch); - Assert.Equal("beta/alpha.txt", match.Pattern); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("beta/alpha.txt", match.Pattern); + + } - [Theory] - [InlineData(@"beta/alpha.txt", @"beta/alpha.txt")] - [InlineData(@"beta\alpha.txt", @"beta/alpha.txt")] - [InlineData(@"beta/alpha.txt", @"beta\alpha.txt")] - [InlineData(@"beta\alpha.txt", @"beta\alpha.txt")] - [InlineData(@"\beta\alpha.txt", @"beta/alpha.txt")] + + + + + [TestMethod] + + + [DataRow(@"beta/alpha.txt", @"beta/alpha.txt")] + + + [DataRow(@"beta\alpha.txt", @"beta/alpha.txt")] + + + [DataRow(@"beta/alpha.txt", @"beta\alpha.txt")] + + + [DataRow(@"beta\alpha.txt", @"beta\alpha.txt")] + + + [DataRow(@"\beta\alpha.txt", @"beta/alpha.txt")] + + public void SlashPolarityIsIgnored(string includePattern, string filePath) + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns(includePattern); + + var globMatcher = matcher.Build(); + + + + var match = globMatcher.Match(filePath); - Assert.True(match.IsMatch); - //Assert.Equal("beta/alpha.txt", match.Pattern); + + + Assert.IsTrue(match.IsMatch); + + + //Assert.AreEqual("beta/alpha.txt", match.Pattern); + + } - [Theory] - [InlineData(@"alpha.*", new[] { "alpha.txt" })] - [InlineData(@"*", new[] { "alpha.txt", "beta.txt", "gamma.dat" })] - [InlineData(@"*et*", new[] { "beta.txt" })] - [InlineData(@"*.*", new[] { "alpha.txt", "beta.txt", "gamma.dat" })] - [InlineData(@"b*et*x", new string[0])] - [InlineData(@"*.txt", new[] { "alpha.txt", "beta.txt" })] - [InlineData(@"b*et*t", new[] { "beta.txt" })] + + + + + [TestMethod] + + + [DataRow(@"alpha.*", new[] { "alpha.txt" })] + + + [DataRow(@"*", new[] { "alpha.txt", "beta.txt", "gamma.dat" })] + + + [DataRow(@"*et*", new[] { "beta.txt" })] + + + [DataRow(@"*.*", new[] { "alpha.txt", "beta.txt", "gamma.dat" })] + + + [DataRow(@"b*et*x", new string[0])] + + + [DataRow(@"*.txt", new[] { "alpha.txt", "beta.txt" })] + + + [DataRow(@"b*et*t", new[] { "beta.txt" })] + + public void CanPatternMatch(string includes, string[] expected) + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns(includes); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "alpha.txt", "beta.txt", "gamma.dat" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(expected, matches); + + + + + Assert.AreSequenceEqual(expected, matches); + + } - [Theory] - [InlineData(@"12345*5678", new string[0])] - [InlineData(@"1234*5678", new[] { "12345678" })] - [InlineData(@"12*23*", new string[0])] - [InlineData(@"12*3456*78", new[] { "12345678" })] - [InlineData(@"*45*56", new string[0])] - [InlineData(@"*67*78", new string[0])] + + + + + [TestMethod] + + + [DataRow(@"12345*5678", new string[0])] + + + [DataRow(@"1234*5678", new[] { "12345678" })] + + + [DataRow(@"12*23*", new string[0])] + + + [DataRow(@"12*3456*78", new[] { "12345678" })] + + + [DataRow(@"*45*56", new string[0])] + + + [DataRow(@"*67*78", new string[0])] + + public void PatternBeginAndEndCantOverlap(string includes, string[] expected) + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns(includes); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "12345678" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(expected, matches); + + + + + Assert.AreSequenceEqual(expected, matches); + + } - [Theory] - [InlineData(@"*alpha*/*", new[] { "alpha/hello.txt" })] - [InlineData(@"/*/*", new[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] - [InlineData(@"*/*", new[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] - [InlineData(@"/*.*/*", new string[] { })] - [InlineData(@"*.*/*", new string[] { })] - [InlineData(@"/*mm*/*", new[] { "gamma/hello.txt" })] - [InlineData(@"*mm*/*", new[] { "gamma/hello.txt" })] - [InlineData(@"/*alpha*/*", new[] { "alpha/hello.txt" })] + + + + + [TestMethod] + + + [DataRow(@"*alpha*/*", new[] { "alpha/hello.txt" })] + + + [DataRow(@"/*/*", new[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + + + [DataRow(@"*/*", new[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + + + [DataRow(@"/*.*/*", new string[] { })] + + + [DataRow(@"*.*/*", new string[] { })] + + + [DataRow(@"/*mm*/*", new[] { "gamma/hello.txt" })] + + + [DataRow(@"*mm*/*", new[] { "gamma/hello.txt" })] + + + [DataRow(@"/*alpha*/*", new[] { "alpha/hello.txt" })] + + public void PatternMatchingWorksInFolders(string includes, string[] expected) + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns(includes); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(expected, matches); + + + + + Assert.AreSequenceEqual(expected, matches); + + } - [Theory] - [InlineData(@"", new string[] { })] - [InlineData(@"./", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] - [InlineData(@"./alpha/hello.txt", new string[] { "alpha/hello.txt" })] - [InlineData(@"./**/hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] - [InlineData(@"././**/hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] - [InlineData(@"././**/./hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] - [InlineData(@"././**/./**/hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] - [InlineData(@"./*mm*/hello.txt", new string[] { "gamma/hello.txt" })] - [InlineData(@"./*mm*/*", new string[] { "gamma/hello.txt" })] + + + + + [TestMethod] + + + [DataRow(@"", new string[] { })] + + + [DataRow(@"./", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + + + [DataRow(@"./alpha/hello.txt", new string[] { "alpha/hello.txt" })] + + + [DataRow(@"./**/hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + + + [DataRow(@"././**/hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + + + [DataRow(@"././**/./hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + + + [DataRow(@"././**/./**/hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] + + + [DataRow(@"./*mm*/hello.txt", new string[] { "gamma/hello.txt" })] + + + [DataRow(@"./*mm*/*", new string[] { "gamma/hello.txt" })] + + public void PatternMatchingCurrent(string includePattern, string[] matchesExpected) + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns(includePattern); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(matchesExpected, matches); + + + + + Assert.AreSequenceEqual(matchesExpected, matches); + + } - [Fact] + + + + + [TestMethod] + + public void StarDotStarIsSameAsStar() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("*.*"); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "alpha.txt", "alpha.", ".txt", ".", "alpha", "txt" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(new[] { "alpha.txt", "alpha.", ".txt" }, matches); + + + + + Assert.AreSequenceEqual(new[] { "alpha.txt", "alpha.", ".txt" }, matches); + + } - [Fact] + + + + + [TestMethod] + + public void IncompletePatternsDoNotInclude() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("*/*.txt"); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "one/x.txt", "two/x.txt", "x.txt" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(new[] { "one/x.txt", "two/x.txt" }, matches); + + + + + Assert.AreSequenceEqual(new[] { "one/x.txt", "two/x.txt" }, matches); + + } - [Fact] + + + + + [TestMethod] + + public void IncompletePatternsDoNotExclude() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("*/*.txt"); + + matcher.AddExcludePatterns("one/hello.txt"); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "one/x.txt", "two/x.txt" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(new[] { "one/x.txt", "two/x.txt" }, matches); + + + + + Assert.AreSequenceEqual(new[] { "one/x.txt", "two/x.txt" }, matches); + + } - [Fact] + + + + + [TestMethod] + + public void TrailingRecursiveWildcardMatchesAllFiles() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("one/**"); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "one/x.txt", "two/x.txt", "one/x/y.txt" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(new[] { "one/x.txt", "one/x/y.txt" }, matches); + + + + + Assert.AreSequenceEqual(new[] { "one/x.txt", "one/x/y.txt" }, matches); + + } - [Fact] + + + + + [TestMethod] + + public void LeadingRecursiveWildcardMatchesAllLeadingPaths() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("**/*.cs"); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "one/x.cs", "two/x.cs", "one/two/x.cs", "x.cs", "one/x.txt", "two/x.txt", "one/two/x.txt", "x.txt" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(new[] { "one/x.cs", "two/x.cs", "one/two/x.cs", "x.cs" }, matches); + + + + + Assert.AreSequenceEqual(new[] { "one/x.cs", "two/x.cs", "one/two/x.cs", "x.cs" }, matches); + + } - [Fact] + + + + + [TestMethod] + + public void InnerRecursiveWildcardMustStartWithAndEndWith() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("one/**/*.cs"); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "one/x.cs", "two/x.cs", "one/two/x.cs", "x.cs", "one/x.txt", "two/x.txt", "one/two/x.txt", "x.txt" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(new[] { "one/x.cs", "one/two/x.cs" }, matches); + + + + + Assert.AreSequenceEqual(new[] { "one/x.cs", "one/two/x.cs" }, matches); + + } - [Fact] + + + + + [TestMethod] + + public void ExcludeMayEndInDirectoryName() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("*.cs", "*/*.cs", "*/*/*.cs"); + + matcher.AddExcludePatterns("bin/", "one/two/"); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "one/x.cs", "two/x.cs", "one/two/x.cs", "x.cs", "bin/x.cs", "bin/two/x.cs" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(new[] { "one/x.cs", "two/x.cs", "x.cs" }, matches); + + + + + Assert.AreSequenceEqual(new[] { "one/x.cs", "two/x.cs", "x.cs" }, matches); + + } - [Fact] + + + + + [TestMethod] + + public void RecursiveWildcardSurroundingContainsWith() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("**/x/**"); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "x/1", "1/x/2", "1/x", "x", "1", "1/2" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(new[] { "x/1", "1/x/2", "1/x", "x" }, matches); + + + + + Assert.AreSequenceEqual(new[] { "x/1", "1/x/2", "1/x", "x" }, matches); + + } - [Fact] + + + + + [TestMethod] + + public void SequentialFoldersMayBeRequired() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("a/b/**/1/2/**/2/3/**"); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "1/2/2/3/x", "1/2/3/y", "a/1/2/4/2/3/b", "a/2/3/1/2/b", "a/b/1/2/2/3/x", "a/b/1/2/3/y", "a/b/a/1/2/4/2/3/b", "a/b/a/2/3/1/2/b" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(new[] { "a/b/1/2/2/3/x", "a/b/a/1/2/4/2/3/b" }, matches); + + + + + Assert.AreSequenceEqual(new[] { "a/b/1/2/2/3/x", "a/b/a/1/2/4/2/3/b" }, matches); + + } - [Fact] + + + + + [TestMethod] + + public void RecursiveAloneIncludesEverything() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("**"); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "1/2/2/3/x", "1/2/3/y" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(new[] { "1/2/2/3/x", "1/2/3/y" }, matches); + + + + + Assert.AreSequenceEqual(new[] { "1/2/2/3/x", "1/2/3/y" }, matches); + + } - [Fact] + + + + + [TestMethod] + + public void ExcludeCanHaveSurroundingRecursiveWildcards() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("**"); + + matcher.AddExcludePatterns("**/x/**"); + + var globMatcher = matcher.Build(); + + + + var matches = new List { "x/1", "1/x/2", "1/x", "x", "1", "1/2" } + + .Where(file => globMatcher.Match(file).IsMatch) + + .ToArray(); - Assert.Equal(new[] { "1", "1/2" }, matches); + + + + + Assert.AreSequenceEqual(new[] { "1", "1/2" }, matches); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.cs index fb2746fb1ce5..a514f7208235 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.cs @@ -2,410 +2,1234 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + using System.Collections.Generic; + + using System.Linq; + + using System.Text; + + using System.Threading.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + + namespace Microsoft.AspNetCore.StaticWebAssets.Tasks.Test; + + + + // Set of things to test: + + // Literals 'a' + + // Multiple literals 'a/b' + + // Extensions '*.a' + + // Longer extensions first '*.a', '*.b.a' + + // Extensions at the beginning '*.a/b' + + // Extensions at the end 'a/*.b' + + // Extensions in the middle 'a/*.b/c' + + // Wildcard '*' + + // Wildcard at the beginning '*/a' + + // Wildcard at the end 'a/*' + + // Wildcard in the middle 'a/*/c' + + // Recursive wildcard '**' + + // Recursive wildcard at the beginning '**/a' + + // Recursive wildcard at the end 'a/**' + + // Recursive wildcard in the middle 'a/**/c' + + +[TestClass] + public partial class StaticWebAssetGlobMatcherTest + + { - [Theory] - [InlineData("**/*.razor.js", "Components/Pages/RegularComponent.razor.js", "Components/Pages/RegularComponent.razor.js")] - [InlineData("**/*.razor.js", "Components/User.Profile.Details.razor.js", "Components/User.Profile.Details.razor.js")] - [InlineData("**/*.razor.js", "Components/Area/Sub/Feature/User.Profile.Details.razor.js", "Components/Area/Sub/Feature/User.Profile.Details.razor.js")] - [InlineData("**/*.razor.js", "Components/Area/Sub/Feature/Deep.Component.Name.With.Many.Parts.razor.js", "Components/Area/Sub/Feature/Deep.Component.Name.With.Many.Parts.razor.js")] - [InlineData("**/*.cshtml.js", "Pages/Shared/_Host.cshtml.js", "Pages/Shared/_Host.cshtml.js")] - [InlineData("**/*.cshtml.js", "Areas/Admin/Pages/Dashboard.cshtml.js", "Areas/Admin/Pages/Dashboard.cshtml.js")] - [InlineData("*.lib.module.js", "Widget.lib.module.js", "Widget.lib.module.js")] - [InlineData("*.razor.css", "Component.razor.css", "Component.razor.css")] - [InlineData("*.cshtml.css", "View.cshtml.css", "View.cshtml.css")] - [InlineData("*.modules.json", "app.modules.json", "app.modules.json")] - [InlineData("*.lib.module.js", "Rcl.Client.Feature.lib.module.js", "Rcl.Client.Feature.lib.module.js")] + + + [TestMethod] + + + [DataRow("**/*.razor.js", "Components/Pages/RegularComponent.razor.js", "Components/Pages/RegularComponent.razor.js")] + + + [DataRow("**/*.razor.js", "Components/User.Profile.Details.razor.js", "Components/User.Profile.Details.razor.js")] + + + [DataRow("**/*.razor.js", "Components/Area/Sub/Feature/User.Profile.Details.razor.js", "Components/Area/Sub/Feature/User.Profile.Details.razor.js")] + + + [DataRow("**/*.razor.js", "Components/Area/Sub/Feature/Deep.Component.Name.With.Many.Parts.razor.js", "Components/Area/Sub/Feature/Deep.Component.Name.With.Many.Parts.razor.js")] + + + [DataRow("**/*.cshtml.js", "Pages/Shared/_Host.cshtml.js", "Pages/Shared/_Host.cshtml.js")] + + + [DataRow("**/*.cshtml.js", "Areas/Admin/Pages/Dashboard.cshtml.js", "Areas/Admin/Pages/Dashboard.cshtml.js")] + + + [DataRow("*.lib.module.js", "Widget.lib.module.js", "Widget.lib.module.js")] + + + [DataRow("*.razor.css", "Component.razor.css", "Component.razor.css")] + + + [DataRow("*.cshtml.css", "View.cshtml.css", "View.cshtml.css")] + + + [DataRow("*.modules.json", "app.modules.json", "app.modules.json")] + + + [DataRow("*.lib.module.js", "Rcl.Client.Feature.lib.module.js", "Rcl.Client.Feature.lib.module.js")] + + public void Can_Match_WellKnownExistingPatterns(string pattern, string path, string expectedStem) + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns(pattern); + + var globMatcher = matcher.Build(); + + + + var match = globMatcher.Match(path); - Assert.True(match.IsMatch); - Assert.Equal(pattern, match.Pattern); - Assert.Equal(expectedStem, match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual(pattern, match.Pattern); + + + Assert.AreEqual(expectedStem, match.Stem); + + } - [Fact] + + + [TestMethod] + + public void CanMatchLiterals() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("a"); + + var globMatcher = matcher.Build(); + + + + var match = globMatcher.Match("a"); - Assert.True(match.IsMatch); - Assert.Equal("a", match.Pattern); - Assert.Equal("a", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("a", match.Pattern); + + + Assert.AreEqual("a", match.Stem); + + } - [Fact] + + + + + [TestMethod] + + public void CanMatchMultipleLiterals() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("a/b"); + + var globMatcher = matcher.Build(); + + + + var match = globMatcher.Match("a/b"); - Assert.True(match.IsMatch); - Assert.Equal("a/b", match.Pattern); - Assert.Equal("b", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("a/b", match.Pattern); + + + Assert.AreEqual("b", match.Stem); + + } - [Fact] + + + + + [TestMethod] + + public void CanMatchExtensions() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("*.a"); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match("a.a"); - Assert.True(match.IsMatch); - Assert.Equal("*.a", match.Pattern); - Assert.Equal("a.a", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("*.a", match.Pattern); + + + Assert.AreEqual("a.a", match.Stem); + + } - [Fact] + + + + + [TestMethod] + + public void MatchesLongerExtensionsFirst() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("*.a", "*.b.a"); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match("c.b.a"); - Assert.True(match.IsMatch); - Assert.Equal("*.b.a", match.Pattern); - Assert.Equal("c.b.a", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("*.b.a", match.Pattern); + + + Assert.AreEqual("c.b.a", match.Stem); + + } - [Fact] + + + + + [TestMethod] + + public void CanMatchExtensionsAtTheBeginning() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("*.a/b"); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match("c.a/b"); - Assert.True(match.IsMatch); - Assert.Equal("*.a/b", match.Pattern); - Assert.Equal("b", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("*.a/b", match.Pattern); + + + Assert.AreEqual("b", match.Stem); + + } - [Fact] + + + + + [TestMethod] + + public void CanMatchExtensionsAtTheEnd() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("a/*.b"); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match("a/c.b"); - Assert.True(match.IsMatch); - Assert.Equal("a/*.b", match.Pattern); - Assert.Equal("c.b", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("a/*.b", match.Pattern); + + + Assert.AreEqual("c.b", match.Stem); + + } - [Fact] + + + + + [TestMethod] + + public void CanMatchExtensionsInMiddle() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("a/*.b/c"); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match("a/d.b/c"); - Assert.True(match.IsMatch); - Assert.Equal("a/*.b/c", match.Pattern); - Assert.Equal("c", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("a/*.b/c", match.Pattern); + + + Assert.AreEqual("c", match.Stem); + + } - [Theory] - [InlineData("a*")] - [InlineData("*a")] - [InlineData("?")] - [InlineData("*?")] - [InlineData("?*")] - [InlineData("**a")] - [InlineData("a**")] - [InlineData("**?")] - [InlineData("?**")] + + + + + [TestMethod] + + + [DataRow("a*")] + + + [DataRow("*a")] + + + [DataRow("?")] + + + [DataRow("*?")] + + + [DataRow("?*")] + + + [DataRow("**a")] + + + [DataRow("a**")] + + + [DataRow("**?")] + + + [DataRow("?**")] + + public void CanMatchComplexSegments(string pattern) + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns(pattern); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match("a"); - Assert.True(match.IsMatch); - Assert.Equal(pattern, match.Pattern); - Assert.Equal("a", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual(pattern, match.Pattern); + + + Assert.AreEqual("a", match.Stem); + + } - [Theory] - [InlineData("a?", "aa", true)] - [InlineData("a?", "a", false)] - [InlineData("a?", "aaa", false)] - [InlineData("?a", "aa", true)] - [InlineData("?a", "a", false)] - [InlineData("?a", "aaa", false)] - [InlineData("a?a", "aaa", true)] - [InlineData("a?a", "aba", true)] - [InlineData("a?a", "abaa", false)] - [InlineData("a?a", "ab", false)] + + + + + [TestMethod] + + + [DataRow("a?", "aa", true)] + + + [DataRow("a?", "a", false)] + + + [DataRow("a?", "aaa", false)] + + + [DataRow("?a", "aa", true)] + + + [DataRow("?a", "a", false)] + + + [DataRow("?a", "aaa", false)] + + + [DataRow("a?a", "aaa", true)] + + + [DataRow("a?a", "aba", true)] + + + [DataRow("a?a", "abaa", false)] + + + [DataRow("a?a", "ab", false)] + + public void QuestionMarksMatchSingleCharacter(string pattern, string input, bool expectedMatchResult) + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns(pattern); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match(input); - Assert.Equal(expectedMatchResult, match.IsMatch); + + + Assert.AreEqual(expectedMatchResult, match.IsMatch); + + if(expectedMatchResult) + + + { + + + Assert.AreEqual(pattern, match.Pattern); + + + Assert.AreEqual(input, match.Stem); + + + } + + + else + + + { + + + Assert.IsNull(match.Pattern); + + + Assert.IsNull(match.Stem); + + + } + + + } + + + + + + [TestMethod] + + + [DataRow("a??", "aaa", true)] + + + [DataRow("a??", "aa", false)] + + + [DataRow("a??", "aaaa", false)] + + + [DataRow("?a?", "aaa", true)] + + + [DataRow("?a?", "aa", false)] + + + [DataRow("?a?", "aaaa", false)] + + + [DataRow("??a", "aaa", true)] + + + [DataRow("??a", "aa", false)] + + + [DataRow("??a", "aaaa", false)] + + + [DataRow("a??a", "aaaa", true)] + + + [DataRow("a??a", "aaba", true)] + + + [DataRow("a??a", "aabaa", false)] + + + [DataRow("a??a", "aba", false)] + + + public void MultipleQuestionMarksMatchExactlyTheNumberOfCharacters(string pattern, string input, bool expectedMatchResult) + + + { + + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + + matcher.AddIncludePatterns(pattern); + + + var globMatcher = matcher.Build(); + + + var match = globMatcher.Match(input); + + + Assert.AreEqual(expectedMatchResult, match.IsMatch); + + + if (expectedMatchResult) + + + { + + + Assert.AreEqual(pattern, match.Pattern); + + + Assert.AreEqual(input, match.Stem); + + + } + + + else + + { - Assert.Equal(pattern, match.Pattern); - Assert.Equal(input, match.Stem); + + + Assert.IsNull(match.Pattern); + + + Assert.IsNull(match.Stem); + + } + + + } + + + + + + [TestMethod] + + + [DataRow("a*", "a", true)] + + + [DataRow("a*", "aa", true)] + + + [DataRow("a*", "aaa", true)] + + + [DataRow("a*", "aaaa", true)] + + + [DataRow("a*", "aaaaa", true)] + + + [DataRow("a*", "aaaaaa", true)] + + + [DataRow("*a", "a", true)] + + + [DataRow("*a", "aa", true)] + + + [DataRow("*a", "aaa", true)] + + + [DataRow("*a", "aaaa", true)] + + + [DataRow("*a", "aaaaa", true)] + + + [DataRow("a*a", "a", false)] + + + [DataRow("a*a", "aa", true)] + + + [DataRow("a*a", "aaa", true)] + + + [DataRow("a*a", "aaaaa", true)] + + + [DataRow("a*a", "aaaaaa", true)] + + + [DataRow("a*a", "aba", true)] + + + [DataRow("a*a", "abaa", true)] + + + [DataRow("a*a", "abba", true)] + + + [DataRow("a*b", "ab", true)] + + + public void WildCardsMatchZeroOrMoreCharacters(string pattern, string input, bool expectedMatchResult) + + + { + + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + + matcher.AddIncludePatterns(pattern); + + + var globMatcher = matcher.Build(); + + + var match = globMatcher.Match(input); + + + Assert.AreEqual(expectedMatchResult, match.IsMatch); + + + if (expectedMatchResult) + + + { + + + Assert.AreEqual(pattern, match.Pattern); + + + Assert.AreEqual(input, match.Stem); + + + } + + else + + { - Assert.Null(match.Pattern); - Assert.Null(match.Stem); + + + Assert.IsNull(match.Pattern); + + + Assert.IsNull(match.Stem); + + } + + } - [Theory] - [InlineData("a??", "aaa", true)] - [InlineData("a??", "aa", false)] - [InlineData("a??", "aaaa", false)] - [InlineData("?a?", "aaa", true)] - [InlineData("?a?", "aa", false)] - [InlineData("?a?", "aaaa", false)] - [InlineData("??a", "aaa", true)] - [InlineData("??a", "aa", false)] - [InlineData("??a", "aaaa", false)] - [InlineData("a??a", "aaaa", true)] - [InlineData("a??a", "aaba", true)] - [InlineData("a??a", "aabaa", false)] - [InlineData("a??a", "aba", false)] - public void MultipleQuestionMarksMatchExactlyTheNumberOfCharacters(string pattern, string input, bool expectedMatchResult) - { - var matcher = new StaticWebAssetGlobMatcherBuilder(); - matcher.AddIncludePatterns(pattern); - var globMatcher = matcher.Build(); - var match = globMatcher.Match(input); - Assert.Equal(expectedMatchResult, match.IsMatch); - if (expectedMatchResult) - { - Assert.Equal(pattern, match.Pattern); - Assert.Equal(input, match.Stem); - } - else - { - Assert.Null(match.Pattern); - Assert.Null(match.Stem); - } - } - [Theory] - [InlineData("a*", "a", true)] - [InlineData("a*", "aa", true)] - [InlineData("a*", "aaa", true)] - [InlineData("a*", "aaaa", true)] - [InlineData("a*", "aaaaa", true)] - [InlineData("a*", "aaaaaa", true)] - [InlineData("*a", "a", true)] - [InlineData("*a", "aa", true)] - [InlineData("*a", "aaa", true)] - [InlineData("*a", "aaaa", true)] - [InlineData("*a", "aaaaa", true)] - [InlineData("a*a", "a", false)] - [InlineData("a*a", "aa", true)] - [InlineData("a*a", "aaa", true)] - [InlineData("a*a", "aaaaa", true)] - [InlineData("a*a", "aaaaaa", true)] - [InlineData("a*a", "aba", true)] - [InlineData("a*a", "abaa", true)] - [InlineData("a*a", "abba", true)] - [InlineData("a*b", "ab", true)] - public void WildCardsMatchZeroOrMoreCharacters(string pattern, string input, bool expectedMatchResult) - { - var matcher = new StaticWebAssetGlobMatcherBuilder(); - matcher.AddIncludePatterns(pattern); - var globMatcher = matcher.Build(); - var match = globMatcher.Match(input); - Assert.Equal(expectedMatchResult, match.IsMatch); - if (expectedMatchResult) - { - Assert.Equal(pattern, match.Pattern); - Assert.Equal(input, match.Stem); - } - else - { - Assert.Null(match.Pattern); - Assert.Null(match.Stem); - } - } - [Theory] - [InlineData("*?a", "a", false)] - [InlineData("*?a", "aa", true)] - [InlineData("*?a", "aaa", true)] - [InlineData("*?a", "aaaa", true)] - [InlineData("*?a", "aaaaa", true)] - [InlineData("*?a", "aaaaaa", true)] - [InlineData("*??a", "aa", false)] - [InlineData("*??a", "aaa", true)] - [InlineData("*???a", "aaa", false)] - [InlineData("*???a", "aaaa", true)] - [InlineData("*????a", "aaaa", false)] - [InlineData("*????a", "aaaaa", true)] - [InlineData("*?????a", "aaaaa", false)] - [InlineData("*?????a", "aaaaaa", true)] - [InlineData("*??????a", "aaaaaa", false)] - [InlineData("*??????a", "aaaaaaa", true)] - [InlineData("a*?", "a", false)] - [InlineData("a*?", "aa", true)] - [InlineData("a*?", "aaa", true)] - [InlineData("a*?", "aaaa", true)] - [InlineData("a*?", "aaaaa", true)] - [InlineData("a*?", "aaaaaa", true)] - [InlineData("a*??", "aa", false)] - [InlineData("a*??", "aaa", true)] - [InlineData("a*???", "aaa", false)] - [InlineData("a*???", "aaaa", true)] - [InlineData("a*????", "aaaa", false)] - [InlineData("a*????", "aaaaa", true)] - [InlineData("a*?????", "aaaaa", false)] - [InlineData("a*?????", "aaaaaa", true)] - [InlineData("a*??????", "aaaaaa", false)] - [InlineData("a*??????", "aaaaaaa", true)] + + + [TestMethod] + + + [DataRow("*?a", "a", false)] + + + [DataRow("*?a", "aa", true)] + + + [DataRow("*?a", "aaa", true)] + + + [DataRow("*?a", "aaaa", true)] + + + [DataRow("*?a", "aaaaa", true)] + + + [DataRow("*?a", "aaaaaa", true)] + + + [DataRow("*??a", "aa", false)] + + + [DataRow("*??a", "aaa", true)] + + + [DataRow("*???a", "aaa", false)] + + + [DataRow("*???a", "aaaa", true)] + + + [DataRow("*????a", "aaaa", false)] + + + [DataRow("*????a", "aaaaa", true)] + + + [DataRow("*?????a", "aaaaa", false)] + + + [DataRow("*?????a", "aaaaaa", true)] + + + [DataRow("*??????a", "aaaaaa", false)] + + + [DataRow("*??????a", "aaaaaaa", true)] + + + [DataRow("a*?", "a", false)] + + + [DataRow("a*?", "aa", true)] + + + [DataRow("a*?", "aaa", true)] + + + [DataRow("a*?", "aaaa", true)] + + + [DataRow("a*?", "aaaaa", true)] + + + [DataRow("a*?", "aaaaaa", true)] + + + [DataRow("a*??", "aa", false)] + + + [DataRow("a*??", "aaa", true)] + + + [DataRow("a*???", "aaa", false)] + + + [DataRow("a*???", "aaaa", true)] + + + [DataRow("a*????", "aaaa", false)] + + + [DataRow("a*????", "aaaaa", true)] + + + [DataRow("a*?????", "aaaaa", false)] + + + [DataRow("a*?????", "aaaaaa", true)] + + + [DataRow("a*??????", "aaaaaa", false)] + + + [DataRow("a*??????", "aaaaaaa", true)] + + + + public void SingleWildcardPrecededOrSucceededByQuestionMarkRequireMinimumNumberOfCharacters(string pattern, string input, bool expectedMatchResult) + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns(pattern); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match(input); - Assert.Equal(expectedMatchResult, match.IsMatch); + + + Assert.AreEqual(expectedMatchResult, match.IsMatch); + + if (expectedMatchResult) + + { - Assert.Equal(pattern, match.Pattern); - Assert.Equal(input, match.Stem); + + + Assert.AreEqual(pattern, match.Pattern); + + + Assert.AreEqual(input, match.Stem); + + } + + else + + { - Assert.Null(match.Pattern); - Assert.Null(match.Stem); + + + Assert.IsNull(match.Pattern); + + + Assert.IsNull(match.Stem); + + } + + } - [Fact] + + + + + [TestMethod] + + public void CanMatchWildCard() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("*"); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match("a"); - Assert.True(match.IsMatch); - Assert.Equal("*", match.Pattern); - Assert.Equal("a", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("*", match.Pattern); + + + Assert.AreEqual("a", match.Stem); + + } - [Fact] + + + + + [TestMethod] + + public void CanMatchWildCardAtTheBeginning() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("*/a"); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match("c/a"); - Assert.True(match.IsMatch); - Assert.Equal("*/a", match.Pattern); - Assert.Equal("a", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("*/a", match.Pattern); + + + Assert.AreEqual("a", match.Stem); + + } - [Fact] + + + + + [TestMethod] + + public void CanMatchWildCardAtTheEnd() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("a/*"); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match("a/c"); - Assert.True(match.IsMatch); - Assert.Equal("a/*", match.Pattern); - Assert.Equal("c", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("a/*", match.Pattern); + + + Assert.AreEqual("c", match.Stem); + + } - [Fact] + + + + + [TestMethod] + + public void CanMatchWildCardInMiddle() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("a/*/c"); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match("a/b/c"); - Assert.True(match.IsMatch); - Assert.Equal("a/*/c", match.Pattern); - Assert.Equal("c", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("a/*/c", match.Pattern); + + + Assert.AreEqual("c", match.Stem); + + } - [Fact] + + + + + [TestMethod] + + public void CanMatchRecursiveWildCard() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("**"); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match("a/b/c"); - Assert.True(match.IsMatch); - Assert.Equal("**", match.Pattern); - Assert.Equal("a/b/c", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("**", match.Pattern); + + + Assert.AreEqual("a/b/c", match.Stem); + + } - [Fact] + + + + + [TestMethod] + + public void CanMatchRecursiveWildCardAtTheBeginning() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("**/a"); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match("c/b/a"); - Assert.True(match.IsMatch); - Assert.Equal("**/a", match.Pattern); - Assert.Equal("c/b/a", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("**/a", match.Pattern); + + + Assert.AreEqual("c/b/a", match.Stem); + + } - [Fact] + + + + + [TestMethod] + + public void CanMatchRecursiveWildCardAtTheEnd() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("a/**"); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match("a/b/c"); - Assert.True(match.IsMatch); - Assert.Equal("a/**", match.Pattern); - Assert.Equal("b/c", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("a/**", match.Pattern); + + + Assert.AreEqual("b/c", match.Stem); + + } - [Fact] + + + + + [TestMethod] + + public void CanMatchRecursiveWildCardInMiddle() + + { + + var matcher = new StaticWebAssetGlobMatcherBuilder(); + + matcher.AddIncludePatterns("a/**/c"); + + var globMatcher = matcher.Build(); + + var match = globMatcher.Match("a/b/c"); - Assert.True(match.IsMatch); - Assert.Equal("a/**/c", match.Pattern); - Assert.Equal("b/c", match.Stem); + + + Assert.IsTrue(match.IsMatch); + + + Assert.AreEqual("a/**/c", match.Pattern); + + + Assert.AreEqual("b/c", match.Stem); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesMultiThreadingTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesMultiThreadingTest.cs index 4e7872590751..05c4d8a46ff2 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesMultiThreadingTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesMultiThreadingTest.cs @@ -1,131 +1,395 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + + + + // Test parallelization is disabled assembly-wide via + + // [assembly:CollectionBehavior(DisableTestParallelization = true)] in + + // LegacyStaticWebAssetsV1IntegrationTest.cs, which already isolates the + + // process-CWD mutation this test performs. + + +[TestClass] + public class MergeConfigurationPropertiesMultiThreadingTest + + { - [Fact] + + + [TestMethod] + + public void ResolvesProjectReferencePathRelativeToTaskEnvironmentProjectDirectory_NotProcessCurrentDirectory() + + { + + // Scope of this test: verify that the *ProjectReferences* side of the path-equality check + + // in FindMatchingProject is rooted against TaskEnvironment.ProjectDirectory rather than the + + // process current directory. The *CandidateConfigurations* side (configuration.GetMetadata("FullPath")) + + // is intentionally passed as an already-absolute path here — that mirrors what MSBuild's + + // well-known %(FullPath) modifier produces in MT mode (it resolves against the per-task + + // AsyncLocal working directory, not the process CWD) and is also what the equality check + + // requires in order to compare against the OS-canonical form on the other side. + + // + + // Layout (must place the two roots in different subtrees so a relative + + // "../reference/myRcl.csproj" produces *different* absolute paths + + // depending on which root it is resolved against): + + // /project/ <-- TaskEnvironment.ProjectDirectory + + // /project/../reference/ <-- candidate's real location + + // /decoy/spawn/ <-- process CWD (the "decoy") + + // /decoy/reference/ <-- where the old CWD-based + + // Path.GetFullPath would point + + var testRoot = Path.Combine(AppContext.BaseDirectory, nameof(MergeConfigurationPropertiesMultiThreadingTest), Guid.NewGuid().ToString("N")); + + var projectDir = Path.Combine(testRoot, "project"); + + var spawnDir = Path.Combine(testRoot, "decoy", "spawn"); + + Directory.CreateDirectory(projectDir); + + Directory.CreateDirectory(spawnDir); + + + + var relativeProjectReference = Path.Combine("..", "reference", "myRcl.csproj"); + + + + // Candidate FullPath represents what MSBuild's well-known FullPath modifier + + // would produce in MT mode: the path resolved against the project directory. + + var candidateAbsolutePath = Path.GetFullPath(Path.Combine(projectDir, relativeProjectReference)); + + + + // Sanity: the decoy CWD would produce a *different* absolute path for the + + // same relative input — that is what proves the equality check is using + + // the right root. + + var decoyAbsolutePath = Path.GetFullPath(Path.Combine(spawnDir, relativeProjectReference)); + + candidateAbsolutePath.Should().NotBe(decoyAbsolutePath, + + "the test setup must place project and decoy in different parents so the migration is actually exercised"); + + + + var originalCurrentDirectory = Directory.GetCurrentDirectory(); + + try + + { + + Directory.SetCurrentDirectory(spawnDir); + + + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new MergeConfigurationProperties + + { + + BuildEngine = buildEngine.Object, + + TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir), + + CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(candidateAbsolutePath) }, + + ProjectReferences = new[] + + { + + CreateProjectReference( + + project: Path.Combine("..", "myRcl", "myRcl.csproj"), + + // Relative MSBuildSourceProjectFile — the task must resolve + + // this against the TaskEnvironment.ProjectDirectory, not + + // against Environment.CurrentDirectory. + + msBuildSourceProjectFile: relativeProjectReference, + + undefineProperties: "TargetFramework;RuntimeIdentifier") + + } + + }; + + + + var result = task.Execute(); + + + + result.Should().BeTrue("the task must find the project reference by absolutizing against the project directory, not the process CWD"); + + errorMessages.Should().BeEmpty(); + + task.ProjectConfigurations.Should().HaveCount(1); + + task.ProjectConfigurations[0].GetMetadata("Source").Should().Be("myRcl"); + + } + + finally + + { + + Directory.SetCurrentDirectory(originalCurrentDirectory); + + if (Directory.Exists(testRoot)) + + { + + Directory.Delete(testRoot, recursive: true); + + } + + } + + } + + + + private static ITaskItem CreateCandidateProjectConfiguration(string project) + + { + + return new TaskItem(project, new Dictionary + + { + + ["AdditionalPublishProperties"] = "", + + ["GetBuildAssetsTargets"] = "GetCurrentProjectBuildStaticWebAssetItems", + + ["GetPublishAssetsTargets"] = "ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems", + + ["Version"] = "2", + + ["AdditionalBuildProperties"] = "", + + ["Source"] = "myRcl", + + ["AdditionalPublishPropertiesToRemove"] = "", + + ["AdditionalBuildPropertiesToRemove"] = "", + + }); + + } + + + + private static ITaskItem CreateProjectReference( + + string project, + + string msBuildSourceProjectFile, + + string undefineProperties = "") + + { + + return new TaskItem(project, new Dictionary + + { + + ["MSBuildSourceProjectFile"] = msBuildSourceProjectFile, + + ["UndefineProperties"] = undefineProperties, + + ["SetConfiguration"] = "", + + ["SetPlatform"] = "", + + ["SetTargetFramework"] = "", + + ["GlobalPropertiesToRemove"] = "", + + }); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesTest.cs index a19ca4bd7a1c..72d84beb67d3 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesTest.cs @@ -1,298 +1,896 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { + + + [TestClass] + public class MergeConfigurationPropertiesTest + + { - [Fact] + + + [TestMethod] + + public void MergesProjectConfigurationWithProjectReferenceWhenMatchingReferenceFound() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var referenceProjectFile = Path.Combine("..", "reference", "myRcl.csproj"); + + var task = new MergeConfigurationProperties + + { + + BuildEngine = buildEngine.Object, + + CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(Path.GetFullPath(referenceProjectFile)) }, + + ProjectReferences = new[] { + + CreateProjectReference( + + project: Path.Combine("..", "myRcl", "myRcl.csproj"), + + msBuildSourceProjectFile: Path.GetFullPath(referenceProjectFile), + + undefineProperties: Path.Combine(";TargetFramework;RuntimeIdentifier")) + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.ProjectConfigurations.Should().HaveCount(1); + + var config = task.ProjectConfigurations[0]; + + config.GetMetadata("Source").Should().Be("myRcl"); + + config.GetMetadata("GetBuildAssetsTargets").Should().Be("GetCurrentProjectBuildStaticWebAssetItems"); + + config.GetMetadata("GetPublishAssetsTargets").Should() + + .Be("ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems"); + + config.GetMetadata("Version").Should().Be("2"); + + config.GetMetadata("AdditionalBuildProperties").Should().Be(""); + + config.GetMetadata("AdditionalBuildPropertiesToRemove").Should().Be("TargetFramework;RuntimeIdentifier"); + + config.GetMetadata("AdditionalPublishProperties").Should().Be(""); + + config.GetMetadata("AdditionalPublishPropertiesToRemove").Should().Be("TargetFramework;RuntimeIdentifier"); + + } - [Fact] + + + + + [TestMethod] + + public void MergesProjectConfigurationWithProjectReference_UsesOSCasingForMatching() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var referenceProjectFile = Path.Combine("..", "reference", "myRcl.csproj"); + + var task = new MergeConfigurationProperties + + { + + BuildEngine = buildEngine.Object, + + CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(Path.GetFullPath(referenceProjectFile)) }, + + ProjectReferences = new[] + + { + + CreateProjectReference( + + project: Path.Combine("..", "myRCL", "myRcl.csproj"), + + msBuildSourceProjectFile: Path.GetFullPath(referenceProjectFile).ToUpperInvariant(), + + undefineProperties: Path.Combine(";TargetFramework;RuntimeIdentifier")) + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(OperatingSystem.IsWindows()); + + } - [Fact] + + + + + [TestMethod] + + public void FailswhenProjectReferenceNotFound() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var referenceProjectFile = Path.Combine("..", "reference", "myRcl.csproj"); + + var task = new MergeConfigurationProperties + + { + + BuildEngine = buildEngine.Object, + + CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(Path.GetFullPath(referenceProjectFile)) }, + + ProjectReferences = Array.Empty() + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(false); + + } - [Fact] + + + + + [TestMethod] + + public void MergesProjectConfigurationRespectsSetTargetFramework() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var referenceProjectFile = Path.Combine("..", "reference", "myRcl.csproj"); + + var task = new MergeConfigurationProperties + + { + + BuildEngine = buildEngine.Object, + + CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(Path.GetFullPath(referenceProjectFile)) }, + + ProjectReferences = new[] { + + CreateProjectReference( + + project: Path.Combine("..", "myRcl", "myRcl.csproj"), + + msBuildSourceProjectFile: Path.GetFullPath(referenceProjectFile), + + setTargetFramework: $"TargetFramework={ToolsetInfo.CurrentTargetFramework}") + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.ProjectConfigurations.Should().HaveCount(1); + + var config = task.ProjectConfigurations[0]; + + config.GetMetadata("Source").Should().Be("myRcl"); + + config.GetMetadata("GetBuildAssetsTargets").Should().Be("GetCurrentProjectBuildStaticWebAssetItems"); + + config.GetMetadata("GetPublishAssetsTargets").Should() + + .Be("ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems"); + + config.GetMetadata("Version").Should().Be("2"); + + config.GetMetadata("AdditionalBuildProperties").Should().Be($"TargetFramework={ToolsetInfo.CurrentTargetFramework}"); + + config.GetMetadata("AdditionalBuildPropertiesToRemove").Should().Be(""); + + config.GetMetadata("AdditionalPublishProperties").Should().Be($"TargetFramework={ToolsetInfo.CurrentTargetFramework}"); + + config.GetMetadata("AdditionalPublishPropertiesToRemove").Should().Be(""); + + } - [Fact] + + + + + [TestMethod] + + public void MergesProjectConfigurationRespectsSetPlatform() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var referenceProjectFile = Path.Combine("..", "reference", "myRcl.csproj"); + + var task = new MergeConfigurationProperties + + { + + BuildEngine = buildEngine.Object, + + CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(Path.GetFullPath(referenceProjectFile)) }, + + ProjectReferences = new[] { + + CreateProjectReference( + + project: Path.Combine("..", "myRcl", "myRcl.csproj"), + + msBuildSourceProjectFile: Path.GetFullPath(referenceProjectFile), + + setPlatform: "RuntimeIdentifier=win-x64") + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.ProjectConfigurations.Should().HaveCount(1); + + var config = task.ProjectConfigurations[0]; + + config.GetMetadata("Source").Should().Be("myRcl"); + + config.GetMetadata("GetBuildAssetsTargets").Should().Be("GetCurrentProjectBuildStaticWebAssetItems"); + + config.GetMetadata("GetPublishAssetsTargets").Should() + + .Be("ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems"); + + config.GetMetadata("Version").Should().Be("2"); + + config.GetMetadata("AdditionalBuildProperties").Should().Be("RuntimeIdentifier=win-x64"); + + config.GetMetadata("AdditionalBuildPropertiesToRemove").Should().Be(""); + + config.GetMetadata("AdditionalPublishProperties").Should().Be("RuntimeIdentifier=win-x64"); + + config.GetMetadata("AdditionalPublishPropertiesToRemove").Should().Be(""); + + } - [Fact] + + + + + [TestMethod] + + public void MergesProjectConfigurationRespectsSetConfiguration() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var referenceProjectFile = Path.Combine("..", "reference", "myRcl.csproj"); + + var task = new MergeConfigurationProperties + + { + + BuildEngine = buildEngine.Object, + + CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(Path.GetFullPath(referenceProjectFile)) }, + + ProjectReferences = new[] { + + CreateProjectReference( + + project: Path.Combine("..", "myRcl", "myRcl.csproj"), + + msBuildSourceProjectFile: Path.GetFullPath(referenceProjectFile), + + setConfiguration: "Configuration=Release") + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.ProjectConfigurations.Should().HaveCount(1); + + var config = task.ProjectConfigurations[0]; + + config.GetMetadata("Source").Should().Be("myRcl"); + + config.GetMetadata("GetBuildAssetsTargets").Should().Be("GetCurrentProjectBuildStaticWebAssetItems"); + + config.GetMetadata("GetPublishAssetsTargets").Should() + + .Be("ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems"); + + config.GetMetadata("Version").Should().Be("2"); + + config.GetMetadata("AdditionalBuildProperties").Should().Be("Configuration=Release"); + + config.GetMetadata("AdditionalBuildPropertiesToRemove").Should().Be(""); + + config.GetMetadata("AdditionalPublishProperties").Should().Be("Configuration=Release"); + + config.GetMetadata("AdditionalPublishPropertiesToRemove").Should().Be(""); + + } - [Fact] + + + + + [TestMethod] + + public void MergesProjectConfigurationRespectsGlobalPropertiesToRemove() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var referenceProjectFile = Path.Combine("..", "reference", "myRcl.csproj"); + + var task = new MergeConfigurationProperties + + { + + BuildEngine = buildEngine.Object, + + CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(Path.GetFullPath(referenceProjectFile)) }, + + ProjectReferences = new[] { + + CreateProjectReference( + + project: Path.Combine("..", "myRcl", "myRcl.csproj"), + + msBuildSourceProjectFile: Path.GetFullPath(referenceProjectFile), + + undefineProperties: "TargetFramework", + + globalPropertiesToRemove: "RuntimeIdentifier") + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.ProjectConfigurations.Should().HaveCount(1); + + var config = task.ProjectConfigurations[0]; + + config.GetMetadata("Source").Should().Be("myRcl"); + + config.GetMetadata("GetBuildAssetsTargets").Should().Be("GetCurrentProjectBuildStaticWebAssetItems"); + + config.GetMetadata("GetPublishAssetsTargets").Should() + + .Be("ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems"); + + config.GetMetadata("Version").Should().Be("2"); + + config.GetMetadata("AdditionalBuildProperties").Should().Be(""); + + config.GetMetadata("AdditionalBuildPropertiesToRemove").Should().Be("RuntimeIdentifier;TargetFramework"); + + config.GetMetadata("AdditionalPublishProperties").Should().Be(""); + + config.GetMetadata("AdditionalPublishPropertiesToRemove").Should().Be("RuntimeIdentifier;TargetFramework"); + + } + + + + private static ITaskItem CreateCandidateProjectConfiguration(string project) + + { + + return new TaskItem(Path.GetFullPath(project), new Dictionary + + { + + ["AdditionalPublishProperties"] = "", + + ["GetBuildAssetsTargets"] = "GetCurrentProjectBuildStaticWebAssetItems", + + ["GetPublishAssetsTargets"] = "ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems", + + ["Version"] = "2", + + ["AdditionalBuildProperties"] = "", + + ["Source"] = "myRcl", + + ["AdditionalPublishPropertiesToRemove"] = "", + + ["AdditionalBuildPropertiesToRemove"] = "", + + }); + + } + + + + private static ITaskItem CreateProjectReference( + + string project, + + string msBuildSourceProjectFile, + + string undefineProperties = "", + + string setConfiguration = "", + + string setPlatform = "", + + string setTargetFramework = "", + + string globalPropertiesToRemove = "") + + { + + return new TaskItem(project, new Dictionary + + { + + ["MSBuildSourceProjectFile"] = msBuildSourceProjectFile, + + ["UndefineProperties"] = undefineProperties, + + ["SetConfiguration"] = setConfiguration, + + ["SetPlatform"] = setPlatform, + + ["SetTargetFramework"] = setTargetFramework, + + ["GlobalPropertiesToRemove"] = globalPropertiesToRemove, + + }); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/OverrideHtmlAssetPlaceholdersTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/OverrideHtmlAssetPlaceholdersTest.cs index 31e709b85f12..e8b0339b0b5e 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/OverrideHtmlAssetPlaceholdersTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/OverrideHtmlAssetPlaceholdersTest.cs @@ -1,290 +1,872 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using System.Text.RegularExpressions; + + + + namespace Microsoft.AspNetCore.Razor.Tasks; + + + + +[TestClass] + public class OverrideHtmlAssetPlaceholdersTest + + { - [Theory] - [InlineData( + + + [TestMethod] + + + [DataRow( + + """ + + + + """, + + true, + + "main.js" + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true, + + "main.js" + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true, + + "main.js" + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true, + + "./main.js" + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true, + + "./folder/folder/file.name.something.js" + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true, + + "main.suffix.js" + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true, + + "/root/main.suffix.js" + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + false + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + false + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + false + + )] - [InlineData( + + + [DataRow( + + """ + +

main#[.{fingerprint}].js

+ + """, + + false + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true, + + "main.js" + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true, + + "./main.js" + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true, + + "main.js" + + )] + + public void ValidateAssetsRegex(string input, bool shouldMatch, string fileName = null) + + { + + var match = OverrideHtmlAssetPlaceholders._assetsRegex.Match(input); - Assert.Equal(shouldMatch, match.Success); + + + Assert.AreEqual(shouldMatch, match.Success); + + + + if (fileName != null) + + { - Assert.Equal(fileName, match.Groups["fileName"].Value + match.Groups["fileExtension"].Value); + + + Assert.AreEqual(fileName, match.Groups["fileName"].Value + match.Groups["fileExtension"].Value); + + } + + } - [Theory] - [InlineData( + + + + + [TestMethod] + + + [DataRow( + + """ + + + + """, + + true + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + false + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + false + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + false + + )] + + public void ValidateImportMapRegex(string input, bool shouldMatch) + + { - Assert.Equal(shouldMatch, OverrideHtmlAssetPlaceholders._importMapRegex.Match(input).Success); + + + Assert.AreEqual(shouldMatch, OverrideHtmlAssetPlaceholders._importMapRegex.Match(input).Success); + + } - [Theory] - [InlineData( + + + + + [TestMethod] + + + [DataRow( + + """ + + + + """, + + true + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + false + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + false + + )] - [InlineData( + + + [DataRow( + + """ + + " + + """, + + false + + )] - [InlineData( + + + [DataRow( + + """ + + " + + """, + + false + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + false + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true, + + "webassembly" + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true, + + "webassembly" + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + false + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + false + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + false + + )] - [InlineData( + + + [DataRow( + + """ + + + + """, + + true, + + "webassembly" + + )] + + public void ValidatePreloadRegex(string input, bool shouldMatch, string group = null) + + { + + var match = OverrideHtmlAssetPlaceholders._preloadRegex.Match(input); - Assert.Equal(shouldMatch, match.Success); + + + Assert.AreEqual(shouldMatch, match.Success); + + + + if (group != null) + + { - Assert.Equal(group, match.Groups["group"]?.Value); + + + Assert.AreEqual(group, match.Groups["group"]?.Value); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadPackageAssetsManifestTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadPackageAssetsManifestTest.cs index 1ce7486112fb..1d72ce670698 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadPackageAssetsManifestTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadPackageAssetsManifestTest.cs @@ -1,488 +1,1466 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Text.Json; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + +[TestClass] + public class ReadPackageAssetsManifestTest : IDisposable + + { + + private readonly string _tempDir; + + private readonly Mock _buildEngine; + + private readonly List _errorMessages; + + private readonly List _logMessages; + + + + public ReadPackageAssetsManifestTest() + + { + + _tempDir = Path.Combine(Path.GetTempPath(), "ReadPkgManifest_" + Guid.NewGuid().ToString("N")); + + Directory.CreateDirectory(_tempDir); + + + + _errorMessages = new List(); + + _logMessages = new List(); + + _buildEngine = new Mock(); + + _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => _errorMessages.Add(args.Message)); + + _buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => _logMessages.Add(args.Message)); + + _buildEngine.Setup(e => e.LogWarningEvent(It.IsAny())) + + .Callback(args => _logMessages.Add(args.Message)); + + } + + + + public void Dispose() + + { + + if (Directory.Exists(_tempDir)) + + { + + try { Directory.Delete(_tempDir, recursive: true); } catch { } + + } + + } - [Fact] + + + + + [TestMethod] + + public void ReadsValidManifest_EmitsAssetsAsTaskItems() + + { + + var packageRoot = SetupPackageRoot("MyLib", + + CreateManifestAsset("staticwebassets/css/site.css", "css/site.css", "_content/mylib", "")); + + + + var manifestItem = CreateManifestItem(packageRoot, "MyLib"); + + + + var task = CreateReadManifestTask(new[] { manifestItem }); + + task.Execute().Should().BeTrue(); + + + + task.Assets.Should().HaveCount(1); + + + + var emitted = task.Assets[0]; + + emitted.GetMetadata("SourceType").Should().Be("Package"); + + emitted.GetMetadata("SourceId").Should().Be("MyLib"); + + emitted.GetMetadata("BasePath").Should().Be("_content/mylib"); + + emitted.GetMetadata("RelativePath").Should().Be("css/site.css"); + + emitted.GetMetadata("Fingerprint").Should().Be("test"); + + } - [Fact] + + + + + [TestMethod] + + public void UngroupedAssets_AlwaysIncluded() + + { + + var packageRoot = SetupPackageRoot("MyLib", + + CreateManifestAsset("staticwebassets/app.js", "app.js", "_content/mylib", "")); + + + + var manifestItem = CreateManifestItem(packageRoot, "MyLib"); + + + + var task = CreateReadManifestTask( + + new[] { manifestItem }, + + new[] { CreateGroup("SomeGroup", "SomeValue", "OtherLib") }); + + task.Execute().Should().BeTrue(); + + + + task.Assets.Should().HaveCount(1, "ungrouped assets should always be included"); + + } - [Fact] + + + + + [TestMethod] + + public void GroupedAsset_MatchingDeclaration_IsIncluded() + + { + + var packageRoot = SetupPackageRoot("IdentityUI", + + CreateManifestAsset("staticwebassets/css/site.css", "css/site.css", "_content/id", "BootstrapVersion=V5")); + + + + var manifestItem = CreateManifestItem(packageRoot, "IdentityUI"); + + + + var task = CreateReadManifestTask( + + new[] { manifestItem }, + + new[] { CreateGroup("BootstrapVersion", "V5", "IdentityUI") }); + + task.Execute().Should().BeTrue(); + + + + task.Assets.Should().HaveCount(1); + + } - [Fact] + + + + + [TestMethod] + + public void GroupedAsset_NoDeclarations_IsExcluded() + + { + + var packageRoot = SetupPackageRoot("IdentityUI", + + CreateManifestAsset("staticwebassets/css/site.css", "css/site.css", "_content/id", "BootstrapVersion=V5")); + + + + var manifestItem = CreateManifestItem(packageRoot, "IdentityUI"); + + + + // No StaticWebAssetGroups + + var task = CreateReadManifestTask(new[] { manifestItem }); + + task.Execute().Should().BeTrue(); + + + + task.Assets.Should().HaveCount(0, "grouped assets should be excluded with no declarations"); + + } - [Fact] + + + + + [TestMethod] + + public void MultiGroup_PartialMatch_IsExcluded() + + { + + var packageRoot = SetupPackageRoot("IdentityUI", + + CreateManifestAsset("staticwebassets/css/site.css", "css/site.css", "_content/id", + + "BootstrapVersion=V5;DebugAssets=true")); + + + + var manifestItem = CreateManifestItem(packageRoot, "IdentityUI"); + + + + // BootstrapVersion declared but DebugAssets not declared + + var task = CreateReadManifestTask( + + new[] { manifestItem }, + + new[] { CreateGroup("BootstrapVersion", "V5", "IdentityUI") }); + + task.Execute().Should().BeTrue(); + + + + task.Assets.Should().HaveCount(0, "AND-matching: all group entries must be satisfied"); + + } - [Fact] + + + + + [TestMethod] + + public void CascadingExclusion_RelatedAssetExcludedWithPrimary() + + { + + var primaryAsset = CreateManifestAsset( + + "staticwebassets/css/site.css", "css/site.css", "_content/id", "BootstrapVersion=V5"); + + var relatedAsset = CreateManifestAsset( + + "staticwebassets/css/site.css.gz", "css/site.css.gz", "_content/id", ""); + + relatedAsset.Value.AssetRole = "Alternative"; + + relatedAsset.Value.AssetTraitName = "Content-Encoding"; + + relatedAsset.Value.AssetTraitValue = "gzip"; + + relatedAsset.Value.RelatedAsset = "staticwebassets/css/site.css"; + + + + var packageRoot = SetupPackageRoot("IdentityUI", primaryAsset, relatedAsset); + + var manifestItem = CreateManifestItem(packageRoot, "IdentityUI"); + + + + // No group declarations → primary excluded + + var task = CreateReadManifestTask(new[] { manifestItem }); + + task.Execute().Should().BeTrue(); + + + + task.Assets.Should().HaveCount(0, "both primary and related should be excluded via cascading"); + + } - [Fact] + + + + + [TestMethod] + + public void Endpoints_ForExcludedAssets_AreFilteredOut() + + { + + var includedAsset = CreateManifestAsset( + + "staticwebassets/app.js", "app.js", "_content/id", ""); + + var excludedAsset = CreateManifestAsset( + + "staticwebassets/css/site.css", "css/site.css", "_content/id", "BootstrapVersion=V5"); + + + + var endpoints = new[] + + { + + new StaticWebAssetEndpoint + + { + + Route = "_content/id/app.js", + + AssetFile = "staticwebassets/app.js", + + Selectors = [], + + ResponseHeaders = [], + + EndpointProperties = [] + + }, + + new StaticWebAssetEndpoint + + { + + Route = "_content/id/css/site.css", + + AssetFile = "staticwebassets/css/site.css", + + Selectors = [], + + ResponseHeaders = [], + + EndpointProperties = [] + + } + + }; + + + + var packageRoot = SetupPackageRootWithEndpoints("IdentityUI", + + new[] { includedAsset, excludedAsset }, endpoints); + + var manifestItem = CreateManifestItem(packageRoot, "IdentityUI"); + + + + // No group declarations → grouped asset excluded + + var task = CreateReadManifestTask(new[] { manifestItem }); + + task.Execute().Should().BeTrue(); + + + + task.Assets.Should().HaveCount(1, "only ungrouped asset should be included"); + + task.Endpoints.Should().HaveCount(1, "endpoint for excluded asset should be removed"); + + task.Endpoints[0].ItemSpec.Should().Be("_content/id/app.js"); + + } - [Fact] + + + + + [TestMethod] + + public void DeferredGroups_SkippedDuringEagerFiltering() + + { + + var packageRoot = SetupPackageRoot("IdentityUI", + + CreateManifestAsset("staticwebassets/css/site.css", "css/site.css", "_content/id", + + "ServerRendering=true")); + + + + var manifestItem = CreateManifestItem(packageRoot, "IdentityUI"); + + + + var task = CreateReadManifestTask( + + new[] { manifestItem }, + + new[] { CreateGroup("ServerRendering", "", "IdentityUI", deferred: true) }); + + task.Execute().Should().BeTrue(); + + + + task.Assets.Should().HaveCount(1, + + "deferred group requirements should be skipped during eager filtering"); + + } - [Fact] + + + + + [TestMethod] + + public void FrameworkAssets_MaterializedToIntermediateDirectory() + + { + + var packageRoot = SetupPackageRoot("MyLib", + + CreateManifestAsset("staticwebassets/js/framework.js", "js/framework.js", "_content/mylib", "", "Framework")); + + + + var manifestItem = CreateManifestItem(packageRoot, "MyLib"); + + + + var task = CreateReadManifestTask(new[] { manifestItem }); + + task.Execute().Should().BeTrue(); + + + + task.Assets.Should().HaveCount(1); + + + + var emitted = task.Assets[0]; + + // Framework assets should be materialized to the fx directory + + var expectedDir = Path.Combine(_tempDir, "obj", "fx", "MyLib"); + + emitted.ItemSpec.Should().StartWith(expectedDir); + + File.Exists(emitted.ItemSpec).Should().BeTrue(); + + + + // SourceType changes to Discovered for framework materialization + + emitted.GetMetadata("SourceType").Should().Be("Discovered"); + + emitted.GetMetadata("SourceId").Should().Be("ConsumerApp"); + + } - [Fact] + + + + + [TestMethod] + + public void GroupedFrameworkAsset_HasAssetGroupsClearedAfterMaterialization() + + { + + // A framework asset that belongs to a group (e.g. blazor.webassembly.js in the + + // BlazorWebAssembly group) passes group filtering when the group is declared, but + + // must have its AssetGroups cleared once it is materialized as a current-project + + // asset. Otherwise downstream endpoint generation treats it as a still-grouped + + // asset, skips it, and the fingerprinted asset 404s at runtime. + + var packageRoot = SetupPackageRoot("Microsoft.AspNetCore.Components.WebAssembly", + + CreateManifestAsset("staticwebassets/_framework/blazor.webassembly.js", "_framework/blazor.webassembly.js", "/", "BlazorWebAssembly=enabled", "Framework")); + + + + var manifestItem = CreateManifestItem(packageRoot, "Microsoft.AspNetCore.Components.WebAssembly"); + + + + var task = CreateReadManifestTask( + + new[] { manifestItem }, + + new[] { CreateGroup("BlazorWebAssembly", "enabled", "Microsoft.AspNetCore.Components.WebAssembly") }); + + task.Execute().Should().BeTrue(); + + + + task.Assets.Should().HaveCount(1); + + + + var emitted = task.Assets[0]; + + emitted.GetMetadata("SourceType").Should().Be("Discovered"); + + emitted.GetMetadata("AssetGroups").Should().BeEmpty("materialized framework assets must not retain group membership or endpoint generation will skip them"); + + } - [Fact] + + + + + [TestMethod] + + public void InvalidManifestVersion_LogsError() + + { + + var packageDir = Path.Combine(_tempDir, "packages", "BadLib"); + + var buildDir = Path.Combine(packageDir, "build"); + + Directory.CreateDirectory(buildDir); + + + + // Write a staticwebassets directory with a file + + var swDir = Path.Combine(packageDir, "staticwebassets"); + + Directory.CreateDirectory(swDir); + + + + var badManifest = new { Version = 2, ManifestType = "Package", Assets = new Dictionary(), Endpoints = Array.Empty() }; + + var manifestPath = Path.Combine(buildDir, "BadLib.PackageAssets.json"); + + File.WriteAllText(manifestPath, JsonSerializer.Serialize(badManifest)); + + + + var manifestItem = new TaskItem(manifestPath, new Dictionary + + { + + ["SourceId"] = "BadLib", + + ["ContentRoot"] = swDir + Path.DirectorySeparatorChar, + + ["PackageRoot"] = packageDir, + + }); + + + + var task = CreateReadManifestTask(new[] { manifestItem }); + + var result = task.Execute(); + + + + result.Should().BeFalse(); + + _errorMessages.Should().ContainSingle(m => m.Contains("Unsupported package manifest version")); + + } - [Fact] + + + + + [TestMethod] + + public void MissingIntermediateOutputPath_ProducesError() + + { + + var packageRoot = SetupPackageRoot("MyLib", + + CreateManifestAsset("staticwebassets/css/site.css", "css/site.css", "_content/mylib", "")); + + var manifestItem = CreateManifestItem(packageRoot, "MyLib"); + + + + var task = new ReadPackageAssetsManifest + + { + + BuildEngine = _buildEngine.Object, + + PackageManifests = new[] { manifestItem }, + + StaticWebAssetGroups = Array.Empty(), + + IntermediateOutputPath = null, + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + var result = task.Execute(); + + + + result.Should().BeFalse(); + + _errorMessages.Should().ContainSingle(m => m.Contains("IntermediateOutputPath is required")); + + } + + + + private ReadPackageAssetsManifest CreateReadManifestTask( + + ITaskItem[] manifests, + + ITaskItem[] groups = null) + + { + + return new ReadPackageAssetsManifest + + { + + BuildEngine = _buildEngine.Object, + + PackageManifests = manifests, + + StaticWebAssetGroups = groups ?? Array.Empty(), + + IntermediateOutputPath = Path.Combine(_tempDir, "obj"), + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + } + + + + private static ITaskItem CreateGroup(string name, string value, string sourceId, bool deferred = false) + + { + + var dict = new Dictionary + + { + + ["Value"] = value, + + ["SourceId"] = sourceId, + + }; + + if (deferred) + + dict["Deferred"] = "true"; + + return new TaskItem(name, dict); + + } + + + + private string SetupPackageRoot(string packageId, params KeyValuePair[] assets) + + { + + return SetupPackageRootWithEndpoints(packageId, assets, Array.Empty()); + + } + + + + private string SetupPackageRootWithEndpoints(string packageId, KeyValuePair[] assets, StaticWebAssetEndpoint[] endpoints) + + { + + var packageDir = Path.Combine(_tempDir, "packages", packageId); + + var buildDir = Path.Combine(packageDir, "build"); + + Directory.CreateDirectory(buildDir); + + + + // Create actual files for each asset and fill in SourceId/ContentRoot + + // the way GeneratePackageAssetsManifestFile does (via copy constructor). + + var contentRoot = Path.Combine(packageDir, "staticwebassets") + Path.DirectorySeparatorChar; + + foreach (var asset in assets) + + { + + var filePath = Path.Combine(packageDir, asset.Key.Replace('/', Path.DirectorySeparatorChar)); + + Directory.CreateDirectory(Path.GetDirectoryName(filePath)); + + File.WriteAllText(filePath, "content-" + asset.Key); + + + + asset.Value.SourceId = packageId; + + asset.Value.ContentRoot = contentRoot; + + } + + + + var manifest = new StaticWebAssetPackageManifest + + { + + Version = 1, + + ManifestType = "Package", + + Assets = assets.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.OrdinalIgnoreCase), + + Endpoints = endpoints, + + }; + + + + var manifestPath = Path.Combine(buildDir, packageId + ".PackageAssets.json"); + + var json = JsonSerializer.SerializeToUtf8Bytes(manifest, + + StaticWebAssetsJsonSerializerContext.Default.StaticWebAssetPackageManifest); + + File.WriteAllBytes(manifestPath, json); + + + + return packageDir; + + } + + + + private ITaskItem CreateManifestItem(string packageRoot, string sourceId) + + { + + var buildDir = Path.Combine(packageRoot, "build"); + + var manifestPath = Path.Combine(buildDir, sourceId + ".PackageAssets.json"); + + var contentRoot = Path.Combine(packageRoot, "staticwebassets") + Path.DirectorySeparatorChar; + + + + return new TaskItem(manifestPath, new Dictionary + + { + + ["SourceId"] = sourceId, + + ["ContentRoot"] = contentRoot, + + ["PackageRoot"] = packageRoot, + + }); + + } + + + + private static KeyValuePair CreateManifestAsset( + + string packagePath, string relativePath, string basePath, string assetGroups, string sourceType = "Package") + + { + + var asset = new StaticWebAsset + + { + + Identity = packagePath, + + RelativePath = relativePath, + + BasePath = basePath, + + SourceType = sourceType, + + AssetKind = "All", + + AssetMode = "All", + + AssetRole = "Primary", + + RelatedAsset = "", + + AssetTraitName = "", + + AssetTraitValue = "", + + AssetGroups = assetGroups, + + Fingerprint = "test", + + Integrity = "sha256-test", + + CopyToOutputDirectory = "Never", + + CopyToPublishDirectory = "PreserveNewest", + + FileLength = 6, + + LastWriteTime = new DateTimeOffset(1990, 11, 15, 0, 0, 0, TimeSpan.Zero), + + }; + + + + return new KeyValuePair(packagePath, asset); + + } + + + + // Scenario 6: Custom .targets author override + + // A package author disables the auto-generated .targets and manually provides + + // their own .targets that still registers a StaticWebAssetPackageManifest item. + + // The consumer's ReadPackageAssetsManifest should still work correctly. - [Fact] + + + [TestMethod] + + public void CustomTargetsOverride_ManualManifestItem_WorksCorrectly() + + { + + // Setup: simulate a package that has its manifest at a custom location + + // (as if the author wrote their own .targets pointing to the manifest) + + var packageRoot = SetupPackageRoot("CustomLib", + + CreateManifestAsset("staticwebassets/js/custom.js", "js/custom.js", "_content/customlib", ""), + + CreateManifestAsset("staticwebassets/css/theme.css", "css/theme.css", "_content/customlib", "")); + + + + // The manifest item metadata mirrors what a hand-authored .targets would produce. + + // The key difference from auto-generated: the author controls the paths. + + var manifestItem = CreateManifestItem(packageRoot, "CustomLib"); + + + + var task = CreateReadManifestTask(new[] { manifestItem }); + + task.Execute().Should().BeTrue(); + + + + task.Assets.Should().HaveCount(2); + + + + // Both assets should have the custom package's SourceId + + task.Assets.Should().OnlyContain(a => a.GetMetadata("SourceId") == "CustomLib"); + + // Both should resolve Identity paths under the package root + + task.Assets.Should().OnlyContain(a => a.ItemSpec.StartsWith( + + Path.Combine(packageRoot, "staticwebassets"))); + + } + + + + // Scenario 7: Multiple packages contributing manifests to the same consumer + + // Two packages each provide a StaticWebAssetPackageManifest item. + + // Group filtering should be applied independently per SourceId. - [Fact] + + + [TestMethod] + + public void MultiplePackages_IndependentGroupFilteringPerSourceId() + + { + + // Package A: has grouped assets (BootstrapVersion=V5) + + var packageRootA = SetupPackageRoot("PkgA", + + CreateManifestAsset("staticwebassets/css/a.css", "css/a.css", "_content/pkga", "BootstrapVersion=V5"), + + CreateManifestAsset("staticwebassets/js/a.js", "js/a.js", "_content/pkga", "")); + + + + // Package B: has grouped assets (Theme=Dark) and ungrouped + + var packageRootB = SetupPackageRoot("PkgB", + + CreateManifestAsset("staticwebassets/css/b.css", "css/b.css", "_content/pkgb", "Theme=Dark"), + + CreateManifestAsset("staticwebassets/js/b.js", "js/b.js", "_content/pkgb", "")); + + + + var manifestA = CreateManifestItem(packageRootA, "PkgA"); + + var manifestB = CreateManifestItem(packageRootB, "PkgB"); + + + + // Consumer declares BootstrapVersion=V5 for PkgA but NOT Theme for PkgB + + var task = CreateReadManifestTask( + + new[] { manifestA, manifestB }, + + new[] { CreateGroup("BootstrapVersion", "V5", "PkgA") }); + + task.Execute().Should().BeTrue(); + + + + // PkgA: css/a.css included (group matched), js/a.js included (ungrouped) + + // PkgB: css/b.css excluded (Theme=Dark not declared), js/b.js included (ungrouped) + + task.Assets.Should().HaveCount(3); + + + + var assetPaths = task.Assets.Select(a => a.GetMetadata("RelativePath")).ToList(); + + assetPaths.Should().Contain("css/a.css"); + + assetPaths.Should().Contain("js/a.js"); + + assetPaths.Should().Contain("js/b.js"); + + assetPaths.Should().NotContain("css/b.css"); + + + + // Verify SourceIds are correct + + task.Assets.Where(a => a.GetMetadata("SourceId") == "PkgA").Should().HaveCount(2); + + task.Assets.Where(a => a.GetMetadata("SourceId") == "PkgB").Should().HaveCount(1); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadStaticWebAssetsManifestFileTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadStaticWebAssetsManifestFileTest.cs index 0ea35e74fd2c..9116840b87f4 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadStaticWebAssetsManifestFileTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadStaticWebAssetsManifestFileTest.cs @@ -1,379 +1,1139 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Text.Json; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { + + + [TestClass] + public class ReadStaticWebAssetsManifestFileTest + + { + + public ReadStaticWebAssetsManifestFileTest() + + { + + Directory.CreateDirectory(Path.Combine(SdkTestContext.Current.TestExecutionDirectory, nameof(ReadStaticWebAssetsManifestFileTest))); + + TempFilePath = Path.Combine(SdkTestContext.Current.TestExecutionDirectory, nameof(ReadStaticWebAssetsManifestFileTest), Guid.NewGuid().ToString("N") + ".json"); + + } + + + + public string TempFilePath { get; } - [Fact] + + + + + [TestMethod] + + public void CanReadManifestWithoutProperties() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var emptyManifest = "{}"; + + File.WriteAllText(TempFilePath, emptyManifest); + + + + var task = new ReadStaticWebAssetsManifestFile + + { + + BuildEngine = buildEngine.Object, + + ManifestPath = TempFilePath + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.Assets.Should().BeEmpty(); + + task.Endpoints.Should().BeEmpty(); + + task.DiscoveryPatterns.Should().BeEmpty(); + + task.ReferencedProjectsConfiguration.Should().BeEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void CanReadEmptyManifest() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var emptyManifest = @"{ + + ""Version"": 1, + + ""Hash"": ""__hash__"", + + ""Source"": ""ComponentApp"", + + ""BasePath"": ""_content/ComponentApp"", + + ""Mode"": ""Default"", + + ""ManifestType"": ""Build"", + + ""ReferencedProjectsConfiguration"": [], + + ""DiscoveryPatterns"": [], + + ""Assets"": [], + + ""Endpoints"": [] + + }"; + + File.WriteAllText(TempFilePath, emptyManifest); + + + + var task = new ReadStaticWebAssetsManifestFile + + { + + BuildEngine = buildEngine.Object, + + ManifestPath = TempFilePath + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.Assets.Should().BeEmpty(); + + task.Endpoints.Should().BeEmpty(); + + task.DiscoveryPatterns.Should().BeEmpty(); + + task.ReferencedProjectsConfiguration.Should().BeEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void ConvertsAssetsToTaskItems() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var contentRoot = Path.GetFullPath("."); + + var encodedContentRoot = JsonEncodedText.Encode(contentRoot); + + var identity = Path.Combine(contentRoot, "ComponentApp.styles.css"); + + var encodedIdentity = JsonEncodedText.Encode(identity); + + var manifest = $@"{{ + + ""Version"": 1, + + ""Hash"": ""__hash__"", + + ""Source"": ""ComponentApp"", + + ""BasePath"": ""_content/ComponentApp"", + + ""Mode"": ""Default"", + + ""ManifestType"": ""Build"", + + ""ReferencedProjectsConfiguration"": [], + + ""DiscoveryPatterns"": [], + + ""Assets"": [ + + {{ + + ""Identity"": ""{encodedIdentity}"", + + ""SourceId"": ""ComponentApp"", + + ""SourceType"": ""Computed"", + + ""ContentRoot"": ""{encodedContentRoot}"", + + ""BasePath"": ""_content/ComponentApp"", + + ""RelativePath"": ""ComponentApp.styles.css"", + + ""AssetKind"": ""All"", + + ""AssetMode"": ""CurrentProject"", + + ""AssetRole"": ""Primary"", + + ""RelatedAsset"": """", + + ""AssetTraitName"": ""ScopedCss"", + + ""AssetTraitValue"": ""ApplicationBundle"", + + ""CopyToOutputDirectory"": ""Never"", + + ""CopyToPublishDirectory"": ""PreserveNewest"", + + ""OriginalItemSpec"": ""{encodedIdentity}"" + + }} + + ], + + ""Endpoints"": [ + + {{ + + ""AssetFile"": ""{encodedIdentity}"", + + ""Route"": ""_content/ComponentApp/ComponentApp.styles.css"", + + ""Selectors"": [ + + {{ + + ""Name"": ""Content-Encoding"", + + ""Value"": ""gzip"", + + ""Quality"": ""0.9"" + + }} + + ], + + ""ResponseHeaders"": [ + + {{ + + ""Name"": ""Content-Length"", + + ""Value"": ""__content-length__"" + + }}, + + {{ + + ""Name"": ""Content-Type"", + + ""Value"": ""text/css"" + + }}, + + {{ + + ""Name"": ""ETag"", + + ""Value"": ""__etag__"" + + }}, + + {{ + + ""Name"": ""Last-Modified"", + + ""Value"": ""__last-modified__"" + + }} + + ] + + }} + + ] + + }}"; + + File.WriteAllText(TempFilePath, manifest); + + + + var task = new ReadStaticWebAssetsManifestFile + + { + + BuildEngine = buildEngine.Object, + + ManifestPath = TempFilePath + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.ReferencedProjectsConfiguration.Should().BeEmpty(); + + task.DiscoveryPatterns.Should().BeEmpty(); + + task.Assets.Length.Should().Be(1); + + var asset = task.Assets[0]; + + asset.GetMetadata(nameof(StaticWebAsset.Identity)).Should().BeEquivalentTo($"{identity}"); + + asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().BeEquivalentTo("ComponentApp"); + + asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().BeEquivalentTo("Computed"); + + asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().BeEquivalentTo($"{contentRoot}"); + + asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().BeEquivalentTo("_content/ComponentApp"); + + asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().BeEquivalentTo("ComponentApp.styles.css"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().BeEquivalentTo("All"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().BeEquivalentTo("CurrentProject"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().BeEquivalentTo("Primary"); + + asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().BeEquivalentTo(""); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().BeEquivalentTo("ScopedCss"); + + asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().BeEquivalentTo("ApplicationBundle"); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().BeEquivalentTo("Never"); + + asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().BeEquivalentTo("PreserveNewest"); + + asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().BeEquivalentTo($"{identity}"); + + + + task.Endpoints.Length.Should().Be(1); + + var endpoint = task.Endpoints[0]; + + endpoint.ItemSpec.Should().BeEquivalentTo("_content/ComponentApp/ComponentApp.styles.css"); + + endpoint.GetMetadata(nameof(StaticWebAssetEndpoint.AssetFile)).Should().BeEquivalentTo($"{identity}"); + + endpoint.GetMetadata(nameof(StaticWebAssetEndpoint.Selectors)).Should().BeEquivalentTo("""[{"Name":"Content-Encoding","Value":"gzip","Quality":"0.9"}]"""); + + endpoint.GetMetadata(nameof(StaticWebAssetEndpoint.ResponseHeaders)) + + .Should() + + .BeEquivalentTo("""[{"Name":"Content-Length","Value":"__content-length__"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"__etag__"},{"Name":"Last-Modified","Value":"__last-modified__"}]"""); + + } - [Fact] + + + + + [TestMethod] + + public void ConvertsReferencedProjectsConfigurationsToTaskItems() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var contentRoot = Path.GetFullPath("."); + + var identity = Path.Combine(contentRoot, "AnotherClassLib", "AnotherClassLib.csproj"); + + var encodedIdentity = JsonEncodedText.Encode(identity); + + var manifest = $@"{{ + + ""Version"": 1, + + ""Hash"": ""__hash__"", + + ""Source"": ""ComponentApp"", + + ""BasePath"": ""_content/ComponentApp"", + + ""Mode"": ""Default"", + + ""ManifestType"": ""Build"", + + ""ReferencedProjectsConfiguration"": [ + + {{ + + ""Identity"": ""{encodedIdentity}"", + + ""Version"": 2, + + ""Source"": ""AnotherClassLib"", + + ""GetPublishAssetsTargets"": ""ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems"", + + ""AdditionalPublishProperties"": "";"", + + ""AdditionalPublishPropertiesToRemove"": "";WebPublishProfileFile"", + + ""GetBuildAssetsTargets"": ""GetCurrentProjectBuildStaticWebAssetItems"", + + ""AdditionalBuildProperties"": "";"", + + ""AdditionalBuildPropertiesToRemove"": "";WebPublishProfileFile"" + + }} + + ], + + ""DiscoveryPatterns"": [], + + ""Assets"": [], + + ""Endpoints"": [] + + }}"; + + File.WriteAllText(TempFilePath, manifest); + + + + var task = new ReadStaticWebAssetsManifestFile + + { + + BuildEngine = buildEngine.Object, + + ManifestPath = TempFilePath + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.ReferencedProjectsConfiguration.Length.Should().Be(1); + + task.Assets.Should().BeEmpty(); + + task.Endpoints.Should().BeEmpty(); + + task.DiscoveryPatterns.Should().BeEmpty(); + + var projectConfiguration = task.ReferencedProjectsConfiguration[0]; + + projectConfiguration.ItemSpec.Should().BeEquivalentTo(identity); + + projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.Version)).Should().BeEquivalentTo("2"); + + projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.Source)).Should().BeEquivalentTo("AnotherClassLib"); + + projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.GetPublishAssetsTargets)).Should().BeEquivalentTo("ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems"); + + projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.AdditionalPublishProperties)).Should().BeEquivalentTo(";"); + + projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.AdditionalPublishPropertiesToRemove)).Should().BeEquivalentTo(";WebPublishProfileFile"); + + projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.GetBuildAssetsTargets)).Should().BeEquivalentTo("GetCurrentProjectBuildStaticWebAssetItems"); + + projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.AdditionalBuildProperties)).Should().BeEquivalentTo(";"); + + projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.AdditionalBuildPropertiesToRemove)).Should().BeEquivalentTo(";WebPublishProfileFile"); + + } - [Fact] + + + + + [TestMethod] + + public void ConvertsDiscoveryPatternsToTaskItems() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var contentRoot = Path.Combine(Path.GetFullPath("."), "AnotherClassLib", "wwwroot"); + + var encodedContentRoot = JsonEncodedText.Encode(contentRoot); + + var manifest = $@"{{ + + ""Version"": 1, + + ""Hash"": ""__hash__"", + + ""Source"": ""ComponentApp"", + + ""BasePath"": ""_content/ComponentApp"", + + ""Mode"": ""Default"", + + ""ManifestType"": ""Build"", + + ""ReferencedProjectsConfiguration"": [ ], + + ""DiscoveryPatterns"": [ + + {{ + + ""Name"": ""AnotherClassLib\\wwwroot"", + + ""Source"": ""AnotherClassLib"", + + ""ContentRoot"": ""{encodedContentRoot}"", + + ""BasePath"": ""_content/AnotherClassLib"", + + ""Pattern"": ""**"" + + }} + + ], + + ""Assets"": [], + + ""Endpoints"": [] + + }}"; + + File.WriteAllText(TempFilePath, manifest); + + + + var task = new ReadStaticWebAssetsManifestFile + + { + + BuildEngine = buildEngine.Object, + + ManifestPath = TempFilePath + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + task.DiscoveryPatterns.Length.Should().Be(1); + + task.ReferencedProjectsConfiguration.Should().BeEmpty(); + + task.Assets.Should().BeEmpty(); + + task.Endpoints.Should().BeEmpty(); + + var discoveryPattern = task.DiscoveryPatterns[0]; + + + + discoveryPattern.ItemSpec.Should().BeEquivalentTo(Path.Combine("AnotherClassLib", "wwwroot")); + + discoveryPattern.GetMetadata(nameof(StaticWebAssetsDiscoveryPattern.Source)).Should().BeEquivalentTo("AnotherClassLib"); + + discoveryPattern.GetMetadata(nameof(StaticWebAssetsDiscoveryPattern.ContentRoot)).Should().BeEquivalentTo($"{contentRoot}"); + + discoveryPattern.GetMetadata(nameof(StaticWebAssetsDiscoveryPattern.BasePath)).Should().BeEquivalentTo("_content/AnotherClassLib"); + + discoveryPattern.GetMetadata(nameof(StaticWebAssetsDiscoveryPattern.Pattern)).Should().BeEquivalentTo("**"); + + } - [Fact] + + + + + [TestMethod] + + public void ReturnsErrorwhenManifestDoesNotExist() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ReadStaticWebAssetsManifestFile + + { + + BuildEngine = buildEngine.Object, + + ManifestPath = "nonexisting.staticwebassets.json" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(false); + + errorMessages.Count.Should().Be(1); + + errorMessages[0].Should().Be("Manifest file at 'nonexisting.staticwebassets.json' not found."); + + task.Assets.Should().BeNull(); + + task.DiscoveryPatterns.Should().BeNull(); + + task.ReferencedProjectsConfiguration.Should().BeNull(); + + } - [Fact] + + + + + [TestMethod] + + public void ReturnsErrorwhenManifestIsMalformed() + + { + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var manifest = "{"; + + File.WriteAllText(TempFilePath, manifest); + + var task = new ReadStaticWebAssetsManifestFile + + { + + BuildEngine = buildEngine.Object, + + ManifestPath = TempFilePath + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(false); + + errorMessages.Count.Should().Be(1); + + task.Assets.Should().BeNull(); + + task.Endpoints.Should().BeNull(); + + task.DiscoveryPatterns.Should().BeNull(); + + task.ReferencedProjectsConfiguration.Should().BeNull(); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveAllScopedCssAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveAllScopedCssAssetsTest.cs index f6261ae15561..9a96f4422cab 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveAllScopedCssAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveAllScopedCssAssetsTest.cs @@ -1,122 +1,368 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.StaticWebAssets.Tasks; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + using Microsoft.Build.Utilities; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + + namespace Microsoft.NET.Sdk.Razor.Test + + { + + + [TestClass] + public class ResolveAllScopedCssAssetsTest + + { - [Fact] + + + [TestMethod] + + public void ResolveAllScopedCssAssets_IgnoresRegularCssFiles() + + { + + // Arrange + + var taskInstance = new ResolveAllScopedCssAssets() + + { + + StaticWebAssets = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.razor.rz.scp.css", new Dictionary + + { + + ["RelativePath"] = "Pages/Counter.razor.rz.scp.css" + + }), + + new TaskItem("site.css", new Dictionary + + { + + ["RelativePath"] = "site.css" + + }), + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.ScopedCssAssets.Should().ContainSingle(); + + taskInstance.ScopedCssAssets.Should().NotContain(scopedCssAsset => scopedCssAsset.ItemSpec == "site.css"); + + } - [Fact] + + + + + [TestMethod] + + public void ResolveAllScopedCssAssets_DetectsScopedCssFiles() + + { + + // Arrange + + var taskInstance = new ResolveAllScopedCssAssets() + + { + + StaticWebAssets = new[] + + { + + new TaskItem("TestFiles/Pages/Counter.razor.rz.scp.css", new Dictionary + + { + + ["RelativePath"] = "Pages/Counter.razor.rz.scp.css" + + }), + + new TaskItem("site.css", new Dictionary + + { + + ["RelativePath"] = "site.css" + + }), + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.ScopedCssAssets.Should().ContainSingle(); + + taskInstance.ScopedCssAssets.Should().Contain(scopedCssAsset => scopedCssAsset.ItemSpec == "TestFiles/Pages/Counter.razor.rz.scp.css"); + + } - [Fact] + + + + + [TestMethod] + + public void ResolveAllScopedCssAssets_DetectsScopedCssProjectBundleFiles() + + { + + // Arrange + + var taskInstance = new ResolveAllScopedCssAssets() + + { + + StaticWebAssets = new[] + + { + + new TaskItem("Folder/Project.bundle.scp.css", new Dictionary + + { + + ["RelativePath"] = "Project.bundle.scp.css" + + }), + + new TaskItem("site.css", new Dictionary + + { + + ["RelativePath"] = "site.css" + + }), + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.ScopedCssProjectBundles.Should().ContainSingle(); + + taskInstance.ScopedCssProjectBundles.Should().Contain(scopedCssBundle => scopedCssBundle.ItemSpec == "Folder/Project.bundle.scp.css"); + + } - [Fact] + + + + + [TestMethod] + + public void ResolveAllScopedCssAssets_IgnoresScopedCssApplicationBundleFiles() + + { + + // Arrange + + var taskInstance = new ResolveAllScopedCssAssets() + + { + + StaticWebAssets = new[] + + { + + new TaskItem("Folder/Project.styles.css", new Dictionary + + { + + ["RelativePath"] = "Project.styles.css" + + }), + + new TaskItem("site.css", new Dictionary + + { + + ["RelativePath"] = "site.css" + + }), + + } + + }; + + + + // Act + + var result = taskInstance.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + taskInstance.ScopedCssProjectBundles.Should().BeEmpty(); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveCompressedAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveCompressedAssetsTest.cs index f35921164312..b4077791136f 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveCompressedAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveCompressedAssetsTest.cs @@ -1,390 +1,1172 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Diagnostics.Metrics; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + using NuGet.ContentModel; + + using NuGet.Packaging.Core; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + + + + +[TestClass] + public class ResolveCompressedAssetsTest + + { + + private readonly List _errorMessages; + + private readonly Mock _buildEngine; + + + + public string ItemSpec { get; } + + + + public string OriginalItemSpec { get; } + + + + public string OutputBasePath { get; } + + + + public ResolveCompressedAssetsTest() + + { + + OutputBasePath = Path.Combine(SdkTestContext.Current.TestExecutionDirectory, nameof(ResolveCompressedAssetsTest)); + + ItemSpec = Path.Combine(OutputBasePath, Guid.NewGuid().ToString("N") + ".tmp"); + + OriginalItemSpec = Path.Combine(OutputBasePath, Guid.NewGuid().ToString("N") + ".tmp"); + + _errorMessages = new List(); + + _buildEngine = new Mock(); + + _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => _errorMessages.Add(args.Message)); + + } - [Fact] + + + + + [TestMethod] + + public void ResolvesExplicitlyProvidedAssets() + + { + + // Arrange + + var asset = CreatePrimaryAsset(); + + + + var gzipExplicitAsset = new TaskItem(asset.ItemSpec, asset.CloneCustomMetadata()); + + var brotliExplicitAsset = new TaskItem(asset.ItemSpec, asset.CloneCustomMetadata()); + + + + var task = new ResolveCompressedAssets() + + { + + OutputPath = OutputBasePath, + + BuildEngine = _buildEngine.Object, + + CandidateAssets = new[] { asset }, + + Formats = "gzip;brotli", + + ExplicitAssets = new[] { gzipExplicitAsset, brotliExplicitAsset }, + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(2); + + task.AssetsToCompress[0].ItemSpec.Should().EndWith(".gz"); + + task.AssetsToCompress[1].ItemSpec.Should().EndWith(".br"); + + } - [Fact] + + + + + [TestMethod] + + public void InfersPreCompressedAssetsCorrectly() + + { + + + + var uncompressedCandidate = new StaticWebAsset + + { + + Identity = Path.Combine(Environment.CurrentDirectory, "wwwroot", "js", "site.js"), + + RelativePath = "js/site#[.{fingerprint}]?.js", + + BasePath = "_content/Test", + + AssetMode = StaticWebAsset.AssetModes.All, + + AssetKind = StaticWebAsset.AssetKinds.All, + + AssetMergeSource = string.Empty, + + SourceId = "Test", + + CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, + + Fingerprint = "xtxxf3hu2r", + + RelatedAsset = string.Empty, + + ContentRoot = Path.Combine(Environment.CurrentDirectory,"wwwroot"), + + SourceType = StaticWebAsset.SourceTypes.Discovered, + + Integrity = "hRQyftXiu1lLX2P9Ly9xa4gHJgLeR1uGN5qegUobtGo=", + + FileLength = 10, + + LastWriteTime = DateTime.UtcNow, + + AssetRole = StaticWebAsset.AssetRoles.Primary, + + AssetMergeBehavior = string.Empty, + + AssetTraitValue = string.Empty, + + AssetTraitName = string.Empty, + + OriginalItemSpec = Path.Combine("wwwroot", "js", "site.js"), + + CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest + + }; + + + + var compressedCandidate = new StaticWebAsset + + { + + Identity = Path.Combine(Environment.CurrentDirectory, "wwwroot", "js", "site.js.gz"), + + RelativePath = "js/site.js#[.{fingerprint}]?.gz", + + BasePath = "_content/Test", + + AssetMode = StaticWebAsset.AssetModes.All, + + AssetKind = StaticWebAsset.AssetKinds.All, + + AssetMergeSource = string.Empty, + + SourceId = "Test", + + CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, + + Fingerprint = "es13vhk42b", + + RelatedAsset = string.Empty, + + ContentRoot = Path.Combine(Environment.CurrentDirectory, "wwwroot"), + + SourceType = StaticWebAsset.SourceTypes.Discovered, + + Integrity = "zs5Fd3XI6+g9f4N1SFLVdgghuiqdvq+nETAjTbvVxx4=", + + AssetRole = StaticWebAsset.AssetRoles.Primary, + + AssetMergeBehavior = string.Empty, + + AssetTraitValue = string.Empty, + + AssetTraitName = string.Empty, + + OriginalItemSpec = Path.Combine("wwwroot", "js", "site.js.gz"), + + CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest, + + FileLength = 10, + + LastWriteTime = DateTime.UtcNow + + }; + + + + var task = new ResolveCompressedAssets + + { + + OutputPath = OutputBasePath, + + CandidateAssets = [uncompressedCandidate.ToTaskItem(), compressedCandidate.ToTaskItem()], + + Formats = "gzip", + + BuildEngine = _buildEngine.Object + + }; + + + + var result = task.Execute(); + + + + result.Should().BeTrue(); - task.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(0); - } - [Fact] + + task.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(0); + + + } + + + + + + [TestMethod] + + public void ResolvesAssetsMatchingIncludePattern() + + { + + // Arrange + + var asset = CreatePrimaryAsset(); + + + + var task = new ResolveCompressedAssets() + + { + + OutputPath = OutputBasePath, + + BuildEngine = _buildEngine.Object, + + CandidateAssets = new[] { asset }, + + IncludePatterns = "**\\*.tmp", + + Formats = "gzip;brotli", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(2); + + task.AssetsToCompress[0].ItemSpec.Should().EndWith(".gz"); + + task.AssetsToCompress[1].ItemSpec.Should().EndWith(".br"); + + } - [Fact] + + + + + [TestMethod] + + public void ResolvesAssets_WithFingerprint_MatchingIncludePattern() + + { + + // Arrange + + var asset = CreatePrimaryAsset( + + Path.GetFileNameWithoutExtension(ItemSpec) + "#[.{fingerprint}]" + Path.GetExtension(ItemSpec)); + + + + var task = new ResolveCompressedAssets() + + { + + OutputPath = OutputBasePath, + + BuildEngine = _buildEngine.Object, + + CandidateAssets = new[] { asset }, + + IncludePatterns = "**\\*.tmp", + + Formats = "gzip;brotli", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(2); + + task.AssetsToCompress[0].ItemSpec.Should().EndWith(".gz"); + + var relativePath = task.AssetsToCompress[0].GetMetadata("RelativePath"); + + relativePath.Should().EndWith(".gz"); + + relativePath = Path.GetFileNameWithoutExtension(relativePath); + + relativePath.Should().EndWith(".tmp"); + + relativePath = Path.GetFileNameWithoutExtension(relativePath); + + relativePath.Should().EndWith("#[.{fingerprint=v1}]"); + + task.AssetsToCompress[1].ItemSpec.Should().EndWith(".br"); + + relativePath = task.AssetsToCompress[1].GetMetadata("RelativePath"); + + relativePath.Should().EndWith(".br"); + + relativePath = Path.GetFileNameWithoutExtension(relativePath); + + relativePath.Should().EndWith(".tmp"); + + relativePath = Path.GetFileNameWithoutExtension(relativePath); + + relativePath.Should().EndWith("#[.{fingerprint=v1}]"); + + } - [Fact] + + + + + [TestMethod] + + public void ExcludesAssetsMatchingExcludePattern() + + { + + // Arrange + + var asset = CreatePrimaryAsset(); + + + + var task = new ResolveCompressedAssets() + + { + + OutputPath = OutputBasePath, + + BuildEngine = _buildEngine.Object, + + IncludePatterns = "**\\*", + + ExcludePatterns = "**\\*.tmp", + + CandidateAssets = new[] { asset }, + + Formats = "gzip;brotli" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.AssetsToCompress.Should().HaveCount(0); + + } - [Fact] + + + + + [TestMethod] + + public void DeduplicatesAssetsResolvedBothExplicitlyAndFromPattern() + + { + + // Arrange + + var asset = CreatePrimaryAsset(); + + + + var gzipExplicitAsset = new TaskItem(asset.ItemSpec, asset.CloneCustomMetadata()); + + var brotliExplicitAsset = new TaskItem(asset.ItemSpec, asset.CloneCustomMetadata()); + + + + var buildTask = new ResolveCompressedAssets() + + { + + OutputPath = OutputBasePath, + + BuildEngine = _buildEngine.Object, + + CandidateAssets = new[] { asset }, + + IncludePatterns = "**\\*.tmp", + + ExplicitAssets = new[] { gzipExplicitAsset, brotliExplicitAsset }, + + Formats = "gzip;brotli" + + }; + + + + // Act + + var buildResult = buildTask.Execute(); + + + + // Assert + + buildResult.Should().BeTrue(); + + buildTask.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(2); + + buildTask.AssetsToCompress[0].ItemSpec.Should().EndWith(".gz"); + + buildTask.AssetsToCompress[1].ItemSpec.Should().EndWith(".br"); + + } - [Theory] - [InlineData("gzip", ".gz", "brotli", ".br")] - [InlineData("brotli", ".br", "gzip", ".gz")] + + + + + [TestMethod] + + + [DataRow("gzip", ".gz", "brotli", ".br")] + + + [DataRow("brotli", ".br", "gzip", ".gz")] + + public void IgnoresAssetsCompressedInPreviousTaskRun( + + string phase1Format, string phase1Ext, string _, string phase2Ext) + + { + + // Arrange + + var asset = CreatePrimaryAsset(); + + + + // Act/Assert + + var task1 = new ResolveCompressedAssets() + + { + + OutputPath = OutputBasePath, + + BuildEngine = _buildEngine.Object, + + CandidateAssets = new[] { asset }, + + IncludePatterns = "**\\*.tmp", + + Formats = phase1Format, + + }; + + + + var result1 = task1.Execute(); + + + + result1.Should().BeTrue(); + + task1.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(1); + + task1.AssetsToCompress[0].ItemSpec.Should().EndWith(phase1Ext); + + task1.AssetsToCompress[0].SetMetadata("Fingerprint", "v1" + phase1Ext.TrimStart('.')); + + task1.AssetsToCompress[0].SetMetadata("Integrity", "abc" + phase1Ext.TrimStart('.')); + + + + var explicitAsset = new TaskItem(asset.ItemSpec, asset.CloneCustomMetadata()); + + explicitAsset.SetMetadata("Fingerprint", "v2"); + + explicitAsset.SetMetadata("Integrity", "def"); + + + + var task2 = new ResolveCompressedAssets() + + { + + OutputPath = OutputBasePath, + + BuildEngine = _buildEngine.Object, + + CandidateAssets = new[] { asset, task1.AssetsToCompress[0] }, + + IncludePatterns = "**\\*.tmp", + + ExplicitAssets = new[] { explicitAsset }, + + Formats = "gzip;brotli" + + }; + + + + var result2 = task2.Execute(); + + + + result2.Should().BeTrue(); + + task2.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(1); + + task2.AssetsToCompress[0].ItemSpec.Should().EndWith(phase2Ext); + + } - [Fact] + + + + + [TestMethod] + + public void ProducesDistinctIdentities_ForGroupVariantsWithIdenticalContent() + + { + + // Arrange — two assets that differ only in AssetGroups but have the same + + // SourceId, BasePath, AssetKind, RelativePath (after token stripping) and + + // Fingerprint (identical file content). Before the fix, these would produce + + // the same compressed asset Identity and crash in ToAssetDictionary. + + var v4ItemSpec = Path.Combine(OutputBasePath, "staticwebassets", "V4", "css", "site.css"); + + var v5ItemSpec = Path.Combine(OutputBasePath, "staticwebassets", "V5", "css", "site.css"); + + + + var v4Asset = new StaticWebAsset() + + { + + Identity = v4ItemSpec, + + OriginalItemSpec = v4ItemSpec, + + RelativePath = "#[{BootstrapVersion}/]~css/site#[.{fingerprint}]?.css", + + ContentRoot = Path.Combine(OutputBasePath, "staticwebassets"), + + SourceType = StaticWebAsset.SourceTypes.Package, + + SourceId = "Microsoft.AspNetCore.Identity.UI", + + BasePath = "_content/Microsoft.AspNetCore.Identity.UI", + + AssetKind = StaticWebAsset.AssetKinds.All, + + AssetMode = StaticWebAsset.AssetModes.All, + + AssetRole = StaticWebAsset.AssetRoles.Primary, + + AssetGroups = "BootstrapVersion=V4", + + Fingerprint = "samehash123", + + Integrity = "sameintegrity", + + FileLength = 42, + + LastWriteTime = DateTime.UtcNow + + }.ToTaskItem(); + + + + var v5Asset = new StaticWebAsset() + + { + + Identity = v5ItemSpec, + + OriginalItemSpec = v5ItemSpec, + + RelativePath = "#[{BootstrapVersion}/]~css/site#[.{fingerprint}]?.css", + + ContentRoot = Path.Combine(OutputBasePath, "staticwebassets"), + + SourceType = StaticWebAsset.SourceTypes.Package, + + SourceId = "Microsoft.AspNetCore.Identity.UI", + + BasePath = "_content/Microsoft.AspNetCore.Identity.UI", + + AssetKind = StaticWebAsset.AssetKinds.All, + + AssetMode = StaticWebAsset.AssetModes.All, + + AssetRole = StaticWebAsset.AssetRoles.Primary, + + AssetGroups = "BootstrapVersion=V5", + + Fingerprint = "samehash123", + + Integrity = "sameintegrity", + + FileLength = 42, + + LastWriteTime = DateTime.UtcNow + + }.ToTaskItem(); + + + + var task = new ResolveCompressedAssets() + + { + + OutputPath = OutputBasePath, + + BuildEngine = _buildEngine.Object, + + CandidateAssets = new[] { v4Asset, v5Asset }, + + IncludePatterns = "**/*.css", + + Formats = "gzip", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var compressed = task.AssetsToCompress.TakeWhile(a => a != null).ToArray(); + + compressed.Should().HaveCount(2); + + compressed[0].ItemSpec.Should().EndWith(".gz"); + + compressed[1].ItemSpec.Should().EndWith(".gz"); + + + + // The critical assertion: the two compressed assets must have different Identities + + // so they don't collide when added to a dictionary keyed by Identity. + + compressed[0].ItemSpec.Should().NotBe(compressed[1].ItemSpec, + + "group variants with identical content must produce distinct compressed asset identities"); + + } + + + + private ITaskItem CreatePrimaryAsset(string relativePath = null) + + { + + return new StaticWebAsset() + + { + + Identity = ItemSpec, + + OriginalItemSpec = OriginalItemSpec, + + RelativePath = relativePath ?? Path.GetFileName(ItemSpec), + + ContentRoot = Path.GetDirectoryName(ItemSpec), + + SourceType = StaticWebAsset.SourceTypes.Discovered, + + SourceId = "App", + + AssetKind = StaticWebAsset.AssetKinds.All, + + AssetMode = StaticWebAsset.AssetModes.All, + + AssetRole = StaticWebAsset.AssetRoles.Primary, + + Fingerprint = "v1", + + Integrity = "abc", + + FileLength = 10, + + LastWriteTime = DateTime.UtcNow + + }.ToTaskItem(); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveFingerprintedStaticWebAssetEndpointsForAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveFingerprintedStaticWebAssetEndpointsForAssetsTest.cs index a27049b45841..39796abaebcd 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveFingerprintedStaticWebAssetEndpointsForAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveFingerprintedStaticWebAssetEndpointsForAssetsTest.cs @@ -1,295 +1,887 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; + + + + +[TestClass] + public class ResolveFingerprintedStaticWebAssetEndpointsForAssetsTest + + { - [Theory] - [InlineData("candidate#[.{fingerprint}]?.js", "candidate.js")] - [InlineData("candidate#[.{fingerprint}]!.js", "candidate.asdf1234.js")] + + + [TestMethod] + + + [DataRow("candidate#[.{fingerprint}]?.js", "candidate.js")] + + + [DataRow("candidate#[.{fingerprint}]!.js", "candidate.asdf1234.js")] + + public void Standalone_Selects_EndpointMatching_FilePath(string pattern, string expectedRoute) + + { + + var now = DateTime.Now; + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + List candidateAssets = [ + + CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + pattern, + + "All", + + "All", + + "asdf1234", + + "integrity" + + ) + + ]; + + + + var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); + + + + var resolvedEndpoints = new ResolveFingerprintedStaticWebAssetEndpointsForAssets + + { + + CandidateAssets = [.. candidateAssets], + + CandidateEndpoints = [..endpoints.Select(e => e.ToTaskItem())], + + IsStandalone = true, + + BuildEngine = buildEngine.Object + + }; + + + + // Act + + var result = resolvedEndpoints.Execute(); + + result.Should().BeTrue(); + + + + // Assert + + resolvedEndpoints.ResolvedEndpoints.Should().HaveCount(1); + + var endpoint = StaticWebAssetEndpoint.FromTaskItem(resolvedEndpoints.ResolvedEndpoints[0]); + + + + endpoint.Route.Should().Be(expectedRoute); + + } - [Fact] + + + + + [TestMethod] + + public void StandaloneFails_MatchingEndpointNotFound() + + { + + var now = DateTime.Now; + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + List candidateAssets = [ + + CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate#[.{fingerprint}]!.js", + + "All", + + "All", + + "asdf1234", + + "integrity" + + ) + + ]; + + + + var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); + + endpoints = endpoints.Where(e => !e.Route.Contains("asdf1234")).ToArray(); + + + + var resolvedEndpoints = new ResolveFingerprintedStaticWebAssetEndpointsForAssets + + { + + CandidateAssets = [.. candidateAssets], + + CandidateEndpoints = [.. endpoints.Select(e => e.ToTaskItem())], + + IsStandalone = true, + + BuildEngine = buildEngine.Object + + }; + + + + // Act + + var result = resolvedEndpoints.Execute(); + + result.Should().BeFalse(); + + } - [Theory] - [InlineData("candidate#[.{fingerprint}]?.js", "candidate.asdf1234.js")] - [InlineData("candidate#[.{fingerprint}]!.js", "candidate.asdf1234.js")] + + + + + [TestMethod] + + + [DataRow("candidate#[.{fingerprint}]?.js", "candidate.asdf1234.js")] + + + [DataRow("candidate#[.{fingerprint}]!.js", "candidate.asdf1234.js")] + + public void Hosted_AlwaysPrefers_FingerprintedEndpoint(string pattern, string expectedRoute) + + { + + var now = DateTime.Now; + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + List candidateAssets = [ + + CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + pattern, + + "All", + + "All", + + "asdf1234", + + "integrity" + + ) + + ]; + + + + var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); + + + + var resolvedEndpoints = new ResolveFingerprintedStaticWebAssetEndpointsForAssets + + { + + CandidateAssets = [.. candidateAssets], + + CandidateEndpoints = [.. endpoints.Select(e => e.ToTaskItem())], + + IsStandalone = false, + + BuildEngine = buildEngine.Object + + }; + + + + // Act + + var result = resolvedEndpoints.Execute(); + + result.Should().BeTrue(); + + + + // Assert + + resolvedEndpoints.ResolvedEndpoints.Should().HaveCount(1); + + var endpoint = StaticWebAssetEndpoint.FromTaskItem(resolvedEndpoints.ResolvedEndpoints[0]); + + + + endpoint.Route.Should().Be(expectedRoute); + + } - [Fact] + + + + + [TestMethod] + + public void Hosted_FallsBackToNonFingerprintedEndpoint_WhenFingerprintedVersionNotAvailable() + + { + + var now = DateTime.Now; + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + List candidateAssets = [ + + CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "asdf1234", + + "integrity" + + ) + + ]; + + + + var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); + + + + var resolvedEndpoints = new ResolveFingerprintedStaticWebAssetEndpointsForAssets + + { + + CandidateAssets = [.. candidateAssets], + + CandidateEndpoints = [.. endpoints.Select(e => e.ToTaskItem())], + + IsStandalone = false, + + BuildEngine = buildEngine.Object + + }; + + + + // Act + + var result = resolvedEndpoints.Execute(); + + result.Should().BeTrue(); + + + + // Assert + + resolvedEndpoints.ResolvedEndpoints.Should().HaveCount(1); + + var endpoint = StaticWebAssetEndpoint.FromTaskItem(resolvedEndpoints.ResolvedEndpoints[0]); + + + + endpoint.Route.Should().Be("candidate.js"); + + } - [Fact] + + + + + [TestMethod] + + public void Hosted_FailsWhen_DoesnotFindMatchingEndpoint() + + { + + var now = DateTime.Now; + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + List candidateAssets = [ + + CreateCandidate( + + Path.Combine("wwwroot", "candidate.js"), + + "MyPackage", + + "Discovered", + + "candidate.js", + + "All", + + "All", + + "asdf1234", + + "integrity" + + ) + + ]; + + + + var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); + + endpoints = endpoints.Where(e => !e.Route.Contains("asdf1234")).ToArray(); + + endpoints[0].AssetFile = Path.GetFullPath("other.js"); + + + + var resolvedEndpoints = new ResolveFingerprintedStaticWebAssetEndpointsForAssets + + { + + CandidateAssets = [.. candidateAssets], + + CandidateEndpoints = [.. endpoints.Select(e => e.ToTaskItem())], + + IsStandalone = false, + + BuildEngine = buildEngine.Object + + }; + + + + // Act + + var result = resolvedEndpoints.Execute(); + + result.Should().BeFalse(); + + } + + + + private static ITaskItem CreateCandidate( + + string itemSpec, + + string sourceId, + + string sourceType, + + string relativePath, + + string assetKind, + + string assetMode, + + string fingerprint = "", + + string integrity = "", + + string relatedAsset = "", + + string assetTraitName = "", + + string assetTraitValue = "") + + { + + var result = new StaticWebAsset() + + { + + Identity = Path.GetFullPath(itemSpec), + + SourceId = sourceId, + + SourceType = sourceType, + + ContentRoot = Directory.GetCurrentDirectory(), + + BasePath = "base", + + RelativePath = relativePath, + + AssetKind = assetKind, + + AssetMode = assetMode, + + AssetRole = "Primary", + + RelatedAsset = relatedAsset, + + AssetTraitName = assetTraitName, + + AssetTraitValue = assetTraitValue, + + CopyToOutputDirectory = "", + + CopyToPublishDirectory = "", + + OriginalItemSpec = itemSpec, + + // Add these to avoid accessing the disk to compute them + + Integrity = integrity, + + Fingerprint = fingerprint, + + FileLength = 10, + + LastWriteTime = DateTime.UtcNow, + + }; + + + + result.ApplyDefaults(); + + result.Normalize(); + + + + return result.ToTaskItem(); + + } + + + + 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(); + + + + defineStaticWebAssetEndpoints.Execute(); + + return StaticWebAssetEndpoint.FromItemGroup(defineStaticWebAssetEndpoints.Endpoints); + + } + + + + private static TaskItem CreateContentMapping(string pattern, string contentType) + + { + + return new TaskItem(contentType, new Dictionary + + { + + { "Pattern", pattern }, + + { "Priority", "0" } + + }); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/RewriteCssTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/RewriteCssTest.cs index 3665a85814bc..eac98917d57c 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/RewriteCssTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/RewriteCssTest.cs @@ -1,389 +1,1159 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.StaticWebAssets.Tasks; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + + + + +[TestClass] + public class RewriteCssTest + + { - [Fact] + + + [TestMethod] + + public void HandlesEmptyFile() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", string.Empty, "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(string.Empty, result); + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(string.Empty, result); + + } - [Fact] + + + + + [TestMethod] + + public void AddsScopeAfterSelector() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + .myclass { color: red; } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + .myclass[TestScope] { color: red; } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void HandlesMultipleSelectors() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + .first, .second { color: red; } + + .third { color: blue; } + + :root { color: green; } + + * { color: white; } + + #some-id { color: yellow; } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + .first[TestScope], .second[TestScope] { color: red; } + + .third[TestScope] { color: blue; } + + :root[TestScope] { color: green; } + + *[TestScope] { color: white; } + + #some-id[TestScope] { color: yellow; } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void HandlesComplexSelectors() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + .first div > li, body .second:not(.fancy)[attr~=whatever] { color: red; } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + .first div > li[TestScope], body .second:not(.fancy)[attr~=whatever][TestScope] { color: red; } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void HandlesSpacesAndCommentsWithinSelectors() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + .first /* space at end {} */ div , .myclass /* comment at end */ { color: red; } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + .first /* space at end {} */ div[TestScope] , .myclass[TestScope] /* comment at end */ { color: red; } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void HandlesPseudoClasses() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + a:fake-pseudo-class { color: red; } + + a:focus b:hover { color: green; } + + tr:nth-child(4n + 1) { color: blue; } + + a:has(b > c) { color: yellow; } + + a:last-child > ::deep b { color: pink; } + + a:not(#something) { color: purple; } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + a:fake-pseudo-class[TestScope] { color: red; } + + a:focus b:hover[TestScope] { color: green; } + + tr:nth-child(4n + 1)[TestScope] { color: blue; } + + a:has(b > c)[TestScope] { color: yellow; } + + a:last-child[TestScope] > b { color: pink; } + + a:not(#something)[TestScope] { color: purple; } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void HandlesPseudoElements() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + a::before { content: ""✋""; } + + a::after::placeholder { content: ""🐯""; } + + custom-element::part(foo) { content: ""🤷‍""; } + + a::before > ::deep another { content: ""👞""; } + + a::fake-PsEuDo-element { content: ""🐔""; } + + ::selection { content: ""😾""; } + + other, ::selection { content: ""👂""; } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + a[TestScope]::before { content: ""✋""; } + + a[TestScope]::after::placeholder { content: ""🐯""; } + + custom-element[TestScope]::part(foo) { content: ""🤷‍""; } + + a[TestScope]::before > another { content: ""👞""; } + + a[TestScope]::fake-PsEuDo-element { content: ""🐔""; } + + [TestScope]::selection { content: ""😾""; } + + other[TestScope], [TestScope]::selection { content: ""👂""; } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void HandlesSingleColonPseudoElements() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + a:after { content: ""x""; } + + a:before { content: ""x""; } + + a:first-letter { content: ""x""; } + + a:first-line { content: ""x""; } + + a:AFTER { content: ""x""; } + + a:not(something):before { content: ""x""; } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + a[TestScope]:after { content: ""x""; } + + a[TestScope]:before { content: ""x""; } + + a[TestScope]:first-letter { content: ""x""; } + + a[TestScope]:first-line { content: ""x""; } + + a[TestScope]:AFTER { content: ""x""; } + + a:not(something)[TestScope]:before { content: ""x""; } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void RespectsDeepCombinator() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + .first ::deep .second { color: red; } + + a ::deep b, c ::deep d { color: blue; } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + .first[TestScope] .second { color: red; } + + a[TestScope] b, c[TestScope] d { color: blue; } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void RespectsDeepCombinatorWithDirectDescendant() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + a > ::deep b { color: red; } + + c ::deep > d { color: blue; } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + a[TestScope] > b { color: red; } + + c[TestScope] > d { color: blue; } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void RespectsDeepCombinatorWithAdjacentSibling() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + a + ::deep b { color: red; } + + c ::deep + d { color: blue; } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + a[TestScope] + b { color: red; } + + c[TestScope] + d { color: blue; } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void RespectsDeepCombinatorWithGeneralSibling() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + a ~ ::deep b { color: red; } + + c ::deep ~ d { color: blue; } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + a[TestScope] ~ b { color: red; } + + c[TestScope] ~ d { color: blue; } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void IgnoresMultipleDeepCombinators() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + .first ::deep .second ::deep .third { color:red; } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + .first[TestScope] .second ::deep .third { color:red; } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void RespectsDeepCombinatorWithSpacesAndComments() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + .a .b /* comment ::deep 1 */ ::deep /* comment ::deep 2 */ .c /* ::deep */ .d { color: red; } + + ::deep * { color: blue; } /* Leading deep combinator */ + + another ::deep { color: green } /* Trailing deep combinator */ + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + .a .b[TestScope] /* comment ::deep 1 */ /* comment ::deep 2 */ .c /* ::deep */ .d { color: red; } + + [TestScope] * { color: blue; } /* Leading deep combinator */ + + another[TestScope] { color: green } /* Trailing deep combinator */ + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void HandlesAtBlocks() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + .myclass { color: red; } + + + + @media only screen and (max-width: 600px) { + + .another .thing { + + content: 'This should not be a selector: .fake-selector { color: red }' + + } + + } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + .myclass[TestScope] { color: red; } + + + + @media only screen and (max-width: 600px) { + + .another .thing[TestScope] { + + content: 'This should not be a selector: .fake-selector { color: red }' + + } + + } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void AddsScopeToKeyframeNames() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + @keyframes my-animation { /* whatever */ } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + @keyframes my-animation-TestScope { /* whatever */ } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void RewritesAnimationNamesWhenMatchingKnownKeyframes() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + .myclass { + + color: red; + + animation: /* ignore comment */ my-animation 1s infinite; + + } + + + + .another-thing { animation-name: different-animation; } + + + + h1 { animation: unknown-animation; } /* Should not be scoped */ + + + + @keyframes my-animation { /* whatever */ } + + @keyframes different-animation { /* whatever */ } + + @keyframes unused-animation { /* whatever */ } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + .myclass[TestScope] { + + color: red; + + animation: /* ignore comment */ my-animation-TestScope 1s infinite; + + } + + + + .another-thing[TestScope] { animation-name: different-animation-TestScope; } + + + + h1[TestScope] { animation: unknown-animation; } /* Should not be scoped */ + + + + @keyframes my-animation-TestScope { /* whatever */ } + + @keyframes different-animation-TestScope { /* whatever */ } + + @keyframes unused-animation-TestScope { /* whatever */ } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void RewritesMultipleAnimationNames() + + { + + // Arrange/act + + var result = RewriteCss.AddScopeToSelectors("file.css", @" + + .myclass1 { animation-name: my-animation , different-animation } + + .myclass2 { animation: 4s linear 0s alternate my-animation infinite, different-animation 0s } + + @keyframes my-animation { } + + @keyframes different-animation { } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Empty(errors); - Assert.Equal(@" + + + Assert.IsEmpty(errors); + + + Assert.AreEqual(@" + + .myclass1[TestScope] { animation-name: my-animation-TestScope , different-animation-TestScope } + + .myclass2[TestScope] { animation: 4s linear 0s alternate my-animation-TestScope infinite, different-animation-TestScope 0s } + + @keyframes my-animation-TestScope { } + + @keyframes different-animation-TestScope { } + + ", result); + + } - [Fact] + + + + + [TestMethod] + + public void RejectsImportStatements() + + { + + // Arrange/act + + RewriteCss.AddScopeToSelectors("file.css", @" + + @import ""basic-import.css""; + + @import ""import-with-media-type.css"" print; + + @import ""import-with-media-query.css"" screen and (orientation:landscape); + + @ImPoRt /* comment */ ""scheme://path/to/complex-import"" /* another-comment */ screen; + + @otheratrule ""should-not-cause-error.css""; + + /* @import ""should-be-ignored-because-it-is-in-a-comment.css""; */ + + .myclass { color: red; } + + ", "TestScope", out var errors); + + + + // Assert - Assert.Collection(errors, - error => Assert.Equal("file.css(2,5): @import rules are not supported within scoped CSS files because the loading order would be undefined. @import may only be placed in non-scoped CSS files.", error.ToString()), - error => Assert.Equal("file.css(3,5): @import rules are not supported within scoped CSS files because the loading order would be undefined. @import may only be placed in non-scoped CSS files.", error.ToString()), - error => Assert.Equal("file.css(4,5): @import rules are not supported within scoped CSS files because the loading order would be undefined. @import may only be placed in non-scoped CSS files.", error.ToString()), - error => Assert.Equal("file.css(5,5): @import rules are not supported within scoped CSS files because the loading order would be undefined. @import may only be placed in non-scoped CSS files.", error.ToString())); + var errorList = errors.ToList(); + Assert.HasCount(4, errorList); + Assert.AreEqual("file.css(2,5): @import rules are not supported within scoped CSS files because the loading order would be undefined. @import may only be placed in non-scoped CSS files.", errorList[0].ToString()); + Assert.AreEqual("file.css(3,5): @import rules are not supported within scoped CSS files because the loading order would be undefined. @import may only be placed in non-scoped CSS files.", errorList[1].ToString()); + Assert.AreEqual("file.css(4,5): @import rules are not supported within scoped CSS files because the loading order would be undefined. @import may only be placed in non-scoped CSS files.", errorList[2].ToString()); + Assert.AreEqual("file.css(5,5): @import rules are not supported within scoped CSS files because the loading order would be undefined. @import may only be placed in non-scoped CSS files.", errorList[3].ToString()); + + } + + } + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetEndpointTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetEndpointTest.cs index 9b5178f5a2b3..8e25846079df 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetEndpointTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetEndpointTest.cs @@ -1,63 +1,191 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + + + + +[TestClass] + public class StaticWebAssetEndpointTest + + { - [Theory] - [InlineData("App1/css/app.css", "App1")] - [InlineData("App1", "App1")] - [InlineData("App1/css/styles/app.css", "App1/css")] - [InlineData("App1/css/app.css", "")] - [InlineData("", "")] - [InlineData("App1\\css\\app.css", "App1")] - [InlineData("App1/css\\app.css", "App1")] - [InlineData("App1/App1.lib.module.js", "App1")] - [InlineData("app1/css/app.css", "App1")] - [InlineData("APP1/css/app.css", "app1")] + + + [TestMethod] + + + [DataRow("App1/css/app.css", "App1")] + + + [DataRow("App1", "App1")] + + + [DataRow("App1/css/styles/app.css", "App1/css")] + + + [DataRow("App1/css/app.css", "")] + + + [DataRow("", "")] + + + [DataRow("App1\\css\\app.css", "App1")] + + + [DataRow("App1/css\\app.css", "App1")] + + + [DataRow("App1/App1.lib.module.js", "App1")] + + + [DataRow("app1/css/app.css", "App1")] + + + [DataRow("APP1/css/app.css", "app1")] + + public void RouteHasPathPrefix_ReturnsTrue_WhenRouteStartsWithPrefixAsPathSegment(string route, string prefix) + + { + + var routeSegments = new List(); + + var prefixSegments = new List(); + + + + var result = StaticWebAssetEndpoint.RouteHasPathPrefix(route, prefix, routeSegments, prefixSegments); + + + + result.Should().BeTrue(); + + } - [Theory] - [InlineData("App1.styles.css", "App1")] - [InlineData("App1", "App1/css/app.css")] - [InlineData("App1/js/app.js", "App1/css")] - [InlineData("App12/css/app.css", "App1")] - [InlineData("App1Bundle/app.js", "App1")] - [InlineData("App1.lib.module.js", "App1")] + + + + + [TestMethod] + + + [DataRow("App1.styles.css", "App1")] + + + [DataRow("App1", "App1/css/app.css")] + + + [DataRow("App1/js/app.js", "App1/css")] + + + [DataRow("App12/css/app.css", "App1")] + + + [DataRow("App1Bundle/app.js", "App1")] + + + [DataRow("App1.lib.module.js", "App1")] + + public void RouteHasPathPrefix_ReturnsFalse_WhenRouteDoesNotStartWithPrefixAsPathSegment(string route, string prefix) + + { + + var routeSegments = new List(); + + var prefixSegments = new List(); + + + + var result = StaticWebAssetEndpoint.RouteHasPathPrefix(route, prefix, routeSegments, prefixSegments); + + + + result.Should().BeFalse(); + + } - [Fact] + + + + + [TestMethod] + + public void RouteHasPathPrefix_ReusesSegmentLists() + + { + + var routeSegments = new List(); + + var prefixSegments = new List(); + + + + StaticWebAssetEndpoint.RouteHasPathPrefix("a/b/c", "a", routeSegments, prefixSegments); + + StaticWebAssetEndpoint.RouteHasPathPrefix("x/y/z", "x/y", routeSegments, prefixSegments); + + + + var result = StaticWebAssetEndpoint.RouteHasPathPrefix("App1/css/app.css", "App1", routeSegments, prefixSegments); + + + + result.Should().BeTrue(); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetPathPatternTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetPathPatternTest.cs index 2f0033ac99b5..234cee19c83b 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetPathPatternTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetPathPatternTest.cs @@ -1,724 +1,2174 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Globalization; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; + + + + +[TestClass] + public class StaticWebAssetPathPatternTest + + { - [Fact] + + + [TestMethod] + + public void CanParse_PathWithNoExpressions() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site.css", "MyApp"); + + var expected = new StaticWebAssetPathPattern("css/site.css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "css/site.css".AsMemory(), IsLiteral = true }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_ComplexFingerprintExpression_Middle() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}].css", "MyApp"); + + var expected = new StaticWebAssetPathPattern("css/site#[.{fingerprint}].css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "css/site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, + + new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_ComplexFingerprintExpression_Start() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}].css", "MyApp"); + + var expected = new StaticWebAssetPathPattern("#[.{fingerprint}].css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, + + new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_ComplexFingerprintExpression_End() + + { + + var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]", "MyApp"); + + var expected = new StaticWebAssetPathPattern("site#[.{fingerprint}]") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_ComplexFingerprintExpression_Only() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}]", "MyApp"); + + var expected = new StaticWebAssetPathPattern("#[.{fingerprint}]") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_ComplexFingerprintExpression_Multiple() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}]-#[.{version}].css", "MyApp"); + + var expected = new StaticWebAssetPathPattern("css/site#[.{fingerprint}]-#[.{version}].css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "css/site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, + + new (){ Parts = [ new() { Name = "-".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "version".AsMemory(), IsLiteral = false }] }, + + new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_ComplexFingerprintExpression_ConsecutiveExpressions() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}]#[.{version}].css", "MyApp"); + + var expected = new StaticWebAssetPathPattern("css/site#[.{fingerprint}]#[.{version}].css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "css/site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "version".AsMemory(), IsLiteral = false }] }, + + new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_SimpleFingerprintExpression_Start() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint}].css", "MyApp"); + + var expected = new StaticWebAssetPathPattern("#[{fingerprint}].css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, + + new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_SimpleFingerprintExpression_Middle() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[{fingerprint}].css", "MyApp"); + + var expected = new StaticWebAssetPathPattern("css/site#[{fingerprint}].css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "css/site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, + + new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_SimpleFingerprintExpression_End() + + { + + var pattern = StaticWebAssetPathPattern.Parse("site#[{fingerprint}]", "MyApp"); + + var expected = new StaticWebAssetPathPattern("site#[{fingerprint}]") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_SimpleFingerprintExpression_Only() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint}]", "MyApp"); + + var expected = new StaticWebAssetPathPattern("#[{fingerprint}]") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_SimpleFingerprintExpression_WithEmbeddedValues() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint=value}]", "MyApp"); + + var expected = new StaticWebAssetPathPattern("#[{fingerprint=value}]") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "fingerprint".AsMemory(), Value = "value".AsMemory(), IsLiteral = false }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_ComplexExpression_MultipleVariables() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}-{version}].css", "MyApp"); + + var expected = new StaticWebAssetPathPattern("css/site#[.{fingerprint}-{version}].css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "css/site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ + + new() { Name = ".".AsMemory(), IsLiteral = true }, + + new() { Name = "fingerprint".AsMemory(), IsLiteral = false }, + + new() { Name = "-".AsMemory(), IsLiteral = true }, + + new() { Name = "version".AsMemory(), IsLiteral = false } + + ] }, + + new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_ComplexExpression_MultipleConsecutiveVariables() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}{version}].css", "MyApp"); + + var expected = new StaticWebAssetPathPattern("css/site#[.{fingerprint}{version}].css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "css/site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ + + new() { Name = ".".AsMemory(), IsLiteral = true }, + + new() { Name = "fingerprint".AsMemory(), IsLiteral = false }, + + new() { Name = "version".AsMemory(), IsLiteral = false } + + ] }, + + new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_ComplexExpression_StartsWithVariable() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint}.]css", "MyApp"); + + var expected = new StaticWebAssetPathPattern("#[{fingerprint}.]css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "fingerprint".AsMemory(), IsLiteral = false }, new() { Name = ".".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = "css".AsMemory(), IsLiteral = true }] } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_OptionalExpressions_End() + + { + + var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]?", "MyApp"); + + var expected = new StaticWebAssetPathPattern("site#[.{fingerprint}]?") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }], IsOptional = true } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_OptionalPreferredExpressions() + + { + + var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]!", "MyApp"); + + var expected = new StaticWebAssetPathPattern("site#[.{fingerprint}]!") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }], IsOptional = true, IsPreferred = true } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_OptionalExpressions_Start() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}]?site", "MyApp"); + + var expected = new StaticWebAssetPathPattern("#[.{fingerprint}]?site") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }], IsOptional = true }, + + new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] + + }] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_OptionalExpressions_Middle() + + { + + var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]?site", "MyApp"); + + var expected = new StaticWebAssetPathPattern("site#[.{fingerprint}]?site") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }], IsOptional = true }, + + new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] + + }] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_OptionalExpressions_Only() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}]?", "MyApp"); + + var expected = new StaticWebAssetPathPattern("#[.{fingerprint}]?") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }], IsOptional = true } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_MultipleOptionalExpressions() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}]?site#[.{version}]?", "MyApp"); + + var expected = new StaticWebAssetPathPattern("#[.{fingerprint}]?site#[.{version}]?") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }], IsOptional = true }, + + new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }], IsOptional = false }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "version".AsMemory(), IsLiteral = false }], IsOptional = true } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanParse_ConsecutiveOptionalExpressions() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}]?#[.{version}]?", "MyApp"); + + var expected = new StaticWebAssetPathPattern("#[.{fingerprint}]?#[.{version}]?") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }], IsOptional = true }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "version".AsMemory(), IsLiteral = false }], IsOptional = true } + + ] + + }; - Assert.Equal(expected, pattern); + + + + + Assert.AreEqual(expected, pattern); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_PathWithNoExpressions() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site.css", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234" + + }; - var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - Assert.Equal("css/site.css", path); - } - [Fact] + var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); + + + + + + Assert.AreEqual("css/site.css", path); + + + } + + + + + + [TestMethod] + + public void CanReplaceTokens_ComplexFingerprintExpression_Middle() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}].css", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234" + + }; + + var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - Assert.Equal("css/site.asdf1234.css", path); + + + + + Assert.AreEqual("css/site.asdf1234.css", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_ComplexFingerprintExpression_Start() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}].css", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234" + + }; + + var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - Assert.Equal(".asdf1234.css", path); + + + + + Assert.AreEqual(".asdf1234.css", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_ComplexFingerprintExpression_End() + + { + + var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234" + + }; + + var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - Assert.Equal("site.asdf1234", path); + + + + + Assert.AreEqual("site.asdf1234", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_ComplexFingerprintExpression_Only() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}]", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234" + + }; + + var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - Assert.Equal(".asdf1234", path); + + + + + Assert.AreEqual(".asdf1234", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_ComplexFingerprintExpression_Multiple() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}]-#[.{version}].css", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234", + + }; + + var (path, _) = pattern.ReplaceTokens( + + tokens, + + CreateTestResolver(new Dictionary(StringComparer.OrdinalIgnoreCase) { ["version"] = "v1" })); - Assert.Equal("css/site.asdf1234-.v1.css", path); + + + + + Assert.AreEqual("css/site.asdf1234-.v1.css", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_ComplexFingerprintExpression_ConsecutiveExpressions() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}]#[.{version}].css", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234", + + }; + + var (path, _) = pattern.ReplaceTokens( + + tokens, + + CreateTestResolver(new Dictionary(StringComparer.OrdinalIgnoreCase) { ["version"] = "v1" })); - Assert.Equal("css/site.asdf1234.v1.css", path); + + + + + Assert.AreEqual("css/site.asdf1234.v1.css", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_SimpleFingerprintExpression_Start() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint}].css", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234" + + }; + + var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - Assert.Equal("asdf1234.css", path); + + + + + Assert.AreEqual("asdf1234.css", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_SimpleFingerprintExpression_Middle() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[{fingerprint}].css", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234" + + }; + + var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - Assert.Equal("css/siteasdf1234.css", path); + + + + + Assert.AreEqual("css/siteasdf1234.css", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_SimpleFingerprintExpression_End() + + { + + var pattern = StaticWebAssetPathPattern.Parse("site#[{fingerprint}]", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234" + + }; + + var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - Assert.Equal("siteasdf1234", path); + + + + + Assert.AreEqual("siteasdf1234", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_SimpleFingerprintExpression_Only() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint}]", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234" + + }; + + var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - Assert.Equal("asdf1234", path); + + + + + Assert.AreEqual("asdf1234", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_SimpleFingerprintExpression_WithEmbeddedValues() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint=embedded}]", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234" + + }; + + var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - Assert.Equal("embedded", path); + + + + + Assert.AreEqual("embedded", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_ComplexExpression_MultipleVariables() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}-{version}].css", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234", + + }; + + var (path, _) = pattern.ReplaceTokens( + + tokens, + + CreateTestResolver(new Dictionary(StringComparer.OrdinalIgnoreCase) { ["version"] = "v1" })); - Assert.Equal("css/site.asdf1234-v1.css", path); + + + + + Assert.AreEqual("css/site.asdf1234-v1.css", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_ComplexExpression_MultipleConsecutiveVariables() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}{version}].css", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234", + + }; + + var (path, _) = pattern.ReplaceTokens( + + tokens, + + CreateTestResolver(new Dictionary(StringComparer.OrdinalIgnoreCase) { ["version"] = "v1" })); - Assert.Equal("css/site.asdf1234v1.css", path); + + + + + Assert.AreEqual("css/site.asdf1234v1.css", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_ComplexExpression_StartsWithVariable() + + { + + var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint}.]css", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234" + + }; + + var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - Assert.Equal("asdf1234.css", path); + + + + + Assert.AreEqual("asdf1234.css", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_ThrowsException_IfRequiredExpressionIsValue() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}].css", "MyApp"); + + var tokens = new StaticWebAsset(); - var exception = Assert.Throws(() => pattern.ReplaceTokens(tokens, CreateTestResolver())); - Assert.Equal("Token 'fingerprint' not provided for 'css/site#[.{fingerprint}].css'.", exception.Message); + + + var exception = Assert.ThrowsExactly(() => pattern.ReplaceTokens(tokens, CreateTestResolver())); + + + Assert.AreEqual("Token 'fingerprint' not provided for 'css/site#[.{fingerprint}].css'.", exception.Message); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_ThrowsException_MultipleTokenComplexExpression_MissingAtLeastOneValue() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}-{version}].css", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234" + + }; - var exception = Assert.Throws(() => pattern.ReplaceTokens(tokens, CreateTestResolver())); - Assert.Equal("Token 'version' not provided for 'css/site#[.{fingerprint}-{version}].css'.", exception.Message); + + + var exception = Assert.ThrowsExactly(() => pattern.ReplaceTokens(tokens, CreateTestResolver())); + + + Assert.AreEqual("Token 'version' not provided for 'css/site#[.{fingerprint}-{version}].css'.", exception.Message); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_OptionalExpression_OmittedWhenValueNotProvided() + + { + + var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]?", "MyApp"); + + var tokens = new StaticWebAsset(); + + var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - Assert.Equal("site", path); + + + + + Assert.AreEqual("site", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanReplaceTokens_OptionalMultipleTokenComplexExpression_OmittedWhenMissingAtLeastOneValue() + + { + + var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}.{version}]?", "MyApp"); + + var tokens = new StaticWebAsset + + { + + Fingerprint = "asdf1234" + + }; + + var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - Assert.Equal("site", path); + + + + + Assert.AreEqual("site", path); + + } - [Fact] + + + + + [TestMethod] + + public void CanExpandRoutes_LiteralPatterns() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site.css", "MyApp"); + + var routePatterns = pattern.ExpandPatternExpression(); - Assert.Equal([pattern], routePatterns); + + + + + Assert.AreSequenceEqual([pattern], routePatterns); + + } - [Fact] + + + + + [TestMethod] + + public void CanExpandRoutes_SingleRequiredExpression() + + { + + var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}].css", "MyApp"); + + var routePatterns = pattern.ExpandPatternExpression(); - Assert.Equal([pattern], routePatterns); + + + + + Assert.AreSequenceEqual([pattern], routePatterns); + + } - [Fact] + + + + + [TestMethod] + + public void CanExpandRoutes_SingleOptionalExpression() + + { + + var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]?.css", "MyApp"); + + var routePatterns = pattern.ExpandPatternExpression(); + + + + var expected = new[] + + { + + new StaticWebAssetPathPattern("site.css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } + + ] + + }, + + new StaticWebAssetPathPattern("site#[.{fingerprint}].css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, + + new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } + + ] + + } + + }; - Assert.Equal(expected, routePatterns); + + + + + Assert.AreSequenceEqual(expected, routePatterns); + + } - [Fact] + + + + + [TestMethod] + + public void CanExpandRoutes_MultipleOptionalExpressions() + + { + + var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]?#[.{version}]?.css", "MyApp"); + + var routePatterns = pattern.ExpandPatternExpression(); + + + + var expected = new[] + + { + + new StaticWebAssetPathPattern("site.css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } + + ] + + }, + + new StaticWebAssetPathPattern("site#[.{fingerprint}].css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, + + new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } + + ] + + }, + + new StaticWebAssetPathPattern("site#[.{version}].css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "version".AsMemory(), IsLiteral = false }] }, + + new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } + + ] + + }, + + new StaticWebAssetPathPattern("site#[.{fingerprint}]#[.{version}].css") + + { + + Segments = + + [ + + new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, + + new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "version".AsMemory(), IsLiteral = false }] }, + + new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } + + ] + + } + + }; - Assert.Equal(expected, routePatterns); + + + + + Assert.AreSequenceEqual(expected, routePatterns); + + } + + + + private static StaticWebAssetTokenResolver CreateTestResolver(Dictionary additionalTokens = null) => new(additionalTokens); + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTaskEnvironmentTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTaskEnvironmentTests.cs index 30a2109492c0..33fa5704f91b 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTaskEnvironmentTests.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTaskEnvironmentTests.cs @@ -1,258 +1,776 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + + + + // Test parallelization is disabled assembly-wide via + + // [assembly:CollectionBehavior(DisableTestParallelization = true)] in + + // LegacyStaticWebAssetsV1IntegrationTest.cs, which already isolates the + + // process-CWD mutation these tests perform. + + +[TestClass] + public class StaticWebAssetTaskEnvironmentTests + + { - [Fact] + + + [TestMethod] + + public void NormalizeContentRootPath_WithTaskEnvironment_AbsolutizesAgainstProjectDirectory_NotProcessCurrentDirectory() + + { + + WithDecoyCwdAndProjectDirectory((projectDir, _) => + + { + + var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); + + + + var result = StaticWebAsset.NormalizeContentRootPath("wwwroot", env); + + + + result.Should().Be(Path.Combine(projectDir, "wwwroot") + Path.DirectorySeparatorChar, + + "the relative ContentRoot must be resolved against TaskEnvironment.ProjectDirectory, not the process CWD"); + + }); + + } - [Fact] + + + + + [TestMethod] + + public void NormalizeContentRootPath_WithoutEnvOverload_StillUsesProcessCurrentDirectory_ForBackCompat() + + { + + WithDecoyCwdAndProjectDirectory((_, spawnDir) => + + { + + // The parameterless overload preserves the pre-existing behavior so unmigrated + + // call sites continue to work; only callers that opt in via the env overload get + + // the MT-safe resolution. + + var result = StaticWebAsset.NormalizeContentRootPath("wwwroot"); + + + + result.Should().Be(Path.Combine(spawnDir, "wwwroot") + Path.DirectorySeparatorChar); + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Normalize_WithTaskEnvironment_AbsolutizesContentRootAndRelatedAssetAgainstProjectDirectory() + + { + + WithDecoyCwdAndProjectDirectory((projectDir, _) => + + { + + var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); + + var asset = new StaticWebAsset + + { + + Identity = Path.Combine(projectDir, "site.css"), + + SourceId = "MyProject", + + SourceType = StaticWebAsset.SourceTypes.Discovered, + + ContentRoot = "wwwroot", + + BasePath = "/", + + RelativePath = "site.css", + + RelatedAsset = "related/asset.css", + + }; + + + + asset.Normalize(env); + + + + asset.ContentRoot.Should().Be(Path.Combine(projectDir, "wwwroot") + Path.DirectorySeparatorChar); + + asset.RelatedAsset.Should().Be(Path.Combine(projectDir, "related", "asset.css")); + + }); + + } - [Fact] + + + + + [TestMethod] + + public void FromTaskItem_WithTaskEnvironment_HydratesAssetWithProjectDirectoryAbsolutizedPaths() + + { + + WithDecoyCwdAndProjectDirectory((projectDir, _) => + + { + + var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); + + var item = new TaskItem(Path.Combine(projectDir, "site.css"), new Dictionary + + { + + [nameof(StaticWebAsset.SourceId)] = "MyProject", + + [nameof(StaticWebAsset.SourceType)] = StaticWebAsset.SourceTypes.Discovered, + + [nameof(StaticWebAsset.ContentRoot)] = "wwwroot", + + [nameof(StaticWebAsset.BasePath)] = "/", + + [nameof(StaticWebAsset.RelativePath)] = "site.css", + + [nameof(StaticWebAsset.RelatedAsset)] = "", + + }); + + + + var asset = StaticWebAsset.FromTaskItem(item, env); + + + + asset.ContentRoot.Should().Be(Path.Combine(projectDir, "wwwroot") + Path.DirectorySeparatorChar); + + }); + + } - [Fact] + + + + + [TestMethod] + + public void FromV1TaskItem_WithTaskEnvironment_HydratesAssetWithProjectDirectoryAbsolutizedPaths() + + { + + WithDecoyCwdAndProjectDirectory((projectDir, _) => + + { + + var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); + + var assetIdentity = Path.Combine(projectDir, "wwwroot", "site.css"); + + Directory.CreateDirectory(Path.GetDirectoryName(assetIdentity)); + + File.WriteAllText(assetIdentity, "body{}"); + + var item = new TaskItem(assetIdentity, new Dictionary + + { + + [nameof(StaticWebAsset.SourceId)] = "SomePackage", + + [nameof(StaticWebAsset.SourceType)] = StaticWebAsset.SourceTypes.Package, + + [nameof(StaticWebAsset.ContentRoot)] = "wwwroot", + + [nameof(StaticWebAsset.BasePath)] = "_content/SomePackage", + + [nameof(StaticWebAsset.RelativePath)] = "site.css", + + [nameof(StaticWebAsset.OriginalItemSpec)] = assetIdentity, + + [nameof(StaticWebAsset.Fingerprint)] = "deadbeef", + + [nameof(StaticWebAsset.Integrity)] = "sha256-fake", + + [nameof(StaticWebAsset.FileLength)] = "6", + + [nameof(StaticWebAsset.LastWriteTime)] = DateTimeOffset.UtcNow.ToString("o"), + + }); + + + + var asset = StaticWebAsset.FromV1TaskItem(item, env); + + + + asset.ContentRoot.Should().Be(Path.Combine(projectDir, "wwwroot") + Path.DirectorySeparatorChar); + + }); + + } - [Fact] + + + + + [TestMethod] + + public void FromTaskItemGroup_WithTaskEnvironment_AbsolutizesAllAssets() + + { + + WithDecoyCwdAndProjectDirectory((projectDir, _) => + + { + + var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); + + var items = new ITaskItem[] + + { + + MakeDiscoveredItem(projectDir, contentRoot: "wwwroot", relativePath: "a.css"), + + MakeDiscoveredItem(projectDir, contentRoot: "wwwroot", relativePath: "b.css"), + + }; + + + + var assets = StaticWebAsset.FromTaskItemGroup(items, env); + + + + assets.Should().AllSatisfy(asset => + + asset.ContentRoot.Should().Be(Path.Combine(projectDir, "wwwroot") + Path.DirectorySeparatorChar)); + + }); + + } - [Fact] + + + + + [TestMethod] + + public void ResolveFile_WithTaskEnvironment_ResolvesIdentityRelativeToProjectDirectory() + + { + + WithDecoyCwdAndProjectDirectory((projectDir, _) => + + { + + var realFile = Path.Combine(projectDir, "wwwroot", "site.css"); + + Directory.CreateDirectory(Path.GetDirectoryName(realFile)); + + File.WriteAllText(realFile, "body{}"); + + + + var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); + + + + var info = StaticWebAsset.ResolveFile(Path.Combine("wwwroot", "site.css"), originalItemSpec: "", env); + + + + info.FullName.Should().Be(realFile); + + info.Exists.Should().BeTrue(); + + }); + + } - [Fact] + + + + + [TestMethod] + + public void HasContentRoot_WithTaskEnvironment_ComparesAgainstProjectDirectoryNormalizedForm() + + { + + WithDecoyCwdAndProjectDirectory((projectDir, _) => + + { + + var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); + + var asset = new StaticWebAsset + + { + + Identity = Path.Combine(projectDir, "site.css"), + + ContentRoot = "wwwroot", + + BasePath = "/", + + RelativePath = "site.css", + + SourceId = "X", + + SourceType = StaticWebAsset.SourceTypes.Discovered, + + }; + + asset.Normalize(env); + + + + asset.HasContentRoot("wwwroot", env).Should().BeTrue(); + + }); + + } - [Fact] + + + + + [TestMethod] + + public void NormalizeContentRootPath_WithTaskEnvironment_PreservesCanonicalization_DotDot() + + { + + WithDecoyCwdAndProjectDirectory((projectDir, _) => + + { + + var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); + + + + var result = StaticWebAsset.NormalizeContentRootPath(Path.Combine("a", "..", "wwwroot"), env); + + + + // Outer Path.GetFullPath collapses ".." segments while the inner env.GetAbsolutePath + + // ensures the base is the project directory. The two together preserve the + + // pre-migration canonicalization semantics callers depend on for equality checks. + + result.Should().Be(Path.Combine(projectDir, "wwwroot") + Path.DirectorySeparatorChar); + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Normalize_WithTaskEnvironment_AbsolutePathInputs_ArePreservedAndCanonicalized() + + { + + // The single most common production case: upstream targets pre-absolutize. The new + + // overload must remain a no-op for already-absolute inputs (no surprise re-rooting). + + WithDecoyCwdAndProjectDirectory((projectDir, _) => + + { + + var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); + + var absoluteContentRoot = Path.Combine(projectDir, "alreadyabsolute"); + + var absoluteRelatedAsset = Path.Combine(projectDir, "other", "asset.css"); + + var asset = new StaticWebAsset + + { + + Identity = Path.Combine(projectDir, "site.css"), + + SourceId = "X", + + SourceType = StaticWebAsset.SourceTypes.Discovered, + + ContentRoot = absoluteContentRoot, + + BasePath = "/", + + RelativePath = "site.css", + + RelatedAsset = absoluteRelatedAsset, + + }; + + + + asset.Normalize(env); + + + + asset.ContentRoot.Should().Be(absoluteContentRoot + Path.DirectorySeparatorChar); + + asset.RelatedAsset.Should().Be(absoluteRelatedAsset); + + }); + + } + + + + private static ITaskItem MakeDiscoveredItem(string projectDir, string contentRoot, string relativePath) + + { + + return new TaskItem(Path.Combine(projectDir, "site.css"), new Dictionary + + { + + [nameof(StaticWebAsset.SourceId)] = "MyProject", + + [nameof(StaticWebAsset.SourceType)] = StaticWebAsset.SourceTypes.Discovered, + + [nameof(StaticWebAsset.ContentRoot)] = contentRoot, + + [nameof(StaticWebAsset.BasePath)] = "/", + + [nameof(StaticWebAsset.RelativePath)] = relativePath, + + }); + + } + + + + private static void WithDecoyCwdAndProjectDirectory(Action body) + + { + + var testRoot = Path.Combine(AppContext.BaseDirectory, nameof(StaticWebAssetTaskEnvironmentTests), Guid.NewGuid().ToString("N")); + + var projectDir = Path.Combine(testRoot, "project"); + + var spawnDir = Path.Combine(testRoot, "decoy", "spawn"); + + Directory.CreateDirectory(projectDir); + + Directory.CreateDirectory(spawnDir); + + + + var originalCwd = Directory.GetCurrentDirectory(); + + try + + { + + Directory.SetCurrentDirectory(spawnDir); + + body(projectDir, spawnDir); + + } + + finally + + { + + Directory.SetCurrentDirectory(originalCwd); + + if (Directory.Exists(testRoot)) + + { + + Directory.Delete(testRoot, recursive: true); + + } + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTest.cs index faf9b7cfe8cf..6de3bb9b4588 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTest.cs @@ -1,450 +1,1347 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; + + + + +[TestClass] + public class StaticWebAssetTest + + { - [Fact] + + + [TestMethod] + + public void ValidateAssetGroup_SingleAsset_ReturnsTrue() + + { + + var asset = CreateAsset("wwwroot/app.js", "app.js", "All", "All"); + + var group = (asset, (StaticWebAsset)null, (IReadOnlyList)null); + + + + var groupSet = new HashSet(StringComparer.Ordinal); + + var result = StaticWebAsset.ValidateAssetGroup("app.js", group, out var reason, groupSet); - Assert.True(result); - Assert.Null(reason); + + + + + Assert.IsTrue(result); + + + Assert.IsNull(reason); + + } - [Fact] + + + + + [TestMethod] + + public void ValidateAssetGroup_TwoAssetsFromDifferentProjects_ReturnsFalse() + + { + + var asset1 = CreateAsset("wwwroot/app.js", "app.js", "All", "All", sourceId: "Project1"); + + var asset2 = CreateAsset("wwwroot/app.js", "app.js", "All", "All", sourceId: "Project2"); + + var group = (asset1, asset2, (IReadOnlyList)null); + + + + var groupSet2 = new HashSet(StringComparer.Ordinal); + + var result = StaticWebAsset.ValidateAssetGroup("app.js", group, out var reason, groupSet2); - Assert.False(result); + + + + + Assert.IsFalse(result); + + Assert.Contains("different projects", reason); + + } - [Fact] + + + + + [TestMethod] + + public void ValidateAssetGroup_TwoAllAssetsFromSameProject_ReturnsFalse() + + { + + var asset1 = CreateAsset("wwwroot/app.js", "app.js", "All", "All"); + + var asset2 = CreateAsset("obj/app.js", "app.js", "All", "All"); + + var group = (asset1, asset2, (IReadOnlyList)null); + + + + var groupSet3 = new HashSet(StringComparer.Ordinal); + + var result = StaticWebAsset.ValidateAssetGroup("app.js", group, out var reason, groupSet3); - Assert.False(result); + + + + + Assert.IsFalse(result); + + Assert.Contains("'All' assets", reason); + + } - [Fact] + + + + + [TestMethod] + + public void ValidateAssetGroup_BuildAndPublishAssetsFromSameProject_ReturnsTrue() + + { + + var buildAsset = CreateAsset("wwwroot/app.js", "app.js", "Build", "All"); + + var publishAsset = CreateAsset("obj/app.js", "app.js", "Publish", "All"); + + var group = (buildAsset, publishAsset, (IReadOnlyList)null); + + + + var groupSet4 = new HashSet(StringComparer.Ordinal); + + var result = StaticWebAsset.ValidateAssetGroup("app.js", group, out var reason, groupSet4); - Assert.True(result); - Assert.Null(reason); + + + + + Assert.IsTrue(result); + + + Assert.IsNull(reason); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeTargetPath_WithoutTokenResolver_KeepsTokensInPath() + + { + + var asset = CreateAsset( + + "wwwroot/MyApp.styles.css", + + "MyApp.styles#[.{fingerprint}]?.css", + + "All", + + "All"); + + asset.Fingerprint = "abc123"; + + + + var targetPath = asset.ComputeTargetPath("", '/'); - Assert.Equal("MyApp.styles#[.{fingerprint}]?.css", targetPath); + + + + + Assert.AreEqual("MyApp.styles#[.{fingerprint}]?.css", targetPath); + + } - [Fact] + + + + + [TestMethod] + + public void ComputeTargetPath_WithTokenResolver_ReplacesOptionalTokens() + + { + + var asset = CreateAsset( + + "wwwroot/MyApp.styles.css", + + "MyApp.styles#[.{fingerprint}]?.css", + + "All", + + "All"); + + asset.Fingerprint = "abc123"; + + + + var targetPath = asset.ComputeTargetPath("", '/', StaticWebAssetTokenResolver.Instance); - Assert.Equal("MyApp.styles.css", targetPath); + + + + + Assert.AreEqual("MyApp.styles.css", targetPath); + + } - [Fact] + + + + + [TestMethod] + + public void TwoAssetsWithDifferentPatternsResolveToSameTargetPath_AfterTokenReplacement() + + { + + var discoveredAsset = CreateAsset( + + "wwwroot/MyApp.styles.css", + + "MyApp.styles#[.{fingerprint}]?.css", + + "All", + + "All"); + + discoveredAsset.Fingerprint = "abc123"; + + + + var computedAsset = CreateAsset( + + "obj/scopedcss/bundle/MyApp.styles.css", + + "MyApp#[.{fingerprint}]?.styles.css", + + "All", + + "CurrentProject"); + + computedAsset.Fingerprint = "xyz789"; + + + + var path1WithTokens = discoveredAsset.ComputeTargetPath("", '/'); + + var path2WithTokens = computedAsset.ComputeTargetPath("", '/'); - Assert.NotEqual(path1WithTokens, path2WithTokens); - Assert.Equal("MyApp.styles#[.{fingerprint}]?.css", path1WithTokens); - Assert.Equal("MyApp#[.{fingerprint}]?.styles.css", path2WithTokens); + + + + + Assert.AreNotEqual(path1WithTokens, path2WithTokens); + + + Assert.AreEqual("MyApp.styles#[.{fingerprint}]?.css", path1WithTokens); + + + Assert.AreEqual("MyApp#[.{fingerprint}]?.styles.css", path2WithTokens); + + + + var path1Resolved = discoveredAsset.ComputeTargetPath("", '/', StaticWebAssetTokenResolver.Instance); + + var path2Resolved = computedAsset.ComputeTargetPath("", '/', StaticWebAssetTokenResolver.Instance); - Assert.Equal("MyApp.styles.css", path1Resolved); - Assert.Equal("MyApp.styles.css", path2Resolved); - Assert.Equal(path1Resolved, path2Resolved); + + + + + Assert.AreEqual("MyApp.styles.css", path1Resolved); + + + Assert.AreEqual("MyApp.styles.css", path2Resolved); + + + Assert.AreEqual(path1Resolved, path2Resolved); + + } - [Fact] + + + + + [TestMethod] + + public void ValidateAssetGroup_DetectsConflict_WhenAssetsHaveDifferentPatterns_ButSameResolvedPath() + + { + + var discoveredAsset = CreateAsset( + + "wwwroot/MyApp.styles.css", + + "MyApp.styles#[.{fingerprint}]?.css", + + "All", + + "All"); + + discoveredAsset.Fingerprint = "abc123"; + + + + var computedAsset = CreateAsset( + + "obj/scopedcss/bundle/MyApp.styles.css", + + "MyApp#[.{fingerprint}]?.styles.css", + + "All", + + "CurrentProject"); + + computedAsset.Fingerprint = "xyz789"; + + + + var group = (discoveredAsset, computedAsset, (IReadOnlyList)null); + + var groupSet = new HashSet(StringComparer.Ordinal); + + var result = StaticWebAsset.ValidateAssetGroup("MyApp.styles.css", group, out var reason, groupSet); - Assert.False(result); + + + + + Assert.IsFalse(result); + + Assert.Contains("'All' assets", reason); + + } + + + + // SortByRelatedAssetInPlace tests - [Fact] + + + + + [TestMethod] + + public void SortByRelatedAssetInPlace_EmptyArray_DoesNothing() + + { + + var assets = Array.Empty(); + + + + StaticWebAsset.SortByRelatedAssetInPlace(assets); - Assert.Empty(assets); + + + + + Assert.IsEmpty(assets); + + } - [Fact] + + + + + [TestMethod] + + public void SortByRelatedAssetInPlace_SingleElement_DoesNothing() + + { + + var a = CreateChainAsset("A"); + + var assets = new[] { a }; + + + + StaticWebAsset.SortByRelatedAssetInPlace(assets); - Assert.Same(a, assets[0]); + + + + + Assert.AreSame(a, assets[0]); + + } - [Fact] + + + + + [TestMethod] + + public void SortByRelatedAssetInPlace_AllRoots_PreservesOrder() + + { + + var a = CreateChainAsset("A"); + + var b = CreateChainAsset("B"); + + var c = CreateChainAsset("C"); + + var assets = new[] { a, b, c }; + + + + StaticWebAsset.SortByRelatedAssetInPlace(assets); - Assert.Same(a, assets[0]); - Assert.Same(b, assets[1]); - Assert.Same(c, assets[2]); + + + + + Assert.AreSame(a, assets[0]); + + + Assert.AreSame(b, assets[1]); + + + Assert.AreSame(c, assets[2]); + + } - [Fact] + + + + + [TestMethod] + + public void SortByRelatedAssetInPlace_AlreadySorted_Chain() + + { + + // D (root) → C → B → A (parents before children) + + var d = CreateChainAsset("D"); + + var c = CreateChainAsset("C", relatedTo: "D"); + + var b = CreateChainAsset("B", relatedTo: "C"); + + var a = CreateChainAsset("A", relatedTo: "B"); + + var assets = new[] { d, c, b, a }; + + + + StaticWebAsset.SortByRelatedAssetInPlace(assets); + + AssertParentsBeforeChildren(assets); + + } - [Fact] + + + + + [TestMethod] + + public void SortByRelatedAssetInPlace_ReversedChain_WorstCase() + + { + + // Chain: A→B→C→D (D is root). Array in child-first order. + + var d = CreateChainAsset("D"); + + var c = CreateChainAsset("C", relatedTo: "D"); + + var b = CreateChainAsset("B", relatedTo: "C"); + + var a = CreateChainAsset("A", relatedTo: "B"); + + var assets = new[] { a, b, c, d }; + + + + StaticWebAsset.SortByRelatedAssetInPlace(assets); + + AssertParentsBeforeChildren(assets); + + } - [Fact] + + + + + [TestMethod] + + public void SortByRelatedAssetInPlace_LongReversedChain() + + { + + // A→B→C→D→E (E is root), worst order [A, B, C, D, E] + + var e = CreateChainAsset("E"); + + var d = CreateChainAsset("D", relatedTo: "E"); + + var c = CreateChainAsset("C", relatedTo: "D"); + + var b = CreateChainAsset("B", relatedTo: "C"); + + var a = CreateChainAsset("A", relatedTo: "B"); + + var assets = new[] { a, b, c, d, e }; + + + + StaticWebAsset.SortByRelatedAssetInPlace(assets); + + AssertParentsBeforeChildren(assets); + + } - [Fact] + + + + + [TestMethod] + + public void SortByRelatedAssetInPlace_ShuffledChain() + + { + + // A→B→C→D→E (E is root), shuffled order [C, A, D, B, E] + + var e = CreateChainAsset("E"); + + var d = CreateChainAsset("D", relatedTo: "E"); + + var c = CreateChainAsset("C", relatedTo: "D"); + + var b = CreateChainAsset("B", relatedTo: "C"); + + var a = CreateChainAsset("A", relatedTo: "B"); + + var assets = new[] { c, a, d, b, e }; + + + + StaticWebAsset.SortByRelatedAssetInPlace(assets); + + AssertParentsBeforeChildren(assets); + + } - [Fact] + + + + + [TestMethod] + + public void SortByRelatedAssetInPlace_MultipleIndependentChains() + + { + + // Chain 1: X→Y (Y root). Chain 2: P→Q→R (R root). + + var y = CreateChainAsset("Y"); + + var x = CreateChainAsset("X", relatedTo: "Y"); + + var r = CreateChainAsset("R"); + + var q = CreateChainAsset("Q", relatedTo: "R"); + + var p = CreateChainAsset("P", relatedTo: "Q"); + + // Interleave: [X, P, Q, Y, R] + + var assets = new[] { x, p, q, y, r }; + + + + StaticWebAsset.SortByRelatedAssetInPlace(assets); + + AssertParentsBeforeChildren(assets); + + } - [Fact] + + + + + [TestMethod] + + public void SortByRelatedAssetInPlace_Diamond_TwoChildrenOneParent() + + { + + // A→C, B→C, C is root. Order: [A, B, C] + + var c = CreateChainAsset("C"); + + var a = CreateChainAsset("A", relatedTo: "C"); + + var b = CreateChainAsset("B", relatedTo: "C"); + + var assets = new[] { a, b, c }; + + + + StaticWebAsset.SortByRelatedAssetInPlace(assets); + + AssertParentsBeforeChildren(assets); + + } - [Fact] + + + + + [TestMethod] + + public void SortByRelatedAssetInPlace_MixedRootsAndChain() + + { + + // Roots: R1, R2. Chain: A→B→C (C root). Order: [A, R1, B, R2, C] + + var r1 = CreateChainAsset("R1"); + + var r2 = CreateChainAsset("R2"); + + var c = CreateChainAsset("C"); + + var b = CreateChainAsset("B", relatedTo: "C"); + + var a = CreateChainAsset("A", relatedTo: "B"); + + var assets = new[] { a, r1, b, r2, c }; + + + + StaticWebAsset.SortByRelatedAssetInPlace(assets); + + AssertParentsBeforeChildren(assets); + + } - [Fact] + + + + + [TestMethod] + + public void SortByRelatedAssetInPlace_OrphanAsset_PlacedAnyway() + + { + + // A→B but B is not in the array. A should still be placed. + + var a = CreateChainAsset("A", relatedTo: "NONEXISTENT"); + + var r = CreateChainAsset("R"); + + var assets = new[] { a, r }; + + + + StaticWebAsset.SortByRelatedAssetInPlace(assets); - Assert.Equal(2, assets.Length); + + + + + Assert.HasCount(2, assets); + + Assert.Contains(a, assets); + + Assert.Contains(r, assets); + + } - [Fact] + + + + + [TestMethod] + + public void SortByRelatedAssetInPlace_TwoElements_ChildBeforeParent() + + { + + var parent = CreateChainAsset("Parent"); + + var child = CreateChainAsset("Child", relatedTo: "Parent"); + + var assets = new[] { child, parent }; - StaticWebAsset.SortByRelatedAssetInPlace(assets); - Assert.Same(parent, assets[0]); - Assert.Same(child, assets[1]); - } - [Fact] + + + StaticWebAsset.SortByRelatedAssetInPlace(assets); + + + + + + Assert.AreSame(parent, assets[0]); + + + Assert.AreSame(child, assets[1]); + + + } + + + + + + [TestMethod] + + public void SortByRelatedAssetInPlace_ProducesValidOrder_OnVariousInputs() + + { + + // Verify the in-place sort produces a valid topological ordering + + // for several shuffled inputs. + + var e = CreateChainAsset("E"); + + var d = CreateChainAsset("D", relatedTo: "E"); + + var c = CreateChainAsset("C", relatedTo: "D"); + + var b = CreateChainAsset("B", relatedTo: "C"); + + var a = CreateChainAsset("A", relatedTo: "B"); + + + + var orderings = new[] + + { + + new[] { a, b, c, d, e }, + + new[] { e, d, c, b, a }, + + new[] { c, a, e, b, d }, + + new[] { b, d, a, e, c }, + + }; + + + + foreach (var order in orderings) + + { + + var copy = (StaticWebAsset[])order.Clone(); + + StaticWebAsset.SortByRelatedAssetInPlace(copy); + + AssertParentsBeforeChildren(copy); + + } + + } - [Fact] + + + + + [TestMethod] + + public void SortByRelatedAssetInPlace_PreservesAllElements() + + { + + var e = CreateChainAsset("E"); + + var d = CreateChainAsset("D", relatedTo: "E"); + + var c = CreateChainAsset("C", relatedTo: "D"); + + var b = CreateChainAsset("B", relatedTo: "C"); + + var a = CreateChainAsset("A", relatedTo: "B"); + + var assets = new[] { a, b, c, d, e }; + + var original = new HashSet(assets); + + + + StaticWebAsset.SortByRelatedAssetInPlace(assets); - Assert.Equal(original, new HashSet(assets)); + + + + + new HashSet(assets).Should().BeEquivalentTo(original); + + } + + + + // Asserts that for every asset in the array, its RelatedAsset (parent) + + // appears at an earlier index. + + private static void AssertParentsBeforeChildren(StaticWebAsset[] assets) + + { + + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var asset in assets) + + { + + if (!string.IsNullOrEmpty(asset.RelatedAsset)) + + { - Assert.True( - seen.Contains(asset.RelatedAsset), + + + seen.Should().Contain( + asset.RelatedAsset, $"Asset '{Path.GetFileName(asset.Identity)}' appears before its parent '{Path.GetFileName(asset.RelatedAsset)}'"); + + } + + seen.Add(asset.Identity); + + } + + } + + + + private static StaticWebAsset CreateChainAsset(string name, string relatedTo = null) + + { + + var result = new StaticWebAsset + + { + + Identity = Path.GetFullPath(Path.Combine("wwwroot", name)), + + SourceId = "MyProject", + + SourceType = "Computed", + + ContentRoot = Directory.GetCurrentDirectory(), + + BasePath = "base", + + RelativePath = name, + + AssetKind = "All", + + AssetMode = "All", + + AssetRole = string.IsNullOrEmpty(relatedTo) ? "Primary" : "Related", + + AssetMergeBehavior = StaticWebAsset.MergeBehaviors.PreferTarget, + + AssetMergeSource = "", + + RelatedAsset = relatedTo == null ? "" : Path.GetFullPath(Path.Combine("wwwroot", relatedTo)), + + AssetTraitName = string.IsNullOrEmpty(relatedTo) ? "" : "Content-Encoding", + + AssetTraitValue = string.IsNullOrEmpty(relatedTo) ? "" : "gzip", + + CopyToOutputDirectory = "Never", + + CopyToPublishDirectory = "PreserveNewest", + + OriginalItemSpec = Path.Combine("wwwroot", name), + + Integrity = "integrity", + + Fingerprint = "fingerprint", + + LastWriteTime = new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero), + + FileLength = 10, + + }; + + + + return result; + + } + + + + private static StaticWebAsset CreateAsset( + + string itemSpec, + + string relativePath, + + string assetKind, + + string assetMode, + + string sourceId = "MyProject", + + string sourceType = "Computed") + + { + + var result = new StaticWebAsset + + { + + Identity = Path.GetFullPath(itemSpec), + + SourceId = sourceId, + + SourceType = sourceType, + + ContentRoot = Directory.GetCurrentDirectory(), + + BasePath = "base", + + RelativePath = relativePath, + + AssetKind = assetKind, + + AssetMode = assetMode, + + AssetRole = "Primary", + + AssetMergeBehavior = StaticWebAsset.MergeBehaviors.PreferTarget, + + AssetMergeSource = "", + + RelatedAsset = "", + + AssetTraitName = "", + + AssetTraitValue = "", + + CopyToOutputDirectory = "Never", + + CopyToPublishDirectory = "PreserveNewest", + + OriginalItemSpec = itemSpec, + + Integrity = "integrity", + + Fingerprint = "fingerprint", + + LastWriteTime = new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero), + + FileLength = 10, + + }; + + + + result.ApplyDefaults(); + + result.Normalize(); + + + + return result; + + } + + } + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileMultiThreadingTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileMultiThreadingTest.cs index 3cf25d0246a0..71142c94aff7 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileMultiThreadingTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileMultiThreadingTest.cs @@ -1,60 +1,182 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Moq; + + + + namespace Microsoft.AspNetCore.Razor.Tasks; + + + + // Test parallelization is disabled assembly-wide via + + // [assembly:CollectionBehavior(DisableTestParallelization = true)] in + + // LegacyStaticWebAssetsV1IntegrationTest.cs, which already isolates the + + // process-CWD mutation this test performs. + + +[TestClass] + public class StaticWebAssetsGeneratePackagePropsFileMultiThreadingTest + + { - [Fact] + + + [TestMethod] + + public void WritesPropsFileRelativeToTaskEnvironmentProjectDirectory() + + { + + var testRoot = Path.Combine(AppContext.BaseDirectory, nameof(StaticWebAssetsGeneratePackagePropsFileMultiThreadingTest), Guid.NewGuid().ToString("N")); + + var projectDir = Path.Combine(testRoot, "project"); + + var spawnDir = Path.Combine(testRoot, "spawn"); + + var projectOutputDir = Path.Combine(projectDir, "output"); + + var spawnOutputDir = Path.Combine(spawnDir, "output"); + + Directory.CreateDirectory(projectOutputDir); + + Directory.CreateDirectory(spawnOutputDir); + + + + var currentDirectory = Directory.GetCurrentDirectory(); + + try + + { + + Directory.SetCurrentDirectory(spawnDir); + + + + var buildEngine = new Mock(); + + var task = new StaticWebAssetsGeneratePackagePropsFile + + { + + BuildEngine = buildEngine.Object, + + TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir), + + PropsFileImport = "Microsoft.AspNetCore.StaticWebAssets.props", + + BuildTargetPath = Path.Combine("output", "props.xml") + + }; + + + + task.Execute().Should().BeTrue(); + + + + var expectedPath = Path.Combine(projectDir, "output", "props.xml"); + + File.Exists(expectedPath).Should().BeTrue("the file should be written under the project dir, not the process CWD"); + + + + var incorrectPath = Path.Combine(spawnDir, "output", "props.xml"); + + File.Exists(incorrectPath).Should().BeFalse(); + + } + + finally + + { + + Directory.SetCurrentDirectory(currentDirectory); + + if (Directory.Exists(testRoot)) + + { + + Directory.Delete(testRoot, recursive: true); + + } + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileTest.cs index 15738d2dac43..732ae39cbc09 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileTest.cs @@ -1,49 +1,149 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.StaticWebAssets.Tasks; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + using Microsoft.Build.Framework; + + using Moq; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + + namespace Microsoft.AspNetCore.Razor.Tasks + + { + + + [TestClass] + public class StaticWebAssetsGeneratePackagePropsFileTest + + { - [Fact] + + + [TestMethod] + + public void WritesPropsFile_WithProvidedImportPath() + + { + + // Arrange + + var file = Path.GetTempFileName(); + + var expectedDocument = @" + + + + "; + + + + try + + { + + var buildEngine = new Mock(); + + + + var task = new StaticWebAssetsGeneratePackagePropsFile + + { + + BuildEngine = buildEngine.Object, + + PropsFileImport = "Microsoft.AspNetCore.StaticWebAssets.props", + + BuildTargetPath = file + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + var document = File.ReadAllText(file); + + document.Should().Contain(expectedDocument); + + } + + finally + + { + + if (File.Exists(file)) + + { + + File.Delete(file); + + } + + } + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateExternallyDefinedStaticWebAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateExternallyDefinedStaticWebAssetsTest.cs index 1c64f37d3d00..b01a07c4bd22 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateExternallyDefinedStaticWebAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateExternallyDefinedStaticWebAssetsTest.cs @@ -1,528 +1,1586 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + +[TestClass] + public class UpdateExternallyDefinedStaticWebAssetsTest + + { - [Fact] + + + [TestMethod] + + public void Execute_UpdatesAssetsWithoutFingerprint() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + Directory.CreateDirectory(Path.Combine(AppContext.BaseDirectory, "dist", "assets")); + + File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"), "body { color: red; }"); + + File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), ""); + + var assets = new ITaskItem [] { + + new TaskItem( + + Path.Combine(AppContext.BaseDirectory, @"dist\assets\index-C5tBAdQX.css"), + + new Dictionary + + { + + ["RelativePath"] = "assets/index-C5tBAdQX.css", + + ["BasePath"] = "", + + ["AssetMode"] = "All", + + ["AssetKind"] = "Publish", + + ["SourceId"] = "MyProject", + + ["CopyToOutputDirectory"] = "PreserveNewest", + + ["RelatedAsset"] = "", + + ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "dist"), + + ["SourceType"] = "Discovered", + + ["AssetRole"] = "Primary", + + ["AssetTraitValue"] = "", + + ["AssetTraitName"] = "", + + ["OriginalItemSpec"] = Path.Combine(AppContext.BaseDirectory, @"dist\assets\index-C5tBAdQX.css"), + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }), + + new TaskItem( + + Path.Combine(AppContext.BaseDirectory, @"dist\index.html"), + + new Dictionary + + { + + ["RelativePath"] = "index.html", + + ["BasePath"] = "", + + ["AssetMode"] = "All", + + ["AssetKind"] = "Publish", + + ["SourceId"] = "MyProject", + + ["CopyToOutputDirectory"] = "PreserveNewest", + + ["RelatedAsset"] = "", + + ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "dist"), + + ["SourceType"] = "Discovered", + + ["AssetRole"] = "Primary", + + ["AssetTraitValue"] = "", + + ["AssetTraitName"] = "", + + ["OriginalItemSpec"] = Path.Combine(AppContext.BaseDirectory, @"dist\index.html"), + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }) + + }; + + + + var task = new UpdateExternallyDefinedStaticWebAssets + + { + + Assets = assets, + + Endpoints = [], + + BuildEngine = buildEngine.Object + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + + + task.UpdatedAssets.Should().HaveCount(2); + + task.AssetsWithoutEndpoints.Should().HaveCount(2); + + task.UpdatedAssets[0].GetMetadata("Fingerprint").Should().NotBeNullOrEmpty(); + + task.UpdatedAssets[1].GetMetadata("Fingerprint").Should().NotBeNullOrEmpty(); + + task.UpdatedAssets[0].GetMetadata("Integrity").Should().NotBeNullOrEmpty(); + + task.UpdatedAssets[1].GetMetadata("Integrity").Should().NotBeNullOrEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_DoesNotAddAssets_WithEndpointsTo_AssetsWithoutEndpoints() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + Directory.CreateDirectory(Path.Combine(AppContext.BaseDirectory, "dist", "assets")); + + File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"), "body { color: red; }"); + + File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), ""); + + var assets = new ITaskItem[] { + + new TaskItem( + + Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"), + + new Dictionary + + { + + ["RelativePath"] = "assets/index-C5tBAdQX.css", + + ["BasePath"] = "", + + ["AssetMode"] = "All", + + ["AssetKind"] = "Publish", + + ["SourceId"] = "MyProject", + + ["CopyToOutputDirectory"] = "PreserveNewest", + + ["RelatedAsset"] = "", + + ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "dist"), + + ["SourceType"] = "Discovered", + + ["AssetRole"] = "Primary", + + ["AssetTraitValue"] = "", + + ["AssetTraitName"] = "", + + ["OriginalItemSpec"] = Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"), + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }), + + new TaskItem( + + Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), + + new Dictionary + + { + + ["RelativePath"] = "index.html", + + ["BasePath"] = "", + + ["AssetMode"] = "All", + + ["AssetKind"] = "Publish", + + ["SourceId"] = "MyProject", + + ["CopyToOutputDirectory"] = "PreserveNewest", + + ["RelatedAsset"] = "", + + ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "dist"), + + ["SourceType"] = "Discovered", + + ["AssetRole"] = "Primary", + + ["AssetTraitValue"] = "", + + ["AssetTraitName"] = "", + + ["OriginalItemSpec"] = Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }) + + }; + + + + var task = new UpdateExternallyDefinedStaticWebAssets + + { + + Assets = assets, + + Endpoints = [ + + new TaskItem( + + "index.html", + + new Dictionary + + { + + ["Route"] = "/index.html", + + ["AssetFile"] = Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), + + ["Selectors"] = "[]", + + ["ResponseHeaders"] = "[]", + + ["EndpointProperties"] = "[]" + + }) + + ], + + BuildEngine = buildEngine.Object + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + + + task.UpdatedAssets.Should().HaveCount(2); + + task.AssetsWithoutEndpoints.Should().HaveCount(1); + + task.AssetsWithoutEndpoints[0].ItemSpec.Should().Be(Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css")); + + task.UpdatedAssets[0].GetMetadata("Fingerprint").Should().NotBeNullOrEmpty(); + + task.UpdatedAssets[1].GetMetadata("Fingerprint").Should().NotBeNullOrEmpty(); + + task.UpdatedAssets[0].GetMetadata("Integrity").Should().NotBeNullOrEmpty(); + + task.UpdatedAssets[1].GetMetadata("Integrity").Should().NotBeNullOrEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_InfersFingerprint_ForMatchingAssets() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + Directory.CreateDirectory(Path.Combine(AppContext.BaseDirectory, "dist", "assets")); + + File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"), "body { color: red; }"); + + File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), ""); + + var assets = new ITaskItem[] { + + new TaskItem( + + Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"), + + new Dictionary + + { + + ["RelativePath"] = "assets/index-C5tBAdQX.css", + + ["BasePath"] = "", + + ["AssetMode"] = "All", + + ["AssetKind"] = "Publish", + + ["SourceId"] = "MyProject", + + ["CopyToOutputDirectory"] = "PreserveNewest", + + ["RelatedAsset"] = "", + + ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "dist"), + + ["SourceType"] = "Discovered", + + ["AssetRole"] = "Primary", + + ["AssetTraitValue"] = "", + + ["AssetTraitName"] = "", + + ["OriginalItemSpec"] = Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"), + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }), + + new TaskItem( + + Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), + + new Dictionary + + { + + ["RelativePath"] = "index.html", + + ["BasePath"] = "", + + ["AssetMode"] = "All", + + ["AssetKind"] = "Publish", + + ["SourceId"] = "MyProject", + + ["CopyToOutputDirectory"] = "PreserveNewest", + + ["RelatedAsset"] = "", + + ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "dist"), + + ["SourceType"] = "Discovered", + + ["AssetRole"] = "Primary", + + ["AssetTraitValue"] = "", + + ["AssetTraitName"] = "", + + ["OriginalItemSpec"] = Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }) + + }; + + + + var fingerprintExpressions = new TaskItem[] + + { + + new TaskItem( + + "React", + + new Dictionary + + { + + ["Pattern"] = "assets/.*-(?.+)\\..*", + + }) + + }; + + + + var task = new UpdateExternallyDefinedStaticWebAssets + + { + + FingerprintInferenceExpressions = fingerprintExpressions, + + Assets = assets, + + Endpoints = [], + + BuildEngine = buildEngine.Object + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.UpdatedAssets.Should().HaveCount(2); + + task.UpdatedAssets[0].GetMetadata("Fingerprint").Should().Be("C5tBAdQX"); + + task.UpdatedAssets[0].GetMetadata("RelativePath").Should().Be("assets/index-#[{fingerprint}].css"); + + task.UpdatedAssets[1].GetMetadata("Fingerprint").Should().NotBeNullOrEmpty(); + + task.UpdatedAssets[0].GetMetadata("Integrity").Should().NotBeNullOrEmpty(); + + task.UpdatedAssets[1].GetMetadata("Integrity").Should().NotBeNullOrEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_MaterializesFrameworkAssetsFromP2PReferences() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var intermediateDir = Path.Combine(AppContext.BaseDirectory, "obj", "fxtest"); + + var sourceDir = Path.Combine(AppContext.BaseDirectory, "fxsource"); + + Directory.CreateDirectory(sourceDir); + + var sourceFile = Path.Combine(sourceDir, "framework.js"); + + File.WriteAllText(sourceFile, "// framework"); + + + + var asset = new TaskItem( + + sourceFile, + + new Dictionary + + { + + ["RelativePath"] = "framework.js", + + ["BasePath"] = "_content/SourceLib", + + ["AssetMode"] = "All", + + ["AssetKind"] = "Build", + + ["SourceId"] = "SourceLib", + + ["CopyToOutputDirectory"] = "PreserveNewest", + + ["RelatedAsset"] = "", + + ["ContentRoot"] = sourceDir + Path.DirectorySeparatorChar, + + ["SourceType"] = "Framework", + + ["AssetRole"] = "Primary", + + ["AssetTraitValue"] = "", + + ["AssetTraitName"] = "", + + ["OriginalItemSpec"] = sourceFile, + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }); + + + + var task = new UpdateExternallyDefinedStaticWebAssets + + { + + Assets = new[] { asset }, + + Endpoints = [], + + BuildEngine = buildEngine.Object, + + IntermediateOutputPath = intermediateDir, + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/ConsumerApp" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.UpdatedAssets.Should().HaveCount(1); + + var materialized = task.UpdatedAssets[0]; + + materialized.GetMetadata("SourceType").Should().Be("Discovered"); + + materialized.GetMetadata("SourceId").Should().Be("ConsumerApp"); + + materialized.GetMetadata("BasePath").Should().Be("_content/ConsumerApp"); + + materialized.GetMetadata("AssetMode").Should().Be("CurrentProject"); + + materialized.ItemSpec.Should().Contain(Path.Combine("fx", "SourceLib")); + + File.Exists(materialized.ItemSpec).Should().BeTrue(); + + + + task.OriginalFrameworkAssets.Should().HaveCount(1); + + task.OriginalFrameworkAssets[0].GetMetadata("SourceType").Should().Be("Framework"); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_RemapsEndpointRoutesForMaterializedFrameworkAssets() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var intermediateDir = Path.Combine(AppContext.BaseDirectory, "obj", "fxroute"); + + var sourceDir = Path.Combine(AppContext.BaseDirectory, "fxroutesource"); + + Directory.CreateDirectory(sourceDir); + + var sourceFile = Path.Combine(sourceDir, "framework.js"); + + File.WriteAllText(sourceFile, "// framework route test"); + + + + var asset = new TaskItem( + + sourceFile, + + new Dictionary + + { + + ["RelativePath"] = "js/framework.js", + + ["BasePath"] = "_content/SourceLib", + + ["AssetMode"] = "All", + + ["AssetKind"] = "Build", + + ["SourceId"] = "SourceLib", + + ["CopyToOutputDirectory"] = "PreserveNewest", + + ["RelatedAsset"] = "", + + ["ContentRoot"] = sourceDir + Path.DirectorySeparatorChar, + + ["SourceType"] = "Framework", + + ["AssetRole"] = "Primary", + + ["AssetTraitValue"] = "", + + ["AssetTraitName"] = "", + + ["OriginalItemSpec"] = sourceFile, + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }); + + + + // Endpoint with the old base path baked into the route. + + var endpoint = new TaskItem( + + "_content/SourceLib/js/framework.js", + + new Dictionary + + { + + ["Route"] = "_content/SourceLib/js/framework.js", + + ["AssetFile"] = sourceFile, + + ["Selectors"] = "[]", + + ["ResponseHeaders"] = "[]", + + ["EndpointProperties"] = """[{"Name":"label","Value":"_content/SourceLib/js/framework.js"}]""" + + }); + + + + var task = new UpdateExternallyDefinedStaticWebAssets + + { + + Assets = [asset], + + Endpoints = [endpoint], + + BuildEngine = buildEngine.Object, + + IntermediateOutputPath = intermediateDir, + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "/" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.UpdatedEndpoints.Should().HaveCount(1); + + var updatedEndpoint = task.UpdatedEndpoints[0]; + + + + // Route should have old base path stripped and new base path applied. + + // "/" base path means just the relative path remains. + + updatedEndpoint.ItemSpec.Should().Be("js/framework.js", + + "endpoint route should have old base path '_content/SourceLib' stripped"); + + + + // AssetFile should point to the materialized path. + + updatedEndpoint.GetMetadata("AssetFile").Should().Contain(Path.Combine("fx", "SourceLib")); + + + + // Label endpoint property should also be remapped. + + var endpointProperties = updatedEndpoint.GetMetadata("EndpointProperties"); + + endpointProperties.Should().Contain("js/framework.js"); + + endpointProperties.Should().NotContain("_content/SourceLib"); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_RemapsEndpointRoutesToConsumerBasePath() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var intermediateDir = Path.Combine(AppContext.BaseDirectory, "obj", "fxroutelib"); + + var sourceDir = Path.Combine(AppContext.BaseDirectory, "fxroutelibsource"); + + Directory.CreateDirectory(sourceDir); + + var sourceFile = Path.Combine(sourceDir, "lib.js"); + + File.WriteAllText(sourceFile, "// lib route test"); + + + + var asset = new TaskItem( + + sourceFile, + + new Dictionary + + { + + ["RelativePath"] = "js/lib.js", + + ["BasePath"] = "_content/SourceLib", + + ["AssetMode"] = "All", + + ["AssetKind"] = "Build", + + ["SourceId"] = "SourceLib", + + ["CopyToOutputDirectory"] = "PreserveNewest", + + ["RelatedAsset"] = "", + + ["ContentRoot"] = sourceDir + Path.DirectorySeparatorChar, + + ["SourceType"] = "Framework", + + ["AssetRole"] = "Primary", + + ["AssetTraitValue"] = "", + + ["AssetTraitName"] = "", + + ["OriginalItemSpec"] = sourceFile, + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }); + + + + var endpoint = new TaskItem( + + "_content/SourceLib/js/lib.js", + + new Dictionary + + { + + ["Route"] = "_content/SourceLib/js/lib.js", + + ["AssetFile"] = sourceFile, + + ["Selectors"] = "[]", + + ["ResponseHeaders"] = "[]", + + ["EndpointProperties"] = """[{"Name":"label","Value":"_content/SourceLib/js/lib.js"}]""" + + }); + + + + var task = new UpdateExternallyDefinedStaticWebAssets + + { + + Assets = [asset], + + Endpoints = [endpoint], + + BuildEngine = buildEngine.Object, + + IntermediateOutputPath = intermediateDir, + + ProjectPackageId = "ConsumerLib", + + ProjectBasePath = "_content/ConsumerLib" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.UpdatedEndpoints.Should().HaveCount(1); + + var updatedEndpoint = task.UpdatedEndpoints[0]; + + + + // Route should have old base path replaced with consumer's base path. + + updatedEndpoint.ItemSpec.Should().Be("_content/ConsumerLib/js/lib.js", + + "endpoint route should use consumer's base path"); + + + + // Label should also reflect the new base path. + + var endpointProperties = updatedEndpoint.GetMetadata("EndpointProperties"); + + endpointProperties.Should().Contain("_content/ConsumerLib/js/lib.js"); + + endpointProperties.Should().NotContain("_content/SourceLib"); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_PassesThroughNonFrameworkAssetsUnchanged() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var sourceDir = Path.Combine(AppContext.BaseDirectory, "normalsource"); + + Directory.CreateDirectory(sourceDir); + + var sourceFile = Path.Combine(sourceDir, "app.js"); + + File.WriteAllText(sourceFile, "// app"); + + + + var asset = new TaskItem( + + sourceFile, + + new Dictionary + + { + + ["RelativePath"] = "app.js", + + ["BasePath"] = "", + + ["AssetMode"] = "All", + + ["AssetKind"] = "Build", + + ["SourceId"] = "OtherLib", + + ["CopyToOutputDirectory"] = "PreserveNewest", + + ["RelatedAsset"] = "", + + ["ContentRoot"] = sourceDir + Path.DirectorySeparatorChar, + + ["SourceType"] = "Discovered", + + ["AssetRole"] = "Primary", + + ["AssetTraitValue"] = "", + + ["AssetTraitName"] = "", + + ["OriginalItemSpec"] = sourceFile, + + ["CopyToPublishDirectory"] = "PreserveNewest" + + }); + + + + var task = new UpdateExternallyDefinedStaticWebAssets + + { + + Assets = new[] { asset }, + + Endpoints = [], + + BuildEngine = buildEngine.Object, + + IntermediateOutputPath = Path.Combine(AppContext.BaseDirectory, "obj", "normal"), + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/ConsumerApp" + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.UpdatedAssets.Should().HaveCount(1); + + task.UpdatedAssets[0].GetMetadata("SourceType").Should().Be("Discovered"); + + task.UpdatedAssets[0].GetMetadata("SourceId").Should().Be("OtherLib"); + + task.OriginalFrameworkAssets.Should().BeEmpty(); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdatePackageStaticWebAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdatePackageStaticWebAssetsTest.cs index e16ab07c55af..b0e4cc5c449c 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdatePackageStaticWebAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdatePackageStaticWebAssetsTest.cs @@ -1,715 +1,2147 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + +[TestClass] + public class UpdatePackageStaticWebAssetsTest : IDisposable + + { + + private readonly string _tempDir; + + private readonly Mock _buildEngine; + + private readonly List _errorMessages; + + private readonly List _logMessages; + + + + public UpdatePackageStaticWebAssetsTest() + + { + + _tempDir = Path.Combine(Path.GetTempPath(), "UpdatePackageSWA_" + Guid.NewGuid().ToString("N")); + + Directory.CreateDirectory(_tempDir); + + + + _errorMessages = new List(); + + _logMessages = new List(); + + _buildEngine = new Mock(); + + _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => _errorMessages.Add(args.Message)); + + _buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) + + .Callback(args => _logMessages.Add(args.Message)); + + } + + + + public void Dispose() + + { + + if (Directory.Exists(_tempDir)) + + { + + try { Directory.Delete(_tempDir, recursive: true); } catch { } + + } + + } - [Fact] + + + + + [TestMethod] + + public void Execute_PackageAssets_ArePassedThrough() + + { + + // Arrange + + var sourceFile = CreateTempFile("pkg", "content.js", "console.log('pkg');"); + + var asset = CreatePackageAsset(sourceFile, "MyLib", "_content/mylib", "content.js"); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + IntermediateOutputPath = Path.Combine(_tempDir, "obj"), + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.UpdatedAssets.Should().HaveCount(1); + + task.OriginalAssets.Should().HaveCount(1); + + task.UpdatedAssets[0].GetMetadata("SourceType").Should().Be("Package"); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_FrameworkAssets_AreMaterialized() + + { + + // Arrange + + var sourceFile = CreateTempFile("source", "js", "framework.js", "console.log('framework');"); + + var asset = CreateFrameworkAsset(sourceFile, "FrameworkLib", "_content/frameworklib", "js/framework.js"); + + var intermediateOutput = Path.Combine(_tempDir, "obj"); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + IntermediateOutputPath = intermediateOutput, + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.UpdatedAssets.Should().HaveCount(1); + + task.OriginalAssets.Should().HaveCount(1); + + + + var updated = task.UpdatedAssets[0]; + + + + // The materialized file should exist in the fx directory + + var expectedDir = Path.Combine(intermediateOutput, "fx", "FrameworkLib"); + + var expectedPath = Path.GetFullPath(Path.Combine(expectedDir, "js", "framework.js")); + + updated.ItemSpec.Should().Be(expectedPath); + + File.Exists(expectedPath).Should().BeTrue(); + + File.ReadAllText(expectedPath).Should().Be("console.log('framework');"); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_FrameworkAssets_SourceTypeChangedToDiscovered() + + { + + // Arrange + + var sourceFile = CreateTempFile("source", "framework.js", "content"); + + var asset = CreateFrameworkAsset(sourceFile, "FrameworkLib", "_content/frameworklib", "framework.js"); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + IntermediateOutputPath = Path.Combine(_tempDir, "obj"), + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var updated = task.UpdatedAssets[0]; + + updated.GetMetadata("SourceType").Should().Be("Discovered"); + + updated.GetMetadata("SourceId").Should().Be("ConsumerApp"); + + updated.GetMetadata("BasePath").Should().Be("_content/consumerapp"); + + updated.GetMetadata("AssetMode").Should().Be("CurrentProject"); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_FrameworkAssets_ContentRootPointsToFxDirectory() + + { + + // Arrange + + var sourceFile = CreateTempFile("source", "framework.js", "content"); + + var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); + + var intermediateOutput = Path.Combine(_tempDir, "obj"); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + IntermediateOutputPath = intermediateOutput, + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var updated = task.UpdatedAssets[0]; + + var expectedContentRoot = Path.Combine(intermediateOutput, "fx", "FxLib") + Path.DirectorySeparatorChar; + + updated.GetMetadata("ContentRoot").Should().Be(expectedContentRoot); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_FrameworkAssets_MissingSourceFile_LogsError() + + { + + // Arrange + + var nonExistentFile = Path.Combine(_tempDir, "does_not_exist.js"); + + var asset = CreateFrameworkAsset(nonExistentFile, "FxLib", "_content/fxlib", "framework.js"); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + IntermediateOutputPath = Path.Combine(_tempDir, "obj"), + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeFalse(); + + _errorMessages.Should().ContainSingle(e => e.Contains("does not exist") && e.Contains("does_not_exist.js")); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_MixedAssets_ProcessesBothTypes() + + { + + // Arrange + + var pkgFile = CreateTempFile("pkg", "package.js", "console.log('pkg');"); + + var fxFile = CreateTempFile("source", "framework.js", "console.log('fx');"); + + + + var pkgAsset = CreatePackageAsset(pkgFile, "MyLib", "_content/mylib", "package.js"); + + var fxAsset = CreateFrameworkAsset(fxFile, "MyLib", "_content/mylib", "framework.js"); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { pkgAsset, fxAsset }, + + IntermediateOutputPath = Path.Combine(_tempDir, "obj"), + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.UpdatedAssets.Should().HaveCount(2); + + task.OriginalAssets.Should().HaveCount(2); + + + + // Package asset stays as Package + + task.UpdatedAssets[0].GetMetadata("SourceType").Should().Be("Package"); + + // Framework asset is converted to Discovered + + task.UpdatedAssets[1].GetMetadata("SourceType").Should().Be("Discovered"); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_FrameworkAssets_PreservesOriginalFingerprintAndIntegrity() + + { + + // Arrange + + var sourceFile = CreateTempFile("source", "framework.js", "content"); + + var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); + + + + // Get the fingerprint/integrity that were computed by FromV1TaskItem in CreateFrameworkAsset + + var originalFingerprint = asset.GetMetadata("Fingerprint"); + + var originalIntegrity = asset.GetMetadata("Integrity"); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + IntermediateOutputPath = Path.Combine(_tempDir, "obj"), + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var updated = task.UpdatedAssets[0]; + + // Fingerprint and integrity should be preserved from the original file + + updated.GetMetadata("Fingerprint").Should().Be(originalFingerprint); + + updated.GetMetadata("Integrity").Should().Be(originalIntegrity); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_FrameworkAssets_IncrementalSkipsCopy_WhenUpToDate() + + { + + // Arrange + + var sourceFile = CreateTempFile("source", "framework.js", "content"); + + var intermediateOutput = Path.Combine(_tempDir, "obj"); + + var fxDir = Path.Combine(intermediateOutput, "fx", "FxLib"); + + var destPath = Path.Combine(fxDir, "framework.js"); + + + + // Pre-create the destination so it's already up-to-date + + Directory.CreateDirectory(fxDir); + + File.Copy(sourceFile, destPath); + + // Make the dest file newer than the source + + File.SetLastWriteTimeUtc(destPath, DateTime.UtcNow.AddMinutes(1)); + + + + var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + IntermediateOutputPath = intermediateOutput, + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.UpdatedAssets.Should().HaveCount(1); + + // Should log the "already up to date" message + + _logMessages.Should().Contain(m => m.Contains("already up to date")); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_FrameworkAssets_OverwritesStaleDestination() + + { + + // Arrange + + var sourceFile = CreateTempFile("source", "framework.js", "new content"); + + var intermediateOutput = Path.Combine(_tempDir, "obj"); + + var fxDir = Path.Combine(intermediateOutput, "fx", "FxLib"); + + var destPath = Path.Combine(fxDir, "framework.js"); + + + + // Pre-create the destination with old content and older timestamp + + Directory.CreateDirectory(fxDir); + + File.WriteAllText(destPath, "old content"); + + File.SetLastWriteTimeUtc(destPath, DateTime.UtcNow.AddMinutes(-10)); + + File.SetLastWriteTimeUtc(sourceFile, DateTime.UtcNow); + + + + var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + IntermediateOutputPath = intermediateOutput, + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + File.ReadAllText(destPath).Should().Be("new content"); + + _logMessages.Should().Contain(m => m.Contains("Materialized framework asset")); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_NoFrameworkAssets_EndpointsNotRemapped() + + { + + // Arrange + + var sourceFile = CreateTempFile("pkg", "content.js", "console.log('pkg');"); + + var pkgAsset = CreatePackageAsset(sourceFile, "MyLib", "_content/mylib", "content.js"); + + + + var endpoint = new TaskItem("content.js", new Dictionary + + { + + ["Route"] = "/content.js", + + ["AssetFile"] = sourceFile, + + ["Selectors"] = "[]", + + ["ResponseHeaders"] = "[]", + + ["EndpointProperties"] = "[]", + + }); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { pkgAsset }, + + Endpoints = new ITaskItem[] { endpoint }, + + IntermediateOutputPath = Path.Combine(_tempDir, "obj"), + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + // No framework assets => no remapping done + + task.RemappedEndpoints.Should().BeNullOrEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_FrameworkAssets_EndpointsAreRemapped() + + { + + // Arrange + + var sourceFile = CreateTempFile("source", "framework.js", "content"); + + var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); + + var intermediateOutput = Path.Combine(_tempDir, "obj"); + + + + var endpoint = new TaskItem("framework.js", new Dictionary + + { + + ["Route"] = "/_content/fxlib/framework.js", + + ["AssetFile"] = sourceFile, + + ["Selectors"] = "[]", + + ["ResponseHeaders"] = "[]", + + ["EndpointProperties"] = "[]", + + }); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + Endpoints = new ITaskItem[] { endpoint }, + + IntermediateOutputPath = intermediateOutput, + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.RemappedEndpoints.Should().HaveCount(1); + + + + var remapped = task.RemappedEndpoints[0]; + + var expectedPath = Path.GetFullPath(Path.Combine(intermediateOutput, "fx", "FxLib", "framework.js")); + + remapped.GetMetadata("AssetFile").Should().Be(expectedPath); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_MultipleEndpoints_SameIdentity_AllRemapped() + + { + + // Arrange — two endpoints share the same Identity (e.g. same route, different selectors) + + var sourceFile = CreateTempFile("source", "framework.js", "content"); + + var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); + + var intermediateOutput = Path.Combine(_tempDir, "obj"); + + + + var endpoint1 = new TaskItem("framework.js", new Dictionary + + { + + ["Route"] = "/_content/fxlib/framework.js", + + ["AssetFile"] = sourceFile, + + ["Selectors"] = "[{\"Name\":\"Content-Encoding\",\"Value\":\"gzip\"}]", + + ["ResponseHeaders"] = "[]", + + ["EndpointProperties"] = "[]", + + }); + + + + var endpoint2 = new TaskItem("framework.js", new Dictionary + + { + + ["Route"] = "/_content/fxlib/framework.js", + + ["AssetFile"] = sourceFile, + + ["Selectors"] = "[]", + + ["ResponseHeaders"] = "[]", + + ["EndpointProperties"] = "[]", + + }); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + Endpoints = new ITaskItem[] { endpoint1, endpoint2 }, + + IntermediateOutputPath = intermediateOutput, + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.RemappedEndpoints.Should().HaveCount(2); + + + + var expectedPath = Path.GetFullPath(Path.Combine(intermediateOutput, "fx", "FxLib", "framework.js")); + + task.RemappedEndpoints[0].GetMetadata("AssetFile").Should().Be(expectedPath); + + task.RemappedEndpoints[1].GetMetadata("AssetFile").Should().Be(expectedPath); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_EndpointsNotMatchingFramework_AreNotRemapped() + + { + + // Arrange — endpoint pointing to a file that is NOT a framework asset + + var fxFile = CreateTempFile("source", "framework.js", "fx"); + + var pkgFile = CreateTempFile("pkg", "package.js", "pkg"); + + var fxAsset = CreateFrameworkAsset(fxFile, "FxLib", "_content/fxlib", "framework.js"); + + var intermediateOutput = Path.Combine(_tempDir, "obj"); + + + + var fxEndpoint = new TaskItem("framework.js", new Dictionary + + { + + ["Route"] = "/_content/fxlib/framework.js", + + ["AssetFile"] = fxFile, + + ["Selectors"] = "[]", + + ["ResponseHeaders"] = "[]", + + ["EndpointProperties"] = "[]", + + }); + + + + var pkgEndpoint = new TaskItem("package.js", new Dictionary + + { + + ["Route"] = "/_content/fxlib/package.js", + + ["AssetFile"] = pkgFile, + + ["Selectors"] = "[]", + + ["ResponseHeaders"] = "[]", + + ["EndpointProperties"] = "[]", + + }); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { fxAsset }, + + Endpoints = new ITaskItem[] { fxEndpoint, pkgEndpoint }, + + IntermediateOutputPath = intermediateOutput, + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + // Only the framework endpoint should be remapped + + task.RemappedEndpoints.Should().HaveCount(1); + + task.RemappedEndpoints[0].ItemSpec.Should().Be("framework.js"); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_NullEndpoints_DoesNotRemapAndSucceeds() + + { + + // Arrange + + var sourceFile = CreateTempFile("source", "framework.js", "content"); + + var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + Endpoints = null, + + IntermediateOutputPath = Path.Combine(_tempDir, "obj"), + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.UpdatedAssets.Should().HaveCount(1); + + task.RemappedEndpoints.Should().BeNullOrEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_EmptyAssetsArray_Succeeds() + + { + + // Arrange + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = Array.Empty(), + + IntermediateOutputPath = Path.Combine(_tempDir, "obj"), + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.UpdatedAssets.Should().BeEmpty(); + + task.OriginalAssets.Should().BeEmpty(); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_FrameworkAssets_SubdirectoriesArePreserved() + + { + + // Arrange + + var sourceFile = CreateTempFile("source", "lib", "deep", "nested", "component.js", "content"); + + var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "lib/deep/nested/component.js"); + + var intermediateOutput = Path.Combine(_tempDir, "obj"); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = new[] { asset }, + + IntermediateOutputPath = intermediateOutput, + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "_content/consumerapp", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + var expectedPath = Path.GetFullPath(Path.Combine(intermediateOutput, "fx", "FxLib", "lib", "deep", "nested", "component.js")); + + task.UpdatedAssets[0].ItemSpec.Should().Be(expectedPath); + + File.Exists(expectedPath).Should().BeTrue(); + + } + + + + // Helpers + + + + private string CreateTempFile(params string[] pathParts) + + { + + // Last part is the content, everything before is path segments + + var content = pathParts[^1]; + + var segments = pathParts[..^1]; + + + + var dir = Path.Combine(new[] { _tempDir }.Concat(segments[..^1]).ToArray()); + + Directory.CreateDirectory(dir); + + var filePath = Path.Combine(dir, segments[^1]); + + File.WriteAllText(filePath, content); + + return filePath; + + } + + + + private ITaskItem CreatePackageAsset(string filePath, string sourceId, string basePath, string relativePath) + + { + + var contentRoot = Path.GetDirectoryName(filePath) + Path.DirectorySeparatorChar; + + return new TaskItem(filePath, new Dictionary + + { + + ["SourceType"] = "Package", + + ["SourceId"] = sourceId, + + ["ContentRoot"] = contentRoot, + + ["BasePath"] = basePath, + + ["RelativePath"] = relativePath, + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["OriginalItemSpec"] = filePath, + + ["Fingerprint"] = "test-fingerprint", + + ["Integrity"] = "test-integrity", + + ["FileLength"] = "10", + + ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat), + + }); + + } + + + + private ITaskItem CreateFrameworkAsset(string filePath, string sourceId, string basePath, string relativePath) + + { + + var contentRoot = Path.GetDirectoryName(filePath) + Path.DirectorySeparatorChar; + + return new TaskItem(filePath, new Dictionary + + { + + ["SourceType"] = "Framework", + + ["SourceId"] = sourceId, + + ["ContentRoot"] = contentRoot, + + ["BasePath"] = basePath, + + ["RelativePath"] = relativePath, + + ["AssetKind"] = "All", + + ["AssetMode"] = "All", + + ["AssetRole"] = "Primary", + + ["RelatedAsset"] = "", + + ["AssetTraitName"] = "", + + ["AssetTraitValue"] = "", + + ["CopyToOutputDirectory"] = "Never", + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["OriginalItemSpec"] = filePath, + + ["Fingerprint"] = "test-fingerprint", + + ["Integrity"] = "test-integrity", + + ["FileLength"] = "10", + + ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat), + + }); + + } + + + + private ITaskItem CreateEndpoint(string route, string assetFile, string label = null) + + { + + var properties = label != null + + ? $$"""[{"Name":"label","Value":"{{label}}"}]""" + + : "[]"; + + + + return new TaskItem(route, new Dictionary + + { + + ["Route"] = route, + + ["AssetFile"] = assetFile, + + ["Selectors"] = "[]", + + ["ResponseHeaders"] = "[]", + + ["EndpointProperties"] = properties, + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_FrameworkAssets_RemapsEndpointRoutes_StripOldBasePath() + + { + + // Arrange + + var sourceFile = CreateTempFile("source", "framework.js", "content"); + + var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/FxLib", "js/framework.js"); + + var endpoint = CreateEndpoint("_content/FxLib/js/framework.js", sourceFile, "_content/FxLib/js/framework.js"); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = [asset], + + Endpoints = [endpoint], + + IntermediateOutputPath = Path.Combine(_tempDir, "obj"), + + ProjectPackageId = "ConsumerApp", + + ProjectBasePath = "/", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.RemappedEndpoints.Should().HaveCount(1); + + var remapped = task.RemappedEndpoints[0]; + + + + // Route should have old base path stripped; "/" means just relative path. + + remapped.ItemSpec.Should().Be("js/framework.js"); + + remapped.GetMetadata("AssetFile").Should().Contain(Path.Combine("fx", "FxLib")); + + + + // Label should also be remapped. + + remapped.GetMetadata("EndpointProperties").Should().Contain("js/framework.js"); + + remapped.GetMetadata("EndpointProperties").Should().NotContain("_content/FxLib"); + + + + // Original endpoints should be output for removal. + + task.OriginalFrameworkEndpoints.Should().HaveCount(1); + + task.OriginalFrameworkEndpoints[0].ItemSpec.Should().Be("_content/FxLib/js/framework.js"); + + } - [Fact] + + + + + [TestMethod] + + public void Execute_FrameworkAssets_RemapsEndpointRoutes_ToConsumerBasePath() + + { + + // Arrange + + var sourceFile = CreateTempFile("source2", "lib.js", "content"); + + var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/FxLib", "js/lib.js"); + + var endpoint = CreateEndpoint("_content/FxLib/js/lib.js", sourceFile, "_content/FxLib/js/lib.js"); + + + + var task = new UpdatePackageStaticWebAssets + + { + + BuildEngine = _buildEngine.Object, + + Assets = [asset], + + Endpoints = [endpoint], + + IntermediateOutputPath = Path.Combine(_tempDir, "obj"), + + ProjectPackageId = "ConsumerLib", + + ProjectBasePath = "_content/ConsumerLib", + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().BeTrue(); + + task.RemappedEndpoints.Should().HaveCount(1); + + var remapped = task.RemappedEndpoints[0]; + + + + // Route should use consumer's base path. + + remapped.ItemSpec.Should().Be("_content/ConsumerLib/js/lib.js"); + + + + // Label should also reflect consumer's base path. + + remapped.GetMetadata("EndpointProperties").Should().Contain("_content/ConsumerLib/js/lib.js"); + + remapped.GetMetadata("EndpointProperties").Should().NotContain("_content/FxLib"); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateStaticWebAssetEndpointsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateStaticWebAssetEndpointsTest.cs index 43956cf963c3..71a5315a31e8 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateStaticWebAssetEndpointsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateStaticWebAssetEndpointsTest.cs @@ -1,381 +1,1145 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; + + + + +[TestClass] + public class UpdateStaticWebAssetEndpointsTest + + { - [Fact] + + + [TestMethod] + + 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() + + }; + + + + // 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] + + + + + [TestMethod] + + 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() + + }; + + + + // 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] + + + + + [TestMethod] + + 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() + + }; + + + + // 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] + + + + + [TestMethod] + + 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() + + }; + + + + // 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] + + + + + [TestMethod] + + 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() + + }; + + + + // 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] + + + + + [TestMethod] + + 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() + + }; + + + + // 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 + + { + + { "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(); + + + + defineStaticWebAssetEndpoints.Execute(); + + return StaticWebAssetEndpoint.FromItemGroup(defineStaticWebAssetEndpoints.Endpoints); + + } + + + + private static TaskItem CreateContentMapping(string pattern, string contentType) + + { + + return new TaskItem(contentType, new Dictionary + + { + + { "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; + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ValidateStaticWebAssetsUniquePathsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ValidateStaticWebAssetsUniquePathsTest.cs index fe6518ed6bda..f81e15c0819f 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ValidateStaticWebAssetsUniquePathsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ValidateStaticWebAssetsUniquePathsTest.cs @@ -1,216 +1,650 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using Microsoft.Build.Framework; + + using Microsoft.Build.Utilities; + + using Moq; + + + + namespace Microsoft.AspNetCore.Razor.Tasks + + { + + + [TestClass] + public class ValidateStaticWebAssetsUniquePathsTest + + { - [Fact] + + + [TestMethod] + + public void ReturnsError_WhenStaticWebAssetsWebRootPathMatchesExistingContentItemPath() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ValidateStaticWebAssetsUniquePaths + + { + + BuildEngine = buildEngine.Object, + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwroot", "js", "project-transitive-dep.js"), new Dictionary + + { + + ["BasePath"] = "_content/ClassLibrary", + + ["RelativePath"] = "js/project-transitive-dep.js", + + ["SourceId"] = "ClassLibrary", + + ["SourceType"] = "Project", + + }), + + }, + + WebRootFiles = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot", "_content", "ClassLibrary", "js", "project-transitive-dep.js"), new Dictionary + + { + + ["CopyToPublishDirectory"] = "PreserveNewest", + + ["ExcludeFromSingleFile"] = "true", + + ["OriginalItemSpec"] = Path.Combine("wwwroot", "_content", "ClassLibrary", "js", "project-transitive-dep.js"), + + ["TargetPath"] = Path.Combine("wwwroot", "_content", "ClassLibrary", "js", "project-transitive-dep.js"), + + }) + + } + + }; + + + + var expectedMessage = $"The static web asset '{Path.Combine("wwroot", "js", "project-transitive-dep.js")}' " + + + "has a conflicting web root path '/wwwroot/_content/ClassLibrary/js/project-transitive-dep.js' with the " + + + $"project file '{Path.Combine("wwwroot", "_content", "ClassLibrary", "js", "project-transitive-dep.js")}'."; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(false); + + errorMessages.Should().NotBeEmpty().And.HaveCount(1); + + errorMessages.Should().Contain(expectedMessage); + + } - [Fact] + + + + + [TestMethod] + + public void AllowsAssetsHavingTheSameBasePathAcrossDifferentSources_WhenTheirFinalDestinationPathIsDifferent() + + { + + // Arrange + + var task = new ValidateStaticWebAssetsUniquePaths + + { + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary + + { + + ["BasePath"] = "MyLibrary", + + ["ContentRoot"] = Path.Combine("nuget", "MyLibrary"), + + ["RelativePath"] = "sample.js", + + ["SourceId"] = "MyLibrary" + + }), + + CreateItem(Path.Combine("wwwroot", "otherLib.js"), new Dictionary + + { + + ["BasePath"] = "MyLibrary", + + ["ContentRoot"] = Path.Combine("nuget", "MyOtherLibrary"), + + ["RelativePath"] = "otherLib.js", + + ["SourceId"] = "MyOtherLibrary" + + }) + + }, + + WebRootFiles = Array.Empty() + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + } - [Fact] + + + + + [TestMethod] + + public void AllowsAssetsHavingTheSameContentRootAndDifferentBasePathsAcrossDifferentSources_WhenTheirFinalDestinationPathIsDifferent() + + { + + // Arrange + + var task = new ValidateStaticWebAssetsUniquePaths + + { + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary + + { + + ["BasePath"] = "MyLibrary", + + ["SourceId"] = "MyLibrary", + + ["RelativePath"] = "sample.js", + + ["ContentRoot"] = Path.Combine(".", "MyLibrary") + + }), + + CreateItem(Path.Combine("wwwroot", "otherLib.js"), new Dictionary + + { + + ["BasePath"] = "MyOtherLibrary", + + ["SourceId"] = "MyOtherLibrary", + + ["RelativePath"] = "otherlib.js", + + ["ContentRoot"] = Path.Combine(".", "MyLibrary") + + }) + + }, + + WebRootFiles = Array.Empty() + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + } - [Fact] + + + + + [TestMethod] + + public void ReturnsError_WhenMultipleStaticWebAssetsHaveTheSameWebRootPath() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + + .Callback(args => errorMessages.Add(args.Message)); + + + + var task = new ValidateStaticWebAssetsUniquePaths + + { + + BuildEngine = buildEngine.Object, + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine(".", "Library", "wwwroot", "sample.js"), new Dictionary + + { + + ["BasePath"] = "/", + + ["RelativePath"] = "/sample.js", + + }), + + CreateItem(Path.Combine(".", "Library", "bin", "dist", "sample.js"), new Dictionary + + { + + ["BasePath"] = "/", + + ["RelativePath"] = "/sample.js", + + }) + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(false); + + errorMessages.Should().NotBeEmpty().And.HaveCount(1); + + errorMessages.Should().Contain($"Conflicting assets with the same path '/wwwroot/sample.js' for content root paths '{Path.Combine(".", "Library", "bin", "dist", "sample.js")}' and '{Path.Combine(".", "Library", "wwwroot", "sample.js")}'."); + + } - [Fact] + + + + + [TestMethod] + + public void ReturnsSuccess_WhenStaticWebAssetsDontConflictWithApplicationContentItems() + + { + + // Arrange + + var errorMessages = new List(); + + var buildEngine = new Mock(); + + + + var task = new ValidateStaticWebAssetsUniquePaths + + { + + BuildEngine = buildEngine.Object, + + StaticWebAssets = new TaskItem[] + + { + + CreateItem(Path.Combine(".", "Library", "wwwroot", "sample.js"), new Dictionary + + { + + ["BasePath"] = "/_library", + + ["RelativePath"] = "/sample.js", + + }), + + CreateItem(Path.Combine(".", "Library", "wwwroot", "sample.js"), new Dictionary + + { + + ["BasePath"] = "/_library", + + ["RelativePath"] = "/sample.js", + + }) + + }, + + WebRootFiles = new TaskItem[] + + { + + CreateItem(Path.Combine(".", "App", "wwwroot", "sample.js"), new Dictionary + + { + + ["TargetPath"] = "/SAMPLE.js", + + }) + + } + + }; + + + + // Act + + var result = task.Execute(); + + + + // Assert + + result.Should().Be(true); + + } + + + + private static TaskItem CreateItem( + + string spec, + + IDictionary metadata) + + { + + var result = new TaskItem(spec); + + foreach (var (key, value) in metadata) + + { + + result.SetMetadata(key, value); + + } + + + + return result; + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineComparer.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineComparer.cs index b1f90ef05e38..c8ae673eabfc 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineComparer.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineComparer.cs @@ -1,521 +1,1043 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; + using System.Linq; + using Microsoft.AspNetCore.StaticWebAssets.Tasks; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + + public class StaticWebAssetsBaselineComparer + { + private static readonly string BaselineGenerationInstructions = + @"If the difference in baselines is expected, please re-generate the baselines. + Start by ensuring you're dogfooding the SDK from the current branch (dotnet --version should be '*.0.0-dev'). + If you're not on the dogfood sdk, from the root of the repository run: + 1. dotnet clean + 2. .\restore.cmd or ./restore.sh + 3. .\build.cmd ./build.sh + 4. .\eng\dogfood.cmd or . ./eng/dogfood.sh + + Then, using the dogfood SDK run the .\src\RazorSdk\update-test-baselines.ps1 script."; + + public static StaticWebAssetsBaselineComparer Instance { get; } = new(); + + internal void AssertManifest(StaticWebAssetsManifest expected, StaticWebAssetsManifest actual) + { + //Many of the properties in the manifest contain full paths, to avoid flakiness on the tests, we don't compare the full paths. + actual.Version.Should().Be(expected.Version); + actual.Source.Should().Be(expected.Source); + actual.BasePath.Should().Be(expected.BasePath); + actual.Mode.Should().Be(expected.Mode); + actual.ManifestType.Should().Be(expected.ManifestType); + + actual.ReferencedProjectsConfiguration.Should().HaveSameCount(expected.ReferencedProjectsConfiguration); + + // Relax the check for project reference configuration items see + // https://github.com/dotnet/sdk/pull/27381#issuecomment-1228764471 + // for details. + //manifest.ReferencedProjectsConfiguration.OrderBy(cm => cm.Identity) + // .Should() + // .BeEquivalentTo(expected.ReferencedProjectsConfiguration.OrderBy(cm => cm.Identity)); + + actual.DiscoveryPatterns.OrderBy(dp => dp.Name).Should().BeEquivalentTo(expected.DiscoveryPatterns.OrderBy(dp => dp.Name)); + + var actualAssets = actual.Assets + .OrderBy(a => a.BasePath) + .ThenBy(a => a.RelativePath) + .ThenBy(a => a.AssetKind) + .GroupBy(a => GetAssetGroup(a)) + .ToDictionary(a => a.Key, a => a.Order().ToArray()); + + var duplicateAssets = actual.Assets + .GroupBy(a => a) + .ToDictionary(a => a.Key, a => a.Order().ToArray()); + + var foundDuplicateAssetss = duplicateAssets.Where(a => a.Value.Length > 1).ToArray(); + duplicateAssets.Where(a => a.Value.Length > 1).Should().BeEmpty($@"no duplicate assets should exist. But found: + {string.Join($"{Environment.NewLine} ", foundDuplicateAssetss.Select(a => @$"{a.Key.Identity} - {a.Value.Length}"))}{Environment.NewLine}"); + + var expectedAssets = expected.Assets + .OrderBy(a => a.BasePath) + .ThenBy(a => a.RelativePath) + .ThenBy(a => a.AssetKind) + .GroupBy(a => GetAssetGroup(a)) + .ToDictionary(a => a.Key, a => a.Order().ToArray()); + + var actualAssetsByIdentity = actual.Assets.GroupBy(a => a.Identity).ToDictionary(a => a.Key, a => a.Order().ToArray()); + foreach (var asset in actual.Assets) + { + if (!string.IsNullOrEmpty(asset.RelatedAsset)) + { + actualAssetsByIdentity.Should().ContainKey(asset.RelatedAsset); + } + } + + foreach (var (group, actualAssetsGroup) in actualAssets) + { + var expectedAssetsGroup = expectedAssets[group]; + CompareAssetGroup(group, actualAssetsGroup, expectedAssetsGroup); + } + + var actualEndpoints = actual.Endpoints + .OrderBy(a => a.Route) + .ThenBy(a => a.AssetFile) + .GroupBy(a => GetEndpointGroup(a)) + .ToDictionary(a => a.Key, a => a.Order().ToArray()); + + SortEndpointProperties(actualEndpoints); + + var duplicateEndpoints = actual.Endpoints + .GroupBy(a => a) + .ToDictionary(a => a.Key, a => a.Order().ToArray()); + + var foundDuplicateEndpoints = duplicateEndpoints.Where(a => DuplicatesExist(a)).ToArray(); + + duplicateEndpoints.Where(a => DuplicatesExist(a)).Should().BeEmpty($@"no duplicate endpoints should exist. But found: + {string.Join($"{Environment.NewLine} ", foundDuplicateEndpoints.Select(a => @$"{a.Key.Route} - {a.Key.AssetFile} - {a.Key.Selectors.Length} - {a.Value.Length}"))}{Environment.NewLine}"); + + foreach (var endpoint in actual.Endpoints) + { + actualAssetsByIdentity.Should().ContainKey(endpoint.AssetFile); + } + + var expectedEndpoints = expected.Endpoints + .OrderBy(a => a.Route) + .ThenBy(a => a.AssetFile) + .GroupBy(a => GetEndpointGroup(a)) + .ToDictionary(a => a.Key, a => a.Order().ToArray()); + + SortEndpointProperties(expectedEndpoints); + + foreach (var (group, actualEndpointsGroup) in actualEndpoints) + { + var expectedEndpointsGroup = expectedEndpoints[group]; + CompareEndpointGroup(group, actualEndpointsGroup, expectedEndpointsGroup); + } + + static bool DuplicatesExist(KeyValuePair a) + { + var endpoint = a.Key; + if (endpoint.Route.EndsWith(".gz") || endpoint.Route.EndsWith(".br") || endpoint.Selectors.Length == 1) + { + // This is not exact, but there are situations in which our templatization process is not biyective and Build and Publish assets defined during build for + // the same asset end up having the same endpoint. To avoid issues with this, we relax the check to support finding more than one. + return a.Value.Length > 2; + } + else + { + return a.Value.Length > 1; + } + } + } + + private static void SortEndpointProperties(Dictionary endpoints) + { + foreach (var endpointGroup in endpoints.Values) + { + foreach (var endpoint in endpointGroup) + { + Array.Sort(endpoint.Selectors, (a, b) => (a.Name, a.Value).CompareTo((b.Name, b.Value))); + Array.Sort(endpoint.ResponseHeaders, (a, b) => (a.Name, a.Value).CompareTo((b.Name, b.Value))); + Array.Sort(endpoint.EndpointProperties, (a, b) => (a.Name, a.Value).CompareTo((b.Name, b.Value))); + } + } + } + + protected virtual void CompareAssetGroup(string group, StaticWebAsset[] manifestAssets, StaticWebAsset[] expectedAssets) + { + var comparisonMode = CompareAssetCounts(group, manifestAssets, expectedAssets); + Array.Sort(manifestAssets, (a, b) => a.Identity.CompareTo(b.Identity)); + Array.Sort(expectedAssets, (a, b) => a.Identity.CompareTo(b.Identity)); + + // Otherwise, do a property level comparison of all assets + switch (comparisonMode) + { + case GroupComparisonMode.Exact: + break; + case GroupComparisonMode.AllowAdditionalAssets: + break; + default: + break; + } + + var differences = new List(); + var assetDifferences = new List(); + var groupLength = Math.Min(manifestAssets.Length, expectedAssets.Length); + for (var i = 0; i < groupLength; i++) + { + var manifestAsset = manifestAssets[i]; + var expectedAsset = expectedAssets[i]; + + ComputeAssetDifferences(assetDifferences, manifestAsset, expectedAsset); + + if (assetDifferences.Any()) + { + differences.Add(@$" + ================================================== + + For {expectedAsset.Identity}: + + {string.Join(Environment.NewLine, assetDifferences)} + + =================================================="); + } + + assetDifferences.Clear(); + } + + differences.Should().BeEmpty( + @$" the generated manifest should match the expected baseline. + + {BaselineGenerationInstructions} + + "); + } + + private GroupComparisonMode CompareAssetCounts(string group, StaticWebAsset[] manifestAssets, StaticWebAsset[] expectedAssets) + { + var comparisonMode = GetGroupComparisonMode(group); + + // If there's a mismatch in the number of assets, just print the strict difference in the asset `Identity` + switch (comparisonMode) + { + case GroupComparisonMode.Exact: + if (manifestAssets.Length != expectedAssets.Length) + { + ThrowAssetCountMismatchError(manifestAssets, expectedAssets); + } + break; + case GroupComparisonMode.AllowAdditionalAssets: + if (expectedAssets.Except(manifestAssets).Any()) + { + ThrowAssetCountMismatchError(manifestAssets, expectedAssets); + } + break; + default: + break; + } + + return comparisonMode; + + static void ThrowAssetCountMismatchError(StaticWebAsset[] manifestAssets, StaticWebAsset[] expectedAssets) + { + var missingAssets = expectedAssets.Except(manifestAssets); + var unexpectedAssets = manifestAssets.Except(expectedAssets); + + var differences = new List(); + + if (missingAssets.Any()) + { + differences.Add($@"The following expected assets weren't found in the manifest: + {string.Join($"{Environment.NewLine}\t", missingAssets.Select(a => a.Identity))}"); + } + + if (unexpectedAssets.Any()) + { + differences.Add($@"The following additional unexpected assets were found in the manifest: + {string.Join($"{Environment.NewLine}\t", unexpectedAssets.Select(a => a.Identity))}"); + } + + throw new Exception($@"{string.Join(Environment.NewLine, differences)} + + {BaselineGenerationInstructions}"); + } + } + + protected virtual GroupComparisonMode GetGroupComparisonMode(string group) + { + return GroupComparisonMode.Exact; + } + + private static void ComputeAssetDifferences(List assetDifferences, StaticWebAsset manifestAsset, StaticWebAsset expectedAsset) + { + if (manifestAsset.Identity != expectedAsset.Identity) + { + assetDifferences.Add($"Expected manifest Identity of {expectedAsset.Identity} but found {manifestAsset.Identity}."); + } + if (manifestAsset.SourceType != expectedAsset.SourceType) + { + assetDifferences.Add($"Expected manifest SourceType of {expectedAsset.SourceType} but found {manifestAsset.SourceType}."); + } + if (manifestAsset.SourceId != expectedAsset.SourceId) + { + assetDifferences.Add($"Expected manifest SourceId of {expectedAsset.SourceId} but found {manifestAsset.SourceId}."); + } + if (manifestAsset.ContentRoot != expectedAsset.ContentRoot) + { + assetDifferences.Add($"Expected manifest ContentRoot of {expectedAsset.ContentRoot} but found {manifestAsset.ContentRoot}."); + } + if (manifestAsset.BasePath != expectedAsset.BasePath) + { + assetDifferences.Add($"Expected manifest BasePath of {expectedAsset.BasePath} but found {manifestAsset.BasePath}."); + } + if (manifestAsset.RelativePath != expectedAsset.RelativePath) + { + assetDifferences.Add($"Expected manifest RelativePath of {expectedAsset.RelativePath} but found {manifestAsset.RelativePath}."); + } + if (manifestAsset.AssetKind != expectedAsset.AssetKind) + { + assetDifferences.Add($"Expected manifest AssetKind of {expectedAsset.AssetKind} but found {manifestAsset.AssetKind}."); + } + if (manifestAsset.AssetMode != expectedAsset.AssetMode) + { + assetDifferences.Add($"Expected manifest AssetMode of {expectedAsset.AssetMode} but found {manifestAsset.AssetMode}."); + } + if (manifestAsset.AssetRole != expectedAsset.AssetRole) + { + assetDifferences.Add($"Expected manifest AssetRole of {expectedAsset.AssetRole} but found {manifestAsset.AssetRole}."); + } + if (manifestAsset.RelatedAsset != expectedAsset.RelatedAsset) + { + assetDifferences.Add($"Expected manifest RelatedAsset of {expectedAsset.RelatedAsset} but found {manifestAsset.RelatedAsset}."); + } + if (manifestAsset.AssetTraitName != expectedAsset.AssetTraitName) + { + assetDifferences.Add($"Expected manifest AssetTraitName of {expectedAsset.AssetTraitName} but found {manifestAsset.AssetTraitName}."); + } + if (manifestAsset.AssetTraitValue != expectedAsset.AssetTraitValue) + { + assetDifferences.Add($"Expected manifest AssetTraitValue of {expectedAsset.AssetTraitValue} but found {manifestAsset.AssetTraitValue}."); + } + if (manifestAsset.CopyToOutputDirectory != expectedAsset.CopyToOutputDirectory) + { + assetDifferences.Add($"Expected manifest CopyToOutputDirectory of {expectedAsset.CopyToOutputDirectory} but found {manifestAsset.CopyToOutputDirectory}."); + } + if (manifestAsset.CopyToPublishDirectory != expectedAsset.CopyToPublishDirectory) + { + assetDifferences.Add($"Expected manifest CopyToPublishDirectory of {expectedAsset.CopyToPublishDirectory} but found {manifestAsset.CopyToPublishDirectory}."); + } + if (manifestAsset.OriginalItemSpec != expectedAsset.OriginalItemSpec) + { + assetDifferences.Add($"Expected manifest OriginalItemSpec of {expectedAsset.OriginalItemSpec} but found {manifestAsset.OriginalItemSpec}."); + } + } + + protected virtual string GetAssetGroup(StaticWebAsset asset) + { + return Path.GetExtension(asset.Identity.TrimEnd(']')); + } + + protected virtual void CompareEndpointGroup(string group, StaticWebAssetEndpoint[] manifestAssets, StaticWebAssetEndpoint[] expectedAssets) + { + var comparisonMode = CompareEndpointCounts(group, manifestAssets, expectedAssets); + Array.Sort(manifestAssets); + Array.Sort(expectedAssets); + + // Otherwise, do a property level comparison of all assets + switch (comparisonMode) + { + case GroupComparisonMode.Exact: + break; + case GroupComparisonMode.AllowAdditionalAssets: + break; + default: + break; + } + + var differences = new List(); + var assetDifferences = new List(); + var groupLength = Math.Min(manifestAssets.Length, expectedAssets.Length); + for (var i = 0; i < groupLength; i++) + { + var manifestAsset = manifestAssets[i]; + var expectedAsset = expectedAssets[i]; + + ComputeEndpointDifferences(assetDifferences, manifestAsset, expectedAsset); + + if (assetDifferences.Any()) + { + differences.Add(@$" + ================================================== + + For {expectedAsset.AssetFile}: + + {string.Join(Environment.NewLine, assetDifferences)} + + =================================================="); + } + + assetDifferences.Clear(); + } + + differences.Should().BeEmpty( + @$" the generated manifest should match the expected baseline. + + {BaselineGenerationInstructions} + + "); + } + + private GroupComparisonMode CompareEndpointCounts(string group, StaticWebAssetEndpoint[] manifestEndpoints, StaticWebAssetEndpoint[] expectedEndpoints) + { + var comparisonMode = GetGroupComparisonMode(group); + + // If there's a mismatch in the number of assets, just print the strict difference in the asset `Identity` + switch (comparisonMode) + { + case GroupComparisonMode.Exact: + if (manifestEndpoints.Length != expectedEndpoints.Length) + { + ThrowEndpointCountMismatchError(group, manifestEndpoints, expectedEndpoints); + } + break; + case GroupComparisonMode.AllowAdditionalAssets: + if (expectedEndpoints.Except(manifestEndpoints).Any()) + { + ThrowEndpointCountMismatchError(group, manifestEndpoints, expectedEndpoints); + } + break; + default: + break; + } + + return comparisonMode; + + static void ThrowEndpointCountMismatchError(string group, StaticWebAssetEndpoint[] manifestEndpoints, StaticWebAssetEndpoint[] expectedEndpoints) + { + var missingEndpoints = expectedEndpoints.Except(manifestEndpoints); + var unexpectedEndpoints = manifestEndpoints.Except(expectedEndpoints); + + var differences = new List + { + $"Expected group '{group}' to have '{expectedEndpoints.Length}' endpoints but found '{manifestEndpoints.Length}'.", + "Expected Endpoints:", + string.Join($"{Environment.NewLine}\t", expectedEndpoints.Select(a => $"{a.Route} - {a.Selectors.Length} - {a.AssetFile}")), + "Actual Endpoints:", + string.Join($"{Environment.NewLine}\t", manifestEndpoints.Select(a => $"{a.Route} - {a.Selectors.Length} - {a.AssetFile}")) + }; + + if (missingEndpoints.Any()) + { + differences.Add($@"The following expected assets weren't found in the manifest: + {string.Join($"{Environment.NewLine}\t", missingEndpoints.Select(a => $"{a.Route} - {a.AssetFile}"))}"); + } + + if (unexpectedEndpoints.Any()) + { + differences.Add($@"The following additional unexpected assets were found in the manifest: + {string.Join($"{Environment.NewLine}\t", unexpectedEndpoints.Select(a => $"{a.Route} - {a.AssetFile}"))}"); + } + + throw new Exception($@"{string.Join(Environment.NewLine, differences)} + + {BaselineGenerationInstructions}"); + } + } + + protected virtual GroupComparisonMode GetAssetGroupComparisonMode(string group) + { + return GroupComparisonMode.Exact; + } + + private static void ComputeEndpointDifferences(List assetDifferences, StaticWebAssetEndpoint manifestAsset, StaticWebAssetEndpoint expectedAsset) + { + if (manifestAsset.Route != expectedAsset.Route) + { + assetDifferences.Add($"Expected manifest Identity of {expectedAsset.Route} but found {manifestAsset.Route}."); + } + if (manifestAsset.AssetFile != expectedAsset.AssetFile) + { + assetDifferences.Add($"Expected manifest SourceType of {expectedAsset.AssetFile} but found {manifestAsset.AssetFile}."); + } + if ((manifestAsset.Order ?? "") != (expectedAsset.Order ?? "")) + { + assetDifferences.Add($"Expected manifest Order of '{expectedAsset.Order}' but found '{manifestAsset.Order}'."); + } + + ComputeSelectorDifferences(assetDifferences, manifestAsset.Selectors, expectedAsset.Selectors); + ComputeResponseHeaderDifferences(assetDifferences, manifestAsset.ResponseHeaders, expectedAsset.ResponseHeaders); + } + + private static void ComputeResponseHeaderDifferences( + List assetDifferences, + StaticWebAssetEndpointResponseHeader[] manifestResponseHeaders, + StaticWebAssetEndpointResponseHeader[] expectedResponseHeaders) + { + if (manifestResponseHeaders.Length != expectedResponseHeaders.Length) + { + assetDifferences.Add($"Expected manifest to have {expectedResponseHeaders.Length} response headers but found {manifestResponseHeaders.Length}."); + } + + var manifest = new HashSet(manifestResponseHeaders); + var differences = new HashSet(manifestResponseHeaders); + var expected = new HashSet(expectedResponseHeaders); + differences.SymmetricExceptWith(expected); + + foreach (var difference in differences) + { + if (!manifest.Contains(difference)) + { + assetDifferences.Add($"Expected manifest to have response header '{difference.Name}={difference.Value}' but it was not found."); + } + else + { + assetDifferences.Add($"Found unexpected response header '{difference.Name}={difference.Value}'."); + } + } + } + + private static void ComputeSelectorDifferences( + List assetDifferences, + StaticWebAssetEndpointSelector[] manifestSelectors, + StaticWebAssetEndpointSelector[] expectedSelectors) + { + if (manifestSelectors.Length != expectedSelectors.Length) + { + assetDifferences.Add($"Expected manifest to have {expectedSelectors.Length} selectors but found {manifestSelectors.Length}."); + } + + var manifest = new HashSet(manifestSelectors); + var differences = new HashSet(manifestSelectors); + var expected = new HashSet(expectedSelectors); + differences.SymmetricExceptWith(expected); + + foreach (var difference in differences) + { + if (!manifest.Contains(difference)) + { + assetDifferences.Add($"Expected manifest to have selector '{difference.Name}={difference.Value};q={difference.Quality}' but it was not found."); + } + else + { + assetDifferences.Add($"Found unexpected selector '{difference.Name}={difference.Value};q={difference.Quality}'."); + } + } + } + + protected virtual string GetEndpointGroup(StaticWebAssetEndpoint asset) + { + return Path.GetExtension(asset.AssetFile.TrimEnd(']')); + } + } + + public enum GroupComparisonMode + { + // We require the same number of assets in a group for the baseline and the template. + Exact, + + // We won't fail when we check against the baseline if additional assets are present for a group. + AllowAdditionalAssets + } + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineFactory.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineFactory.cs index bf33b8a5b711..7e0490d54b5f 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineFactory.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineFactory.cs @@ -1,486 +1,973 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + using System.Runtime.Versioning; + using System.Text.RegularExpressions; + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + using NuGet.Frameworks; + using NuGet.ProjectModel; + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + public partial class StaticWebAssetsBaselineFactory + { + [GeneratedRegex("""(.*\.)([0123456789abcdefghijklmnopqrstuvwxyz]{10})(\.bundle\.scp\.css)((?:\.gz)|(?:\.br))?$""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex ScopedProjectBundleRegex(); + + [GeneratedRegex("""(.*\.)([0123456789abcdefghijklmnopqrstuvwxyz]{10})(\.styles\.css)((?:\.gz)|(?:\.br))?$""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex ScopedAppBundleRegex(); + + [GeneratedRegex("""fingerprint-site(\.)([0123456789abcdefghijklmnopqrstuvwxyz]{10})(\.css)((?:\.gz)|(?:\.br))?$""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex FingerprintedSiteCssRegex(); + + [GeneratedRegex("""(?:#\[\.{fingerprint=[0123456789abcdefghijklmnopqrstuvwxyz]{10}\}](\?|\!)?)""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex EmbeddedFingerprintExpression(); + + [GeneratedRegex("""(.*\.)([0123456789abcdefghijklmnopqrstuvwxyz]{10})(\.lib\.module\.js)((?:\.gz)|(?:\.br))?$""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex JSInitializerRegex(); + + [GeneratedRegex("""(.*\.)([0123456789abcdefghijklmnopqrstuvwxyz]{10})(\.modules\.json)((?:\.gz)|(?:\.br))?$""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] + private static partial Regex JSModuleManifestRegex(); + + private static readonly IList<(Regex expression, string replacement)> WellKnownFileNamePatternsAndReplacements = + [ + (ScopedProjectBundleRegex(),"$1__fingerprint__$3$4"), + (ScopedAppBundleRegex(),"$1__fingerprint__$3$4"), + (JSInitializerRegex(), "$1__fingerprint__$3$4"), + (JSModuleManifestRegex(), "$1__fingerprint__$3$4"), + (EmbeddedFingerprintExpression(), "#[.{fingerprint=__fingerprint__}]$1"), + (FingerprintedSiteCssRegex(), "fingerprint-site$1__fingerprint__$3$4"), + ]; + + public static StaticWebAssetsBaselineFactory Instance { get; } = new(); + + public IList KnownExtensions { get; } = + [ + // Keep this list of most specific to less specific + ".dll.gz", + ".dll.br", + ".dll", + ".wasm.gz", + ".wasm.br", + ".wasm", + ".js.gz", + ".js.br", + ".js", + ".html", + ".pdb", + ]; + + public IList KnownFilePrefixesWithHashOrVersion { get; } = + [ + "blazor.web.", + "blazor.server", + "dotnet.runtime", + "dotnet.native", + "dotnet.boot", + "dotnet" + ]; + + public void ToTemplate( + StaticWebAssetsManifest manifest, + string projectRoot, + string restorePath, + string runtimeIdentifier) + { + manifest.Hash = "__hash__"; + var assetsByIdentity = manifest.Assets.ToDictionary(a => a.Identity); + var endpointsByAssetFile = manifest.Endpoints.GroupBy(e => e.AssetFile).ToDictionary(g => g.Key, g => g.ToArray()); + foreach (var asset in manifest.Assets) + { + var relatedEndpoints = endpointsByAssetFile.GetValueOrDefault(asset.Identity); + TemplatizeAsset(projectRoot, restorePath, runtimeIdentifier, asset); + foreach (var endpoint in relatedEndpoints ?? []) + { + endpoint.AssetFile = asset.Identity; + } + if (asset.AssetTraitName == "Content-Encoding") + { + var basePath = asset.BasePath.Replace('/', Path.DirectorySeparatorChar).TrimStart(Path.DirectorySeparatorChar); + var relativePath = asset.RelativePath.Replace('/', Path.DirectorySeparatorChar); + var identity = asset.Identity.Replace('\\', Path.DirectorySeparatorChar); + var originalItemSpec = asset.OriginalItemSpec.Replace('\\', Path.DirectorySeparatorChar); + + asset.Identity = Path.Combine(Path.GetDirectoryName(identity), basePath, relativePath); + asset.Identity = asset.Identity.Replace(Path.DirectorySeparatorChar, '\\'); + foreach (var endpoint in relatedEndpoints ?? []) + { + endpoint.AssetFile = asset.Identity; + } + asset.OriginalItemSpec = Path.Combine(Path.GetDirectoryName(originalItemSpec), basePath, relativePath); + asset.OriginalItemSpec = asset.OriginalItemSpec.Replace(Path.DirectorySeparatorChar, '\\'); + } + else if ((asset.Identity.EndsWith(".gz", StringComparison.OrdinalIgnoreCase) || asset.Identity.EndsWith(".br", StringComparison.OrdinalIgnoreCase)) + && asset.AssetTraitName == "" && asset.RelatedAsset == "") + { + // Old .NET 5.0 implementation + var identity = asset.Identity.Replace('\\', Path.DirectorySeparatorChar); + var originalItemSpec = asset.OriginalItemSpec.Replace('\\', Path.DirectorySeparatorChar); + + asset.Identity = Path.Combine(Path.GetDirectoryName(identity), Path.GetFileName(originalItemSpec) + Path.GetExtension(identity)) + .Replace(Path.DirectorySeparatorChar, '\\'); + } + } + + foreach (var endpoint in manifest.Endpoints) + { + for (var i = 0; i < endpoint.ResponseHeaders.Length; i++) + { + ref var header = ref endpoint.ResponseHeaders[i]; + switch (header.Name) + { + case "Content-Length": + header.Value = "__content-length__"; + break; + case "ETag": + header.Value = "__etag__"; + break; + case "Last-Modified": + header.Value = "__last-modified__"; + break; + case "Link": + var cleaned = new List(); + var values = header.Value.Split(',').Select(v => v.Trim()); + foreach (var value in values) + { + var segments = value.Split(';').Select(v => v.Trim()).ToArray(); + var file = segments[0][1..^1]; + segments[0] = $"<{ReplaceFileName(file).Replace('\\', '/')}>"; + cleaned.Add(string.Join("; ", segments)); + } + header.Value = string.Join(", ", cleaned); + + break; + default: + break; + } + } + + for (var i = 0; i < endpoint.EndpointProperties.Length; i++) + { + ref var property = ref endpoint.EndpointProperties[i]; + switch (property.Name) + { + case "fingerprint": + property.Value = "__fingerprint__"; + endpoint.Route = endpoint.Route.Replace(property.Value, $"__{property.Name}__"); + break; + case "integrity": + property.Value = "__integrity__"; + break; + case "original-resource": + property.Value = "__original-resource__"; + break; + default: + break; + } + + ReplaceFileName(endpoint.Route); + } + + for (var i = 0; i < endpoint.Selectors.Length; i++) + { + ref var selector = ref endpoint.Selectors[i]; + selector.Quality = "__quality__"; + } + + endpoint.Route = TemplatizeFilePath(endpoint.Route, null, null, null, null, null).Replace("\\", "/"); + + endpoint.AssetFile = TemplatizeFilePath( + endpoint.AssetFile, + restorePath, + projectRoot, + null, + null, + runtimeIdentifier); + } + + foreach (var discovery in manifest.DiscoveryPatterns) + { + discovery.ContentRoot = discovery.ContentRoot.Replace(projectRoot, "${ProjectPath}", StringComparison.OrdinalIgnoreCase); + discovery.ContentRoot = discovery.ContentRoot.Replace(Path.DirectorySeparatorChar, '\\'); + + discovery.Name = discovery.Name.Replace(Path.DirectorySeparatorChar, '\\'); + discovery.Pattern = discovery.Pattern.Replace(Path.DirectorySeparatorChar, '\\'); + } + + foreach (var relatedManifest in manifest.ReferencedProjectsConfiguration) + { + relatedManifest.Identity = relatedManifest.Identity.Replace(projectRoot, "${ProjectPath}").Replace(Path.DirectorySeparatorChar, '\\'); + } + + // Sor everything now to ensure we produce stable baselines independent of the machine they were generated on. + Array.Sort(manifest.DiscoveryPatterns, (l, r) => StringComparer.Ordinal.Compare(l.Name, r.Name)); + Array.Sort(manifest.Assets); + foreach (var endpoint in manifest.Endpoints) + { + Array.Sort(endpoint.Selectors); + Array.Sort(endpoint.EndpointProperties); + Array.Sort(endpoint.ResponseHeaders); + } + Array.Sort(manifest.Endpoints); + + Array.Sort(manifest.ReferencedProjectsConfiguration, (l, r) => StringComparer.Ordinal.Compare(l.Identity, r.Identity)); + } + + private void TemplatizeAsset(string projectRoot, string restorePath, string runtimeIdentifier, StaticWebAsset asset) + { + asset.Identity = TemplatizeFilePath( + asset.Identity, + restorePath, + projectRoot, + null, + null, + runtimeIdentifier); + + asset.RelativePath = TemplatizeFilePath( + asset.RelativePath, + null, + null, + null, + null, + runtimeIdentifier).Replace('\\', '/'); + + asset.ContentRoot = TemplatizeFilePath( + asset.ContentRoot, + restorePath, + projectRoot, + null, + null, + runtimeIdentifier); + + asset.RelatedAsset = TemplatizeFilePath( + asset.RelatedAsset, + restorePath, + projectRoot, + null, + null, + runtimeIdentifier); + + asset.OriginalItemSpec = TemplatizeFilePath( + asset.OriginalItemSpec, + restorePath, + projectRoot, + null, + null, + runtimeIdentifier); + + asset.Fingerprint = string.IsNullOrEmpty(asset.Fingerprint) ? asset.Fingerprint : "__fingerprint__"; + asset.Integrity = string.IsNullOrEmpty(asset.Integrity) ? asset.Integrity : "__integrity__"; + asset.FileLength = -1; + asset.LastWriteTime = DateTimeOffset.MinValue; + } + + internal IEnumerable TemplatizeExpectedFiles( + IEnumerable files, + string restorePath, + string projectPath, + string intermediateOutputPath, + string buildOrPublishFolder) + { + foreach (var file in files) + { + var updated = TemplatizeFilePath( + file, + restorePath, + projectPath, + intermediateOutputPath, + buildOrPublishFolder, + null); + + yield return updated; + } + } + + public string TemplatizeFilePath( + string file, + string restorePath, + string projectPath, + string intermediateOutputPath, + string buildOrPublishFolder, + string runtimeIdentifier) + { + var updated = file switch + { + var processed when file.StartsWith('$') => processed, + var fromBuildOrPublishPath when buildOrPublishFolder is not null && file.StartsWith(buildOrPublishFolder, StringComparison.OrdinalIgnoreCase) => + TemplatizeBuildOrPublishPath(buildOrPublishFolder, fromBuildOrPublishPath), + var fromIntermediateOutputPath when intermediateOutputPath is not null && file.StartsWith(intermediateOutputPath, StringComparison.OrdinalIgnoreCase) => + TemplatizeIntermediatePath(intermediateOutputPath, fromIntermediateOutputPath), + var fromPackage when restorePath is not null && file.StartsWith(restorePath, StringComparison.OrdinalIgnoreCase) => + TemplatizeNugetPath(restorePath, fromPackage), + var fromProject when projectPath is not null && file.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase) => + TemplatizeProjectPath(projectPath, fromProject, runtimeIdentifier), + var fromWebAssemblySdk when file.Replace('\\', '/').Contains("/Sdks/Microsoft.NET.Sdk.WebAssembly", StringComparison.OrdinalIgnoreCase) => + TemplatizeWebAssemblySdkPath(fromWebAssemblySdk), + _ => + ReplaceSegments(file, (i, segments) => i switch + { + 2 when segments[0] is "obj" or "bin" => "${Tfm}", + var last when i == segments.Length - 1 => RemovePossibleHash(segments[last]), + _ => segments[i] + }) + }; + + return ReplaceFileName(updated).Replace('/', '\\'); + } + + private string TemplatizeWebAssemblySdkPath(string file) + { + var normalized = file.Replace('\\', '/'); + var marker = "/Sdks/Microsoft.NET.Sdk.WebAssembly"; + var idx = normalized.IndexOf(marker, StringComparison.OrdinalIgnoreCase); + if (idx < 0) + { + return file; + } + + // Replace everything up to and including the SDK folder with the token + var remainder = normalized.Substring(idx + marker.Length); + if (remainder.StartsWith('/')) + { + remainder = remainder[1..]; + } + + var templated = "${WebAssemblySdkPath}" + (string.IsNullOrEmpty(remainder) ? string.Empty : "/" + remainder); + + // Replace filename hashes if any + templated = ReplaceSegments(templated, (i, segments) => i switch + { + _ when i == segments.Length - 1 => RemovePossibleHash(segments[i]), + _ => segments[i] + }); + + return templated; + } + + private static string ReplaceFileName(string path) + { + var directory = Path.GetDirectoryName(path); + var fileName = Path.GetFileName(path); + foreach (var (expression, replacement) in WellKnownFileNamePatternsAndReplacements) + { + if (expression.IsMatch(fileName)) + { + fileName = expression.Replace(fileName, replacement); + return Path.Combine(directory, fileName); + } + } + + return path; + } + + private string TemplatizeBuildOrPublishPath(string outputPath, string file) + { + file = file.Replace(outputPath, "${OutputPath}") + .Replace('\\', '/'); + + file = ReplaceSegments(file, (i, segments) => i switch + { + _ when i == segments.Length - 1 => RemovePossibleHash(segments[i]), + _ => segments[i], + }); + + return file; + } + + private string TemplatizeIntermediatePath(string intermediatePath, string file) + { + file = file.Replace(intermediatePath, "${IntermediateOutputPath}") + .Replace('\\', '/'); + + file = ReplaceSegments(file, (i, segments) => i switch + { + 3 when segments[1] is "obj" or "bin" => "${Tfm}", + _ when i == segments.Length - 1 => RemovePossibleHash(segments[i]), + _ => segments[i] + }); + + return file; + } + + private string TemplatizeProjectPath(string projectPath, string file, string runtimeIdentifier) + { + file = file.Replace(projectPath, "${ProjectPath}") + .Replace('\\', '/'); + + file = ReplaceSegments(file, (i, segments) => i switch + { + 3 when segments[1] is "obj" or "bin" => "${Tfm}", + 4 when segments[2] is "obj" or "bin" => "${Tfm}", + 4 when segments[1] is "obj" or "bin" && segments[4] == runtimeIdentifier => "${Rid}", + 5 when segments[2] is "obj" or "bin" && segments[5] == runtimeIdentifier => "${Rid}", + _ when i == segments.Length - 1 => RemovePossibleHash(segments[i]), + _ => segments[i] + }); + + return file; + } + + private string TemplatizeNugetPath(string restorePath, string file) + { + file = file.Replace(restorePath, "${RestorePath}", StringComparison.OrdinalIgnoreCase) + .Replace('\\', '/'); + if (file.Contains("runtimes")) + { + file = ReplaceSegments(file, (i, segments) => i switch + { + 2 => "${RuntimeVersion}", + 6 when !file.Contains("native") => "${Tfm}", + _ when i == segments.Length - 1 => RemovePossibleHash(segments[i]), + _ => segments[i], + }); + } + else + { + file = ReplaceSegments(file, (i, segments) => i switch + { + 2 => "${PackageVersion}", + 4 when IsFramework(segments[4]) => "${Tfm}", + _ when i == segments.Length - 1 => RemovePossibleHash(segments[i]), + _ => segments[i], + }); + } + + return file; + + static bool IsFramework(string segment) + { + try + { + var tfm = NuGetFramework.ParseFolder(segment); + + return tfm.Framework is FrameworkConstants.FrameworkIdentifiers.NetCoreApp or + FrameworkConstants.FrameworkIdentifiers.NetStandard or + FrameworkConstants.FrameworkIdentifiers.NetCore or + FrameworkConstants.FrameworkIdentifiers.Net; + } + catch + { + return false; + } + } + } + + private static string ReplaceSegments(string file, Func selector) + { + var segments = file.Split('\\', '/'); + var newSegments = new List(); + + // Segments have the following shape `${RestorePath}/PackageName/PackageVersion/lib/Tfm/dll`. + // We want to replace PackageVersion and Tfm with tokens so that they do not cause issues. + for (var i = 0; i < segments.Length; i++) + { + newSegments.Add(selector(i, segments)); + } + + return string.Join(Path.DirectorySeparatorChar, newSegments); + } + + private string RemovePossibleHash(string fileNameAndExtension) + { + var filename = KnownFilePrefixesWithHashOrVersion.FirstOrDefault(p => fileNameAndExtension.StartsWith(p)); + if (filename != null && filename.EndsWith(".")) + { + filename = filename[..^1]; + } + var extension = KnownExtensions.FirstOrDefault(f => fileNameAndExtension.EndsWith(f, StringComparison.OrdinalIgnoreCase)); + if (filename != null && extension != null) + { + fileNameAndExtension = filename + extension; + } + + return fileNameAndExtension; + } + } + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCompressionIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCompressionIntegrationTest.cs index df2caf4709da..3561d6164551 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCompressionIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCompressionIntegrationTest.cs @@ -1,209 +1,626 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.IO.Compression; +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + using System.Net.Http.Headers; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { + + + [TestClass] + public class StaticWebAssetsCompressionIntegrationTest : AspNetSdkBaselineTest + + { - public StaticWebAssetsCompressionIntegrationTest(ITestOutputHelper log) : base(log, GenerateBaselines) { } - [Fact] + + + + + [TestMethod] + + public void Build_Detects_PrecompressedAssets() + + { + + var expectedManifest = LoadBuildManifest(); + + var testAsset = "RazorAppWithP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var file = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js"); + + var gzipFile = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js.gz"); + + var brotliFile = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js.br"); + + + + // Compress file into gzip and brotli + + using (var gzipStream = new GZipStream(File.Create(gzipFile), CompressionLevel.NoCompression)) + + { + + using var stream = File.OpenRead(file); + + stream.CopyTo(gzipStream); + + } + + + + using (var brotliStream = new BrotliStream(File.Create(brotliFile), CompressionLevel.NoCompression)) + + { + + using var stream = File.OpenRead(file); + + stream.CopyTo(brotliStream); + + } + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithP2PReference"); + + 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)); + + AssertManifest(manifest, expectedManifest); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "AppWithP2PReference.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + + + var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + AssertManifest(manifest1, expectedManifest); + + AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); + + + + var manifest2 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + + + var standardEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, file, StringComparison.Ordinal)).ToArray(); + + var gzipEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, gzipFile, StringComparison.Ordinal)).ToArray(); + + var brotliEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, brotliFile, StringComparison.Ordinal)).ToArray(); + + + + var gzipAsset = manifest2.Assets.Single(a => string.Equals(a.Identity, gzipFile, StringComparison.Ordinal)); + + var brotliAsset = manifest2.Assets.Single(a => string.Equals(a.Identity, brotliFile, StringComparison.Ordinal)); + + + + standardEndpoints.Should().HaveCount(1); + + gzipEndpoints.Should().HaveCount(2); + + brotliEndpoints.Should().HaveCount(2); + + + + + + foreach (var endpoint in gzipEndpoints) + + { + + endpoint.ResponseHeaders.Where(e => e.Name == "Content-Encoding").Select(e => e.Value).Single().Should().Be("gzip"); + + + + var etags = endpoint.ResponseHeaders.Where(e => e.Name == "ETag").Select(e => EntityTagHeaderValue.Parse(e.Value)); + + etags.Where(e=> !e.IsWeak).Select(e => e.Tag).Single().Should().BeEquivalentTo($"\"{gzipAsset.Integrity}\""); + + if (endpoint.Route.EndsWith(".gz")) + + { + + continue; + + } + + } + + + + foreach (var endpoint in brotliEndpoints) + + { + + endpoint.ResponseHeaders.Where(e => e.Name == "Content-Encoding").Select(e => e.Value).Single().Should().Be("br"); + + + + var etags = endpoint.ResponseHeaders.Where(e => e.Name == "ETag").Select(e => EntityTagHeaderValue.Parse(e.Value)); + + etags.Where(e => !e.IsWeak).Select(e => e.Tag).Single().Should().BeEquivalentTo($"\"{brotliAsset.Integrity}\""); + + if (endpoint.Route.EndsWith(".br")) + + { + + continue; + + } + + } + + } - [Fact] + + + + + [TestMethod] + + public void CanEnable_CompressionOnAllAssets() + + { + + var expectedManifest = LoadBuildManifest(); + + var testAsset = "RazorAppWithP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) + + .WithProjectChanges((project, xml) => + + { + + if (project.Contains("ClassLibrary")) + + { + + xml.Descendants("PropertyGroup") + + .First().Add(new XElement("StaticWebAssetBuildCompressAllAssets", "true")); + + } + + }); + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithP2PReference"); + + 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)); + + AssertManifest(manifest, expectedManifest); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "AppWithP2PReference.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + } - [Fact] + + + + + [TestMethod] + + public void PublishWorks_With_PrecompressedAssets() + + { + + var expectedManifest = LoadBuildManifest(); + + var testAsset = "RazorAppWithP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var file = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js"); + + var gzipFile = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js.gz"); + + var brotliFile = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js.br"); + + + + // Compress file into gzip and brotli + + using (var gzipStream = new GZipStream(File.Create(gzipFile), CompressionLevel.NoCompression)) + + { + + using var stream = File.OpenRead(file); + + stream.CopyTo(gzipStream); + + } + + + + using (var brotliStream = new BrotliStream(File.Create(brotliFile), CompressionLevel.NoCompression)) + + { + + using var stream = File.OpenRead(file); + + stream.CopyTo(brotliStream); + + } + + + + var build = CreatePublishCommand(ProjectDirectory, "AppWithP2PReference"); + + 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)); + + AssertManifest(manifest, expectedManifest); + + + + + + var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + AssertManifest(manifest1, expectedManifest); + + AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); + + + + var manifest2 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"))); + + + + var standardEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, file, StringComparison.Ordinal)).ToArray(); + + var gzipEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, gzipFile, StringComparison.Ordinal)).ToArray(); + + var brotliEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, brotliFile, StringComparison.Ordinal)).ToArray(); + + + + var gzipAsset = manifest2.Assets.Single(a => string.Equals(a.Identity, gzipFile, StringComparison.Ordinal)); + + var brotliAsset = manifest2.Assets.Single(a => string.Equals(a.Identity, brotliFile, StringComparison.Ordinal)); + + + + standardEndpoints.Should().HaveCount(1); + + gzipEndpoints.Should().HaveCount(2); + + brotliEndpoints.Should().HaveCount(2); + + + + + + foreach (var endpoint in gzipEndpoints) + + { + + endpoint.ResponseHeaders.Where(e => e.Name == "Content-Encoding").Select(e => e.Value).Single().Should().Be("gzip"); + + + + var etags = endpoint.ResponseHeaders.Where(e => e.Name == "ETag").Select(e => EntityTagHeaderValue.Parse(e.Value)); + + etags.Where(e => !e.IsWeak).Select(e => e.Tag).Single().Should().BeEquivalentTo($"\"{gzipAsset.Integrity}\""); + + if (endpoint.Route.EndsWith(".gz")) + + { + + continue; + + } + + } + + + + foreach (var endpoint in brotliEndpoints) + + { + + endpoint.ResponseHeaders.Where(e => e.Name == "Content-Encoding").Select(e => e.Value).Single().Should().Be("br"); + + + + var etags = endpoint.ResponseHeaders.Where(e => e.Name == "ETag").Select(e => EntityTagHeaderValue.Parse(e.Value)); + + etags.Where(e => !e.IsWeak).Select(e => e.Tag).Single().Should().BeEquivalentTo($"\"{brotliAsset.Integrity}\""); + + if (endpoint.Route.EndsWith(".br")) + + { + + continue; + + } + + } + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCrossTargetingTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCrossTargetingTests.cs index 8e00dd1f9f2f..b41d48158da1 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCrossTargetingTests.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCrossTargetingTests.cs @@ -2,108 +2,328 @@ // The .NET Foundation licenses this file to you under the MIT license. // 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { - public class StaticWebAssetsCrossTargetingTests(ITestOutputHelper log) - : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, nameof(StaticWebAssetsCrossTargetingTests)) + + + [TestClass] + + public class StaticWebAssetsCrossTargetingTests : IsolatedNuGetPackageFolderAspNetSdkBaselineTest + { + + protected override string RestoreNugetPackagePath => nameof(StaticWebAssetsCrossTargetingTests); + + // Build Standalone project - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + + + [TestMethod] + + [RequiresMSBuildVersion("17.12", Reason = "Needs System.Text.Json 8.0.5")] + + public void Build_CrosstargetingTests_CanIncludeBrowserAssets() + + { + + var expectedManifest = LoadBuildManifest(); + + var testAsset = "RazorComponentAppMultitarget"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + ProjectDirectory.WithProjectChanges(d => + + { + + d.Root.Element("PropertyGroup").Add( + + XElement.Parse("""/""")); + + + + d.Root.LastNode.AddBeforeSelf( + + XElement.Parse(""" + + + + + + browser + + + + + + """)); + + }); + + + + var wwwroot = Directory.CreateDirectory(Path.Combine(ProjectDirectory.TestRoot, "wwwroot")); + + File.WriteAllText(Path.Combine(wwwroot.FullName, "test.js"), "console.log('hello')"); + + + + 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)); + + AssertManifest(manifest, expectedManifest); + + AssertBuildAssets(manifest, outputPath, intermediateOutputPath); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "RazorComponentAppMultitarget.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + } - [Fact] + + + + + [TestMethod] + + public void Publish_CrosstargetingTests_CanIncludeBrowserAssets() + + { + + var testAsset = "RazorComponentAppMultitarget"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + ProjectDirectory.WithProjectChanges(d => + + { + + d.Root.Element("PropertyGroup").Add( + + XElement.Parse("""/""")); + + + + d.Root.LastNode.AddBeforeSelf( + + XElement.Parse(""" + + + + + + browser + + + + + + """)); + + }); + + + + var wwwroot = Directory.CreateDirectory(Path.Combine(ProjectDirectory.TestRoot, "wwwroot")); + + File.WriteAllText(Path.Combine(wwwroot.FullName, "test.js"), "console.log('hello')"); + + + + var restore = CreateRestoreCommand(ProjectDirectory); + + ExecuteCommand(restore).Should().Pass(); + + + + var publish = CreatePublishCommand(ProjectDirectory); + + ExecuteCommandWithoutRestore(publish, "/bl", "/p:TargetFramework=net11.0").Should().Pass(); + + + + var publishPath = publish.GetOutputDirectory(DefaultTfm).ToString(); + + var intermediateOutputPath = publish.GetIntermediateDirectory(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)); + + AssertManifest(manifest, LoadPublishManifest()); + + + + AssertPublishAssets( + + manifest, + + publishPath, + + intermediateOutputPath); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsDesignTimeTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsDesignTimeTest.cs index 5b57ef2b2c53..e9009e50f619 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsDesignTimeTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsDesignTimeTest.cs @@ -2,162 +2,490 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; +using Microsoft.NET.TestFramework.Commands; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System; + + using System.Collections.Generic; + + using System.Linq; + + using System.Text; + + using System.Threading.Tasks; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; -public class StaticWebAssetsDesignTimeTest(ITestOutputHelper log) : AspNetSdkBaselineTest(log) + + + + +[TestClass] + +public class StaticWebAssetsDesignTimeTest : AspNetSdkBaselineTest + + { + + #if DEBUG + + public const string Configuration = "Debug"; + + #else + + public const string Configuration = "Release"; + + #endif - [Fact] + + + + + [TestMethod] + + public void CollectUpToDateCheckInputOutputsDesignTime_ReportsAddedFiles() + + { + + // Arrange + + var testAsset = "RazorAppWithP2PReference"; + + ProjectDirectory = AddIntrospection(CreateAspNetSdkTestAsset(testAsset)); + + + + var build = CreateBuildCommand(ProjectDirectory, "ClassLibrary"); + + + + build.Execute("/p:DesignTimeBuild=true", "/p:BuildingInsideVisualStudio=true", "/bl:build.binlog").Should().Pass(); + + + + File.WriteAllText(Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "file.js"), "New File"); + + + + var msbuild = CreateMSBuildCommand( + + ProjectDirectory, + + "ClassLibrary", + + "ResolveStaticWebAssetsConfiguration;ResolveProjectStaticWebAssets;CollectStaticWebAssetInputsDesignTime;CollectStaticWebAssetOutputsDesignTime"); + + + + msbuild.ExecuteWithoutRestore("/p:DesignTimeBuild=true", "/p:BuildingInsideVisualStudio=true", "/bl:design.binlog").Should().Pass(); + + + + // Check the contents of the input and output files + + var inputFilePath = Path.Combine(build.GetIntermediateDirectory().FullName, "StaticWebAssetsUTDCInput.txt"); + + new FileInfo(inputFilePath).Should().Exist(); + + var inputFiles = File.ReadAllLines(inputFilePath); + + inputFiles.Should().HaveCount(3); + + inputFiles.Should().Contain(Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "file.js")); + + inputFiles.Should().Contain(Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js")); + + inputFiles.Should().Contain(Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.v4.js")); + + + + var outputFilePath = Path.Combine(build.GetIntermediateDirectory().FullName, "StaticWebAssetsUTDCOutput.txt"); + + new FileInfo(outputFilePath).Should().Exist(); + + var outputFiles = File.ReadAllLines(outputFilePath); + + outputFiles.Should().ContainSingle(); + + Path.GetFileName(outputFiles[0]).Should().Be("staticwebassets.build.json"); + + } - [Fact] + + + + + [TestMethod] + + public void CollectUpToDateCheckInputOutputsDesignTime_ReportsRemovedFiles_Once() + + { + + // Arrange + + var testAsset = "RazorAppWithP2PReference"; + + ProjectDirectory = AddIntrospection(CreateAspNetSdkTestAsset(testAsset)); + + + + var build = CreateBuildCommand(ProjectDirectory, "ClassLibrary"); + + + + build.Execute("/p:DesignTimeBuild=true", "/p:BuildingInsideVisualStudio=true", "/bl:build.binlog").Should().Pass(); + + + + File.Delete(Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js")); + + + + var msbuild = CreateMSBuildCommand( + + ProjectDirectory, + + "ClassLibrary", + + "ResolveStaticWebAssetsConfiguration;ResolveProjectStaticWebAssets;CollectStaticWebAssetInputsDesignTime;CollectStaticWebAssetOutputsDesignTime"); + + + + msbuild.ExecuteWithoutRestore("/p:DesignTimeBuild=true", "/p:BuildingInsideVisualStudio=true", "/bl:design.binlog").Should().Pass(); + + + + // Check the contents of the input and output files + + var inputFilePath = Path.Combine(build.GetIntermediateDirectory().FullName, "StaticWebAssetsUTDCInput.txt"); + + new FileInfo(inputFilePath).Should().Exist(); + + var inputFiles = File.ReadAllLines(inputFilePath); + + inputFiles.Should().HaveCount(2); + + inputFiles.Should().Contain(Path.Combine(build.GetIntermediateDirectory().FullName, "staticwebassets.removed.txt")); + + inputFiles.Should().Contain(Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.v4.js")); + + + + var outputFilePath = Path.Combine(build.GetIntermediateDirectory().FullName, "StaticWebAssetsUTDCOutput.txt"); + + new FileInfo(outputFilePath).Should().Exist(); + + var outputFiles = File.ReadAllLines(outputFilePath); + + outputFiles.Should().ContainSingle(); + + Path.GetFileName(outputFiles[0]).Should().Be("staticwebassets.build.json"); + + } - [Fact] + + + + + [TestMethod] + + public void CollectUpToDateCheckInputOutputsDesignTime_IncludesReferencedProjectsManifests() + + { + + // Arrange + + var testAsset = "RazorAppWithP2PReference"; + + ProjectDirectory = AddIntrospection(CreateAspNetSdkTestAsset(testAsset)); + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithP2PReference"); + + + + build.Execute("/bl:build.binlog").Should().Pass(); + + build.Execute("/p:DesignTimeBuild=true", "/p:BuildingInsideVisualStudio=true", "/bl:build.binlog").Should().Pass(); + + + + var msbuild = CreateMSBuildCommand( + + ProjectDirectory, + + "AppWithP2PReference", + + "ResolveStaticWebAssetsConfiguration;ResolveProjectStaticWebAssets;CollectStaticWebAssetInputsDesignTime;CollectStaticWebAssetOutputsDesignTime"); + + + + msbuild.ExecuteWithoutRestore("/p:DesignTimeBuild=true", "/p:BuildingInsideVisualStudio=true", "/bl:design.binlog").Should().Pass(); + + + + // Check the contents of the input and output files + + var inputFilePath = Path.Combine(build.GetIntermediateDirectory().FullName, "StaticWebAssetsUTDCInput.txt"); + + new FileInfo(inputFilePath).Should().Exist(); + + var inputFiles = File.ReadAllLines(inputFilePath); + + inputFiles.Should().HaveCount(1); + + inputFiles.Should().Contain(Path.Combine(ProjectDirectory.Path, "ClassLibrary", "obj", "Debug", DefaultTfm, "staticwebassets.build.json")); + + + + var outputFilePath = Path.Combine(build.GetIntermediateDirectory().FullName, "StaticWebAssetsUTDCOutput.txt"); + + new FileInfo(outputFilePath).Should().Exist(); + + var outputFiles = File.ReadAllLines(outputFilePath); + + outputFiles.Should().ContainSingle(); + + Path.GetFileName(outputFiles[0]).Should().Be("staticwebassets.build.json"); + + } + + + + private static MSBuildCommand CreateMSBuildCommand(TestAsset testAsset, string relativeProjectPath, string targets) + + { + + return (MSBuildCommand)new MSBuildCommand(testAsset.Log, targets, testAsset.TestRoot, relativeProjectPath) + + .WithWorkingDirectory(testAsset.TestRoot); + + } + + + + private static TestAsset AddIntrospection(TestAsset testAsset) + + { + + return testAsset + + .WithProjectChanges((name, project) => + + { + + project.Document.Root.LastNode.AddAfterSelf( + + XElement.Parse(""" + + + + + + <_StaticWebAssetsUTDCInput Include="@(UpToDateCheckInput)" Condition="'%(UpToDateCheckInput.Set)' == 'StaticWebAssets'" /> + + <_StaticWebAssetsUTDCOutput Include="@(UpToDateCheckOutput)" Condition="'%(UpToDateCheckOutput.Set)' == 'StaticWebAssets'" /> + + + + + + + + + + + + + + """ + + )); + + }); + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsFingerprintingTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsFingerprintingTest.cs index 741aad03ed4f..07160373e115 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsFingerprintingTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsFingerprintingTest.cs @@ -2,266 +2,802 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + using System.Text.Json; + + using System.IO.Compression; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; -public class StaticWebAssetsContentFingerprintingIntegrationTest(ITestOutputHelper log) : AspNetSdkBaselineTest(log) + + + + +[TestClass] + +public class StaticWebAssetsContentFingerprintingIntegrationTest : AspNetSdkBaselineTest + + { - [Fact] + + + [TestMethod] + + public void Build_FingerprintsContent_WhenEnabled() + + { + + var expectedManifest = LoadBuildManifest(); + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) + + .WithProjectChanges(p => { + + var fingerprintContent = p.Descendants() + + .SingleOrDefault(e => e.Name.LocalName == "StaticWebAssetsFingerprintContent"); + + fingerprintContent.Value = "true"; + + }); + + + + Directory.CreateDirectory(Path.Combine(ProjectDirectory.Path, "wwwroot", "css")); + + File.WriteAllText(Path.Combine(ProjectDirectory.Path, "wwwroot", "css", "fingerprint-site.css"), "body { color: red; }"); + + + + 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)); + + AssertManifest(manifest, expectedManifest); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + + + var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + AssertManifest(manifest1, expectedManifest); + + AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); + + } + + + + public static TheoryData OverrideHtmlAssetPlaceholdersData => new TheoryData + + { + + { "VanillaWasm", "main.js", "main#[.{fingerprint}].js", true, true }, + + { "VanillaWasm", "main.js", null, false, false }, + + { "BlazorWasmMinimal", "_framework/blazor.webassembly.js", "_framework/blazor.webassembly#[.{fingerprint}].js", false, true } + + }; - [Theory] - [MemberData(nameof(OverrideHtmlAssetPlaceholdersData))] + + + + + [TestMethod] + + + [DynamicData(nameof(OverrideHtmlAssetPlaceholdersData))] + + public void Build_OverrideHtmlAssetPlaceholders(string testAsset, string scriptPath, string scriptPathWithFingerprintPattern, bool fingerprintUserJavascriptAssets, bool expectFingerprintOnScript) + + { + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset, identifier: $"{testAsset}_{fingerprintUserJavascriptAssets}_{expectFingerprintOnScript}"); + + ReplaceStringInIndexHtml(ProjectDirectory, scriptPath, scriptPathWithFingerprintPattern); + + FingerprintUserJavascriptAssets(fingerprintUserJavascriptAssets); + + + + var build = CreateBuildCommand(ProjectDirectory); + + ExecuteCommand(build, "-p:OverrideHtmlAssetPlaceholders=true", $"-p:FingerprintUserJavascriptAssets={fingerprintUserJavascriptAssets.ToString().ToLower()}").Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var indexHtmlPath = Directory.EnumerateFiles(Path.Combine(intermediateOutputPath, "staticwebassets", "htmlassetplaceholders", "build"), "*.html").Single(); + + var endpointsManifestPath = Path.Combine(intermediateOutputPath, $"staticwebassets.build.endpoints.json"); + + + + AssertImportMapInHtml(indexHtmlPath, endpointsManifestPath, scriptPath, expectFingerprintOnScript: expectFingerprintOnScript, expectPreloadElement: testAsset == "VanillaWasm"); + + } - [Fact] + + + + + [TestMethod] + + public void Build_OverrideHtmlAssetPlaceholders_PreservesAdditionalEndpointDefinitions() + + { + + ProjectDirectory = CreateAspNetSdkTestAsset("VanillaWasm", identifier: nameof(Build_OverrideHtmlAssetPlaceholders_PreservesAdditionalEndpointDefinitions)); + + EnableDefaultDocumentAndSpaFallback(); + + ReplaceStringInIndexHtml(ProjectDirectory, "main.js", "main#[.{fingerprint}].js"); + + FingerprintUserJavascriptAssets(true); + + + + var build = CreateBuildCommand(ProjectDirectory); + + ExecuteCommand(build, "-p:OverrideHtmlAssetPlaceholders=true", "-p:FingerprintUserJavascriptAssets=true").Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var endpointsManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.endpoints.json"); + + AssertAdditionalEndpointDefinitionsExist(endpointsManifestPath); + + } - [Theory] - [MemberData(nameof(OverrideHtmlAssetPlaceholdersData))] + + + + + [TestMethod] + + + [DynamicData(nameof(OverrideHtmlAssetPlaceholdersData))] + + public void Publish_OverrideHtmlAssetPlaceholders(string testAsset, string scriptPath, string scriptPathWithFingerprintPattern, bool fingerprintUserJavascriptAssets, bool expectFingerprintOnScript) + + { + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset, identifier: $"{testAsset}_{fingerprintUserJavascriptAssets}_{expectFingerprintOnScript}"); + + ReplaceStringInIndexHtml(ProjectDirectory, scriptPath, scriptPathWithFingerprintPattern); + + FingerprintUserJavascriptAssets(fingerprintUserJavascriptAssets); + + + + var projectName = Path.GetFileNameWithoutExtension(Directory.EnumerateFiles(ProjectDirectory.TestRoot, "*.csproj").Single()); + + + + var publish = CreatePublishCommand(ProjectDirectory); + + ExecuteCommand(publish, "-p:OverrideHtmlAssetPlaceholders=true", $"-p:FingerprintUserJavascriptAssets={fingerprintUserJavascriptAssets.ToString().ToLower()}").Should().Pass(); + + + + var outputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + var indexHtmlOutputPath = Path.Combine(outputPath, "wwwroot", "index.html"); + + var endpointsManifestPath = Path.Combine(outputPath, $"{projectName}.staticwebassets.endpoints.json"); + + + + AssertImportMapInHtml(indexHtmlOutputPath, endpointsManifestPath, scriptPath, expectFingerprintOnScript: expectFingerprintOnScript, expectPreloadElement: testAsset == "VanillaWasm", assertHtmlCompressed: true); + + } - [Fact] + + + + + [TestMethod] + + public void Publish_OverrideHtmlAssetPlaceholders_PreservesAdditionalEndpointDefinitions() + + { + + ProjectDirectory = CreateAspNetSdkTestAsset("VanillaWasm", identifier: nameof(Publish_OverrideHtmlAssetPlaceholders_PreservesAdditionalEndpointDefinitions)); + + EnableDefaultDocumentAndSpaFallback(); + + ReplaceStringInIndexHtml(ProjectDirectory, "main.js", "main#[.{fingerprint}].js"); + + FingerprintUserJavascriptAssets(true); + + + + var projectName = Path.GetFileNameWithoutExtension(Directory.EnumerateFiles(ProjectDirectory.TestRoot, "*.csproj").Single()); + + var publish = CreatePublishCommand(ProjectDirectory); + + ExecuteCommand(publish, "-p:OverrideHtmlAssetPlaceholders=true", "-p:FingerprintUserJavascriptAssets=true").Should().Pass(); + + + + var outputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + var endpointsManifestPath = Path.Combine(outputPath, $"{projectName}.staticwebassets.endpoints.json"); + + AssertAdditionalEndpointDefinitionsExist(endpointsManifestPath); + + } + + + + private void EnableDefaultDocumentAndSpaFallback() + + { + + ProjectDirectory.WithProjectChanges(p => + + { + + if (p.Root != null) + + { + + p.Root.AddFirst( + + new XElement("PropertyGroup", + + new XElement("StaticWebAssetDefaultDocumentEnabled", "true"), + + new XElement("StaticWebAssetSpaFallbackEnabled", "true"))); + + } + + }); + + } + + + + private static void AssertAdditionalEndpointDefinitionsExist(string endpointsManifestPath) + + { + + var endpoints = JsonSerializer.Deserialize(File.ReadAllText(endpointsManifestPath)); + + endpoints.Should().NotBeNull(); + + + + var indexEndpoint = endpoints.Endpoints.Single(e => e.Route == "index.html" && e.Selectors.Length == 0); + + var defaultDocumentEndpoint = endpoints.Endpoints.Single(e => e.Route == "/" && e.Selectors.Length == 0); + + var spaFallbackEndpoint = endpoints.Endpoints.Single(e => e.Route == "{**fallback:nonfile}" && e.Selectors.Length == 0); + + + + defaultDocumentEndpoint.AssetFile.Should().Be(indexEndpoint.AssetFile); + + spaFallbackEndpoint.AssetFile.Should().Be(indexEndpoint.AssetFile); + + spaFallbackEndpoint.Order.Should().Be("2147483647"); + + } + + + + private void FingerprintUserJavascriptAssets(bool fingerprintUserJavascriptAssets) + + { + + if (fingerprintUserJavascriptAssets) + + { + + ProjectDirectory.WithProjectChanges(p => + + { + + if (p.Root != null) + + { + + var itemGroup = new XElement("ItemGroup"); + + var pattern = new XElement("StaticWebAssetFingerprintPattern"); + + pattern.SetAttributeValue("Include", "Js"); + + pattern.SetAttributeValue("Pattern", "*.js"); + + pattern.SetAttributeValue("Expression", "#[.{fingerprint}]!"); + + itemGroup.Add(pattern); + + p.Root.Add(itemGroup); + + } + + }); + + } + + } + + + + private void ReplaceStringInIndexHtml(TestAsset testAsset, string sourceValue, string targetValue) + + { + + if (targetValue != null) + + { + + var indexHtmlPath = Path.Combine(testAsset.TestRoot, "wwwroot", "index.html"); + + var indexHtmlContent = File.ReadAllText(indexHtmlPath); + + var newIndexHtmlContent = indexHtmlContent.Replace(sourceValue, targetValue); + + if (indexHtmlContent == newIndexHtmlContent) + + throw new Exception($"String replacement '{sourceValue}' for '{targetValue}' didn't produce any change in '{indexHtmlPath}'"); + + + + File.WriteAllText(indexHtmlPath, newIndexHtmlContent); + + } + + } + + + + private void AssertImportMapInHtml(string indexHtmlPath, string endpointsManifestPath, string scriptPath, bool expectFingerprintOnScript = true, bool expectPreloadElement = false, bool assertHtmlCompressed = false) + + { + + + + var endpoints = JsonSerializer.Deserialize(File.ReadAllText(endpointsManifestPath)); + + var fingerprintedScriptPath = GetFingerprintedPath(scriptPath); + + + + var indexHtmlContent = File.ReadAllText(indexHtmlPath); + + AssertHtmlContent(indexHtmlContent); + + + + if (assertHtmlCompressed) + + { + + var indexHtmlGzipContent = DecompressGzipFile(indexHtmlPath + ".gz"); + + AssertHtmlContent(indexHtmlGzipContent); + + + + var indexHtmlBrotliContent = DecompressBrotliFile(indexHtmlPath + ".br"); + + AssertHtmlContent(indexHtmlBrotliContent); + + } + + + + void AssertHtmlContent(string content) + + { + + if (expectFingerprintOnScript) + + { + + Assert.DoesNotContain($"src=\"{scriptPath}\"", content); + + Assert.Contains($"src=\"{fingerprintedScriptPath}\"", content); + + } + + else + + { + + Assert.Contains(scriptPath, content); + + + + if (scriptPath != fingerprintedScriptPath) + + { + + Assert.DoesNotContain(fingerprintedScriptPath, content); + + } + + } + + + + Assert.Contains(GetFingerprintedPath("_framework/dotnet.js"), content); + + Assert.Contains(GetFingerprintedPath("_framework/dotnet.native.js"), content); + + Assert.Contains(GetFingerprintedPath("_framework/dotnet.runtime.js"), content); + + + + if (expectPreloadElement) + + { + + Assert.DoesNotContain(" endpoints.Endpoints.FirstOrDefault(e => e.Route == route && e.Selectors.Length == 0)?.AssetFile ?? throw new Exception($"Missing endpoint for file '{route}' in '{endpointsManifestPath}'"); + + + + string DecompressGzipFile(string path) + + { + + if (File.Exists(path)) + + { + + using var fileStream = File.OpenRead(path); + + using var compressedStream = new GZipStream(fileStream, CompressionMode.Decompress); + + using var reader = new StreamReader(compressedStream); + + return reader.ReadToEnd(); + + } + + + + Assert.Fail($"File '{path}' does not exist."); + + return null; + + } + + + + string DecompressBrotliFile(string path) + + { + + if (File.Exists(path)) + + { + + using var fileStream = File.OpenRead(path); + + using var compressedStream = new BrotliStream(fileStream, CompressionMode.Decompress); + + using var reader = new StreamReader(compressedStream); + + return reader.ReadToEnd(); + + } + + + + Assert.Fail($"File '{path}' does not exist."); + + return null; + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs index c13c8b2d5c6b..0d51d1fd69c5 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs @@ -1,976 +1,2927 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + using System.Reflection; + + using Microsoft.AspNetCore.StaticWebAssets.Tasks; + + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { + + + [TestClass] + public class StaticWebAssetsIntegrationTest : AspNetSdkBaselineTest + + { - public StaticWebAssetsIntegrationTest(ITestOutputHelper log) : base(log, GenerateBaselines) { } + + + + // Build Standalone project - [Fact] + + + [TestMethod] + + public void Build_GeneratesJsonManifestAndCopiesItToOutputFolder() + + { + + var expectedManifest = LoadBuildManifest(); + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + 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)); + + AssertManifest(manifest, expectedManifest); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + + + var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + AssertManifest(manifest1, expectedManifest); + + AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void Build_Can_DisableAssetCaching() + + { + + var expectedManifest = LoadBuildManifest(); + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var build = CreateBuildCommand(ProjectDirectory); + + ExecuteCommand(build, "/p:StaticWebAssetsCacheDefineStaticWebAssetsEnabled=false").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)); + + AssertManifest(manifest, expectedManifest); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + + + // The caches shouldn't exist. + + // Manifest + + new FileInfo(Path.Combine(intermediateOutputPath, "rpswa.dswa.cache.json")).Should().NotExist(); + + // Compressed assets + + new FileInfo(Path.Combine(intermediateOutputPath, "rbcswa.dswa.cache.json")).Should().NotExist(); + + // Initializers + + new FileInfo(Path.Combine(intermediateOutputPath, "rjimswa.dswa.cache.json")).Should().NotExist(); + + // JS Modules + + new FileInfo(Path.Combine(intermediateOutputPath, "rjsmcshtml.dswa.cache.json")).Should().NotExist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "rjsmrazor.dswa.cache.json")).Should().NotExist(); + + + + var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + AssertManifest(manifest1, expectedManifest); + + AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void Build_DoesNotUpdateManifest_WhenHasNotChanged() + + { + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + 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"); + + var originalObjFile = new FileInfo(path); + + originalObjFile.Should().Exist(); + + var objManifestContents = File.ReadAllText(Path.Combine(intermediateOutputPath, "staticwebassets.build.json")); + + AssertManifest( + + StaticWebAssetsManifest.FromJsonString(objManifestContents), + + LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); + + var originalFile = new FileInfo(finalPath); + + originalFile.Should().Exist(); + + var binManifestContents = File.ReadAllText(finalPath); + + + + var secondBuild = CreateBuildCommand(ProjectDirectory); + + secondBuild.Execute().Should().Pass(); + + + + var secondPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + var secondObjFile = new FileInfo(secondPath); + + secondObjFile.Should().Exist(); + + var secondObjManifest = File.ReadAllText(secondPath); + + secondObjManifest.Should().Be(objManifestContents); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var secondFinalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); + + var secondFinalFile = new FileInfo(secondFinalPath); + + secondFinalFile.Should().Exist(); + + var secondBinManifest = File.ReadAllText(secondFinalPath); + + secondBinManifest.Should().Be(binManifestContents); + + + + secondFinalFile.LastWriteTimeUtc.Should().Be(originalFile.LastWriteTimeUtc); + + } - [Fact] + + + + + [TestMethod] + + public void Build_UpdatesManifest_WhenFilesChange() + + { + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + 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"); + + var originalObjFile = new FileInfo(path); + + originalObjFile.Should().Exist(); + + var objManifestContents = File.ReadAllText(Path.Combine(intermediateOutputPath, "staticwebassets.build.json")); + + var firstManifest = StaticWebAssetsManifest.FromJsonString(objManifestContents); + + AssertManifest(firstManifest, LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); + + var originalFile = new FileInfo(finalPath); + + originalFile.Should().Exist(); + + var binManifestContents = File.ReadAllText(finalPath); + + + + AssertBuildAssets( + + firstManifest, + + outputPath, + + intermediateOutputPath); + + + + // Second build + + Directory.CreateDirectory(Path.Combine(ProjectDirectory.Path, "wwwroot")); + + File.WriteAllText(Path.Combine(ProjectDirectory.Path, "wwwroot", "index.html"), "some html"); + + + + var secondBuild = CreateBuildCommand(ProjectDirectory); + + secondBuild.Execute().Should().Pass(); + + + + var secondPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + var secondObjFile = new FileInfo(secondPath); + + secondObjFile.Should().Exist(); + + var secondObjManifest = File.ReadAllText(secondPath); + + var secondManifest = StaticWebAssetsManifest.FromJsonString(secondObjManifest); + + AssertManifest( + + secondManifest, + + LoadBuildManifest("Updated"), + + "Updated"); + + + + secondObjManifest.Should().NotBe(objManifestContents); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var secondFinalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); + + var secondFinalFile = new FileInfo(secondFinalPath); + + secondFinalFile.Should().Exist(); + + var secondBinManifest = File.ReadAllText(secondFinalPath); + + secondBinManifest.Should().NotBe(binManifestContents); + + + + secondObjFile.LastWriteTimeUtc.Should().NotBe(originalObjFile.LastWriteTimeUtc); + + secondFinalFile.LastWriteTimeUtc.Should().NotBe(originalFile.LastWriteTimeUtc); + + + + AssertBuildAssets( + + secondManifest, + + outputPath, + + intermediateOutputPath, + + "Updated"); + + } + + + + // Rebuild - [Fact] + + + [TestMethod] + + public void Rebuild_RegeneratesJsonManifestAndCopiesItToOutputFolder() + + { + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + 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"); + + var originalObjFile = new FileInfo(path); + + originalObjFile.Should().Exist(); + + var objManifestContents = File.ReadAllText(Path.Combine(intermediateOutputPath, "staticwebassets.build.json")); + + AssertManifest(StaticWebAssetsManifest.FromJsonString(objManifestContents), LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); + + var originalFile = new FileInfo(finalPath); + + originalFile.Should().Exist(); + + var binManifestContents = File.ReadAllText(finalPath); + + + + // rebuild build + + var rebuild = CreateRebuildCommand(ProjectDirectory); + + ExecuteCommand(rebuild).Should().Pass(); + + + + var secondPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + var secondObjFile = new FileInfo(secondPath); + + secondObjFile.Should().Exist(); + + var secondObjManifestContents = File.ReadAllText(secondPath); + + var secondManifest = StaticWebAssetsManifest.FromJsonString(secondObjManifestContents); + + AssertManifest( + + secondManifest, + + LoadBuildManifest("Rebuild"), + + "Rebuild"); + + + + // This is no longer true because the manifests include the timestamp for the last modified + + // time of the file, etc. + + //secondObjManifestContents.Should().Be(objManifestContents); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var secondFinalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); + + var secondFinalFile = new FileInfo(secondFinalPath); + + secondFinalFile.Should().Exist(); + + var secondBinManifest = File.ReadAllText(secondFinalPath); + + secondBinManifest.Should().Be(binManifestContents); + + + + secondObjFile.LastWriteTimeUtc.Should().NotBe(originalObjFile.LastWriteTimeUtc); + + secondFinalFile.LastWriteTimeUtc.Should().NotBe(originalFile.LastWriteTimeUtc); + + + + AssertBuildAssets( + + secondManifest, + + outputPath, + + intermediateOutputPath, + + "Rebuild"); + + } + + + + // Publish - [Fact] + + + [TestMethod] + + public void Publish_GeneratesPublishJsonManifestAndCopiesPublishAssets() + + { + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var publish = CreatePublishCommand(ProjectDirectory); + + ExecuteCommand(publish).Should().Pass(); + + + + var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var publishPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + // GenerateStaticWebAssetsManifest should generate the build manifest file. + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + new FileInfo(path).Should().Exist(); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + AssertManifest(manifest, LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(publishPath, "ComponentApp.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().NotExist(); + + + + // GenerateStaticWebAssetsManifest should generate the publish manifest file. + + var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + new FileInfo(path).Should().Exist(); + + var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); + + AssertManifest(publishManifest, LoadPublishManifest()); + + + + AssertPublishAssets( + + publishManifest, + + publishPath, + + intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void Publish_PublishSingleFile_GeneratesPublishJsonManifestAndCopiesPublishAssets() + + { + + var expectedManifest = LoadBuildManifest(); + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var publish = CreatePublishCommand(ProjectDirectory); + + ExecuteCommand(publish, "/p:PublishSingleFile=true", $"/p:RuntimeIdentifier={RuntimeInformation.RuntimeIdentifier}").Should().Pass(); + + + + var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug", RuntimeInformation.RuntimeIdentifier).ToString(); + + var publishPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + // GenerateStaticWebAssetsManifest should generate the build manifest file. + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + new FileInfo(path).Should().Exist(); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + AssertManifest(manifest, expectedManifest, runtimeIdentifier: RuntimeInformation.RuntimeIdentifier); + + + + // GenerateStaticWebAssetsManifest should not copy the file to the output folder. + + var finalPath = Path.Combine(publishPath, "ComponentApp.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().NotExist(); + + + + // GenerateStaticWebAssetsManifest should generate the publish manifest file. + + var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + new FileInfo(path).Should().Exist(); + + var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); + + AssertManifest( + + publishManifest, + + LoadPublishManifest(), + + runtimeIdentifier: RuntimeInformation.RuntimeIdentifier); + + + + AssertPublishAssets( + + publishManifest, + + publishPath, + + intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void Publish_NoBuild_GeneratesPublishJsonManifestAndCopiesPublishAssets() + + { + + var expectedManifest = LoadBuildManifest(); + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - var build = CreateBuildCommand(ProjectDirectory); + + + + + var build = CreateBuildCommand(ProjectDirectory); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var publishPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + // GenerateStaticWebAssetsManifest should generate the manifest file. + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + var objManifestFile = new FileInfo(path); + + objManifestFile.Should().Exist(); + + var objManifestFileTimeStamp = objManifestFile.LastWriteTimeUtc; + + + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + AssertManifest(manifest, expectedManifest); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(publishPath, "ComponentApp.staticwebassets.runtime.json"); + + var binManifestFile = new FileInfo(finalPath); + + binManifestFile.Should().Exist(); + + var binManifestTimeStamp = binManifestFile.LastWriteTimeUtc; + + + + var finalManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + AssertManifest(finalManifest, expectedManifest); + + + + // Publish no build + + + + var publish = CreatePublishCommand(ProjectDirectory); + + ExecuteCommand(publish, "/p:NoBuild=true").Should().Pass(); + + + + var secondObjTimeStamp = new FileInfo(path).LastWriteTimeUtc; + + + + secondObjTimeStamp.Should().Be(objManifestFileTimeStamp); + + + + var seconbObjManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + AssertManifest(seconbObjManifest, expectedManifest); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var seconBinManifestPath = Path.Combine(publishPath, "ComponentApp.staticwebassets.runtime.json"); + + var secondBinManifestFile = new FileInfo(seconBinManifestPath); + + secondBinManifestFile.Should().Exist(); + + + + secondBinManifestFile.LastWriteTimeUtc.Should().Be(binManifestTimeStamp); + + + + var secondBinManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + AssertManifest(secondBinManifest, expectedManifest); + + + + // GenerateStaticWebAssetsManifest should generate the publish manifest file. + + var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + new FileInfo(path).Should().Exist(); + + var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); + + AssertManifest( + + publishManifest, + + LoadPublishManifest()); + + + + AssertPublishAssets( + + publishManifest, + + publishPath, + + intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void Build_DeployOnBuild_GeneratesPublishJsonManifestAndCopiesPublishAssets() + + { + + var expectedManifest = LoadBuildManifest(); + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var build = CreateBuildCommand(ProjectDirectory); + + build.Execute("/p:DeployOnBuild=true").Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + // GenerateStaticWebAssetsManifest should generate the build manifest file. + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + new FileInfo(path).Should().Exist(); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + AssertManifest(manifest, LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + + + // GenerateStaticWebAssetsManifest should generate the publish manifest file. + + var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + new FileInfo(path).Should().Exist(); + + var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); + + AssertManifest(publishManifest, LoadPublishManifest()); + + + + AssertPublishAssets( + + publishManifest, + + Path.Combine(outputPath, "publish"), + + intermediateOutputPath); + + } + + + + // Clean - [Fact] + + + [TestMethod] + + public void Clean_RemovesManifestFrom_BuildAndIntermediateOutput() + + { + + var expectedManifest = LoadBuildManifest(); + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + 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)); + + AssertManifest(manifest, expectedManifest); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + var finalManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + AssertManifest(finalManifest, expectedManifest); + + + + var clean = new CleanCommand(Log, ProjectDirectory.Path); + + clean.Execute().Should().Pass(); + + + + // Obj folder manifest does not exist + + new FileInfo(path).Should().NotExist(); - // Bin folder manifest does not exist + + + + + // Bin folder manifest does not exist + + new FileInfo(finalPath).Should().NotExist(); + + } - [Fact] + + + + + [TestMethod] + + public void Publish_WithExternalProjectReference_UpdatesAssets() + + { + + var testAsset = "RazorAppWithP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) + + .WithProjectChanges((name, project) => + + { + + if (Path.GetFileName(name).Equals("ClassLibrary.csproj", StringComparison.Ordinal)) + + { + + var sdkAttribute = project.Root.Attribute("Sdk"); + + if (sdkAttribute == null) + + { + + sdkAttribute = new XAttribute("Sdk", "Microsoft.NET.Sdk"); + + project.Root.Add(sdkAttribute); + + } + + else + + { + + sdkAttribute.Value = "Microsoft.NET.Sdk"; + + } + + project.Root.AddFirst(new XElement("Import", new XAttribute("Project", "ExternalStaticAssets.targets"))); + + + + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Microsoft.NET.Sdk.StaticWebAssets.Tests.content.ExternalStaticAssets.targets"); + + using var destination = File.OpenWrite(Path.Combine(Path.GetDirectoryName(name), "ExternalStaticAssets.targets")); + + stream.CopyTo(destination); + + } + + }); + + + + var publish = CreatePublishCommand(ProjectDirectory, "AppWithP2PReference"); + + ExecuteCommand(publish).Should().Pass(); + + + + var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var publishPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + // GenerateStaticWebAssetsManifest should generate the build manifest file. + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + new FileInfo(path).Should().Exist(); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + AssertManifest(manifest, LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(publishPath, "ComponentApp.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().NotExist(); + + + + // GenerateStaticWebAssetsManifest should generate the publish manifest file. + + var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + new FileInfo(path).Should().Exist(); + + var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); + + AssertManifest(publishManifest, LoadPublishManifest()); + + + + AssertPublishAssets( + + publishManifest, + + publishPath, + + intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void Build_WithExternalProjectReference_UpdatesAssets() + + { + + var testAsset = "RazorAppWithP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) + + .WithProjectChanges((name, project) => + + { + + if (Path.GetFileName(name).Equals("ClassLibrary.csproj", StringComparison.Ordinal)) + + { + + var sdkAttribute = project.Root.Attribute("Sdk"); + + if (sdkAttribute == null) + + { + + sdkAttribute = new XAttribute("Sdk", "Microsoft.NET.Sdk"); + + project.Root.Add(sdkAttribute); + + } + + else + + { + + sdkAttribute.Value = "Microsoft.NET.Sdk"; + + } + + project.Root.AddFirst(new XElement("Import", new XAttribute("Project", "ExternalStaticAssets.targets"))); + + + + using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Microsoft.NET.Sdk.StaticWebAssets.Tests.content.ExternalStaticAssets.targets"); + + using var destination = File.OpenWrite(Path.Combine(Path.GetDirectoryName(name), "ExternalStaticAssets.targets")); + + stream.CopyTo(destination); + + } + + + + if (Path.GetFileName(name).Equals("AppWithP2PReference.csproj", StringComparison.Ordinal)) + + { + + project.Root.AddFirst(new XElement("ItemGroup", + + new XElement( + + "StaticWebAssetFingerprintInferenceExpression", + + new XAttribute("Include", "Version"), + + new XAttribute("Pattern", ".*(?v\\d{1})\\.js$")))); + + } + + }); + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithP2PReference"); + + ExecuteCommand(build).Should().Pass(); + + + + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var buildPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + // GenerateStaticWebAssetsManifest should generate the build manifest file. + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + new FileInfo(path).Should().Exist(); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + AssertManifest(manifest, LoadBuildManifest()); + + + + AssertBuildAssets( + + manifest, + + buildPath, + + intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void Build_DoesNotFailToCompress_TwoAssetsWith_TheSameContent() + + { + + var expectedManifest = LoadBuildManifest(); + + var testAsset = "RazorComponentApp"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) + + .WithProjectChanges(document => + + { + + document.Root.AddFirst(new XElement("ItemGroup", + + new XElement("Content", + + new XAttribute("Update", "wwwroot\\file.build.txt"), + + new XAttribute("TargetPath", "wwwroot\\file.txt"), + + new XAttribute("CopyToPublishDirectory", "Never")), + + new XElement("Content", + + new XAttribute("Update", "wwwroot\\file.publish.txt"), + + new XAttribute("TargetPath", "wwwroot\\file.txt"), + + new XAttribute("CopyToOutputDirectory", "Never")))); + + }); + + + + Directory.CreateDirectory(Path.Combine(ProjectDirectory.Path, "wwwroot")); + + File.WriteAllText(Path.Combine(ProjectDirectory.Path, "wwwroot", "file.build.txt"), "file1"); + + File.WriteAllText(Path.Combine(ProjectDirectory.Path, "wwwroot", "file.publish.txt"), "file1"); + + + + 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)); + + AssertManifest(manifest, expectedManifest); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + + + var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + AssertManifest(manifest1, expectedManifest); + + AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); + + } + + } - public class StaticWebAssetsAppWithPackagesIntegrationTest(ITestOutputHelper log) - : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, nameof(StaticWebAssetsAppWithPackagesIntegrationTest)) + + [TestClass] + + + + + public class StaticWebAssetsAppWithPackagesIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest + { - [Fact] + + protected override string RestoreNugetPackagePath => nameof(StaticWebAssetsAppWithPackagesIntegrationTest); + + + [TestMethod] + + public void Build_Fails_WhenConflictingAssetsFoundBetweenAStaticWebAssetAndAFileInTheWebRootFolder() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + Directory.CreateDirectory(Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "wwwroot", "_content", "ClassLibrary", "js")); + + File.WriteAllText(Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "wwwroot", "_content", "ClassLibrary", "js", "project-transitive-dep.js"), "console.log('transitive-dep');"); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(build).Should().Fail(); + + } - [Fact] + + + + + [TestMethod] + + public void BuildProjectWithReferences_DeployOnBuild_GeneratesPublishJsonManifestAndCopiesPublishAssets() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + build.Execute("/p:DeployOnBuild=true").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(); + + + + AssertManifest( + + StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)), + + LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + + + // GenerateStaticWebAssetsManifest should generate the publish manifest file. + + var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + new FileInfo(path).Should().Exist(); + + var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); + + AssertManifest(publishManifest, LoadPublishManifest()); + + + + AssertPublishAssets( + + publishManifest, + + Path.Combine(outputPath, "publish"), + + intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void BuildProjectWithReferences_GeneratesJsonManifestAndCopiesItToOutputFolder() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + 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)); + + AssertManifest( + + manifest, + + LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + + + AssertBuildAssets( + + manifest, + + outputPath, + + intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void BuildProjectWithReferences_NoDependencies_GeneratesJsonManifestAndCopiesItToOutputFolder() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + 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(); + + AssertManifest( + + StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)), + + LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().Exist(); + + var manifestContents = File.ReadAllText(finalPath); + + var initialManifest = StaticWebAssetsManifest.FromJsonString(File.ReadAllText(path)); + + AssertManifest( + + initialManifest, + + LoadBuildManifest()); + + + + // Second build + + var secondBuild = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(secondBuild,"/p:BuildProjectReferences=false").Should().Pass(); + + + + // GenerateStaticWebAssetsManifest should generate the manifest file. + + new FileInfo(path).Should().Exist(); + + var manifestNoDeps = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + AssertManifest( + + manifestNoDeps, + + LoadBuildManifest("NoDependencies"), + + "NoDependencies"); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + new FileInfo(finalPath).Should().Exist(); + + var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); + + AssertManifest( + + manifest, + + LoadBuildManifest("NoDependencies"), + + "NoDependencies"); + + + + AssertBuildAssets( + + manifest, + + outputPath, + + intermediateOutputPath, + + "NoDependencies"); + + + + // Check that the two manifests are the same + + manifestContents.Should().Be(File.ReadAllText(finalPath)); + + } - [Fact] + + + + + [TestMethod] + + public void PublishProjectWithReferences_GeneratesPublishJsonManifestAndCopiesPublishAssets() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + var publish = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(publish).Should().Pass(); + + + + var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var publishPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + // GenerateStaticWebAssetsManifest should generate the manifest file. + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + new FileInfo(path).Should().Exist(); + + AssertManifest( + + StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)), + + LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(publishPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().NotExist(); + + + + // GenerateStaticWebAssetsPublishManifest should generate the publish manifest file. + + var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + new FileInfo(path).Should().Exist(); + + var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); + + AssertManifest( + + publishManifest, + + LoadPublishManifest()); + + + + AssertPublishAssets( + + publishManifest, + + publishPath, + + intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void PublishProjectWithReferences_PublishSingleFile_GeneratesPublishJsonManifestAndCopiesPublishAssets() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + var publish = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(publish, "/p:PublishSingleFile=true", $"/p:RuntimeIdentifier={RuntimeInformation.RuntimeIdentifier}").Should().Pass(); + + + + var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug", RuntimeInformation.RuntimeIdentifier).ToString(); + + var publishPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + // GenerateStaticWebAssetsManifest should generate the manifest file. + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + new FileInfo(path).Should().Exist(); + + AssertManifest( + + StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)), + + LoadBuildManifest(), + + runtimeIdentifier: RuntimeInformation.RuntimeIdentifier); + + + + // GenerateStaticWebAssetsManifest should not copy the file to the output folder. + + var finalPath = Path.Combine(publishPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().NotExist(); + + + + // GenerateStaticWebAssetsPublishManifest should generate the publish manifest file. + + var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + new FileInfo(path).Should().Exist(); + + var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); + + AssertManifest(publishManifest, LoadPublishManifest(), runtimeIdentifier: RuntimeInformation.RuntimeIdentifier); + + + + AssertPublishAssets( + + publishManifest, + + publishPath, + + intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void PublishProjectWithReferences_NoBuild_GeneratesPublishJsonManifestAndCopiesPublishAssets() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + 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"); + + var objManifestFile = new FileInfo(path); + + objManifestFile.Should().Exist(); + + var objManifestFileTimeStamp = objManifestFile.LastWriteTimeUtc; + + + + AssertManifest( + + StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)), + + LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); + + var binManifestFile = new FileInfo(finalPath); + + binManifestFile.Should().Exist(); + + var binManifestTimeStamp = binManifestFile.LastWriteTimeUtc; + + + + AssertManifest( + + StaticWebAssetsManifest.FromJsonString(File.ReadAllText(path)), + + LoadBuildManifest()); + + + + // Publish no build + + var publish = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + var publishResult = ExecuteCommand(publish, "/p:NoBuild=true", "/p:ErrorOnDuplicatePublishOutputFiles=false"); + + var publishPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + publishResult.Should().Pass(); + + + + new FileInfo(path).LastWriteTimeUtc.Should().Be(objManifestFileTimeStamp); + + + + var seconbObjManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); + + AssertManifest(seconbObjManifest, LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var seconBinManifestPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); + + var secondBinManifestFile = new FileInfo(seconBinManifestPath); + + secondBinManifestFile.Should().Exist(); + + + + secondBinManifestFile.LastWriteTimeUtc.Should().Be(binManifestTimeStamp); + + + + // GenerateStaticWebAssetsManifest should generate the publish manifest file. + + var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + new FileInfo(path).Should().Exist(); + + var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); + + AssertManifest(publishManifest, LoadPublishManifest()); + + + + AssertPublishAssets( + + publishManifest, + + publishPath, + + intermediateOutputPath); + + } - [Fact] + + + + + [TestMethod] + + public void PublishProjectWithReferences_AppendTargetFrameworkToOutputPathFalse_GeneratesPublishJsonManifestAndCopiesPublishAssets() + + { + + var testAsset = "RazorAppWithPackageAndP2PReference"; + + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + EnsureLocalPackagesExists(); + + + + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(restore).Should().Pass(); + + + + var publish = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + + ExecuteCommand(publish, "/p:AppendTargetFrameworkToOutputPath=false").Should().Pass(); + + + + // Hard code output paths here to account for AppendTargetFrameworkToOutputPath=false + + var intermediateOutputPath = Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "obj", "Debug"); + + var publishPath = Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "bin", "Debug", "publish"); + + + + // GenerateStaticWebAssetsManifest should generate the manifest file. + + var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); + + new FileInfo(path).Should().Exist(); + + AssertManifest( + + StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)), + + LoadBuildManifest()); + + + + // GenerateStaticWebAssetsManifest should copy the file to the output folder. + + var finalPath = Path.Combine(publishPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); + + new FileInfo(finalPath).Should().NotExist(); + + + + // GenerateStaticWebAssetsPublishManifest should generate the publish manifest file. + + var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); + + new FileInfo(path).Should().Exist(); + + var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); + + AssertManifest( + + publishManifest, + + LoadPublishManifest()); + + + + AssertPublishAssets( + + publishManifest, + + publishPath, + + intermediateOutputPath); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsPackIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsPackIntegrationTest.cs index a42564667788..0d8ca43390d8 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsPackIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsPackIntegrationTest.cs @@ -2,1353 +2,4061 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable +using Microsoft.NET.TestFramework; +using Microsoft.NET.TestFramework.Commands; +using Microsoft.NET.TestFramework.Assertions; +using Microsoft.NET.TestFramework.Utilities; + + +using Microsoft.VisualStudio.TestTools.UnitTesting; + + + namespace Microsoft.NET.Sdk.StaticWebAssets.Tests + + { - public class StaticWebAssetsPackIntegrationTest(ITestOutputHelper log) - : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, nameof(StaticWebAssetsPackIntegrationTest)) + + + [TestClass] + + public class StaticWebAssetsPackIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest + { - [Fact] + + protected override string RestoreNugetPackagePath => nameof(StaticWebAssetsPackIntegrationTest); + + + [TestMethod] + + public void Pack_FailsWhenStaticWebAssetsHaveConflictingPaths() + + { + + var testAsset = "PackageLibraryDirectDependency"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages") + + .WithProjectChanges(project => + + { + + var ns = project.Root.Name.Namespace; + + var itemGroup = new XElement(ns + "ItemGroup"); + + var element = new XElement("StaticWebAsset", new XAttribute("Include", @"bundle\js\pkg-direct-dep.js")); + + element.Add(new XElement("SourceType")); + + element.Add(new XElement("SourceId", "PackageLibraryDirectDependency")); + + element.Add(new XElement("ContentRoot", "$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)\\bundle\\'))")); + + element.Add(new XElement("BasePath", "_content/PackageLibraryDirectDependency")); + + element.Add(new XElement("RelativePath", "js/pkg-direct-dep.js")); + + itemGroup.Add(element); + + project.Root.Add(itemGroup); + + }); + + + + Directory.CreateDirectory(Path.Combine(projectDirectory.Path, "bundle", "js")); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "bundle", "js", "pkg-direct-dep.js"), "console.log('bundle');"); + + + + var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); + + ExecuteCommand(pack).Should().Fail(); + + } + + + + // If you modify this test, make sure you also modify the test below this one to assert that things are not included as content. - [Fact] + + + [TestMethod] + + public void Pack_IncludesStaticWebAssets() + + { + + var testAsset = "PackageLibraryDirectDependency"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result.Should().NuPkgContainsPatterns( + + Path.Combine(pack.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePatterns: new[] + + { + + Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), + + Path.Combine("staticwebassets", "css", "site.css"), + + Path.Combine("staticwebassets", "PackageLibraryDirectDependency.*.bundle.scp.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), + + Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), + + Path.Combine("build", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_NoAssets_DoesNothing() + + { + + var testAsset = "PackageLibraryNoStaticAssets"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + var pack = CreatePackCommand(projectDirectory); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryNoStaticAssets.dll")).Should().Exist(); + + + + result.Should().NuPkgDoesNotContain( + + Path.Combine(pack.GetPackageDirectory().FullName, "PackageLibraryNoStaticAssets.1.0.0.nupkg"), + + filePaths: new[] + + { + + Path.Combine("staticwebassets"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), + + Path.Combine("build", "PackageLibraryNoStaticAssets.props"), + + Path.Combine("buildMultiTargeting", "PackageLibraryNoStaticAssets.props"), + + Path.Combine("buildTransitive", "PackageLibraryNoStaticAssets.props") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_NoAssets_Multitargeting_DoesNothing() + + { + + var testAsset = "PackageLibraryNoStaticAssets"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + projectDirectory.WithProjectChanges(project => + + { + + var tfm = project.Root.Descendants("TargetFramework").Single(); + + tfm.Name = "TargetFrameworks"; + + tfm.Value = "net6.0;" + DefaultTfm; + + }); + + + + var pack = CreatePackCommand(projectDirectory); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryNoStaticAssets.dll")).Should().Exist(); + + + + result.Should().NuPkgDoesNotContain( + + Path.Combine(projectDirectory.Path, "bin", "Debug", "PackageLibraryNoStaticAssets.1.0.0.nupkg"), + + filePaths: new[] + + { + + Path.Combine("staticwebassets"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), + + Path.Combine("build", "PackageLibraryNoStaticAssets.props"), + + Path.Combine("buildMultiTargeting", "PackageLibraryNoStaticAssets.props"), + + Path.Combine("buildTransitive", "PackageLibraryNoStaticAssets.props") + + }); + + } - [Fact] - public void Pack_Incremental_IncludesStaticWebAssets() - { - var testAsset = "PackageLibraryDirectDependency"; - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - var result = ExecuteCommand(pack); - result.Should().Pass(); - var pack2 = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - var result2 = ExecuteCommand(pack2); - result2.Should().Pass(); + [TestMethod] + + + public void Pack_Incremental_IncludesStaticWebAssets() + + + { + + + var testAsset = "PackageLibraryDirectDependency"; + + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + + + var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); + + + var result = ExecuteCommand(pack); + + + + + + result.Should().Pass(); + + + + + + var pack2 = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); + + + var result2 = ExecuteCommand(pack2); + + + + + + result2.Should().Pass(); + + + + var outputPath = pack2.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result2.Should().NuPkgContainsPatterns( + + Path.Combine(pack2.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePatterns: new[] + + { + + Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), + + Path.Combine("staticwebassets", "css", "site.css"), + + Path.Combine("staticwebassets", "PackageLibraryDirectDependency.*.bundle.scp.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), + + Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), + + Path.Combine("build", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_StaticWebAssets_WithoutFileExtension_AreCorrectlyPacked() + + { + + var testAsset = "PackageLibraryDirectDependency"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + File.WriteAllText(Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "wwwroot", "LICENSE"), "license file contents"); + + + + var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result.Should().NuPkgContainsPatterns( + + Path.Combine(pack.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePatterns: new[] + + { + + Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), + + Path.Combine("staticwebassets", "css", "site.css"), + + Path.Combine("staticwebassets", "LICENSE"), + + Path.Combine("staticwebassets", "PackageLibraryDirectDependency.*.bundle.scp.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), + + Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), + + Path.Combine("build", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_MultipleTargetFrameworks_Works() + + { + + var projectDirectory = SetupMultiTargetProject(); + + + + var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result.Should().NuPkgContain( + + Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "bin", "Debug", "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePaths: new[] + + { + + Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), + + Path.Combine("staticwebassets", "css", "site.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), + + Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), + + Path.Combine("build", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_MultipleTargetFrameworks_NoBuild_IncludesStaticWebAssets() + + { + + var projectDirectory = SetupMultiTargetProject(); + + + + var build = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var buildResult = build.Execute(); + + + + var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var result = pack.Execute("/p:NoBuild=true"); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result.Should().NuPkgContain( + + Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "bin", "Debug", "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePaths: new[] + + { + + Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), + + Path.Combine("staticwebassets", "css", "site.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), + + Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), + + Path.Combine("build", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_MultipleTargetFrameworks_NoBuild_DoesNotIncludeAssetsAsContent() + + { + + var projectDirectory = SetupMultiTargetProject(); + + + + var build = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var buildResult = build.Execute(); + + + + var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var result = pack.Execute("/p:NoBuild=true"); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result.Should().NuPkgDoesNotContain( + + Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "bin", "Debug", "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePaths: new[] + + { + + Path.Combine("content", "wwwroot", "js", "pkg-direct-dep.js"), + + Path.Combine("content", "wwwroot", "css", "site.css"), + + Path.Combine("contentFiles", "wwwroot", "js", "pkg-direct-dep.js"), + + Path.Combine("contentFiles", "wwwroot", "css", "site.css"), + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_MultipleTargetFrameworks_GeneratePackageOnBuild_IncludesStaticWebAssets() + + { + + var projectDirectory = SetupMultiTargetProject(); + + + + var build = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var result = build.Execute("/p:GeneratePackageOnBuild=true"); + + + + result.Should().Pass(); + + + + var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result.Should().NuPkgContain( + + Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "bin", "Debug", "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePaths: new[] + + { + + Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), + + Path.Combine("staticwebassets", "css", "site.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), + + Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), + + Path.Combine("build", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_MultipleTargetFrameworks_GeneratePackageOnBuild_DoesNotIncludeAssetsAsContent() + + { + + var projectDirectory = SetupMultiTargetProject(); + + + + var build = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var result = build.Execute("/p:GeneratePackageOnBuild=true"); + + + + result.Should().Pass(); + + + + var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result.Should().NuPkgDoesNotContain( + + Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "bin", "Debug", "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePaths: new[] + + { + + Path.Combine("content", "wwwroot", "js", "pkg-direct-dep.js"), + + Path.Combine("content", "wwwroot", "css", "site.css"), + + Path.Combine("contentFiles", "wwwroot", "js", "pkg-direct-dep.js"), + + Path.Combine("contentFiles", "wwwroot", "css", "site.css"), + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_BeforeNet60_MultipleTargetFrameworks_WithScopedCss_IncludesAssetsAndProjectBundle() + + { + + var projectDirectory = SetupBeforeNet60ScopedCssProject(); + + + + var pack = CreatePackCommand(projectDirectory); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result.Should().NuPkgContainsPatterns( + + packagePath, + + filePatterns: new[] + + { + + Path.Combine("staticwebassets", "exampleJsInterop.js"), + + Path.Combine("staticwebassets", "background.png"), + + Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), + + Path.Combine("build", "PackageLibraryTransitiveDependency.props"), + + Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.props"), + + Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.props") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_BeforeNet60_MultipleTargetFrameworks_WithScopedCss_DoesNotIncludeAssetsAsContent() + + { + + var projectDirectory = SetupBeforeNet60ScopedCssProject(); + + + + var pack = CreatePackCommand(projectDirectory); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result.Should().NuPkgDoesNotContain( + + packagePath, + + filePaths: new[] + + { + + Path.Combine("content", "exampleJsInterop.js"), + + Path.Combine("content", "background.png"), + + Path.Combine("content", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + Path.Combine("contentFiles", "exampleJsInterop.js"), + + Path.Combine("contentFiles", "background.png"), + + Path.Combine("contentFiles", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_BeforeNet60_MultipleTargetFrameworks_NoBuild_WithScopedCss_IncludesAssetsAndProjectBundle() + + { + + var projectDirectory = SetupBeforeNet60ScopedCssProject(); + + + + var build = CreateBuildCommand(projectDirectory); + + var buildResult = build.Execute(); + + + + buildResult.Should().Pass(); + + + + var pack = CreatePackCommand(projectDirectory); + + var result = pack.Execute("/p:NoBuild=true"); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result.Should().NuPkgContain( + + packagePath, + + filePaths: new[] + + { + + Path.Combine("staticwebassets", "exampleJsInterop.js"), + + Path.Combine("staticwebassets", "background.png"), + + Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), + + Path.Combine("build", "PackageLibraryTransitiveDependency.props"), + + Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.props"), + + Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.props") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_BeforeNet60_MultipleTargetFrameworks_NoBuild_WithScopedCss_DoesNotIncludeAssetsAsContent() + + { + + var projectDirectory = SetupBeforeNet60ScopedCssProject(); + + + + var build = CreateBuildCommand(projectDirectory); + + var buildResult = build.Execute(); + + + + buildResult.Should().Pass(); + + + + var pack = CreatePackCommand(projectDirectory); + + var result = pack.Execute("/p:NoBuild=true"); - result.Should().Pass(); - var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); + + + + result.Should().Pass(); + + + + + + var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result.Should().NuPkgDoesNotContain( + + packagePath, + + filePaths: new[] + + { + + Path.Combine("content", "exampleJsInterop.js"), + + Path.Combine("content", "background.png"), + + Path.Combine("content", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + Path.Combine("contentFiles", "exampleJsInterop.js"), + + Path.Combine("contentFiles", "background.png"), + + Path.Combine("contentFiles", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_BeforeNet60_MultipleTargetFrameworks_GeneratePackageOnBuild_WithScopedCss_IncludesAssetsAndProjectBundle() + + { + + var projectDirectory = SetupBeforeNet60ScopedCssProject(); + + + + var build = CreateBuildCommand(projectDirectory); + + var result = build.Execute("/p:GeneratePackageOnBuild=true"); + + + + result.Should().Pass(); + + + + var outputPath = build.GetOutputDirectory("net5.0", "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result.Should().NuPkgContain( + + packagePath, + + filePaths: new[] + + { + + Path.Combine("staticwebassets", "exampleJsInterop.js"), + + Path.Combine("staticwebassets", "background.png"), + + Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), + + Path.Combine("build", "PackageLibraryTransitiveDependency.props"), + + Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.props"), + + Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.props") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_BeforeNet60_MultipleTargetFrameworks_GeneratePackageOnBuild_WithScopedCss_DoesNotIncludeAssetsAsContent() + + { + + var projectDirectory = SetupBeforeNet60ScopedCssProject(); + + + + var build = CreateBuildCommand(projectDirectory); + + var result = build.Execute("/p:GeneratePackageOnBuild=true"); + + + + result.Should().Pass(); + + + + var outputPath = build.GetOutputDirectory("net5.0", "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result.Should().NuPkgDoesNotContain( + + packagePath, + + filePaths: new[] + + { + + Path.Combine("content", "exampleJsInterop.js"), + + Path.Combine("content", "background.png"), + + Path.Combine("content", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + Path.Combine("contentFiles", "exampleJsInterop.js"), + + Path.Combine("contentFiles", "background.png"), + + Path.Combine("contentFiles", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_Net50_WithScopedCss_IncludesAssetsAndProjectBundle() + + { + + var projectDirectory = SetupNet50ScopedCssProject(); + + + + var pack = CreatePackCommand(projectDirectory); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result.Should().NuPkgContain( + + packagePath, + + filePaths: new[] + + { + + Path.Combine("staticwebassets", "exampleJsInterop.js"), + + Path.Combine("staticwebassets", "background.png"), + + Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), + + Path.Combine("build", "PackageLibraryTransitiveDependency.props"), + + Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.props"), + + Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.props") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_Net50_WithScopedCss_DoesNotIncludeAssetsAsContent() + + { + + var projectDirectory = SetupNet50ScopedCssProject(); + + + + var pack = CreatePackCommand(projectDirectory); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result.Should().NuPkgDoesNotContain( + + packagePath, + + filePaths: new[] + + { + + Path.Combine("content", "exampleJsInterop.js"), + + Path.Combine("content", "background.png"), + + Path.Combine("content", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + Path.Combine("contentFiles", "exampleJsInterop.js"), + + Path.Combine("contentFiles", "background.png"), + + Path.Combine("contentFiles", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_Net50_NoBuild_WithScopedCss_IncludesAssetsAndProjectBundle() + + { + + var projectDirectory = SetupNet50ScopedCssProject(); + + + + var build = CreateBuildCommand(projectDirectory); + + var buildResult = build.Execute(); + + + + buildResult.Should().Pass(); + + + + var pack = CreatePackCommand(projectDirectory); + + var result = pack.Execute("/p:NoBuild=true"); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result.Should().NuPkgContain( + + packagePath, + + filePaths: new[] + + { + + Path.Combine("staticwebassets", "exampleJsInterop.js"), + + Path.Combine("staticwebassets", "background.png"), + + Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), + + Path.Combine("build", "PackageLibraryTransitiveDependency.props"), + + Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.props"), + + Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.props") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_Net50_NoBuild_WithScopedCss_DoesNotIncludeAssetsAsContent() + + { + + var projectDirectory = SetupNet50ScopedCssProject(); + + + + var build = CreateBuildCommand(projectDirectory); + + var buildResult = build.Execute(); + + + + buildResult.Should().Pass(); + + + + var pack = CreatePackCommand(projectDirectory); + + var result = pack.Execute("/p:NoBuild=true"); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result.Should().NuPkgDoesNotContain( + + packagePath, + + filePaths: new[] + + { + + Path.Combine("content", "exampleJsInterop.js"), + + Path.Combine("content", "background.png"), + + Path.Combine("content", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + Path.Combine("contentFiles", "exampleJsInterop.js"), + + Path.Combine("contentFiles", "background.png"), + + Path.Combine("contentFiles", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_Net50_GeneratePackageOnBuild_WithScopedCss_IncludesAssetsAndProjectBundle() + + { + + var projectDirectory = SetupNet50ScopedCssProject(); + + + + var build = CreateBuildCommand(projectDirectory); + + var result = build.Execute("/p:GeneratePackageOnBuild=true"); + + + + result.Should().Pass(); + + + + var outputPath = build.GetOutputDirectory("net5.0", "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result.Should().NuPkgContain( + + packagePath, + + filePaths: new[] + + { + + Path.Combine("staticwebassets", "exampleJsInterop.js"), + + Path.Combine("staticwebassets", "background.png"), + + Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), + + Path.Combine("build", "PackageLibraryTransitiveDependency.props"), + + Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.props"), + + Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.props") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_Net50_GeneratePackageOnBuild_WithScopedCss_DoesNotIncludeAssetsAsContent() + + { + + var projectDirectory = SetupNet50ScopedCssProject(); + + + + var build = CreateBuildCommand(projectDirectory); + + var result = build.Execute("/p:GeneratePackageOnBuild=true"); + + + + result.Should().Pass(); + + + + var outputPath = build.GetOutputDirectory("net5.0", "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result.Should().NuPkgDoesNotContain( + + packagePath, + + filePaths: new[] + + { + + Path.Combine("content", "exampleJsInterop.js"), + + Path.Combine("content", "background.png"), + + Path.Combine("content", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + Path.Combine("contentFiles", "exampleJsInterop.js"), + + Path.Combine("contentFiles", "background.png"), + + Path.Combine("contentFiles", "PackageLibraryTransitiveDependency.bundle.scp.css"), + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_MultipleTargetFrameworks_WithScopedCssAndJsModules_IncludesAssetsAndProjectBundle() + + { + + var testAsset = "PackageLibraryTransitiveDependency"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + projectDirectory.WithProjectChanges(document => + + { + + var parse = XDocument.Parse($@" + + + + + + {ToolsetInfo.CurrentTargetFramework};net8.0;net7.0;net6.0;net5.0 + + + + + + + + + + + + + + + + + + + + + + + + "); + + document.Root.ReplaceWith(parse.Root); + + }); + + + + Directory.Delete(Path.Combine(projectDirectory.Path, "wwwroot"), recursive: true); + + + + var componentText = @"
+ + This component is defined in the razorclasslibrarypack library. + +
"; + + + + // This mimics the structure of our default template project + + Directory.CreateDirectory(Path.Combine(projectDirectory.Path, "wwwroot")); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "_Imports.razor"), "@using Microsoft.AspNetCore.Components.Web" + Environment.NewLine); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor"), componentText); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor.css"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor.js"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "ExampleJsInterop.cs"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "background.png"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "PackageLibraryTransitiveDependency.lib.module.js"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "exampleJsInterop.js"), ""); + + + + var pack = CreatePackCommand(projectDirectory); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result.Should().NuPkgContainsPatterns( + + packagePath, + + filePatterns: new[] + + { + + Path.Combine("staticwebassets", "exampleJsInterop.js"), + + Path.Combine("staticwebassets", "background.png"), + + Path.Combine("staticwebassets", "Component1.razor.js"), + + Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.*.bundle.scp.css"), + + Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.*.lib.module.js"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), + + Path.Combine("build", "PackageLibraryTransitiveDependency.PackageAssets.json"), + + Path.Combine("build", "PackageLibraryTransitiveDependency.targets"), + + Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.targets"), + + Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.targets") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_Incremental_MultipleTargetFrameworks_WithScopedCssAndJsModules_IncludesAssetsAndProjectBundle() + + { + + var testAsset = "PackageLibraryTransitiveDependency"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + projectDirectory.WithProjectChanges(document => + + { + + var parse = XDocument.Parse($@" + + + + + + {ToolsetInfo.CurrentTargetFramework};net8.0;net7.0;net6.0;net5.0 + + + + + + + + + + + + + + + + + + + + + + + + "); + + document.Root.ReplaceWith(parse.Root); + + }); + + + + Directory.Delete(Path.Combine(projectDirectory.Path, "wwwroot"), recursive: true); + + + + var componentText = @"
+ + This component is defined in the razorclasslibrarypack library. + +
"; + + + + // This mimics the structure of our default template project + + Directory.CreateDirectory(Path.Combine(projectDirectory.Path, "wwwroot")); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "_Imports.razor"), "@using Microsoft.AspNetCore.Components.Web" + Environment.NewLine); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor"), componentText); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor.css"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor.js"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "ExampleJsInterop.cs"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "background.png"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "PackageLibraryTransitiveDependency.lib.module.js"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "exampleJsInterop.js"), ""); + + + + var pack = CreatePackCommand(projectDirectory); + + + + var pack2 = CreatePackCommand(projectDirectory); + + var result2 = pack2.Execute(); + + + + result2.Should().Pass(); + + + + var outputPath = pack2.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result2.Should().NuPkgContainsPatterns( + + packagePath, + + filePatterns: new[] + + { + + Path.Combine("staticwebassets", "exampleJsInterop.js"), + + Path.Combine("staticwebassets", "background.png"), + + Path.Combine("staticwebassets", "Component1.razor.js"), + + Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.*.bundle.scp.css"), + + Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.*.lib.module.js"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), + + Path.Combine("build", "PackageLibraryTransitiveDependency.PackageAssets.json"), + + Path.Combine("build", "PackageLibraryTransitiveDependency.targets"), + + Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.targets"), + + Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.targets") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_MultipleTargetFrameworks_WithScopedCssAndJsModules_DoesNotIncludeApplicationBundleNorModulesManifest() + + { + + var testAsset = "PackageLibraryTransitiveDependency"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + projectDirectory.WithProjectChanges(document => + + { + + var parse = XDocument.Parse($@" + + + + + + {ToolsetInfo.CurrentTargetFramework};net8.0;net7.0;net6.0;net5.0 + + + + + + + + + + + + + + + + + + + + + + + + "); + + document.Root.ReplaceWith(parse.Root); + + }); + + + + Directory.Delete(Path.Combine(projectDirectory.Path, "wwwroot"), recursive: true); + + + + var componentText = @"
+ + This component is defined in the razorclasslibrarypack library. + +
"; + + + + // This mimics the structure of our default template project + + Directory.CreateDirectory(Path.Combine(projectDirectory.Path, "wwwroot")); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "_Imports.razor"), "@using Microsoft.AspNetCore.Components.Web" + Environment.NewLine); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor"), componentText); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor.css"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "ExampleJsInterop.cs"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "background.png"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "exampleJsInterop.js"), ""); + + + + var pack = CreatePackCommand(projectDirectory); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + var packagePath = Path.Combine( + + projectDirectory.Path, + + "bin", + + "Debug", + + "PackageLibraryTransitiveDependency.1.0.0.nupkg"); + + + + result.Should().NuPkgDoesNotContain( + + packagePath, + + filePaths: new[] + + { + + Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.styles.css"), + + Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.modules.json"), + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_MultipleTargetFrameworks_DoesNotIncludeAssetsAsContent() + + { + + var testAsset = "PackageLibraryDirectDependency"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + projectDirectory.WithProjectChanges((project, document) => + + { + + var tfm = document.Descendants("TargetFramework").Single(); + + tfm.Name = "TargetFrameworks"; + + tfm.FirstNode.ReplaceWith(tfm.FirstNode.ToString() + ";netstandard2.1"); + + + + document.Descendants("AddRazorSupportForMvc").SingleOrDefault()?.Remove(); + + document.Descendants("FrameworkReference").SingleOrDefault()?.Remove(); + + }); + + + + Directory.Delete(Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "Components"), recursive: true); + + + + var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result.Should().NuPkgDoesNotContain( + + Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "bin", "Debug", "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePaths: new[] + + { + + Path.Combine("content", "wwwroot", "js", "pkg-direct-dep.js"), + + Path.Combine("content", "wwwroot", "css", "site.css"), + + Path.Combine("contentFiles", "wwwroot", "js", "pkg-direct-dep.js"), + + Path.Combine("contentFiles", "wwwroot", "css", "site.css"), + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_DoesNotInclude_TransitiveBundleOrScopedCssAsStaticWebAsset() + + { + + var testAsset = "PackageLibraryDirectDependency"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result.Should().NuPkgDoesNotContain( + + Path.Combine(pack.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePaths: new[] + + { + + // This is to make sure we don't include the scoped css files on the package when bundling is enabled. + + Path.Combine("staticwebassets", "Components", "App.razor.rz.scp.css"), + + Path.Combine("staticwebassets", "PackageLibraryDirectDependency.styles.css"), + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_DoesNotIncludeStaticWebAssetsAsContent() + + { + + var testAsset = "PackageLibraryDirectDependency"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var result = ExecuteCommand(pack); + + + + result.Should().Pass(); + + + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result.Should().NuPkgDoesNotContain( + + Path.Combine(pack.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePaths: new[] + + { + + Path.Combine("content", "wwwroot", "js", "pkg-direct-dep.js"), + + Path.Combine("content", "wwwroot", "css", "site.css"), + + Path.Combine("content", "Components", "App.razor.css"), + + // This is to make sure we don't include the unscoped css file on the package. + + Path.Combine("content", "Components", "App.razor.css"), + + Path.Combine("content", "Components", "App.razor.rz.scp.css"), + + Path.Combine("contentFiles", "wwwroot", "js", "pkg-direct-dep.js"), + + Path.Combine("contentFiles", "wwwroot", "css", "site.css"), + + Path.Combine("contentFiles", "Components", "App.razor.css"), + + Path.Combine("contentFiles", "Components", "App.razor.rz.scp.css"), + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_NoBuild_IncludesStaticWebAssets() + + { + + var testAsset = "PackageLibraryDirectDependency"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + var build = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); + + build.Execute().Should().Pass(); + + + + var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var result = pack.Execute("/p:NoBuild=true"); + + + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result.Should().NuPkgContainsPatterns( + + Path.Combine(build.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePatterns: new[] + + { + + Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), + + Path.Combine("staticwebassets", "PackageLibraryDirectDependency.*.bundle.scp.css"), + + Path.Combine("staticwebassets", "css", "site.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), + + Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), + + Path.Combine("build", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_NoBuild_DoesNotIncludeFilesAsContent() + + { + + var testAsset = "PackageLibraryDirectDependency"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + var build = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); + + build.Execute().Should().Pass(); + + + + var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var result = pack.Execute("/p:NoBuild=true"); + + + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result.Should().NuPkgDoesNotContain( + + Path.Combine(pack.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePaths: new[] + + { + + Path.Combine("content", "wwwroot", "js", "pkg-direct-dep.js"), + + Path.Combine("content", "PackageLibraryDirectDependency.bundle.scp.css"), + + Path.Combine("content", "wwwroot", "css", "site.css"), + + Path.Combine("contentFiles", "wwwroot", "js", "pkg-direct-dep.js"), + + Path.Combine("contentFiles", "PackageLibraryDirectDependency.bundle.scp.css"), + + Path.Combine("contentFiles", "wwwroot", "css", "site.css"), + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_DoesNotIncludeAnyCustomPropsFiles_WhenNoStaticAssetsAreAvailable() + + { + + var testAsset = "RazorComponentLibrary"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset); + + + + var pack = CreatePackCommand(projectDirectory); + + var result = ExecuteCommand(pack); + + + + var outputPath = pack.GetOutputDirectory("netstandard2.0", "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "ComponentLibrary.dll")).Should().Exist(); + + + + result.Should().NuPkgDoesNotContain( + + Path.Combine(projectDirectory.Path, "bin", "Debug", "ComponentLibrary.1.0.0.nupkg"), + + filePaths: new[] + + { + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), + + Path.Combine("build", "ComponentLibrary.props"), + + Path.Combine("buildMultiTargeting", "ComponentLibrary.props"), + + Path.Combine("buildTransitive", "ComponentLibrary.props") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Pack_Incremental_DoesNotRegenerateCacheAndPropsFiles() + + { + + var testAsset = "PackageLibraryTransitiveDependency"; + + var projectDirectory = TestAssetsManager + + .CopyTestAsset(testAsset, testAssetSubdirectory: "TestPackages") + + .WithSource(); + + + + var pack = CreatePackCommand(projectDirectory); + + var result = ExecuteCommand(pack); + + + + var intermediateOutputPath = pack.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + + var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); + + + + new FileInfo(Path.Combine(intermediateOutputPath, "staticwebassets", "msbuild.PackageLibraryTransitiveDependency.Microsoft.AspNetCore.StaticWebAssets.targets")).Should().Exist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "staticwebassets", "msbuild.build.PackageLibraryTransitiveDependency.targets")).Should().Exist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "staticwebassets", "msbuild.buildMultiTargeting.PackageLibraryTransitiveDependency.targets")).Should().Exist(); + + new FileInfo(Path.Combine(intermediateOutputPath, "staticwebassets", "msbuild.buildTransitive.PackageLibraryTransitiveDependency.targets")).Should().Exist(); + + + + var directoryPath = Path.Combine(intermediateOutputPath, "staticwebassets"); + + var thumbPrints = new Dictionary(); + + var thumbPrintFiles = new[] + + { + + Path.Combine(directoryPath, "msbuild.PackageLibraryTransitiveDependency.Microsoft.AspNetCore.StaticWebAssets.targets"), + + Path.Combine(directoryPath, "msbuild.build.PackageLibraryTransitiveDependency.targets"), + + Path.Combine(directoryPath, "msbuild.buildMultiTargeting.PackageLibraryTransitiveDependency.targets"), + + Path.Combine(directoryPath, "msbuild.buildTransitive.PackageLibraryTransitiveDependency.targets"), + + }; + + + + foreach (var file in thumbPrintFiles) + + { + + var thumbprint = FileThumbPrint.Create(file); + + thumbPrints[file] = thumbprint; + + } + + + + // Act + + var incremental = CreatePackCommand(projectDirectory); + + incremental.Execute().Should().Pass(); + + foreach (var file in thumbPrintFiles) + + { + + var thumbprint = FileThumbPrint.Create(file); - Assert.Equal(thumbPrints[file], thumbprint); + + + Assert.AreEqual(thumbPrints[file], thumbprint); + + } + + } - [Fact] + + + + + [TestMethod] + + public void Build_StaticWebAssets_GeneratePackageOnBuild_PacksStaticWebAssets() + + { + + var testAsset = "PackageLibraryDirectDependency"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + File.WriteAllText(Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "wwwroot", "LICENSE"), "license file contents"); + + + + var buildCommand = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var result = buildCommand.Execute("/p:GeneratePackageOnBuild=true"); + + + + result.Should().Pass(); + + + + var outputPath = buildCommand.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result.Should().NuPkgContainsPatterns( + + Path.Combine(buildCommand.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePatterns: new[] + + { + + Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), + + Path.Combine("staticwebassets", "css", "site.css"), + + Path.Combine("staticwebassets", "PackageLibraryDirectDependency.*.bundle.scp.css"), + + Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), + + Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), + + Path.Combine("build", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), + + Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") + + }); + + } - [Fact] + + + + + [TestMethod] + + public void Build_StaticWebAssets_GeneratePackageOnBuild_DoesNotIncludeAssetsAsContent() + + { + + var testAsset = "PackageLibraryDirectDependency"; + + var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); + + + + File.WriteAllText(Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "wwwroot", "LICENSE"), "license file contents"); + + + + var buildCommand = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); + + var result = buildCommand.Execute("/p:GeneratePackageOnBuild=true"); + + + + result.Should().Pass(); + + + + var outputPath = buildCommand.GetOutputDirectory(DefaultTfm, "Debug").ToString(); + + + + new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); + + + + result.Should().NuPkgDoesNotContainPatterns( + + Path.Combine(buildCommand.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), + + filePatterns: new[] + + { + + Path.Combine("content", "js", "pkg-direct-dep.js"), + + Path.Combine("content", "css", "site.css"), + + Path.Combine("content", "PackageLibraryDirectDependency.*.bundle.scp.css"), + + Path.Combine("contentFiles", "js", "pkg-direct-dep.js"), + + Path.Combine("contentFiles", "css", "site.css"), + + Path.Combine("contentFiles", "PackageLibraryDirectDependency.bundle.scp.css"), + + }); + + } + + + + private TestAsset SetupMultiTargetProject() + + { + + var projectDirectory = CreateAspNetSdkTestAsset("PackageLibraryDirectDependency", subdirectory: "TestPackages"); + + + + projectDirectory.WithProjectChanges((project, document) => + + { + + var tfm = document.Descendants("TargetFramework").Single(); + + tfm.Name = "TargetFrameworks"; + + tfm.FirstNode.ReplaceWith(tfm.FirstNode.ToString() + ";netstandard2.1"); + + + + document.Descendants("AddRazorSupportForMvc").SingleOrDefault()?.Remove(); + + document.Descendants("FrameworkReference").SingleOrDefault()?.Remove(); + + }); + + + + Directory.Delete(Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "Components"), recursive: true); + + return projectDirectory; + + } + + + + private TestAsset SetupBeforeNet60ScopedCssProject() + + { + + var projectDirectory = CreateAspNetSdkTestAsset("PackageLibraryTransitiveDependency", subdirectory: "TestPackages"); + + + + projectDirectory.WithProjectChanges(document => + + { + + var parse = XDocument.Parse($@" + + + + + + netstandard2.0;net5.0 + + 3.0 + + + + + + + + + + + + + + + + + + "); + + document.Root.ReplaceWith(parse.Root); + + }); + + + + SetupScopedCssFiles(projectDirectory); + + return projectDirectory; + + } + + + + private TestAsset SetupNet50ScopedCssProject() + + { + + var projectDirectory = CreateAspNetSdkTestAsset("PackageLibraryTransitiveDependency", subdirectory: "TestPackages"); + + + + projectDirectory.WithProjectChanges(document => + + { + + var parse = XDocument.Parse($@" + + + + + + net5.0 + + + + + + + + + + + + + + + + "); + + document.Root.ReplaceWith(parse.Root); + + }); + + + + SetupScopedCssFiles(projectDirectory); + + return projectDirectory; + + } + + + + private static void SetupScopedCssFiles(TestAsset projectDirectory) + + { + + Directory.Delete(Path.Combine(projectDirectory.Path, "wwwroot"), recursive: true); + + + + var componentText = @"
+ + This component is defined in the razorclasslibrarypack library. + +
"; + + + + Directory.CreateDirectory(Path.Combine(projectDirectory.Path, "wwwroot")); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "_Imports.razor"), "@using Microsoft.AspNetCore.Components.Web" + Environment.NewLine); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor"), componentText); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor.css"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "ExampleJsInterop.cs"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "background.png"), ""); + + File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "exampleJsInterop.js"), ""); + + } + + } + + } + + diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/TheoryData.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/TheoryData.cs new file mode 100644 index 000000000000..bd239168a0b0 --- /dev/null +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/TheoryData.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; + +public class TheoryData : List +{ + public void Add(T1 item1) => Add([item1]); +} + +public class TheoryData : List +{ + public void Add(T1 item1, T2 item2) => Add([item1, item2]); +} + +public class TheoryData : List +{ + public void Add(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5) => Add([item1, item2, item3, item4, item5]); +} From 8bdb578ef2ad546040092d3f6e3e6cda525601a5 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 18 Jun 2026 11:32:11 +0200 Subject: [PATCH 2/5] Migrate Microsoft.NET.Sdk.BlazorWebAssembly.Tests to MSTest.Sdk on MTP Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BlazorLegacyIntegrationTest50.cs | 24 +++-- .../BlazorLegacyIntegrationTest60.cs | 19 ++-- .../BlazorMultitargetIntegrationTest.cs | 14 ++- .../BlazorReadSatelliteAssemblyFileTest.cs | 6 +- .../BlazorWasmBaselineTests.cs | 10 +- ...lazorWasmStaticWebAssetsIntegrationTest.cs | 28 ++++-- ...oft.NET.Sdk.BlazorWebAssembly.Tests.csproj | 14 +-- .../VanillaWasmTests.cs | 12 ++- .../WasmBuildIncrementalismTest.cs | 23 +++-- .../WasmBuildIntegrationTest.cs | 79 +++++++++------ .../WasmBuildLazyLoadTest.cs | 23 +++-- .../WasmCompressionTests.cs | 45 +++++---- .../WasmJsModulesIntegrationTests.cs | 24 +++-- .../WasmPublishIntegrationTest.cs | 98 ++++++++++++------- .../WasmPublishIntegrationTestBase.cs | 10 +- .../WasmPwaManifestTests.cs | 26 +++-- 16 files changed, 285 insertions(+), 170 deletions(-) diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorLegacyIntegrationTest50.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorLegacyIntegrationTest50.cs index 50d8f0efa332..d6ae42623d7a 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorLegacyIntegrationTest50.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorLegacyIntegrationTest50.cs @@ -1,14 +1,17 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.NET.Sdk.StaticWebAssets.Tests; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { - public class BlazorLegacyIntegrationTest50(ITestOutputHelper log) - : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, nameof(BlazorLegacyIntegrationTest50)) + [TestClass] + public class BlazorLegacyIntegrationTest50 : IsolatedNuGetPackageFolderAspNetSdkBaselineTest { - [CoreMSBuildOnlyFact] + protected override string RestoreNugetPackagePath => nameof(BlazorLegacyIntegrationTest50); + [TestMethod] + [CoreMSBuildOnly] public void Build50Hosted_Works() { // Arrange @@ -45,16 +48,11 @@ public void Build50Hosted_Works() content.Should().Contain(Path.Combine("Client", "wwwroot")); } - [CoreMSBuildOnlyFact] + [TestMethod] + [CoreMSBuildOnly] + [PlatformSpecific(skipPlatforms: TestPlatforms.OSX, skipReason: "https://github.com/dotnet/sdk/issues/49665")] public void Publish50Hosted_Works() { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - // https://github.com/dotnet/sdk/issues/49665 - // tried: '/private/tmp/helix/working/A452091E/p/d/shared/Microsoft.NETCore.App/7.0.0/libhostpolicy.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')), - return; - } - // Arrange var testAsset = "BlazorWasmHosted50"; var targetFramework = "net5.0"; @@ -87,4 +85,4 @@ public void Publish50Hosted_Works() }); } } -} +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorLegacyIntegrationTest60.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorLegacyIntegrationTest60.cs index 6ea4020a48ff..c6850bd90760 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorLegacyIntegrationTest60.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorLegacyIntegrationTest60.cs @@ -1,14 +1,16 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.StaticWebAssets.Tasks; using Microsoft.NET.Sdk.StaticWebAssets.Tests; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { - public class BlazorLegacyIntegrationTest60(ITestOutputHelper log) - : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, nameof(BlazorLegacyIntegrationTest60)) + [TestClass] + public class BlazorLegacyIntegrationTest60 : IsolatedNuGetPackageFolderAspNetSdkBaselineTest { + protected override string RestoreNugetPackagePath => nameof(BlazorLegacyIntegrationTest60); protected override string EmbeddedResourcePrefix => string.Join('.', "Microsoft.NET.Sdk.BlazorWebAssembly.Tests", "StaticWebAssetsBaselines"); @@ -16,7 +18,8 @@ public class BlazorLegacyIntegrationTest60(ITestOutputHelper log) protected override string ComputeBaselineFolder() => Path.Combine(SdkTestContext.GetRepoRoot() ?? AppContext.BaseDirectory, "test", "Microsoft.NET.Sdk.BlazorWebAssembly.Tests", "StaticWebAssetsBaselines"); - [CoreMSBuildOnlyFact] + [TestMethod] + [CoreMSBuildOnly] public void Build60Hosted_Works() { // Arrange @@ -43,8 +46,10 @@ public void Build60Hosted_Works() new FileInfo(Path.Combine(serverBuildOutputDirectory, $"{testAsset}.Shared.dll")).Should().Exist(); } - [WindowsOnlyRequiresMSBuildVersionFact("17.13", Reason = "Needs System.Text.Json 8.0.5", Skip = "https://github.com/dotnet/sdk/issues/49925")] // https://github.com/dotnet/sdk/issues/44886 - [SkipOnPlatform(TestPlatforms.Linux | TestPlatforms.OSX, "https://github.com/dotnet/sdk/issues/42145")] + [TestMethod] + [WindowsOnlyRequiresMSBuildVersion("17.13")] + [Ignore("https://github.com/dotnet/sdk/issues/49925")] // https://github.com/dotnet/sdk/issues/44886 + [PlatformSpecific(skipPlatforms: TestPlatforms.Linux | TestPlatforms.OSX, skipReason: "https://github.com/dotnet/sdk/issues/42145")] public void Publish60Hosted_Works() { // Arrange @@ -92,4 +97,4 @@ public void Publish60Hosted_Works() intermediateOutputPath); } } -} +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorMultitargetIntegrationTest.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorMultitargetIntegrationTest.cs index 2fe340a9e079..a7abfb11edd8 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorMultitargetIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorMultitargetIntegrationTest.cs @@ -2,14 +2,17 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.NET.Sdk.StaticWebAssets.Tests; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { - public class BlazorMultitargetIntegrationTest(ITestOutputHelper log) - : IsolatedNuGetPackageFolderAspNetSdkBaselineTest(log, nameof(BlazorMultitargetIntegrationTest)) + [TestClass] + public class BlazorMultitargetIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest { + protected override string RestoreNugetPackagePath => nameof(BlazorMultitargetIntegrationTest); - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void MultiTargetApp_LoadsTheCorrectSdkBasedOnTfm() { // Arrange @@ -29,7 +32,8 @@ public void MultiTargetApp_LoadsTheCorrectSdkBasedOnTfm() browserDependencies.File("captured-references.txt").Should().NotContain("Microsoft.AspNetCore.Components.Server.dll"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void ReferencedMultiTargetApp_LoadsTheCorrectSdkBasedOnTfm() { // Arrange @@ -49,4 +53,4 @@ public void ReferencedMultiTargetApp_LoadsTheCorrectSdkBasedOnTfm() browserDependencies.File("captured-references.txt").Should().NotContain("Microsoft.AspNetCore.Components.Server.dll"); } } -} +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorReadSatelliteAssemblyFileTest.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorReadSatelliteAssemblyFileTest.cs index 9fd3bb0865b5..76c7016b06e8 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorReadSatelliteAssemblyFileTest.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorReadSatelliteAssemblyFileTest.cs @@ -4,12 +4,14 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Moq; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { + [TestClass] public class BlazorReadSatelliteAssemblyFileTest { - [Fact] + [TestMethod] public void WritesAndReadsRoundTrip() { // Arrange/Act @@ -59,4 +61,4 @@ public void WritesAndReadsRoundTrip() ); } } -} +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorWasmBaselineTests.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorWasmBaselineTests.cs index 1afe686bdf97..00a28b107cde 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorWasmBaselineTests.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorWasmBaselineTests.cs @@ -1,15 +1,19 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.NET.Sdk.StaticWebAssets.Tests; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { - public class BlazorWasmBaselineTests(ITestOutputHelper log, bool generateBaselines) : AspNetSdkBaselineTest(log, generateBaselines) +#pragma warning disable MSTEST0016 + [TestClass] + public class BlazorWasmBaselineTests : AspNetSdkBaselineTest { protected override string EmbeddedResourcePrefix => string.Join('.', "Microsoft.NET.Sdk.BlazorWebAssembly.Tests", "StaticWebAssetsBaselines"); protected override string ComputeBaselineFolder() => Path.Combine(SdkTestContext.GetRepoRoot() ?? AppContext.BaseDirectory, "test", "Microsoft.NET.Sdk.BlazorWebAssembly.Tests", "StaticWebAssetsBaselines"); } -} +#pragma warning restore MSTEST0016 +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorWasmStaticWebAssetsIntegrationTest.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorWasmStaticWebAssetsIntegrationTest.cs index 2380d0f17f63..81e80404ba68 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorWasmStaticWebAssetsIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/BlazorWasmStaticWebAssetsIntegrationTest.cs @@ -1,15 +1,18 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.VisualStudio.TestTools.UnitTesting; namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { - public class BlazorWasmStaticWebAssetsIntegrationTest(ITestOutputHelper log) : BlazorWasmBaselineTests(log, GenerateBaselines) + [TestClass] + public class BlazorWasmStaticWebAssetsIntegrationTest : BlazorWasmBaselineTests { - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void StaticWebAssets_BuildMinimal_Works() { // Arrange @@ -51,7 +54,8 @@ public void StaticWebAssets_BuildMinimal_Works() intermediateOutputPath); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void StaticWebAssets_PublishMinimal_Works() { // Arrange @@ -88,7 +92,8 @@ public void StaticWebAssets_PublishMinimal_Works() intermediateOutputPath); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void StaticWebAssets_Build_Hosted_Works() { // Arrange @@ -128,7 +133,8 @@ public void StaticWebAssets_Build_Hosted_Works() intermediateOutputPath); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void StaticWebAssets_Publish_Hosted_Works() { // Arrange @@ -168,7 +174,8 @@ public void StaticWebAssets_Publish_Hosted_Works() intermediateOutputPath); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void StaticWebAssets_Publish_DoesNotIncludeXmlDocumentationFiles_AsAssets() { // Arrange @@ -208,7 +215,7 @@ public void StaticWebAssets_Publish_DoesNotIncludeXmlDocumentationFiles_AsAssets intermediateOutputPath); } - [Fact] + [TestMethod] public void StaticWebAssets_HostedApp_ReferencingNetStandardLibrary_Works() { // Arrange @@ -259,7 +266,8 @@ public void StaticWebAssets_HostedApp_ReferencingNetStandardLibrary_Works() // https://github.com/dotnet/sdk/issues/49665 // ILLINK : Failed to load /private/tmp/helix/working/B3F609DC/p/d/shared/Microsoft.NETCore.App/7.0.0/libhostpolicy.dylib, error : dlopen(/private/tmp/helix/working/B3F609DC/p/d/shared/Microsoft.NETCore.App/7.0.0/libhostpolicy.dylib, 0x0001): tried: '/private/tmp/helix/working/B3F609DC/p/d/shared/Microsoft.NETCore.App/7.0.0/libhostpolicy.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')), '/System/Volumes/Preboot/Cryptexes/OS/private/tmp/helix/working/B3F609DC/p/d/shared/Microsoft.NETCore.App/7.0.0/libhostpolicy.dylib' (no such file), '/private/tmp/helix/working/B3F609DC/p/d/shared/Microsoft.NETCore.App/7.0.0/libhostpolicy.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64')) - [PlatformSpecificFact(TestPlatforms.Any & ~TestPlatforms.OSX)] + [TestMethod] + [PlatformSpecific(TestPlatforms.Any & ~TestPlatforms.OSX)] public void StaticWebAssets_BackCompatibilityPublish_Hosted_Works() { // Arrange @@ -308,4 +316,4 @@ public void StaticWebAssets_BackCompatibilityPublish_Hosted_Works() intermediateOutputPath); } } -} +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj index 63e7982fac3e..62f487b3b63f 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj @@ -1,4 +1,4 @@ - + false @@ -8,11 +8,6 @@ $(SdkTargetFramework) - - Exe - testSdkBlazorWasm - - @@ -46,13 +41,18 @@ + + + + + - + diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/VanillaWasmTests.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/VanillaWasmTests.cs index 833819dd317f..364af53b2776 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/VanillaWasmTests.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/VanillaWasmTests.cs @@ -1,13 +1,17 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.VisualStudio.TestTools.UnitTesting; + namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { - public class VanillaWasmTests(ITestOutputHelper log) : BlazorWasmBaselineTests(log, GenerateBaselines) + [TestClass] + public class VanillaWasmTests : BlazorWasmBaselineTests { - [CoreMSBuildOnlyFact] + [TestMethod] + [CoreMSBuildOnly] public void Build_Works() { var testAsset = "VanillaWasm"; @@ -35,4 +39,4 @@ public void Build_Works() new FileInfo(Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "dotnet.native.wasm")).Should().NotExist(); } } -} +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildIncrementalismTest.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildIncrementalismTest.cs index 98d6f9f03242..c868c4e210d8 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildIncrementalismTest.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildIncrementalismTest.cs @@ -5,12 +5,15 @@ using System.Text.Json; using Microsoft.NET.Sdk.WebAssembly; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { - public class WasmBuildIncrementalismTest(ITestOutputHelper log) : AspNetSdkTest(log) + [TestClass] + public class WasmBuildIncrementalismTest : AspNetSdkTest { - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_IsIncremental() { // Arrange @@ -54,7 +57,8 @@ public void Build_IsIncremental() } } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_GzipCompression_IsIncremental() { // Arrange @@ -91,7 +95,7 @@ public void Build_GzipCompression_IsIncremental() .Pass(); var newThumbPrint = FileThumbPrint.CreateFolderThumbprint(projectDirectory, gzipCompressionDirectory); - Assert.Equal(thumbPrint.Count, newThumbPrint.Count); + newThumbPrint.Should().HaveCount(thumbPrint.Count); for (var j = 0; j < thumbPrint.Count; j++) { thumbPrint[j].Equals(newThumbPrint[j]).Should().BeTrue($"because {thumbPrint[j].Hash} should be the same as {newThumbPrint[j].Hash} for file {thumbPrint[j].Path}"); @@ -99,7 +103,8 @@ public void Build_GzipCompression_IsIncremental() } } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_SatelliteAssembliesFileIsPreserved() { // Arrange @@ -163,7 +168,8 @@ void Verify() } } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_SatelliteAssembliesFileIsCreated_IfNewFileIsAdded() { // Arrange @@ -214,7 +220,8 @@ public void Build_SatelliteAssembliesFileIsCreated_IfNewFileIsAdded() kvp.Value.Should().ContainKey("blazorwasm.resources.wasm"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_SatelliteAssembliesFileIsDeleted_IfAllSatelliteFilesAreRemoved() { // Arrange @@ -266,4 +273,4 @@ public void Build_SatelliteAssembliesFileIsDeleted_IfAllSatelliteFilesAreRemoved satelliteResources.Should().BeNull(); } } -} +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildIntegrationTest.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildIntegrationTest.cs index bf998cfd5da0..9350d5fa3b30 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildIntegrationTest.cs @@ -7,10 +7,12 @@ using System.Text.Json; using Microsoft.AspNetCore.StaticWebAssets.Tasks; using Microsoft.NET.Sdk.WebAssembly; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { - public class WasmBuildIntegrationTest(ITestOutputHelper log) : BlazorWasmBaselineTests(log, GenerateBaselines) + [TestClass] + public class WasmBuildIntegrationTest : BlazorWasmBaselineTests { private static string customIcuFilename = "icudt_custom.dat"; private static string fullIcuFilename = "icudt.dat"; @@ -20,7 +22,8 @@ public class WasmBuildIntegrationTest(ITestOutputHelper log) : BlazorWasmBaselin "icudt_no_CJK.dat" }; - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void BuildMinimal_Works() { // Arrange @@ -50,9 +53,10 @@ public void BuildMinimal_Works() new FileInfo(Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "blazorwasm-minimal.wasm")).Should().NotExist(); } - [RequiresMSBuildVersionTheory("17.12", Reason = "Needs System.Text.Json 8.0.5")] - [InlineData("blazor")] - [InlineData("blazor spaces")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] + [DataRow("blazor")] + [DataRow("blazor spaces")] public void Build_Works(string identifier) { // Arrange @@ -78,7 +82,8 @@ public void Build_Works(string identifier) new FileInfo(Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "blazorwasm.wasm")).Should().NotExist(); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_Works_WithLibraryUsingHintPath() { // Arrange @@ -121,7 +126,8 @@ public void Build_Works_WithLibraryUsingHintPath() new FileInfo(Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "blazorwasm.wasm")).Should().NotExist(); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_InRelease_Works() { // Arrange @@ -150,7 +156,8 @@ public void Build_InRelease_Works() new FileInfo(Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "blazorwasm.wasm")).Should().NotExist(); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_ProducesBootJsonDataWithExpectedContent() { // Arrange @@ -197,7 +204,8 @@ public void Build_ProducesBootJsonDataWithExpectedContent() bootJsonData.config.Should().Contain("../appsettings.development.json"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_InRelease_ProducesBootJsonDataWithExpectedContent() { // Arrange @@ -241,7 +249,8 @@ public void Build_InRelease_ProducesBootJsonDataWithExpectedContent() bootJsonData.resources.satelliteResources.Should().BeNull(); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_WithBlazorEnableTimeZoneSupportDisabled_DoesNotCopyTimeZoneInfo() { // Arrange @@ -274,7 +283,8 @@ public void Build_WithBlazorEnableTimeZoneSupportDisabled_DoesNotCopyTimeZoneInf new FileInfo(Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "dotnet.timezones.blat")).Should().NotExist(); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_WithInvariantGlobalizationEnabled_DoesNotCopyGlobalizationData() { // Arrange @@ -313,7 +323,8 @@ public void Build_WithInvariantGlobalizationEnabled_DoesNotCopyGlobalizationData } } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_WithInvariantGlobalizationEnabled_DoesNotCopyGlobalizationData() { // Arrange @@ -351,7 +362,8 @@ public void Publish_WithInvariantGlobalizationEnabled_DoesNotCopyGlobalizationDa } } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_WithBlazorWebAssemblyLoadCustomGlobalizationData_SetsGlobalizationMode() { // Arrange @@ -396,7 +408,8 @@ public void Build_WithBlazorWebAssemblyLoadCustomGlobalizationData_SetsGlobaliza } } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_WithBlazorWebAssemblyLoadCustomGlobalizationData_SetsGlobalizationMode() { var testAppName = "BlazorHosted"; @@ -439,7 +452,8 @@ public void Publish_WithBlazorWebAssemblyLoadCustomGlobalizationData_SetsGlobali } } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_WithBlazorWebAssemblyLoadAllGlobalizationData_SetsICUDataMode() { // Arrange @@ -482,7 +496,8 @@ public void Build_WithBlazorWebAssemblyLoadAllGlobalizationData_SetsICUDataMode( } } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_WithBlazorWebAssemblyLoadAllGlobalizationData_SetsGlobalizationMode() { // Arrange @@ -524,7 +539,8 @@ public void Publish_WithBlazorWebAssemblyLoadAllGlobalizationData_SetsGlobalizat } } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_Hosted_Works() { // Arrange @@ -539,7 +555,8 @@ public void Build_Hosted_Works() new FileInfo(Path.Combine(buildOutputDirectory, "wwwroot", "_framework", "_bin", "blazorwasm.wasm")).Should().NotExist(); } - [Fact(Skip = "https://github.com/dotnet/sdk/issues/52429")] + [TestMethod] + [Ignore("https://github.com/dotnet/sdk/issues/52429")] public void Build_SatelliteAssembliesAreCopiedToBuildOutput() { // Arrange @@ -597,7 +614,8 @@ public void Build_SatelliteAssembliesAreCopiedToBuildOutput() bootJsonPath.Should().Contain("\"Microsoft.CodeAnalysis.CSharp.resources.wasm\""); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_WithCustomOutputPath_Works() { var testAppName = "BlazorWasmWithLibrary"; @@ -619,7 +637,8 @@ public void Build_WithCustomOutputPath_Works() ExecuteCommand(buildCommand).Should().Pass(); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_WithTransitiveReference_Works() { // Regression test for https://github.com/dotnet/aspnetcore/issues/37574. @@ -676,7 +695,8 @@ public class TestReference fileInWwwroot.Should().NotExist(); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Restore_WithRuntime_Works() { var testInstance = CreateAspNetSdkTestAsset("BlazorHosted"); @@ -693,7 +713,8 @@ public void Restore_WithRuntime_Works() .Should().NotExist(); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_WithReference_Works() { // Regression test for https://github.com/dotnet/aspnetcore/issues/37574. @@ -757,10 +778,11 @@ public class TestReference fileInWwwroot.Should().NotExist(); } - [RequiresMSBuildVersionTheory("17.12", Reason = "Needs System.Text.Json 8.0.5")] - [InlineData(true)] - [InlineData(false)] - [InlineData(null)] + [TestMethod] + [RequiresMSBuildVersion("17.12")] + [DataRow(true)] + [DataRow(false)] + [DataRow(null)] public void Build_WithJiterpreter(bool? value) => BuildWasmMinimalAndValidateBootConfig(new[] { ("BlazorWebAssemblyJiterpreter", value?.ToString()) }, b => { @@ -775,7 +797,8 @@ public void Build_WithJiterpreter(bool? value) } }); - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_WithJiterpreter_Advanced() => BuildWasmMinimalAndValidateBootConfig(new[] { ("BlazorWebAssemblyJiterpreter", "true"), ("BlazorWebAssemblyRuntimeOptions", "--no-jiterpreter-interp-entry-enabled") }, b => { @@ -823,4 +846,4 @@ private static BootJsonData ReadBootJsonData(string path) return BootJsonDataLoader.ParseBootData(path); } } -} +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildLazyLoadTest.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildLazyLoadTest.cs index 684bea14741c..ccc77096bc63 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildLazyLoadTest.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmBuildLazyLoadTest.cs @@ -5,14 +5,16 @@ using System.Text.Json; using Microsoft.NET.Sdk.WebAssembly; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { + [TestClass] public class WasmBuildLazyLoadTest : AspNetSdkTest { - public WasmBuildLazyLoadTest(ITestOutputHelper log) : base(log) { } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_LazyLoadExplicitAssembly_Debug_Works() { // Arrange @@ -69,7 +71,8 @@ public void Build_LazyLoadExplicitAssembly_Debug_Works() assemblies.Keys.Should().Contain("blazorwasm.wasm"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_LazyLoadExplicitAssembly_Release_Works() { // Arrange @@ -126,7 +129,8 @@ public void Build_LazyLoadExplicitAssembly_Release_Works() assemblies.Keys.Should().Contain("blazorwasm.wasm"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_LazyLoadExplicitAssembly_Debug_Works() { // Arrange @@ -178,7 +182,8 @@ public void Publish_LazyLoadExplicitAssembly_Debug_Works() assemblies.Keys.Should().Contain("blazorwasm.wasm"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_LazyLoadExplicitAssembly_Release_Works() { // Arrange @@ -230,7 +235,8 @@ public void Publish_LazyLoadExplicitAssembly_Release_Works() assemblies.Keys.Should().Contain("blazorwasm.wasm"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_LazyLoadExplicitAssembly_InvalidAssembly() { // Arrange @@ -251,7 +257,8 @@ public void Build_LazyLoadExplicitAssembly_InvalidAssembly() ExecuteCommand(buildCommand).Should().Fail().And.HaveStdOutContaining("BLAZORSDK1001"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_LazyLoadExplicitAssembly_InvalidAssembly() { // Arrange @@ -277,4 +284,4 @@ private static BootJsonData ReadBootJsonData(string path) return BootJsonDataLoader.ParseBootData(path); } } -} +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmCompressionTests.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmCompressionTests.cs index 9eb941e19ad2..18a9840a638b 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmCompressionTests.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmCompressionTests.cs @@ -3,13 +3,16 @@ #nullable disable +using Microsoft.VisualStudio.TestTools.UnitTesting; + namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { + [TestClass] public class WasmCompressionTests : AspNetSdkTest { - public WasmCompressionTests(ITestOutputHelper log) : base(log) { } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_UpdatesFilesWhenSourcesChange() { // Arrange @@ -55,14 +58,15 @@ public void Publish_UpdatesFilesWhenSourcesChange() var newBlazorBootJsonThumbPrint = FileThumbPrint.Create(blazorBootJson); var newBlazorBootJsonCompressedThumbPrint = FileThumbPrint.Create(blazorBootJsonCompressed); - Assert.NotEqual(mainAppDllThumbPrint, newMainAppDllThumbPrint); - Assert.NotEqual(mainAppCompressedDllThumbPrint, newMainAppCompressedDllThumbPrint); + Assert.AreNotEqual(mainAppDllThumbPrint, newMainAppDllThumbPrint); + Assert.AreNotEqual(mainAppCompressedDllThumbPrint, newMainAppCompressedDllThumbPrint); - Assert.NotEqual(blazorBootJsonThumbPrint, newBlazorBootJsonThumbPrint); - Assert.NotEqual(blazorBootJsonCompressedThumbPrint, newBlazorBootJsonCompressedThumbPrint); + Assert.AreNotEqual(blazorBootJsonThumbPrint, newBlazorBootJsonThumbPrint); + Assert.AreNotEqual(blazorBootJsonCompressedThumbPrint, newBlazorBootJsonCompressedThumbPrint); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_WithoutLinkerAndCompression_UpdatesFilesWhenSourcesChange() { // Arrange @@ -102,11 +106,12 @@ public void Publish_WithoutLinkerAndCompression_UpdatesFilesWhenSourcesChange() var newMainAppDllThumbPrint = FileThumbPrint.Create(mainAppDll); var newMainAppCompressedDllThumbPrint = FileThumbPrint.Create(mainAppCompressedDll); - Assert.NotEqual(mainAppDllThumbPrint, newMainAppDllThumbPrint); - Assert.NotEqual(mainAppCompressedDllThumbPrint, newMainAppCompressedDllThumbPrint); + Assert.AreNotEqual(mainAppDllThumbPrint, newMainAppDllThumbPrint); + Assert.AreNotEqual(mainAppCompressedDllThumbPrint, newMainAppCompressedDllThumbPrint); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_WithLinkerAndCompression_IsIncremental() { // Arrange @@ -130,15 +135,16 @@ public void Publish_WithLinkerAndCompression_IsIncremental() ExecuteCommand(buildCommand).Should().Pass(); var newThumbPrint = FileThumbPrint.CreateFolderThumbprint(testInstance, compressedFilesFolder); - Assert.Equal(thumbPrint.Count, newThumbPrint.Count); + newThumbPrint.Should().HaveCount(thumbPrint.Count); for (var j = 0; j < thumbPrint.Count; j++) { - Assert.Equal(thumbPrint[j], newThumbPrint[j]); + Assert.AreEqual(thumbPrint[j], newThumbPrint[j]); } } } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_WithoutLinkerAndCompression_IsIncremental() { // Arrange @@ -162,15 +168,16 @@ public void Publish_WithoutLinkerAndCompression_IsIncremental() ExecuteCommand(buildCommand, "/p:BlazorWebAssemblyEnableLinking=false").Should().Pass(); var newThumbPrint = FileThumbPrint.CreateFolderThumbprint(testInstance, compressedFilesFolder); - Assert.Equal(thumbPrint.Count, newThumbPrint.Count); + newThumbPrint.Should().HaveCount(thumbPrint.Count); for (var j = 0; j < thumbPrint.Count; j++) { - Assert.Equal(thumbPrint[j], newThumbPrint[j]); + Assert.AreEqual(thumbPrint[j], newThumbPrint[j]); } } } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_CompressesAllFrameworkFiles() { // Arrange @@ -193,10 +200,10 @@ public void Publish_CompressesAllFrameworkFiles() var extension = Path.GetExtension(file); if (extension != ".br" && extension != ".gz") { - Assert.True(File.Exists($"{file}.gz"), $"Expected file {$"{file}.gz"} to exist, but it did not."); - Assert.True(File.Exists($"{file}.br"), $"Expected file {$"{file}.br"} to exist, but it did not."); + Assert.IsTrue(File.Exists($"{file}.gz"), $"Expected file {$"{file}.gz"} to exist, but it did not."); + Assert.IsTrue(File.Exists($"{file}.br"), $"Expected file {$"{file}.br"} to exist, but it did not."); } } } } -} +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmJsModulesIntegrationTests.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmJsModulesIntegrationTests.cs index 4f0aa5c29ea1..1db707f431e3 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmJsModulesIntegrationTests.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmJsModulesIntegrationTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable disable @@ -6,12 +6,15 @@ using System.Text.Json; using Microsoft.AspNetCore.StaticWebAssets.Tasks; using Microsoft.NET.Sdk.WebAssembly; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { - public class WasmJsModulesIntegrationTests(ITestOutputHelper log) : BlazorWasmBaselineTests(log, GenerateBaselines) + [TestClass] + public class WasmJsModulesIntegrationTests : BlazorWasmBaselineTests { - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_DoesNotGenerateManifestJson_IncludesJSModulesOnBlazorBootJsonManifest() { // Arrange @@ -52,7 +55,8 @@ public void Build_DoesNotGenerateManifestJson_IncludesJSModulesOnBlazorBootJsonM new FileInfo(Path.Combine(outputPath, "wwwroot", "blazorwasm-minimal.modules.json")).Should().NotExist(); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void JSModules_ManifestIncludesModuleTargetPaths() { // Arrange @@ -100,7 +104,8 @@ public void JSModules_ManifestIncludesModuleTargetPaths() new FileInfo(Path.Combine(outputPath, "wwwroot", "blazorhosted.modules.json")).Should().NotExist(); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_DoesNotGenerateManifestJson_IncludesJSModulesOnBlazorBootJsonManifest() { // Arrange @@ -149,7 +154,8 @@ public void Publish_DoesNotGenerateManifestJson_IncludesJSModulesOnBlazorBootJso intermediateOutputPath); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void JsModules_CanHaveDifferentBuildAndPublishModules() { // Arrange @@ -202,7 +208,7 @@ public void JsModules_CanHaveDifferentBuildAndPublishModules() intermediateOutputPath); } - [Fact] + [TestMethod] public void JsModules_CanCustomizeBlazorInitialization() { // Arrange @@ -267,7 +273,7 @@ public void JsModules_CanCustomizeBlazorInitialization() intermediateOutputPath); } - [Fact] + [TestMethod] public void JsModules_Hosted_CanCustomizeBlazorInitialization() { // Arrange @@ -360,4 +366,4 @@ private static int GetPublishExtensionEntriesCount(string path) return extension.EnumerateObject().Count(); } } -} +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPublishIntegrationTest.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPublishIntegrationTest.cs index b326fe8e13e4..18661b52a81b 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPublishIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPublishIntegrationTest.cs @@ -7,14 +7,16 @@ using System.Text.Json; using Microsoft.NET.Sdk.WebAssembly; using static Microsoft.NET.Sdk.BlazorWebAssembly.Tests.ServiceWorkerAssert; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { + [TestClass] public class WasmPublishIntegrationTest : WasmPublishIntegrationTestBase { - public WasmPublishIntegrationTest(ITestOutputHelper log) : base(log) { } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_MinimalApp_Works() { // Arrange @@ -53,7 +55,8 @@ public void Publish_MinimalApp_Works() VerifyBootManifestHashes(testInstance, Path.Combine(publishDirectory.ToString(), "wwwroot")); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_WithDefaultSettings_Works() { // Arrange @@ -105,7 +108,8 @@ public void Publish_WithDefaultSettings_Works() VerifyTypeGranularTrimming(blazorPublishDirectory); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_Works_WithLibraryUsingHintPath() { // Arrange @@ -158,7 +162,8 @@ public void Publish_Works_WithLibraryUsingHintPath() new FileInfo(Path.Combine(publishOutputDirectory, "wwwroot", "_framework", "RazorClassLibrary.wasm")).Should().Exist(); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_WithScopedCss_Works() { // Arrange @@ -207,7 +212,8 @@ public void Publish_WithScopedCss_Works() assetsManifestPath: "custom-service-worker-assets.js"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_InRelease_Works() { // Arrange @@ -251,7 +257,8 @@ public void Publish_InRelease_Works() new FileInfo(Path.Combine(blazorPublishDirectory, "css", "app.css")).Should().Contain(".publish"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_WithExistingWebConfig_Works() { // Arrange @@ -271,7 +278,8 @@ public void Publish_WithExistingWebConfig_Works() webConfig.Should().Contain(webConfigContents); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_WithNoBuild_Works() { // Arrange @@ -320,9 +328,10 @@ public void Publish_WithNoBuild_Works() VerifyCompression(testInstance, blazorPublishDirectory); } - [RequiresMSBuildVersionTheory("17.12", Reason = "Needs System.Text.Json 8.0.5")] - [InlineData("different-path")] - [InlineData("/different-path")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] + [DataRow("different-path")] + [DataRow("/different-path")] public void Publish_WithStaticWebBasePathWorks(string basePath) { // Arrange @@ -383,9 +392,10 @@ public void Publish_WithStaticWebBasePathWorks(string basePath) staticWebAssetsBasePath: "different-path"); } - [RequiresMSBuildVersionTheory("17.12", Reason = "Needs System.Text.Json 8.0.5")] - [InlineData("different-path/")] - [InlineData("/different-path/")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] + [DataRow("different-path/")] + [DataRow("/different-path/")] public void Publish_Hosted_WithStaticWebBasePathWorks(string basePath) { var testAppName = "BlazorHosted"; @@ -451,7 +461,8 @@ private static void VerifyCompression(TestAsset testAsset, string blazorPublishD uncompressedText.Should().Be(originalText); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_WithTrimmingdDisabled_Works() { // Arrange @@ -522,7 +533,8 @@ public void Publish_WithTrimmingdDisabled_Works() VerifyAssemblyHasTypes(loggingAssemblyPath, new[] { "Microsoft.Extensions.Logging.LoggerFactory" }); } - [Fact(Skip = "https://github.com/dotnet/sdk/issues/52429")] + [TestMethod] + [Ignore("https://github.com/dotnet/sdk/issues/52429")] public void Publish_SatelliteAssemblies_AreCopiedToBuildOutput() { // Arrange @@ -574,7 +586,8 @@ public void Publish_SatelliteAssemblies_AreCopiedToBuildOutput() VerifyBootManifestHashes(testInstance, blazorPublishDirectory); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_HostedApp_DefaultSettings_Works() { // Arrange @@ -667,7 +680,8 @@ public void Publish_HostedApp_DefaultSettings_Works() VerifyTypeGranularTrimming(blazorPublishDirectory); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_HostedApp_ProducesBootJsonDataWithExpectedContent() { // Arrange @@ -709,7 +723,8 @@ public void Publish_HostedApp_ProducesBootJsonDataWithExpectedContent() bootJsonData.config.Should().Contain("../appsettings.development.json"); } - [Fact(Skip = "https://github.com/dotnet/sdk/issues/52429")] + [TestMethod] + [Ignore("https://github.com/dotnet/sdk/issues/52429")] public void Publish_HostedApp_WithSatelliteAssemblies() { // Arrange @@ -768,7 +783,8 @@ public void Publish_HostedApp_WithSatelliteAssemblies() bootJsonData.Should().Contain("\"Microsoft.CodeAnalysis.CSharp.resources.wasm\""); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] // Regression test for https://github.com/dotnet/aspnetcore/issues/18752 public void Publish_HostedApp_WithoutTrimming_Works() { @@ -869,7 +885,8 @@ public void Publish_HostedApp_WithoutTrimming_Works() assetsManifestPath: "custom-service-worker-assets.js"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_HostedApp_WithNoBuild_Works() { // Arrange @@ -931,7 +948,8 @@ public void Publish_HostedApp_WithNoBuild_Works() assetsManifestPath: "custom-service-worker-assets.js"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_HostedApp_VisualStudio() { // Simulates publishing the same way VS does by setting BuildProjectReferences=false. @@ -1025,7 +1043,8 @@ public void Publish_HostedApp_VisualStudio() assetsManifestPath: "custom-service-worker-assets.js"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_HostedAppWithScopedCss_VisualStudio() { // Simulates publishing the same way VS does by setting BuildProjectReferences=false. @@ -1128,7 +1147,8 @@ public void Publish_HostedAppWithScopedCss_VisualStudio() // Regression test to verify satellite assemblies from the blazor app are copied to the published app's wwwroot output directory as // part of publishing in VS - [Fact(Skip = "https://github.com/dotnet/sdk/issues/52429")] + [TestMethod] + [Ignore("https://github.com/dotnet/sdk/issues/52429")] public void Publish_HostedApp_VisualStudio_WithSatelliteAssemblies() { var testAppName = "BlazorWasmWithLibrary"; @@ -1189,7 +1209,8 @@ public void Publish_HostedApp_VisualStudio_WithSatelliteAssemblies() VerifyBootManifestHashes(testInstance, blazorPublishDirectory); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_HostedApp_WithRidSpecifiedInCLI_Works() { // Arrange @@ -1212,7 +1233,8 @@ public void Publish_HostedApp_WithRidSpecifiedInCLI_Works() AssertRIDPublishOutput(publishCommand, testInstance, hosted: true); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_HostedApp_WithRidSpecifiedAsArgument_NoSelfContained_Works() { // Arrange @@ -1248,7 +1270,7 @@ public void Publish_HostedApp_WithRidSpecifiedAsArgument_NoSelfContained_Works() AssertRIDPublishOutput(publishCommand, testInstance, hosted: true, selfContained: false); } - [Fact] + [TestMethod] public void Publish_HostedApp_WithRidSpecifiedAsArgument_Works() { // Arrange @@ -1274,7 +1296,8 @@ public void Publish_HostedApp_WithRidSpecifiedAsArgument_Works() AssertRIDPublishOutput(publishCommand, testInstance, hosted: true); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_HostedApp_WithRid_Works() { // Arrange @@ -1459,7 +1482,8 @@ private void AssertRIDPublishOutput(DotnetPublishCommand command, TestAsset test assetsManifestPath: "custom-service-worker-assets.js"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_WithInvariantGlobalizationEnabled_DoesNotCopyGlobalizationData() { // Arrange @@ -1495,7 +1519,9 @@ public void Publish_WithInvariantGlobalizationEnabled_DoesNotCopyGlobalizationDa new FileInfo(Path.Combine(publishOutputDirectory, "wwwroot", "_framework", "icudt_no_CJK.dat")).Should().NotExist(); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5", Skip = "https://github.com/dotnet/sdk/issues/53689")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] + [Ignore("https://github.com/dotnet/sdk/issues/53689")] public void Publish_HostingMultipleBlazorWebApps_Works() { // Regression test for https://github.com/dotnet/aspnetcore/issues/29264 @@ -1556,7 +1582,8 @@ public void Publish_HostingMultipleBlazorWebApps_Works() new FileInfo(Path.Combine(secondAppPublishDirectory, "_framework", "Newtonsoft.Json.wasm.br")).Should().NotExist(); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_WithTransitiveReference_Works() { // Regression test for https://github.com/dotnet/aspnetcore/issues/37574. @@ -1612,9 +1639,10 @@ public class TestReference fileInWwwroot.Should().Exist(); } - [RequiresMSBuildVersionTheory("17.12", Reason = "Needs System.Text.Json 8.0.5")] - [InlineData("")] - [InlineData("/p:BlazorFingerprintBlazorJs=false")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] + [DataRow("")] + [DataRow("/p:BlazorFingerprintBlazorJs=false")] public void Publish_BlazorWasmReferencedByAspNetCoreServer(string publishArg) { var testInstance = CreateAspNetSdkTestAsset("BlazorWasmReferencedByAspNetCoreServer"); @@ -1669,4 +1697,4 @@ public static DirectoryInfo GetOutputDirectory(this DotnetPublishCommand command return new DirectoryInfo(Path.Combine(baseDirectory.FullName, "publish")); } } -} +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPublishIntegrationTestBase.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPublishIntegrationTestBase.cs index 60b933cd6629..a868417a2a42 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPublishIntegrationTestBase.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPublishIntegrationTestBase.cs @@ -6,10 +6,13 @@ using System.Text.Json; using Microsoft.NET.Sdk.WebAssembly; using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { - public abstract class WasmPublishIntegrationTestBase(ITestOutputHelper log) : AspNetSdkTest(log) +#pragma warning disable MSTEST0016 + [TestClass] + public abstract class WasmPublishIntegrationTestBase : AspNetSdkTest { protected static void VerifyBootManifestHashes(TestAsset testAsset, string blazorPublishDirectory) { @@ -60,9 +63,10 @@ static void VerifyBootManifestHashes(TestAsset testAsset, string blazorPublishDi static string ParseWebFormattedHash(string webFormattedHash) { - Assert.StartsWith("sha256-", webFormattedHash); + webFormattedHash.Should().StartWith("sha256-"); return webFormattedHash.Substring(7); } } } -} +#pragma warning restore MSTEST0016 +} \ No newline at end of file diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPwaManifestTests.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPwaManifestTests.cs index 8e5cedbbe27b..8122275a4bd6 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPwaManifestTests.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.Tests/WasmPwaManifestTests.cs @@ -6,13 +6,16 @@ using System.Text.Json; using System.Text.RegularExpressions; using static Microsoft.NET.Sdk.BlazorWebAssembly.Tests.ServiceWorkerAssert; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Microsoft.NET.Sdk.BlazorWebAssembly.Tests { - public class WasmPwaManifestTests(ITestOutputHelper log) : AspNetSdkTest(log) + [TestClass] + public class WasmPwaManifestTests : AspNetSdkTest { - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_ServiceWorkerAssetsManifest_Works() { // Arrange @@ -61,7 +64,8 @@ public void Build_ServiceWorkerAssetsManifest_Works() assetsManifestPath: "service-worker-assets.js"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Build_HostedAppWithServiceWorker_Works() { // Arrange @@ -87,7 +91,8 @@ public void Build_HostedAppWithServiceWorker_Works() entries.Should().Contain(e => expectedExtensions.Contains(Path.GetExtension(e))); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void PublishWithPWA_ProducesAssets() { // Arrange @@ -106,7 +111,7 @@ public void PublishWithPWA_ProducesAssets() var manifestContentsJson = JsonDocument.Parse(manifestContents); manifestContentsJson.RootElement.TryGetProperty("assets", out var assets).Should().BeTrue(); - Assert.Equal(JsonValueKind.Array, assets.ValueKind); + Assert.AreEqual(JsonValueKind.Array, assets.ValueKind); var entries = assets.EnumerateArray().Select(e => e.GetProperty("url").GetString()).OrderBy(e => e).ToArray(); entries.Should().Contain(e => expectedExtensions.Contains(Path.GetExtension(e))); @@ -115,7 +120,8 @@ public void PublishWithPWA_ProducesAssets() // Assert.FileContainsLine(result, serviceWorkerFile, "// This is the production service worker"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void PublishHostedWithPWA_ProducesAssets() { // Arrange @@ -143,7 +149,8 @@ public void PublishHostedWithPWA_ProducesAssets() // Assert.FileContainsLine(result, serviceWorkerFile, "// This is the production service worker"); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_UpdatesServiceWorkerVersionHash_WhenSourcesChange() { // Arrange @@ -193,7 +200,8 @@ public void Publish_UpdatesServiceWorkerVersionHash_WhenSourcesChange() updatedCapture.Should().NotBe(capture); } - [RequiresMSBuildVersionFact("17.12", Reason = "Needs System.Text.Json 8.0.5")] + [TestMethod] + [RequiresMSBuildVersion("17.12")] public void Publish_DeterministicAcrossBuilds_WhenNoSourcesChange() { // Arrange @@ -239,4 +247,4 @@ public void Publish_DeterministicAcrossBuilds_WhenNoSourcesChange() updatedCapture.Should().Be(capture); } } -} +} \ No newline at end of file From d8b82cdfc0bfb5f5119b7d4ccc40ba196453feee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 19 Jun 2026 12:13:27 +0200 Subject: [PATCH 3/5] Migrate BlazorWebAssembly.AoT.Tests to MSTest.Sdk on MTP The AoT.Tests project link-compiles WasmPublishIntegrationTestBase.cs from the migrated .Tests project, which now uses MSTest's [TestClass]. Since AoT.Tests was still on Microsoft.NET.Sdk/xUnit, the shared file failed to compile (CS0234/CS0246: missing Microsoft.VisualStudio.TestTools.UnitTesting). Migrate it consistently with the .Tests project: switch to MSTest.Sdk, drop OutputType/PackageId, reference Microsoft.NET.TestFramework.MSTest, add the TestFramework Using items, and convert the xUnit test class to MSTest ([TestClass]/[TestMethod], drop the ITestOutputHelper ctor since the MSTest base uses TestContext). Preserve the AoT-only BuildHelixPayload gating. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...crosoft.NET.Sdk.BlazorWebAssembly.AoT.Tests.csproj | 11 +++++++---- .../WasmAoTPublishIntegrationTest.cs | 9 ++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests.csproj b/test/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests.csproj index b5abb23bac4c..a6b7463145bc 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests.csproj +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests.csproj @@ -1,4 +1,4 @@ - + false @@ -9,8 +9,6 @@ - Exe - testSdkBlazorWasmAoT false @@ -52,11 +50,16 @@ + + + + + - + diff --git a/test/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests/WasmAoTPublishIntegrationTest.cs b/test/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests/WasmAoTPublishIntegrationTest.cs index c31b418a5aff..b151e9f579ba 100644 --- a/test/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests/WasmAoTPublishIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests/WasmAoTPublishIntegrationTest.cs @@ -9,11 +9,10 @@ namespace Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests { + [TestClass] public class WasmAoTPublishIntegrationTest : WasmPublishIntegrationTestBase { - public WasmAoTPublishIntegrationTest(ITestOutputHelper log) : base(log) { } - - [Fact] + [TestMethod] public void AoT_Publish_InRelease_Works() { // Arrange @@ -56,7 +55,7 @@ public void AoT_Publish_InRelease_Works() new FileInfo(Path.Combine(blazorPublishDirectory, "css", "app.css")).Should().Contain(".publish"); } - [Fact] + [TestMethod] public void AoT_Publish_WithExistingWebConfig_Works() { // Arrange @@ -84,7 +83,7 @@ public void AoT_Publish_WithExistingWebConfig_Works() webConfig.Should().Contain(webConfigContents); } - [Fact] + [TestMethod] public void AoT_Publish_HostedAppWithScopedCss_VisualStudio() { // Simulates publishing the same way VS does by setting BuildProjectReferences=false. From 9311dd90e33f15777bf6f775c13f2cb630d72f86 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Mon, 22 Jun 2026 19:37:21 +0200 Subject: [PATCH 4/5] Convert TransientSdkResolutionErrorDetectorTests to MSTest to fix the build Microsoft.DotNet.Cli.Utils.Tests is an MSTest.Sdk project, but TransientSdkResolutionErrorDetectorTests.cs was added still using xUnit [Fact], so the test build fails with CS0246 'Fact'/'FactAttribute' on main and on every PR built against it (including this one). Add [TestClass] and convert the five [Fact] methods to [TestMethod]; MSTest.Sdk provides the MSTest namespace and FluentAssertions as implicit global usings, so no using changes are needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TransientSdkResolutionErrorDetectorTests.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/TransientSdkResolutionErrorDetectorTests.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/TransientSdkResolutionErrorDetectorTests.cs index 793954e8856e..a731be88fef0 100644 --- a/test/Microsoft.DotNet.Cli.Utils.Tests/TransientSdkResolutionErrorDetectorTests.cs +++ b/test/Microsoft.DotNet.Cli.Utils.Tests/TransientSdkResolutionErrorDetectorTests.cs @@ -3,9 +3,10 @@ namespace Microsoft.DotNet.Cli.Utils.Tests { + [TestClass] public class TransientSdkResolutionErrorDetectorTests { - [Fact] + [TestMethod] public void TransientInBoxSdkResolutionFailureIsDetected() { string input = @@ -20,13 +21,13 @@ public void TransientInBoxSdkResolutionFailureIsDetected() TransientSdkResolutionErrorDetector.IsTransientError(input).Should().BeTrue(); } - [Fact] + [TestMethod] public void NullInputIsNotTransient() { TransientSdkResolutionErrorDetector.IsTransientError(null).Should().BeFalse(); } - [Fact] + [TestMethod] public void SuccessfulBuildIsNotTransient() { string input = @@ -37,7 +38,7 @@ public void SuccessfulBuildIsNotTransient() TransientSdkResolutionErrorDetector.IsTransientError(input).Should().BeFalse(); } - [Fact] + [TestMethod] public void MissingVersionedSdkWithoutResolverNullIsNotTransient() { // A genuinely missing, version-specified SDK is a deterministic failure (no workload resolver @@ -47,7 +48,7 @@ public void MissingVersionedSdkWithoutResolverNullIsNotTransient() TransientSdkResolutionErrorDetector.IsTransientError(input).Should().BeFalse(); } - [Fact] + [TestMethod] public void OtherResolverReturningNullIsNotTransient() { // A "returned null" message from a different resolver must not trigger a retry: only the in-box From 5a22e0125af4c3fc909c4ffb6574bcfb5c13b183 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Tue, 23 Jun 2026 12:00:24 +0200 Subject: [PATCH 5/5] Fix StaticWebAssets.Tests MSTest migration: formatting and sequential execution The migration to MSTest.Sdk introduced two regressions in Microsoft.NET.Sdk.StaticWebAssets.Tests: 1. Pervasive spurious blank lines were inserted across 69 test files. Where the extra blanks landed inside multi-line verbatim/raw string literals used as assertion expectations, tests failed because the expected text no longer matched the generated output (e.g. GenerateV1StaticWebAssetsManifestTest.AllowsMultipleContentRootsWithSameBasePath_ForTheSameSourceId). Each file is reconstructed using main as the ground truth for blank-line placement; every non-blank line is byte-identical to the migrated content, so only blank lines changed. 2. The assembly-wide xUnit [assembly: CollectionBehavior(DisableTestParallelization = true)] was dropped. MSTest.Sdk parallelizes at MethodLevel by default (set repo-wide in test/Directory.Build.props), so tests began racing on shared on-disk test asset directories and NuGet caches (e.g. the BuildFrameworkAssetsConsumer helper), producing failures such as FrameworkAssetsIntegrationTest.Build_Consumer_MaterializesFrameworkAssets "Root element is missing". Setting MSTestParallelizeScope=None makes MSTest.Sdk emit [assembly: DoNotParallelize], restoring sequential execution. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AspNetSdkBaselineTest.cs | 618 +-- .../AssetGroupsIntegrationTest.cs | 403 +- ...cWebAssetsTargetPathsMultiThreadingTest.cs | 203 +- .../ComputeStaticWebAssetsTargetPathsTest.cs | 277 +- .../DeferredAssetGroupsIntegrationTest.cs | 335 +- .../FrameworkAssetsIntegrationTest.cs | 413 +- .../FrameworkAssetsP2PIntegrationTest.cs | 267 +- .../GroupedFrameworkAssetsIntegrationTest.cs | 215 +- ...NuGetPackageFolderAspNetSdkBaselineTest.cs | 22 +- .../JsModulesIntegrationTest.cs | 694 +--- .../LegacyStaticWebAssetsV1IntegrationTest.cs | 255 +- ...osoft.NET.Sdk.StaticWebAssets.Tests.csproj | 9 + .../ScopedCssIntegrationTests.cs | 1573 +------- .../StaticWebAssetEndpointsIntegrationTest.cs | 1311 +------ .../ApplyCompressionNegotiationTest.cs | 3398 +---------------- .../StaticWebAssets/ApplyCssScopesTest.cs | 753 ---- .../AssetGroupFilteringTest.cs | 1803 +-------- .../StaticWebAssets/AssetToCompressTest.cs | 342 +- .../StaticWebAssets/ComputeCssScopesTests.cs | 261 -- ...erenceStaticWebAssetsMultiThreadingTest.cs | 220 +- ...ndpointsForReferenceStaticWebAssetsTest.cs | 436 +-- ...uteStaticWebAssetsForCurrentProjectTest.cs | 629 +-- .../StaticWebAssets/ConcatenateFilesTest.cs | 700 ---- .../ContentTypeProviderTests.cs | 406 -- .../DefineStaticWebAssetEndpointsTest.cs | 1754 +-------- .../DiscoverDefaultScopedCssItemsTests.cs | 197 - ...erPrecompressedAssetsMultiThreadingTest.cs | 202 +- .../DiscoverPrecompressedAssetsTest.cs | 226 +- .../DiscoverStaticWebAssetsTest.cs | 1851 +-------- .../FilterStaticWebAssetEndpointsTest.cs | 629 +-- .../FilterStaticWebAssetGroupsTest.cs | 620 +-- .../FingerprintPatternMatcherTest.cs | 250 -- .../GeneratePackageAssetsManifestFileTest.cs | 766 +--- .../GeneratePackageAssetsTargetsFileTest.cs | 258 +- ...rateStaticWebAssetEndpointsManifestTest.cs | 986 +---- ...ateStaticWebAssetEndpointsPropsFileTest.cs | 424 +- ...eStaticWebAssetsDevelopmentManifestTest.cs | 1515 +------- .../GenerateStaticWebAssetsManifestTest.cs | 909 +---- .../GenerateStaticWebAssetsPropsFileTest.cs | 1745 +-------- .../GenerateV1StaticWebAssetsManifestTest.cs | 543 +-- .../Globbing/PathTokenizerTest.cs | 260 -- ...icWebAssetGlobMatcherTest.Compatibility.cs | 606 --- .../Globbing/StaticWebAssetGlobMatcherTest.cs | 817 ---- ...nfigurationPropertiesMultiThreadingTest.cs | 259 +- .../MergeConfigurationPropertiesTest.cs | 593 +-- .../OverrideHtmlAssetPlaceholdersTest.cs | 576 +-- .../ReadPackageAssetsManifestTest.cs | 980 +---- .../ReadStaticWebAssetsManifestFileTest.cs | 757 +--- .../ResolveAllScopedCssAssetsTest.cs | 239 -- .../ResolveCompressedAssetsTest.cs | 776 +--- ...tedStaticWebAssetEndpointsForAssetsTest.cs | 586 +-- .../StaticWebAssets/RewriteCssTest.cs | 765 +--- .../StaticWebAssetEndpointTest.cs | 122 +- .../StaticWebAssetPathPatternTest.cs | 1444 +------ .../StaticWebAssetTaskEnvironmentTests.cs | 515 +-- .../StaticWebAssets/StaticWebAssetTest.cs | 897 +---- ...eratePackagePropsFileMultiThreadingTest.cs | 117 +- ...icWebAssetsGeneratePackagePropsFileTest.cs | 93 - ...ateExternallyDefinedStaticWebAssetsTest.cs | 1056 +---- .../UpdatePackageStaticWebAssetsTest.cs | 1428 +------ .../UpdateStaticWebAssetEndpointsTest.cs | 758 +--- .../ValidateStaticWebAssetsUniquePathsTest.cs | 429 +-- .../StaticWebAssetsBaselineComparer.cs | 517 --- .../StaticWebAssetsBaselineFactory.cs | 482 --- ...aticWebAssetsCompressionIntegrationTest.cs | 412 -- .../StaticWebAssetsCrossTargetingTests.cs | 214 +- .../StaticWebAssetsDesignTimeTest.cs | 323 +- .../StaticWebAssetsFingerprintingTest.cs | 531 +-- .../StaticWebAssetsIntegrationTest.cs | 1974 +--------- .../StaticWebAssetsPackIntegrationTest.cs | 2707 +------------ 70 files changed, 125 insertions(+), 49526 deletions(-) diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AspNetSdkBaselineTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AspNetSdkBaselineTest.cs index 0f2898d99d87..0a6adfca6746 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AspNetSdkBaselineTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AspNetSdkBaselineTest.cs @@ -2,942 +2,326 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Reflection; - - using System.Runtime.CompilerServices; - - using System.Text.Json; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestCategory("BaselineTest")] [TestProperty("AspNetCore", "BaselineTest")] public class AspNetSdkBaselineTest : AspNetSdkTest - - { - - private static readonly JsonSerializerOptions BaselineSerializationOptions = new() { WriteIndented = true }; - - private readonly StaticWebAssetsBaselineComparer _comparer; - - private readonly StaticWebAssetsBaselineFactory _baselineFactory; - - - - private string _baselinesFolder; - - - - #if GENERATE_SWA_BASELINES - - public static bool GenerateBaselines = true; - - #else - - public static bool GenerateBaselines = bool.TryParse(Environment.GetEnvironmentVariable("ASPNETCORE_TEST_BASELINES"), out var result) && result; - - #endif - - - - private bool _generateBaselines = GenerateBaselines; public AspNetSdkBaselineTest() - { - TestAssembly = GetType().Assembly; - var testAssemblyMetadata = TestAssembly.GetCustomAttributes(); - RuntimeVersion = testAssemblyMetadata.SingleOrDefault(a => a.Key == "NetCoreAppRuntimePackageVersion").Value; - DefaultPackageVersion = testAssemblyMetadata.SingleOrDefault(a => a.Key == "DefaultTestBaselinePackageVersion").Value; - _comparer = CreateBaselineComparer(); - _baselineFactory = CreateBaselineFactory(); - } - - - protected void EnsureLocalPackagesExists() - - { - - var packTransitiveDependency = CreatePackCommand(ProjectDirectory, "RazorPackageLibraryTransitiveDependency"); - - ExecuteCommand(packTransitiveDependency).Should().Pass(); - - - - var packDirectDependency = CreatePackCommand(ProjectDirectory, "RazorPackageLibraryDirectDependency"); - - ExecuteCommand(packDirectDependency).Should().Pass(); - - } - - - public TestAsset ProjectDirectory { get; set; } - - - - public string RuntimeVersion { get; set; } - - - - public string DefaultPackageVersion { get; set; } - - - - public string BaselinesFolder => - - _baselinesFolder ??= ComputeBaselineFolder(); - - - - protected Assembly TestAssembly { get; } - - - - protected virtual StaticWebAssetsBaselineComparer CreateBaselineComparer() => StaticWebAssetsBaselineComparer.Instance; - - - - private static StaticWebAssetsBaselineFactory CreateBaselineFactory() => StaticWebAssetsBaselineFactory.Instance; - - - - protected virtual string ComputeBaselineFolder() => - - Path.Combine(SdkTestContext.GetRepoRoot() ?? AppContext.BaseDirectory, "test", "Microsoft.NET.Sdk.StaticWebAssets.Tests", "StaticWebAssetsBaselines"); - - - - protected virtual string EmbeddedResourcePrefix => string.Join('.', "Microsoft.NET.Sdk.StaticWebAssets.Tests", "StaticWebAssetsBaselines"); - - - - public StaticWebAssetsManifest LoadBuildManifest(string suffix = "", [CallerMemberName] string name = "") - - { - - if (_generateBaselines) - - { - - return default; - - } - - else - - { - - using var stream = GetManifestEmbeddedResource(suffix, name, "Build"); - - var manifest = StaticWebAssetsManifest.FromStream(stream); - - return manifest; - - } - - } - - - - public StaticWebAssetsManifest LoadPublishManifest(string suffix = "", [CallerMemberName] string name = "") - - { - - if (_generateBaselines) - - { - - return default; - - } - - else - - { - - using var stream = GetManifestEmbeddedResource(suffix, name, "Publish"); - - var manifest = StaticWebAssetsManifest.FromStream(stream); - - return manifest; - - } - - } - - - - protected void AssertBuildAssets( - - StaticWebAssetsManifest manifest, - - string outputFolder, - - string intermediateOutputPath, - - string suffix = "", - - [CallerMemberName] string name = "") - - { - - var fileEnumerationOptions = new EnumerationOptions { RecurseSubdirectories = true }; - - var wwwRootFolder = Path.Combine(outputFolder, "wwwroot"); - - var wwwRootFiles = Directory.Exists(wwwRootFolder) ? - - Directory.GetFiles(wwwRootFolder, "*", fileEnumerationOptions) : - - []; - - - - var computedFiles = manifest.Assets - - .Where(a => a.SourceType is StaticWebAsset.SourceTypes.Computed && - - a.AssetKind is not StaticWebAsset.AssetKinds.Publish); - - - - // We keep track of assets that need to be copied to the output folder. - - // In addition to that, we copy assets that are defined somewhere different - - // from their content root folder when the content root does not match the output folder. - - // We do this to allow copying things like Publish assets to temporary locations during the - - // build process if they are later on going to be transformed. - - var copyToOutputDirectoryAssets = manifest.Assets.Where(a => a.ShouldCopyToOutputDirectory()).ToArray(); - - var temporaryAsssets = manifest.Assets - - .Where(a => - - !a.HasContentRoot(Path.Combine(outputFolder, "wwwroot")) && - - File.Exists(a.Identity) && - - !File.Exists(Path.Combine(a.ContentRoot, a.RelativePath)) && - - a.AssetTraitName != "Content-Encoding").ToArray(); - - - - var copyToOutputDirectoryFiles = copyToOutputDirectoryAssets - - .Select(a => Path.GetFullPath(Path.Combine(outputFolder, "wwwroot", a.RelativePath))) - - .Concat(temporaryAsssets - - .Select(a => Path.GetFullPath(Path.Combine(a.ContentRoot, a.RelativePath)))) - - .ToArray(); - - - - var existingFiles = _baselineFactory.TemplatizeExpectedFiles( - - wwwRootFiles - - .Concat(computedFiles.Select(a => a.Identity)) - - .Concat(copyToOutputDirectoryFiles) - - .Distinct() - - .OrderBy(f => f, StringComparer.Ordinal) - - .ToArray(), - - GetNuGetCachePath() ?? SdkTestContext.Current.NuGetCachePath, - - ProjectDirectory.TestRoot, - - intermediateOutputPath, - - outputFolder).ToArray(); - - - - - - if (!_generateBaselines) - - { - - var expected = LoadExpectedFilesBaseline(manifest.ManifestType, suffix, name) - - .OrderBy(f => f, StringComparer.Ordinal); - - - - AssertFilesCore(existingFiles, expected); - - } - - else - - { - - File.WriteAllText( - - GetExpectedFilesPath(suffix, name, manifest.ManifestType), - - JsonSerializer.Serialize(existingFiles, BaselineSerializationOptions)); - - } - - } - - - - private static void AssertFilesCore(IEnumerable existingFiles, IEnumerable expected) - - { - - var existingSet = new HashSet(existingFiles); - - var expectedSet = new HashSet(expected); - - var different = new HashSet(existingFiles); - - - - different.SymmetricExceptWith(expectedSet); - - - - var messages = new List(); - - if (existingSet.Count < expectedSet.Count) - - { - - messages.Add("The build produced less files than expected."); - - } - - else if (expectedSet.Count < existingSet.Count) - - { - - messages.Add("The build produced more files than expected."); - - } - - else if (different.Count > 0) - - { - - messages.Add("The build produced different files than expected."); - - } - - - - ComputeDifferences(expectedSet, different, messages); - - string.Join(Environment.NewLine, messages).Should().BeEmpty(); - - - - static void ComputeDifferences(HashSet existingSet, HashSet different, List messages) - - { - - foreach (var file in different) - - { - - if (existingSet.Contains(file)) - - { - - messages.Add($"The file '{file}' is not in the baseline."); - - } - - else - - { - - messages.Add($"The file '{file}' is missing from the build."); - - } - - } - - } - - } - - - - protected void AssertPublishAssets( - - StaticWebAssetsManifest manifest, - - string publishFolder, - - string intermediateOutputPath, - - string suffix = "", - - [CallerMemberName] string name = "") - - { - - var fileEnumerationOptions = new EnumerationOptions { RecurseSubdirectories = true }; - - string wwwRootFolder = Path.Combine(publishFolder, "wwwroot"); - - var wwwRootFiles = Directory.Exists(wwwRootFolder) ? - - Directory.GetFiles(wwwRootFolder, "*", fileEnumerationOptions) - - .Select(f => _baselineFactory.TemplatizeFilePath(f, null, null, intermediateOutputPath, publishFolder, null)) : - - []; - - - - // Computed publish assets must exist on disk (we do this check to quickly identify when something is not being - - // generated vs when its being copied to the wrong place) - - var computedFiles = manifest.Assets - - .Where(a => a.SourceType is StaticWebAsset.SourceTypes.Computed && - - a.AssetKind is not StaticWebAsset.AssetKinds.Build); - - - - // For assets that are copied to the publish folder, the path is always based on - - // the wwwroot folder, the relative path and the base path for project or package - - // assets. - - var copyToPublishDirectoryFiles = manifest.Assets - - .Where(a => !string.Equals(a.SourceId, manifest.Source, StringComparison.Ordinal) || - - !string.Equals(a.AssetMode, StaticWebAsset.AssetModes.Reference)) - - .Select(a => Path.Combine(wwwRootFolder, a.ComputeTargetPath("", Path.DirectorySeparatorChar))); - - - - var existingFiles = _baselineFactory.TemplatizeExpectedFiles( - - [.. wwwRootFiles - - .Concat(computedFiles.Select(a => a.Identity)) - - .Concat(copyToPublishDirectoryFiles) - - .Distinct() - - .OrderBy(f => f, StringComparer.Ordinal)], - - GetNuGetCachePath() ?? SdkTestContext.Current.NuGetCachePath, - - ProjectDirectory.TestRoot, - - intermediateOutputPath, - - publishFolder); - - - - if (!_generateBaselines) - - { - - var expected = LoadExpectedFilesBaseline(manifest.ManifestType, suffix, name); - - existingFiles.Should().BeEquivalentTo(expected); - - } - - else - - { - - File.WriteAllText( - - GetExpectedFilesPath(suffix, name, manifest.ManifestType), - - JsonSerializer.Serialize(existingFiles, BaselineSerializationOptions)); - - } - - } - - - - public string[] LoadExpectedFilesBaseline( - - string type, - - string suffix, - - string name) - - { - - if (!_generateBaselines) - - { - - using var filesBaselineStream = GetExpectedFilesEmbeddedResource(suffix, name, type); - - return JsonSerializer.Deserialize(filesBaselineStream); - - } - - else - - { - - return []; - - } - - } - - - - internal void AssertManifest( - - StaticWebAssetsManifest actual, - - StaticWebAssetsManifest expected, - - string suffix = "", - - string runtimeIdentifier = null, - - [CallerMemberName] string name = "") - - { - - if (!_generateBaselines) - - { - - // We are going to compare the generated manifest with the current manifest. - - // For that, we "templatize" the current manifest to avoid issues with hashes, versions, etc. - - _baselineFactory.ToTemplate( - - actual, - - ProjectDirectory.Path, - - GetNuGetCachePath() ?? SdkTestContext.Current.NuGetCachePath, - - runtimeIdentifier); - - - - _comparer.AssertManifest(expected, actual); - - } - - else - - { - - var template = Templatize(actual, ProjectDirectory.Path, GetNuGetCachePath() ?? SdkTestContext.Current.NuGetCachePath, runtimeIdentifier); - - if (!Directory.Exists(Path.Combine(BaselinesFolder))) - - { - - Directory.CreateDirectory(Path.Combine(BaselinesFolder)); - - } - - - - File.WriteAllText(GetManifestPath(suffix, name, actual.ManifestType), template); - - } - - } - - - - private string GetManifestPath(string suffix, string name, string manifestType) - - => Path.Combine(BaselinesFolder, $"{name}{(!string.IsNullOrEmpty(suffix) ? $"_{suffix}" : "")}.{manifestType}.staticwebassets.json"); - - - - private Stream GetManifestEmbeddedResource(string suffix, string name, string manifestType) - - => TestAssembly.GetManifestResourceStream(string.Join('.', EmbeddedResourcePrefix, $"{name}{(!string.IsNullOrEmpty(suffix) ? $"_{suffix}" : "")}.{manifestType}.staticwebassets.json")); - - - - - - private string GetExpectedFilesPath(string suffix, string name, string manifestType) - - => Path.Combine(BaselinesFolder, $"{name}{(!string.IsNullOrEmpty(suffix) ? $"_{suffix}" : "")}.{manifestType}.files.json"); - - - - private Stream GetExpectedFilesEmbeddedResource(string suffix, string name, string manifestType) - - => TestAssembly.GetManifestResourceStream(string.Join('.', EmbeddedResourcePrefix, $"{name}{(!string.IsNullOrEmpty(suffix) ? $"_{suffix}" : "")}.{manifestType}.files.json")); - - - - private string Templatize(StaticWebAssetsManifest manifest, string projectRoot, string restorePath, string runtimeIdentifier) - - { - - _baselineFactory.ToTemplate(manifest, projectRoot, restorePath, runtimeIdentifier); - - return JsonSerializer.Serialize(manifest, BaselineSerializationOptions); - - } - - } - - } diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AssetGroupsIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AssetGroupsIntegrationTest.cs index e9ffd750e2b4..de60c92c16cd 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AssetGroupsIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/AssetGroupsIntegrationTest.cs @@ -2,611 +2,210 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.IO.Compression; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class AssetGroupsIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest - { - protected override string RestoreNugetPackagePath => nameof(AssetGroupsIntegrationTest); - - [TestMethod] - - public void Pack_NupkgContains_GroupedStaticWebAssets() - - { - - var packagePath = PackIdentityUILib("Pack_Nupkg"); - - - - new FileInfo(packagePath).Should().Exist(); - - - - using var archive = ZipFile.OpenRead(packagePath); - - var entryNames = archive.Entries.Select(e => e.FullName).ToList(); - - - - var expectedPatterns = new[] - - { - - "staticwebassets/V4/css/site.css", - - "staticwebassets/V4/js/site.js", - - "staticwebassets/V5/css/site.css", - - "staticwebassets/V5/js/site.js", - - "build/IdentityUILib.PackageAssets.json", - - "build/Microsoft.AspNetCore.StaticWebAssets.targets", - - "build/StaticWebAssets.Groups.targets", - - "build/IdentityUILib.targets", - - "buildMultiTargeting/IdentityUILib.targets", - - "buildTransitive/IdentityUILib.targets", - - }; - - - - foreach (var pattern in expectedPatterns) - - { - - entryNames.Should().Contain( - - e => e.Replace('\\', '/').EndsWith(pattern, StringComparison.OrdinalIgnoreCase), - - $"nupkg should contain entry matching '{pattern}'"); - - } - - } - - - - [TestMethod] - - public void Pack_PropsFile_ContainsAssetGroups_Metadata() - - { - - var packagePath = PackIdentityUILib("Pack_Props"); - - - - new FileInfo(packagePath).Should().Exist(); - - - - using var archive = ZipFile.OpenRead(packagePath); - - var manifestEntry = archive.Entries.FirstOrDefault( - - e => e.FullName.Equals("build/IdentityUILib.PackageAssets.json", StringComparison.OrdinalIgnoreCase)); - - - - manifestEntry.Should().NotBeNull("the nupkg should contain a PackageAssets.json manifest file"); - - - - using var stream = manifestEntry.Open(); - - using var reader = new StreamReader(stream); - - var manifestContent = reader.ReadToEnd(); - - - - // V5 assets should carry AssetGroups metadata containing BootstrapVersion=V5 - - manifestContent.Should().Contain("BootstrapVersion=V5", - - "V5 assets should have AssetGroups metadata with BootstrapVersion=V5"); - - - - // V4 assets should carry AssetGroups metadata containing BootstrapVersion=V4 - - manifestContent.Should().Contain("BootstrapVersion=V4", - - "V4 assets should have AssetGroups metadata with BootstrapVersion=V4"); - - } - - - - [TestMethod] - - public void Build_ConsumerDefault_ExcludesGroupedAssets() - - { - - var manifest = BuildConsumer("Build_Default", "IdentityUIConsumer"); - - - - // The library's StaticWebAssets.Groups.props defaults IdentityUIFrameworkVersion=V5, - - // so V5 is selected automatically and V4 is excluded. - - var v4Assets = manifest.Assets - - .Where(a => (a.AssetGroups ?? "").Contains("V4")) - - .ToList(); - - - - var v5PrimaryAssets = manifest.Assets - - .Where(a => (a.AssetGroups ?? "").Contains("V5") && a.AssetRole == "Primary") - - .ToList(); - - - - v4Assets.Should().BeEmpty("V4 grouped assets should be excluded when the default selects V5"); - - v5PrimaryAssets.Should().HaveCountGreaterThan(1, - - "V5 should be the default group — at least css/site.css and js/site.js expected"); - - } - - - - [TestMethod] - - public void Build_ConsumerV4_IncludesOnlyV4Assets() - - { - - var manifest = BuildConsumer("Build_V4", "IdentityUIConsumerV4"); - - - - var includedAssets = manifest.Assets - - .Where(a => (a.AssetGroups ?? "").Contains("V4") && a.AssetRole == "Primary") - - .ToList(); - - - - includedAssets.Should().HaveCountGreaterThan(1, - - "V4 assets should be included when consumer selects V4 — at least css/site.css and js/site.js"); - - - - var excludedAssets = manifest.Assets - - .Where(a => (a.AssetGroups ?? "").Contains("V5")) - - .ToList(); - - - - excludedAssets.Should().BeEmpty( - - "V5 assets should be excluded when consumer only selects V4"); - - - - var includedAssetFiles = new HashSet(includedAssets.Select(a => a.Identity)); - - var includedEndpoints = manifest.Endpoints - - ?.Where(e => includedAssetFiles.Contains(e.AssetFile)) - - .ToList(); - - - - includedEndpoints.Should().NotBeNull().And.HaveCountGreaterThan(1, - - "endpoints should exist for V4 assets — at least one per asset"); - - - - includedEndpoints.Should().AllSatisfy(e => - - e.Route.Should().NotContain("V4/", - - "file-only segment (~) should be excluded from endpoint routes")); - - } - - - - [TestMethod] - - public void Build_ConsumerV5_IncludesOnlyV5Assets() - - { - - var manifest = BuildConsumer("Build_V5", "IdentityUIConsumerV5"); - - - - var includedAssets = manifest.Assets - - .Where(a => (a.AssetGroups ?? "").Contains("V5") && a.AssetRole == "Primary") - - .ToList(); - - - - includedAssets.Should().HaveCountGreaterThan(1, - - "V5 assets should be included when consumer selects V5 — at least css/site.css and js/site.js"); - - - - var excludedAssets = manifest.Assets - - .Where(a => (a.AssetGroups ?? "").Contains("V4")) - - .ToList(); - - - - excludedAssets.Should().BeEmpty( - - "V4 assets should be excluded when consumer only selects V5"); - - - - var includedAssetFiles = new HashSet(includedAssets.Select(a => a.Identity)); - - var includedEndpoints = manifest.Endpoints - - ?.Where(e => includedAssetFiles.Contains(e.AssetFile)) - - .ToList(); - - - - includedEndpoints.Should().NotBeNull().And.HaveCountGreaterThan(1, - - "endpoints should exist for V5 assets — at least one per asset"); - - - - includedEndpoints.Should().AllSatisfy(e => - - e.Route.Should().NotContain("V5/", - - "file-only segment (~) should be excluded from endpoint routes")); - - } - - - - // Clear the cached package so NuGet re-extracts from the freshly-packed nupkg. - - private void ClearCachedPackage(string packageId) - - { - - var cached = Path.Combine(GetNuGetCachePath(), packageId); - - if (Directory.Exists(cached)) - - { - - Directory.Delete(cached, recursive: true); - - } - - } - - - - private string PackIdentityUILib(string identifier) - - { - - var testAsset = "AssetGroupsSample"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset, identifier: identifier); - - - - var pack = CreatePackCommand(ProjectDirectory, "IdentityUILib"); - - ExecuteCommand(pack).Should().Pass(); - - - - return Path.Combine( - - ProjectDirectory.TestRoot, - - "TestPackages", - - "IdentityUILib.1.0.0.nupkg"); - - } - - - - private StaticWebAssetsManifest BuildConsumer(string identifier, string consumerProject) - - { - - var testAsset = "AssetGroupsSample"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset, identifier: identifier); - - - - var pack = CreatePackCommand(ProjectDirectory, "IdentityUILib"); - - ExecuteCommand(pack).Should().Pass(); - - ClearCachedPackage("identityuilib"); - - - - var restore = CreateRestoreCommand(ProjectDirectory, consumerProject); - - ExecuteCommand(restore).Should().Pass(); - - - - var build = CreateBuildCommand(ProjectDirectory, consumerProject); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var manifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - new FileInfo(manifestPath).Should().Exist(); - - - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(manifestPath)); - - manifest.Should().NotBeNull(); - - return manifest; - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsMultiThreadingTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsMultiThreadingTest.cs index a60977797cfe..52e64da90738 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsMultiThreadingTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsMultiThreadingTest.cs @@ -2,310 +2,109 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - - - [DoNotParallelize] - - [TestClass] - public class ComputeStaticWebAssetsTargetPathsMultiThreadingTest - - { - - [TestMethod] - - public void ResolvesRelativeContentRootAgainstTaskEnvironmentProjectDirectoryNotProcessCurrentDirectory() - - { - - var testRoot = Path.Combine(AppContext.BaseDirectory, nameof(ComputeStaticWebAssetsTargetPathsMultiThreadingTest), Guid.NewGuid().ToString("N")); - - var projectDir = Path.Combine(testRoot, "project"); - - var spawnDir = Path.Combine(testRoot, "decoy", "spawn"); - - Directory.CreateDirectory(projectDir); - - Directory.CreateDirectory(spawnDir); - - - - const string relativeContentRoot = "wwwroot"; - - - - var projectAbsoluteContentRoot = Path.GetFullPath(Path.Combine(projectDir, relativeContentRoot)); - - var spawnAbsoluteContentRoot = Path.GetFullPath(Path.Combine(spawnDir, relativeContentRoot)); - - projectAbsoluteContentRoot.Should().NotBe(spawnAbsoluteContentRoot, - - "the test setup must place project and decoy in different parents so a relative path resolves differently against each"); - - - - var originalCurrentDirectory = Directory.GetCurrentDirectory(); - - try - - { - - Directory.SetCurrentDirectory(spawnDir); - - - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeStaticWebAssetsTargetPaths - - { - - BuildEngine = buildEngine.Object, - - TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir), - - Assets = [CreateCandidateWithRelativeContentRoot(Path.Combine(relativeContentRoot, "candidate.js"), relativeContentRoot)], - - PathPrefix = "wwwroot", - - }; - - - - var result = task.Execute(); - - - - result.Should().Be(true, "the task must run to completion when TaskEnvironment.ProjectDirectory differs from the process CWD"); - - errorMessages.Should().BeEmpty(); - - task.AssetsWithTargetPath.Should().ContainSingle(); - - - - // The relative ContentRoot must be absolutized against the task's ProjectDirectory, - - // not the process current working directory (the decoy). This is the multithreaded-safe behavior. - - var contentRoot = task.AssetsWithTargetPath[0].GetMetadata("ContentRoot"); - - contentRoot.Should().StartWith(projectAbsoluteContentRoot); - - contentRoot.Should().NotStartWith(spawnAbsoluteContentRoot); - - - - // The computed TargetPath is unaffected by the ContentRoot resolution. - - task.AssetsWithTargetPath[0].GetMetadata("TargetPath").Should().Be(Path.Combine("wwwroot", "candidate.js")); - - } - - finally - - { - - Directory.SetCurrentDirectory(originalCurrentDirectory); - - if (Directory.Exists(testRoot)) - - { - - try { Directory.Delete(testRoot, recursive: true); } catch { } - - } - - } - - } - - - - private static ITaskItem CreateCandidateWithRelativeContentRoot(string itemSpec, string relativeContentRoot) - - { - - // Intentionally skips Normalize() so the relative ContentRoot reaches the task unmodified. - - var result = new StaticWebAsset() - - { - - Identity = Path.GetFullPath(itemSpec), - - SourceId = "MyPackage", - - SourceType = "Discovered", - - ContentRoot = relativeContentRoot, - - BasePath = "base", - - RelativePath = "candidate.js", - - AssetKind = "All", - - AssetMode = "All", - - AssetRole = "Primary", - - RelatedAsset = "", - - AssetTraitName = "", - - AssetTraitValue = "", - - CopyToOutputDirectory = "", - - CopyToPublishDirectory = "", - - OriginalItemSpec = itemSpec, - - // Add these to avoid accessing the disk to compute them - - Integrity = "integrity", - - Fingerprint = "fingerprint", - - FileLength = 10, - - LastWriteTime = DateTime.UtcNow, - - }; - - - - return result.ToTaskItem(); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsTest.cs index 24ca68de7bec..36b8afa2b2c9 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ComputeStaticWebAssetsTargetPathsTest.cs @@ -2,421 +2,146 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System; - - using System.Collections.Generic; - - using System.Linq; - - using System.Text; - - using System.Threading.Tasks; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - [TestClass] - public class ComputeStaticWebAssetsTargetPathsTest - - { - - [TestMethod] - - public void IncludesFingerprintInFileWhenPreferred() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - - - var task = new ComputeStaticWebAssetsTargetPaths - - { - - BuildEngine = buildEngine.Object, - - Assets = [CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate#[.{fingerprint}]!.js", "All", "All", fingerprint: "1234asdf")], - - PathPrefix = "wwwroot", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.AssetsWithTargetPath.Should().ContainSingle(); - - var asset = task.AssetsWithTargetPath[0]; - - asset.Should().NotBeNull(); - - asset.GetMetadata("TargetPath").Should().Be(Path.Combine("wwwroot", "candidate.1234asdf.js")); - - } - - - - [TestMethod] - - public void IncludesFingerprintInFileWhenRequired() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - - - var task = new ComputeStaticWebAssetsTargetPaths - - { - - BuildEngine = buildEngine.Object, - - Assets = [CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate#[.{fingerprint}].js", "All", "All", fingerprint: "1234asdf")], - - PathPrefix = "wwwroot", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.AssetsWithTargetPath.Should().ContainSingle(); - - var asset = task.AssetsWithTargetPath[0]; - - asset.Should().NotBeNull(); - - asset.GetMetadata("TargetPath").Should().Be(Path.Combine("wwwroot", "candidate.1234asdf.js")); - - } - - - - [TestMethod] - - public void DoesNotIncludeFingerprintInFileWhenNotPreferred() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - - - var task = new ComputeStaticWebAssetsTargetPaths - - { - - BuildEngine = buildEngine.Object, - - Assets = [CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate#[.{fingerprint}]?.js", "All", "All", fingerprint: "1234asdf")], - - PathPrefix = "wwwroot", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.AssetsWithTargetPath.Should().ContainSingle(); - - var asset = task.AssetsWithTargetPath[0]; - - asset.Should().NotBeNull(); - - asset.GetMetadata("TargetPath").Should().Be(Path.Combine("wwwroot", "candidate.js")); - - } - - - - private static ITaskItem CreateCandidate( - - string itemSpec, - - string sourceId, - - string sourceType, - - string relativePath, - - string assetKind, - - string assetMode, - - string fingerprint = null) - - { - - var result = new StaticWebAsset() - - { - - Identity = Path.GetFullPath(itemSpec), - - SourceId = sourceId, - - SourceType = sourceType, - - ContentRoot = Directory.GetCurrentDirectory(), - - BasePath = "base", - - RelativePath = relativePath, - - AssetKind = assetKind, - - AssetMode = assetMode, - - AssetRole = "Primary", - - RelatedAsset = "", - - AssetTraitName = "", - - AssetTraitValue = "", - - CopyToOutputDirectory = "", - - CopyToPublishDirectory = "", - - OriginalItemSpec = itemSpec, - - // Add these to avoid accessing the disk to compute them - - Integrity = "integrity", - - Fingerprint = fingerprint ?? "fingerprint", - - FileLength = 10, - - LastWriteTime = DateTime.UtcNow, - - }; - - - - result.ApplyDefaults(); - - result.Normalize(); - - - - return result.ToTaskItem(); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/DeferredAssetGroupsIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/DeferredAssetGroupsIntegrationTest.cs index 106f71f47b94..05e7e93b1bd0 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/DeferredAssetGroupsIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/DeferredAssetGroupsIntegrationTest.cs @@ -2,508 +2,175 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Text.Json; - - using System.Xml.Linq; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class DeferredAssetGroupsIntegrationTest : AspNetSdkTest - - { - - [TestMethod] - - public void Build_DeferredGroupEnabled_IncludesGroupedAssetAndEndpoints() - - { - - var intermediateOutputPath = BuildWithDeferredGroup(enableBlazorGroup: "enabled"); - - var manifest = LoadBuildManifest(intermediateOutputPath); - - var endpoints = LoadEndpointsManifest(intermediateOutputPath); - - - - // Primary asset should be present - - var primaryAsset = manifest.Assets - - .Where(a => a.RelativePath == "deferred.blazor.js" && a.AssetRole == "Primary") - - .ToList(); - - primaryAsset.Should().ContainSingle("deferred.blazor.js primary asset should be present when BlazorGroup=enabled"); - - - - // Compressed alternatives should be present - - var compressedAssets = manifest.Assets - - .Where(a => a.RelativePath.StartsWith("deferred.blazor.js.") && a.AssetRole == "Alternative" && a.AssetTraitName == "Content-Encoding") - - .ToList(); - - compressedAssets.Should().NotBeEmpty("compressed alternatives (gzip/brotli) should exist for deferred.blazor.js"); - - - - // Uncompressed endpoint (no selector) - - var primaryEndpoints = endpoints - - .Where(e => e.Route.EndsWith("deferred.blazor.js") && e.Selectors.Length == 0) - - .ToList(); - - primaryEndpoints.Should().ContainSingle("an uncompressed endpoint should exist for deferred.blazor.js"); - - - - // Compressed endpoints (with Content-Encoding selector on the same route) - - var compressedEndpoints = endpoints - - .Where(e => e.Route.EndsWith("deferred.blazor.js") && e.Selectors.Length == 1 && e.Selectors[0].Name == "Content-Encoding") - - .ToList(); - - compressedEndpoints.Should().NotBeEmpty("compressed endpoints with Content-Encoding selector should exist for deferred.blazor.js"); - - - - // Direct .gz/.br route endpoints - - var directCompressedEndpoints = endpoints - - .Where(e => e.Route.EndsWith("deferred.blazor.js.gz") || e.Route.EndsWith("deferred.blazor.js.br")) - - .ToList(); - - directCompressedEndpoints.Should().NotBeEmpty("direct .gz/.br route endpoints should exist for deferred.blazor.js"); - - - - // Existing non-grouped assets should be unaffected - - var existingEndpoints = endpoints - - .Where(e => e.Route.Contains("project-transitive-dep.js")) - - .ToList(); - - existingEndpoints.Should().NotBeEmpty("endpoints for existing non-grouped assets should be unaffected"); - - } - - - - [TestMethod] - - public void Build_DeferredGroupDisabled_ExcludesGroupedAssetAndEndpoints() - - { - - var intermediateOutputPath = BuildWithDeferredGroup(enableBlazorGroup: "disabled"); - - var manifest = LoadBuildManifest(intermediateOutputPath); - - var endpoints = LoadEndpointsManifest(intermediateOutputPath); - - - - // Build manifest retains all assets (unfiltered) — the primary asset is still there - - var primaryAsset = manifest.Assets - - .Where(a => a.RelativePath == "deferred.blazor.js" && a.AssetRole == "Primary") - - .ToList(); - - primaryAsset.Should().ContainSingle("build manifest retains all variants; deferred.blazor.js should still be present"); - - - - // But all deferred.blazor.js endpoints should be excluded from the endpoints manifest - - var deferredEndpoints = endpoints - - .Where(e => e.Route.Contains("deferred.blazor.js")) - - .ToList(); - - deferredEndpoints.Should().BeEmpty("no endpoints should exist for deferred.blazor.js when BlazorGroup=disabled"); - - - - // That includes direct .gz/.br routes - - var directCompressedEndpoints = endpoints - - .Where(e => e.Route.EndsWith("deferred.blazor.js.gz") || e.Route.EndsWith("deferred.blazor.js.br")) - - .ToList(); - - directCompressedEndpoints.Should().BeEmpty("no compressed route endpoints should exist for excluded deferred.blazor.js"); - - - - // Existing non-grouped assets should be unaffected - - var existingEndpoints = endpoints - - .Where(e => e.Route.Contains("project-transitive-dep.js")) - - .ToList(); - - existingEndpoints.Should().NotBeEmpty("endpoints for existing non-grouped assets should be unaffected"); - - } - - - - private string BuildWithDeferredGroup(string enableBlazorGroup) - - { - - var testAsset = "RazorAppWithP2PReference"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, identifier: enableBlazorGroup) - - .WithProjectChanges((path, document) => - - { - - if (Path.GetFileName(path) == "ClassLibrary.csproj") - - { - - document.Root.Add( - - new XElement("ItemGroup", - - new XElement("StaticWebAssetGroupDefinition", - - new XAttribute("Include", "BlazorGroup"), - - new XAttribute("Value", "enabled"), - - new XAttribute("Order", "0"), - - new XAttribute("SourceId", "ClassLibrary"), - - new XAttribute("IncludePattern", "deferred.blazor.js")))); - - document.Root.Add( - - new XElement("Import", - - new XAttribute("Project", "StaticWebAssets.Groups.targets"))); - - } - - - - if (Path.GetFileName(path) == "AppWithP2PReference.csproj") - - { - - document.Root.Add( - - new XElement("Import", - - new XAttribute("Project", @"..\ClassLibrary\StaticWebAssets.Groups.targets"))); - - } - - }); - - - - var classLibDir = Path.Combine(projectDirectory.TestRoot, "ClassLibrary"); - - - - File.WriteAllText( - - Path.Combine(classLibDir, "wwwroot", "deferred.blazor.js"), - - "console.log('deferred blazor');"); - - - - File.WriteAllText( - - Path.Combine(classLibDir, "StaticWebAssets.Groups.targets"), - - $""" - - - - - - {enableBlazorGroup} - - - - $(FilterDeferredStaticWebAssetGroupsDependsOn); - - ResolveDeferredBlazorGroup - - - - - - - - - - - - - - - - - - - - - - - - - - """); - - - - var build = CreateBuildCommand(projectDirectory, "AppWithP2PReference"); - - ExecuteCommand(build).Should().Pass(); - - - - return build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - } - - - - private static StaticWebAssetsManifest LoadBuildManifest(string intermediateOutputPath) - - { - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - return StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - } - - - - private static StaticWebAssetEndpoint[] LoadEndpointsManifest(string intermediateOutputPath) - - { - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.endpoints.json"); - - var manifest = JsonSerializer.Deserialize( - - File.ReadAllBytes(path), - - StaticWebAssetsJsonSerializerContext.Default.StaticWebAssetEndpointsManifest); - - return manifest?.Endpoints ?? []; - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsIntegrationTest.cs index 8ae2207bd148..0f00d7cee2b7 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsIntegrationTest.cs @@ -2,626 +2,215 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; using Microsoft.NET.TestFramework.Commands; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.IO.Compression; - - using System.Text.Json; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class FrameworkAssetsIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest - { - protected override string RestoreNugetPackagePath => nameof(FrameworkAssetsIntegrationTest); - - [TestMethod] - - public void Pack_PropsFile_ContainsFrameworkSourceType_ForMatchedAssets() - - { - - var testAsset = "FrameworkAssetsSample"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var pack = CreatePackCommand(ProjectDirectory, "FrameworkAssetsLib"); - - ExecuteCommand(pack).Should().Pass(); - - - - var packagePath = Path.Combine( - - ProjectDirectory.TestRoot, - - "TestPackages", - - "FrameworkAssetsLib.1.0.0.nupkg"); - - - - new FileInfo(packagePath).Should().Exist(); - - - - // Extract the JSON manifest from the nupkg and verify SourceType - - using var archive = ZipFile.OpenRead(packagePath); - - var manifestEntry = archive.Entries.FirstOrDefault( - - e => e.FullName.Equals("build/FrameworkAssetsLib.PackageAssets.json", StringComparison.OrdinalIgnoreCase)); - - - - manifestEntry.Should().NotBeNull("the nupkg should contain a PackageAssets.json file"); - - - - using var stream = manifestEntry.Open(); - - var manifest = JsonSerializer.Deserialize(stream, - - StaticWebAssetsJsonSerializerContext.Default.StaticWebAssetPackageManifest); - - - - manifest.Should().NotBeNull(); - - manifest.Assets.Should().NotBeEmpty(); - - - - // JS files should be marked as Framework - - manifest.Assets.Values.Should().Contain( - - a => a.SourceType == "Framework", - - "JS assets matching the FrameworkPattern should have SourceType=Framework"); - - - - // CSS files should remain as Package - - manifest.Assets.Values.Should().Contain( - - a => a.SourceType == "Package", - - "CSS assets not matching the FrameworkPattern should have SourceType=Package"); - - } - - - - [TestMethod] - - public void Pack_NupkgContains_ExpectedStaticWebAssets() - - { - - var testAsset = "FrameworkAssetsSample"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var pack = CreatePackCommand(ProjectDirectory, "FrameworkAssetsLib"); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var packagePath = Path.Combine( - - ProjectDirectory.TestRoot, - - "TestPackages", - - "FrameworkAssetsLib.1.0.0.nupkg"); - - - - result.Should().NuPkgContainsPatterns( - - packagePath, - - filePatterns: new[] - - { - - Path.Combine("staticwebassets", "js", "framework.js"), - - Path.Combine("staticwebassets", "css", "site.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), - - Path.Combine("build", "FrameworkAssetsLib.targets"), - - Path.Combine("build", "FrameworkAssetsLib.PackageAssets.json"), - - Path.Combine("buildMultiTargeting", "FrameworkAssetsLib.targets"), - - Path.Combine("buildTransitive", "FrameworkAssetsLib.targets"), - - }); - - } - - - - [TestMethod] - - public void Build_Consumer_MaterializesFrameworkAssets() - - { - - var (intermediateOutputPath, _) = BuildFrameworkAssetsConsumer(); - - var manifest = LoadBuildManifest(intermediateOutputPath); - - - - // The framework JS asset should be materialized (SourceType changed from Framework to Discovered) - - var frameworkAssets = manifest.Assets - - .Where(a => a.RelativePath.Contains("framework.js")) - - .ToList(); - - - - frameworkAssets.Should().HaveCountGreaterThan(0, "framework.js should appear in the build manifest"); - - - - // After materialization, the framework asset should have SourceType=Discovered - - // and be under the fx/ intermediate directory - - var materializedAsset = frameworkAssets - - .FirstOrDefault(a => a.Identity.Contains(Path.Combine("fx", "FrameworkAssetsLib"))); - - - - materializedAsset.Should().NotBeNull( - - "framework.js should be materialized under the fx/FrameworkAssetsLib directory"); - - materializedAsset.SourceType.Should().Be("Discovered"); - - materializedAsset.AssetMode.Should().Be("CurrentProject"); - - - - // The CSS asset should remain as a regular Package asset (not materialized) - - var cssAssets = manifest.Assets - - .Where(a => a.RelativePath.Contains("site.css")) - - .ToList(); - - - - cssAssets.Should().HaveCountGreaterThan(0, "site.css should appear in the build manifest"); - - cssAssets.Should().OnlyContain(a => a.SourceType == "Package", - - "CSS assets should remain as Package type since they don't match the FrameworkPattern"); - - } - - - - [TestMethod] - - public void Build_Consumer_MaterializedFrameworkAsset_FileExistsOnDisk() - - { - - var (intermediateOutputPath, _) = BuildFrameworkAssetsConsumer(); - - - - // The materialized file should exist on disk under the staticwebassets/fx directory - - var fxDir = Path.Combine(intermediateOutputPath, "staticwebassets", "fx", "FrameworkAssetsLib"); - - var materializedFile = Directory.GetFiles(fxDir, "framework.js", SearchOption.AllDirectories); - - - - materializedFile.Should().HaveCount(1, "framework.js should be materialized exactly once"); - - File.ReadAllText(materializedFile[0]).Should().Contain("framework", - - "materialized file should contain the original framework.js content"); - - } - - - - [TestMethod] - - public void Build_Consumer_EndpointsRemapped_ForFrameworkAssets() - - { - - var (intermediateOutputPath, _) = BuildFrameworkAssetsConsumer(); - - var manifest = LoadBuildManifest(intermediateOutputPath); - - - - // Check that the framework asset in the manifest has been remapped to the materialized path - - var frameworkAssets = manifest.Assets - - .Where(a => a.RelativePath.Contains("framework.js") - - && a.Identity.Contains(Path.Combine("staticwebassets", "fx", "FrameworkAssetsLib"))) - - .ToList(); - - - - frameworkAssets.Should().HaveCountGreaterThan(0, - - "the manifest should contain a materialized framework asset under staticwebassets/fx/"); - - - - // Endpoints for the route should exist (some may be compressed variants) - - var fxEndpoints = manifest.Endpoints - - ?.Where(e => e.Route.Contains("framework.js")) - - .ToList(); - - - - fxEndpoints.Should().NotBeNull().And.HaveCountGreaterThan(0, - - "there should be at least one endpoint for framework.js"); - - - - // At least one endpoint should reference the materialized asset (not all will — compressed endpoints point elsewhere) - - var endpointsPointingToMaterialized = fxEndpoints - - .Where(e => e.AssetFile.Contains(Path.Combine("staticwebassets", "fx", "FrameworkAssetsLib"))) - - .ToList(); - - - - endpointsPointingToMaterialized.Should().HaveCountGreaterThan(0, - - "at least one endpoint for framework.js should point to the materialized file path under staticwebassets/fx/"); - - } - - - - [TestMethod] - - public void Build_Consumer_IsIncremental() - - { - - var (intermediateOutputPath, _) = BuildFrameworkAssetsConsumer(); - - - - var fxDir = Path.Combine(intermediateOutputPath, "staticwebassets", "fx", "FrameworkAssetsLib"); - - var materializedFile = Directory.GetFiles(fxDir, "framework.js", SearchOption.AllDirectories).Single(); - - var firstWriteTime = File.GetLastWriteTimeUtc(materializedFile); - - - - // Second build — should be incremental (file not re-copied) - - var build2 = CreateBuildCommand(ProjectDirectory, "FrameworkAssetsConsumer"); - - ExecuteCommand(build2).Should().Pass(); - - - - var secondWriteTime = File.GetLastWriteTimeUtc(materializedFile); - - secondWriteTime.Should().Be(firstWriteTime, - - "framework asset should not be re-copied on incremental build"); - - } - - - - private (string intermediateOutputPath, BuildCommand build) BuildFrameworkAssetsConsumer() - - { - - var testAsset = "FrameworkAssetsSample"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var pack = CreatePackCommand(ProjectDirectory, "FrameworkAssetsLib"); - - ExecuteCommand(pack).Should().Pass(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "FrameworkAssetsConsumer"); - - ExecuteCommand(restore).Should().Pass(); - - - - var build = CreateBuildCommand(ProjectDirectory, "FrameworkAssetsConsumer"); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - return (intermediateOutputPath, build); - - } - - - - private StaticWebAssetsManifest LoadBuildManifest(string intermediateOutputPath) - - { - - var manifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(manifestPath)); - - manifest.Should().NotBeNull(); - - return manifest; - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsP2PIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsP2PIntegrationTest.cs index c43587b5dbb0..64f141b08a99 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsP2PIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/FrameworkAssetsP2PIntegrationTest.cs @@ -2,406 +2,141 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Xml.Linq; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class FrameworkAssetsP2PIntegrationTest : AspNetSdkTest - - { - - [TestMethod] - - public void Build_Consumer_MaterializesFrameworkAssetsFromProjectReference() - - { - - var intermediateOutputPath = BuildConsumerWithFrameworkPattern(); - - var manifest = LoadBuildManifest(intermediateOutputPath); - - - - // The JS assets matching the FrameworkPattern should be materialized under fx/ - - var materializedAssets = manifest.Assets - - .Where(a => a.RelativePath.Contains(".js") - - && a.Identity.Contains(Path.Combine("fx", "ClassLibrary"))) - - .ToList(); - - - - materializedAssets.Should().NotBeEmpty( - - "JS assets matching FrameworkPattern should be materialized under the fx/ directory"); - - - - foreach (var asset in materializedAssets) - - { - - // SourceType should be Discovered (changed from Framework during materialization) - - asset.SourceType.Should().Be("Discovered", - - $"materialized framework asset {asset.RelativePath} should have SourceType=Discovered"); - - - - // SourceId should be updated to the consuming project's PackageId - - asset.SourceId.Should().Be("AppWithP2PReference", - - $"materialized framework asset {asset.RelativePath} should have SourceId updated to the consumer"); - - - - // BasePath should be the consumer's base path ("/" for a web app) - - asset.BasePath.Should().Be("/", - - $"materialized framework asset {asset.RelativePath} should have BasePath updated to the consumer"); - - - - // AssetMode should be CurrentProject - - asset.AssetMode.Should().Be("CurrentProject", - - $"materialized framework asset {asset.RelativePath} should have AssetMode=CurrentProject"); - - } - - } - - - - [TestMethod] - - public void Build_Consumer_NonMatchingAssetsRemainUnchanged() - - { - - var intermediateOutputPath = BuildConsumerWithFrameworkPattern(); - - var manifest = LoadBuildManifest(intermediateOutputPath); - - - - // CSS assets from ClassLibrary should remain as Project type (they don't match **/*.js) - - var cssAssets = manifest.Assets - - .Where(a => a.RelativePath.Contains(".css") && a.SourceId == "ClassLibrary") - - .ToList(); - - - - cssAssets.Should().NotBeEmpty("CSS assets from ClassLibrary should be present"); - - cssAssets.Should().OnlyContain(a => a.SourceType == "Project", - - "CSS assets not matching FrameworkPattern should remain as Project type"); - - } - - - - [TestMethod] - - public void Build_Consumer_MaterializedFrameworkAssetFilesExistOnDisk() - - { - - var intermediateOutputPath = BuildConsumerWithFrameworkPattern(); - - - - var fxDir = Path.Combine(intermediateOutputPath, "staticwebassets", "fx", "ClassLibrary"); - - Directory.Exists(fxDir).Should().BeTrue("the fx/ClassLibrary directory should be created"); - - - - var materializedFiles = Directory.GetFiles(fxDir, "*.js", SearchOption.AllDirectories); - - materializedFiles.Should().NotBeEmpty("JS framework assets should be copied to the fx/ directory"); - - } - - - - [TestMethod] - - public void Build_Consumer_EndpointsExistForMaterializedFrameworkAssets() - - { - - var intermediateOutputPath = BuildConsumerWithFrameworkPattern(); - - var manifest = LoadBuildManifest(intermediateOutputPath); - - - - var materializedAssets = manifest.Assets - - .Where(a => a.RelativePath.Contains(".js") - - && a.Identity.Contains(Path.Combine("fx", "ClassLibrary"))) - - .ToList(); - - - - materializedAssets.Should().NotBeEmpty(); - - - - // Each materialized asset should have at least one endpoint referencing it - - foreach (var asset in materializedAssets) - - { - - var matchingEndpoints = manifest.Endpoints - - ?.Where(e => string.Equals(e.AssetFile, asset.Identity, StringComparison.OrdinalIgnoreCase)) - - .ToList(); - - - - matchingEndpoints.Should().NotBeNullOrEmpty( - - $"materialized framework asset {asset.RelativePath} should have at least one endpoint"); - - - - // Endpoint routes should NOT contain the library's base path — they should - - // reflect the consumer's base path (which is "/" for a web app). - - foreach (var ep in matchingEndpoints) - - { - - ep.Route.Should().NotContain("_content/ClassLibrary", - - "endpoint route should not retain the library's base path after materialization"); - - } - - } - - } - - - - private string BuildConsumerWithFrameworkPattern() - - { - - var projectDirectory = CreateAspNetSdkTestAsset("RazorAppWithP2PReference") - - .WithProjectChanges((path, document) => - - { - - if (Path.GetFileName(path) == "ClassLibrary.csproj") - - { - - // Add FrameworkPattern to mark all .js files as framework assets - - var propertyGroup = document.Root.Descendants("TargetFramework").First().Parent; - - propertyGroup.Add( - - new XElement("StaticWebAssetFrameworkPattern", "**/*.js")); - - } - - }); - - - - var build = CreateBuildCommand(projectDirectory, "AppWithP2PReference"); - - ExecuteCommand(build).Should().Pass(); - - - - return build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - } - - - - private static StaticWebAssetsManifest LoadBuildManifest(string intermediateOutputPath) - - { - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - return StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/GroupedFrameworkAssetsIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/GroupedFrameworkAssetsIntegrationTest.cs index d2fc5409aed8..d5cd9612dd90 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/GroupedFrameworkAssetsIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/GroupedFrameworkAssetsIntegrationTest.cs @@ -2,329 +2,116 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Xml.Linq; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class GroupedFrameworkAssetsIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest - { - protected override string RestoreNugetPackagePath => nameof(GroupedFrameworkAssetsIntegrationTest); - - // Regression coverage for the blazor.webassembly.js 404. A package ships an asset that is both a - - // framework asset and a member of a group (as Microsoft.AspNetCore.Components.WebAssembly does for - - // blazor.webassembly.js). The scenario is Package -> Library -> App: - - // * The group is opt-in via the IncludeGroupedFrameworkAssets property set by the library, so - - // inclusion is conditional. - - // * When the library enables the group, the framework asset materializes into the library under the - - // library base path (_content/) with its AssetGroups cleared. If AssetGroups is not - - // cleared during materialization, downstream endpoint generation skips the asset and it 404s. - - // * The materialized framework asset is a current-project asset of the library, so the app (which - - // does not enable the group) does not include it at the root. - - [TestMethod] - - [DataRow(true)] - - [DataRow(false)] - - public void Build_PackageToLibraryToApp_GroupedFrameworkAsset_IsConditionalAndMaterializedIntoLibrary(bool includeGroupedFrameworkAssets) - - { - - ProjectDirectory = CreateAspNetSdkTestAsset("GroupedFrameworkAssetsSample", identifier: includeGroupedFrameworkAssets.ToString()) - - .WithProjectChanges((path, document) => - - { - - if (Path.GetFileName(path) == "GroupedFrameworkLibrary.csproj") - - { - - // Only the library opts into the group, so the app does not re-include the asset. - - var propertyGroup = document.Root.Descendants("TargetFramework").First().Parent; - - propertyGroup.Add( - - new XElement("IncludeGroupedFrameworkAssets", includeGroupedFrameworkAssets.ToString())); - - } - - }); - - - - var pack = CreatePackCommand(ProjectDirectory, "GroupedFrameworkPackage"); - - ExecuteCommand(pack).Should().Pass(); - - ClearCachedPackage("groupedframeworkpackage"); - - - - var build = CreateBuildCommand(ProjectDirectory, "GroupedFrameworkApp"); - - ExecuteCommand(build).Should().Pass(); - - - - var libraryManifest = LoadBuildManifest( - - Path.Combine(ProjectDirectory.TestRoot, "GroupedFrameworkLibrary", "obj", "Debug", DefaultTfm)); - - var appManifest = LoadBuildManifest(build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString()); - - - - // The materialized asset lives under fx\ (the originating framework - - // package), but is owned by the consuming library (SourceId, BasePath are remapped). - - var libraryMaterializedJs = libraryManifest.Assets - - .Where(a => a.RelativePath.Contains(".js") - - && a.Identity.Contains(Path.Combine("fx", "GroupedFrameworkPackage"))) - - .ToList(); - - - - if (includeGroupedFrameworkAssets) - - { - - libraryMaterializedJs.Should().NotBeEmpty( - - "the grouped JS framework asset should be materialized into the library when the group is enabled"); - - - - foreach (var asset in libraryMaterializedJs) - - { - - // Materialized into the library, under the library base path. - - asset.SourceId.Should().Be("GroupedFrameworkLibrary", - - $"materialized framework asset {asset.RelativePath} should belong to the library"); - - asset.BasePath.Should().Be("_content/GroupedFrameworkLibrary", - - $"materialized framework asset {asset.RelativePath} should be under the library base path"); - - - - // AssetGroups must be cleared, otherwise endpoint generation skips the asset (the 404). - - asset.AssetGroups.Should().BeNullOrEmpty( - - $"materialized framework asset {asset.RelativePath} must have its AssetGroups cleared"); - - } - - - - // The framework asset is a current-project asset of the library, so the app does not - - // include it at the root. - - appManifest.Assets - - .Where(a => a.RelativePath.Contains("feature") && a.BasePath == "/") - - .Should().BeEmpty("the app should not include the library's framework asset at the root"); - - } - - else - - { - - // The group is not enabled, so the grouped framework asset is excluded entirely. - - libraryMaterializedJs.Should().BeEmpty( - - "the grouped JS framework asset should be excluded when the group is not enabled"); - - } - - } - - - - private static StaticWebAssetsManifest LoadBuildManifest(string intermediateOutputPath) - - { - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - new FileInfo(path).Should().Exist(); - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - manifest.Should().NotBeNull(); - - return manifest; - - } - - - - // Clear the cached package so NuGet re-extracts from the freshly-packed nupkg. - - private void ClearCachedPackage(string packageId) - - { - - var cached = Path.Combine(GetNuGetCachePath(), packageId); - - if (Directory.Exists(cached)) - - { - - Directory.Delete(cached, recursive: true); - - } - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/IsolatedNuGetPackageFolderAspNetSdkBaselineTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/IsolatedNuGetPackageFolderAspNetSdkBaselineTest.cs index 6bcf96ff6b33..53ee63351588 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/IsolatedNuGetPackageFolderAspNetSdkBaselineTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/IsolatedNuGetPackageFolderAspNetSdkBaselineTest.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; @@ -10,41 +10,21 @@ namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - { - [TestCategory("NugetIsolation")] [TestCategory("BaselineTest")] [TestProperty("AspNetCore", "NugetIsolation")] - [TestProperty("AspNetCore", "BaselineTest")] - public abstract class IsolatedNuGetPackageFolderAspNetSdkBaselineTest : AspNetSdkBaselineTest - { - protected abstract string RestoreNugetPackagePath { get; } - - - private string? _cachePath; - - - protected override string GetNuGetCachePath() => - _cachePath ??= Path.GetFullPath(Path.Combine(SdkTestContext.Current.TestExecutionDirectory, Shorten(RestoreNugetPackagePath))); - - private static string Shorten(string restoreNugetPackagePath) => - restoreNugetPackagePath - .Replace("IntegrationTest", string.Empty, StringComparison.OrdinalIgnoreCase) - .Replace("Tests", string.Empty, StringComparison.OrdinalIgnoreCase); - } - } diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/JsModulesIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/JsModulesIntegrationTest.cs index 465ae5302515..58888340bd1b 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/JsModulesIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/JsModulesIntegrationTest.cs @@ -2,1046 +2,356 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class JsModulesIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest - { - protected override string RestoreNugetPackagePath => nameof(JsModulesIntegrationTest); - - [TestMethod] - - public void Build_NoOps_WhenJsModulesIsDisabled() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - Directory.CreateDirectory(Path.Combine(projectDirectory.TestRoot, "wwwroot")); - - File.WriteAllText(Path.Combine(projectDirectory.TestRoot, "wwwroot", "ComponentApp.lib.module.js"), "console.log('Hello world!');"); - - - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build, "/p:JsModulesEnabled=false").Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - - - new FileInfo(Path.Combine(intermediateOutputPath, "jsmodules", "jsmodules.build.manifest.json")).Should().NotExist(); - - } - - - - [TestMethod] - - public void Build_GeneratesManifestWhenItFindsALibrary() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset) - - .WithProjectChanges(p => { - - var fingerprintContent = p.Descendants() - - .SingleOrDefault(e => e.Name.LocalName == "StaticWebAssetsFingerprintContent"); - - fingerprintContent.Value = "true"; - - }); - - - - Directory.CreateDirectory(Path.Combine(projectDirectory.TestRoot, "wwwroot")); - - File.WriteAllText(Path.Combine(projectDirectory.TestRoot, "wwwroot", "ComponentApp.lib.module.js"), "console.log('Hello world!');"); - - - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - - - var file = new FileInfo(Path.Combine(intermediateOutputPath, "jsmodules", "jsmodules.build.manifest.json")); - - file.Should().Exist(); - - file.Should().Match("""ComponentApp\.[a-zA-Z-0-9]{10}\.lib\.module\.js"""); - - } - - - - [TestMethod] - - public void Build_DiscoversJsModulesBasedOnPatterns() - - { - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - // Components - - CreateFile("", ProjectDirectory.TestRoot, "Components", "Pages", "Counter.razor.js"); - - - - // MVC | Razor pages - - CreateFile("", ProjectDirectory.TestRoot, "Pages", "Index.cshtml"); - - CreateFile("", ProjectDirectory.TestRoot, "Pages", "Index.cshtml.js"); - - - - var build = CreateBuildCommand(ProjectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(finalPath)); - - AssertManifest( - - buildManifest, - - LoadBuildManifest()); - - - - buildManifest.Should().NotBeNull(); - - buildManifest.DiscoveryPatterns.Should().BeEmpty(); - - - - AssertBuildAssets( - - buildManifest, - - outputPath, - - intermediateOutputPath); - - } - - - - [TestMethod] - - public void Publish_PublishesJsModuleBundleBundleToTheRightLocation() - - { - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) - - .WithProjectChanges(p => { - - var fingerprintContent = p.Descendants() - - .SingleOrDefault(e => e.Name.LocalName == "StaticWebAssetsFingerprintContent"); - - fingerprintContent.Value = "true"; - - }); - - Directory.CreateDirectory(Path.Combine(ProjectDirectory.TestRoot, "wwwroot")); - - File.WriteAllText(Path.Combine(ProjectDirectory.TestRoot, "wwwroot", "ComponentApp.lib.module.js"), "console.log('Hello world!');"); - - - - var publish = CreatePublishCommand(ProjectDirectory); - - var publishResult = ExecuteCommand(publish); - - publishResult.Should().Pass(); - - - - var outputPath = publish.GetOutputDirectory(DefaultTfm).ToString(); - - var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - new FileInfo(path).Should().Exist(); - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - AssertManifest(manifest, LoadPublishManifest()); - - - - AssertPublishAssets( - - manifest, - - outputPath, - - intermediateOutputPath); - - } - - - - [TestMethod] - - public void Publish_DoesNotPublishAnyFile_WhenThereAreNoJsModulesFiles() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var publish = CreatePublishCommand(projectDirectory); - - ExecuteCommand(publish).Should().Pass(); - - - - var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "ComponentApp.lib.module.js")).Should().NotExist(); - - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "ComponentApp.modules.json")).Should().NotExist(); - - - } - - - - + } [TestMethod] - - public void Does_Nothing_WhenThereAreNoJsModulesFiles() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - - - var file = new FileInfo(Path.Combine(intermediateOutputPath, "jsmodules", "jsmodules.build.manifest.json")); - - file.Should().NotExist(); - - } - - - - [TestMethod] - - public void Build_JsModules_IsIncremental() - - { - - // Arrange - - var thumbprintLookup = new Dictionary(); - - - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - Directory.CreateDirectory(Path.Combine(projectDirectory.TestRoot, "wwwroot")); - - File.WriteAllText(Path.Combine(projectDirectory.TestRoot, "wwwroot", "ComponentApp.lib.module.js"), "console.log('Hello world!');"); - - - - // Act & Assert 1 - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - var directoryPath = Path.Combine(intermediateOutputPath, "jsmodules"); - - - - var files = Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories); - - foreach (var file in files) - - { - - var thumbprint = FileThumbPrint.Create(file); - - thumbprintLookup[file] = thumbprint; - - } - - - - // Act & Assert 2 - - for (var i = 0; i < 2; i++) - - { - - build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - foreach (var file in files) - - { - - var thumbprint = FileThumbPrint.Create(file); - - Assert.AreEqual(thumbprintLookup[file], thumbprint); - - } - - } - - } - - - - private void CreateFile(string content, params string[] path) - - { - - Directory.CreateDirectory(Path.Combine(path[..^1].Prepend(ProjectDirectory.TestRoot).ToArray())); - - File.WriteAllText(Path.Combine(path.Prepend(ProjectDirectory.TestRoot).ToArray()), content); - - } - - } - [TestClass] - - - - public class JsModulesPackagesIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest - { - protected override string RestoreNugetPackagePath => nameof(JsModulesPackagesIntegrationTest); - - [TestMethod] - - public void BuildProjectWithReferences_IncorporatesInitializersFromClassLibraries() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - CreateFile("console.log('Hello world AnotherClassLib')", "AnotherClassLib", "wwwroot", "AnotherClassLib.lib.module.js"); - - CreateFile("console.log('Hello world ClassLibrary')", "ClassLibrary", "wwwroot", "ClassLibrary.lib.module.js"); - - - - var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(finalPath)); - - AssertManifest( - - manifest, - - LoadBuildManifest()); - - - - AssertBuildAssets( - - manifest, - - outputPath, - - intermediateOutputPath); - - - - var file = new FileInfo(Path.Combine(intermediateOutputPath, "jsmodules", "jsmodules.build.manifest.json")); - - file.Should().Exist(); - - file.Should().Contain("_content/AnotherClassLib/AnotherClassLib.lib.module.js"); - - file.Should().Contain("_content/ClassLibrary/ClassLibrary.lib.module.js"); - - } - - - - [TestMethod] - - public void PublishProjectWithReferences_IncorporatesInitializersFromClassLibrariesAndPublishesAssetsToTheRightLocation() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - CreateFile("console.log('Hello world AnotherClassLib')", "AnotherClassLib", "wwwroot", "AnotherClassLib.lib.module.js"); - - - - // Notice that it does not follow the pattern $(PackageId).lib.module.js - - CreateFile("console.log('Hello world ClassLibrary')", "ClassLibrary", "wwwroot", "AnotherClassLib.lib.module.js"); - - - - var publish = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(publish).Should().Pass(); - - - - var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var outputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - AssertManifest( - - buildManifest, - - LoadBuildManifest()); - - - - var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(finalPath)); - - AssertManifest( - - publishManifest, - - LoadPublishManifest()); - - - - AssertPublishAssets( - - publishManifest, - - outputPath, - - intermediateOutputPath); - - - - var file = new FileInfo(Path.Combine(outputPath, "wwwroot", "AppWithPackageAndP2PReference.modules.json")); - - file.Should().Exist(); - - file.Should().Contain("_content/AnotherClassLib/AnotherClassLib.lib.module.js"); - - file.Should().NotContain("_content/ClassLibrary/AnotherClassLib.lib.module.js"); - - } - - - - [TestMethod] - - public void PublishProjectWithReferences_DifferentBuildAndPublish_LibraryInitializers() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - CreateFile("console.log('Hello world AnotherClassLib publish')", "AnotherClassLib", "wwwroot", "AnotherClassLib.lib.module.js"); - - CreateFile("console.log('Hello world AnotherClassLib')", "AnotherClassLib", "wwwroot", "AnotherClassLib.lib.module.build.js"); - - ProjectDirectory.WithProjectChanges((project, document) => - - { - - if (project.EndsWith("AnotherClassLib.csproj")) - - { - - document.Root.Add(new XElement("ItemGroup", - - new XElement("Content", - - new XAttribute("Update", "wwwroot\\AnotherClassLib.lib.module.build.js"), - - new XAttribute("CopyToPublishDirectory", "Never"), - - new XAttribute("TargetPath", "wwwroot\\AnotherClassLib.lib.module.js")))); - - } - - }); - - var publish = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(publish).Should().Pass(); - - - - var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var outputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); ; - - var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - - - var initializers = buildManifest.Assets.Where(a => a.RelativePath == "AnotherClassLib.lib.module.js"); - - initializers.Should().HaveCount(1); - - initializers.Should().Contain(a => a.IsBuildOnly()); - - - - AssertManifest( - - buildManifest, - - LoadBuildManifest()); - - - - var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(finalPath)); - - AssertManifest( - - manifest, - - LoadPublishManifest()); - - - - AssertBuildAssets( - - manifest, - - outputPath, - - intermediateOutputPath); - - - - var modulesManifest = new FileInfo(Path.Combine(outputPath, "wwwroot", "AppWithPackageAndP2PReference.modules.json")); - - modulesManifest.Should().Exist(); - - modulesManifest.Should().Contain("_content/AnotherClassLib/AnotherClassLib.lib.module.js"); - - modulesManifest.Should().NotContain("_content/ClassLibrary/AnotherClassLib.lib.module.js"); - - - - var moduleFile = new FileInfo(Path.Combine(outputPath, "wwwroot", "_content", "AnotherClassLib", "AnotherClassLib.lib.module.js")); - - moduleFile.Should().Exist(); - - moduleFile.Should().Contain("console.log('Hello world AnotherClassLib publish')"); - - } - - - - private void CreateFile(string content, params string[] path) - - { - - Directory.CreateDirectory(Path.Combine(path[..^1].Prepend(ProjectDirectory.TestRoot).ToArray())); - - File.WriteAllText(Path.Combine(path.Prepend(ProjectDirectory.TestRoot).ToArray()), content); - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/LegacyStaticWebAssetsV1IntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/LegacyStaticWebAssetsV1IntegrationTest.cs index 4d4fe9a2997f..ab19e8d1a519 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/LegacyStaticWebAssetsV1IntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/LegacyStaticWebAssetsV1IntegrationTest.cs @@ -2,389 +2,136 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.StaticWebAssets.Tasks; + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class LegacyStaticWebAssetsV1IntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest - { - protected override string RestoreNugetPackagePath => nameof(LegacyStaticWebAssetsV1IntegrationTest); - - [TestMethod] - - public void PublishProjectWithReferences_WorksWithStaticWebAssetsV1ClassLibraries() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) - - .WithProjectChanges((project, document) => - - { - - if (Path.GetFileName(project) == "AnotherClassLib.csproj") - - { - - document.Descendants("TargetFramework").Single().ReplaceNodes("netstandard2.1"); - - document.Descendants("FrameworkReference").Single().Remove(); - - document.Descendants("PropertyGroup").First().Add(new XElement("RazorLangVersion", "3.0")); - - } - - if (Path.GetFileName(project) == "ClassLibrary.csproj") - - { - - document.Descendants("TargetFramework").Single().ReplaceNodes("netstandard2.0"); - - document.Descendants("FrameworkReference").Single().Remove(); - - document.Descendants("PropertyGroup").First().Add(new XElement("RazorLangVersion", "3.0")); - - } - - }); - - - - // We are deleting Views and Components because we are only interested in the static web assets behavior for this test - - // and this makes it easier to validate the test. - - Directory.Delete(Path.Combine(ProjectDirectory.TestRoot, "AnotherClassLib", "Views"), recursive: true); - - Directory.Delete(Path.Combine(ProjectDirectory.TestRoot, "ClassLibrary", "Views"), recursive: true); - - Directory.Delete(Path.Combine(ProjectDirectory.TestRoot, "ClassLibrary", "Components"), recursive: true); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - var publish = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(publish).Should().Pass(); - - - - var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var publishPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - // GenerateStaticWebAssetsManifest should generate the manifest file. - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - new FileInfo(path).Should().Exist(); - - AssertManifest( - - StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)), - - LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(publishPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().NotExist(); - - - - // GenerateStaticWebAssetsPublishManifest should generate the publish manifest file. - - var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - new FileInfo(path).Should().Exist(); - - var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); - - AssertManifest( - - publishManifest, - - LoadPublishManifest()); - - - - AssertPublishAssets( - - publishManifest, - - publishPath, - - intermediateOutputPath); - - } - - - - [TestMethod] - - public void BuildProjectWithReferences_WorksWithStaticWebAssetsV1ClassLibraries() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) - - .WithProjectChanges((project, document) => - - { - - if (Path.GetFileName(project) == "AnotherClassLib.csproj") - - { - - document.Descendants("TargetFramework").Single().ReplaceNodes("netstandard2.1"); - - document.Descendants("FrameworkReference").Single().Remove(); - - document.Descendants("PropertyGroup").First().Add(new XElement("RazorLangVersion", "3.0")); - - } - - if (Path.GetFileName(project) == "ClassLibrary.csproj") - - { - - document.Descendants("TargetFramework").Single().ReplaceNodes("netstandard2.0"); - - document.Descendants("FrameworkReference").Single().Remove(); - - document.Descendants("PropertyGroup").First().Add(new XElement("RazorLangVersion", "3.0")); - - } - - }); - - - - // We are deleting Views and Components because we are only interested in the static web assets behavior for this test - - // and this makes it easier to validate the test. - - Directory.Delete(Path.Combine(ProjectDirectory.TestRoot, "AnotherClassLib", "Views"), recursive: true); - - Directory.Delete(Path.Combine(ProjectDirectory.TestRoot, "ClassLibrary", "Views"), recursive: true); - - Directory.Delete(Path.Combine(ProjectDirectory.TestRoot, "ClassLibrary", "Components"), recursive: true); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - 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.Combine(intermediateOutputPath, "staticwebassets.build.json"))); - - AssertManifest( - - manifest, - - LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - - - AssertBuildAssets( - - manifest, - - outputPath, - - intermediateOutputPath); - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj index 403bb5d40e5e..4f33ec6df026 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/Microsoft.NET.Sdk.StaticWebAssets.Tests.csproj @@ -9,6 +9,15 @@ $(SdkTargetFramework) + + + None + + testSdkStaticWebAssets diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs index d1df366e1bc8..f49728631816 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/ScopedCssIntegrationTests.cs @@ -2,2352 +2,793 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Text.Json; - - using System.Text.RegularExpressions; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class ScopedCssIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest - { - protected override string RestoreNugetPackagePath => nameof(ScopedCssIntegrationTest); - - [TestMethod] - - public void Build_NoOps_WhenScopedCssIsDisabled() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build, "/p:ScopedCssEnabled=false").Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css")).Should().NotExist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "FetchData.razor.rz.scp.css")).Should().NotExist(); - - } - - - - [TestMethod] - - public void Build_NoOps_ForMvcApp_WhenScopedCssIsDisabled() - - { - - var testAsset = "RazorSimpleMvc"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build, "/p:ScopedCssEnabled=false").Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Index.cshtml.rz.scp.css")).Should().NotExist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Contact.cshtml.rz.scp.css")).Should().NotExist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "SimpleMvc.styles.css")).Should().NotExist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "About.cshtml.rz.scp.css")).Should().NotExist(); - - } - - - - [TestMethod] - - public void CanDisableDefaultDiscoveryConvention() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build, "/p:EnableDefaultScopedCssItems=false").Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css")).Should().NotExist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "FetchData.razor.rz.scp.css")).Should().NotExist(); - - } - - - - [TestMethod] - [CoreMSBuildOnly] - - public void CanOverrideScopeIdentifiers() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset) - - .WithProjectChanges(project => - - { - - var ns = project.Root.Name.Namespace; - - var itemGroup = new XElement(ns + "ItemGroup"); - - var element = new XElement("ScopedCssInput", new XAttribute("Include", @"Styles\Pages\Counter.css")); - - element.Add(new XElement("RazorComponent", @"Components\Pages\Counter.razor")); - - element.Add(new XElement("CssScope", "b-overridden")); - - itemGroup.Add(element); - - project.Root.Add(itemGroup); - - }); - - - - var stylesFolder = Path.Combine(projectDirectory.Path, "Styles", "Pages"); - - Directory.CreateDirectory(stylesFolder); - - var styles = Path.Combine(stylesFolder, "Counter.css"); - - File.Move(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css"), styles); - - - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build, "/p:EnableDefaultScopedCssItems=false", "/p:EmitCompilerGeneratedFiles=true").Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - - - var scoped = Path.Combine(intermediateOutputPath, "scopedcss", "Styles", "Pages", "Counter.rz.scp.css"); - - new FileInfo(scoped).Should().Exist(); - - new FileInfo(scoped).Should().Contain("b-overridden"); - - var generated = Path.Combine(intermediateOutputPath, "generated", "Microsoft.CodeAnalysis.Razor.Compiler", "Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator", "Components", "Pages", "Counter_razor.g.cs"); - - new FileInfo(generated).Should().Exist(); - - new FileInfo(generated).Should().Contain("b-overridden"); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist(); - - } - - - - [TestMethod] - - public void Build_GeneratesTransformedFilesAndBundle_ForComponentsWithScopedCss() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().Exist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().Exist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css")).Should().Exist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "projectbundle", "ComponentApp.bundle.scp.css")).Should().Exist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "FetchData.razor.rz.scp.css")).Should().NotExist(); - - } - - - - [TestMethod] - - public void Build_GeneratesTransformedFilesAndBundle_ForViewsWithScopedCss() - - { - - var testAsset = "RazorSimpleMvc"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Index.cshtml.rz.scp.css")).Should().Exist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Contact.cshtml.rz.scp.css")).Should().Exist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "SimpleMvc.styles.css")).Should().Exist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "projectbundle", "SimpleMvc.bundle.scp.css")).Should().Exist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "About.cshtml.rz.scp.css")).Should().Exist(); - - } - - - - [TestMethod] - - public void Build_ScopedCssFiles_ContainsUniqueScopesPerFile() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - - - var generatedCounter = Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css"); - - new FileInfo(generatedCounter).Should().Exist(); - - var generatedIndex = Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css"); - - new FileInfo(generatedIndex).Should().Exist(); - - var counterContent = File.ReadAllText(generatedCounter); - - var indexContent = File.ReadAllText(generatedIndex); - - - - var counterScopeMatch = Regex.Match(counterContent, ".*button\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); - - Assert.IsTrue(counterScopeMatch.Success, "Couldn't find a scope id in the generated Counter scoped css file."); - - var counterScopeId = counterScopeMatch.Groups[1].Captures[0].Value; - - - - var indexScopeMatch = Regex.Match(indexContent, ".*h1\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); - - Assert.IsTrue(indexScopeMatch.Success, "Couldn't find a scope id in the generated Index scoped css file."); - - var indexScopeId = indexScopeMatch.Groups[1].Captures[0].Value; - - - - Assert.AreNotEqual(counterScopeId, indexScopeId); - - } - - - - [TestMethod] - - public void Build_ScopedCssViews_ContainsUniqueScopesPerView() - - { - - var testAsset = "RazorSimpleMvc"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - - - var generatedIndex = Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Index.cshtml.rz.scp.css"); - - new FileInfo(generatedIndex).Should().Exist(); - - var generatedAbout = Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "About.cshtml.rz.scp.css"); - - new FileInfo(generatedAbout).Should().Exist(); - - var generatedContact = Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Contact.cshtml.rz.scp.css"); - - new FileInfo(generatedContact).Should().Exist(); - - var indexContent = File.ReadAllText(generatedIndex); - - var aboutContent = File.ReadAllText(generatedAbout); - - var contactContent = File.ReadAllText(generatedContact); - - - - var indexScopeMatch = Regex.Match(indexContent, ".*p\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); - - Assert.IsTrue(indexScopeMatch.Success, "Couldn't find a scope id in the generated Index scoped css file."); - - var indexScopeId = indexScopeMatch.Groups[1].Captures[0].Value; - - - - var aboutScopeMatch = Regex.Match(aboutContent, ".*h2\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); - - Assert.IsTrue(aboutScopeMatch.Success, "Couldn't find a scope id in the generated About scoped css file."); - - var aboutScopeId = aboutScopeMatch.Groups[1].Captures[0].Value; - - - - var contactScopeMatch = Regex.Match(contactContent, ".*a\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); - - Assert.IsTrue(contactScopeMatch.Success, "Couldn't find a scope id in the generated Contact scoped css file."); - - var contactScopeId = contactScopeMatch.Groups[1].Captures[0].Value; - - - - Assert.AreNotEqual(indexScopeId, aboutScopeId); - - Assert.AreNotEqual(indexScopeId, contactScopeId); - - Assert.AreNotEqual(aboutScopeId, contactScopeId); - - } - - - - [TestMethod] - - public void Build_WorksWhenViewsAndComponentsArePartOfTheSameProject_ContainsUniqueScopesPerFile() - - { - - var testAsset = "RazorMvcWithComponents"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - - - var generatedIndex = Path.Combine(intermediateOutputPath, "scopedcss", "Views", "Home", "Index.cshtml.rz.scp.css"); - - new FileInfo(generatedIndex).Should().Exist(); - - - - var generatedCounter = Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Counter.razor.rz.scp.css"); - - new FileInfo(generatedCounter).Should().Exist(); - - - - var indexContent = File.ReadAllText(generatedIndex); - - var counterContent = File.ReadAllText(generatedCounter); - - - - var indexScopeMatch = Regex.Match(indexContent, ".*p\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); - - Assert.IsTrue(indexScopeMatch.Success, "Couldn't find a scope id in the generated Index scoped css file."); - - var indexScopeId = indexScopeMatch.Groups[1].Captures[0].Value; - - - - var counterScopeMatch = Regex.Match(counterContent, ".*div\\[(.*)\\].*", RegexOptions.Multiline | RegexOptions.IgnoreCase); - - Assert.IsTrue(counterScopeMatch.Success, "Couldn't find a scope id in the generated Counter scoped css file."); - - var counterScopeId = counterScopeMatch.Groups[1].Captures[0].Value; - - - - Assert.AreNotEqual(indexScopeId, counterScopeId); - - } - - - - [TestMethod] - - public void Publish_PublishesScopedCssBundleToTheRightLocation() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var publish = CreatePublishCommand(projectDirectory); - - ExecuteCommand(publish).Should().Pass(); - - - - var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "ComponentApp.styles.css")).Should().Exist(); - - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist(); - - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); - - } - - - - [TestMethod] - - public void Publish_NoBuild_PublishesBundleToTheRightLocation() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(projectDirectory); - - var buildResult = ExecuteCommand(build); - - buildResult.Should().Pass(); - - - - var publish = CreatePublishCommand(projectDirectory); - - ExecuteCommand(publish, "/p:NoBuild=true").Should().Pass(); - - - - var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "ComponentApp.styles.css")).Should().Exist(); - - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist(); - - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); - - } - - - - [TestMethod] - - public void Publish_DoesNotPublishAnyFile_WhenThereAreNoScopedCssFiles() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css")); - - File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Index.razor.css")); - - - - var publish = CreatePublishCommand(projectDirectory); - - ExecuteCommand(publish).Should().Pass(); - - - - var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "_framework", "scoped.styles.css")).Should().NotExist(); - - } - - - - [TestMethod] - - public void Publish_Publishes_IndividualScopedCssFiles_WhenNoBundlingIsEnabled() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var publish = CreatePublishCommand(projectDirectory); - - ExecuteCommand(publish, "/p:DisableScopedCssBundling=true").Should().Pass(); - - - - var publishOutputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "_content", "ComponentApp", "ComponentApp.styles.css")).Should().NotExist(); - - - - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "Components", "Pages", "Index.razor.rz.scp.css")).Should().Exist(); - - new FileInfo(Path.Combine(publishOutputPath, "wwwroot", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().Exist(); - - } - - - - [TestMethod] - [CoreMSBuildOnly] - - public void Build_RemovingScopedCssAndBuilding_UpdatesGeneratedCodeAndBundle() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build, "/p:EmitCompilerGeneratedFiles=true").Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().Exist(); - - var generatedBundle = Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css"); - - new FileInfo(generatedBundle).Should().Exist(); - - var generatedProjectBundle = Path.Combine(intermediateOutputPath, "scopedcss", "projectbundle", "ComponentApp.bundle.scp.css"); - - new FileInfo(generatedProjectBundle).Should().Exist(); - - var generatedCounter = Path.Combine(intermediateOutputPath, "generated", "Microsoft.CodeAnalysis.Razor.Compiler", "Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator", "Components", "Pages", "Counter_razor.g.cs"); - - new FileInfo(generatedCounter).Should().Exist(); - - - - var componentThumbprint = FileThumbPrint.Create(generatedCounter); - - var bundleThumbprint = FileThumbPrint.Create(generatedBundle); - - - - File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css")); - - - - build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build, "/p:EmitCompilerGeneratedFiles=true").Should().Pass(); - - - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); - - new FileInfo(generatedCounter).Should().Exist(); - - - - var newComponentThumbprint = FileThumbPrint.Create(generatedCounter); - - var newBundleThumbprint = FileThumbPrint.Create(generatedBundle); - - - - Assert.AreNotEqual(componentThumbprint, newComponentThumbprint); - - Assert.AreNotEqual(bundleThumbprint, newBundleThumbprint); - - } - - - - [TestMethod] - - public void Does_Nothing_WhenThereAreNoScopedCssFiles() - - { - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Counter.razor.css")); - - File.Delete(Path.Combine(projectDirectory.Path, "Components", "Pages", "Index.razor.css")); - - - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css")).Should().NotExist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Index.razor.rz.scp.css")).Should().NotExist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "_framework", "scoped.styles.css")).Should().NotExist(); - - } - - - - [TestMethod] - - public void Build_ScopedCssTransformation_AndBundling_IsIncremental() - - { - - // Arrange - - var thumbprintLookup = new Dictionary(); - - - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - // Act & Assert 1 - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - var directoryPath = Path.Combine(intermediateOutputPath, "scopedcss"); - - - - var files = Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories); - - foreach (var file in files) - - { - - var thumbprint = FileThumbPrint.Create(file); - - thumbprintLookup[file] = thumbprint; - - } - - - - // Act & Assert 2 - - for (var i = 0; i < 2; i++) - - { - - build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - foreach (var file in files) - - { - - var thumbprint = FileThumbPrint.Create(file); - - Assert.AreEqual(thumbprintLookup[file], thumbprint); - - } - - } - - } - - - - // Regression test for https://github.com/dotnet/sdk/issues/50646 - - [TestMethod] - - public void Build_RegeneratesScopedCss_WhenCssScopeMetadataChanges() - - { - - // Arrange - - var testAsset = "RazorComponentApp"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - // Act 1: First build without custom scope - - var build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = Path.Combine(build.GetBaseIntermediateDirectory().ToString(), "Debug", DefaultTfm); - - var scopedCssFile = Path.Combine(intermediateOutputPath, "scopedcss", "Components", "Pages", "Counter.razor.rz.scp.css"); - - var bundleFile = Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css"); - - - - new FileInfo(scopedCssFile).Should().Exist(); - - new FileInfo(bundleFile).Should().Exist(); - - - - // Get initial thumbprints - - var initialScopedCssThumbprint = FileThumbPrint.Create(scopedCssFile); - - var initialBundleThumbprint = FileThumbPrint.Create(bundleFile); - - - - // Verify initial build uses auto-generated scope (starts with 'b-') - - var initialContent = File.ReadAllText(scopedCssFile); - - initialContent.Should().MatchRegex(@"\[b-[a-z0-9]+\]"); - - - - // Act 2: Add custom CssScope metadata to the project - - File.WriteAllText( - - Path.Combine(projectDirectory.Path, "Directory.Build.targets"), - - """ - - - - - - - - my-custom-scope - - - - - - - - """); - - - - build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - // Assert: Files should be regenerated with the new scope - - var newScopedCssThumbprint = FileThumbPrint.Create(scopedCssFile); - - var newBundleThumbprint = FileThumbPrint.Create(bundleFile); - - - - Assert.AreNotEqual(initialScopedCssThumbprint, newScopedCssThumbprint); - - Assert.AreNotEqual(initialBundleThumbprint, newBundleThumbprint); - - - - // Verify the new content uses the custom scope - - var newContent = File.ReadAllText(scopedCssFile); - - newContent.Should().Contain("[my-custom-scope]"); - - newContent.Should().NotMatchRegex(@"\[b-[a-z0-9]+\]"); - - - - // Act 3: Change the custom scope to a different value - - File.WriteAllText( - - Path.Combine(projectDirectory.Path, "Directory.Build.targets"), - - """ - - - - - - - - my-updated-scope - - - - - - - - """); - - - - build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - // Assert: Files should be regenerated again with the updated scope - - var updatedScopedCssThumbprint = FileThumbPrint.Create(scopedCssFile); - - var updatedBundleThumbprint = FileThumbPrint.Create(bundleFile); - - - - Assert.AreNotEqual(newScopedCssThumbprint, updatedScopedCssThumbprint); - - Assert.AreNotEqual(newBundleThumbprint, updatedBundleThumbprint); - - - - // Verify the content uses the updated scope - - var updatedContent = File.ReadAllText(scopedCssFile); - - updatedContent.Should().Contain("[my-updated-scope]"); - - updatedContent.Should().NotContain("[my-custom-scope]"); - - - - // Act 4: Verify that building again without changes doesn't regenerate - - var finalScopedCssThumbprint = FileThumbPrint.Create(scopedCssFile); - - var finalBundleThumbprint = FileThumbPrint.Create(bundleFile); - - - - build = CreateBuildCommand(projectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - Assert.AreEqual(finalScopedCssThumbprint, FileThumbPrint.Create(scopedCssFile)); - - Assert.AreEqual(finalBundleThumbprint, FileThumbPrint.Create(bundleFile)); - - } - - - - // This test verifies if the targets that VS calls to update scoped css works to update these files - - [TestMethod] - - public void RegeneratingScopedCss_ForProject() - - { - - // Arrange - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(ProjectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var bundlePath = Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "ComponentApp.styles.css"); - - - - new FileInfo(bundlePath).Should().Exist(); - - - - // Make an edit - - var scopedCssFile = Path.Combine(ProjectDirectory.TestRoot, "Components", "Pages", "Index.razor.css"); - - File.WriteAllLines(scopedCssFile, File.ReadAllLines(scopedCssFile).Concat(["body { background-color: orangered; }"])); - - - - build = CreateBuildCommand(ProjectDirectory); - - ExecuteCommand(build, "/t:UpdateStaticWebAssetsDesignTime").Should().Pass(); - - - - - // Verify the generated file contains newly added css - - - AssertFileContains(bundlePath, "background-color: orangered"); - - - - + // Verify the generated file contains newly added css + AssertFileContains(bundlePath, "background-color: orangered"); // Verify that CSS edits continue to apply after new JS modules are added to the project - - // https://github.com/dotnet/aspnetcore/issues/57599 - - var collocatedJsFile = Path.Combine(ProjectDirectory.TestRoot, "Components", "Pages", "Index.razor.js"); - - File.WriteAllLines(collocatedJsFile, ["console.log('Hello, world!');"]); - - File.WriteAllLines(scopedCssFile, File.ReadAllLines(scopedCssFile).Concat(["h1 { color: purple; }"])); - - - - build = CreateBuildCommand(ProjectDirectory); - - ExecuteCommand(build, "/t:UpdateStaticWebAssetsDesignTime").Should().Pass(); - - - - // Verify the generated file contains newly added css - - AssertFileContains(bundlePath, "color: purple"); - - - - static void AssertFileContains(string fileName, string content) - - { - - var fileInfo = new FileInfo(fileName); - - fileInfo.Should().Exist(); - - fileInfo.ReadAllText().Should().Contain(content); - - } - - } - - } - [TestClass] - - - - public class ScopedCssCompatibilityIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest - { - protected override string RestoreNugetPackagePath => Path.Combine(nameof(ScopedCssCompatibilityIntegrationTest), ".nuget"); - - [TestMethod] - - public void ScopedCss_IsBackwardsCompatible_WithPreviousVersions() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) - - .WithProjectChanges((project, document) => - - { - - if (Path.GetFileName(project) == "AnotherClassLib.csproj") - - { - - document.Descendants("TargetFramework").Single().ReplaceNodes("net5.0"); - - } - - if (Path.GetFileName(project) == "ClassLibrary.csproj") - - { - - document.Descendants("TargetFramework").Single().ReplaceNodes("net5.0"); - - } - - }); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); - - AssertManifest( - - manifest, - - LoadBuildManifest()); - - - - AssertBuildAssets( - - manifest, - - outputPath, - - intermediateOutputPath); - - - - var appBundle = new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "AppWithPackageAndP2PReference.styles.css")); - - appBundle.Should().Exist(); - - - - appBundle.Should().Contain("_content/ClassLibrary/ClassLibrary.bundle.scp.css"); - - appBundle.Should().Match(""".*_content/RazorPackageLibraryDirectDependency/RazorPackageLibraryDirectDependency\.[a-zA-Z0-9]+\.bundle\.scp\.css.*"""); - - } - - - - [TestMethod] - - public void ScopedCss_PublishIsBackwardsCompatible_WithPreviousVersions() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) - - .WithProjectChanges((project, document) => - - { - - if (Path.GetFileName(project) == "AnotherClassLib.csproj") - - { - - document.Descendants("TargetFramework").Single().ReplaceNodes("net5.0"); - - } - - if (Path.GetFileName(project) == "ClassLibrary.csproj") - - { - - document.Descendants("TargetFramework").Single().ReplaceNodes("net5.0"); - - } - - }); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - var build = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(build, "/bl").Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - var finalPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - new FileInfo(finalPath).Should().Exist(); - - var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"))); - - AssertManifest( - - publishManifest, - - LoadPublishManifest()); - - - - AssertPublishAssets( - - publishManifest, - - outputPath, - - intermediateOutputPath); - - - - var appBundle = new FileInfo(Path.Combine(outputPath, "wwwroot", "AppWithPackageAndP2PReference.styles.css")); - - appBundle.Should().Exist(); - - - - appBundle.Should().Contain("_content/ClassLibrary/ClassLibrary.bundle.scp.css"); - - appBundle.Should().Match("""_content/RazorPackageLibraryDirectDependency/RazorPackageLibraryDirectDependency\.[a-zA-Z0-9]+\.bundle\.scp\.css"""); - - } - - } - [TestClass] - - - - public class ScopedCssPackageReferences : IsolatedNuGetPackageFolderAspNetSdkBaselineTest - { - protected override string RestoreNugetPackagePath => Path.Combine(nameof(ScopedCssPackageReferences), ".nuget"); - - [TestMethod] - - public void BuildProjectWithReferences_CorrectlyBundlesScopedCssFiles() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); + var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + ExecuteCommand(build).Should().Pass(); - - - - var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - - ExecuteCommand(build).Should().Pass(); - - - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - - var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - + var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); + var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - var buildManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); - - AssertManifest( - - buildManifest, - - LoadBuildManifest()); - - - - AssertBuildAssets( - - buildManifest, - - outputPath, - - intermediateOutputPath); - - - - var appBundle = new FileInfo(Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "AppWithPackageAndP2PReference.styles.css")); - - appBundle.Should().Exist(); - - - - appBundle.Should().Match(""".*_content/RazorPackageLibraryDirectDependency/RazorPackageLibraryDirectDependency\.[a-zA-Z0-9]+\.bundle\.scp\.css.*"""); - - appBundle.Should().Match(""".*_content/ClassLibrary/ClassLibrary\.[a-zA-Z0-9]+\.bundle\.scp\.css.*"""); - - } - - - - // Regression test for https://github.com/dotnet/aspnetcore/issues/37592 - - [TestMethod] - - public void RegeneratingScopedCss_ForProjectWithReferences() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var scopedCssFile = Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "Index.razor.css"); - - File.WriteAllText(scopedCssFile, "/* Empty css */"); - - File.WriteAllText(Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "Index.razor"), "This is a test razor component."); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var bundlePath = Path.Combine(intermediateOutputPath, "scopedcss", "bundle", "AppWithPackageAndP2PReference.styles.css"); - - - - new FileInfo(bundlePath).Should().Exist(); - - - - // Make an edit to a scoped css file - - File.WriteAllLines(scopedCssFile, File.ReadAllLines(scopedCssFile).Concat(new[] { "body { background-color: orangered; }" })); - - - - build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(build, "/t:UpdateStaticWebAssetsDesignTime").Should().Pass(); - - - - var fileInfo = new FileInfo(bundlePath); - - fileInfo.Should().Exist(); - - // Verify the generated file contains newly added css - - var text = fileInfo.ReadAllText(); - - text.Should().Contain("background-color: orangered"); - - text.Should().MatchRegex(""".*@import '_content/ClassLibrary/ClassLibrary\.[a-zA-Z0-9]+\.bundle\.scp\.css.*"""); - - } - - - - [TestMethod] - - public void Build_GeneratesUrlEncodedLinkHeaderForNonAsciiProjectName() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - // Rename the ClassLibrary project to have non-ASCII characters - - var originalLibPath = Path.Combine(ProjectDirectory.Path, "AnotherClassLib"); - - var newLibPath = Path.Combine(ProjectDirectory.Path, "项目"); - - Directory.Move(originalLibPath, newLibPath); - - - - // Update the project file to set the assembly name and package ID - - var libProjectFile = Path.Combine(newLibPath, "AnotherClassLib.csproj"); - - var newLibProjectFile = Path.Combine(newLibPath, "项目.csproj"); - - File.Move(libProjectFile, newLibProjectFile); - - - - // Add assembly name property to ensure consistent naming - - var libProjectContent = File.ReadAllText(newLibProjectFile); - - // Find the first PropertyGroup closing tag and replace it - - var targetPattern = ""; - - var replacement = " 项目\n 项目\n "; - - var index = libProjectContent.IndexOf(targetPattern); - - if (index >= 0) - - { - - libProjectContent = libProjectContent.Substring(0, index) + replacement + libProjectContent.Substring(index + targetPattern.Length); - - } - - File.WriteAllText(newLibProjectFile, libProjectContent); - - - - // Update the main project to reference the renamed library - - var mainProjectFile = Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "AppWithPackageAndP2PReference.csproj"); - - var mainProjectContent = File.ReadAllText(mainProjectFile); - - mainProjectContent = mainProjectContent.Replace(@"..\AnotherClassLib\AnotherClassLib.csproj", @"..\项目\项目.csproj"); - - File.WriteAllText(mainProjectFile, mainProjectContent); - - - - // Ensure library has scoped CSS - - var libCssFile = Path.Combine(newLibPath, "Views", "Shared", "Index.cshtml.css"); - - if (!File.Exists(libCssFile)) - - { - - Directory.CreateDirectory(Path.GetDirectoryName(libCssFile)); - - File.WriteAllText(libCssFile, ".test { color: red; }"); - - } - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - - - // Check that the staticwebassets.build.endpoints.json file contains URL-encoded characters - - var endpointsFile = Path.Combine(intermediateOutputPath, "staticwebassets.build.endpoints.json"); - - new FileInfo(endpointsFile).Should().Exist(); - - - - var endpointsContent = File.ReadAllText(endpointsFile); - - var json = JsonSerializer.Deserialize(endpointsContent, new JsonSerializerOptions(JsonSerializerDefaults.Web)); - - - - var styles = json.Endpoints.Where(e => e.Route.EndsWith("styles.css")); - - - - foreach (var styleEndpoint in styles) - - { - - styleEndpoint.ResponseHeaders.Should().Contain(h => h.Name.Equals("Link", StringComparison.OrdinalIgnoreCase) && h.Value.Contains("%E9%A1%B9%E7%9B%AE")); - - } - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetEndpointsIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetEndpointsIntegrationTest.cs index 7eece0c1b1f7..d833294b744f 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetEndpointsIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetEndpointsIntegrationTest.cs @@ -2,1963 +2,660 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - 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; - - - - [TestClass] - public partial class StaticWebAssetEndpointsIntegrationTest : AspNetSdkBaselineTest - - { - - [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(); - - - - [TestMethod] - - 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(39); - - 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"); - - - - [TestMethod] - - 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(65); - - 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()); - - } - - - - [TestMethod] - - 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); - - - - + 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(65); - - - - AssertManifest(publishManifest, LoadPublishManifest()); - - } - - - - [TestMethod] - [RequiresMSBuildVersion("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); - - } - - - - [TestMethod] - [RequiresMSBuildVersion("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); - - } - - - - [TestMethod] - [RequiresMSBuildVersion("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"); - - } - - - - [TestMethod] - [RequiresMSBuildVersion("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"); - - } - - - - [TestMethod] - - public void Build_DefaultDocumentAndSpaFallback_CreatesAdditionalEndpoints() - - { - - ProjectDirectory = CreateAspNetSdkTestAsset("RazorComponentApp") - - .WithProjectChanges(document => - - { - - document.Root.AddFirst( - - new XElement("PropertyGroup", - - new XElement("StaticWebAssetDefaultDocumentEnabled", "true"), - - new XElement("StaticWebAssetSpaFallbackEnabled", "true"))); - - }); - - var root = ProjectDirectory.TestRoot; - - - - - var dir = Directory.CreateDirectory(Path.Combine(root, "wwwroot")); - - - File.WriteAllText(Path.Combine(dir.FullName, "index.html"), "Hello"); - - - - + var dir = Directory.CreateDirectory(Path.Combine(root, "wwwroot")); + File.WriteAllText(Path.Combine(dir.FullName, "index.html"), "Hello"); var build = CreateBuildCommand(ProjectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - new FileInfo(path).Should().Exist(); - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - - - var endpoints = manifest.Endpoints; - - - - // There should be endpoints for index.html (original + fingerprinted + default document + spa fallback) - - var indexHtmlEndpoints = endpoints.Where(ep => ep.AssetFile.Contains("index.html") && !ep.AssetFile.Contains(".gz") && !ep.AssetFile.Contains(".br")); - - - - // Original index.html endpoint - - indexHtmlEndpoints.Should().Contain(e => e.Route == "index.html"); - - - - // SPA fallback endpoint with catch-all route and max int order - - var fallback = endpoints.FirstOrDefault(e => e.Route == "{**fallback:nonfile}"); - - fallback.Should().NotBeNull(); - - fallback.Order.Should().Be("2147483647"); - - - - AssertManifest(manifest, LoadBuildManifest()); - - } - - - - // 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(File.ReadAllText(endpointsManifestFile)); - - - - var wwwrootFolderFiles = GetWwwrootFolderFiles(outputDirectory); - - - - var foundAssets = new HashSet(); - - 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().HaveCountGreaterThanOrEqualTo(2); - - } - - else if (endpointsByAssetFile[file].Length > 1) - - { - - endpointsByAssetFile[file].Where(e => e.EndpointProperties.Any(p => p.Name == "integrity")).Count().Should().BeGreaterThanOrEqualTo(1); - - } - - else - - { - - endpointsByAssetFile[file].Should().HaveCount(1); - - } - - } - - - - HashSet 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(File.ReadAllText(Path.Combine(outputDirectory, $"{projectName}.staticwebassets.runtime.json"))); - - var endpoints = new HashSet(); - - - - //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 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 Children { get; set; } - - public StaticWebAssetMatch Asset { get; set; } - - public StaticWebAssetPattern[] Patterns { get; set; } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCompressionNegotiationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCompressionNegotiationTest.cs index b26dbddabf4a..357fca78a162 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCompressionNegotiationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCompressionNegotiationTest.cs @@ -2,5098 +2,1706 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Globalization; - - 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; - - - - [TestClass] public class ApplyCompressionNegotiationTest - - { - - [TestMethod] - - public void AppliesContentNegotiationRules_ForExistingAssets() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ApplyCompressionNegotiation - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = - - [ - - CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "original-fingerprint", - - "original", - - fileLength: 20 - - ), - - CreateCandidate( - - Path.Combine("compressed", "candidate.js.gz"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "compressed-fingerprint", - - "compressed", - - Path.Combine("wwwroot", "candidate.js"), - - "Content-Encoding", - - "gzip", - - 9 - - ) - - ], - - CandidateEndpoints = - - [ - - CreateCandidateEndpoint( - - "candidate.js", - - Path.Combine("wwwroot", "candidate.js"), - - CreateHeaders("text/javascript", [("Content-Length", "20")])), - - - - CreateCandidateEndpoint( - - "candidate.js.gz", - - Path.Combine("compressed", "candidate.js.gz"), - - CreateHeaders("text/javascript", [("Content-Length", "9")])) - - ], - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); - - endpoints.Should().BeEquivalentTo((StaticWebAssetEndpoint[])[ - - new () - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Length", Value = "9" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [ new () { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], - - }, - - new () - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Length", Value = "20" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" }, - - ], - - EndpointProperties = [], - - Selectors = [], - - }, - - new () - - { - - Route = "candidate.js.gz", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Length", Value = "9" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [] - - } - - ]); - - } - - - - [TestMethod] - - public void AppliesContentNegotiationRules_ForExistingAssets_WithFingerprints() - - { - - var now = DateTime.Now; - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - List candidateAssets = [ - - CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate#[.{fingerprint}]?.js", - - "All", - - "All", - - "original-fingerprint", - - "original", - - fileLength: 20, - - lastModified: now - - ) - - ]; - - - - var compressedTask = new ResolveCompressedAssets - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [.. candidateAssets], - - Formats = "gzip;brotli", - - IncludePatterns = "*.js", - - OutputPath = AppContext.BaseDirectory - - }; - - compressedTask.Execute().Should().BeTrue(); - - - - var compressedAssets = compressedTask.AssetsToCompress; - - compressedAssets[0].SetMetadata(nameof(StaticWebAsset.Fingerprint), "gzip"); - - compressedAssets[0].SetMetadata(nameof(StaticWebAsset.Integrity), "compressed-gzip"); - - compressedAssets[0].SetMetadata(nameof(StaticWebAsset.FileLength), "9"); - - compressedAssets[1].SetMetadata(nameof(StaticWebAsset.Fingerprint), "brotli"); - - compressedAssets[1].SetMetadata(nameof(StaticWebAsset.Integrity), "compressed-brotli"); - - compressedAssets[1].SetMetadata(nameof(StaticWebAsset.FileLength), "7"); - - candidateAssets.AddRange(compressedAssets); - - var expectedName = Path.GetFileNameWithoutExtension(compressedAssets[0].ItemSpec); - - var defineStaticAssetEndpointsTask = new DefineStaticWebAssetEndpoints - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [.. candidateAssets], - - ExistingEndpoints = [], - - ContentTypeMappings = [] - - }; - - defineStaticAssetEndpointsTask.Execute().Should().BeTrue(); - - var compressed = defineStaticAssetEndpointsTask.Endpoints; - - - - var task = new ApplyCompressionNegotiation - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [.. candidateAssets], - - CandidateEndpoints = compressed, - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); - - var expectedEndpoints = new StaticWebAssetEndpoint[] - - { - - new() - - { - - Route = "candidate.fingerprint.js", - - AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.br"), - - Selectors = [ - - new () - - { - - Name = "Content-Encoding", - - Value = "br", - - Quality = "0.125000000000" - - } - - ], - - ResponseHeaders = [ new () - - { - - Name = "Cache-Control", - - Value = "max-age=31536000, immutable" - - }, - - new () - - { - - Name = "Content-Encoding", - - Value = "br" - - }, - - new () - - { - - Name = "Content-Length", - - Value = "7" - - }, - - new () - - { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new () - - { - - Name = "ETag", - - Value = "\u0022compressed-brotli\u0022" - - }, - - new () - - { - - Name = "Last-Modified", - - Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) - - }, - - new () - - { - - Name = "Vary", - - Value = "Accept-Encoding" - - } - - ], - - EndpointProperties = [ - - new () - - { - - Name = "fingerprint", - - Value = "fingerprint" - - }, - - new () - - { - - Name = "integrity", - - Value = "sha256-original" - - }, - - new () - - { - - Name = "label", - - Value = "candidate.js" - - } - - ] - - }, - - new() - - { - - Route = "candidate.fingerprint.js", - - AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.gz"), - - Selectors = [ - - new () - - { - - Name = "Content-Encoding", - - Value = "gzip", - - Quality = "0.100000000000" - - } - - ], - - ResponseHeaders = [ new () - - { - - Name = "Cache-Control", - - Value = "max-age=31536000, immutable" - - }, - - new () - - { - - Name = "Content-Encoding", - - Value = "gzip" - - }, - - new () - - { - - Name = "Content-Length", - - Value = "9" - - }, - - new () - - { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new () - - { - - Name = "ETag", - - Value = "\u0022compressed-gzip\u0022" - - }, - - new () - - { - - Name = "Last-Modified", - - Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) - - }, - - new () - - { - - Name = "Vary", - - Value = "Accept-Encoding" - - } - - ], - - EndpointProperties = [ - - new () - - { - - Name = "fingerprint", - - Value = "fingerprint" - - }, - - new () - - { - - Name = "integrity", - - Value = "sha256-original" - - }, - - new () - - { - - Name = "label", - - Value = "candidate.js" - - } - - ] - - }, - - new() - - { - - Route = "candidate.fingerprint.js", - - AssetFile = Path.Combine(AppContext.BaseDirectory, "wwwroot", "candidate.js"), - - ResponseHeaders = [ new () - - { - - Name = "Cache-Control", - - Value = "max-age=31536000, immutable" - - }, - - new () - - { - - Name = "Content-Length", - - Value = "20" - - }, - - new () - - { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new () - - { - - Name = "ETag", - - Value = "\u0022original\u0022" - - }, - - new () - - { - - Name = "Last-Modified", - - Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) - - }, - - new () - - { - - Name = "Vary", - - Value = "Accept-Encoding" - - } - - ], - - EndpointProperties = [ - - new () - - { - - Name = "fingerprint", - - Value = "fingerprint" - - }, - - new () - - { - - Name = "integrity", - - Value = "sha256-original" - - }, - - new () - - { - - Name = "label", - - Value = "candidate.js" - - } - - ] - - }, - - new() - - { - - Route = "candidate.fingerprint.js.br", - - AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.br"), - - ResponseHeaders = [ new () - - { - - Name = "Cache-Control", - - Value = "max-age=31536000, immutable" - - }, - - new () - - { - - Name = "Content-Encoding", - - Value = "br" - - }, - - new () - - { - - Name = "Content-Length", - - Value = "7" - - }, - - new () - - { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new () - - { - - Name = "ETag", - - Value = "\u0022compressed-brotli\u0022" - - }, - - new () - - { - - Name = "Last-Modified", - - Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) - - }, - - new () - - { - - Name = "Vary", - - Value = "Accept-Encoding" - - } - - ], - - EndpointProperties = [ - - new () - - { - - Name = "fingerprint", - - Value = "fingerprint" - - }, - - new () - - { - - Name = "integrity", - - Value = "sha256-compressed-brotli" - - }, - - new () - - { - - Name = "label", - - Value = "candidate.js.br" - - } - - ] - - }, - - new() - - { - - Route = "candidate.fingerprint.js.gz", - - AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.gz"), - - ResponseHeaders = [ new () - - { - - Name = "Cache-Control", - - Value = "max-age=31536000, immutable" - - }, - - new () - - { - - Name = "Content-Encoding", - - Value = "gzip" - - }, - - new () - - { - - Name = "Content-Length", - - Value = "9" - - }, - - new () - - { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new () - - { - - Name = "ETag", - - Value = "\u0022compressed-gzip\u0022" - - }, - - new () - - { - - Name = "Last-Modified", - - Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) - - }, - - new () - - { - - Name = "Vary", - - Value = "Accept-Encoding" - - } - - ], - - EndpointProperties = [ - - new () - - { - - Name = "fingerprint", - - Value = "fingerprint" - - }, - - new () - - { - - Name = "integrity", - - Value = "sha256-compressed-gzip" - - }, - - new () - - { - - Name = "label", - - Value = "candidate.js.gz" - - } - - ] - - }, - - new() - - { - - Route = "candidate.js", - - AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.br"), - - Selectors = [ - - new () - - { - - Name = "Content-Encoding", - - Value = "br", - - Quality = "0.125000000000" - - } - - ], - - ResponseHeaders = [ new () - - { - - Name = "Cache-Control", - - Value = "no-cache" - - }, - - new () - - { - - Name = "Content-Encoding", - - Value = "br" - - }, - - new () - - { - - Name = "Content-Length", - - Value = "7" - - }, - - new () - - { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new () - - { - - Name = "ETag", - - Value = "\u0022compressed-brotli\u0022" - - }, - - new () - - { - - Name = "Last-Modified", - - Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) - - }, - - new () - - { - - Name = "Vary", - - Value = "Accept-Encoding" - - } - - ], - - EndpointProperties = [ - - new () - - { - - Name = "integrity", - - Value = "sha256-original" - - } - - ] - - }, - - new() - - { - - Route = "candidate.js", - - AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.gz"), - - Selectors = [ - - new () - - { - - Name = "Content-Encoding", - - Value = "gzip", - - Quality = "0.100000000000" - - } - - ], - - ResponseHeaders = [ new () - - { - - Name = "Cache-Control", - - Value = "no-cache" - - }, - - new () - - { - - Name = "Content-Encoding", - - Value = "gzip" - - }, - - new () - - { - - Name = "Content-Length", - - Value = "9" - - }, - - new () - - { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new () - - { - - Name = "ETag", - - Value = "\u0022compressed-gzip\u0022" - - }, - - new () - - { - - Name = "Last-Modified", - - Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) - - }, - - new () - - { - - Name = "Vary", - - Value = "Accept-Encoding" - - } - - ], - - EndpointProperties = [ - - new () - - { - - Name = "integrity", - - Value = "sha256-original" - - } - - ] - - }, - - new() - - { - - Route = "candidate.js", - - AssetFile = Path.Combine(AppContext.BaseDirectory, "wwwroot", "candidate.js"), - - ResponseHeaders = [ new () - - { - - Name = "Cache-Control", - - Value = "no-cache" - - }, - - new () - - { - - Name = "Content-Length", - - Value = "20" - - }, - - new () - - { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new () - - { - - Name = "ETag", - - Value = "\u0022original\u0022" - - }, - - new () - - { - - Name = "Last-Modified", - - Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) - - }, - - new () - - { - - Name = "Vary", - - Value = "Accept-Encoding" - - } - - ], - - EndpointProperties = [ - - new () - - { - - Name = "integrity", - - Value = "sha256-original" - - } - - ] - - }, - - new() - - { - - Route = "candidate.js.br", - - AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.br"), - - ResponseHeaders = [ new () - - { - - Name = "Cache-Control", - - Value = "no-cache" - - }, - - new () - - { - - Name = "Content-Encoding", - - Value = "br" - - }, - - new () - - { - - Name = "Content-Length", - - Value = "7" - - }, - - new () - - { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new () - - { - - Name = "ETag", - - Value = "\u0022compressed-brotli\u0022" - - }, - - new () - - { - - Name = "Last-Modified", - - Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) - - }, - - new () - - { - - Name = "Vary", - - Value = "Accept-Encoding" - - } - - ], - - EndpointProperties = [ - - new () - - { - - Name = "integrity", - - Value = "sha256-compressed-brotli" - - } - - ] - - }, - - new() - - { - - Route = "candidate.js.gz", - - AssetFile = Path.Combine(AppContext.BaseDirectory, $"{expectedName}.gz"), - - ResponseHeaders = [ new () { - - Name = "Cache-Control", - - Value = "no-cache" - - }, - - new () { - - Name = "Content-Encoding", - - Value = "gzip" - - }, - - new () { - - Name = "Content-Length", - - Value = "9" - - }, - - new () { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new () { - - Name = "ETag", - - Value = "\u0022compressed-gzip\u0022" - - }, - - new () { - - Name = "Last-Modified", - - Value = now.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) - - }, - - new () { - - Name = "Vary", - - Value = "Accept-Encoding" - - } - - ], - - EndpointProperties = [ - - new () { - - Name = "integrity", - - Value = "sha256-compressed-gzip" - - } - - ] - - } - - }; - - - - endpoints.Should().BeEquivalentTo(expectedEndpoints); - - } - - - - [TestMethod] - - public void AppliesContentNegotiationRules_ToAllRelatedAssetEndpoints() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ApplyCompressionNegotiation - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = - - [ - - CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "original-fingerprint", - - "original", - - fileLength: 20 - - ), - - CreateCandidate( - - Path.Combine("compressed", "candidate.js.gz"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "compressed-fingerprint", - - "compressed", - - Path.Combine("wwwroot", "candidate.js"), - - "Content-Encoding", - - "gzip", - - fileLength: 9 - - ) - - ], - - CandidateEndpoints = - - [ - - CreateCandidateEndpoint( - - "candidate.js", - - Path.Combine("wwwroot", "candidate.js"), - - CreateHeaders("text/javascript")), - - - - CreateCandidateEndpoint( - - "candidate.fingerprint.js", - - Path.Combine("wwwroot", "candidate.js"), - - CreateHeaders("text/javascript")), - - - - CreateCandidateEndpoint( - - "candidate.js.gz", - - Path.Combine("compressed", "candidate.js.gz"), - - CreateHeaders("text/javascript")) - - ], - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); - - endpoints.Should().BeEquivalentTo((StaticWebAssetEndpoint[])[ - - new () - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [ new () { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], - - }, - - new () - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [], - - }, - - new () - - { - - Route = "candidate.fingerprint.js", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [ new () { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], - - }, - - new () - - { - - Route = "candidate.fingerprint.js", - - AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [], - - }, - - new () - - { - - Route = "candidate.js.gz", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [] - - } - - ]); - - } - - - - [TestMethod] - - public void AppliesContentNegotiationRules_IgnoresAlreadyProcessedEndpoints() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ApplyCompressionNegotiation - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = - - [ - - CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "original-fingerprint", - - "original" - - ), - - CreateCandidate( - - Path.Combine("compressed", "candidate.js.gz"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "compressed-fingerprint", - - "compressed", - - Path.Combine("wwwroot", "candidate.js"), - - "Content-Encoding", - - "gzip" - - ) - - ], - - CandidateEndpoints = new StaticWebAssetEndpoint[] - - { - - new() - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new (){ Name = "Content-Type", Value = "text/javascript" }, - - new (){ Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], - - }, - - new() - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), - - ResponseHeaders = - - [ - - new (){ Name = "Content-Type", Value = "text/javascript" } - - ], - - EndpointProperties = [], - - Selectors = [], - - }, - - new() - - { - - Route = "candidate.fingerprint.js", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new (){ Name = "Content-Encoding", Value = "gzip" }, - - new (){ Name = "Content-Type", Value = "text/javascript" }, - - new (){ Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [ new () { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], - - }, - - new() - - { - - Route = "candidate.fingerprint.js", - - AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Type", Value = "text/javascript" } - - ], - - EndpointProperties = [], - - Selectors = [], - - }, - - new() - - { - - Route = "candidate.js.gz", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [] - - } - - }.Select(e => e.ToTaskItem()).ToArray(), - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); - - endpoints.Should().BeEquivalentTo([ - - new StaticWebAssetEndpoint - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], - - }, - - new StaticWebAssetEndpoint - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [], - - }, - - new StaticWebAssetEndpoint - - { - - Route = "candidate.fingerprint.js", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], - - }, - - new StaticWebAssetEndpoint - - { - - Route = "candidate.fingerprint.js", - - AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [], - - }, - - new StaticWebAssetEndpoint - - { - - Route = "candidate.js.gz", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [] - - } - - ]); - - } - - - - [TestMethod] - - public void AppliesContentNegotiationRules_ProcessesNewCompressedFormatsWhenAvailable() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ApplyCompressionNegotiation - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = - - [ - - CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "original-fingerprint", - - "original", - - fileLength: 20 - - ), - - CreateCandidate( - - Path.Combine("compressed", "candidate.js.gz"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "compressed-gzip", - - "compressed", - - Path.Combine("wwwroot", "candidate.js"), - - "Content-Encoding", - - "gzip", - - fileLength: 9 - - ), - - CreateCandidate( - - Path.Combine("compressed", "candidate.js.br"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "compressed-brotli", - - "compressed", - - Path.Combine("wwwroot", "candidate.js"), - - "Content-Encoding", - - "br", - - fileLength: 9 - - ) - - ], - - CandidateEndpoints = new StaticWebAssetEndpoint[] - - { - - new() - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new (){ Name = "Content-Type", Value = "text/javascript" }, - - new (){ Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], - - }, - - new() - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), - - ResponseHeaders = - - [ - - new (){ Name = "Content-Type", Value = "text/javascript" } - - ], - - EndpointProperties = [], - - Selectors = [], - - }, - - new() - - { - - Route = "candidate.fingerprint.js", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new (){ Name = "Content-Encoding", Value = "gzip" }, - - new (){ Name = "Content-Type", Value = "text/javascript" }, - - new (){ Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [ new () { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], - - }, - - new() - - { - - Route = "candidate.fingerprint.js", - - AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Type", Value = "text/javascript" } - - ], - - EndpointProperties = [], - - Selectors = [], - - }, - - new() - - { - - Route = "candidate.js.gz", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [] - - }, - - new() - - { - - Route = "candidate.js.br", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.br")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Type", Value = "text/javascript" }, - - ], - - EndpointProperties = [], - - Selectors = [] - - } - - }.Select(e => e.ToTaskItem()).ToArray(), - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); - - endpoints.Should().BeEquivalentTo([ - - new StaticWebAssetEndpoint - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], - - }, - - new StaticWebAssetEndpoint - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.br")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "br" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "br", Quality = "0.100000000000" } ], - - }, - - new StaticWebAssetEndpoint - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [], - - }, - - new StaticWebAssetEndpoint - - { - - Route = "candidate.fingerprint.js", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], - - }, - - new StaticWebAssetEndpoint - - { - - Route = "candidate.fingerprint.js", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.br")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "br" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [ new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "br", Quality = "0.100000000000" } ], - - }, - - new StaticWebAssetEndpoint - - { - - Route = "candidate.fingerprint.js", - - AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [], - - }, - - new StaticWebAssetEndpoint - - { - - Route = "candidate.js.gz", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [] - - }, - - new StaticWebAssetEndpoint - - { - - Route = "candidate.js.br", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.br")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "br" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [] - - } - - ]); - - } - - - - [TestMethod] - - public void AppliesContentNegotiationRules_AddsVaryHeaderToEndpointsWithSameRouteButDifferentAssets() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ApplyCompressionNegotiation - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = - - [ - - CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "original-fingerprint", - - "original", - - fileLength: 20 - - ), - - CreateCandidate( - - Path.Combine("compressed", "candidate.js.gz"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "compressed-fingerprint", - - "compressed", - - Path.Combine("wwwroot", "candidate.js"), - - "Content-Encoding", - - "gzip", - - 9 - - ), - - // This represents a different asset (e.g., a publish asset) that shares the same route - - // but wasn't part of the compression processing - - CreateCandidate( - - Path.Combine("publish", "candidate.js"), - - "PublishPackage", - - "Discovered", - - "candidate.js", - - "Publish", - - "All", - - "publish-fingerprint", - - "publish", - - fileLength: 18 - - ) - - ], - - CandidateEndpoints = - - [ - - CreateCandidateEndpoint( - - "candidate.js", - - Path.Combine("wwwroot", "candidate.js"), - - CreateHeaders("text/javascript", [("Content-Length", "20")])), - - - - CreateCandidateEndpoint( - - "candidate.js.gz", - - Path.Combine("compressed", "candidate.js.gz"), - - CreateHeaders("text/javascript", [("Content-Length", "9")])), - - - - // This endpoint shares the route but points to a different asset - - CreateCandidateEndpoint( - - "candidate.js", - - Path.Combine("publish", "candidate.js"), - - CreateHeaders("text/javascript", [("Content-Length", "18")])) - - ], - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); - - endpoints.Should().BeEquivalentTo((StaticWebAssetEndpoint[])[ - - new () - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Length", Value = "9" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [ new () { Name = "Content-Encoding", Value = "gzip", Quality = "0.100000000000" } ], - - }, - - new () - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Length", Value = "20" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" }, - - ], - - EndpointProperties = [], - - Selectors = [], - - }, - - new () - - { - - Route = "candidate.js.gz", - - AssetFile = Path.GetFullPath(Path.Combine("compressed", "candidate.js.gz")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Encoding", Value = "gzip" }, - - new () { Name = "Content-Length", Value = "9" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" } - - ], - - EndpointProperties = [], - - Selectors = [] - - }, - - new () - - { - - Route = "candidate.js", - - AssetFile = Path.GetFullPath(Path.Combine("publish", "candidate.js")), - - ResponseHeaders = - - [ - - new () { Name = "Content-Length", Value = "18" }, - - new () { Name = "Content-Type", Value = "text/javascript" }, - - new () { Name = "Vary", Value = "Accept-Encoding" }, - - ], - - EndpointProperties = [], - - Selectors = [], - - } - - ]); - - } - - - - private static StaticWebAssetEndpointResponseHeader[] CreateHeaders(string contentType, params (string name, string value)[] AdditionalHeaders) - - { - - return - - [ - - new StaticWebAssetEndpointResponseHeader { - - Name = "Content-Type", - - Value = contentType - - }, - - ..(AdditionalHeaders ?? []).Select(h => new StaticWebAssetEndpointResponseHeader { Name = h.name, Value = h.value }) - - ]; - - } - - - - private static ITaskItem CreateCandidate( - - string itemSpec, - - string sourceId, - - string sourceType, - - string relativePath, - - string assetKind, - - string assetMode, - - string fingerprint = "", - - string integrity = "", - - string relatedAsset = "", - - string assetTraitName = "", - - string assetTraitValue = "", - - long fileLength = 9, - - DateTimeOffset? lastModified = null) - - { - - lastModified ??= new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero); - - var result = new StaticWebAsset() - - { - - Identity = Path.GetFullPath(itemSpec), - - SourceId = sourceId, - - SourceType = sourceType, - - ContentRoot = Directory.GetCurrentDirectory(), - - BasePath = "base", - - RelativePath = relativePath, - - AssetKind = assetKind, - - AssetMode = assetMode, - - AssetRole = "Primary", - - RelatedAsset = relatedAsset, - - AssetTraitName = assetTraitName, - - AssetTraitValue = assetTraitValue, - - CopyToOutputDirectory = "", - - CopyToPublishDirectory = "", - - OriginalItemSpec = itemSpec, - - // Add these to avoid accessing the disk to compute them - - Integrity = integrity, - - Fingerprint = "fingerprint", - - FileLength = fileLength, - - LastWriteTime = lastModified.Value, - - }; - - - - result.ApplyDefaults(); - - result.Normalize(); - - - - return result.ToTaskItem(); - - } - - - - private static ITaskItem CreateCandidateEndpoint( - - string route, - - string assetFile, - - StaticWebAssetEndpointResponseHeader[] responseHeaders = null, - - StaticWebAssetEndpointSelector[] responseSelector = null, - - StaticWebAssetEndpointProperty[] properties = null) - - { - - return new StaticWebAssetEndpoint - - { - - Route = route, - - AssetFile = Path.GetFullPath(assetFile), - - ResponseHeaders = responseHeaders ?? [], - - EndpointProperties = properties ?? [], - - Selectors = responseSelector ?? [] - - }.ToTaskItem(); - - } - - - - [TestMethod] - - public void AppliesContentNegotiationRules_AttachesWeakETagAsResponseHeader() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ApplyCompressionNegotiation - - { - - BuildEngine = buildEngine.Object, - - AttachWeakETagToCompressedAssets = "ResponseHeader", - - CandidateAssets = - - [ - - CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "original-fingerprint", - - "original", - - fileLength: 20 - - ), - - CreateCandidate( - - Path.Combine("compressed", "candidate.js.gz"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "compressed-fingerprint", - - "compressed", - - Path.Combine("wwwroot", "candidate.js"), - - "Content-Encoding", - - "gzip", - - 9 - - ) - - ], - - CandidateEndpoints = - - [ - - CreateCandidateEndpoint( - - "candidate.js", - - Path.Combine("wwwroot", "candidate.js"), - - CreateHeaders("text/javascript", [("Content-Length", "20"), ("ETag", "\"original-etag\"")])), - - - - CreateCandidateEndpoint( - - "candidate.js.gz", - - Path.Combine("compressed", "candidate.js.gz"), - - CreateHeaders("text/javascript", [("Content-Length", "9"), ("ETag", "\"compressed-etag\"")])) - - ], - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); - - - - // The compressed endpoint for the original route should have the weak ETag from the original - - var compressedEndpoint = endpoints.FirstOrDefault(e => e.Route == "candidate.js" && e.AssetFile.Contains("candidate.js.gz")); - - compressedEndpoint.Should().NotBeNull(); - - compressedEndpoint.ResponseHeaders.Should().Contain(h => h.Name == "ETag" && h.Value == "W/\"original-etag\""); - - } - - - - [TestMethod] - - public void AppliesContentNegotiationRules_AttachesWeakETagAsEndpointProperty() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - - .Callback(args => errorMessages.Add(args.Message)); - - - - + buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) + .Callback(args => errorMessages.Add(args.Message)); var task = new ApplyCompressionNegotiation - - { - - BuildEngine = buildEngine.Object, - - AttachWeakETagToCompressedAssets = "EndpointProperty", - - CandidateAssets = - - [ - - CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "original-fingerprint", - - "original", - - fileLength: 20 - - ), - - CreateCandidate( - - Path.Combine("compressed", "candidate.js.gz"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "compressed-fingerprint", - - "compressed", - - Path.Combine("wwwroot", "candidate.js"), - - "Content-Encoding", - - "gzip", - - 9 - - ) - - ], - - CandidateEndpoints = - - [ - - CreateCandidateEndpoint( - - "candidate.js", - - Path.Combine("wwwroot", "candidate.js"), - - CreateHeaders("text/javascript", [("Content-Length", "20"), ("ETag", "\"original-etag\"")])), - - - - CreateCandidateEndpoint( - - "candidate.js.gz", - - Path.Combine("compressed", "candidate.js.gz"), - - CreateHeaders("text/javascript", [("Content-Length", "9"), ("ETag", "\"compressed-etag\"")])) - - ], - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); - - - - // The compressed endpoint for the original route should have the original-resource property - - var compressedEndpoint = endpoints.FirstOrDefault(e => e.Route == "candidate.js" && e.AssetFile.Contains("candidate.js.gz")); - - compressedEndpoint.Should().NotBeNull(); - - compressedEndpoint.EndpointProperties.Should().Contain(p => p.Name == "original-resource" && p.Value == "\"original-etag\""); - - } - - - - [TestMethod] - - public void AppliesContentNegotiationRules_DoesNotAttachETagWhenModeIsEmpty() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ApplyCompressionNegotiation - - { - - BuildEngine = buildEngine.Object, - - AttachWeakETagToCompressedAssets = "", // Empty string should not attach ETag - - CandidateAssets = - - [ - - CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "original-fingerprint", - - "original", - - fileLength: 20 - - ), - - CreateCandidate( - - Path.Combine("compressed", "candidate.js.gz"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "compressed-fingerprint", - - "compressed", - - Path.Combine("wwwroot", "candidate.js"), - - "Content-Encoding", - - "gzip", - - 9 - - ) - - ], - - CandidateEndpoints = - - [ - - CreateCandidateEndpoint( - - "candidate.js", - - Path.Combine("wwwroot", "candidate.js"), - - CreateHeaders("text/javascript", [("Content-Length", "20"), ("ETag", "\"original-etag\"")])), - - - - CreateCandidateEndpoint( - - "candidate.js.gz", - - Path.Combine("compressed", "candidate.js.gz"), - - CreateHeaders("text/javascript", [("Content-Length", "9"), ("ETag", "\"compressed-etag\"")])) - - ], - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.UpdatedEndpoints); - - - - // The compressed endpoint for the original route should not have weak ETag or original-resource property - - var compressedEndpoint = endpoints.FirstOrDefault(e => e.Route == "candidate.js" && e.AssetFile.Contains("candidate.js.gz")); - - compressedEndpoint.Should().NotBeNull(); - - compressedEndpoint.ResponseHeaders.Should().NotContain(h => h.Name == "ETag" && h.Value.StartsWith("W/")); - - compressedEndpoint.EndpointProperties.Should().NotContain(p => p.Name == "original-resource"); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCssScopesTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCssScopesTest.cs index 6b43265bcc98..aecb16809e36 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCssScopesTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ApplyCssScopesTest.cs @@ -6,1134 +6,381 @@ using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class ApplyAllCssScopesTest - - { - - [TestMethod] - - public void ApplyAllCssScopes_AppliesScopesToRazorComponentFiles() - - { - - // Arrange - - var taskInstance = new ApplyCssScopes() - - { - - RazorComponents = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.razor"), - - new TaskItem("TestFiles/Pages/Index.razor"), - - }, - - RazorGenerate = Array.Empty(), - - ScopedCss = new[] - - { - - new TaskItem("TestFiles/Pages/Index.razor.css", new Dictionary { ["CssScope"] = "index-scope" }), - - new TaskItem("TestFiles/Pages/Counter.razor.css", new Dictionary { ["CssScope"] = "counter-scope" }), - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.RazorComponentsWithScopes.Should().HaveCount(2); - - taskInstance.RazorComponentsWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/Index.razor" && rcws.GetMetadata("CssScope") == "index-scope"); - - taskInstance.RazorComponentsWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/Counter.razor" && rcws.GetMetadata("CssScope") == "counter-scope"); - - } - - - - [TestMethod] - - public void ApplyAllCssScopes_AppliesScopesToRazorViewFiles() - - { - - // Arrange - - var taskInstance = new ApplyCssScopes() - - { - - RazorGenerate = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.cshtml"), - - new TaskItem("TestFiles/Pages/Index.cshtml"), - - }, - - RazorComponents = Array.Empty(), - - ScopedCss = new[] - - { - - new TaskItem("TestFiles/Pages/Index.cshtml.css", new Dictionary { ["CssScope"] = "index-scope" }), - - new TaskItem("TestFiles/Pages/Counter.cshtml.css", new Dictionary { ["CssScope"] = "counter-scope" }), - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.RazorGenerateWithScopes.Should().HaveCount(2); - - taskInstance.RazorGenerateWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/Index.cshtml" && rcws.GetMetadata("CssScope") == "index-scope"); - - taskInstance.RazorGenerateWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/Counter.cshtml" && rcws.GetMetadata("CssScope") == "counter-scope"); - - } - - - - [TestMethod] - - public void DoesNotApplyCssScopes_ToRazorComponentsWithoutAssociatedFiles() - - { - - // Arrange - - var taskInstance = new ApplyCssScopes() - - { - - RazorComponents = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.razor"), - - new TaskItem("TestFiles/Pages/Index.razor"), - - new TaskItem("TestFiles/Pages/FetchData.razor"), - - }, - - RazorGenerate = Array.Empty(), - - ScopedCss = new[] - - { - - new TaskItem("TestFiles/Pages/Index.razor.css", new Dictionary { ["CssScope"] = "index-scope" }), - - new TaskItem("TestFiles/Pages/Counter.razor.css", new Dictionary { ["CssScope"] = "counter-scope" }) - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - Assert.IsTrue(result); - - result.Should().BeTrue(); - - taskInstance.RazorComponentsWithScopes.Should().NotContain(rcws => rcws.ItemSpec == "TestFiles/Pages/Fetchdata.razor"); - - } - - - - [TestMethod] - - public void DoesNotApplyCssScopes_ToRazorViewsWithoutAssociatedFiles() - - { - - // Arrange - - var taskInstance = new ApplyCssScopes() - - { - - RazorGenerate = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.cshtml"), - - new TaskItem("TestFiles/Pages/Index.cshtml"), - - new TaskItem("TestFiles/Pages/FetchData.cshtml"), - - }, - - RazorComponents = Array.Empty(), - - ScopedCss = new[] - - { - - new TaskItem("TestFiles/Pages/Index.cshtml.css", new Dictionary { ["CssScope"] = "index-scope" }), - - new TaskItem("TestFiles/Pages/Counter.cshtml.css", new Dictionary { ["CssScope"] = "counter-scope" }) - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - Assert.IsTrue(result); - - result.Should().BeTrue(); - - taskInstance.RazorGenerateWithScopes.Should().NotContain(rcws => rcws.ItemSpec == "TestFiles/Pages/Fetchdata.razor"); - - } - - - - [TestMethod] - - public void ApplyAllCssScopes_FailsWhenTheScopedCss_DoesNotMatchTheRazorComponent() - - { - - // Arrange - - var taskInstance = new ApplyCssScopes - - { - - RazorComponents = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.razor"), - - new TaskItem("TestFiles/Pages/Index.razor"), - - }, - - RazorGenerate = Array.Empty(), - - ScopedCss = new[] - - { - - new TaskItem("TestFiles/Pages/Index.razor.css", new Dictionary { ["CssScope"] = "index-scope" }), - - new TaskItem("TestFiles/Pages/Counter.razor.css", new Dictionary { ["CssScope"] = "counter-scope" }), - - new TaskItem("TestFiles/Pages/Profile.razor.css", new Dictionary { ["CssScope"] = "profile-scope" }), - - }, - - BuildEngine = Mock.Of() - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - } - - - - [TestMethod] - - public void ApplyAllCssScopes_FailsWhenTheScopedCss_DoesNotMatchTheRazorView() - - { - - // Arrange - - var taskInstance = new ApplyCssScopes - - { - - RazorGenerate = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.cshtml"), - - new TaskItem("TestFiles/Pages/Index.cshtml"), - - }, - - RazorComponents = Array.Empty(), - - ScopedCss = new[] - - { - - new TaskItem("TestFiles/Pages/Index.cshtml.css", new Dictionary { ["CssScope"] = "index-scope" }), - - new TaskItem("TestFiles/Pages/Counter.cshtml.css", new Dictionary { ["CssScope"] = "counter-scope" }), - - new TaskItem("TestFiles/Pages/Profile.cshtml.css", new Dictionary { ["CssScope"] = "profile-scope" }), - - }, - - BuildEngine = Mock.Of() - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - } - - - - [TestMethod] - - public void ScopedCssCanDefineAssociatedRazorComponentFile() - - { - - // Arrange - - var taskInstance = new ApplyCssScopes() - - { - - RazorComponents = new[] - - { - - new TaskItem("TestFiles/Pages/FetchData.razor") - - }, - - RazorGenerate = Array.Empty(), - - ScopedCss = new[] - - { - - new TaskItem("TestFiles/Pages/Profile.razor.css", new Dictionary - - { - - ["CssScope"] = "fetchdata-scope", - - ["RazorComponent"] = "TestFiles/Pages/FetchData.razor" - - }) - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.RazorComponentsWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/FetchData.razor" && rcws.GetMetadata("CssScope") == "fetchdata-scope"); - - } - - - - [TestMethod] - - public void ScopedCssCanDefineAssociatedRazorGenerateFile() - - { - - // Arrange - - var taskInstance = new ApplyCssScopes() - - { - - RazorGenerate = new[] - - { - - new TaskItem("TestFiles/Pages/FetchData.cshtml") - - }, - - RazorComponents = Array.Empty(), - - ScopedCss = new[] - - { - - new TaskItem("TestFiles/Pages/Profile.cshtml.css", new Dictionary - - { - - ["CssScope"] = "fetchdata-scope", - - ["View"] = "TestFiles/Pages/FetchData.cshtml" - - }) - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.RazorGenerateWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/FetchData.cshtml" && rcws.GetMetadata("CssScope") == "fetchdata-scope"); - - } - - - - [TestMethod] - - public void ApplyAllCssScopes_FailsWhenMultipleScopedCssFiles_MatchTheSameRazorComponent() - - { - - // Arrange - - var taskInstance = new ApplyCssScopes - - { - - RazorComponents = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.razor"), - - new TaskItem("TestFiles/Pages/Index.razor"), - - }, - - RazorGenerate = Array.Empty(), - - ScopedCss = new[] - - { - - new TaskItem("TestFiles/Pages/Index.razor.css", new Dictionary { ["CssScope"] = "index-scope" }), - - new TaskItem("TestFiles/Pages/Counter.razor.css", new Dictionary { ["CssScope"] = "counter-scope" }), - - new TaskItem("TestFiles/Pages/Profile.razor.css", new Dictionary - - { - - ["CssScope"] = "conflict-scope", - - ["RazorComponent"] = "TestFiles/Pages/Index.razor" - - }), - - }, - - BuildEngine = Mock.Of() - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - } - - - - [TestMethod] - - public void ApplyAllCssScopes_FailsWhenMultipleScopedCssFiles_MatchTheSameRazorView() - - { - - // Arrange - - var taskInstance = new ApplyCssScopes - - { - - RazorGenerate = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.cshtml"), - - new TaskItem("TestFiles/Pages/Index.cshtml"), - - }, - - RazorComponents = Array.Empty(), - - ScopedCss = new[] - - { - - new TaskItem("TestFiles/Pages/Index.cshtml.css", new Dictionary { ["CssScope"] = "index-scope" }), - - new TaskItem("TestFiles/Pages/Counter.cshtml.css", new Dictionary { ["CssScope"] = "counter-scope" }), - - new TaskItem("TestFiles/Pages/Profile.cshtml.css", new Dictionary - - { - - ["CssScope"] = "conflict-scope", - - ["View"] = "TestFiles/Pages/Index.cshtml" - - }), - - }, - - BuildEngine = Mock.Of() - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - } - - - - [TestMethod] - - public void ApplyAllCssScopes_AppliesScopesToRazorComponentAndViewFiles() - - { - - // Arrange - - var taskInstance = new ApplyCssScopes() - - { - - RazorComponents = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.razor"), - - new TaskItem("TestFiles/Pages/Index.razor"), - - }, - - RazorGenerate = new[] - - { - - new TaskItem("TestFiles/Pages/Home.cshtml"), - - new TaskItem("TestFiles/Pages/_Host.cshtml"), - - }, - - ScopedCss = new[] - - { - - new TaskItem("TestFiles/Pages/Home.cshtml.css", new Dictionary { ["CssScope"] = "home-scope" }), - - new TaskItem("TestFiles/Pages/_Host.cshtml.css", new Dictionary { ["CssScope"] = "_host-scope" }), - - new TaskItem("TestFiles/Pages/Index.razor.css", new Dictionary { ["CssScope"] = "index-scope" }), - - new TaskItem("TestFiles/Pages/Counter.razor.css", new Dictionary { ["CssScope"] = "counter-scope" }), - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.RazorComponentsWithScopes.Should().HaveCount(2); - - taskInstance.RazorComponentsWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/Index.razor" && rcws.GetMetadata("CssScope") == "index-scope"); - - taskInstance.RazorComponentsWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/Counter.razor" && rcws.GetMetadata("CssScope") == "counter-scope"); - - - - taskInstance.RazorGenerateWithScopes.Should().HaveCount(2); - - taskInstance.RazorGenerateWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/Home.cshtml" && rcws.GetMetadata("CssScope") == "home-scope"); - - taskInstance.RazorGenerateWithScopes.Should().ContainSingle(rcws => rcws.ItemSpec == "TestFiles/Pages/_Host.cshtml" && rcws.GetMetadata("CssScope") == "_host-scope"); - - } - - - - [TestMethod] - - public void ApplyAllCssScopes_ScopedCssComponentsDontMatchWithScopedCssViewStylesAndViceversa() - - { - - // Arrange - - var taskInstance = new ApplyCssScopes - - { - - RazorComponents = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.razor"), - - new TaskItem("TestFiles/Pages/Index.razor"), - - }, - - RazorGenerate = new[] - - { - - new TaskItem("TestFiles/Pages/Home.cshtml"), - - new TaskItem("TestFiles/Pages/_Host.cshtml"), - - }, - - ScopedCss = new[] - - { - - new TaskItem("TestFiles/Pages/Home.razor.css", new Dictionary { ["CssScope"] = "home-scope" }), - - new TaskItem("TestFiles/Pages/_Host.razor.css", new Dictionary { ["CssScope"] = "_host-scope" }), - - new TaskItem("TestFiles/Pages/Index.cshtml.css", new Dictionary { ["CssScope"] = "index-scope" }), - - new TaskItem("TestFiles/Pages/Counter.cshtml.css", new Dictionary { ["CssScope"] = "counter-scope" }), - - }, - - BuildEngine = Mock.Of() - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetGroupFilteringTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetGroupFilteringTest.cs index 3f95e7909509..eeb5a05f58b4 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetGroupFilteringTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetGroupFilteringTest.cs @@ -2,2698 +2,905 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - /// - - /// Unit tests for asset-group filtering logic across UpdatePackageStaticWebAssets, - - /// UpdateExternallyDefinedStaticWebAssets, ComputeReferenceStaticWebAssetItems and - - /// DefineStaticWebAssets.ApplyGroupDefinitions. - - /// - - [TestClass] - public class AssetGroupFilteringTest : IDisposable - - { - - private readonly string _tempDir; - - private readonly Mock _buildEngine; - - private readonly List _errorMessages; - - private readonly List _logMessages; - - - - public AssetGroupFilteringTest() - - { - - _tempDir = Path.Combine(Path.GetTempPath(), "AssetGroupTest_" + Guid.NewGuid().ToString("N")); - - Directory.CreateDirectory(_tempDir); - - - - _errorMessages = new List(); - - _logMessages = new List(); - - _buildEngine = new Mock(); - - _buildEngine.Setup(e => e.ProjectFileOfTaskNode).Returns(Path.Combine(_tempDir, "test.csproj")); - - _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => _errorMessages.Add(args.Message)); - - _buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => _logMessages.Add(args.Message)); - - } - - - - public void Dispose() - - { - - if (Directory.Exists(_tempDir)) - - { - - try { Directory.Delete(_tempDir, recursive: true); } catch { } - - } - - } - - - - [TestMethod] - - public void UpdateExternal_AssetWithGroups_MatchingDeclaration_IsIncluded() - - { - - var file = CreateTempFile("ext", "site.css", "body{}"); - - var asset = CreateExternalAssetWithGroups(file, "IdentityUI", "css/site.css", "BootstrapVersion=V5"); - - - - var task = new UpdateExternallyDefinedStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - Endpoints = Array.Empty(), - - StaticWebAssetGroups = new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V5", - - ["SourceId"] = "IdentityUI" - - }) - - } - - }; - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - task.UpdatedAssets.Should().HaveCount(1); - - } - - - - [TestMethod] - - public void UpdateExternal_AssetWithGroups_NoDeclarations_IsExcluded() - - { - - var file = CreateTempFile("ext", "site.css", "body{}"); - - var asset = CreateExternalAssetWithGroups(file, "IdentityUI", "css/site.css", "BootstrapVersion=V5"); - - - - var task = new UpdateExternallyDefinedStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - Endpoints = Array.Empty(), - - // No StaticWebAssetGroups - - }; - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - task.UpdatedAssets.Should().HaveCount(0, "grouped assets should be excluded when no declarations exist"); - - } - - - - [TestMethod] - - public void UpdateExternal_MultiGroup_PartialMatch_IsExcluded() - - { - - var file = CreateTempFile("ext", "site.css", "body{}"); - - var asset = CreateExternalAssetWithGroups(file, "IdentityUI", "css/site.css", "BootstrapVersion=V5;DebugAssets=true"); - - - - var task = new UpdateExternallyDefinedStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - Endpoints = Array.Empty(), - - StaticWebAssetGroups = new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V5", - - ["SourceId"] = "IdentityUI" - - }) - - } - - }; - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - task.UpdatedAssets.Should().HaveCount(0, "AND-matching requires all entries satisfied"); - - } - - - - [TestMethod] - - public void UpdateExternal_CascadingExclusion_RelatedAssetExcludedWithPrimary() - - { - - var primaryFile = CreateTempFile("ext", "css", "site.css", "body{}"); - - var relatedFile = CreateTempFile("ext", "css", "site.css.gz", "compressed"); - - - - var primary = CreateExternalAssetWithGroups(primaryFile, "IdentityUI", "css/site.css", "BootstrapVersion=V5"); - - var related = CreateExternalAsset(relatedFile, "IdentityUI", "css/site.css.gz"); - - related.SetMetadata("AssetRole", "Alternative"); - - related.SetMetadata("AssetTraitName", "Content-Encoding"); - - related.SetMetadata("AssetTraitValue", "gzip"); - - related.SetMetadata("RelatedAsset", Path.GetFullPath(primaryFile)); - - - - var task = new UpdateExternallyDefinedStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { primary, related }, - - Endpoints = Array.Empty(), - - // No declarations → primary excluded → related cascades - - }; - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - task.UpdatedAssets.Should().HaveCount(0, "related asset should cascade-exclude with primary"); - - } - - - - [TestMethod] - - public void UpdateExternal_EndpointFiltering_ExcludedAssetEndpointsRemoved() - - { - - var includedFile = CreateTempFile("ext", "app.js", "var x;"); - - var excludedFile = CreateTempFile("ext2", "site.css", "body{}"); - - - - var includedAsset = CreateExternalAsset(includedFile, "SomeLib", "app.js"); - - var excludedAsset = CreateExternalAssetWithGroups(excludedFile, "IdentityUI", "css/site.css", "BootstrapVersion=V5"); - - - - var includedEndpoint = new TaskItem("app.js", new Dictionary - - { - - ["AssetFile"] = Path.GetFullPath(includedFile), - - ["Route"] = "app.js", - - ["Selectors"] = "[]", - - ["EndpointProperties"] = "[]", - - ["ResponseHeaders"] = "[]", - - }); - - var excludedEndpoint = new TaskItem("css/site.css", new Dictionary - - { - - ["AssetFile"] = Path.GetFullPath(excludedFile), - - ["Route"] = "css/site.css", - - ["Selectors"] = "[]", - - ["EndpointProperties"] = "[]", - - ["ResponseHeaders"] = "[]", - - }); - - - - var task = new UpdateExternallyDefinedStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { includedAsset, excludedAsset }, - - Endpoints = new[] { includedEndpoint, excludedEndpoint }, - - // No declarations → grouped asset excluded - - }; - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - task.UpdatedEndpoints.Should().HaveCount(1, "only endpoints for included assets should remain"); - - } - - - - [TestMethod] - - public void ComputeReference_GroupedFrameworkAsset_PreservesSourceType() - - { - - // Two grouped framework assets at the same target path - - var asset1 = CreateReferenceAsset("item1.css", "FrameworkLib", "Framework", "css/site.css", "All", "All", "BootstrapVersion=V4"); - - var asset2 = CreateReferenceAsset("item2.css", "FrameworkLib", "Framework", "css/site.css", "All", "All", "BootstrapVersion=V5"); - - - - var task = new ComputeReferenceStaticWebAssetItems - - { - - BuildEngine = _buildEngine.Object, - - Source = "FrameworkLib", - - Assets = new[] { asset1, asset2 }, - - Patterns = Array.Empty(), - - AssetKind = "Build", - - ProjectMode = "Default", - - UpdateSourceType = true - - }; - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - // Both should be included (distinct groups) - - task.StaticWebAssets.Should().HaveCount(2); - - // And both should STILL be Framework, not overwritten to Project - - foreach (var asset in task.StaticWebAssets) - - { - - asset.GetMetadata("SourceType").Should().Be("Framework", - - "grouped framework assets should preserve SourceType=Framework"); - - } - - } - - - - [TestMethod] - - public void ComputeReference_NonGroupedFrameworkAsset_PreservesSourceType() - - { - - var asset = CreateReferenceAsset("framework.js", "FrameworkLib", "Framework", "js/framework.js", "All", "All"); - - - - var task = new ComputeReferenceStaticWebAssetItems - - { - - BuildEngine = _buildEngine.Object, - - Source = "FrameworkLib", - - Assets = new[] { asset }, - - Patterns = Array.Empty(), - - AssetKind = "Build", - - ProjectMode = "Default", - - UpdateSourceType = true - - }; - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - task.StaticWebAssets.Should().HaveCount(1); - - task.StaticWebAssets[0].GetMetadata("SourceType").Should().Be("Framework", - - "non-grouped framework assets should also preserve SourceType=Framework"); - - } - - - - [TestMethod] - - public void ApplyGroupDefinitions_SameOrderSameSourceId_ProducesError() - - { - - var file = CreateTempFile("wwwroot", "V5", "css", "site.css", "body{}"); - - - - var task = CreateDefineStaticWebAssetsTask( - - new[] { CreateCandidateAssetItem(file) }, - - new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V5", - - ["Order"] = "0", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "V5/**", - - ["RelativePathPattern"] = "V5/**" - - }), - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V4", - - ["Order"] = "0", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "V4/**", - - ["RelativePathPattern"] = "V4/**" - - }) - - }, - - sourceId: "MyProject", - - basePath: "_content/myproject"); - - - - var result = task.Execute(); - - - - result.Should().BeFalse("same Order + same SourceId should produce an error"); - - _errorMessages.Should().ContainMatch("*same Order*"); - - } - - - - [TestMethod] - - public void ApplyGroupDefinitions_DifferentOrderSameSourceId_NoError() - - { - - var file = CreateTempFile("wwwroot", "V5", "css", "site.css", "body{}"); - - - - var task = CreateDefineStaticWebAssetsTask( - - new[] { CreateCandidateAssetItem(file) }, - - new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V5", - - ["Order"] = "0", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "V5/**", - - ["RelativePathPattern"] = "V5/**" - - }), - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V4", - - ["Order"] = "1", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "V4/**", - - ["RelativePathPattern"] = "V4/**" - - }) - - }, - - sourceId: "MyProject", - - basePath: "_content/myproject"); - - - - var result = task.Execute(); - - - - result.Should().BeTrue("different Orders should not trigger the same-Order-same-SourceId validation"); - - _errorMessages.Should().BeEmpty(); - - } - - - - [TestMethod] - - public void ApplyGroupDefinitions_MissingValue_ProducesError() - - { - - var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body{}"); - - - - var task = CreateDefineStaticWebAssetsTask( - - new[] { CreateCandidateAssetItem(file) }, - - new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - // No "Value" - - ["Order"] = "0", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "V4/**", - - }) - - }); - - - - var result = task.Execute(); - - - - result.Should().BeFalse(); - - _errorMessages.Should().ContainSingle(m => m.Contains("missing required metadata 'Value'")); - - } - - - - [TestMethod] - - public void ApplyGroupDefinitions_MissingSourceId_ProducesError() - - { - - var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body{}"); - - - - var task = CreateDefineStaticWebAssetsTask( - - new[] { CreateCandidateAssetItem(file) }, - - new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V4", - - ["Order"] = "0", - - // No "SourceId" - - ["IncludePattern"] = "V4/**", - - }) - - }); - - - - var result = task.Execute(); - - - - result.Should().BeFalse(); - - _errorMessages.Should().ContainSingle(m => m.Contains("missing required metadata 'SourceId'")); - - } - - - - [TestMethod] - - public void ApplyGroupDefinitions_InvalidOrder_ProducesError() - - { - - var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body{}"); - - - - var task = CreateDefineStaticWebAssetsTask( - - new[] { CreateCandidateAssetItem(file) }, - - new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V4", - - ["Order"] = "not-a-number", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "V4/**", - - }) - - }); - - - - var result = task.Execute(); - - - - result.Should().BeFalse(); - - _errorMessages.Should().ContainSingle(m => m.Contains("invalid or missing 'Order'")); - - } - - - - - [TestMethod] - - - public void ApplyGroupDefinitions_MissingIncludePattern_ProducesError() - - - { - - - var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body{}"); - - - - + [TestMethod] + public void ApplyGroupDefinitions_MissingIncludePattern_ProducesError() + { + var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body{}"); var task = CreateDefineStaticWebAssetsTask( - - new[] { CreateCandidateAssetItem(file) }, - - new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V4", - - ["Order"] = "0", - - ["SourceId"] = "IdentityUI", - - // No "IncludePattern" - - }) - - }); - - - - var result = task.Execute(); - - - - result.Should().BeFalse(); - - _errorMessages.Should().ContainSingle(m => m.Contains("missing required metadata 'IncludePattern'")); - - } - - - - [TestMethod] - - public void ApplyGroupDefinitions_SourceIdMismatch_DefinitionsIgnored() - - { - - var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body-v4{}"); - - - - var task = CreateDefineStaticWebAssetsTask( - - new[] { CreateCandidateAssetItem(file) }, - - new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V4", - - ["Order"] = "0", - - ["SourceId"] = "OtherLib", - - ["IncludePattern"] = "V4/**", - - ["RelativePathPattern"] = "V4/**", - - ["ContentRootSuffix"] = "V4" - - }) - - }); - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - _errorMessages.Should().BeEmpty(); - - task.Assets.Should().HaveCount(1); - - - - var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); - - asset.AssetGroups.Should().BeNullOrEmpty("definitions with mismatched SourceId should not apply"); - - asset.RelativePath.Should().Contain("V4", "RelativePath should not be transformed"); - - } - - - - [TestMethod] - - public void ApplyGroupDefinitions_MultipleContentRootSuffix_Compose() - - { - - var file = CreateTempFile("wwwroot", "shared", "site.css", "body{}"); - - - - var task = CreateDefineStaticWebAssetsTask( - - new[] { CreateCandidateAssetItem(file) }, - - new ITaskItem[] - - { - - new TaskItem("GroupA", new Dictionary - - { - - ["Value"] = "V1", - - ["Order"] = "0", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "**", - - ["ContentRootSuffix"] = "suffixA" - - }), - - new TaskItem("GroupB", new Dictionary - - { - - ["Value"] = "V2", - - ["Order"] = "1", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "**", - - ["ContentRootSuffix"] = "suffixB" - - }) - - }); - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); - - asset.ContentRoot.Should().Contain("suffixA"); - - asset.ContentRoot.Should().Contain("suffixB"); - - // Suffixes compose in order: suffixA/suffixB - - asset.ContentRoot.Should().Contain(Path.Combine("suffixA", "suffixB")); - - } - - - - [TestMethod] - - [DataRow(new[] { "BootstrapVersion=V4", "BootstrapVersion=V5" }, true)] - - [DataRow(new[] { "BootstrapVersion=V4", "" }, false)] - - [DataRow(new[] { "BootstrapVersion=V5", "BootstrapVersion=V5" }, false)] - - public void AllAssetsHaveDistinctGroups_ReturnsExpectedResult(string[] groups, bool expected) - - { - - var assets = groups.Select((g, i) => CreateStaticWebAsset($"{(char)('a' + i)}.css", g)).ToList(); - - var groupSet = new HashSet(StringComparer.Ordinal); - - StaticWebAsset.AllAssetsHaveDistinctGroups(assets, groupSet).Should().Be(expected); - - } - - - - private string CreateTempFile(params string[] pathParts) - - { - - var content = pathParts[^1]; - - var segments = pathParts[..^1]; - - - - var dir = Path.Combine(new[] { _tempDir }.Concat(segments[..^1]).ToArray()); - - Directory.CreateDirectory(dir); - - var filePath = Path.Combine(dir, segments[^1]); - - File.WriteAllText(filePath, content); - - return filePath; - - } - - - - private ITaskItem CreateExternalAsset(string filePath, string sourceId, string relativePath) - - { - - var contentRoot = Path.GetDirectoryName(filePath) + Path.DirectorySeparatorChar; - - return new TaskItem(filePath, new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = sourceId, - - ["ContentRoot"] = contentRoot, - - ["BasePath"] = "", - - ["RelativePath"] = relativePath, - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["CopyToOutputDirectory"] = "PreserveNewest", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["OriginalItemSpec"] = filePath, - - }); - - } - - - - private ITaskItem CreateExternalAssetWithGroups(string filePath, string sourceId, string relativePath, string assetGroups) - - { - - var item = CreateExternalAsset(filePath, sourceId, relativePath); - - item.SetMetadata("AssetGroups", assetGroups); - - return item; - - } - - - - private static ITaskItem CreateReferenceAsset( - - string itemSpec, - - string sourceId, - - string sourceType, - - string relativePath, - - string assetKind, - - string assetMode, - - string assetGroups = null) - - { - - var result = new StaticWebAsset() - - { - - Identity = Path.GetFullPath(itemSpec), - - SourceId = sourceId, - - SourceType = sourceType, - - ContentRoot = Directory.GetCurrentDirectory(), - - BasePath = "base", - - RelativePath = relativePath, - - AssetKind = assetKind, - - AssetMode = assetMode, - - AssetRole = "Primary", - - RelatedAsset = "", - - AssetTraitName = "", - - AssetTraitValue = "", - - CopyToOutputDirectory = "", - - CopyToPublishDirectory = "", - - OriginalItemSpec = itemSpec, - - Integrity = "integrity", - - Fingerprint = "fingerprint", - - FileLength = 10, - - LastWriteTime = DateTime.UtcNow, - - }; - - - - if (!string.IsNullOrEmpty(assetGroups)) - - { - - result.AssetGroups = assetGroups; - - } - - - - result.ApplyDefaults(); - - result.Normalize(); - - - - return result.ToTaskItem(); - - } - - - - private static StaticWebAsset CreateStaticWebAsset(string identity, string assetGroups) - - { - - var asset = new StaticWebAsset - - { - - Identity = Path.GetFullPath(identity), - - SourceId = "TestLib", - - SourceType = "Package", - - ContentRoot = Directory.GetCurrentDirectory(), - - BasePath = "base", - - RelativePath = identity, - - AssetKind = "All", - - AssetMode = "All", - - AssetRole = "Primary", - - RelatedAsset = "", - - AssetTraitName = "", - - AssetTraitValue = "", - - CopyToOutputDirectory = "", - - CopyToPublishDirectory = "", - - OriginalItemSpec = identity, - - Integrity = "integrity", - - Fingerprint = "fingerprint", - - FileLength = 10, - - LastWriteTime = DateTime.UtcNow, - - AssetGroups = assetGroups, - - }; - - - - asset.ApplyDefaults(); - - asset.Normalize(); - - return asset; - - } - - - - private ITaskItem CreateCandidateAssetItem(string file, string integrity = "integrity", string fingerprint = "fingerprint", string fileLength = "10") - - { - - return new TaskItem(file, new Dictionary - - { - - ["RelativePath"] = "", - - ["TargetPath"] = "", - - ["Link"] = "", - - ["CopyToOutputDirectory"] = "", - - ["CopyToPublishDirectory"] = "", - - ["Integrity"] = integrity, - - ["Fingerprint"] = fingerprint, - - ["LastWriteTime"] = DateTime.UtcNow.ToString(StaticWebAsset.DateTimeAssetFormat), - - ["FileLength"] = fileLength, - - }); - - } - - - - private DefineStaticWebAssets CreateDefineStaticWebAssetsTask( - - ITaskItem[] candidates, - - ITaskItem[] groupDefs, - - string sourceId = "IdentityUI", - - string contentRoot = null, - - string basePath = "Identity") - - { - - return new DefineStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - TestResolveFileDetails = (_, _) => (null, 10, new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero)), - - CandidateAssets = candidates, - - RelativePathPattern = "wwwroot/**", - - SourceType = "Discovered", - - SourceId = sourceId, - - ContentRoot = contentRoot ?? Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar, - - BasePath = basePath, - - StaticWebAssetGroupDefinitions = groupDefs - - }; - - } - - - - [TestMethod] - - public void ApplyGroupDefinitions_ContentRootSuffix_AdjustsContentRoot() - - { - - var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body-v4{}"); - - var wwwrootPath = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; - - - - var task = CreateDefineStaticWebAssetsTask( - - new[] { CreateCandidateAssetItem(file) }, - - new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V4", - - ["Order"] = "0", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "V4/**", - - ["RelativePathPattern"] = "V4/**", - - ["ContentRootSuffix"] = "V4" - - }) - - }); - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - _errorMessages.Should().BeEmpty(); - - task.Assets.Should().HaveCount(1); - - - - var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); - - asset.RelativePath.Should().Be("css/site.css", "RelativePathPattern stripped the V4/ prefix"); - - asset.ContentRoot.Should().Be(wwwrootPath + "V4" + Path.DirectorySeparatorChar, - - "ContentRootSuffix appended V4 to wwwroot path"); - - asset.AssetGroups.Should().Contain("BootstrapVersion=V4"); - - } - - - - [TestMethod] - - public void ApplyGroupDefinitions_PatternOnly_NoAutoFileOnlyToken() - - { - - var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body-v4{}"); - - var wwwrootPath = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; - - - - var task = CreateDefineStaticWebAssetsTask( - - new[] { CreateCandidateAssetItem(file) }, - - new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V4", - - ["Order"] = "0", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "V4/**", - - ["RelativePathPattern"] = "V4/**" - - // No RelativePathPrefix, no ContentRootSuffix - - }) - - }); - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - _errorMessages.Should().BeEmpty(); - - task.Assets.Should().HaveCount(1); - - - - var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); - - asset.RelativePath.Should().Be("css/site.css", - - "RelativePathPattern stripped V4/ prefix — no auto-injection of file-only ~ token"); - - asset.RelativePath.Should().NotContain("~", "SDK must not auto-inject file-only tokens"); - - asset.ContentRoot.Should().Be(wwwrootPath, "ContentRoot unchanged when no ContentRootSuffix"); - - } - - - - [TestMethod] - - public void ApplyGroupDefinitions_RelativePathPrefix_FileOnlyToken() - - { - - var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body-v4{}"); - - var wwwrootPath = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; - - - - var task = CreateDefineStaticWebAssetsTask( - - new[] { CreateCandidateAssetItem(file) }, - - new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V4", - - ["Order"] = "0", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "V4/**", - - ["RelativePathPattern"] = "V4/**", - - ["RelativePathPrefix"] = "#[{BootstrapVersion}/]~" - - }) - - }); - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - _errorMessages.Should().BeEmpty(); - - task.Assets.Should().HaveCount(1); - - - - var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); - - asset.RelativePath.Should().Be("#[{BootstrapVersion}/]~css/site.css", - - "pattern strips V4/, prefix prepends file-only token expression"); - - asset.ContentRoot.Should().Be(wwwrootPath, "ContentRoot unchanged when no ContentRootSuffix"); - - asset.AssetGroups.Should().Contain("BootstrapVersion=V4"); - - } - - - - [TestMethod] - - public void ApplyGroupDefinitions_RelativePathPrefix_LiteralPrepend() - - { - - var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body-v4{}"); - - var wwwrootPath = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; - - - - var task = CreateDefineStaticWebAssetsTask( - - new[] { CreateCandidateAssetItem(file) }, - - new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V4", - - ["Order"] = "0", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "V4/**", - - ["RelativePathPattern"] = "V4/**", - - ["RelativePathPrefix"] = "shared/" - - }) - - }); - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - _errorMessages.Should().BeEmpty(); - - task.Assets.Should().HaveCount(1); - - - - var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); - - asset.RelativePath.Should().Be("shared/css/site.css", - - "pattern strips V4/, prefix prepends literal 'shared/'"); - - asset.ContentRoot.Should().Be(wwwrootPath, "ContentRoot unchanged when no ContentRootSuffix"); - - } - - - - [TestMethod] - - public void ApplyGroupDefinitions_AllThreeOrthogonal() - - { - - var file = CreateTempFile("wwwroot", "V4", "css", "site.css", "body-v4{}"); - - var wwwrootPath = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; - - - - var task = CreateDefineStaticWebAssetsTask( - - new[] { CreateCandidateAssetItem(file) }, - - new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V4", - - ["Order"] = "0", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "V4/**", - - ["RelativePathPattern"] = "V4/**", - - ["RelativePathPrefix"] = "shared/", - - ["ContentRootSuffix"] = "V4" - - }) - - }); - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - _errorMessages.Should().BeEmpty(); - - task.Assets.Should().HaveCount(1); - - - - var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); - - asset.RelativePath.Should().Be("shared/css/site.css", - - "pattern strips V4/, prefix prepends 'shared/'"); - - asset.ContentRoot.Should().Be(wwwrootPath + "V4" + Path.DirectorySeparatorChar, - - "ContentRootSuffix applied independently"); - - asset.AssetGroups.Should().Contain("BootstrapVersion=V4"); - - } - - - - [TestMethod] - - public void ApplyGroupDefinitions_RelativePathPrefix_WithoutPattern_PrependsToOriginalPath() - - { - - var file = CreateTempFile("wwwroot", "css", "site.css", "body{}"); - - var wwwrootPath = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; - - - - var task = CreateDefineStaticWebAssetsTask( - - new[] { CreateCandidateAssetItem(file) }, - - new ITaskItem[] - - { - - new TaskItem("Theme", new Dictionary - - { - - ["Value"] = "Default", - - ["Order"] = "0", - - ["SourceId"] = "MyLib", - - ["IncludePattern"] = "**", - - ["RelativePathPrefix"] = "lib/" - - // No RelativePathPattern — no stripping, just prepend - - }) - - }, - - sourceId: "MyLib", - - basePath: "mylib"); - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - _errorMessages.Should().BeEmpty(); - - task.Assets.Should().HaveCount(1); - - - - var asset = StaticWebAsset.FromTaskItem(task.Assets[0]); - - asset.RelativePath.Should().Be("lib/css/site.css", - - "no pattern stripping, but prefix 'lib/' prepended to original path"); - - asset.AssetGroups.Should().Contain("Theme=Default"); - - } - - - - [TestMethod] - - public void ApplyGroupDefinitions_ContentRootSuffix_MultipleGroups_EachGetsOwnContentRoot() - - { - - var fileV4 = CreateTempFile("wwwroot", "V4", "css", "site.css", "body-v4{}"); - - var fileV5 = CreateTempFile("wwwroot", "V5", "css", "site.css", "body-v5{}"); - - var wwwrootPath = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; - - - - var task = CreateDefineStaticWebAssetsTask( - - new[] - - { - - CreateCandidateAssetItem(fileV4, integrity: "integrity-v4", fingerprint: "fingerprint-v4"), - - CreateCandidateAssetItem(fileV5, integrity: "integrity-v5", fingerprint: "fingerprint-v5", fileLength: "11") - - }, - - new ITaskItem[] - - { - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V5", - - ["Order"] = "0", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "V5/**", - - ["RelativePathPattern"] = "V5/**", - - ["ContentRootSuffix"] = "V5" - - }), - - new TaskItem("BootstrapVersion", new Dictionary - - { - - ["Value"] = "V4", - - ["Order"] = "1", - - ["SourceId"] = "IdentityUI", - - ["IncludePattern"] = "V4/**", - - ["RelativePathPattern"] = "V4/**", - - ["ContentRootSuffix"] = "V4" - - }) - - }); - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - _errorMessages.Should().BeEmpty(); - - task.Assets.Should().HaveCount(2); - - - - var assets = task.Assets.Select(a => StaticWebAsset.FromTaskItem(a)).ToList(); - - var v4Asset = assets.Single(a => a.AssetGroups.Contains("BootstrapVersion=V4")); - - var v5Asset = assets.Single(a => a.AssetGroups.Contains("BootstrapVersion=V5")); - - - - v4Asset.ContentRoot.Should().Be(wwwrootPath + "V4" + Path.DirectorySeparatorChar, - - "V4 asset gets its own ContentRoot with V4 suffix"); - - v4Asset.RelativePath.Should().Be("css/site.css"); - - - - v5Asset.ContentRoot.Should().Be(wwwrootPath + "V5" + Path.DirectorySeparatorChar, - - "V5 asset gets its own ContentRoot with V5 suffix"); - - v5Asset.RelativePath.Should().Be("css/site.css"); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetToCompressTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetToCompressTest.cs index 60f5a6d6a4b1..babd0f1abecb 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetToCompressTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetToCompressTest.cs @@ -2,520 +2,180 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks.Utils; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - - - [TestClass] public class AssetToCompressTest : IDisposable - - { - - private readonly string _testDirectory; - - private readonly string _testFilePath; - - private readonly Mock _buildEngine; - - private readonly TaskLoggingHelper _log; - - private readonly List _errorMessages; - - private readonly List _logMessages; - - - - public AssetToCompressTest() - - { - - _testDirectory = Path.Combine(SdkTestContext.Current.TestExecutionDirectory, nameof(AssetToCompressTest), Guid.NewGuid().ToString("N")); - - Directory.CreateDirectory(_testDirectory); - - _testFilePath = Path.Combine(_testDirectory, "test-asset.js"); - - File.WriteAllText(_testFilePath, "// test content"); - - - - _errorMessages = new List(); - - _logMessages = new List(); - - _buildEngine = new Mock(); - - _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => _errorMessages.Add(args.Message)); - - _buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => _logMessages.Add(args.Message)); - - - - var dummyTask = new Mock(); - - dummyTask.Setup(t => t.BuildEngine).Returns(_buildEngine.Object); - - _log = new TaskLoggingHelper(dummyTask.Object); - - } - - - - public void Dispose() - - { - - if (Directory.Exists(_testDirectory)) - - { - - Directory.Delete(_testDirectory, recursive: true); - - } - - } - - - - [TestMethod] - - public void TryFindInputFilePath_UsesRelatedAsset_WhenFileExists() - - { - - // Arrange - - var assetToCompress = new TaskItem("test.js.gz"); - - assetToCompress.SetMetadata("RelatedAsset", _testFilePath); - - assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", "some-other-path.js"); - - - - // Act - - var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); - - - - // Assert - - result.Should().BeTrue(); - - fullPath.Should().Be(_testFilePath); - - _errorMessages.Should().BeEmpty(); - - } - - - - [TestMethod] - - public void TryFindInputFilePath_FallsBackToRelatedAssetOriginalItemSpec_WhenRelatedAssetDoesNotExist() - - { - - // Arrange - - var assetToCompress = new TaskItem("test.js.gz"); - - assetToCompress.SetMetadata("RelatedAsset", Path.Combine(_testDirectory, "non-existent.js")); - - assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", _testFilePath); - - - - // Act - - var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); - - - - // Assert - - result.Should().BeTrue(); - - fullPath.Should().Be(_testFilePath); - - _errorMessages.Should().BeEmpty(); - - } - - - - [TestMethod] - - public void TryFindInputFilePath_ReturnsError_WhenNeitherPathExists() - - { - - // Arrange - - var nonExistentPath1 = Path.Combine(_testDirectory, "non-existent1.js"); - - var nonExistentPath2 = Path.Combine(_testDirectory, "non-existent2.js"); - - var assetToCompress = new TaskItem("test.js.gz"); - - assetToCompress.SetMetadata("RelatedAsset", nonExistentPath1); - - assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", nonExistentPath2); - - - - // Act - - var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); - - - - // Assert - - result.Should().BeFalse(); - - fullPath.Should().BeNull(); - - _errorMessages.Should().ContainSingle(); - - _errorMessages[0].Should().Contain("can not be found"); - - _errorMessages[0].Should().Contain(nonExistentPath1); - - _errorMessages[0].Should().Contain(nonExistentPath2); - - } - - - - [TestMethod] - - public void TryFindInputFilePath_PrefersRelatedAsset_OverRelatedAssetOriginalItemSpec_WhenBothExist() - - { - - // Arrange - create two files to simulate the scenario where both metadata values point to existing files - - var relatedAssetPath = Path.Combine(_testDirectory, "correct-asset.js"); - - var originalItemSpecPath = Path.Combine(_testDirectory, "project-file.esproj"); - - File.WriteAllText(relatedAssetPath, "// correct JavaScript content"); - - File.WriteAllText(originalItemSpecPath, ""); - - - - var assetToCompress = new TaskItem("test.js.gz"); - - assetToCompress.SetMetadata("RelatedAsset", relatedAssetPath); - - assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", originalItemSpecPath); - - - - // Act - - var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); - - - - // Assert - should prefer RelatedAsset (the actual JavaScript file) over RelatedAssetOriginalItemSpec (the esproj file) - - result.Should().BeTrue(); - - fullPath.Should().Be(relatedAssetPath); - - fullPath.Should().NotBe(originalItemSpecPath); - - _errorMessages.Should().BeEmpty(); - - } - - - - [TestMethod] - - public void TryFindInputFilePath_HandlesEmptyRelatedAsset_AndUsesRelatedAssetOriginalItemSpec() - - { - - // Arrange - - var assetToCompress = new TaskItem("test.js.gz"); - - assetToCompress.SetMetadata("RelatedAsset", ""); - - assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", _testFilePath); - - - - // Act - - var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); - - - - // Assert - - result.Should().BeTrue(); - - fullPath.Should().Be(_testFilePath); - - _errorMessages.Should().BeEmpty(); - - } - - - - [TestMethod] - - public void TryFindInputFilePath_HandlesEsprojScenario_WhereOriginalItemSpecPointsToProjectFile() - - { - - // Arrange - simulate the esproj bug scenario where RelatedAssetOriginalItemSpec - - // incorrectly points to the .esproj project file instead of the actual JS asset - - var esprojFile = Path.Combine(_testDirectory, "MyProject.esproj"); - - var actualJsFile = Path.Combine(_testDirectory, "dist", "app.min.js"); - - - - Directory.CreateDirectory(Path.GetDirectoryName(actualJsFile)); - - File.WriteAllText(esprojFile, ""); - - File.WriteAllText(actualJsFile, "// actual JavaScript content"); - - - - var assetToCompress = new TaskItem(Path.Combine(_testDirectory, "compressed", "app.min.js.gz")); - - // RelatedAsset should contain the correct path to the actual JS file - - assetToCompress.SetMetadata("RelatedAsset", actualJsFile); - - // RelatedAssetOriginalItemSpec may incorrectly point to .esproj due to esproj SDK bug - - assetToCompress.SetMetadata("RelatedAssetOriginalItemSpec", esprojFile); - - - - // Act - - var result = AssetToCompress.TryFindInputFilePath(assetToCompress, _log, out var fullPath); - - - - // Assert - should use RelatedAsset (correct JS file) not RelatedAssetOriginalItemSpec (esproj file) - - result.Should().BeTrue(); - - fullPath.Should().Be(actualJsFile); - - fullPath.Should().NotBe(esprojFile); - - _errorMessages.Should().BeEmpty(); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeCssScopesTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeCssScopesTests.cs index bf3726bf10a5..54ef5da1423f 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeCssScopesTests.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeCssScopesTests.cs @@ -6,396 +6,135 @@ using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Utilities; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - namespace Microsoft.NET.Sdk.Razor.Test - - { - - [TestClass] - public class ComputeCssScopesTests - - { - - [TestMethod] - - public void ComputesScopes_ComputesUniqueScopes_ForCssFiles() - - { - - // Arrange - - var taskInstance = new ComputeCssScope() - - { - - ScopedCssInput = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.razor.css"), - - new TaskItem("TestFiles/Pages/Index.razor.css"), - - new TaskItem("TestFiles/Pages/Profile.razor.css"), - - }, - - TargetName = "Test" - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().Be(true); - - taskInstance.ScopedCss.Select(s => s.GetMetadata("CssScope")).Should().OnlyContain(item => - - !string.IsNullOrEmpty(item) && new Regex("b-[a-z0-9]+").IsMatch(item)); - - - - taskInstance.ScopedCss.Select(s => s.GetMetadata("CssScope")).Should().HaveCount(3).And.OnlyHaveUniqueItems(); - - } - - - - [TestMethod] - - public void ComputesScopes_ScopeVariesByTargetName() - - { - - // Arrange - - var taskInstance = new ComputeCssScope() - - { - - ScopedCssInput = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.razor.css"), - - new TaskItem("TestFiles/Pages/Index.razor.css"), - - new TaskItem("TestFiles/Pages/Profile.razor.css"), - - }, - - TargetName = "Test" - - }; - - - - // Act - - taskInstance.Execute(); - - var existing = taskInstance.ScopedCss.Select(s => s.GetMetadata("CssScope")).ToArray(); - - - - taskInstance.TargetName = "AnotherLibrary"; - - var result = taskInstance.Execute(); - - - - // Assert - - taskInstance.ScopedCss.Should().OnlyContain(newScoped => !existing.Contains(newScoped.GetMetadata("ScopedCss"))); - - } - - - - [TestMethod] - - public void ComputesScopes_IsDeterministic() - - { - - // Arrange - - var taskInstance = new ComputeCssScope() - - { - - ScopedCssInput = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.razor.css"), - - new TaskItem("TestFiles/Pages/Index.razor.css"), - - new TaskItem("TestFiles/Pages/Profile.razor.css"), - - }, - - TargetName = "Test" - - }; - - - - // Act - - taskInstance.Execute(); - - var existing = taskInstance.ScopedCss.Select(s => s.GetMetadata("CssScope")).OrderBy(id => id).ToArray(); - - - - var result = taskInstance.Execute(); - - - - // Assert - - var computed = taskInstance.ScopedCss.Select(newScoped => newScoped.GetMetadata("CssScope")).OrderBy(id => id).ToArray(); - - computed.Should().Equal(existing); - - } - - - - [TestMethod] - - public void ComputesScopes_VariesByPath() - - { - - // Arrange - - var taskInstance = new ComputeCssScope() - - { - - ScopedCssInput = new[] - - { - - new TaskItem("TestFiles/Pages/Index.razor.css"), - - new TaskItem("TestFiles/Index.razor.css"), - - }, - - TargetName = "Test" - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.ScopedCss.Should().HaveCount(2); - - taskInstance.ScopedCss[0].GetMetadata("CssScope").Should().NotBe(taskInstance.ScopedCss[1].GetMetadata("CssScope")); - - } - - - - [TestMethod] - - public void ComputesScopes_PreservesUserDefinedScopes() - - { - - // Arrange - - var taskInstance = new ComputeCssScope() - - { - - ScopedCssInput = new[] - - { - - new TaskItem("TestFiles/Pages/Index.razor.css", new Dictionary{ ["CssScope"] = "b-predefined" }), }, - - TargetName = "Test" - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.ScopedCss.Should().ContainSingle(scopedCss => scopedCss.GetMetadata("CssScope") == "b-predefined"); - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsMultiThreadingTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsMultiThreadingTest.cs index 12700f664f74..a88ca0dab1c8 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsMultiThreadingTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsMultiThreadingTest.cs @@ -2,338 +2,120 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - - - [DoNotParallelize] [TestClass] public class ComputeEndpointsForReferenceStaticWebAssetsMultiThreadingTest - - { - - [TestMethod] - - public void ProducesCorrectEndpointsWhenTaskEnvironmentProjectDirectoryDiffersFromProcessCurrentDirectory() - - { - - var testRoot = Path.Combine(AppContext.BaseDirectory, nameof(ComputeEndpointsForReferenceStaticWebAssetsMultiThreadingTest), Guid.NewGuid().ToString("N")); - - var projectDir = Path.Combine(testRoot, "project"); - - var spawnDir = Path.Combine(testRoot, "decoy", "spawn"); - - Directory.CreateDirectory(projectDir); - - Directory.CreateDirectory(spawnDir); - - - - const string relativeContentRoot = "wwwroot"; - - - - var projectAbsoluteContentRoot = Path.GetFullPath(Path.Combine(projectDir, relativeContentRoot)); - - var spawnAbsoluteContentRoot = Path.GetFullPath(Path.Combine(spawnDir, relativeContentRoot)); - - projectAbsoluteContentRoot.Should().NotBe(spawnAbsoluteContentRoot, - - "the test setup must place project and decoy in different parents so a relative path resolves differently against each"); - - - - var assetIdentity = Path.Combine(projectAbsoluteContentRoot, "candidate.js"); - - - - var originalCurrentDirectory = Directory.GetCurrentDirectory(); - - try - - { - - Directory.SetCurrentDirectory(spawnDir); - - - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeEndpointsForReferenceStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir), - - Assets = new[] { CreateAssetItemWithRelativeContentRoot(assetIdentity, relativeContentRoot, basePath: "base") }, - - CandidateEndpoints = new[] { CreateCandidateEndpoint(route: "candidate.js", assetFile: assetIdentity) } - - }; - - - - var result = task.Execute(); - - - - result.Should().BeTrue("the task must run to completion when TaskEnvironment.ProjectDirectory differs from the process CWD"); - - errorMessages.Should().BeEmpty(); - - task.Endpoints.Should().ContainSingle(); - - - - // The route is re-rooted under the asset's BasePath — proves the endpoint - - // matched the asset in the dictionary (assets[endpoint.AssetFile] succeeded) - - // and the BasePath-application branch ran. - - task.Endpoints[0].ItemSpec.Should().Be("base/candidate.js"); - - - - // AssetFile is passed through unchanged from the input endpoint. - - task.Endpoints[0].GetMetadata("AssetFile").Should().Be(assetIdentity); - - } - - finally - - { - - Directory.SetCurrentDirectory(originalCurrentDirectory); - - if (Directory.Exists(testRoot)) - - { - - Directory.Delete(testRoot, recursive: true); - - } - - } - - } - - - - private static ITaskItem CreateAssetItemWithRelativeContentRoot(string identity, string relativeContentRoot, string basePath) - - { - - var asset = new StaticWebAsset - - { - - Identity = identity, - - SourceId = "MyPackage", - - SourceType = StaticWebAsset.SourceTypes.Discovered, - - ContentRoot = relativeContentRoot, - - BasePath = basePath, - - RelativePath = Path.GetFileName(identity), - - AssetKind = StaticWebAsset.AssetKinds.All, - - AssetMode = StaticWebAsset.AssetModes.All, - - AssetRole = StaticWebAsset.AssetRoles.Primary, - - RelatedAsset = "", - - AssetTraitName = "", - - AssetTraitValue = "", - - CopyToOutputDirectory = "", - - CopyToPublishDirectory = "", - - OriginalItemSpec = identity, - - Integrity = "integrity", - - Fingerprint = "fingerprint", - - LastWriteTime = DateTime.UtcNow, - - FileLength = 10, - - }; - - - - return asset.ToTaskItem(); - - } - - - - private static ITaskItem CreateCandidateEndpoint(string route, string assetFile) - - { - - return new StaticWebAssetEndpoint - - { - - Route = route, - - AssetFile = assetFile, - - EndpointProperties = [], - - }.ToTaskItem(); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsTest.cs index f7abdf300736..b1f79a0dfb47 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeEndpointsForReferenceStaticWebAssetsTest.cs @@ -2,661 +2,227 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - - - [TestClass] public class ComputeEndpointsForReferenceStaticWebAssetsTest - - { - - [TestMethod] - - public void IncludesEndpointsForAssetsFromCurrentProject() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeEndpointsForReferenceStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - Assets = [CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All")], - - CandidateEndpoints = [CreateCandidateEndpoint("candidate.js", Path.Combine("wwwroot", "candidate.js"))] - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.Endpoints.Should().ContainSingle(); - - task.Endpoints[0].ItemSpec.Should().Be("base/candidate.js"); - - task.Endpoints[0].GetMetadata("AssetFile").Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - } - - - - [TestMethod] - - public void UpdatesLabelAsNecessary_ForChosenEndpoints() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeEndpointsForReferenceStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - Assets = [CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All")], - - CandidateEndpoints = [CreateCandidateEndpoint("candidate.js", Path.Combine("wwwroot", "candidate.js"), addLabel: true)] - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.Endpoints.Should().ContainSingle(); - - task.Endpoints[0].ItemSpec.Should().Be("base/candidate.js"); - - task.Endpoints[0].GetMetadata("AssetFile").Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - var properties = StaticWebAssetEndpointProperty.FromMetadataValue(task.Endpoints[0].GetMetadata("EndpointProperties")); - - properties.Should().ContainSingle(); - - properties[0].Name.Should().Be("label"); - - properties[0].Value.Should().Be("base/label-value"); - - } - - - - [TestMethod] - - public void FiltersOutEndpointsForAssetsNotFound() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeEndpointsForReferenceStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - Assets = [CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All")], - - CandidateEndpoints = [ - - CreateCandidateEndpoint("candidate.js", Path.Combine("wwwroot", "candidate.js")), - - CreateCandidateEndpoint("package.js", Path.Combine("..", "_content", "package-id", "package.js")) - - ] - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.Endpoints.Where(e => e != null).Should().ContainSingle(); - - task.Endpoints[0].ItemSpec.Should().Be("base/candidate.js"); - - task.Endpoints[0].GetMetadata("AssetFile").Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - } - - - - [TestMethod] - - public void AppliesBasePathWhenRouteStartsWithBasePathButNotAsPathSegment() - - { - - // This test verifies the fix for a bug where routes like "App1.styles.css" - - // were incorrectly skipped because they start with the BasePath "App1". - - // The correct behavior is that the base path should only be considered - - // "already applied" if the route starts with "App1/" (as a path segment), - - // not just any string starting with "App1". - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - // Create an asset with BasePath "App1" and a route "App1.styles.css" - - // The route starts with "App1" but NOT "App1/", so the base path should still be applied - - var task = new ComputeEndpointsForReferenceStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - Assets = [CreateCandidate( - - Path.Combine("obj", "scopedcss", "bundle", "App1.styles.css"), - - "App1", - - "Project", - - "App1.styles.css", - - "All", - - "CurrentProject", - - basePath: "App1")], - - CandidateEndpoints = [CreateCandidateEndpoint("App1.styles.css", Path.Combine("obj", "scopedcss", "bundle", "App1.styles.css"))] - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.Endpoints.Should().ContainSingle(); - - // The route should be "App1/App1.styles.css", not just "App1.styles.css" - - task.Endpoints[0].ItemSpec.Should().Be("App1/App1.styles.css"); - - } - - - - [TestMethod] - - public void SkipsBasePathApplicationWhenRouteAlreadyHasBasePathAsPathSegment() - - { - - // This test verifies that routes already starting with "BasePath/" are correctly skipped - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeEndpointsForReferenceStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - Assets = [CreateCandidate( - - Path.Combine("wwwroot", "css", "app.css"), - - "App1", - - "Discovered", - - "css/app.css", - - "All", - - "All", - - basePath: "App1")], - - // Route already has the base path as a path segment - - CandidateEndpoints = [CreateCandidateEndpoint("App1/css/app.css", Path.Combine("wwwroot", "css", "app.css"))] - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.Endpoints.Should().ContainSingle(); - - // Should remain "App1/css/app.css", not become "App1/App1/css/app.css" - - task.Endpoints[0].ItemSpec.Should().Be("App1/css/app.css"); - - } - - - - private static ITaskItem CreateCandidate( - - string itemSpec, - - string sourceId, - - string sourceType, - - string relativePath, - - string assetKind, - - string assetMode, - - string basePath = "base") - - { - - var result = new StaticWebAsset() - - { - - Identity = Path.GetFullPath(itemSpec), - - SourceId = sourceId, - - SourceType = sourceType, - - ContentRoot = Directory.GetCurrentDirectory(), - - BasePath = basePath, - - RelativePath = relativePath, - - AssetKind = assetKind, - - AssetMode = assetMode, - - AssetRole = "Primary", - - RelatedAsset = "", - - AssetTraitName = "", - - AssetTraitValue = "", - - CopyToOutputDirectory = "", - - CopyToPublishDirectory = "", - - OriginalItemSpec = itemSpec, - - // Add these to avoid accessing the disk to compute them - - Integrity = "integrity", - - Fingerprint = "fingerprint", - - LastWriteTime = DateTime.UtcNow, - - FileLength = 10, - - }; - - - - result.ApplyDefaults(); - - result.Normalize(); - - - - return result.ToTaskItem(); - - } - - - - private static ITaskItem CreateCandidateEndpoint(string route, string assetFile, bool addLabel = false) - - { - - return new StaticWebAssetEndpoint - - { - - Route = route, - - AssetFile = Path.GetFullPath(assetFile), - - EndpointProperties = addLabel - - ? [new StaticWebAssetEndpointProperty { Name = "label", Value = "label-value" }] - - : [], - - }.ToTaskItem(); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeStaticWebAssetsForCurrentProjectTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeStaticWebAssetsForCurrentProjectTest.cs index b1136ff2be8d..9410b7b91e0b 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeStaticWebAssetsForCurrentProjectTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ComputeStaticWebAssetsForCurrentProjectTest.cs @@ -2,949 +2,322 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Text.Json; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class ComputeStaticWebAssetsForCurrentProjectTest - - { - - [TestMethod] - - public void IncludesAssetsFromCurrentProject() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeStaticWebAssetsForCurrentProject - - { - - BuildEngine = buildEngine.Object, - - Source = "MyPackage", - - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "All") }, - - AssetKind = "Build", - - ProjectMode = "Default" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.StaticWebAssets.Should().HaveCount(1); - - } - - - - [TestMethod] - - public void PrefersSpecificKindAssetsOverAllKindAssets() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeStaticWebAssetsForCurrentProject - - { - - BuildEngine = buildEngine.Object, - - Source = "MyPackage", - - Assets = new[] - - { - - CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All"), - - CreateCandidate(Path.Combine("wwwroot", "candidate.other.js"), "MyPackage", "Discovered", "candidate.js", "Build", "All") - - }, - - AssetKind = "Build", - - ProjectMode = "Default" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.StaticWebAssets.Should().HaveCount(1); - - task.StaticWebAssets[0].ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.other.js"))); - - } - - - - [TestMethod] - - public void AllAssetGetsIgnoredWhenBuildAndPublishAssetsAreDefined() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeStaticWebAssetsForCurrentProject - - { - - BuildEngine = buildEngine.Object, - - Source = "MyPackage", - - Assets = new[] - - { - - CreateCandidate(Path.Combine("wwwroot", "candidate.js"), "MyPackage", "Discovered", "candidate.js", "All", "All"), - - CreateCandidate(Path.Combine("wwwroot", "candidate.other.js"), "MyPackage", "Discovered", "candidate.js", "Build", "All"), - - CreateCandidate(Path.Combine("wwwroot", "candidate.publish.js"), "MyPackage", "Discovered", "candidate.js", "Publish", "All") - - }, - - AssetKind = "Build", - - ProjectMode = "Default" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.StaticWebAssets.Should().HaveCount(1); - - task.StaticWebAssets[0].ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.other.js"))); - - } - - - - [TestMethod] - - [DataRow("Build", "Publish")] - - [DataRow("Publish", "Build")] - - public void FiltersAssetsForOppositeKind(string assetKind, string manifestKind) - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeStaticWebAssetsForCurrentProject - - { - - BuildEngine = buildEngine.Object, - - Source = "MyPackage", - - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", assetKind, "All") }, - - AssetKind = manifestKind, - - ProjectMode = "Default" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.StaticWebAssets.Should().HaveCount(0); - - } - - - - [TestMethod] - - public void IncludesCurrentProjectOnlyAssetsInDefaultMode() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeStaticWebAssetsForCurrentProject - - { - - BuildEngine = buildEngine.Object, - - Source = "MyPackage", - - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "CurrentProject") }, - - AssetKind = "Default", - - ProjectMode = "Default" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.StaticWebAssets.Should().HaveCount(1); - - } - - - - [TestMethod] - - public void FiltersReferenceAssetsInDefaultMode() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeStaticWebAssetsForCurrentProject - - { - - BuildEngine = buildEngine.Object, - - Source = "MyPackage", - - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "Reference") }, - - AssetKind = "Default", - - ProjectMode = "Default" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.StaticWebAssets.Should().HaveCount(0); - - } - - - - [TestMethod] - - public void IncludesCurrentProjectAssetsInRootMode() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeStaticWebAssetsForCurrentProject - - { - - BuildEngine = buildEngine.Object, - - Source = "MyPackage", - - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "CurrentProject") }, - - AssetKind = "Default", - - ProjectMode = "Root" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.StaticWebAssets.Should().HaveCount(1); - - } - - - - [TestMethod] - - public void FiltersReferenceOnlyAssetsInRootMode() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeStaticWebAssetsForCurrentProject - - { - - BuildEngine = buildEngine.Object, - - Source = "MyPackage", - - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "MyPackage", "Discovered", "candidate.js", "All", "Reference") }, - - AssetKind = "Default", - - ProjectMode = "Root" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.StaticWebAssets.Should().HaveCount(0); - - } - - - - [TestMethod] - - public void IncludesAssetsFromOtherProjects() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeStaticWebAssetsForCurrentProject - - { - - BuildEngine = buildEngine.Object, - - Source = "MyPackage", - - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "Other", "Project", "candidate.js", "All", "All") }, - - AssetKind = "Build", - - ProjectMode = "Default" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.StaticWebAssets.Should().HaveCount(1); - - } - - - - [TestMethod] - - public void IncludesAssetsFromPackages() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ComputeStaticWebAssetsForCurrentProject - - { - - BuildEngine = buildEngine.Object, - - Source = "MyPackage", - - Assets = new[] { CreateCandidate("wwwroot\\candidate.js", "Other", "Package", "candidate.js", "All", "All") }, - - AssetKind = "Build", - - ProjectMode = "Default" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.StaticWebAssets.Should().HaveCount(1); - - } - - - - private static ITaskItem CreateCandidate( - - string itemSpec, - - string sourceId, - - string sourceType, - - string relativePath, - - string assetKind, - - string assetMode) - - { - - var result = new StaticWebAsset() - - { - - Identity = Path.GetFullPath(itemSpec), - - SourceId = sourceId, - - SourceType = sourceType, - - ContentRoot = Directory.GetCurrentDirectory(), - - BasePath = "base", - - RelativePath = relativePath, - - AssetKind = assetKind, - - AssetMode = assetMode, - - AssetRole = "Primary", - - RelatedAsset = "", - - AssetTraitName = "", - - AssetTraitValue = "", - - CopyToOutputDirectory = "", - - CopyToPublishDirectory = "", - - OriginalItemSpec = itemSpec, - - // Add these to avoid accessing the disk to compute them - - Integrity = "integrity", - - Fingerprint = "fingerprint", - - LastWriteTime = DateTime.UtcNow, - - FileLength = 10, - - }; - - - - result.ApplyDefaults(); - - result.Normalize(); - - - - return result.ToTaskItem(); - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ConcatenateFilesTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ConcatenateFilesTest.cs index fd41edc17719..a6bf84bf262a 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ConcatenateFilesTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ConcatenateFilesTest.cs @@ -6,1056 +6,356 @@ using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - namespace Microsoft.NET.Sdk.Razor.Test - - { - - [TestClass] - public class ConcatenateCssFilesTest - - { public TestContext TestContext { get; set; } = null!; - - private static readonly string BundleContent = - - @"/* _content/Test/TestFiles/Generated/Counter.razor.rz.scp.css */ - - .counter { - - font-size: 2rem; - - } - - /* _content/Test/TestFiles/Generated/Index.razor.rz.scp.css */ - - .index { - - font-weight: bold; - - } - - "; - - - - private static readonly string BundleWithImportsContent = """ - - @import '_content/Test/TestFiles/Generated/lib.bundle.scp.css'; - - @import '_content/Test/TestFiles/Generated/package.bundle.scp.css'; - - - - /* _content/Test/TestFiles/Generated/Counter.razor.rz.scp.css */ - - .counter { - - font-size: 2rem; - - } - - /* _content/Test/TestFiles/Generated/Index.razor.rz.scp.css */ - - .index { - - font-weight: bold; - - } - - """; - - - - private static readonly string UpdatedBundleContent = - - @"/* _content/Test/TestFiles/Generated/Counter.razor.rz.scp.css */ - - .counter { - - font-size: 2rem; - - } - - /* _content/Test/TestFiles/Generated/FetchData.razor.rz.scp.css */ - - .fetchData { - - font-family: Helvetica; - - } - - /* _content/Test/TestFiles/Generated/Index.razor.rz.scp.css */ - - .index { - - font-weight: bold; - - } - - "; - - - - [TestMethod] - - public void BundlesScopedCssFiles_ProducesEmpyBundleIfNoFilesAvailable() - - { - - // Arrange - - var expectedFile = Path.Combine(Directory.GetCurrentDirectory(), $"{Guid.NewGuid():N}.css"); - - var taskInstance = new ConcatenateCssFiles() - - { - - ScopedCssFiles = Array.Empty(), - - ProjectBundles = Array.Empty(), - - OutputFile = expectedFile - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - File.Exists(expectedFile).Should().BeTrue(); - - File.ReadAllText(expectedFile).Should().BeEmpty(); - - } - - - - [TestMethod] - - public void BundlesScopedCssFiles_ProducesBundle() - - { - - // Arrange - - var expectedFile = Path.Combine(Directory.GetCurrentDirectory(), $"{Guid.NewGuid():N}.css"); - - var taskInstance = new ConcatenateCssFiles() - - { - - ScopedCssFiles = new[] - - { - - CreateStaticAsset( - - "TestFiles/Generated/Counter.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/Counter.razor.rz.scp.css"), - - CreateStaticAsset( - - "TestFiles/Generated/Index.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/Index.razor.rz.scp.css"), - - }, - - ProjectBundles = Array.Empty(), - - OutputFile = expectedFile - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - File.Exists(expectedFile).Should().BeTrue(); - - - - var actualContents = File.ReadAllText(expectedFile); - - actualContents.Should().Contain(BundleContent); - - } - - - - private static TaskItem CreateEndpoint(string route) => - - new TaskItem(route); - - - - private static TaskItem CreateStaticAsset(string identity, string basePath, string relativePath) => - - new TaskItem( - - identity, - - new Dictionary - - { - - ["BasePath"] = basePath, - - ["RelativePath"] = relativePath, - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "staticwebassets"), - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["OriginalItemSpec"] = identity, - - ["Fingerprint"] = $"{Path.GetFileNameWithoutExtension(identity)}-fingerprint", - - ["Integrity"] = $"{Path.GetFileNameWithoutExtension(identity)}-integrity", - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }); - - - - [TestMethod] - - public void BundlesScopedCssFiles_IncludesOtherBundles() - - { - - // Arrange - - var expectedFile = Path.Combine(Directory.GetCurrentDirectory(), $"{Guid.NewGuid():N}.css"); - - var taskInstance = new ConcatenateCssFiles() - - { - - ScopedCssFiles = new[] - - { - - CreateStaticAsset( - - "TestFiles/Generated/Counter.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/Counter.razor.rz.scp.css"), - - CreateStaticAsset( - - "TestFiles/Generated/Index.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/Index.razor.rz.scp.css"), - - }, - - ProjectBundles = new[] - - { - - CreateEndpoint("_content/Test/TestFiles/Generated/lib.bundle.scp.css"), - - CreateEndpoint("_content/Test/TestFiles/Generated/package.bundle.scp.css"), - - }, - - ScopedCssBundleBasePath = "/", - - OutputFile = expectedFile - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - File.Exists(expectedFile).Should().BeTrue(); - - - - var actualContents = File.ReadAllText(expectedFile); - - actualContents.Should().Contain(BundleWithImportsContent); - - } - - - - [TestMethod] - - [DataRow("", "", "TestFiles/Generated/lib.bundle.scp.css")] - - [DataRow("/", "/", "TestFiles/Generated/lib.bundle.scp.css")] - - [DataRow("app", "_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - - [DataRow("app", "/_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - - [DataRow("app", "/_content/", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - - [DataRow("/app", "_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - - [DataRow("/app", "/_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - - [DataRow("/app", "/_content/", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - - [DataRow("app/", "_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - - [DataRow("app/", "/_content", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - - [DataRow("app/", "/_content/", "../_content/TestFiles/Generated/lib.bundle.scp.css")] - - [DataRow("/company/app/", "_content", "../../_content/TestFiles/Generated/lib.bundle.scp.css")] - - [DataRow("/company/app/", "/_content", "../../_content/TestFiles/Generated/lib.bundle.scp.css")] - - [DataRow("/company/app/", "/_content/", "../../_content/TestFiles/Generated/lib.bundle.scp.css")] - - public void BundlesScopedCssFiles_HandlesBasePathCombinationsCorrectly(string finalBasePath, string libraryBasePath, string expectedImport) - - { - - // Arrange - - var expectedContent = BundleWithImportsContent - - .Replace("_content/Test/TestFiles/Generated/lib.bundle.scp.css", expectedImport) - - .Replace("@import '_content/Test/TestFiles/Generated/package.bundle.scp.css';", "") - - .Replace("\r\n", "\n") - - .Replace("\n\n", "\n"); - - - - var expectedFile = Path.Combine(Directory.GetCurrentDirectory(), $"{Guid.NewGuid():N}.css"); - - var taskInstance = new ConcatenateCssFiles() - - { - - ScopedCssFiles = new[] - - { - - CreateStaticAsset( - - "TestFiles/Generated/Counter.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/Counter.razor.rz.scp.css"), - - CreateStaticAsset( - - "TestFiles/Generated/Index.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/Index.razor.rz.scp.css"), - - }, - - ProjectBundles = new[] - - { - - CreateEndpoint(StaticWebAsset.CombineNormalizedPaths("",libraryBasePath,"TestFiles/Generated/lib.bundle.scp.css", '/')) - - }, - - ScopedCssBundleBasePath = finalBasePath, - - OutputFile = expectedFile - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - File.Exists(expectedFile).Should().BeTrue(); - - - - var actualContents = File.ReadAllText(expectedFile); - - actualContents.Should().BeVisuallyEquivalentTo(expectedContent); - - } - - - - [TestMethod] - - public void BundlesScopedCssFiles_BundlesFilesInOrder() - - { - - // Arrange - - var expectedFile = Path.Combine(Directory.GetCurrentDirectory(), $"{Guid.NewGuid():N}.css"); - - var taskInstance = new ConcatenateCssFiles() - - { - - ScopedCssFiles = new[] - - { - - CreateStaticAsset( - - "TestFiles/Generated/Index.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/Index.razor.rz.scp.css"), - - CreateStaticAsset( - - "TestFiles/Generated/Counter.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/Counter.razor.rz.scp.css") - - }, - - ProjectBundles = Array.Empty(), - - OutputFile = expectedFile - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - File.Exists(expectedFile).Should().BeTrue(); - - - - var actualContents = File.ReadAllText(expectedFile); - - actualContents.Should().Contain(BundleContent); - - } - - - - [TestMethod] - - public void BundlesScopedCssFiles_DoesNotOverrideBundleForSameContents() - - { - - // Arrange - - var expectedFile = Path.Combine(Directory.GetCurrentDirectory(), $"{Guid.NewGuid():N}.css"); - - var taskInstance = new ConcatenateCssFiles() - - { - - ScopedCssFiles = new[] - - { - - CreateStaticAsset( - - "TestFiles/Generated/Index.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/Index.razor.rz.scp.css"), - - CreateStaticAsset( - - "TestFiles/Generated/Counter.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/Counter.razor.rz.scp.css") - - }, - - ProjectBundles = Array.Empty(), - - OutputFile = expectedFile - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - var lastModified = File.GetLastWriteTimeUtc(expectedFile); - - - - taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - File.Exists(expectedFile).Should().BeTrue(); - - var actualContents = File.ReadAllText(expectedFile); - - actualContents.Should().Contain(BundleContent); - - - - lastModified.Should().BeSameDateAs(File.GetLastWriteTimeUtc(expectedFile)); - - } - - - - [TestMethod] - - public async System.Threading.Tasks.Task BundlesScopedCssFiles_UpdatesBundleWhenContentsChange() - - { - - // Arrange - - var expectedFile = Path.Combine(Directory.GetCurrentDirectory(), $"{Guid.NewGuid():N}.css"); - - var taskInstance = new ConcatenateCssFiles() - - { - - ScopedCssFiles = new[] - - { - - CreateStaticAsset( - - "TestFiles/Generated/Index.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/Index.razor.rz.scp.css"), - - CreateStaticAsset( - - "TestFiles/Generated/Counter.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/Counter.razor.rz.scp.css") - - }, - - ProjectBundles = Array.Empty(), - - OutputFile = expectedFile - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - var lastModified = File.GetLastWriteTimeUtc(expectedFile); - - - - taskInstance.ScopedCssFiles = new[] - - { - - CreateStaticAsset( - - "TestFiles/Generated/Index.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/Index.razor.rz.scp.css"), - - CreateStaticAsset( - - "TestFiles/Generated/Counter.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/Counter.razor.rz.scp.css"), - - CreateStaticAsset( - - "TestFiles/Generated/FetchData.razor.rz.scp.css", - - "_content/Test/", - - "TestFiles/Generated/FetchData.razor.rz.scp.css"), - - }; - - - - await System.Threading.Tasks.Task.Delay(1000, TestContext.CancellationToken); - - taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - File.Exists(expectedFile).Should().BeTrue(); - - var actualContents = File.ReadAllText(expectedFile); - - - - actualContents.Should().Contain(UpdatedBundleContent); - - lastModified.Should().NotBe(File.GetLastWriteTimeUtc(expectedFile)); - - } - - } - - } - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ContentTypeProviderTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ContentTypeProviderTests.cs index 91b66bc42d6e..9577c021f859 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ContentTypeProviderTests.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ContentTypeProviderTests.cs @@ -6,615 +6,209 @@ using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; - - - - [TestClass] public class ContentTypeProviderTests - - { - - private readonly TaskLoggingHelper _log = new TestTaskLoggingHelper(); - - - - [TestMethod] - - public void GetContentType_ReturnsTextPlainForTextFiles() - - { - - // Arrange - - var provider = new ContentTypeProvider([]); - - - - // Act - - var contentType = provider.ResolveContentTypeMapping(CreateContext("Fake-License.txt"), _log); - - - - // Assert - - Assert.AreEqual("text/plain", contentType.MimeType); - - } - - - - [TestMethod] - - public void GetContentType_ReturnsMappingForRelativePath() - - { - - // Arrange - - var provider = new ContentTypeProvider([]); - - - - // Act - - var contentType = provider.ResolveContentTypeMapping(CreateContext("Components/Pages/Counter.razor.js"), _log); - - - - // Assert - - Assert.AreEqual("text/javascript", contentType.MimeType); - - } - - - - private StaticWebAssetGlobMatcher.MatchContext CreateContext(string v) - - { - - var ctx = StaticWebAssetGlobMatcher.CreateMatchContext(); - - ctx.SetPathAndReinitialize(v); - - return ctx; - - } - - - - // wwwroot\exampleJsInterop.js.gz - - - - [TestMethod] - - public void GetContentType_ReturnsMappingForCompressedRelativePath() - - { - - // Arrange - - var provider = new ContentTypeProvider([]); - - - - // Act - - var contentType = provider.ResolveContentTypeMapping(CreateContext("wwwroot/exampleJsInterop.js.gz"), _log); - - - - // Assert - - Assert.AreEqual("text/javascript", contentType.MimeType); - - } - - - - [TestMethod] - - public void GetContentType_HandlesFingerprintedPaths() - - { - - // Arrange - - var provider = new ContentTypeProvider([]); - - // Act - - var contentType = provider.ResolveContentTypeMapping(CreateContext("_content/RazorPackageLibraryDirectDependency/RazorPackageLibraryDirectDependency#[.{fingerprint}].bundle.scp.css.gz"), _log); - - // Assert - - Assert.AreEqual("text/css", contentType.MimeType); - - } - - - - [TestMethod] - - public void GetContentType_ReturnsDefaultForUnknownMappings() - - { - - // Arrange - - var provider = new ContentTypeProvider([]); - - - - // Act - - var contentType = provider.ResolveContentTypeMapping(CreateContext("something.unknown"), _log); - - - - // Assert - - Assert.IsNull(contentType.MimeType); - - } - - - - [TestMethod] - - [DataRow("something.unknown.gz", "application/x-gzip")] - - [DataRow("something.unknown.br", "application/octet-stream")] - - public void GetContentType_ReturnsGzipOrBrotliForUnknownCompressedMappings(string path, string expectedMapping) - - { - - // Arrange - - var provider = new ContentTypeProvider([]); - - - - // Act - - var contentType = provider.ResolveContentTypeMapping(CreateContext(path), _log); - - - - // Assert - - Assert.AreEqual(expectedMapping, contentType.MimeType); - - } - - - - [TestMethod] - - [DataRow("Fake-License.txt.gz")] - - [DataRow("Fake-License.txt.br")] - - public void GetContentType_ReturnsTextPlainForCompressedTextFiles(string path) - - { - - // Arrange - - var provider = new ContentTypeProvider([]); - - - - // Act - - var contentType = provider.ResolveContentTypeMapping(CreateContext(path), _log); - - - - // Assert - - Assert.AreEqual("text/plain", contentType.MimeType); - - } - - - - [TestMethod] - - public void GetContentType_CustomMappingOverridesBuiltInMapping() - - { - - // Arrange - - var customMapping = new ContentTypeMapping("text/html", "no-store, must-revalidate, no-cache", "*.html", 2); - - var provider = new ContentTypeProvider([customMapping]); - - - - // Act - - var contentType = provider.ResolveContentTypeMapping(CreateContext("index.html"), _log); - - - - // Assert - - Assert.AreEqual("text/html", contentType.MimeType); - - Assert.AreEqual("no-store, must-revalidate, no-cache", contentType.Cache); - - Assert.AreEqual("*.html", contentType.Pattern); - - Assert.AreEqual(2, contentType.Priority); - - } - - - - [TestMethod] - - public void GetContentType_CustomMappingOverridesBuiltInMappingForCompressedFiles() - - { - - // Arrange - - var customMapping = new ContentTypeMapping("text/html", "no-store, must-revalidate, no-cache", "*.html", 2); - - var provider = new ContentTypeProvider([customMapping]); - - - - // Act - - var contentType = provider.ResolveContentTypeMapping(CreateContext("index.html.gz"), _log); - - - - // Assert - - Assert.AreEqual("text/html", contentType.MimeType); - - Assert.AreEqual("no-store, must-revalidate, no-cache", contentType.Cache); - - Assert.AreEqual("*.html", contentType.Pattern); - - Assert.AreEqual(2, contentType.Priority); - - } - - - - [TestMethod] - - public void GetContentType_CustomJavaScriptMappingOverridesBuiltIn() - - { - - // Arrange - - var customMapping = new ContentTypeMapping("text/javascript", "max-age=3600", "*.js", 3); - - var provider = new ContentTypeProvider([customMapping]); - - - - // Act - - var contentType = provider.ResolveContentTypeMapping(CreateContext("app.js"), _log); - - - - // Assert - - Assert.AreEqual("text/javascript", contentType.MimeType); - - Assert.AreEqual("max-age=3600", contentType.Cache); - - Assert.AreEqual("*.js", contentType.Pattern); - - Assert.AreEqual(3, contentType.Priority); - - } - - - - private class TestTaskLoggingHelper : TaskLoggingHelper - - { - - public TestTaskLoggingHelper() : base(new TestTask()) - - { - - } - - - - private class TestTask : ITask - - { - - public IBuildEngine BuildEngine { get; set; } = new TestBuildEngine(); - - public ITaskHost HostObject { get; set; } = new TestTaskHost(); - - - - public bool Execute() => true; - - } - - - - private class TestBuildEngine : IBuildEngine - - { - - public bool ContinueOnError => true; - - - - public int LineNumberOfTaskNode => 0; - - - - public int ColumnNumberOfTaskNode => 0; - - - - public string ProjectFileOfTaskNode => "test.csproj"; - - - - public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => true; - - - - public void LogCustomEvent(CustomBuildEventArgs e) { } - - public void LogErrorEvent(BuildErrorEventArgs e) { } - - public void LogMessageEvent(BuildMessageEventArgs e) { } - - public void LogWarningEvent(BuildWarningEventArgs e) { } - - } - - - - private class TestTaskHost : ITaskHost - - { - - public object HostObject { get; set; } = new object(); - - } - - } - - - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DefineStaticWebAssetEndpointsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DefineStaticWebAssetEndpointsTest.cs index 094df230a3fe..9dd5f057063d 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DefineStaticWebAssetEndpointsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DefineStaticWebAssetEndpointsTest.cs @@ -2,2623 +2,883 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Diagnostics.Metrics; - - using System.Diagnostics; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - using NuGet.Packaging.Core; - - using System.Net; - - using System.Globalization; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - - - [TestClass] public class DefineStaticWebAssetEndpointsTest - - { - - [TestMethod] - - [DataRow(StaticWebAsset.SourceTypes.Discovered)] - - [DataRow(StaticWebAsset.SourceTypes.Computed)] - - public void DefinesEndpointsForAssets(string sourceType) - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - - - var task = new DefineStaticWebAssetEndpoints - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - sourceType, - - "candidate.js", - - "All", - - "All", - - fileLength: 10, - - lastWriteTime: lastWrite)], - - ExistingEndpoints = [], - - ContentTypeMappings = [CreateContentMapping("**/*.js", "text/javascript")], - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); - - endpoints.Should().ContainSingle(); - - var endpoint = endpoints[0]; - - - - endpoint.Route.Should().Be("candidate.js"); - - endpoint.AssetFile.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - endpoint.ResponseHeaders.Should().BeEquivalentTo( - - [ - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Cache-Control", - - Value = "no-cache" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Content-Length", - - Value = "10" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "ETag", - - Value = "\"integrity\"" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Last-Modified", - - Value = "Thu, 15 Nov 1990 00:00:00 GMT" - - } - - ]); - - } - - - - [TestMethod] - - public void CanDefineFingerprintedEndpoints() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - - - var task = new DefineStaticWebAssetEndpoints - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate#[.{fingerprint}]?.js", - - "All", - - "All", - - fingerprint: "1234asdf", - - integrity: "asdf1234", - - lastWriteTime: lastWrite)], - - ExistingEndpoints = [], - - ContentTypeMappings = [CreateContentMapping("**/*.js", "text/javascript")], - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); - - endpoints.Length.Should().Be(2); - - var endpoint = endpoints[0]; - - - - endpoint.Route.Should().Be("candidate.1234asdf.js"); - - endpoint.AssetFile.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - endpoint.EndpointProperties.Should().BeEquivalentTo([ - - new StaticWebAssetEndpointProperty - - { - - Name = "fingerprint", - - Value = "1234asdf" - - }, - - new StaticWebAssetEndpointProperty - - { - - Name = "integrity", - - Value = "sha256-asdf1234" - - }, - - new StaticWebAssetEndpointProperty - - { - - Name = "label", - - Value = "candidate.js" - - } - - ]); - - endpoint.ResponseHeaders.Should().BeEquivalentTo( - - [ - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Content-Length", - - Value = "10" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "ETag", - - Value = "\"asdf1234\"" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Last-Modified", - - Value = "Thu, 15 Nov 1990 00:00:00 GMT" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Cache-Control", - - Value = "max-age=31536000, immutable" - - } - - ]); - - - - var otherEndpoint = endpoints[1]; - - otherEndpoint.Route.Should().Be("candidate.js"); - - otherEndpoint.AssetFile.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - otherEndpoint.ResponseHeaders.Should().BeEquivalentTo( - - [ - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Cache-Control", - - Value = "no-cache" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Content-Length", - - Value = "10" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "ETag", - - Value = "\"asdf1234\"" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Last-Modified", - - Value = "Thu, 15 Nov 1990 00:00:00 GMT" - - } - - ]); - - } - - - - [TestMethod] - - public void CanDefineFingerprintedEndpoints_WithEmbeddedFingerprint() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - - - var task = new DefineStaticWebAssetEndpoints - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate#[.{fingerprint=yolo}]?.js", - - "All", - - "All", - - fingerprint: "1234asdf", - - integrity: "asdf1234", - - lastWriteTime : lastWrite)], - - ExistingEndpoints = [], - - ContentTypeMappings = [CreateContentMapping("**/*.js", "text/javascript")], - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); - - endpoints.Length.Should().Be(2); - - var endpoint = endpoints[1]; - - - - endpoint.Route.Should().Be("candidate.yolo.js"); - - endpoint.AssetFile.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - endpoint.EndpointProperties.Should().BeEquivalentTo([ - - new StaticWebAssetEndpointProperty - - { - - Name = "fingerprint", - - Value = "yolo" - - }, - - new StaticWebAssetEndpointProperty - - { - - Name = "integrity", - - Value = "sha256-asdf1234" - - }, - - new StaticWebAssetEndpointProperty - - { - - Name = "label", - - Value = "candidate.js" - - } - - ]); - - endpoint.ResponseHeaders.Should().BeEquivalentTo( - - [ - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Content-Length", - - Value = "10" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "ETag", - - Value = "\"asdf1234\"" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Last-Modified", - - Value = "Thu, 15 Nov 1990 00:00:00 GMT" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Cache-Control", - - Value = "max-age=31536000, immutable" - - } - - ]); - - - - var otherEndpoint = endpoints[0]; - - otherEndpoint.Route.Should().Be("candidate.js"); - - otherEndpoint.AssetFile.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - otherEndpoint.ResponseHeaders.Should().BeEquivalentTo( - - [ - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Cache-Control", - - Value = "no-cache" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Content-Length", - - Value = "10" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "ETag", - - Value = "\"asdf1234\"" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Last-Modified", - - Value = "Thu, 15 Nov 1990 00:00:00 GMT" - - } - - ]); - - } - - - - [TestMethod] - - public void DoesNotDefineNewEndpointsWhenAnExistingEndpointAlreadyExists() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - var headers = new StaticWebAssetEndpointResponseHeader[] - - { - - new() { - - Name = "Content-Length", - - Value = "10" - - }, - - new() { - - Name = "Content-Type", - - Value = "text/javascript" - - }, - - new() { - - Name = "ETag", - - Value = "integrity" - - }, - - new() { - - Name = "Last-Modified", - - Value = "Thu, 15 Nov 1990 00:00:00 GMT" - - } - - }; - - - - var task = new DefineStaticWebAssetEndpoints - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - lastWriteTime : lastWrite)], - - ExistingEndpoints = [ - - CreateCandidateEndpoint( - - "candidate.js", - - Path.GetFullPath(Path.Combine("wwwroot", "candidate.js")), - - headers)], - - ContentTypeMappings = [CreateContentMapping("**/*.js", "text/javascript")], - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); - - endpoints.Should().BeEmpty(); - - } - - - - [TestMethod] - - public void ResolvesContentType_ForCompressedAssets() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - - - + var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); var task = new DefineStaticWebAssetEndpoints - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [ - - new TaskItem( - - Path.Combine(AppContext.BaseDirectory, "Client", "obj", "Debug", "net6.0", "compressed", "rdfmaxp4ta-43emfwee4b.gz"), - - new Dictionary - - { - - ["RelativePath"] = "_framework/dotnet.timezones.blat.gz", - - ["BasePath"] = "/", - - ["AssetMode"] = "All", - - ["AssetKind"] = "Build", - - ["SourceId"] = "BlazorWasmHosted60.Client", - - ["CopyToOutputDirectory"] = "PreserveNewest", - - ["Fingerprint"] = "3ji2l2o1xa", - - ["RelatedAsset"] = Path.Combine(AppContext.BaseDirectory, "Client", "bin", "Debug", "net6.0", "wwwroot", "_framework", "dotnet.timezones.blat"), - - ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "Client", "obj", "Debug", "net6.0", "compressed"), - - ["SourceType"] = "Computed", - - ["Integrity"] = "TwfyUDDMyF5dWUB2oRhrZaTk8sEa9o8ezAlKdxypsX4=", - - ["AssetRole"] = "Alternative", - - ["AssetTraitValue"] = "gzip", - - ["AssetTraitName"] = "Content-Encoding", - - ["OriginalItemSpec"] = Path.Combine("D:", "work", "dotnet-sdk", "artifacts", "tmp", "Release", "testing", "Publish60Host---0200F604", "Client", "bin", "Debug", "net6.0", "wwwroot", "_framework", "dotnet.timezones.blat"), - - ["CopyToPublishDirectory"] = "Never", - - ["FileLength"] = "10", - - ["LastWriteTime"] = lastWrite.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) - - }) - - ], - - ExistingEndpoints = [], - - ContentTypeMappings = [], - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); - - endpoints.Length.Should().Be(1); - - var endpoint = endpoints[0]; - - endpoint.ResponseHeaders.Should().ContainSingle(h => h.Name == "Content-Type" && h.Value == "application/x-gzip"); - - } - - - - [TestMethod] - - public void ResolvesContentType_ForFingerprintedAssets() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - - - var task = new DefineStaticWebAssetEndpoints - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [ - - new TaskItem( - - Path.Combine(AppContext.BaseDirectory, "Client", "obj", "Debug", "net6.0", "compressed", "rdfmaxp4ta-43emfwee4b.gz"), - - new Dictionary - - { - - ["RelativePath"] = "RazorPackageLibraryDirectDependency.iiugt355ct.bundle.scp.css.gz", - - ["BasePath"] = "_content/RazorPackageLibraryDirectDependency", - - ["AssetMode"] = "Reference", - - ["AssetKind"] = "All", - - ["SourceId"] = "RazorPackageLibraryDirectDependency", - - ["CopyToOutputDirectory"] = "Never", - - ["Fingerprint"] = "olx7vzw7zz", - - ["RelatedAsset"] = Path.Combine(AppContext.BaseDirectory, "Client", "obj", "Debug", "net6.0", "compressed", "RazorPackageLibraryDirectDependency.iiugt355ct.bundle.scp.css"), - - ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "Client", "obj", "Debug", "net6.0", "compressed"), - - ["SourceType"] = "Package", - - ["Integrity"] = "JK/W3g5zqZGxAM7zbv/pJ3ngpJheT01SXQ+NofKgQcc=", - - ["AssetRole"] = "Alternative", - - ["AssetTraitValue"] = "gzip", - - ["AssetTraitName"] = "Content-Encoding", - - ["OriginalItemSpec"] = Path.Combine(AppContext.BaseDirectory, "Client", "obj", "Debug", "net6.0", "compressed", "RazorPackageLibraryDirectDependency.iiugt355ct.bundle.scp.css"), - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["FileLength"] = "10", - - ["LastWriteTime"] = lastWrite.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) - - }) - - ], - - ExistingEndpoints = [], - - ContentTypeMappings = [], - - }; - - - - // Act - - var result = task.Execute(); - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); - - endpoints.Length.Should().Be(1); - - var endpoint = endpoints[0]; - - endpoint.ResponseHeaders.Should().ContainSingle(h => h.Name == "Content-Type" && h.Value == "text/css"); - - } - - - - [TestMethod] - - public void Produces_TheExpectedEndpoint_ForExternalAssets() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - - - var assetIdentity = Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"); - - var task = new DefineStaticWebAssetEndpoints - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [ - - new TaskItem( - - assetIdentity, - - new Dictionary - - { - - ["RelativePath"] = "assets/index-#[{fingerprint}].css", - - ["BasePath"] = "", - - ["AssetMode"] = "All", - - ["AssetKind"] = "Publish", - - ["SourceId"] = "MyProject", - - ["CopyToOutputDirectory"] = "PreserveNewest", - - ["RelatedAsset"] = "", - - ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "dist"), - - ["SourceType"] = "Discovered", - - ["AssetRole"] = "Primary", - - ["AssetTraitValue"] = "", - - ["AssetTraitName"] = "", - - ["Integrity"] = "asdf1234", - - ["Fingerprint"] = "C5tBAdQX", - - ["OriginalItemSpec"] = assetIdentity, - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["FileLength"] = "10", - - ["LastWriteTime"] = lastWrite.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture) - - }), - - ], - - ExistingEndpoints = [], - - ContentTypeMappings = [CreateContentMapping("**/*.css", "text/css")], - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); - - endpoints.Length.Should().Be(1); - - var endpoint = endpoints[0]; - - - - endpoint.Route.Should().Be("assets/index-C5tBAdQX.css"); - - endpoint.AssetFile.Should().Be(assetIdentity); - - endpoint.EndpointProperties.Should().BeEquivalentTo([ - - new StaticWebAssetEndpointProperty - - { - - Name = "fingerprint", - - Value = "C5tBAdQX" - - }, - - new StaticWebAssetEndpointProperty - - { - - Name = "integrity", - - Value = "sha256-asdf1234" - - }, - - new StaticWebAssetEndpointProperty - - { - - Name = "label", - - Value = "assets/index-.css" - - } - - ]); - - endpoint.ResponseHeaders.Should().BeEquivalentTo( - - [ - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Content-Length", - - Value = "10" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Content-Type", - - Value = "text/css" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "ETag", - - Value = "\"asdf1234\"" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Last-Modified", - - Value = "Thu, 15 Nov 1990 00:00:00 GMT" - - }, - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Cache-Control", - - Value = "max-age=31536000, immutable" - - } - - ]); - - } - - - - private static ITaskItem CreateCandidate( - - string itemSpec, - - string sourceId, - - string sourceType, - - string relativePath, - - string assetKind, - - string assetMode, - - string fingerprint = null, - - string integrity = null, - - long fileLength = 10, - - DateTimeOffset? lastWriteTime = null) - - { - - lastWriteTime ??= DateTimeOffset.UtcNow; - - var result = new StaticWebAsset() - - { - - Identity = Path.GetFullPath(itemSpec), - - SourceId = sourceId, - - SourceType = sourceType, - - ContentRoot = Directory.GetCurrentDirectory(), - - BasePath = "base", - - RelativePath = relativePath, - - AssetKind = assetKind, - - AssetMode = assetMode, - - AssetRole = "Primary", - - RelatedAsset = "", - - AssetTraitName = "", - - AssetTraitValue = "", - - CopyToOutputDirectory = "", - - CopyToPublishDirectory = "", - - OriginalItemSpec = itemSpec, - - // Add these to avoid accessing the disk to compute them - - Integrity = integrity ?? "integrity", - - Fingerprint = fingerprint ?? "fingerprint", - - FileLength = fileLength, - - LastWriteTime = lastWriteTime.Value, - - }; - - - - result.ApplyDefaults(); - - result.Normalize(); - - - - return result.ToTaskItem(); - - } - - - - private static TaskItem CreateContentMapping(string pattern, string contentType) - - { - - return new TaskItem(contentType, new Dictionary - - { - - { "Pattern", pattern }, - - { "Priority", "0" } - - }); - - } - - - - private static ITaskItem CreateCandidateEndpoint( - - string route, - - string assetFile, - - StaticWebAssetEndpointResponseHeader[] responseHeaders = null, - - StaticWebAssetEndpointSelector[] responseSelector = null, - - StaticWebAssetEndpointProperty[] properties = null) - - { - - return new StaticWebAssetEndpoint - - { - - Route = route, - - AssetFile = Path.GetFullPath(assetFile), - - ResponseHeaders = responseHeaders ?? [], - - EndpointProperties = properties ?? [], - - Selectors = responseSelector ?? [] - - }.ToTaskItem(); - - } - - - - private static TaskItem CreateAdditionalEndpointDefinition(string name, string pattern, string replacement, string order = "") - - { - - return new TaskItem(name, new Dictionary - - { - - { "Pattern", pattern }, - - { "Replacement", replacement }, - - { "Order", order } - - }); - - } - - - - [TestMethod] - - [DataRow("index.html", "index.html", "/")] - - [DataRow("admin/index.html", "admin/index.html", "admin")] - - public void AdditionalEndpointDefinitions_DefaultDocument_CreatesEndpointWithCapturedStem( - - string relativeSubPath, string expectedOriginalRoute, string expectedAdditionalRoute) - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - var physicalPath = Path.Combine(new[] { "wwwroot" }.Concat(relativeSubPath.Split('/')).ToArray()); - - - - var task = new DefineStaticWebAssetEndpoints - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [CreateCandidate( - - physicalPath, - - "MyPackage", - - "Discovered", - - relativeSubPath, - - "All", - - "All", - - fileLength: 100, - - lastWriteTime: lastWrite)], - - ExistingEndpoints = [], - - ContentTypeMappings = [CreateContentMapping("**/*.html", "text/html")], - - AdditionalEndpointDefinitions = [ - - CreateAdditionalEndpointDefinition("DefaultDocument", "**/index.html", "") - - ], - - }; - - - - var result = task.Execute(); - - - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); - - // Should have original endpoint + additional endpoint - - endpoints.Length.Should().Be(2); - - - - var original = endpoints.First(e => e.Route == expectedOriginalRoute); - - original.Should().NotBeNull(); - - original.Order.Should().BeNullOrEmpty(); - - - - var additional = endpoints.First(e => e.Route != expectedOriginalRoute); - - additional.Route.Should().Be(expectedAdditionalRoute); - - additional.AssetFile.Should().Be(original.AssetFile); - - additional.ResponseHeaders.Should().BeEquivalentTo(original.ResponseHeaders); - - } - - - - [TestMethod] - - public void AdditionalEndpointDefinitions_SpaFallback_CreatesEndpointWithFallbackRoute() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - - - var task = new DefineStaticWebAssetEndpoints - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [CreateCandidate( - - Path.Combine("wwwroot", "index.html"), - - "MyPackage", - - "Discovered", - - "index.html", - - "All", - - "All", - - fileLength: 100, - - lastWriteTime: lastWrite)], - - ExistingEndpoints = [], - - ContentTypeMappings = [CreateContentMapping("**/*.html", "text/html")], - - AdditionalEndpointDefinitions = [ - - CreateAdditionalEndpointDefinition("SpaFallback", "index.html", "{**fallback:nonfile}", "2147483647") - - ], - - }; + var result = task.Execute(); - - - - var result = task.Execute(); - - - - - - result.Should().Be(true); - - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); - - - endpoints.Length.Should().Be(2); - - - - + result.Should().Be(true); + var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); + endpoints.Length.Should().Be(2); var original = endpoints.First(e => e.Route == "index.html"); - - original.Should().NotBeNull(); - - original.Order.Should().BeNullOrEmpty(); - - - - var fallback = endpoints.First(e => e.Route != "index.html"); - - fallback.Route.Should().Be("{**fallback:nonfile}"); - - fallback.AssetFile.Should().Be(original.AssetFile); - - fallback.Order.Should().Be("2147483647"); - - fallback.ResponseHeaders.Should().BeEquivalentTo(original.ResponseHeaders); - - } - - - - [TestMethod] - - public void AdditionalEndpointDefinitions_DoesNotMatchNonMatchingRoutes() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - - - var task = new DefineStaticWebAssetEndpoints - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [CreateCandidate( - - Path.Combine("wwwroot", "app.js"), - - "MyPackage", - - "Discovered", - - "app.js", - - "All", - - "All", - - fileLength: 50, - - lastWriteTime: lastWrite)], - - ExistingEndpoints = [], - - ContentTypeMappings = [CreateContentMapping("**/*.js", "text/javascript")], - - AdditionalEndpointDefinitions = [ - - CreateAdditionalEndpointDefinition("DefaultDocument", "**/index.html", ""), - - CreateAdditionalEndpointDefinition("SpaFallback", "index.html", "{**fallback:nonfile}", "2147483647") - - ], - - }; - - - - var result = task.Execute(); - - - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); - - // Only the original endpoint, no additional ones - - endpoints.Should().ContainSingle(); - - endpoints[0].Route.Should().Be("app.js"); - - } - - - - [TestMethod] - - public void AdditionalEndpointDefinitions_BothRules_CreateMultipleAdditionalEndpoints() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - - - var task = new DefineStaticWebAssetEndpoints - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [CreateCandidate( - - Path.Combine("wwwroot", "index.html"), - - "MyPackage", - - "Discovered", - - "index.html", - - "All", - - "All", - - fileLength: 100, - - lastWriteTime: lastWrite)], - - ExistingEndpoints = [], - - ContentTypeMappings = [CreateContentMapping("**/*.html", "text/html")], - - AdditionalEndpointDefinitions = [ - - CreateAdditionalEndpointDefinition("DefaultDocument", "**/index.html", ""), - - CreateAdditionalEndpointDefinition("SpaFallback", "index.html", "{**fallback:nonfile}", "2147483647") - - ], - - }; - - - - var result = task.Execute(); - - - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); - - // Original + DefaultDocument + SpaFallback - - endpoints.Length.Should().Be(3); - - - - endpoints.Should().Contain(e => e.Route == "index.html"); - - endpoints.Should().Contain(e => e.Route == "/"); - - endpoints.Should().Contain(e => e.Route == "{**fallback:nonfile}" && e.Order == "2147483647"); - - } - - - - [TestMethod] - - public void AdditionalEndpointDefinitions_EmptyArray_NoAdditionalEndpoints() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var lastWrite = new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc); - - - - var task = new DefineStaticWebAssetEndpoints - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [CreateCandidate( - - Path.Combine("wwwroot", "index.html"), - - "MyPackage", - - "Discovered", - - "index.html", - - "All", - - "All", - - fileLength: 100, - - lastWriteTime: lastWrite)], - - ExistingEndpoints = [], - - ContentTypeMappings = [CreateContentMapping("**/*.html", "text/html")], - - AdditionalEndpointDefinitions = [], - - }; - - - - var result = task.Execute(); - - - - result.Should().Be(true); - - var endpoints = StaticWebAssetEndpoint.FromItemGroup(task.Endpoints); - - endpoints.Should().ContainSingle(); - - endpoints[0].Route.Should().Be("index.html"); - - } [TestMethod] @@ -2735,5 +995,3 @@ private static ITaskItem CreateExistingEndpoint(string route, string assetFile) }.ToTaskItem(); } } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverDefaultScopedCssItemsTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverDefaultScopedCssItemsTests.cs index 3c46ee0e1bfe..58e55d2e35f6 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverDefaultScopedCssItemsTests.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverDefaultScopedCssItemsTests.cs @@ -6,300 +6,103 @@ using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.Build.Utilities; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - namespace Microsoft.NET.Sdk.Razor.Test - - { - - [TestClass] - public class DiscoverDefaultScopedCssItemsTests - - { - - [TestMethod] - - public void DiscoversScopedCssFiles_BasedOnTheirExtension() - - { - - // Arrange - - var taskInstance = new DiscoverDefaultScopedCssItems() - - { - - Content = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.razor.css"), - - new TaskItem("TestFiles/Pages/Index.razor.css"), - - new TaskItem("TestFiles/Pages/Profile.razor.css"), - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.DiscoveredScopedCssInputs.Should().HaveCount(3); - - } - - - - [TestMethod] - - public void DoesNotDiscoversScopedCssFilesForViews_IfFeatureIsUnsupported() - - { - - // Arrange - - var taskInstance = new DiscoverDefaultScopedCssItems() - - { - - Content = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.cshtml.css"), - - new TaskItem("TestFiles/Pages/Index.cshtml.css"), - - new TaskItem("TestFiles/Pages/Profile.cshtml.css"), - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.DiscoveredScopedCssInputs.Should().BeEmpty(); - - } - - - - [TestMethod] - - public void DiscoversScopedCssFilesForViews_BasedOnTheirExtension() - - { - - // Arrange - - var taskInstance = new DiscoverDefaultScopedCssItems() - - { - - SupportsScopedCshtmlCss = true, - - Content = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.cshtml.css"), - - new TaskItem("TestFiles/Pages/Index.cshtml.css"), - - new TaskItem("TestFiles/Pages/Profile.cshtml.css"), - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.DiscoveredScopedCssInputs.Should().HaveCount(3); - - } - - - - [TestMethod] - - public void DiscoversScopedCssFilesForViews_SkipsFilesWithScopedAttributeWithAFalseValue() - - { - - // Arrange - - var taskInstance = new DiscoverDefaultScopedCssItems() - - { - - SupportsScopedCshtmlCss = true, - - Content = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.cshtml.css"), - - new TaskItem("TestFiles/Pages/Index.cshtml.css"), - - new TaskItem("TestFiles/Pages/Profile.cshtml.css", new Dictionary{ ["Scoped"] = "false" }), - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.DiscoveredScopedCssInputs.Should().HaveCount(2); - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsMultiThreadingTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsMultiThreadingTest.cs index bd49b2f83c02..a1f5fe52ca9a 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsMultiThreadingTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsMultiThreadingTest.cs @@ -2,311 +2,111 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - - - [DoNotParallelize] [TestClass] public class DiscoverPrecompressedAssetsMultiThreadingTest - - { - - [TestMethod] - - public void ResolvesContentRootRelativeToTaskEnvironmentProjectDirectory_NotProcessCurrentDirectory() - - { - - var testRoot = Path.Combine(AppContext.BaseDirectory, nameof(DiscoverPrecompressedAssetsMultiThreadingTest), Guid.NewGuid().ToString("N")); - - var projectDir = Path.Combine(testRoot, "project"); - - var spawnDir = Path.Combine(testRoot, "decoy", "spawn"); - - Directory.CreateDirectory(projectDir); - - Directory.CreateDirectory(spawnDir); - - - - const string relativeContentRoot = "wwwroot"; - - var expectedContentRoot = Path.GetFullPath(Path.Combine(projectDir, relativeContentRoot)) + Path.DirectorySeparatorChar; - - var decoyContentRoot = Path.GetFullPath(Path.Combine(spawnDir, relativeContentRoot)) + Path.DirectorySeparatorChar; - - expectedContentRoot.Should().NotBe(decoyContentRoot, - - "the test setup must place project and decoy in different parents so the migration is actually exercised"); - - - - var baseIdentity = Path.Combine(projectDir, "wwwroot", "js", "site.js"); - - var compressedIdentity = baseIdentity + ".gz"; - - - - var originalCurrentDirectory = Directory.GetCurrentDirectory(); - - try - - { - - Directory.SetCurrentDirectory(spawnDir); - - - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new DiscoverPrecompressedAssets - - { - - BuildEngine = buildEngine.Object, - - TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir), - - CandidateAssets = - - [ - - CreateCandidate(baseIdentity, relativePath: "js/site.js", relativeContentRoot), - - CreateCandidate(compressedIdentity, relativePath: "js/site.js.gz", relativeContentRoot), - - ], - - }; - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - errorMessages.Should().BeEmpty(); - - task.DiscoveredCompressedAssets.Should().ContainSingle(); - - - - var discovered = task.DiscoveredCompressedAssets[0]; - - discovered.GetMetadata("ContentRoot").Should().Be(expectedContentRoot, - - "ContentRoot must be absolutized against TaskEnvironment.ProjectDirectory, not the process CWD"); - - discovered.GetMetadata("ContentRoot").Should().NotBe(decoyContentRoot); - - discovered.GetMetadata("RelatedAsset").Should().Be(baseIdentity); - - } - - finally - - { - - Directory.SetCurrentDirectory(originalCurrentDirectory); - - if (Directory.Exists(testRoot)) - - { - - Directory.Delete(testRoot, recursive: true); - - } - - } - - } - - - - private static ITaskItem CreateCandidate(string identity, string relativePath, string contentRoot) - - { - - var asset = new StaticWebAsset - - { - - Identity = identity, - - RelativePath = relativePath, - - BasePath = "_content/Test", - - AssetMode = StaticWebAsset.AssetModes.All, - - AssetKind = StaticWebAsset.AssetKinds.All, - - AssetMergeSource = string.Empty, - - SourceId = "Test", - - CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, - - Fingerprint = "fingerprint", - - RelatedAsset = string.Empty, - - ContentRoot = contentRoot, - - SourceType = StaticWebAsset.SourceTypes.Discovered, - - Integrity = "integrity", - - AssetRole = StaticWebAsset.AssetRoles.Primary, - - AssetMergeBehavior = string.Empty, - - AssetTraitValue = string.Empty, - - AssetTraitName = string.Empty, - - OriginalItemSpec = identity, - - CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest, - - FileLength = 10, - - LastWriteTime = DateTime.UtcNow, - - }; - - return asset.ToTaskItem(); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs index 4b69e72ec96e..cbf54cc01cf2 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverPrecompressedAssetsTest.cs @@ -2,346 +2,122 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - - - [TestClass] public class DiscoverPrecompressedAssetsTest - - { - - public string ItemSpec { get; } - - - - public string OriginalItemSpec { get; } - - - - public string OutputBasePath { get; } - - - - public DiscoverPrecompressedAssetsTest() - - { - - OutputBasePath = Path.Combine(SdkTestContext.Current.TestExecutionDirectory, nameof(ResolveCompressedAssetsTest)); - - ItemSpec = Path.Combine(OutputBasePath, Guid.NewGuid().ToString("N") + ".tmp"); - - OriginalItemSpec = Path.Combine(OutputBasePath, Guid.NewGuid().ToString("N") + ".tmp"); - - } - - - - [TestMethod] - - public void DiscoversPrecompressedAssetsCorrectly() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var uncompressedCandidate = new StaticWebAsset - - { - - Identity = Path.Combine(Environment.CurrentDirectory, "wwwroot", "js", "site.js"), - - RelativePath = "js/site#[.{fingerprint}]?.js", - - BasePath = "_content/Test", - - AssetMode = StaticWebAsset.AssetModes.All, - - AssetKind = StaticWebAsset.AssetKinds.All, - - AssetMergeSource = string.Empty, - - SourceId = "Test", - - CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, - - Fingerprint = "uncompressed", - - RelatedAsset = string.Empty, - - ContentRoot = Path.Combine(Environment.CurrentDirectory,"wwwroot"), - - SourceType = StaticWebAsset.SourceTypes.Discovered, - - Integrity = "uncompressed-integrity", - - AssetRole = StaticWebAsset.AssetRoles.Primary, - - AssetMergeBehavior = string.Empty, - - AssetTraitValue = string.Empty, - - AssetTraitName = string.Empty, - - OriginalItemSpec = Path.Combine("wwwroot", "js", "site.js"), - - CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest, - - FileLength = 10, - - LastWriteTime = DateTime.UtcNow - - }; - - - - var compressedCandidate = new StaticWebAsset - - { - - Identity = Path.Combine(Environment.CurrentDirectory, "wwwroot", "js", "site.js.gz"), - - RelativePath = "js/site.js#[.{fingerprint}]?.gz", - - BasePath = "_content/Test", - - AssetMode = StaticWebAsset.AssetModes.All, - - AssetKind = StaticWebAsset.AssetKinds.All, - - AssetMergeSource = string.Empty, - - SourceId = "Test", - - CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, - - Fingerprint = "compressed", - - RelatedAsset = string.Empty, - - ContentRoot = Path.Combine(Environment.CurrentDirectory, "wwwroot"), - - SourceType = StaticWebAsset.SourceTypes.Discovered, - - Integrity = "compressed-integrity", - - AssetRole = StaticWebAsset.AssetRoles.Primary, - - AssetMergeBehavior = string.Empty, - - AssetTraitValue = string.Empty, - - AssetTraitName = string.Empty, - - OriginalItemSpec = Path.Combine("wwwroot", "js", "site.js.gz"), - - CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest, - - FileLength = 10, - - LastWriteTime = DateTime.UtcNow - - }; - - - - var task = new DiscoverPrecompressedAssets - - { - - CandidateAssets = [uncompressedCandidate.ToTaskItem(), compressedCandidate.ToTaskItem()], - - BuildEngine = buildEngine.Object - - }; - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - task.DiscoveredCompressedAssets.Should().ContainSingle(); - - var asset = task.DiscoveredCompressedAssets[0]; - - asset.ItemSpec.Should().Be(compressedCandidate.Identity); - - asset.GetMetadata("RelatedAsset").Should().Be(uncompressedCandidate.Identity); - - asset.GetMetadata("OriginalItemSpec").Should().Be(uncompressedCandidate.Identity); - - asset.GetMetadata("RelativePath").Should().Be("js/site#[.{fingerprint=uncompressed}]?.js.gz"); - - asset.GetMetadata("AssetRole").Should().Be("Alternative"); - - asset.GetMetadata("AssetTraitName").Should().Be("Content-Encoding"); - - asset.GetMetadata("AssetTraitValue").Should().Be("gzip"); - - asset.GetMetadata("Fingerprint").Should().Be("compressed"); - - asset.GetMetadata("Integrity").Should().Be("compressed-integrity"); - - asset.GetMetadata("CopyToPublishDirectory").Should().Be("PreserveNewest"); - - asset.GetMetadata("CopyToOutputDirectory").Should().Be("Never"); - - asset.GetMetadata("AssetMergeSource").Should().Be(string.Empty); - - asset.GetMetadata("AssetMergeBehavior").Should().Be(string.Empty); - - asset.GetMetadata("AssetKind").Should().Be("All"); - - asset.GetMetadata("AssetMode").Should().Be("All"); - - asset.GetMetadata("SourceId").Should().Be("Test"); - - asset.GetMetadata("SourceType").Should().Be("Discovered"); - - asset.GetMetadata("ContentRoot").Should().Be(Path.Combine(Environment.CurrentDirectory, $"wwwroot{Path.DirectorySeparatorChar}")); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverStaticWebAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverStaticWebAssetsTest.cs index 7682e626f8db..d408060146a3 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverStaticWebAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/DiscoverStaticWebAssetsTest.cs @@ -2,2777 +2,932 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class DiscoverStaticWebAssetsTest - - { - - private readonly Func _testResolveFileDetails = - - (string identity, string originalItemSpec) => (null, 10, new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero)); - - - - [TestMethod] - - public void DiscoversMatchingAssetsBasedOnPattern() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - TestResolveFileDetails = _testResolveFileDetails, - - CandidateAssets = - - [ - - CreateCandidate(Path.Combine("wwwroot", "candidate.js")) - - ], - - RelativePathPattern = "wwwroot\\**", - - SourceType = "Discovered", - - SourceId = "MyProject", - - ContentRoot = "wwwroot", - - BasePath = "_content/Path" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); - - task.Assets.Length.Should().Be(1); - - var asset = task.Assets[0]; - - asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().Be("MyProject"); - - asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().Be("Discovered"); - - asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar); - - asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Path"); - - asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("candidate.js"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().Be("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().Be("Primary"); - - asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); - - asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", "candidate.js")); - - } - - - - [TestMethod] - - [DataRow("index.js", "index#[.{fingerprint}]?.js", "")] - - [DataRow("css/site.css", "css/site#[.{fingerprint}]!.css", "#[.{fingerprint}]!")] - - public void FingerprintsContentWhenEnabled(string file, string expectedRelativePath, string expression) - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - TestResolveFileDetails = _testResolveFileDetails, - - CandidateAssets = - - [ - - CreateCandidate(Path.Combine("wwwroot", file)) - - ], - - RelativePathPattern = "wwwroot\\**", - - FingerprintCandidates = true, - - SourceType = "Discovered", - - SourceId = "MyProject", - - ContentRoot = "wwwroot", - - BasePath = "_content/Path" - - }; - - if (!string.IsNullOrEmpty(expression)) - - { - - task.FingerprintPatterns = [new TaskItem("CssFile", new Dictionary { ["Pattern"] = "*.css", ["Expression"] = expression })]; - - } - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); - - task.Assets.Length.Should().Be(1); - - var asset = task.Assets[0]; - - asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", file))); - - asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().Be("MyProject"); - - asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().Be("Discovered"); - - asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar); - - asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Path"); - - asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be(expectedRelativePath); - - asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().Be("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().Be("Primary"); - - asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); - - asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", file)); - - } - - - - [TestMethod] - - [DataRow("index.js")] - - [DataRow("css/site.js")] - - public void DoesNotFingerprintsContentWhenNotEnabled(string candidate) - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - TestResolveFileDetails = _testResolveFileDetails, - - CandidateAssets = - - [ - - CreateCandidate(Path.Combine("wwwroot", candidate.Replace('/', Path.DirectorySeparatorChar))) - - ], - - RelativePathPattern = "wwwroot\\**", - - FingerprintCandidates = false, - - SourceType = "Discovered", - - SourceId = "MyProject", - - ContentRoot = "wwwroot", - - BasePath = "_content/Path" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); - - task.Assets.Length.Should().Be(1); - - var asset = task.Assets[0]; - - asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", candidate))); - - asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().Be("MyProject"); - - asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().Be("Discovered"); - - asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar); - - asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Path"); - - asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be(candidate); - - asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().Be("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().Be("Primary"); - - asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); - - asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", Path.Combine(candidate.Split('/')))); - - } - - - - [TestMethod] - - [DataRow("candidate.lib.module.js", "candidate#[.{fingerprint}]?.lib.module.js", "")] - - [DataRow("library.candidate.lib.module.js", "library.candidate#[.{fingerprint}]!.lib.module.js", "#[.{fingerprint}]!")] - - public void FingerprintsContentUsingPatternsWhenMoreThanOneExtension(string fileName, string expectedRelativePath, string expression) - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - TestResolveFileDetails = _testResolveFileDetails, - - CandidateAssets = - - [ - - CreateCandidate(Path.Combine("wwwroot", fileName)) - - ], - - FingerprintPatterns = [new TaskItem("JsModule", new Dictionary { ["Pattern"] = "*.lib.module.js", ["Expression"] = expression })], - - FingerprintCandidates = true, - - RelativePathPattern = "wwwroot\\**", - - SourceType = "Discovered", - - SourceId = "MyProject", - - ContentRoot = "wwwroot", - - BasePath = "_content/Path" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); - - task.Assets.Length.Should().Be(1); - - var asset = task.Assets[0]; - - asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", fileName))); - - asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().Be("MyProject"); - - asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().Be("Discovered"); - - asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar); - - asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Path"); - - asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be(expectedRelativePath); - - asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().Be("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().Be("Primary"); - - asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); - - asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", fileName)); - - } - - - - [TestMethod] - - [TestCategory("FingerprintIdentity")] - - public void ComputesIdentity_UsingFingerprintPattern_ForComputedAssets_WhenIdentityNeedsComputation() - - { - - // Arrange: simulate a packaged asset (outside content root) with a RelativePath inside the app - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - // Create a physical file to allow fingerprint computation (tests override ResolveFileDetails returning null file otherwise) - - var tempRoot = Path.Combine(Path.GetTempPath(), "swafp_identity_test"); - - var nugetPackagePath = Path.Combine(tempRoot, "microsoft.aspnetcore.components.webassembly", "10.0.0-rc.1.25451.107", "build", "net10.0"); - - Directory.CreateDirectory(nugetPackagePath); - - var assetFileName = "blazor.webassembly.js"; - - var assetFullPath = Path.Combine(nugetPackagePath, assetFileName); - - File.WriteAllText(assetFullPath, "console.log('test');"); - - // Relative path provided by the item (pre-fingerprinting) - - var relativePath = Path.Combine("_framework", assetFileName).Replace('\\', '/'); - - var contentRoot = Path.Combine("bin", "Release", "net10.0", "wwwroot"); - - - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - // Use default file resolution so the file we created is used for hashing. - - TestResolveFileDetails = null, - - CandidateAssets = - - [ - - new TaskItem(assetFullPath, new Dictionary - - { - - ["RelativePath"] = relativePath - - }) - - ], - - // No RelativePathPattern, we trigger the branch that synthesizes identity under content root. - - FingerprintPatterns = [ new TaskItem("Js", new Dictionary{{"Pattern","*.js"},{"Expression","#[.{fingerprint}]!"}})], - - FingerprintCandidates = true, - - SourceType = "Computed", - - SourceId = "Client", - - ContentRoot = contentRoot, - - BasePath = "/", - - AssetKind = StaticWebAsset.AssetKinds.All, - - AssetTraitName = "WasmResource", - - AssetTraitValue = "boot" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue($"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); - - task.Assets.Length.Should().Be(1); - - var asset = task.Assets[0]; - - - - // RelativePath should still contain the hard fingerprint pattern placeholder (not expanded yet) - - asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("_framework/blazor.webassembly#[.{fingerprint}]!.js"); - - - - // Identity must contain the ACTUAL fingerprint value in the file name (placeholder expanded) - - var actualFingerprint = asset.GetMetadata(nameof(StaticWebAsset.Fingerprint)); - - actualFingerprint.Should().NotBeNullOrEmpty(); - - var expectedIdentity = Path.GetFullPath(Path.Combine(contentRoot, "_framework", $"blazor.webassembly.{actualFingerprint}.js")); - - asset.ItemSpec.Should().Be(expectedIdentity); - - } - - - - [TestMethod] - - public void RespectsItemRelativePathWhenExplicitlySpecified() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - TestResolveFileDetails = _testResolveFileDetails, - - CandidateAssets = - - [ - - CreateCandidate(Path.Combine("wwwroot", "candidate.js"), relativePath: "subdir/candidate.js") - - ], - - RelativePathPattern = "wwwroot\\**", - - SourceType = "Discovered", - - SourceId = "MyProject", - - ContentRoot = "wwwroot", - - BasePath = "_content/Path" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); - - task.Assets.Length.Should().Be(1); - - var asset = task.Assets[0]; - - asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().Be("MyProject"); - - asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().Be("Discovered"); - - asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar); - - asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Path"); - - asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("subdir/candidate.js"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().Be("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().Be("Primary"); - - asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); - - asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", "candidate.js")); - - } - - - - [TestMethod] - - public void UsesTargetPathWhenFound() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - TestResolveFileDetails = _testResolveFileDetails, - - CandidateAssets = - - [ - - CreateCandidate(Path.Combine("wwwroot", "candidate.js"), targetPath: Path.Combine("wwwroot", "subdir", "candidate.publish.js")) - - ], - - RelativePathPattern = "wwwroot\\**", - - SourceType = "Discovered", - - SourceId = "MyProject", - - ContentRoot = "wwwroot", - - BasePath = "_content/Path" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); - - task.Assets.Length.Should().Be(1); - - var asset = task.Assets[0]; - - asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().Be("MyProject"); - - asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().Be("Discovered"); - - asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar); - - asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Path"); - - asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("subdir/candidate.publish.js"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().Be("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().Be("Primary"); - - asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); - - asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", "candidate.js")); - - } - - - - [TestMethod] - - public void UsesLinkPathWhenFound() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - TestResolveFileDetails = _testResolveFileDetails, - - CandidateAssets = - - [ - - CreateCandidate(Path.Combine("wwwroot", "candidate.js"), link: Path.Combine("wwwroot", "subdir", "candidate.link.js")) - - ], - - RelativePathPattern = "wwwroot\\**", - - SourceType = "Discovered", - - SourceId = "MyProject", - - ContentRoot = "wwwroot", - - BasePath = "_content/Path" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); - - task.Assets.Length.Should().Be(1); - - var asset = task.Assets[0]; - - asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().Be("MyProject"); - - asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().Be("Discovered"); - - asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar); - - asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Path"); - - asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("subdir/candidate.link.js"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().Be("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().Be("Primary"); - - asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().Be(""); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); - - asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().Be(Path.Combine("wwwroot", "candidate.js")); - - } - - - - [TestMethod] - - public void AutomaticallyDetectsAssetKindWhenMultipleAssetsTargetTheSameRelativePath() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - TestResolveFileDetails = _testResolveFileDetails, - - CandidateAssets = - - [ - - CreateCandidate(Path.Combine("wwwroot", "candidate.js"), copyToPublishDirectory: "Never"), - - CreateCandidate(Path.Combine("wwwroot", "candidate.publish.js"), relativePath: "candidate.js") - - ], - - RelativePathPattern = "wwwroot\\**", - - SourceType = "Discovered", - - SourceId = "MyProject", - - ContentRoot = "wwwroot", - - BasePath = "_content/Path" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); - - task.Assets.Length.Should().Be(2); - - var buildAsset = task.Assets.Single(a => a.ItemSpec == Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - var publishAsset = task.Assets.Single(a => a.ItemSpec == Path.GetFullPath(Path.Combine("wwwroot", "candidate.publish.js"))); - - buildAsset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - buildAsset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("Build"); - - buildAsset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); - - buildAsset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("Never"); - - - - publishAsset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.publish.js"))); - - publishAsset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().Be("Publish"); - - publishAsset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().Be("Never"); - - publishAsset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().Be("PreserveNewest"); - - } - - - - [TestMethod] - - [DataRow("Never", "Never", "Build", "Never", "Never", "Build")] - - [DataRow("PreserveNewest", "PreserveNewest", "All", "PreserveNewest", "PreserveNewest", "All")] - - [DataRow("Always", "Always", "All", "Always", "Always", "All")] - - [DataRow("Never", "Always", "All", "Never", "Always", "All")] - - [DataRow("Always", "Never", "Build", "Always", "Never", "Build")] - - public void FailsDiscoveringAssetsWhenThereIsAConflict( - - string copyToOutputDirectoryFirst, - - string copyToPublishDirectoryFirst, - - string firstKind, - - string copyToOutputDirectorySecond, - - string copyToPublishDirectorySecond, - - string secondKind) - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - TestResolveFileDetails = _testResolveFileDetails, - - CandidateAssets = - - [ - - CreateCandidate( - - Path.Combine("wwwroot","candidate.js"), - - copyToOutputDirectory: copyToOutputDirectoryFirst, - - copyToPublishDirectory: copyToPublishDirectoryFirst), - - - - CreateCandidate( - - Path.Combine("wwwroot","candidate.publish.js"), - - relativePath: "candidate.js", - - copyToOutputDirectory: copyToOutputDirectorySecond, - - copyToPublishDirectory: copyToPublishDirectorySecond) - - ], - - RelativePathPattern = "wwwroot\\**", - - SourceType = "Discovered", - - SourceId = "MyProject", - - ContentRoot = "wwwroot", - - BasePath = "_content/Path" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(false); - - errorMessages.Count.Should().Be(1); - - errorMessages[0].Should().Be($@"Two assets found targeting the same path with incompatible asset kinds: - - '{Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))}' with kind '{firstKind}' - - '{Path.GetFullPath(Path.Combine("wwwroot", "candidate.publish.js"))}' with kind '{secondKind}' - - for path 'candidate.js'"); - - } - - - - [TestMethod] - - [DataRow("\\_content\\Path\\", "_content/Path")] - - [DataRow("\\_content\\Path", "_content/Path")] - - [DataRow("_content\\Path", "_content/Path")] - - [DataRow("/_content/Path/", "_content/Path")] - - [DataRow("/_content/Path", "_content/Path")] - - [DataRow("_content/Path", "_content/Path")] - - [DataRow("\\_content/Path\\", "_content/Path")] - - [DataRow("/_content\\Path/", "_content/Path")] - - [DataRow("", "/")] - - [DataRow("/", "/")] - - [DataRow("\\", "/")] - - public void NormalizesBasePath(string givenPath, string expectedPath) - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - TestResolveFileDetails = _testResolveFileDetails, - - CandidateAssets = - - [ - - CreateCandidate("wwwroot\\candidate.js") - - ], - - RelativePathPattern = "wwwroot\\**", - - SourceType = "Discovered", - - SourceId = "MyProject", - - ContentRoot = "wwwroot", - - BasePath = givenPath - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); - - task.Assets.Length.Should().Be(1); - - var asset = task.Assets[0]; - - asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be(expectedPath); - - } - - - - public static TheoryData NormalizesContentRootData - - { - - get - - { - - var currentPath = Path.GetFullPath("."); - - var result = new TheoryData - - { - - { "wwwroot", Path.GetFullPath("wwwroot") + Path.DirectorySeparatorChar }, - - { currentPath + Path.DirectorySeparatorChar + "wwwroot" + Path.DirectorySeparatorChar + "subdir", Path.GetFullPath("wwwroot/subdir") + Path.DirectorySeparatorChar }, - - { currentPath + Path.DirectorySeparatorChar + "wwwroot" + Path.DirectorySeparatorChar + "subdir" + Path.DirectorySeparatorChar, Path.GetFullPath("wwwroot/subdir") + Path.DirectorySeparatorChar }, - - { currentPath + Path.DirectorySeparatorChar + "wwwroot" + Path.DirectorySeparatorChar + "subdir" + Path.AltDirectorySeparatorChar, Path.GetFullPath("wwwroot/subdir") + Path.DirectorySeparatorChar }, - - { currentPath + Path.AltDirectorySeparatorChar + "wwwroot" + Path.AltDirectorySeparatorChar + "subdir", Path.GetFullPath("wwwroot/subdir") + Path.DirectorySeparatorChar }, - - { currentPath + Path.DirectorySeparatorChar + "wwwroot" + Path.AltDirectorySeparatorChar + "subdir", Path.GetFullPath("wwwroot/subdir") + Path.DirectorySeparatorChar }, - - { currentPath + Path.AltDirectorySeparatorChar + "wwwroot" + Path.DirectorySeparatorChar + "subdir", Path.GetFullPath("wwwroot/subdir") + Path.DirectorySeparatorChar } - - }; - - return result; - - } - - } - - - - [TestMethod] - - [DynamicData(nameof(NormalizesContentRootData))] - - public void NormalizesContentRoot(string contentRoot, string expected) - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - TestResolveFileDetails = _testResolveFileDetails, - - CandidateAssets = - - [ - - CreateCandidate("wwwroot\\candidate.js") - - ], - - RelativePathPattern = "wwwroot\\**", - - SourceType = "Discovered", - - SourceId = "MyProject", - - ContentRoot = contentRoot, - - BasePath = "base" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true, $"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); - - task.Assets.Length.Should().Be(1); - - var asset = task.Assets[0]; - - asset.ItemSpec.Should().Be(Path.GetFullPath(Path.Combine("wwwroot", "candidate.js"))); - - asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be(expected); - - } - - - - [TestMethod] - - public void DefineStaticWebAssetsCache_UpToDate() - - { - - // Arrange - - var (cache, inputHashes) = SetupCache([], []); - - // Assert - - cache.Update([], [], [], inputHashes); - - - - // Assert - - Assert.IsTrue(cache.IsUpToDate()); - - } - - - - [TestMethod] - - public void DefineStaticWebAssetsCache_UpToDate_WithAssets() - - { - - // Arrange - - var (cache, inputHashes) = SetupCache(["input1"], ["input1"]); - - - - // Act - - cache.Update([], [], [], inputHashes); - - - - // Assert - - Assert.IsTrue(cache.IsUpToDate()); - - } - - - - [TestMethod] - - [DataRow(UpdatedHash.GlobalProperties)] - - [DataRow(UpdatedHash.FingerprintPatterns)] - - [DataRow(UpdatedHash.Overrides)] - - public void DefineStaticWebAssetsCache_Recomputes_All_WhenPropertiesChange(UpdatedHash updated) - - { - - // Arrange - - var (cache, inputHashes) = SetupCache(["input1", "input2"], ["input1", "input2"]); - - - - // Act - - switch (updated) - - { - - case UpdatedHash.GlobalProperties: - - cache.Update([1], [], [], inputHashes); - - break; - - case UpdatedHash.FingerprintPatterns: - - cache.Update([], [1], [], inputHashes); - - break; - - case UpdatedHash.Overrides: - - cache.Update([], [], [1], inputHashes); - - break; - - } - - - - - Assert.IsFalse(cache.IsUpToDate()); - - - Assert.AreSame(inputHashes, cache.OutOfDateInputs()); - - + Assert.IsFalse(cache.IsUpToDate()); + Assert.AreSame(inputHashes, cache.OutOfDateInputs()); Assert.IsEmpty(cache.CachedAssets); - - Assert.IsEmpty(cache.CachedCopyCandidates); - - } - - - - [TestMethod] - - public void DefineStaticWebAssetsCache_PartialUpdate_WhenOnlySome_InputsChange() - - { - - // Arrange - - var (cache, inputHashes) = SetupCache(["input1"], ["input2"], appendCachedToInputHashes: true); - - var cachedAsset = cache.CachedAssets.Values.Single(); - - - - // Act - - cache.Update([], [], [], inputHashes); - - - - // Assert - - Assert.IsFalse(cache.IsUpToDate()); - - Assert.AreNotSame(inputHashes, cache.OutOfDateInputs()); - - var input1 = Assert.ContainsSingle(cache.OutOfDateInputs()); - - var ouput = cache.GetComputedOutputs(); - - var input2 = Assert.ContainsSingle(ouput.Assets); - - } - - - - [TestMethod] - - public void DefineStaticWebAssetsCache_PartialUpdate_NewAssetsCanBeAddedToTheCache() - - { - - // Arrange - - var (cache, inputHashes) = SetupCache(["input1"], ["input2"], appendCachedToInputHashes: true); - - cache.Update([], [], [], inputHashes); - - - - // Act - - var newAssetItem = inputHashes["input1"]; - - var newAsset = new StaticWebAsset { Identity = newAssetItem.ItemSpec }; - - cache.AppendAsset("input1", newAsset, newAssetItem); - - - - // Assert - - Assert.IsFalse(cache.IsUpToDate()); - - Assert.AreNotSame(inputHashes, cache.OutOfDateInputs()); - - var input1 = Assert.ContainsSingle(cache.OutOfDateInputs()); - - Assert.Contains("input1", cache.CachedAssets.Keys); - - - - var ouput = cache.GetComputedOutputs(); - - Assert.HasCount(2, ouput.Assets); - - Assert.AreEqual("input2", ouput.Assets[0].ItemSpec); - - Assert.AreEqual("input1", ouput.Assets[1].ItemSpec); - - } - - - - [TestMethod] - - public void DefineStaticWebAssetsCache_CanRoundtripManifest() - - { - - var manifestPath = Path.Combine(Environment.CurrentDirectory, "CanRoundtripManifest.json"); - - if (File.Exists(manifestPath)) - - { - - File.Delete(manifestPath); - - } - - try - - { - - var (cache, inputHashes) = SetupCache([], [], appendCachedToInputHashes: true, manifestPath: manifestPath); - - - - var cachedAsset = CreateCandidate(Path.Combine(Environment.CurrentDirectory, "Input2.txt"), "Input2.txt"); - - cache.InputHashes = ["input2"]; - - cache.CachedAssets["input2"] = new StaticWebAsset { Identity = cachedAsset.ItemSpec, RelativePath = "Input2.txt" }; - - inputHashes["input2"] = cachedAsset; - - - - var newAsset = CreateCandidate(Path.Combine(Environment.CurrentDirectory, "Input1.txt"), "Input1.txt"); - - inputHashes["input1"] = newAsset; - - - - cache.Update([], [], [], inputHashes); - - cache.AppendAsset("input1", new StaticWebAsset { Identity = newAsset.ItemSpec, RelativePath = "Input1.txt" }, newAsset); - - cache.WriteCacheManifest(); - - - - var otherManifest = DefineStaticWebAssets.DefineStaticWebAssetsCache.ReadOrCreateCache(CreateLogger(), manifestPath); - - otherManifest.InputHashes.Should().BeEquivalentTo(cache.InputHashes); - - Assert.HasCount(cache.CachedAssets.Count, otherManifest.CachedAssets); - - Assert.AreEqual(cache.CachedAssets["input2"].Identity, otherManifest.CachedAssets["input2"].Identity); - - Assert.AreEqual(cache.CachedAssets["input2"].RelativePath, otherManifest.CachedAssets["input2"].RelativePath); - - Assert.AreEqual(cache.CachedAssets["input1"].Identity, otherManifest.CachedAssets["input1"].Identity); - - Assert.AreEqual(cache.CachedAssets["input1"].RelativePath, otherManifest.CachedAssets["input1"].RelativePath); - - } - - finally - - { - - File.Delete(manifestPath); - - } - - } - - - - [TestMethod] - - public void ComputesRelativePath_ForDiscoveredAssetsWithFullPath() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - buildEngine.SetupGet(e => e.ProjectFileOfTaskNode) - - .Returns(Path.Combine(Environment.CurrentDirectory, "Debug", "TestProject.csproj")); - - - - var debugDir = Path.Combine(Environment.CurrentDirectory, "Debug", "wwwroot"); - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [ - - new TaskItem(Path.Combine(debugDir, "Microsoft.AspNetCore.Components.CustomElements.lib.module.js"), - - new Dictionary{ ["Integrity"] = "integrity", ["Fingerprint"] = "fingerprint"}), - - new TaskItem(Path.Combine(debugDir, "Microsoft.AspNetCore.Components.CustomElements.lib.module.js.map"), - - new Dictionary{ ["Integrity"] = "integrity", ["Fingerprint"] = "fingerprint"}) - - ], - - RelativePathPattern = "wwwroot/**", - - SourceType = "Discovered", - - SourceId = "Microsoft.AspNetCore.Components.CustomElements", - - ContentRoot = debugDir, - - BasePath = "_content/Microsoft.AspNetCore.Components.CustomElements", - - TestResolveFileDetails = _testResolveFileDetails, - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue($"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); - - task.Assets.Length.Should().Be(2); - - task.Assets[0].GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("Microsoft.AspNetCore.Components.CustomElements.lib.module.js"); - - task.Assets[0].GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Microsoft.AspNetCore.Components.CustomElements"); - - task.Assets[1].GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("Microsoft.AspNetCore.Components.CustomElements.lib.module.js.map"); - - task.Assets[1].GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Microsoft.AspNetCore.Components.CustomElements"); - - } - - - - [TestMethod] - - public void ComputesRelativePath_WorksForItemsWithRelativePaths() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - buildEngine.SetupGet(e => e.ProjectFileOfTaskNode) - - .Returns(Path.Combine(Environment.CurrentDirectory, "Debug", "TestProject.csproj")); - - - - var debugDir = Path.Combine(Environment.CurrentDirectory, "Debug", "wwwroot"); - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [ - - new TaskItem(Path.Combine("wwwroot", "Microsoft.AspNetCore.Components.CustomElements.lib.module.js"), - - new Dictionary{ ["Integrity"] = "integrity", ["Fingerprint"] = "fingerprint"}), - - new TaskItem(Path.Combine("wwwroot", "Microsoft.AspNetCore.Components.CustomElements.lib.module.js.map"), - - new Dictionary{ ["Integrity"] = "integrity", ["Fingerprint"] = "fingerprint"}) - - ], - - RelativePathPattern = "wwwroot/**", - - SourceType = "Discovered", - - SourceId = "Microsoft.AspNetCore.Components.CustomElements", - - ContentRoot = debugDir, - - BasePath = "_content/Microsoft.AspNetCore.Components.CustomElements", - - TestResolveFileDetails = _testResolveFileDetails, - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue($"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); - - task.Assets.Length.Should().Be(2); - - task.Assets[0].GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("Microsoft.AspNetCore.Components.CustomElements.lib.module.js"); - - task.Assets[0].GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Microsoft.AspNetCore.Components.CustomElements"); - - task.Assets[1].GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("Microsoft.AspNetCore.Components.CustomElements.lib.module.js.map"); - - task.Assets[1].GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Microsoft.AspNetCore.Components.CustomElements"); - - } - - - - [TestMethod] [OSCondition(OperatingSystems.Linux)] - - public void ComputesRelativePath_ForAssets_ExplicitPaths() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - buildEngine.SetupGet(e => e.ProjectFileOfTaskNode) - - .Returns("/home/user/work/Repo/Project/Project.csproj"); - - - - var task = new DefineStaticWebAssets - - { - - BuildEngine = buildEngine.Object, - - CandidateAssets = [ - - new TaskItem("/home/user/work/Repo/Project/Components/Dropdown/Dropdown.razor.js", - - new Dictionary{ ["Integrity"] = "integrity", ["Fingerprint"] = "fingerprint"}), - - ], - - RelativePathPattern = "**", - - SourceType = "Discovered", - - SourceId = "Project", - - ContentRoot = "/home/user/work/Repo/Project", - - BasePath = "_content/Project", - - TestResolveFileDetails = _testResolveFileDetails, - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue($"Errors: {Environment.NewLine} {string.Join($"{Environment.NewLine} ", errorMessages)}"); - - task.Assets.Length.Should().Be(1); - - task.Assets[0].GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().Be("Components/Dropdown/Dropdown.razor.js"); - - task.Assets[0].GetMetadata(nameof(StaticWebAsset.BasePath)).Should().Be("_content/Project"); - - task.Assets[0].GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().Be("/home/user/work/Repo/Project/"); - - } - - - - private static TaskLoggingHelper CreateLogger() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - var loggingHelper = new TaskLoggingHelper(buildEngine.Object, "DefineStaticWebAssets"); - - return loggingHelper; - - } - - - - private (DefineStaticWebAssets.DefineStaticWebAssetsCache cache, Dictionary inputHashes) SetupCache( - - string[] newAssets, - - string[] cached, - - bool appendCachedToInputHashes = false, - - string manifestPath = null) - - { - - var loggingHelper = CreateLogger(); - - var cache = DefineStaticWebAssets.DefineStaticWebAssetsCache.ReadOrCreateCache(loggingHelper, manifestPath); - - cache.InputHashes = [.. cached]; - - cache.CachedAssets = cached.ToDictionary(c => c, c => new StaticWebAsset { Identity = c }); - - - - return (cache, newAssets.Concat(appendCachedToInputHashes ? cached : []).ToDictionary(c => c, c => new TaskItem(c) as ITaskItem)); - - } - - - - public enum UpdatedHash - - { - - GlobalProperties, - - FingerprintPatterns, - - Overrides - - } - - - - private static ITaskItem CreateCandidate( - - string itemSpec, - - string relativePath = null, - - string targetPath = null, - - string link = null, - - string copyToOutputDirectory = null, - - string copyToPublishDirectory = null) - - { - - return new TaskItem(itemSpec, new Dictionary - - { - - ["RelativePath"] = relativePath ?? "", - - ["TargetPath"] = targetPath ?? "", - - ["Link"] = link ?? "", - - ["CopyToOutputDirectory"] = copyToOutputDirectory ?? "", - - ["CopyToPublishDirectory"] = copyToPublishDirectory ?? "", - - // Add these to avoid accessing the disk to compute them - - ["Integrity"] = "integrity", - - ["Fingerprint"] = "fingerprint", - - ["LastWriteTime"] = DateTime.UtcNow.ToString(StaticWebAsset.DateTimeAssetFormat), - - ["FileLength"] = "10", - - }); - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetEndpointsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetEndpointsTest.cs index d6f8aae97dde..dd5f929841fe 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetEndpointsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetEndpointsTest.cs @@ -2,949 +2,322 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; - - [TestClass] - public class FilterStaticWebAssetEndpointsTest - - { - - [TestMethod] - - public void CanFilterEndpoints_ByAssetFile() - - { - - 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 expectedEndpoints = new[] { - - endpoints[0], //index.css - - endpoints[1], //index.fingerprint.css - - endpoints[8], // other.html - - endpoints[10] // other.fingerprint.html - - }; - - Array.Sort(expectedEndpoints); - - var filterEndpointsTask = new FilterStaticWebAssetEndpoints() - - { - - Endpoints = endpoints.Select(endpoints => endpoints.ToTaskItem()).ToArray(), - - Assets = - - [ - - // index.css - - assets[0].ToTaskItem(), - - // other.html - - assets[4].ToTaskItem(), - - ], - - }; - - - - // Act - - var result = filterEndpointsTask.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var filteredEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterEndpointsTask.FilteredEndpoints); - - Array.Sort(filteredEndpoints); - - filteredEndpoints.Should().HaveCount(4); - - filteredEndpoints.Should().BeEquivalentTo(expectedEndpoints); - - } - - - - [TestMethod] - - public void CanFilterEndpoints_ByProperty() - - { - - 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 filterEndpointsTask = new FilterStaticWebAssetEndpoints() - - { - - Endpoints = endpoints.Select(endpoints => endpoints.ToTaskItem()).ToArray(), - - Filters = [ - - new TaskItem("Property", new Dictionary{ - - ["Name"] = "fingerprint" - - }) - - ], - - BuildEngine = Mock.Of() - - }; - - - - // Act - - var result = filterEndpointsTask.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var filteredEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterEndpointsTask.FilteredEndpoints); - - Array.Sort(filteredEndpoints); - - filteredEndpoints.Should().HaveCount(6); - - filteredEndpoints.Should().AllSatisfy(e => e.EndpointProperties.Should().ContainSingle(p => p.Name == "fingerprint")); - - } - - - - [TestMethod] - - public void CanFilterEndpoints_ByResponseHeader() - - { - - 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 filterEndpointsTask = new FilterStaticWebAssetEndpoints() - - { - - Endpoints = endpoints.Select(endpoints => endpoints.ToTaskItem()).ToArray(), - - Filters = [ - - new TaskItem("Header", new Dictionary{ - - ["Name"] = "Content-Type", - - ["Value"] = "text/html" - - }) - - ], - - BuildEngine = Mock.Of() - - }; - - - - // Act - - var result = filterEndpointsTask.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var filteredEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterEndpointsTask.FilteredEndpoints); - - Array.Sort(filteredEndpoints); - - filteredEndpoints.Should().HaveCount(4); - - filteredEndpoints.Should().AllSatisfy(e => e.ResponseHeaders.Should().ContainSingle(p => p.Name == "Content-Type" && p.Value == "text/html")); - - } - - - - [TestMethod] - - public void CanFilterEndpoints_Standalone() - - { - - var assets = new[] { - - CreateAsset("index.html", relativePath: "index#[.{fingerprint}]?.html"), - - CreateAsset("other.js", relativePath: "other#[.{fingerprint}]!.js"), - - }; - - Array.Sort(assets, (l, r) => string.Compare(l.Identity, r.Identity, StringComparison.Ordinal)); - - - - var endpoints = CreateEndpoints(assets); - - var filterEndpointsTask = new FilterStaticWebAssetEndpoints() - - { - - Endpoints = endpoints.Select(endpoints => endpoints.ToTaskItem()).ToArray(), - - Assets = [.. assets.Select(a => a.ToTaskItem())], - - Filters = [ - - new TaskItem("Standalone", new Dictionary{ }) - - ], - - BuildEngine = Mock.Of() - - }; - - - - // Act - - var result = filterEndpointsTask.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var filteredEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterEndpointsTask.FilteredEndpoints); - - Array.Sort(filteredEndpoints); - - filteredEndpoints.Should().HaveCount(2); - - filteredEndpoints.Where(e => e.Route == "index.html").Should().ContainSingle(); - - filteredEndpoints.Where(e => e.Route == "other.fingerprint.js").Should().ContainSingle(); - - } - - - - [TestMethod] - - public void CanFilterEndpoints_BySelector() - - { - - 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); - - endpoints[0].Selectors = [new StaticWebAssetEndpointSelector { Name = "Content-Encoding", Value = "gzip" }]; - - var filterEndpointsTask = new FilterStaticWebAssetEndpoints() - - { - - Endpoints = endpoints.Select(endpoints => endpoints.ToTaskItem()).ToArray(), - - Filters = [ - - new TaskItem("Selector", new Dictionary{ - - ["Name"] = "Content-Encoding", - - ["Value"] = "gzip" - - }) - - ], - - BuildEngine = Mock.Of() - - }; - - - - // Act - - var result = filterEndpointsTask.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var filteredEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterEndpointsTask.FilteredEndpoints); - - Array.Sort(filteredEndpoints); - - filteredEndpoints.Should().ContainSingle(); - - filteredEndpoints[0].Route.Should().Be(endpoints[0].Route); - - } - - - - [TestMethod] - - public void CanFilterEndpoints_ByMultipleCriteria() - - { - - 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 filterEndpointsTask = new FilterStaticWebAssetEndpoints() - - { - - Endpoints = endpoints.Select(endpoints => endpoints.ToTaskItem()).ToArray(), - - Filters = [ - - new TaskItem("Header", new Dictionary{ - - ["Name"] = "Content-Type", - - ["Value"] = "text/html" - - }), - - new TaskItem("Property", new Dictionary{ - - ["Name"] = "fingerprint" - - }) - - ], - - BuildEngine = Mock.Of() - - }; - - - - // Act - - var result = filterEndpointsTask.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var filteredEndpoints = StaticWebAssetEndpoint.FromItemGroup(filterEndpointsTask.FilteredEndpoints); - - Array.Sort(filteredEndpoints); - - filteredEndpoints.Should().HaveCount(2); - - filteredEndpoints.Should().AllSatisfy(e => e.ResponseHeaders.Should().ContainSingle(p => p.Name == "Content-Type" && p.Value == "text/html")); - - filteredEndpoints.Should().AllSatisfy(e => e.EndpointProperties.Should().ContainSingle(p => p.Name == "fingerprint")); - - } - - - - 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(); - - - - defineStaticWebAssetEndpoints.Execute(); - - return StaticWebAssetEndpoint.FromItemGroup(defineStaticWebAssetEndpoints.Endpoints); - - } - - - - private static TaskItem CreateContentMapping(string pattern, string contentType) - - { - - return new TaskItem(contentType, new Dictionary - - { - - { "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 = DateTime.UtcNow, - - }; - - - - result.ApplyDefaults(); - - result.Normalize(); - - - - return result; - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetGroupsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetGroupsTest.cs index 4001a52b6ffa..0b2d4abf387f 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetGroupsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FilterStaticWebAssetGroupsTest.cs @@ -2,937 +2,319 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - [TestClass] public class FilterStaticWebAssetGroupsTest : IDisposable - - { - - private readonly string _tempDir; - - private readonly Mock _buildEngine; - - private readonly List _errorMessages; - - private readonly List _logMessages; - - - - public FilterStaticWebAssetGroupsTest() - - { - - _tempDir = Path.Combine(Path.GetTempPath(), "FilterGroups_" + Guid.NewGuid().ToString("N")); - - Directory.CreateDirectory(_tempDir); - - - - _errorMessages = new List(); - - _logMessages = new List(); - - _buildEngine = new Mock(); - - _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => _errorMessages.Add(args.Message)); - - _buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => _logMessages.Add(args.Message)); - - } - - - - public void Dispose() - - { - - if (Directory.Exists(_tempDir)) - - { - - try { Directory.Delete(_tempDir, recursive: true); } catch { } - - } - - } - - - - [TestMethod] - - public void ConcreteGroupSatisfied_AssetIncluded() - - { - - var asset1 = CreateAssetItem("app.js", "MyLib", ""); - - var asset2 = CreateAssetItem("site.css", "MyLib", "BootstrapVersion=V5"); - - - - var endpoint1 = CreateEndpointItem("app.js", asset1.ItemSpec); - - var endpoint2 = CreateEndpointItem("site.css", asset2.ItemSpec); - - - - var (task, result) = ExecuteFilterTask( - - new[] { asset1, asset2 }, - - new[] { endpoint1, endpoint2 }, - - new[] { CreateGroup("BootstrapVersion", "V5", "MyLib") }); - - - - result.Should().BeTrue(); - - task.FilteredAssets.Should().HaveCount(2, "all assets should pass through when groups are satisfied"); - - task.SurvivingEndpoints.Should().HaveCount(2); - - } - - - - [TestMethod] - - public void ConcreteGroupUnsatisfied_AssetExcluded() - - { - - var asset = CreateAssetItem("server.js", "MyLib", "ServerRendering=true"); - - var endpoint = CreateEndpointItem("server.js", asset.ItemSpec); - - - - var (task, result) = ExecuteFilterTask( - - new[] { asset }, - - new[] { endpoint }, - - new[] { CreateGroup("ServerRendering", "false", "MyLib") }); // Value doesn't match - - - - result.Should().BeTrue(); - - task.FilteredAssets.Where(a => a != null).Should().HaveCount(0, "asset with unsatisfied group should be excluded"); - - task.SurvivingEndpoints.Should().HaveCount(0, "endpoint for excluded asset should be removed"); - - } - - - - [TestMethod] - - public void DeferredGroupInFinalPass_Errors() - - { - - var asset = CreateAssetItem("server.js", "MyLib", "ServerRendering=true"); - - var endpoint = CreateEndpointItem("server.js", asset.ItemSpec); - - - - // SkipDeferred defaults to false (final pass) - - var (_, result) = ExecuteFilterTask( - - new[] { asset }, - - new[] { endpoint }, - - new[] { CreateGroup("ServerRendering", "true", "MyLib", deferred: true) }); - - - - result.Should().BeFalse("deferred groups in the final pass should produce an error"); - - _errorMessages.Should().ContainSingle() - - .Which.Should().Contain("Deferred"); - - } - - - - [TestMethod] - - public void SkipDeferred_DeferredGroupsSkipped_AssetPassesThrough() - - { - - var asset = CreateAssetItem("server.js", "MyLib", "ServerRendering=true"); - - var endpoint = CreateEndpointItem("server.js", asset.ItemSpec); - - - - var (task, result) = ExecuteFilterTask( - - new[] { asset }, - - new[] { endpoint }, - - new[] { CreateGroup("ServerRendering", "false", "MyLib", deferred: true) }, // Would fail if evaluated - - skipDeferred: true); - - - - result.Should().BeTrue(); - - task.FilteredAssets.Should().HaveCount(1, "deferred groups should be skipped during pre-filter"); - - task.SurvivingEndpoints.Should().HaveCount(1); - - } - - - - [TestMethod] - - public void CascadingExclusion_RelatedAssetsExcludedWithPrimary() - - { - - var primary = CreateAssetItem("server.js", "MyLib", "ServerRendering=true"); - - var related = CreateRelatedAssetItem("server.js.gz", "server.js.gz", "MyLib", primary); - - - - var primaryEndpoint = CreateEndpointItem("server.js", primary.ItemSpec); - - var relatedEndpoint = CreateEndpointItem("server.js.gz", related.ItemSpec); - - - - var (task, result) = ExecuteFilterTask( - - new[] { primary, related }, - - new[] { primaryEndpoint, relatedEndpoint }, - - new[] { CreateGroup("ServerRendering", "false", "MyLib") }); // Not satisfied - - - - result.Should().BeTrue(); - - task.FilteredAssets.Where(a => a != null).Should().HaveCount(0, "both primary and related should be excluded via cascading"); - - task.SurvivingEndpoints.Should().HaveCount(0, "endpoints for both excluded assets should be removed"); - - } - - - - [TestMethod] - - public void CascadingExclusion_RelatedAssetPathResolvedAgainstTaskEnvironment() - - { - - var primary = CreateAssetItem("server.js", "MyLib", "ServerRendering=true"); - - var related = CreateRelatedAssetItem("server.js.gz", "server.js.gz", "MyLib", primary, relatedAsset: "server.js"); - - - - var primaryEndpoint = CreateEndpointItem("server.js", primary.ItemSpec); - - var relatedEndpoint = CreateEndpointItem("server.js.gz", related.ItemSpec); - - - - var (task, result) = ExecuteFilterTask( - - new[] { primary, related }, - - new[] { primaryEndpoint, relatedEndpoint }, - - new[] { CreateGroup("ServerRendering", "false", "MyLib") }, - - taskEnvironment: TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(_tempDir)); - - - - result.Should().BeTrue(); - - task.FilteredAssets.Where(a => a != null).Should().HaveCount(0, "the related asset path should resolve against the project directory"); - - task.SurvivingEndpoints.Should().HaveCount(0); - - } - - - - [TestMethod] - - public void EndpointsFiltered_ForExcludedAssets() - - { - - var includedAsset = CreateAssetItem("app.js", "MyLib", ""); - - var excludedAsset = CreateAssetItem("server.js", "MyLib", "ServerRendering=true"); - - - - var includedEndpoint = CreateEndpointItem("app.js", includedAsset.ItemSpec); - - var excludedEndpoint = CreateEndpointItem("server.js", excludedAsset.ItemSpec); - - - - var (task, result) = ExecuteFilterTask( - - new[] { includedAsset, excludedAsset }, - - new[] { includedEndpoint, excludedEndpoint }, - - new[] { CreateGroup("ServerRendering", "false", "MyLib") }); - - - - result.Should().BeTrue(); - - var nonNullAssets = task.FilteredAssets.Where(a => a != null).ToArray(); - - nonNullAssets.Should().HaveCount(1); - - nonNullAssets[0].ItemSpec.Should().Be(includedAsset.ItemSpec); - - task.SurvivingEndpoints.Should().HaveCount(1); - - task.SurvivingEndpoints[0].ItemSpec.Should().Be("app.js"); - - } - - - - [TestMethod] - - public void SkipDeferred_NonDeferredGroupsStillEvaluated() - - { - - // An asset has both a non-deferred group requirement and a deferred group requirement. - - // In SkipDeferred mode, only the non-deferred group is evaluated. - - var asset = CreateAssetItem("site.css", "MyLib", "BootstrapVersion=V5;ServerRendering=true"); - - - - var endpoint = CreateEndpointItem("site.css", asset.ItemSpec); - - - - var (task, result) = ExecuteFilterTask( - - new[] { asset }, - - new[] { endpoint }, - - new[] - - { - - CreateGroup("BootstrapVersion", "V5", "MyLib"), - - CreateGroup("ServerRendering", "true", "MyLib", deferred: true) - - }, - - skipDeferred: true); - - - - result.Should().BeTrue(); - - task.FilteredAssets.Should().HaveCount(1, - - "non-deferred BootstrapVersion is satisfied; deferred ServerRendering is skipped"); - - } - - - - [TestMethod] - - public void NullStaticWebAssetGroups_PassesThrough() - - { - - var asset = CreateAssetItem("app.js", "MyLib", ""); - - var endpoint = CreateEndpointItem("app.js", asset.ItemSpec); - - - - var (task, result) = ExecuteFilterTask( - - new[] { asset }, - - new[] { endpoint }); - - - - result.Should().BeTrue(); - - task.FilteredAssets.Should().HaveCount(1); - - task.SurvivingEndpoints.Should().HaveCount(1); - - } - - - - private static ITaskItem CreateGroup(string name, string value, string sourceId, bool deferred = false) - - { - - var dict = new Dictionary - - { - - ["Value"] = value, - - ["SourceId"] = sourceId, - - }; - - if (deferred) - - dict["Deferred"] = "true"; - - return new TaskItem(name, dict); - - } - - - - private (FilterStaticWebAssetGroups Task, bool Result) ExecuteFilterTask( - - ITaskItem[] assets, - - ITaskItem[] endpoints, - - ITaskItem[] groups = null, - - bool skipDeferred = false, - - TaskEnvironment taskEnvironment = null) - - { - - var task = new FilterStaticWebAssetGroups - - { - - BuildEngine = _buildEngine.Object, - - TaskEnvironment = taskEnvironment ?? TaskEnvironment.Fallback, - - Assets = assets, - - Endpoints = endpoints, - - SkipDeferred = skipDeferred, - - StaticWebAssetGroups = groups, - - }; - - var result = task.Execute(); - - return (task, result); - - } - - - - private ITaskItem CreateRelatedAssetItem(string fileName, string relativePath, string sourceId, ITaskItem primaryAsset, string traitName = "Content-Encoding", string traitValue = "gzip", string relatedAsset = null) - - { - - var filePath = Path.Combine(_tempDir, fileName); - - if (!File.Exists(filePath)) - - { - - File.WriteAllText(filePath, "content-" + fileName); - - } - - - - return new TaskItem(filePath, new Dictionary - - { - - ["SourceType"] = "Package", - - ["SourceId"] = sourceId, - - ["ContentRoot"] = _tempDir + Path.DirectorySeparatorChar, - - ["BasePath"] = "_content/" + sourceId.ToLowerInvariant(), - - ["RelativePath"] = relativePath, - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Alternative", - - ["RelatedAsset"] = relatedAsset ?? primaryAsset.ItemSpec, - - ["AssetTraitName"] = traitName, - - ["AssetTraitValue"] = traitValue, - - ["AssetGroups"] = "", - - ["Fingerprint"] = "test", - - ["Integrity"] = "sha256-test", - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["OriginalItemSpec"] = filePath, - - }); - - } - - - - private ITaskItem CreateAssetItem(string fileName, string sourceId, string assetGroups) - - { - - var filePath = Path.Combine(_tempDir, fileName); - - if (!File.Exists(filePath)) - - { - - File.WriteAllText(filePath, "content-" + fileName); - - } - - - - return new TaskItem(filePath, new Dictionary - - { - - ["SourceType"] = "Package", - - ["SourceId"] = sourceId, - - ["ContentRoot"] = _tempDir + Path.DirectorySeparatorChar, - - ["BasePath"] = "_content/" + sourceId.ToLowerInvariant(), - - ["RelativePath"] = fileName, - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["AssetGroups"] = assetGroups, - - ["Fingerprint"] = "test", - - ["Integrity"] = "sha256-test", - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["OriginalItemSpec"] = filePath, - - }); - - } - - - - private static ITaskItem CreateEndpointItem(string route, string assetFile) - - { - - return new TaskItem(route, new Dictionary - - { - - ["AssetFile"] = assetFile, - - ["Selectors"] = "[]", - - ["ResponseHeaders"] = "[]", - - ["EndpointProperties"] = "[]", - - }); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FingerprintPatternMatcherTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FingerprintPatternMatcherTest.cs index 026c45c12ccc..472461199e05 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FingerprintPatternMatcherTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/FingerprintPatternMatcherTest.cs @@ -6,381 +6,131 @@ using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; - - - - [TestClass] public class FingerprintPatternMatcherTest - - { - - private readonly TaskLoggingHelper _log = new TestTaskLoggingHelper(); - - - - [TestMethod] - - public void AppendFingerprintPattern_AlreadyContainsFingerprint_ReturnsIdentity() - - { - - // Arrange - - var relativePath = "test#[.{fingerprint}].txt"; - - - - // Act - - var result = new FingerprintPatternMatcher(_log, []).AppendFingerprintPattern(CreateMatchContext(relativePath), "Identity"); - - - - // Assert - - Assert.AreEqual(relativePath, result); - - } - - - - [TestMethod] - - public void AppendFingerprintPattern_AppendsPattern_AtTheEndOfTheFileName() - - { - - // Arrange - - var relativePath = Path.Combine("folder", "test.txt"); - - var expected = Path.Combine("folder", "test#[.{fingerprint}]?.txt"); - - - - // Act - - var result = new FingerprintPatternMatcher(_log, []).AppendFingerprintPattern(CreateMatchContext(relativePath), "Identity"); - - - - // Assert - - Assert.AreEqual(expected, result); - - } - - - - [TestMethod] - - public void AppendFingerprintPattern_AppendsPattern_AtTheEndOfTheFileName_WhenFileNameContainsDots() - - { - - // Arrange - - var relativePath = Path.Combine("folder", "test.v1.txt"); - - var expected = Path.Combine("folder", "test.v1#[.{fingerprint}]?.txt"); - - // Act - - var result = new FingerprintPatternMatcher(_log, []).AppendFingerprintPattern(CreateMatchContext(relativePath), "Identity"); - - // Assert - - Assert.AreEqual(expected, result); - - } - - - - [TestMethod] - - public void AppendFingerprintPattern_AppendsPattern_AtTheEndOfTheFileName_WhenFileDoesNotHaveExtension() - - { - - // Arrange - - var relativePath = Path.Combine("folder", "README"); - - var expected = Path.Combine("folder", "README#[.{fingerprint}]?"); - - // Act - - var result = new FingerprintPatternMatcher(_log, []).AppendFingerprintPattern(CreateMatchContext(relativePath), "Identity"); - - // Assert - - Assert.AreEqual(expected, result); - - } - - - - [TestMethod] - - public void AppendFingerprintPattern_AppendsPattern_AtTheRightLocation_WhenACustomPatternIsProvided() - - { - - // Arrange - - var relativePath = Path.Combine("folder", "test.bundle.scp.css"); - - var expected = Path.Combine("folder", "test#[.{fingerprint}]!.bundle.scp.css"); - - - - // Act - - var result = new FingerprintPatternMatcher( - - _log, - - [new TaskItem("ScopedCSS", new Dictionary { ["Pattern"] = "*.bundle.scp.css", ["Expression"] = "#[.{fingerprint}]!" })]) - - .AppendFingerprintPattern(CreateMatchContext(relativePath), "Identity"); - - - - // Assert - - Assert.AreEqual(expected, result); - - } - - - - private StaticWebAssetGlobMatcher.MatchContext CreateMatchContext(string path) - - { - - var context = new StaticWebAssetGlobMatcher.MatchContext(); - - context.SetPathAndReinitialize(path); - - return context; - - } - - - - private class TestTaskLoggingHelper : TaskLoggingHelper - - { - - public TestTaskLoggingHelper() : base(new TestTask()) - - { - - } - - - - private class TestTask : ITask - - { - - public IBuildEngine BuildEngine { get; set; } = new TestBuildEngine(); - - public ITaskHost HostObject { get; set; } = new TestTaskHost(); - - - - public bool Execute() => true; - - } - - - - private class TestBuildEngine : IBuildEngine - - { - - public bool ContinueOnError => true; - - - - public int LineNumberOfTaskNode => 0; - - - - public int ColumnNumberOfTaskNode => 0; - - - - public string ProjectFileOfTaskNode => "test.csproj"; - - - - public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs) => true; - - - - public void LogCustomEvent(CustomBuildEventArgs e) { } - - public void LogErrorEvent(BuildErrorEventArgs e) { } - - public void LogMessageEvent(BuildMessageEventArgs e) { } - - public void LogWarningEvent(BuildWarningEventArgs e) { } - - } - - - - private class TestTaskHost : ITaskHost - - { - - public object HostObject { get; set; } = new object(); - - } - - } - - - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsManifestFileTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsManifestFileTest.cs index 74d939ea87d8..1ed30cccf405 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsManifestFileTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsManifestFileTest.cs @@ -2,1156 +2,392 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Text.Json; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - [TestClass] public class GeneratePackageAssetsManifestFileTest : IDisposable - - { - - private readonly string _tempDir; - - private readonly Mock _buildEngine; - - private readonly List _errorMessages; - - - - public GeneratePackageAssetsManifestFileTest() - - { - - _tempDir = Path.Combine(Path.GetTempPath(), "GenPkgManifest_" + Guid.NewGuid().ToString("N")); - - Directory.CreateDirectory(_tempDir); - - - - _errorMessages = new List(); - - _buildEngine = new Mock(); - - _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => _errorMessages.Add(args.Message)); - - _buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())); - - } - - - - public void Dispose() - - { - - if (Directory.Exists(_tempDir)) - - { - - try { Directory.Delete(_tempDir, recursive: true); } catch { } - - } - - } - - - - [TestMethod] - - public void EmptyAssets_DoesNotGenerateManifestFile() - - { - - var manifestPath = Path.Combine(_tempDir, "empty.json"); - - - - var task = new GeneratePackageAssetsManifestFile - - { - - BuildEngine = _buildEngine.Object, - - StaticWebAssets = Array.Empty(), - - StaticWebAssetEndpoints = Array.Empty(), - - TargetManifestPath = manifestPath, - - }; - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - File.Exists(manifestPath).Should().BeFalse(); - - } - - - - [TestMethod] - - public void Assets_SerializedWithCorrectPackagePaths() - - { - - var file = CreateTempFile("wwwroot", "css", "site.css", "body{}"); - - var contentRoot = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; - - - - var asset = CreateAsset(file, contentRoot, "css/site#[.{fingerprint}]?.css", "abc123"); - - - - var task = CreateManifestTask(new[] { asset.ToTaskItem() }); - - task.Execute().Should().BeTrue(); - - var manifest = DeserializeManifest(task.TargetManifestPath); - - - - manifest.Assets.Should().HaveCount(1); - - var manifestAsset = manifest.Assets.Values.Single(); - - var packagePath = manifest.Assets.Keys.Single(); - - - - // Discovered assets don't include BasePath in the target path - - packagePath.Should().EndWith("css/site.css"); - - manifestAsset.RelativePath.Should().Be("css/site#[.{fingerprint}]?.css"); - - manifestAsset.AssetRole.Should().Be("Primary"); - - } - - - - [TestMethod] - - public void RelatedAsset_RemappedToPackageRelativePath() - - { - - var primaryFile = CreateTempFile("wwwroot", "css", "site.css", "body{}"); - - var relatedFile = CreateTempFile("wwwroot", "css", "site.css.gz", "compressed"); - - var contentRoot = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; - - - - var primary = CreateAsset(primaryFile, contentRoot, "css/site.css", "abc"); - - - - var related = CreateAsset(relatedFile, contentRoot, "css/site.css.gz", "def"); - - related.AssetRole = "Alternative"; - - related.RelatedAsset = primaryFile; - - related.AssetTraitName = "Content-Encoding"; - - related.AssetTraitValue = "gzip"; - - - - var task = CreateManifestTask( - - new[] { primary.ToTaskItem(), related.ToTaskItem() }); - - task.Execute().Should().BeTrue(); - - var manifest = DeserializeManifest(task.TargetManifestPath); - - - - manifest.Assets.Should().HaveCount(2); - - - - var relatedAsset = manifest.Assets.Values.First(a => a.AssetRole == "Alternative"); - - // The RelatedAsset should be remapped from the absolute path to a package-relative path - - relatedAsset.RelatedAsset.Should().NotBe(primaryFile); - - relatedAsset.RelatedAsset.Should().NotBeNullOrEmpty(); - - // It should match the primary's PackagePath - - var primaryAssetPath = manifest.Assets.First(kvp => kvp.Value.AssetRole == "Primary").Key; - - relatedAsset.RelatedAsset.Should().Be(primaryAssetPath); - - } - - - - [TestMethod] - - public void Endpoints_AssetFileRemappedToPackageRelativePath() - - { - - var file = CreateTempFile("wwwroot", "js", "app.js", "var x;"); - - var contentRoot = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; - - - - var asset = CreateAsset(file, contentRoot, "js/app.js", "abc"); - - - - var endpoint = new StaticWebAssetEndpoint - - { - - Route = "_content/mylib/js/app.js", - - AssetFile = file, - - Selectors = [], - - ResponseHeaders = [new() { Name = "Content-Type", Value = "text/javascript" }], - - EndpointProperties = [], - - }; - - - - var task = CreateManifestTask( - - new[] { asset.ToTaskItem() }, - - StaticWebAssetEndpoint.ToTaskItems(new[] { endpoint })); - - task.Execute().Should().BeTrue(); - - var manifest = DeserializeManifest(task.TargetManifestPath); - - - - manifest.Endpoints.Should().HaveCount(1); - - var ep = manifest.Endpoints[0]; - - // AssetFile should be remapped from absolute to package-relative - - ep.AssetFile.Should().NotBe(file); - - ep.AssetFile.Should().Be(manifest.Assets.Keys.Single()); - - } - - - - [TestMethod] - - public void FrameworkPattern_TagsMatchingAssetsAsFramework() - - { - - var fwFile = CreateTempFile("wwwroot", "js", "framework.js", "fw"); - - var nonFwFile = CreateTempFile("wwwroot", "js", "app.js", "app"); - - var contentRoot = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; - - - - var fwAsset = CreateAsset(fwFile, contentRoot, "js/framework.js", "abc"); - - var nonFwAsset = CreateAsset(nonFwFile, contentRoot, "js/app.js", "def"); - - - - var task = CreateManifestTask( - - new[] { fwAsset.ToTaskItem(), nonFwAsset.ToTaskItem() }, - - frameworkPattern: "js/framework*"); - - task.Execute().Should().BeTrue(); - - var manifest = DeserializeManifest(task.TargetManifestPath); - - - - manifest.Assets.Should().HaveCount(2); - - - - var fwManifestAsset = manifest.Assets.Values.First(a => a.RelativePath == "js/framework.js"); - - fwManifestAsset.SourceType.Should().Be("Framework"); - - - - var nonFwManifestAsset = manifest.Assets.Values.First(a => a.RelativePath == "js/app.js"); - - nonFwManifestAsset.SourceType.Should().Be("Package"); - - } - - - - [TestMethod] - - public void AssetGroups_PreservedInManifest() - - { - - var file = CreateTempFile("wwwroot", "css", "site.css", "body{}"); - - var contentRoot = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; - - - - var asset = CreateAsset(file, contentRoot, "css/site.css", "abc"); - - asset.AssetGroups = "BootstrapVersion=V5"; - - - - var task = CreateManifestTask(new[] { asset.ToTaskItem() }); - - task.Execute().Should().BeTrue(); - - var manifest = DeserializeManifest(task.TargetManifestPath); - - - - manifest.Assets.Should().HaveCount(1); - - manifest.Assets.Values.Single().AssetGroups.Should().Be("BootstrapVersion=V5"); - - } - - - - [TestMethod] - - public void RelatedAsset_Unmapped_ProducesError() - - { - - // Symmetric to Endpoints_UnmappedAssetFile_ProducesError: exercises the - - // GeneratePackageAssetsManifestFile.cs error branch for RelatedAsset that - - // can't be remapped to a package-relative path (i.e., points outside the - - // packaged asset set). Without this test the RelatedAsset error branch is - - // unexercised by automated tests. - - var primaryFile = CreateTempFile("wwwroot", "css", "site.css", "body{}"); - - var relatedFile = CreateTempFile("wwwroot", "css", "site.css.gz", "gz"); - - var contentRoot = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; - - - - var primary = CreateAsset(primaryFile, contentRoot, "css/site.css", "abc"); - - - - var related = CreateAsset(relatedFile, contentRoot, "css/site.css.gz", "def"); - - related.AssetRole = "Alternative"; - - related.AssetTraitName = "Content-Encoding"; - - related.AssetTraitValue = "gzip"; - - // RelatedAsset points to a file that is NOT in the StaticWebAssets input set, - - // so it has no entry in identityToPackagePath and cannot be remapped. - - related.RelatedAsset = Path.Combine(_tempDir, "nonexistent", "primary.css"); - - - - var task = CreateManifestTask( - - new[] { primary.ToTaskItem(), related.ToTaskItem() }); - - var result = task.Execute(); - - - - result.Should().BeFalse(); - - _errorMessages.Should().ContainSingle(m => - - m.Contains("could not be mapped to a package-relative path") && - - m.Contains("RelatedAsset")); - - File.Exists(task.TargetManifestPath).Should().BeFalse( - - "the manifest must not be written when a referential integrity error is detected"); - - } - - - - [TestMethod] - - public void RoundTrip_GenerateThenRead_RelatedAssetResolvesToConsumerAbsolutePath() - - { - - // End-to-end cross-boundary test. Proves the contract between - - // GeneratePackageAssetsManifestFile (producer) and ReadPackageAssetsManifest - - // (consumer): an absolute build-time RelatedAsset is remapped to a - - // package-relative form on the producer side, then re-resolved to an - - // absolute path under the consumer's packageRoot on the consumer side. - - // Two distinct directory trees stand in for producer and consumer machines. - - var primaryFile = CreateTempFile("source", "wwwroot", "css", "site.css", "body{}"); - - var relatedFile = CreateTempFile("source", "wwwroot", "css", "site.css.gz", "gz"); - - var contentRoot = Path.Combine(_tempDir, "source", "wwwroot") + Path.DirectorySeparatorChar; - - - - var primary = CreateAsset(primaryFile, contentRoot, "css/site.css", "abc"); - - var related = CreateAsset(relatedFile, contentRoot, "css/site.css.gz", "def"); - - related.AssetRole = "Alternative"; - - related.AssetTraitName = "Content-Encoding"; - - related.AssetTraitValue = "gzip"; - - related.RelatedAsset = primaryFile; - - - - // Producer side: write the manifest at the layout ReadPackageAssetsManifest expects: - - // packageRoot/build/.PackageAssets.json - - var packageRoot = Path.Combine(_tempDir, "packages", "MyLib"); - - var buildDir = Path.Combine(packageRoot, "build"); - - Directory.CreateDirectory(buildDir); - - var manifestPath = Path.Combine(buildDir, "MyLib.PackageAssets.json"); - - - - var generateTask = new GeneratePackageAssetsManifestFile - - { - - BuildEngine = _buildEngine.Object, - - StaticWebAssets = new[] { primary.ToTaskItem(), related.ToTaskItem() }, - - StaticWebAssetEndpoints = Array.Empty(), - - TargetManifestPath = manifestPath, - - }; - - generateTask.Execute().Should().BeTrue(); - - File.Exists(manifestPath).Should().BeTrue(); - - - - // Consumer side: feed the producer's manifest into ReadPackageAssetsManifest - - // pretending we're on a different machine (different packageRoot than the - - // producer's contentRoot). The whole point of producer-side package-relative - - // remap is that the consumer can re-anchor without knowing the producer's CWD. - - var consumerContentRoot = Path.Combine(packageRoot, "staticwebassets") + Path.DirectorySeparatorChar; - - var manifestItem = new TaskItem(manifestPath, new Dictionary - - { - - ["SourceId"] = "MyLib", - - ["ContentRoot"] = consumerContentRoot, - - ["PackageRoot"] = packageRoot, - - }); - - - - var readTask = new ReadPackageAssetsManifest - - { - - BuildEngine = _buildEngine.Object, - - PackageManifests = new[] { manifestItem }, - - StaticWebAssetGroups = Array.Empty(), - - IntermediateOutputPath = Path.Combine(_tempDir, "obj"), - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - readTask.Execute().Should().BeTrue(); - - readTask.Assets.Should().HaveCount(2); - - - - var emittedRelated = readTask.Assets.Single(a => a.GetMetadata("AssetRole") == "Alternative"); - - var emittedPrimary = readTask.Assets.Single(a => a.GetMetadata("AssetRole") == "Primary"); - - - - // The producer's contentRoot is _tempDir/source/wwwroot — the consumer must - - // not see any leakage of it. RelatedAsset on the consumer side must be the - - // primary's Identity (absolute path under the consumer's packageRoot). - - emittedRelated.GetMetadata("RelatedAsset").Should().Be(emittedPrimary.ItemSpec, - - "consumer's RelatedAsset must equal primary's Identity after package-root re-resolution"); - - emittedRelated.GetMetadata("RelatedAsset").Should().StartWith(packageRoot, - - "RelatedAsset must be re-anchored to the consumer's packageRoot, not the producer's contentRoot"); - - emittedRelated.GetMetadata("RelatedAsset").Should().NotContain( - - Path.Combine(_tempDir, "source"), - - "no producer-side build-time path may leak through the manifest to the consumer"); - - } - - - - [TestMethod] - - public void Endpoints_UnmappedAssetFile_ProducesError() - - { - - var file = CreateTempFile("wwwroot", "js", "app.js", "var x;"); - - var contentRoot = Path.Combine(_tempDir, "wwwroot") + Path.DirectorySeparatorChar; - - - - var asset = CreateAsset(file, contentRoot, "js/app.js", "abc"); - - - - var endpoint = new StaticWebAssetEndpoint - - { - - Route = "_content/mylib/js/missing.js", - - AssetFile = Path.Combine(_tempDir, "nonexistent", "missing.js"), - - Selectors = [], - - ResponseHeaders = [], - - EndpointProperties = [], - - }; - - - - var task = CreateManifestTask( - - new[] { asset.ToTaskItem() }, - - StaticWebAssetEndpoint.ToTaskItems(new[] { endpoint })); - - var result = task.Execute(); - - - - result.Should().BeFalse(); - - _errorMessages.Should().ContainSingle(m => m.Contains("could not be mapped to a package-relative path")); - - } - - - - private GeneratePackageAssetsManifestFile CreateManifestTask( - - ITaskItem[] assets, - - ITaskItem[] endpoints = null, - - string frameworkPattern = null) - - { - - var task = new GeneratePackageAssetsManifestFile - - { - - BuildEngine = _buildEngine.Object, - - StaticWebAssets = assets, - - StaticWebAssetEndpoints = endpoints ?? Array.Empty(), - - TargetManifestPath = Path.Combine(_tempDir, "manifest.json"), - - }; - - - - if (frameworkPattern != null) - - task.FrameworkPattern = frameworkPattern; - - - - return task; - - } - - - - private static StaticWebAssetPackageManifest DeserializeManifest(string manifestPath) - - { - - return JsonSerializer.Deserialize( - - File.ReadAllBytes(manifestPath), - - StaticWebAssetsJsonSerializerContext.Default.StaticWebAssetPackageManifest); - - } - - - - private string CreateTempFile(params string[] pathParts) - - { - - var content = pathParts[^1]; - - var segments = pathParts[..^1]; - - - - var dir = Path.Combine(new[] { _tempDir }.Concat(segments[..^1]).ToArray()); - - Directory.CreateDirectory(dir); - - var filePath = Path.Combine(dir, segments[^1]); - - File.WriteAllText(filePath, content); - - return filePath; - - } - - - - private StaticWebAsset CreateAsset(string filePath, string contentRoot, string relativePath, string fingerprint) - - { - - var asset = new StaticWebAsset - - { - - Identity = filePath, - - SourceType = "Discovered", - - SourceId = "MyLib", - - ContentRoot = contentRoot, - - BasePath = "_content/mylib", - - RelativePath = relativePath, - - AssetKind = "All", - - AssetMode = "All", - - AssetRole = "Primary", - - RelatedAsset = "", - - AssetTraitName = "", - - AssetTraitValue = "", - - CopyToOutputDirectory = "Never", - - CopyToPublishDirectory = "PreserveNewest", - - OriginalItemSpec = filePath, - - Fingerprint = fingerprint, - - Integrity = "sha256-" + fingerprint, - - FileLength = 6, - - LastWriteTime = DateTime.UtcNow, - - }; - - asset.ApplyDefaults(); - - asset.Normalize(); - - return asset; - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsTargetsFileTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsTargetsFileTest.cs index 58e2e7d0546a..c8418ffbe8c5 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsTargetsFileTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GeneratePackageAssetsTargetsFileTest.cs @@ -2,394 +2,138 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Xml.Linq; - - using Microsoft.Build.Framework; - - using Moq; - - - - namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - [TestClass] public class GeneratePackageAssetsTargetsFileTest : IDisposable - - { - - private readonly string _tempDir; - - private readonly Mock _buildEngine; - - private readonly List _errorMessages; - - - - public GeneratePackageAssetsTargetsFileTest() - - { - - _tempDir = Path.Combine(Path.GetTempPath(), "GenPkgTargets_" + Guid.NewGuid().ToString("N")); - - Directory.CreateDirectory(_tempDir); - - - - _errorMessages = new List(); - - _buildEngine = new Mock(); - - _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => _errorMessages.Add(args.Message)); - - _buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())); - - } - - - - public void Dispose() - - { - - if (Directory.Exists(_tempDir)) - - { - - try { Directory.Delete(_tempDir, recursive: true); } catch { } - - } - - } - - - - [TestMethod] - - public void GeneratesValidXml_WithStaticWebAssetPackageManifestItem() - - { - - var task = CreateTask(); - - - - task.Execute().Should().BeTrue(); - - File.Exists(task.TargetFilePath).Should().BeTrue(); - - - - var doc = XDocument.Load(task.TargetFilePath); - - var root = doc.Root; - - root.Should().NotBeNull(); - - root.Name.LocalName.Should().Be("Project"); - - - - var itemGroup = root.Element("ItemGroup"); - - itemGroup.Should().NotBeNull(); - - - - var manifestItem = itemGroup.Element("StaticWebAssetPackageManifest"); - - manifestItem.Should().NotBeNull(); - - - - var includeAttr = manifestItem.Attribute("Include"); - - includeAttr.Should().NotBeNull(); - - includeAttr.Value.Should().Contain("MyLib.PackageAssets.json"); - - - - var sourceIdElement = manifestItem.Element("SourceId"); - - sourceIdElement.Should().NotBeNull(); - - sourceIdElement.Value.Should().Be("MyLib"); - - - - var contentRootElement = manifestItem.Element("ContentRoot"); - - contentRootElement.Should().NotBeNull(); - - contentRootElement.Value.Should().Contain("staticwebassets"); - - - - var packageRootElement = manifestItem.Element("PackageRoot"); - - packageRootElement.Should().NotBeNull(); - - } - - - - [TestMethod] - - public void Incremental_FileNotRewritten_WhenContentUnchanged() - - { - - var task = CreateTask(); - - - - // First write - - task.Execute().Should().BeTrue(); - - var firstWriteTime = File.GetLastWriteTimeUtc(task.TargetFilePath); - - var firstContent = File.ReadAllText(task.TargetFilePath); - - - - // Advance the file timestamp so we can detect if it gets rewritten - - File.SetLastWriteTimeUtc(task.TargetFilePath, firstWriteTime.AddSeconds(10)); - - firstWriteTime = File.GetLastWriteTimeUtc(task.TargetFilePath); - - - - // Second write with same inputs - - var task2 = CreateTask(); - - task2.Execute().Should().BeTrue(); - - var secondWriteTime = File.GetLastWriteTimeUtc(task2.TargetFilePath); - - var secondContent = File.ReadAllText(task2.TargetFilePath); - - - - // Content should be identical - - secondContent.Should().Be(firstContent); - - // File should not have been rewritten (timestamp preserved) - - secondWriteTime.Should().Be(firstWriteTime); - - } - - - - [TestMethod] - - public void CustomPackagePathPrefix_ReflectedInContentRoot() - - { - - var task = CreateTask(packagePathPrefix: "custom/assets"); - - - - task.Execute().Should().BeTrue(); - - var manifestItem = LoadManifestItem(task.TargetFilePath); - - - - manifestItem.Element("ContentRoot").Value.Should().Contain("custom"); - - } - - - - private GeneratePackageAssetsTargetsFile CreateTask(string packagePathPrefix = null) - - { - - var task = new GeneratePackageAssetsTargetsFile - - { - - BuildEngine = _buildEngine.Object, - - PackageId = "MyLib", - - TargetFilePath = Path.Combine(_tempDir, "MyLib.targets"), - - ManifestFileName = "MyLib.PackageAssets.json", - - }; - - if (packagePathPrefix != null) - - { - - task.PackagePathPrefix = packagePathPrefix; - - } - - return task; - - } - - - - private static XElement LoadManifestItem(string targetFilePath) - - { - - var doc = XDocument.Load(targetFilePath); - - return doc.Root.Element("ItemGroup").Element("StaticWebAssetPackageManifest"); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsManifestTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsManifestTest.cs index 65678deb71b5..f5599e5dac1e 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsManifestTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsManifestTest.cs @@ -2,1483 +2,501 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - 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; - - - - [TestClass] public class GenerateStaticWebAssetEndpointsManifestTest - - { - - [TestMethod] - - 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() - - }; - - - - try - - { - - // Act - - task.Execute(); - - - - // Assert - - new FileInfo(path).Should().Exist(); - - var manifest = File.ReadAllText(path); - - var json = JsonSerializer.Deserialize(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); - - } - - } - - } - - - - [TestMethod] - - 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() - - }; - - - - try - - { - - // Act - - task.Execute(); - - - - // Assert - - new FileInfo(path).Should().Exist(); - - new FileInfo(exclusionCachePath).Should().Exist(); - - - - var manifest = File.ReadAllText(path); - - var json = JsonSerializer.Deserialize(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); - - } - - } - - } - - - - [TestMethod] - - 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() - - }; - - - - 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() - - }; - - 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); - - } - - } - - } - - - - [TestMethod] - - 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"); - - - - + 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() - - }; - - - - 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() - - }; - - 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(); - - - - 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; - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsPropsFileTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsPropsFileTest.cs index bc67a5870403..a3609dc8cd18 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsPropsFileTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetEndpointsPropsFileTest.cs @@ -2,484 +2,170 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Net; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - using NuGet.ContentModel; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; - - - - [TestClass] public class GenerateStaticWebAssetEndpointsPropsFileTest - - { - - [TestMethod] - - public void Generates_ValidEndpointsDefinitions() - - { - - // Arrange - - var file = Path.GetTempFileName(); - - var expectedDocument = """ - - - - - - - - $([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)..\staticwebassets\js\sample.js)) - - - - - - - - - - - - - - """; - - - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new GenerateStaticWebAssetEndpointsPropsFile - - { - - BuildEngine = buildEngine.Object, - - StaticWebAssets = - - [ - - CreateStaticWebAsset( - - Path.Combine("wwwroot","js","sample.js"), - - "MyLibrary", - - "Discovered", - - Path.Combine("js", "sample.js"), - - "All", - - "All") - - ], - - StaticWebAssetEndpoints = - - [ - - CreateStaticWebAssetEndpoint( - - Path.Combine("js", "sample.js"), - - Path.GetFullPath(Path.Combine("wwwroot","js","sample.js")), - - [ - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Content-Length", - - Value = "10" - - } - - ], - - [ - - new StaticWebAssetEndpointSelector - - { - - Name = "Content-Encoding", - - Value = "gzip", - - Quality = "0.1" - - } - - ], - - [ - - new StaticWebAssetEndpointProperty - - { - - Name = "integrity", - - Value = "__integrity__" - - } - - ]) - - ], - - PackagePathPrefix = "staticwebassets", - - TargetPropsFilePath = file - - }; - - - - // Act - - try - - { - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - new FileInfo(file).Should().Exist(); - - var document = File.ReadAllText(file); - - document.Should().BeVisuallyEquivalentTo(expectedDocument); - - } - - finally - - { - - if (File.Exists(file)) - - { - - try - - { - - File.Delete(file); - - } - - catch - - { - - } - - } - - } - - } - - - - [TestMethod] - - public void Fails_WhenEndpointWithoutAssetExists() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new GenerateStaticWebAssetEndpointsPropsFile - - { - - BuildEngine = buildEngine.Object, - - StaticWebAssets = [], - - StaticWebAssetEndpoints = - - [ - - CreateStaticWebAssetEndpoint( - - Path.Combine("js", "sample.js").Replace('\\', '/'), - - Path.GetFullPath(Path.Combine("wwwroot","js","sample.js")), - - [ - - new StaticWebAssetEndpointResponseHeader - - { - - Name = "Content-Length", - - Value = "10" - - } - - ], - - [ - - new StaticWebAssetEndpointSelector - - { - - Name = "Content-Encoding", - - Value = "gzip", - - Quality = "0.1" - - } - - ], - - [ - - new StaticWebAssetEndpointProperty - - { - - Name = "integrity", - - Value = "__integrity__" - - } - - ]) - - ], - - PackagePathPrefix = "staticwebassets", - - TargetPropsFilePath = Path.GetTempFileName(), - - }; - - - - // Act - - var result = task.Execute(); - - - - result.Should().BeFalse(); - - errorMessages.Should().ContainSingle(); - - errorMessages[0].Should().Be($"""The asset file '{Path.GetFullPath(Path.Combine("wwwroot", "js", "sample.js"))}' specified in the endpoint '{Path.Combine("js","sample.js").Replace('\\', '/')}' does not exist."""); - - } [TestMethod] @@ -531,164 +217,58 @@ public void Execute_RelativeTargetPropsFilePath_ResolvesAgainstProjectDirectory_ } private static ITaskItem CreateStaticWebAsset( - - string itemSpec, - - string sourceId, - - string sourceType, - - string relativePath, - - string assetKind, - - string assetMode) - - { - - var result = new StaticWebAsset() - - { - - Identity = Path.GetFullPath(itemSpec), - - SourceId = sourceId, - - SourceType = sourceType, - - ContentRoot = Directory.GetCurrentDirectory(), - - BasePath = "base", - - RelativePath = relativePath, - - AssetKind = assetKind, - - AssetMode = assetMode, - - AssetRole = "Primary", - - RelatedAsset = "", - - AssetTraitName = "", - - AssetTraitValue = "", - - CopyToOutputDirectory = "", - - CopyToPublishDirectory = "", - - OriginalItemSpec = itemSpec, - - // Add these to avoid accessing the disk to compute them - - Integrity = "integrity", - - Fingerprint = "fingerprint", - - FileLength = 10, - - LastWriteTime = DateTime.UtcNow, - - }; - - - - result.ApplyDefaults(); - - result.Normalize(); - - - - return result.ToTaskItem(); - - } - - - - private static ITaskItem CreateStaticWebAssetEndpoint( - - string route, - - string assetFile, - - StaticWebAssetEndpointResponseHeader[] responseHeaders = null, - - StaticWebAssetEndpointSelector[] responseSelector = null, - - StaticWebAssetEndpointProperty[] properties = null) - - { - - return new StaticWebAssetEndpoint - - { - - Route = route, - - AssetFile = Path.GetFullPath(assetFile), - - ResponseHeaders = responseHeaders ?? [], - - EndpointProperties = properties ?? [], - - Selectors = responseSelector ?? [] - - }.ToTaskItem(); - - } private static void WithDecoyCwdAndProjectDirectory(Action body) @@ -715,5 +295,3 @@ private static void WithDecoyCwdAndProjectDirectory(Action body) } } } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsDevelopmentManifestTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsDevelopmentManifestTest.cs index 6bae1d09f433..e16a9d7c3203 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsDevelopmentManifestTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsDevelopmentManifestTest.cs @@ -2,2266 +2,761 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Moq; - - using static Microsoft.AspNetCore.StaticWebAssets.Tasks.GenerateStaticWebAssetsDevelopmentManifest; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class GenerateStaticWebAssetsDevelopmentManifestTest - - { - - [TestMethod] - - public void SkipsManifestGenerationWhen_ThereAreNoAssetsNorDiscoveryPatterns() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - Assets = Array.Empty(), - - DiscoveryPatterns = Array.Empty() - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - messages.Should().HaveCount(1); - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_IncludesBuildAssets() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - ("index.html", CreateMatchNode(0, "index.html"))), - - Environment.CurrentDirectory); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - }; - - - - var assets = new[] { CreateAsset("index.html", "index.html", assetKind: StaticWebAsset.AssetKinds.Build) }; - - var patterns = Array.Empty(); - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - - - [TestMethod] - - [DataRow("#[.{fingerprint}]?", "index.html", "optional.html")] - - [DataRow("#[.{fingerprint}]!", "index.fingerprint.html", "preferred.html")] - - [DataRow("#[.{fingerprint}]", "index.fingerprint.html", "required.html")] - - public void ComputeDevelopmentManifest_ReplacesAssetTokens(string fingerprintExpression, string path, string fileName) - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - (path, CreateMatchNode(0, path))), - - Environment.CurrentDirectory); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - }; - - - - var assets = new[] { CreateAsset(fileName, $"index{fingerprintExpression}.html", assetKind: StaticWebAsset.AssetKinds.All) }; - - var patterns = Array.Empty(); - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - - - [TestMethod] - - [DataRow("#[.{fingerprint}]?", "index.html", "optional.html")] - - [DataRow("#[.{fingerprint}]!", "index.fingerprint.html", "preferred.html")] - - [DataRow("#[.{fingerprint}]", "index.fingerprint.html", "required.html")] - - public void ComputeDevelopmentManifest_ReplacesAssetTokens_FileExists(string fingerprintExpression, string path, string subPath) - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - (path, CreateMatchNode(0, subPath))), - - Environment.CurrentDirectory); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - }; - - - - var assets = new[] { CreateAsset(subPath, $"index{fingerprintExpression}.html", assetKind: StaticWebAsset.AssetKinds.All) }; - - var patterns = Array.Empty(); - - - - var fileName = Path.Combine(Environment.CurrentDirectory, subPath); - - try - - { - - File.WriteAllText(fileName, "content"); - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - finally - - { - - if (File.Exists(fileName)) - - { - - File.Delete(fileName); - - } - - } - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_UsesIdentitySubpath_WhenFileExists_AndContentRoot_IsPrefix() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - ("_framework", - - CreateIntermediateNode( - - ("dotnet.native.fingerprint.js.gz", CreateMatchNode(0, "blob-hash.gz"))))), - - Environment.CurrentDirectory); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - }; - - - - var fileName = Path.Combine(Environment.CurrentDirectory, "blob-hash.gz"); - - try - - { - - File.WriteAllText(fileName, "content"); - - var assets = new[] { CreateAsset( - - fileName, - - $$"""_framework/dotnet.native#[.{fingerprint}]!.js.gz""", - - contentRoot: Environment.CurrentDirectory, - - assetKind: StaticWebAsset.AssetKinds.All) }; - - var patterns = Array.Empty(); - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - finally - - { - - if (File.Exists(fileName)) - - { - - File.Delete(fileName); - - } - - } - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_UsesRelativePath_ReplacesAssetTokens_WhenFileDoesNotExist_AtIdentity() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - ("_framework", - - CreateIntermediateNode( - - ("dotnet.native.fingerprint.js", CreateMatchNode(0, "_framework/dotnet.native.fingerprint.js"))))), - - Environment.CurrentDirectory); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - }; - - - - var fileName = Path.Combine(Environment.CurrentDirectory, "dotnet.native.js"); - - var assets = new[] { CreateAsset( - - fileName, - - $$"""_framework/dotnet.native#[.{fingerprint}]!.js""", - - contentRoot: Environment.CurrentDirectory, - - assetKind: StaticWebAsset.AssetKinds.All) }; - - var patterns = Array.Empty(); - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_IncludesAllAssets() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - ("index.html", CreateMatchNode(0, "index.html"))), - - Environment.CurrentDirectory); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - }; - - - - var assets = new[] { CreateAsset("index.html", "index.html", assetKind: StaticWebAsset.AssetKinds.All) }; - - var patterns = Array.Empty(); - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_ExcludesPublishAssets() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode()); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - }; - - - - var assets = new[] { CreateAsset("index.html", "index.html", assetKind: StaticWebAsset.AssetKinds.Publish) }; - - var patterns = Array.Empty(); - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_ExcludesReferenceAssets() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode()); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - Source = "CurrentProjectId" - - }; - - - - var assets = new[] { CreateAsset("index.html", "index.html", assetMode: StaticWebAsset.AssetModes.Reference) }; - - var patterns = Array.Empty(); - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_PrefersBuildAssetsOverAllAssets() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - ("index.html", CreateMatchNode(0, "index.build.html"))), - - Environment.CurrentDirectory); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - Source = "CurrentProjectId" - - }; - - - - var assets = new[] { - - CreateAsset("index.build.html", "index.html", assetKind: StaticWebAsset.AssetKinds.Build), - - CreateAsset("index.html", "index.html", assetKind: StaticWebAsset.AssetKinds.All) - - }; - - var patterns = Array.Empty(); - - - - var fileName = Path.Combine(Environment.CurrentDirectory, "index.build.html"); - - try - - { - - File.WriteAllText(fileName, "content"); - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - finally - - { - - if (File.Exists(fileName)) - - { - - File.Delete(fileName); - - } - - } - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_UsesIdentityWhenContentRootStartsByIdentity() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var filePath = Path.Combine("some", "subfolder", "index.build.html"); - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - ("index.html", CreateMatchNode(0, StaticWebAsset.Normalize(filePath)))), - - Environment.CurrentDirectory); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - Source = "CurrentProjectId" - - }; - - - - var assets = new[] { - - CreateAsset(filePath, "index.html"), - - }; - - var patterns = Array.Empty(); - - - - try - - { - - Directory.CreateDirectory(Path.GetDirectoryName(filePath)); - - File.WriteAllText(filePath, "content"); - - - - - // Act - - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - + // Act + var manifest = task.ComputeDevelopmentManifest(assets, patterns); // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - finally - - { - - if (File.Exists(filePath)) - - { - - File.Delete(filePath); - - } - - } - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_UsesRelativePathContentRootDoesNotStartByIdentity() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - ("index.html", CreateMatchNode(0, "index.html"))), - - Path.GetFullPath(Path.Combine("bin", "debug", "wwwroot"))); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - Source = "CurrentProjectId" - - }; - - - - var assets = new[] { - - CreateAsset(Path.Combine("some", "subfolder", "index.build.html"), "index.html", contentRoot: Path.Combine("bin", "debug", "wwwroot")), - - }; - - var patterns = Array.Empty(); - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_MapsPatternsFromCurrentProject() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode() - - .AddPatterns((0, "**", 0)), - - Path.GetFullPath("wwwroot")); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - Source = "CurrentProjectId" - - }; - - - - var assets = Array.Empty(); - - var patterns = new[] { CreatePattern() }; - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_MapsPatternsFromOtherProjects() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - ("_other", CreateIntermediateNode( - - ("_project", CreateIntermediateNode().AddPatterns((0, "**", 2)))))), - - Path.GetFullPath("wwwroot")); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - Source = "CurrentProjectId" - - }; - - - - var assets = Array.Empty(); - - var patterns = new[] { CreatePattern(basePath: "_other/_project", source: "OtherProject") }; - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_CanMapMultiplePatternsOnSameNode() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - ("_other", CreateIntermediateNode( - - ("_project", CreateIntermediateNode().AddPatterns( - - (0, "*.js", 2), - - (0, "*.css", 2)))))), - - Path.GetFullPath("wwwroot")); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - Source = "CurrentProjectId" - - }; - - - - var assets = Array.Empty(); - - var patterns = new[] - - { - - CreatePattern(basePath: "_other/_project", source: "OtherProject", pattern: "*.js"), - - CreatePattern(basePath: "_other/_project", source: "OtherProject", pattern: "*.css") - - }; - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_CanMapMultiplePatternsOnSameNodeWithDifferentContentRoots() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - ("_other", CreateIntermediateNode( - - ("_project", CreateIntermediateNode().AddPatterns( - - (0, "*.css", 2), - - (1, "*.js", 2)))))), - - Path.GetFullPath("wwwroot"), - - Path.GetFullPath("styles")); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - Source = "CurrentProjectId" - - }; - - - - var assets = Array.Empty(); - - var patterns = new[] - - { - - CreatePattern(basePath: "_other/_project", source: "OtherProject", pattern: "*.js"), - - CreatePattern(basePath: "_other/_project", source: "OtherProject", pattern: "*.css", contentRoot: Path.GetFullPath("styles")) - - }; - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_MultipleAssetsSameContentRoot() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - ("css", CreateIntermediateNode(("site.css", CreateMatchNode(0, "css/site.css")))), - - ("js", CreateIntermediateNode(("index.js", CreateMatchNode(0, "js/index.js"))))), - - Environment.CurrentDirectory); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - Source = "CurrentProjectId" - - }; - - - - var assets = new[] - - { - - CreateAsset(Path.Combine(Environment.CurrentDirectory, "css", "site.css"), "css/site.css"), - - CreateAsset(Path.Combine(Environment.CurrentDirectory, "js", "index.js"), "js/index.js") - - }; - - - - var patterns = Array.Empty(); - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_DifferentCasingEndUpInDifferentNodes() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - ("css", CreateIntermediateNode(("site.css", CreateMatchNode(0, "css/site.css")))), - - ("CSS", CreateIntermediateNode(("site.css", CreateMatchNode(0, "CSS/site.css"))))), - - Environment.CurrentDirectory); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - Source = "CurrentProjectId" - - }; - - - - - var assets = new[] - - - { - - + var assets = new[] + { CreateAsset(Path.Combine(Environment.CurrentDirectory, "css", "site.css"), "css/site.css"), - - CreateAsset(Path.Combine(Environment.CurrentDirectory, "CSS", "site.css"), "CSS/site.css"), - - }; - - - - var patterns = Array.Empty(); - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - - - [TestMethod] - - public void ComputeDevelopmentManifest_UsesBasePathForAssetsFromDifferentProjects() - - { - - // Arrange - - var messages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => messages.Add(args.Message)); - - - - var expectedManifest = CreateExpectedManifest( - - CreateIntermediateNode( - - ("css", CreateIntermediateNode(("site.css", CreateMatchNode(0, "css/site.css")))), - - ("_content", CreateIntermediateNode( - - ("OtherProject", CreateIntermediateNode( - - ("CSS", CreateIntermediateNode(("site.css", CreateMatchNode(1, "CSS/site.css"))))))))), - - Environment.CurrentDirectory, - - Path.GetFullPath("otherProject")); - - - - var task = new GenerateStaticWebAssetsDevelopmentManifest() - - { - - BuildEngine = buildEngine.Object, - - Source = "CurrentProjectId" - - }; - - - - var assets = new[] - - { - - CreateAsset(Path.Combine(Environment.CurrentDirectory, "css", "site.css"), "css/site.css"), - - CreateAsset( - - Path.Combine(Environment.CurrentDirectory, "CSS", "site.css"), - - "CSS/site.css", - - basePath: "_content/OtherProject", - - sourceType: "Project", - - contentRoot: Path.GetFullPath("otherProject")), - - }; - - - - var patterns = Array.Empty(); - - - - // Act - - var manifest = task.ComputeDevelopmentManifest(assets, patterns); - - - - // Assert - - manifest.Should().BeEquivalentTo(expectedManifest); - - } - - - - private static StaticWebAssetsDiscoveryPattern CreatePattern( - - string name = null, - - string contentRoot = null, - - string pattern = null, - - string basePath = null, - - string source = null) => - - new() - - { - - Name = name ?? "CurrentProjectId\\wwwroot", - - Pattern = pattern ?? "**", - - BasePath = basePath ?? "_content/CurrentProjectId", - - Source = source ?? "CurrentProjectId", - - ContentRoot = StaticWebAsset.NormalizeContentRootPath(contentRoot ?? Path.Combine(Environment.CurrentDirectory, "wwwroot")) - - }; - - - - private static StaticWebAssetsDevelopmentManifest CreateExpectedManifest(StaticWebAssetNode root, params string[] contentRoots) - - { - - return new StaticWebAssetsDevelopmentManifest() - - { - - ContentRoots = contentRoots.Select(cr => StaticWebAsset.NormalizeContentRootPath(cr)).ToArray(), - - Root = root - - }; - - } - - - - private static StaticWebAssetNode CreateIntermediateNode(params (string key, StaticWebAssetNode node)[] children) => new() - - { - - Children = children.Length == 0 ? null : children.ToDictionary(pair => pair.key, pair => pair.node) - - }; - - - - private static StaticWebAssetNode CreateMatchNode(int index, string subpath) => new() - - { - - Asset = new StaticWebAssetMatch { ContentRootIndex = index, SubPath = subpath } - - }; - - - - private static StaticWebAsset CreateAsset( - - string identity, - - string relativePath, - - string assetKind = default, - - string assetMode = default, - - string sourceId = default, - - string sourceType = default, - - string basePath = default, - - string contentRoot = default) - - { - - return new StaticWebAsset() - - { - - Identity = Path.GetFullPath(identity), - - SourceId = sourceId ?? "CurrentProjectId", - - SourceType = sourceType ?? StaticWebAsset.SourceTypes.Discovered, - - BasePath = basePath ?? "_content/Base", - - RelativePath = relativePath, - - AssetKind = assetKind ?? StaticWebAsset.AssetKinds.All, - - AssetMode = assetMode ?? StaticWebAsset.AssetModes.All, - - AssetRole = StaticWebAsset.AssetRoles.Primary, - - Fingerprint = "fingerprint", - - ContentRoot = StaticWebAsset.NormalizeContentRootPath(contentRoot ?? Environment.CurrentDirectory), - - OriginalItemSpec = identity - - }; - - } - - } - - - - internal static class StaticWebAssetNodeTestExtensions - - { - - public static StaticWebAssetNode AddPatterns(this StaticWebAssetNode node, params (int contentRoot, string pattern, int depth)[] patterns) - - { - - node.Patterns = patterns.Select(p => new StaticWebAssetPattern { ContentRootIndex = p.contentRoot, Pattern = p.pattern, Depth = p.depth }).ToArray(); - - return node; - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsManifestTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsManifestTest.cs index 1ebf7a66c8c4..8bd884e9d8d6 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsManifestTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsManifestTest.cs @@ -2,1366 +2,461 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class GenerateStaticWebAssetsManifestTest - - { - - public GenerateStaticWebAssetsManifestTest() - - { - - Directory.CreateDirectory(Path.Combine(SdkTestContext.Current.TestExecutionDirectory, nameof(GenerateStaticWebAssetsManifestTest))); - - TempFilePath = Path.Combine(SdkTestContext.Current.TestExecutionDirectory, nameof(GenerateStaticWebAssetsManifestTest), Guid.NewGuid().ToString("N") + ".json"); - - } - - - - public string TempFilePath { get; } - - - - [TestMethod] - - public void CanGenerateEmptyManifest() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(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(), - - Endpoints = Array.Empty(), - - ReferencedProjectsConfigurations = Array.Empty(), - - DiscoveryPatterns = Array.Empty(), - - 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"); - - } - - - - [TestMethod] - - public void GeneratesManifestWithAssets() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(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(), - - DiscoveryPatterns = Array.Empty(), - - 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> GeneratesManifestFailsWhenInvalidAssetsAreProvidedData - - { - - get - - { - - var theoryData = new TheoryData> - - { - - 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; - - } - - } - - - - [TestMethod] - - [DynamicData(nameof(GeneratesManifestFailsWhenInvalidAssetsAreProvidedData))] - - public void GeneratesManifestFailsWhenInvalidAssetsAreProvided(Action change) - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(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(), - - ReferencedProjectsConfigurations = Array.Empty(), - - DiscoveryPatterns = Array.Empty(), - - BasePath = "/", - - Source = "MyProject", - - ManifestType = "Build", - - Mode = "Default", - - ManifestPath = TempFilePath, - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(false); - - } - - - - public static TheoryData GeneratesManifestFailsWhenTwoAssetsEndUpOnTheSamePathData - - { - - get - - { - - var data = new TheoryData - - { - - // 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; - - } - - } - - - - [TestMethod] - - [DynamicData(nameof(GeneratesManifestFailsWhenTwoAssetsEndUpOnTheSamePathData))] - - public void GeneratesManifestFailsWhenTwoAssetsEndUpOnTheSamePath(StaticWebAsset first, StaticWebAsset second) - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - - .Callback(args => errorMessages.Add(args.Message)); - - - - + .Callback(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(), - - ReferencedProjectsConfigurations = Array.Empty(), - - DiscoveryPatterns = Array.Empty(), - - BasePath = "/", - - Source = "MyProject", - - ManifestType = "Build", - - Mode = "Default", - - ManifestPath = TempFilePath, - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(false); - - } - - - - - - [TestMethod] - - public void GeneratesManifestWithReferencedProjectConfigurations() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(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(), - - Endpoints = Array.Empty(), - - ReferencedProjectsConfigurations = new[] { projectReference.ToTaskItem() }, - - DiscoveryPatterns = Array.Empty(), - - 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); - - } - - - - [TestMethod] - - public void GeneratesManifestWithDiscoveryPatterns() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(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(), - - Endpoints = Array.Empty(), - - ReferencedProjectsConfigurations = Array.Empty(), - - 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; - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsPropsFileTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsPropsFileTest.cs index b216474546b5..2a06566c1d55 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsPropsFileTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateStaticWebAssetsPropsFileTest.cs @@ -2,2623 +2,880 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - using NuGet.Packaging.Core; - - - - namespace Microsoft.NET.Sdk.Razor.Test - - { - - [TestClass] - public class GenerateStaticWebAssetsPropsFileTest - - { - - [TestMethod] - - public void Fails_WhenStaticWebAsset_DoesNotContainSourceType() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new GenerateStaticWebAssetsPropsFile - - { - - BuildEngine = buildEngine.Object, - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary - - { - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = Path.Combine("js", "sample.js"), - - }) - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - var expectedError = $"Missing required metadata 'SourceType' for '{Path.Combine("wwwroot", "js", "sample.js")}'."; - - errorMessages.Should().ContainSingle(message => message == expectedError); - - } - - - - [TestMethod] - - public void Fails_WhenStaticWebAsset_DoesNotContainSourceId() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new GenerateStaticWebAssetsPropsFile - - { - - BuildEngine = buildEngine.Object, - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = Path.Combine("js", "sample.js"), - - }) - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - var expectedError = $"Missing required metadata 'SourceId' for '{Path.Combine("wwwroot", "js", "sample.js")}'."; - - errorMessages.Should().ContainSingle(message => message == expectedError); - - } - - - - [TestMethod] - - public void Fails_WhenStaticWebAsset_DoesNotContainContentRoot() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new GenerateStaticWebAssetsPropsFile - - { - - BuildEngine = buildEngine.Object, - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = Path.Combine("js", "sample.js"), - - }) - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - var expectedError = $"Missing required metadata 'ContentRoot' for '{Path.Combine("wwwroot", "js", "sample.js")}'."; - - errorMessages.Should().ContainSingle(message => message == expectedError); - - } - - - - [TestMethod] - - public void Fails_WhenStaticWebAsset_DoesNotContainBasePath() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new GenerateStaticWebAssetsPropsFile - - { - - BuildEngine = buildEngine.Object, - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["RelativePath"] = Path.Combine("js", "sample.js"), - - }) - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - var expectedError = $"Missing required metadata 'BasePath' for '{Path.Combine("wwwroot", "js", "sample.js")}'."; - - errorMessages.Should().ContainSingle(message => message == expectedError); - - } - - - - [TestMethod] - - public void Fails_WhenStaticWebAsset_DoesNotContainRelativePath() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new GenerateStaticWebAssetsPropsFile - - { - - BuildEngine = buildEngine.Object, - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }) - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - var expectedError = $"Missing required metadata 'RelativePath' for '{Path.Combine("wwwroot", "js", "sample.js")}'."; - - errorMessages.Should().ContainSingle(message => message == expectedError); - - } - - - - [TestMethod] - - public void Fails_WhenStaticWebAsset_HasInvalidSourceType() - - { - - // Arrange - - - - var expectedError = $"Static web asset '{Path.Combine("wwwroot", "css", "site.css")}' has invalid source type 'Package'."; - - - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new GenerateStaticWebAssetsPropsFile - - { - - BuildEngine = buildEngine.Object, - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = Path.Combine("js", "sample.js"), - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }), - - CreateItem(Path.Combine("wwwroot","css","site.css"), new Dictionary - - { - - ["SourceType"] = "Package", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = Path.Combine("css", "site.css"), - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }) - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - errorMessages.Should().ContainSingle(message => message == expectedError); - - } - - - - [TestMethod] - - public void Fails_WhenStaticWebAsset_HaveDifferentSourceId() - - { - - // Arrange - - var expectedError = "Static web assets have different 'SourceId' metadata values " + - - "'MyLibrary' and 'MyLibrary2' " + - - $"for '{Path.Combine("wwwroot", "js", "sample.js")}' and '{Path.Combine("wwwroot", "css", "site.css")}'."; - - - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new GenerateStaticWebAssetsPropsFile - - { - - BuildEngine = buildEngine.Object, - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = Path.Combine("js", "sample.js"), - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }), - - CreateItem(Path.Combine("wwwroot","css","site.css"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary2", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = Path.Combine("css", "site.css"), - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }) - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - errorMessages.Should().ContainSingle(message => message == expectedError); - - } - - - - [TestMethod] - - public void WritesPropsFile_WhenThereIsAtLeastOneStaticAsset() - - { - - // Arrange - - var file = Path.GetTempFileName(); - - var expectedDocument = @" - - - - - - Package - - MyLibrary - - $(MSBuildThisFileDirectory)..\staticwebassets\ - - _content/mylibrary - - js/sample.js - - All - - All - - Primary - - - - - - - - sample-fingerprint - - sample-integrity - - Never - - PreserveNewest - - 10 - - Thu, 15 Nov 1990 00:00:00 GMT - - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\js\sample.js')) - - - - - - "; - - - - try - - { - - var buildEngine = new Mock(); - - - - var task = new GenerateStaticWebAssetsPropsFile - - { - - BuildEngine = buildEngine.Object, - - TargetPropsFilePath = file, - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = Path.Combine("js", "sample.js").Replace("\\","/"), - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["Fingerprint"] = "sample-fingerprint", - - ["Integrity"] = "sample-integrity", - - ["OriginalItemSpec"] = Path.Combine("wwwroot","js","sample.js"), - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["FileLength"] = "10", - - ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) - - }), - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var document = File.ReadAllText(file); - - document.Should().Contain(expectedDocument); - - } - - finally - - { - - if (File.Exists(file)) - - { - - File.Delete(file); - - } - - } - - } - - - - [TestMethod] - - public void WritesIndividualItems_WithTheirRespectiveBaseAndRelativePaths() - - { - - // Arrange - - var file = Path.GetTempFileName(); - - var expectedDocument = @" - - - - - - Package - - MyLibrary - - $(MSBuildThisFileDirectory)..\staticwebassets\ - - / - - App.styles.css - - All - - All - - Primary - - - - - - - - styles-fingerprint - - styles-integrity - - Never - - PreserveNewest - - 10 - - Thu, 15 Nov 1990 00:00:00 GMT - - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\App.styles.css')) - - - - - - Package - - MyLibrary - - $(MSBuildThisFileDirectory)..\staticwebassets\ - - _content/mylibrary - - js/sample.js - - All - - All - - Primary - - - - - - - - sample-fingerprint - - sample-integrity - - Never - - PreserveNewest - - 10 - - Thu, 15 Nov 1990 00:00:00 GMT - - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\js\sample.js')) - - - - - - "; - - - - try - - { - - var buildEngine = new Mock(); - - - - var task = new GenerateStaticWebAssetsPropsFile - - { - - BuildEngine = buildEngine.Object, - - TargetPropsFilePath = file, - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","js","sample.js"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = Path.Combine("js", "sample.js").Replace("\\","/"), - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["OriginalItemSpec"] = Path.Combine("wwwroot","js","sample.js"), - - ["Fingerprint"] = "sample-fingerprint", - - ["Integrity"] = "sample-integrity", - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["FileLength"] = "10", - - ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) - - }), - - CreateItem(Path.Combine("wwwroot","App.styles.css"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "/", - - ["RelativePath"] = "App.styles.css", - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["OriginalItemSpec"] = Path.Combine("wwwroot","App.styles.css"), - - ["Fingerprint"] = "styles-fingerprint", - - ["Integrity"] = "styles-integrity", - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["FileLength"] = "10", - - ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) - - }), - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - Assert.IsTrue(result); - - var document = File.ReadAllText(file); - - Assert.AreEqual(expectedDocument.ReplaceLineEndings(), document.ReplaceLineEndings()); - - } - - finally - - { - - if (File.Exists(file)) - - { - - File.Delete(file); - - } - - } - - } - - - - [TestMethod] - - public void WritesFrameworkSourceType_WhenAssetMatchesFrameworkPattern() - - { - - // Arrange - - var file = Path.GetTempFileName(); - - var expectedDocument = @" - - - - - - Package - - MyLibrary - - $(MSBuildThisFileDirectory)..\staticwebassets\ - - _content/mylibrary - - css/site.css - - All - - All - - Primary - - - - - - - - css-fingerprint - - css-integrity - - Never - - PreserveNewest - - 10 - - Thu, 15 Nov 1990 00:00:00 GMT - - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\css\site.css')) - - - - - - Framework - - MyLibrary - - $(MSBuildThisFileDirectory)..\staticwebassets\ - - _content/mylibrary - - js/framework.js - - All - - All - - Primary - - - - - - - - js-fingerprint - - js-integrity - - Never - - PreserveNewest - - 10 - - Thu, 15 Nov 1990 00:00:00 GMT - - $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\staticwebassets\js\framework.js')) - - - - - - "; - - - - try - - { - - var buildEngine = new Mock(); - - - - var task = new GenerateStaticWebAssetsPropsFile - - { - - BuildEngine = buildEngine.Object, - - TargetPropsFilePath = file, - - FrameworkPattern = "**/*.js", - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","js","framework.js"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = "js/framework.js", - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["Fingerprint"] = "js-fingerprint", - - ["Integrity"] = "js-integrity", - - ["OriginalItemSpec"] = Path.Combine("wwwroot","js","framework.js"), - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["FileLength"] = "10", - - ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) - - }), - - CreateItem(Path.Combine("wwwroot","css","site.css"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = "css/site.css", - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["Fingerprint"] = "css-fingerprint", - - ["Integrity"] = "css-integrity", - - ["OriginalItemSpec"] = Path.Combine("wwwroot","css","site.css"), - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["FileLength"] = "10", - - ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) - - }), - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - Assert.IsTrue(result); - - var document = File.ReadAllText(file); - - Assert.AreEqual(expectedDocument.ReplaceLineEndings(), document.ReplaceLineEndings()); - - } - - finally - - { - - if (File.Exists(file)) - - { - - File.Delete(file); - - } - - } - - } - - - - [TestMethod] - - public void WritesAllAsPackage_WhenFrameworkPatternIsNull() - - { - - // Arrange - - var file = Path.GetTempFileName(); - - - - try - - { - - var buildEngine = new Mock(); - - - - var task = new GenerateStaticWebAssetsPropsFile - - { - - BuildEngine = buildEngine.Object, - - TargetPropsFilePath = file, - - FrameworkPattern = null, - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","js","app.js"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = "js/app.js", - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["Fingerprint"] = "fp", - - ["Integrity"] = "int", - - ["OriginalItemSpec"] = Path.Combine("wwwroot","js","app.js"), - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["FileLength"] = "10", - - ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) - - }), - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var document = File.ReadAllText(file); - - document.Should().Contain("Package"); - - document.Should().NotContain("Framework"); - - } - - finally - - { - - if (File.Exists(file)) - - { - - File.Delete(file); - - } - - } - - } - - - - [TestMethod] - - public void WritesFrameworkSourceType_WithMultiplePatterns() - - { - - // Arrange - - var file = Path.GetTempFileName(); - - - - try - - { - - var buildEngine = new Mock(); - - - - var task = new GenerateStaticWebAssetsPropsFile - - { - - BuildEngine = buildEngine.Object, - - TargetPropsFilePath = file, - - FrameworkPattern = "**/*.js;**/*.css", - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","js","app.js"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = "js/app.js", - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["Fingerprint"] = "fp1", - - ["Integrity"] = "int1", - - ["OriginalItemSpec"] = Path.Combine("wwwroot","js","app.js"), - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["FileLength"] = "10", - - ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) - - }), - - CreateItem(Path.Combine("wwwroot","css","site.css"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = "css/site.css", - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["Fingerprint"] = "fp2", - - ["Integrity"] = "int2", - - ["OriginalItemSpec"] = Path.Combine("wwwroot","css","site.css"), - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["FileLength"] = "10", - - ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) - - }), - - CreateItem(Path.Combine("wwwroot","images","logo.png"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = "images/logo.png", - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["Fingerprint"] = "fp3", - - ["Integrity"] = "int3", - - ["OriginalItemSpec"] = Path.Combine("wwwroot","images","logo.png"), - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["FileLength"] = "10", - - ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) - - }), - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var document = File.ReadAllText(file); - - // JS and CSS files should be Framework, PNG should be Package - - var lines = document.Split('\n').Select(l => l.Trim()).ToList(); - - var sourceTypeLines = lines.Where(l => l.StartsWith("")).ToList(); - - // Order is: css/site.css, images/logo.png, js/app.js (sorted by BasePath then RelativePath) - - sourceTypeLines.Should().HaveCount(3); - - sourceTypeLines[0].Should().Be("Framework"); // css/site.css - - sourceTypeLines[1].Should().Be("Package"); // images/logo.png - - sourceTypeLines[2].Should().Be("Framework"); // js/app.js - - } - - finally - - { - - if (File.Exists(file)) - - { - - File.Delete(file); - - } - - } - - } - - - - [TestMethod] - - public void WritesAllAsPackage_WhenFrameworkPatternMatchesNothing() - - { - - // Arrange - - var file = Path.GetTempFileName(); - - - - try - - { - - var buildEngine = new Mock(); - - - - var task = new GenerateStaticWebAssetsPropsFile - - { - - BuildEngine = buildEngine.Object, - - TargetPropsFilePath = file, - - FrameworkPattern = "**/*.wasm", - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","js","app.js"), new Dictionary - - { - - ["SourceType"] = "Discovered", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = @"$(MSBuildThisFileDirectory)..\staticwebassets", - - ["BasePath"] = "_content/mylibrary", - - ["RelativePath"] = "js/app.js", - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["Fingerprint"] = "fp", - - ["Integrity"] = "int", - - ["OriginalItemSpec"] = Path.Combine("wwwroot","js","app.js"), - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["FileLength"] = "10", - - ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat) - - }), - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var document = File.ReadAllText(file); - - document.Should().Contain("Package"); - - document.Should().NotContain("Framework"); - - } - - finally - - { - - if (File.Exists(file)) - - { - - File.Delete(file); - - } - - } - - } - - - - private static TaskItem CreateItem( - - string spec, - - IDictionary metadata) - - { - - var result = new TaskItem(spec); - - foreach (var (key, value) in metadata) - - { - - result.SetMetadata(key, value); - - } - - - - return result; - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateV1StaticWebAssetsManifestTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateV1StaticWebAssetsManifestTest.cs index f65a05b9f6be..de2ee9d92ab8 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateV1StaticWebAssetsManifestTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/GenerateV1StaticWebAssetsManifestTest.cs @@ -2,820 +2,279 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.NET.Sdk.Razor.Test - - { - - [TestClass] - public class GenerateV1StaticWebAssetsManifestTest - - { - - [TestMethod] - - public void ReturnsError_WhenBasePathIsMissing() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new GenerateV1StaticWebAssetsManifest - - { - - BuildEngine = buildEngine.Object, - - ContentRootDefinitions = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot", "sample.js"), new Dictionary - - { - - ["ContentRoot"] = "/" - - }) - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - var expectedError = $"Missing required metadata 'BasePath' for '{Path.Combine("wwwroot", "sample.js")}'."; - - errorMessages.Should().ContainSingle(message => message == expectedError); - - } - - - - [TestMethod] - - public void ReturnsError_WhenContentRootIsMissing() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new GenerateV1StaticWebAssetsManifest - - { - - BuildEngine = buildEngine.Object, - - ContentRootDefinitions = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary - - { - - ["BasePath"] = "MyLibrary" - - }) - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - var expectedError = $"Missing required metadata 'ContentRoot' for '{Path.Combine("wwwroot", "sample.js")}'."; - - errorMessages.Should().ContainSingle(message => message == expectedError); - - } - - - - [TestMethod] - - public void AllowsMultipleContentRootsWithSameBasePath_ForTheSameSourceId() - - { - - // Arrange - - var file = Path.GetTempFileName(); - - var expectedDocument = $@" - - - - - - "; - - - - var buildEngine = new Mock(); - - - - var task = new GenerateV1StaticWebAssetsManifest - - { - - BuildEngine = buildEngine.Object, - - ContentRootDefinitions = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary - - { - - ["BasePath"] = "Blazor.Client", - - ["ContentRoot"] = Path.Combine(".", "nuget","Blazor.Client"), - - ["SourceId"] = "Blazor.Client" - - }), - - CreateItem(Path.Combine("wwwroot", "otherLib.js"), new Dictionary - - { - - ["BasePath"] = "Blazor.Client", - - ["ContentRoot"] = Path.Combine(".", "nuget", "bin","debug","netstandard2.1"), - - ["SourceId"] = "Blazor.Client" - - }) - - }, - - TargetManifestPath = file - - }; - - - - try - - { - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var document = File.ReadAllText(file); - - document.Should().Contain(expectedDocument); - - } - - finally - - { - - if (File.Exists(file)) - - { - - File.Delete(file); - - } - - } - - } - - - - [TestMethod] - - public void Generates_EmptyManifest_WhenNoItems_Passed() - - { - - // Arrange - - var file = Path.GetTempFileName(); - - var expectedDocument = @""; - - - - try - - { - - var buildEngine = new Mock(); - - - - var task = new GenerateV1StaticWebAssetsManifest - - { - - BuildEngine = buildEngine.Object, - - ContentRootDefinitions = new TaskItem[] { }, - - TargetManifestPath = file - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var document = File.ReadAllText(file); - - document.Should().Contain(expectedDocument); - - } - - finally - - { - - if (File.Exists(file)) - - { - - File.Delete(file); - - } - - } - - } - - - - [TestMethod] - - public void Generates_Manifest_WhenContentRootsAvailable() - - { - - // Arrange - - var file = Path.GetTempFileName(); - - var expectedDocument = $@" - - - - "; - - - - try - - { - - var buildEngine = new Mock(); - - - - var task = new GenerateV1StaticWebAssetsManifest - - { - - BuildEngine = buildEngine.Object, - - ContentRootDefinitions = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary - - { - - ["BasePath"] = "MyLibrary", - - ["ContentRoot"] = Path.Combine(".", "nuget", "MyLibrary", "razorContent"), - - ["SourceId"] = "MyLibrary" - - }), - - }, - - TargetManifestPath = file - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var document = File.ReadAllText(file); - - document.Should().Contain(expectedDocument); - - } - - finally - - { - - if (File.Exists(file)) - - { - - File.Delete(file); - - } - - } - - } - - - - [TestMethod] - - public void SkipsAdditionalElements_WithSameBasePathAndSameContentRoot() - - { - - // Arrange - - var file = Path.GetTempFileName(); - - var expectedDocument = $@" - - - - "; - - - - try - - { - - var buildEngine = new Mock(); - - - - var task = new GenerateV1StaticWebAssetsManifest - - { - - BuildEngine = buildEngine.Object, - - ContentRootDefinitions = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary - - { - - // Base path needs to be normalized to '/' as it goes in the url - - ["BasePath"] = "Base\\MyLibrary", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = Path.Combine(".", "nuget", "MyLibrary", "razorContent") - - }), - - // Comparisons are case insensitive - - CreateItem(Path.Combine("wwwroot, site.css"), new Dictionary - - { - - ["BasePath"] = "Base\\MyLIBRARY", - - ["SourceId"] = "MyLibrary", - - ["ContentRoot"] = Path.Combine(".", "nuget", "MyLIBRARY", "razorContent") - - }), - - }, - - TargetManifestPath = file - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var document = File.ReadAllText(file); - - document.Should().Contain(expectedDocument); - - } - - finally - - { - - if (File.Exists(file)) - - { - - File.Delete(file); - - } - - } - - } - - - - private static TaskItem CreateItem( - - string spec, - - IDictionary metadata) - - { - - var result = new TaskItem(spec); - - foreach (var (key, value) in metadata) - - { - - result.SetMetadata(key, value); - - } - - - - return result; - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/PathTokenizerTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/PathTokenizerTest.cs index d2e232176c09..03af8c66f0da 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/PathTokenizerTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/PathTokenizerTest.cs @@ -6,396 +6,136 @@ using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using System.Collections.Generic; - - using System.Linq; - - using System.Text; - - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - namespace Microsoft.AspNetCore.StaticWebAssets.Tasks.Test; - - - - [TestClass] public class PathTokenizerTest - - { - - [TestMethod] - - public void RootSeparator_ProducesEmptySegment() - - { - - var path = "/a/b/c"; - - var tokenizer = new PathTokenizer(path.AsMemory().Span); - - var segments = new List(); - - var collection = tokenizer.Fill(segments); - - Assert.AreEqual("a", collection[0].ToString()); - - Assert.AreEqual("b", collection[1].ToString()); - - Assert.AreEqual("c", collection[2].ToString()); - - } - - - - [TestMethod] - - public void NonRootSeparator_ProducesInitialSegment() - - { - - var path = "a/b/c"; - - var tokenizer = new PathTokenizer(path.AsMemory().Span); - - var segments = new List(); - - var collection = tokenizer.Fill(segments); - - Assert.AreEqual("a", collection[0].ToString()); - - Assert.AreEqual("b", collection[1].ToString()); - - Assert.AreEqual("c", collection[2].ToString()); - - } - - - - [TestMethod] - - public void NonRootSeparator_MatchesMultipleCharacters() - - { - - var path = "aa/b/c"; - - var tokenizer = new PathTokenizer(path.AsMemory().Span); - - var segments = new List(); - - var collection = tokenizer.Fill(segments); - - Assert.AreEqual("aa", collection[0].ToString()); - - Assert.AreEqual("b", collection[1].ToString()); - - Assert.AreEqual("c", collection[2].ToString()); - - } - - - - [TestMethod] - - public void NonRootSeparator_HandlesConsecutivePathSeparators() - - { - - var path = "aa//b/c"; - - var tokenizer = new PathTokenizer(path.AsMemory().Span); - - var segments = new List(); - - var collection = tokenizer.Fill(segments); - - Assert.AreEqual("aa", collection[0].ToString()); - - Assert.AreEqual("b", collection[1].ToString()); - - Assert.AreEqual("c", collection[2].ToString()); - - } - - - - [TestMethod] - - public void NonRootSeparator_HandlesFinalPathSeparator() - - { - - var path = "aa/b/c/"; - - var tokenizer = new PathTokenizer(path.AsMemory().Span); - - var segments = new List(); - - var collection = tokenizer.Fill(segments); - - Assert.AreEqual("aa", collection[0].ToString()); - - Assert.AreEqual("b", collection[1].ToString()); - - Assert.AreEqual("c", collection[2].ToString()); - - } - - - - [TestMethod] - - public void NonRootSeparator_HandlesAlternativePathSeparators() - - { - - var path = "aa\\b\\c\\"; - - var tokenizer = new PathTokenizer(path.AsMemory().Span); - - var segments = new List(); - - var collection = tokenizer.Fill(segments); - - Assert.AreEqual("aa", collection[0].ToString()); - - Assert.AreEqual("b", collection[1].ToString()); - - Assert.AreEqual("c", collection[2].ToString()); - - } - - - - [TestMethod] - - public void NonRootSeparator_HandlesMixedPathSeparators() - - { - - var path = "aa/b\\c/"; - - var tokenizer = new PathTokenizer(path.AsMemory().Span); - - var segments = new List(); - - var collection = tokenizer.Fill(segments); - - Assert.AreEqual("aa", collection[0].ToString()); - - Assert.AreEqual("b", collection[1].ToString()); - - Assert.AreEqual("c", collection[2].ToString()); - - } - - - - [TestMethod] - - public void Ignores_EmpySegments() - - { - - var path = "aa//b//c"; - - var tokenizer = new PathTokenizer(path.AsMemory().Span); - - var segments = new List(); - - var collection = tokenizer.Fill(segments); - - Assert.AreEqual("aa", collection[0].ToString()); - - Assert.AreEqual("b", collection[1].ToString()); - - Assert.AreEqual("c", collection[2].ToString()); - - } - - - - [TestMethod] - - public void Ignores_DotSegments() - - { - - var path = "./aa/./b/./c/."; - - var tokenizer = new PathTokenizer(path.AsMemory().Span); - - var segments = new List(); - - var collection = tokenizer.Fill(segments); - - Assert.AreEqual("aa", collection[0].ToString()); - - Assert.AreEqual("b", collection[1].ToString()); - - Assert.AreEqual("c", collection[2].ToString()); - - } - - - - [TestMethod] - - public void Ignores_DotDotSegments() - - { - - var path = "../aa/../b/../c/.."; - - var tokenizer = new PathTokenizer(path.AsMemory().Span); - - var segments = new List(); - - var collection = tokenizer.Fill(segments); - - Assert.AreEqual("aa", collection[0].ToString()); - - Assert.AreEqual("b", collection[1].ToString()); - - Assert.AreEqual("c", collection[2].ToString()); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.Compatibility.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.Compatibility.cs index eee33c68b996..b75a3531bad1 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.Compatibility.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.Compatibility.cs @@ -1,915 +1,309 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - using Microsoft.VisualStudio.TestTools.UnitTesting; - using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; // Licensed to the .NET Foundation under one or more agreements. - - // The .NET Foundation licenses this file to you under the MIT license. - - - - namespace Microsoft.AspNetCore.StaticWebAssets.Tasks.Test; - - - - public partial class StaticWebAssetGlobMatcherTest - - { - - [TestMethod] - - public void MatchingFileIsFound() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("alpha.txt"); - - var globMatcher = matcher.Build(); - - - - var match = globMatcher.Match("alpha.txt"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("alpha.txt", match.Pattern); - - } - - - - [TestMethod] - - public void MismatchedFileIsIgnored() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("alpha.txt"); - - var globMatcher = matcher.Build(); - - - - var match = globMatcher.Match("omega.txt"); - - Assert.IsFalse(match.IsMatch); - - } - - - - [TestMethod] - - public void FolderNamesAreTraversed() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("beta/alpha.txt"); - - var globMatcher = matcher.Build(); - - - - var match = globMatcher.Match("beta/alpha.txt"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("beta/alpha.txt", match.Pattern); - - } - - - - [TestMethod] - - [DataRow(@"beta/alpha.txt", @"beta/alpha.txt")] - - [DataRow(@"beta\alpha.txt", @"beta/alpha.txt")] - - [DataRow(@"beta/alpha.txt", @"beta\alpha.txt")] - - [DataRow(@"beta\alpha.txt", @"beta\alpha.txt")] - - [DataRow(@"\beta\alpha.txt", @"beta/alpha.txt")] - - public void SlashPolarityIsIgnored(string includePattern, string filePath) - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns(includePattern); - - var globMatcher = matcher.Build(); - - - - var match = globMatcher.Match(filePath); - - Assert.IsTrue(match.IsMatch); - - //Assert.AreEqual("beta/alpha.txt", match.Pattern); - - } - - - - [TestMethod] - - [DataRow(@"alpha.*", new[] { "alpha.txt" })] - - [DataRow(@"*", new[] { "alpha.txt", "beta.txt", "gamma.dat" })] - - [DataRow(@"*et*", new[] { "beta.txt" })] - - [DataRow(@"*.*", new[] { "alpha.txt", "beta.txt", "gamma.dat" })] - - [DataRow(@"b*et*x", new string[0])] - - [DataRow(@"*.txt", new[] { "alpha.txt", "beta.txt" })] - - [DataRow(@"b*et*t", new[] { "beta.txt" })] - - public void CanPatternMatch(string includes, string[] expected) - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns(includes); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "alpha.txt", "beta.txt", "gamma.dat" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(expected, matches); - - } - - - - [TestMethod] - - [DataRow(@"12345*5678", new string[0])] - - [DataRow(@"1234*5678", new[] { "12345678" })] - - [DataRow(@"12*23*", new string[0])] - - [DataRow(@"12*3456*78", new[] { "12345678" })] - - [DataRow(@"*45*56", new string[0])] - - [DataRow(@"*67*78", new string[0])] - - public void PatternBeginAndEndCantOverlap(string includes, string[] expected) - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns(includes); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "12345678" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(expected, matches); - - } - - - - [TestMethod] - - [DataRow(@"*alpha*/*", new[] { "alpha/hello.txt" })] - - [DataRow(@"/*/*", new[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] - - [DataRow(@"*/*", new[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] - - [DataRow(@"/*.*/*", new string[] { })] - - [DataRow(@"*.*/*", new string[] { })] - - [DataRow(@"/*mm*/*", new[] { "gamma/hello.txt" })] - - [DataRow(@"*mm*/*", new[] { "gamma/hello.txt" })] - - [DataRow(@"/*alpha*/*", new[] { "alpha/hello.txt" })] - - public void PatternMatchingWorksInFolders(string includes, string[] expected) - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns(includes); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(expected, matches); - - } - - - - [TestMethod] - - [DataRow(@"", new string[] { })] - - [DataRow(@"./", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] - - [DataRow(@"./alpha/hello.txt", new string[] { "alpha/hello.txt" })] - - [DataRow(@"./**/hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] - - [DataRow(@"././**/hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] - - [DataRow(@"././**/./hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] - - [DataRow(@"././**/./**/hello.txt", new string[] { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" })] - - [DataRow(@"./*mm*/hello.txt", new string[] { "gamma/hello.txt" })] - - [DataRow(@"./*mm*/*", new string[] { "gamma/hello.txt" })] - - public void PatternMatchingCurrent(string includePattern, string[] matchesExpected) - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns(includePattern); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "alpha/hello.txt", "beta/hello.txt", "gamma/hello.txt" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(matchesExpected, matches); - - } - - - - [TestMethod] - - public void StarDotStarIsSameAsStar() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("*.*"); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "alpha.txt", "alpha.", ".txt", ".", "alpha", "txt" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(new[] { "alpha.txt", "alpha.", ".txt" }, matches); - - } - - - - [TestMethod] - - public void IncompletePatternsDoNotInclude() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("*/*.txt"); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "one/x.txt", "two/x.txt", "x.txt" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(new[] { "one/x.txt", "two/x.txt" }, matches); - - } - - - - [TestMethod] - - public void IncompletePatternsDoNotExclude() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("*/*.txt"); - - matcher.AddExcludePatterns("one/hello.txt"); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "one/x.txt", "two/x.txt" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(new[] { "one/x.txt", "two/x.txt" }, matches); - - } - - - - [TestMethod] - - public void TrailingRecursiveWildcardMatchesAllFiles() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("one/**"); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "one/x.txt", "two/x.txt", "one/x/y.txt" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(new[] { "one/x.txt", "one/x/y.txt" }, matches); - - } - - - - [TestMethod] - - public void LeadingRecursiveWildcardMatchesAllLeadingPaths() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("**/*.cs"); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "one/x.cs", "two/x.cs", "one/two/x.cs", "x.cs", "one/x.txt", "two/x.txt", "one/two/x.txt", "x.txt" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(new[] { "one/x.cs", "two/x.cs", "one/two/x.cs", "x.cs" }, matches); - - } - - - - [TestMethod] - - public void InnerRecursiveWildcardMustStartWithAndEndWith() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("one/**/*.cs"); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "one/x.cs", "two/x.cs", "one/two/x.cs", "x.cs", "one/x.txt", "two/x.txt", "one/two/x.txt", "x.txt" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(new[] { "one/x.cs", "one/two/x.cs" }, matches); - - } - - - - [TestMethod] - - public void ExcludeMayEndInDirectoryName() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("*.cs", "*/*.cs", "*/*/*.cs"); - - matcher.AddExcludePatterns("bin/", "one/two/"); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "one/x.cs", "two/x.cs", "one/two/x.cs", "x.cs", "bin/x.cs", "bin/two/x.cs" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(new[] { "one/x.cs", "two/x.cs", "x.cs" }, matches); - - } - - - - [TestMethod] - - public void RecursiveWildcardSurroundingContainsWith() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("**/x/**"); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "x/1", "1/x/2", "1/x", "x", "1", "1/2" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(new[] { "x/1", "1/x/2", "1/x", "x" }, matches); - - } - - - - [TestMethod] - - public void SequentialFoldersMayBeRequired() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("a/b/**/1/2/**/2/3/**"); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "1/2/2/3/x", "1/2/3/y", "a/1/2/4/2/3/b", "a/2/3/1/2/b", "a/b/1/2/2/3/x", "a/b/1/2/3/y", "a/b/a/1/2/4/2/3/b", "a/b/a/2/3/1/2/b" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(new[] { "a/b/1/2/2/3/x", "a/b/a/1/2/4/2/3/b" }, matches); - - } - - - - [TestMethod] - - public void RecursiveAloneIncludesEverything() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("**"); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "1/2/2/3/x", "1/2/3/y" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(new[] { "1/2/2/3/x", "1/2/3/y" }, matches); - - } - - - - [TestMethod] - - public void ExcludeCanHaveSurroundingRecursiveWildcards() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("**"); - - matcher.AddExcludePatterns("**/x/**"); - - var globMatcher = matcher.Build(); - - - - var matches = new List { "x/1", "1/x/2", "1/x", "x", "1", "1/2" } - - .Where(file => globMatcher.Match(file).IsMatch) - - .ToArray(); - - - - Assert.AreSequenceEqual(new[] { "1", "1/2" }, matches); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.cs index a514f7208235..fdf5b6700ac6 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/Globbing/StaticWebAssetGlobMatcherTest.cs @@ -6,1230 +6,413 @@ using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using System.Collections.Generic; - - using System.Linq; - - using System.Text; - - using System.Threading.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - namespace Microsoft.AspNetCore.StaticWebAssets.Tasks.Test; - - - - // Set of things to test: - - // Literals 'a' - - // Multiple literals 'a/b' - - // Extensions '*.a' - - // Longer extensions first '*.a', '*.b.a' - - // Extensions at the beginning '*.a/b' - - // Extensions at the end 'a/*.b' - - // Extensions in the middle 'a/*.b/c' - - // Wildcard '*' - - // Wildcard at the beginning '*/a' - - // Wildcard at the end 'a/*' - - // Wildcard in the middle 'a/*/c' - - // Recursive wildcard '**' - - // Recursive wildcard at the beginning '**/a' - - // Recursive wildcard at the end 'a/**' - - // Recursive wildcard in the middle 'a/**/c' - - [TestClass] - public partial class StaticWebAssetGlobMatcherTest - - { - - [TestMethod] - - [DataRow("**/*.razor.js", "Components/Pages/RegularComponent.razor.js", "Components/Pages/RegularComponent.razor.js")] - - [DataRow("**/*.razor.js", "Components/User.Profile.Details.razor.js", "Components/User.Profile.Details.razor.js")] - - [DataRow("**/*.razor.js", "Components/Area/Sub/Feature/User.Profile.Details.razor.js", "Components/Area/Sub/Feature/User.Profile.Details.razor.js")] - - [DataRow("**/*.razor.js", "Components/Area/Sub/Feature/Deep.Component.Name.With.Many.Parts.razor.js", "Components/Area/Sub/Feature/Deep.Component.Name.With.Many.Parts.razor.js")] - - [DataRow("**/*.cshtml.js", "Pages/Shared/_Host.cshtml.js", "Pages/Shared/_Host.cshtml.js")] - - [DataRow("**/*.cshtml.js", "Areas/Admin/Pages/Dashboard.cshtml.js", "Areas/Admin/Pages/Dashboard.cshtml.js")] - - [DataRow("*.lib.module.js", "Widget.lib.module.js", "Widget.lib.module.js")] - - [DataRow("*.razor.css", "Component.razor.css", "Component.razor.css")] - - [DataRow("*.cshtml.css", "View.cshtml.css", "View.cshtml.css")] - - [DataRow("*.modules.json", "app.modules.json", "app.modules.json")] - - [DataRow("*.lib.module.js", "Rcl.Client.Feature.lib.module.js", "Rcl.Client.Feature.lib.module.js")] - - public void Can_Match_WellKnownExistingPatterns(string pattern, string path, string expectedStem) - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns(pattern); - - var globMatcher = matcher.Build(); - - - - var match = globMatcher.Match(path); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual(pattern, match.Pattern); - - Assert.AreEqual(expectedStem, match.Stem); - - } - - [TestMethod] - - public void CanMatchLiterals() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("a"); - - var globMatcher = matcher.Build(); - - - - var match = globMatcher.Match("a"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("a", match.Pattern); - - Assert.AreEqual("a", match.Stem); - - } - - - - [TestMethod] - - public void CanMatchMultipleLiterals() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("a/b"); - - var globMatcher = matcher.Build(); - - - - var match = globMatcher.Match("a/b"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("a/b", match.Pattern); - - Assert.AreEqual("b", match.Stem); - - } - - - - [TestMethod] - - public void CanMatchExtensions() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("*.a"); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match("a.a"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("*.a", match.Pattern); - - Assert.AreEqual("a.a", match.Stem); - - } - - - - [TestMethod] - - public void MatchesLongerExtensionsFirst() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("*.a", "*.b.a"); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match("c.b.a"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("*.b.a", match.Pattern); - - Assert.AreEqual("c.b.a", match.Stem); - - } - - - - [TestMethod] - - public void CanMatchExtensionsAtTheBeginning() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("*.a/b"); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match("c.a/b"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("*.a/b", match.Pattern); - - Assert.AreEqual("b", match.Stem); - - } - - - - [TestMethod] - - public void CanMatchExtensionsAtTheEnd() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("a/*.b"); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match("a/c.b"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("a/*.b", match.Pattern); - - Assert.AreEqual("c.b", match.Stem); - - } - - - - [TestMethod] - - public void CanMatchExtensionsInMiddle() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("a/*.b/c"); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match("a/d.b/c"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("a/*.b/c", match.Pattern); - - Assert.AreEqual("c", match.Stem); - - } - - - - [TestMethod] - - [DataRow("a*")] - - [DataRow("*a")] - - [DataRow("?")] - - [DataRow("*?")] - - [DataRow("?*")] - - [DataRow("**a")] - - [DataRow("a**")] - - [DataRow("**?")] - - [DataRow("?**")] - - public void CanMatchComplexSegments(string pattern) - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns(pattern); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match("a"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual(pattern, match.Pattern); - - Assert.AreEqual("a", match.Stem); - - } - - - - [TestMethod] - - [DataRow("a?", "aa", true)] - - [DataRow("a?", "a", false)] - - [DataRow("a?", "aaa", false)] - - [DataRow("?a", "aa", true)] - - [DataRow("?a", "a", false)] - - [DataRow("?a", "aaa", false)] - - [DataRow("a?a", "aaa", true)] - - [DataRow("a?a", "aba", true)] - - [DataRow("a?a", "abaa", false)] - - [DataRow("a?a", "ab", false)] - - public void QuestionMarksMatchSingleCharacter(string pattern, string input, bool expectedMatchResult) - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns(pattern); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match(input); - - Assert.AreEqual(expectedMatchResult, match.IsMatch); - - if(expectedMatchResult) - - { - - Assert.AreEqual(pattern, match.Pattern); - - Assert.AreEqual(input, match.Stem); - - } - - else - - { - - Assert.IsNull(match.Pattern); - - Assert.IsNull(match.Stem); - - } - - } - - - - [TestMethod] - - [DataRow("a??", "aaa", true)] - - [DataRow("a??", "aa", false)] - - [DataRow("a??", "aaaa", false)] - - [DataRow("?a?", "aaa", true)] - - [DataRow("?a?", "aa", false)] - - [DataRow("?a?", "aaaa", false)] - - [DataRow("??a", "aaa", true)] - - [DataRow("??a", "aa", false)] - - [DataRow("??a", "aaaa", false)] - - [DataRow("a??a", "aaaa", true)] - - [DataRow("a??a", "aaba", true)] - - [DataRow("a??a", "aabaa", false)] - - [DataRow("a??a", "aba", false)] - - public void MultipleQuestionMarksMatchExactlyTheNumberOfCharacters(string pattern, string input, bool expectedMatchResult) - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns(pattern); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match(input); - - Assert.AreEqual(expectedMatchResult, match.IsMatch); - - if (expectedMatchResult) - - { - - Assert.AreEqual(pattern, match.Pattern); - - Assert.AreEqual(input, match.Stem); - - } - - else - - { - - Assert.IsNull(match.Pattern); - - Assert.IsNull(match.Stem); - - } - - } - - - - [TestMethod] - - [DataRow("a*", "a", true)] - - [DataRow("a*", "aa", true)] - - [DataRow("a*", "aaa", true)] - - [DataRow("a*", "aaaa", true)] - - [DataRow("a*", "aaaaa", true)] - - [DataRow("a*", "aaaaaa", true)] - - [DataRow("*a", "a", true)] - - [DataRow("*a", "aa", true)] - - [DataRow("*a", "aaa", true)] - - [DataRow("*a", "aaaa", true)] - - [DataRow("*a", "aaaaa", true)] - - [DataRow("a*a", "a", false)] - - [DataRow("a*a", "aa", true)] - - [DataRow("a*a", "aaa", true)] - - [DataRow("a*a", "aaaaa", true)] - - [DataRow("a*a", "aaaaaa", true)] - - [DataRow("a*a", "aba", true)] - - [DataRow("a*a", "abaa", true)] - - [DataRow("a*a", "abba", true)] - - [DataRow("a*b", "ab", true)] - - public void WildCardsMatchZeroOrMoreCharacters(string pattern, string input, bool expectedMatchResult) - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns(pattern); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match(input); - - Assert.AreEqual(expectedMatchResult, match.IsMatch); - - if (expectedMatchResult) - - { - - Assert.AreEqual(pattern, match.Pattern); - - Assert.AreEqual(input, match.Stem); - - } - - else - - { - - Assert.IsNull(match.Pattern); - - Assert.IsNull(match.Stem); - - } - - } - - - - [TestMethod] - - [DataRow("*?a", "a", false)] - - [DataRow("*?a", "aa", true)] - - [DataRow("*?a", "aaa", true)] - - [DataRow("*?a", "aaaa", true)] - - [DataRow("*?a", "aaaaa", true)] - - [DataRow("*?a", "aaaaaa", true)] - - [DataRow("*??a", "aa", false)] - - [DataRow("*??a", "aaa", true)] - - [DataRow("*???a", "aaa", false)] - - [DataRow("*???a", "aaaa", true)] - - [DataRow("*????a", "aaaa", false)] - - [DataRow("*????a", "aaaaa", true)] - - [DataRow("*?????a", "aaaaa", false)] - - [DataRow("*?????a", "aaaaaa", true)] - - [DataRow("*??????a", "aaaaaa", false)] - - [DataRow("*??????a", "aaaaaaa", true)] - - [DataRow("a*?", "a", false)] - - [DataRow("a*?", "aa", true)] - - [DataRow("a*?", "aaa", true)] - - [DataRow("a*?", "aaaa", true)] - - [DataRow("a*?", "aaaaa", true)] - - [DataRow("a*?", "aaaaaa", true)] - - [DataRow("a*??", "aa", false)] - - [DataRow("a*??", "aaa", true)] - - [DataRow("a*???", "aaa", false)] - - [DataRow("a*???", "aaaa", true)] - - [DataRow("a*????", "aaaa", false)] - - [DataRow("a*????", "aaaaa", true)] - - [DataRow("a*?????", "aaaaa", false)] - - [DataRow("a*?????", "aaaaaa", true)] - - [DataRow("a*??????", "aaaaaa", false)] - - [DataRow("a*??????", "aaaaaaa", true)] - - - - public void SingleWildcardPrecededOrSucceededByQuestionMarkRequireMinimumNumberOfCharacters(string pattern, string input, bool expectedMatchResult) - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns(pattern); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match(input); - - Assert.AreEqual(expectedMatchResult, match.IsMatch); - - if (expectedMatchResult) - - { - - Assert.AreEqual(pattern, match.Pattern); - - Assert.AreEqual(input, match.Stem); - - } - - else - - { - - Assert.IsNull(match.Pattern); - - Assert.IsNull(match.Stem); - - } - - } - - - - [TestMethod] - - public void CanMatchWildCard() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("*"); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match("a"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("*", match.Pattern); - - Assert.AreEqual("a", match.Stem); - - } - - - - [TestMethod] - - public void CanMatchWildCardAtTheBeginning() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("*/a"); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match("c/a"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("*/a", match.Pattern); - - Assert.AreEqual("a", match.Stem); - - } - - - - [TestMethod] - - public void CanMatchWildCardAtTheEnd() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("a/*"); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match("a/c"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("a/*", match.Pattern); - - Assert.AreEqual("c", match.Stem); - - } - - - - [TestMethod] - - public void CanMatchWildCardInMiddle() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("a/*/c"); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match("a/b/c"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("a/*/c", match.Pattern); - - Assert.AreEqual("c", match.Stem); - - } - - - - [TestMethod] - - public void CanMatchRecursiveWildCard() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("**"); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match("a/b/c"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("**", match.Pattern); - - Assert.AreEqual("a/b/c", match.Stem); - - } - - - - [TestMethod] - - public void CanMatchRecursiveWildCardAtTheBeginning() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("**/a"); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match("c/b/a"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("**/a", match.Pattern); - - Assert.AreEqual("c/b/a", match.Stem); - - } - - - - [TestMethod] - - public void CanMatchRecursiveWildCardAtTheEnd() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("a/**"); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match("a/b/c"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("a/**", match.Pattern); - - Assert.AreEqual("b/c", match.Stem); - - } - - - - [TestMethod] - - public void CanMatchRecursiveWildCardInMiddle() - - { - - var matcher = new StaticWebAssetGlobMatcherBuilder(); - - matcher.AddIncludePatterns("a/**/c"); - - var globMatcher = matcher.Build(); - - var match = globMatcher.Match("a/b/c"); - - Assert.IsTrue(match.IsMatch); - - Assert.AreEqual("a/**/c", match.Pattern); - - Assert.AreEqual("b/c", match.Stem); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesMultiThreadingTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesMultiThreadingTest.cs index b3f8498f4c40..e5b9ef277e71 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesMultiThreadingTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesMultiThreadingTest.cs @@ -2,395 +2,138 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - - - // Test parallelization is disabled assembly-wide via - - // [assembly:CollectionBehavior(DisableTestParallelization = true)] in - - // LegacyStaticWebAssetsV1IntegrationTest.cs, which already isolates the - - // process-CWD mutation this test performs. - - [DoNotParallelize] [TestClass] - public class MergeConfigurationPropertiesMultiThreadingTest - - { - - [TestMethod] - - public void ResolvesProjectReferencePathRelativeToTaskEnvironmentProjectDirectory_NotProcessCurrentDirectory() - - { - - // Scope of this test: verify that the *ProjectReferences* side of the path-equality check - - // in FindMatchingProject is rooted against TaskEnvironment.ProjectDirectory rather than the - - // process current directory. The *CandidateConfigurations* side (configuration.GetMetadata("FullPath")) - - // is intentionally passed as an already-absolute path here — that mirrors what MSBuild's - - // well-known %(FullPath) modifier produces in MT mode (it resolves against the per-task - - // AsyncLocal working directory, not the process CWD) and is also what the equality check - - // requires in order to compare against the OS-canonical form on the other side. - - // - - // Layout (must place the two roots in different subtrees so a relative - - // "../reference/myRcl.csproj" produces *different* absolute paths - - // depending on which root it is resolved against): - - // /project/ <-- TaskEnvironment.ProjectDirectory - - // /project/../reference/ <-- candidate's real location - - // /decoy/spawn/ <-- process CWD (the "decoy") - - // /decoy/reference/ <-- where the old CWD-based - - // Path.GetFullPath would point - - var testRoot = Path.Combine(AppContext.BaseDirectory, nameof(MergeConfigurationPropertiesMultiThreadingTest), Guid.NewGuid().ToString("N")); - - var projectDir = Path.Combine(testRoot, "project"); - - var spawnDir = Path.Combine(testRoot, "decoy", "spawn"); - - Directory.CreateDirectory(projectDir); - - Directory.CreateDirectory(spawnDir); - - - - var relativeProjectReference = Path.Combine("..", "reference", "myRcl.csproj"); - - - - // Candidate FullPath represents what MSBuild's well-known FullPath modifier - - // would produce in MT mode: the path resolved against the project directory. - - var candidateAbsolutePath = Path.GetFullPath(Path.Combine(projectDir, relativeProjectReference)); - - - - // Sanity: the decoy CWD would produce a *different* absolute path for the - - // same relative input — that is what proves the equality check is using - - // the right root. - - var decoyAbsolutePath = Path.GetFullPath(Path.Combine(spawnDir, relativeProjectReference)); - - candidateAbsolutePath.Should().NotBe(decoyAbsolutePath, - - "the test setup must place project and decoy in different parents so the migration is actually exercised"); - - - - var originalCurrentDirectory = Directory.GetCurrentDirectory(); - - try - - { - - Directory.SetCurrentDirectory(spawnDir); - - - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new MergeConfigurationProperties - - { - - BuildEngine = buildEngine.Object, - - TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir), - - CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(candidateAbsolutePath) }, - - ProjectReferences = new[] - - { - - CreateProjectReference( - - project: Path.Combine("..", "myRcl", "myRcl.csproj"), - - // Relative MSBuildSourceProjectFile — the task must resolve - - // this against the TaskEnvironment.ProjectDirectory, not - - // against Environment.CurrentDirectory. - - msBuildSourceProjectFile: relativeProjectReference, - - undefineProperties: "TargetFramework;RuntimeIdentifier") - - } - - }; - - - - var result = task.Execute(); - - - - result.Should().BeTrue("the task must find the project reference by absolutizing against the project directory, not the process CWD"); - - errorMessages.Should().BeEmpty(); - - task.ProjectConfigurations.Should().HaveCount(1); - - task.ProjectConfigurations[0].GetMetadata("Source").Should().Be("myRcl"); - - } - - finally - - { - - Directory.SetCurrentDirectory(originalCurrentDirectory); - - if (Directory.Exists(testRoot)) - - { - - Directory.Delete(testRoot, recursive: true); - - } - - } - - } - - - - private static ITaskItem CreateCandidateProjectConfiguration(string project) - - { - - return new TaskItem(project, new Dictionary - - { - - ["AdditionalPublishProperties"] = "", - - ["GetBuildAssetsTargets"] = "GetCurrentProjectBuildStaticWebAssetItems", - - ["GetPublishAssetsTargets"] = "ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems", - - ["Version"] = "2", - - ["AdditionalBuildProperties"] = "", - - ["Source"] = "myRcl", - - ["AdditionalPublishPropertiesToRemove"] = "", - - ["AdditionalBuildPropertiesToRemove"] = "", - - }); - - } - - - - private static ITaskItem CreateProjectReference( - - string project, - - string msBuildSourceProjectFile, - - string undefineProperties = "") - - { - - return new TaskItem(project, new Dictionary - - { - - ["MSBuildSourceProjectFile"] = msBuildSourceProjectFile, - - ["UndefineProperties"] = undefineProperties, - - ["SetConfiguration"] = "", - - ["SetPlatform"] = "", - - ["SetTargetFramework"] = "", - - ["GlobalPropertiesToRemove"] = "", - - }); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesTest.cs index 72d84beb67d3..039c9593ef5e 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/MergeConfigurationPropertiesTest.cs @@ -2,895 +2,304 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class MergeConfigurationPropertiesTest - - { - - [TestMethod] - - public void MergesProjectConfigurationWithProjectReferenceWhenMatchingReferenceFound() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var referenceProjectFile = Path.Combine("..", "reference", "myRcl.csproj"); - - var task = new MergeConfigurationProperties - - { - - BuildEngine = buildEngine.Object, - - CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(Path.GetFullPath(referenceProjectFile)) }, - - ProjectReferences = new[] { - - CreateProjectReference( - - project: Path.Combine("..", "myRcl", "myRcl.csproj"), - - msBuildSourceProjectFile: Path.GetFullPath(referenceProjectFile), - - undefineProperties: Path.Combine(";TargetFramework;RuntimeIdentifier")) - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.ProjectConfigurations.Should().HaveCount(1); - - var config = task.ProjectConfigurations[0]; - - config.GetMetadata("Source").Should().Be("myRcl"); - - config.GetMetadata("GetBuildAssetsTargets").Should().Be("GetCurrentProjectBuildStaticWebAssetItems"); - - config.GetMetadata("GetPublishAssetsTargets").Should() - - .Be("ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems"); - - config.GetMetadata("Version").Should().Be("2"); - - config.GetMetadata("AdditionalBuildProperties").Should().Be(""); - - config.GetMetadata("AdditionalBuildPropertiesToRemove").Should().Be("TargetFramework;RuntimeIdentifier"); - - config.GetMetadata("AdditionalPublishProperties").Should().Be(""); - - config.GetMetadata("AdditionalPublishPropertiesToRemove").Should().Be("TargetFramework;RuntimeIdentifier"); - - } - - - - [TestMethod] - - public void MergesProjectConfigurationWithProjectReference_UsesOSCasingForMatching() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var referenceProjectFile = Path.Combine("..", "reference", "myRcl.csproj"); - - var task = new MergeConfigurationProperties - - { - - BuildEngine = buildEngine.Object, - - CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(Path.GetFullPath(referenceProjectFile)) }, - - ProjectReferences = new[] - - { - - CreateProjectReference( - - project: Path.Combine("..", "myRCL", "myRcl.csproj"), - - msBuildSourceProjectFile: Path.GetFullPath(referenceProjectFile).ToUpperInvariant(), - - undefineProperties: Path.Combine(";TargetFramework;RuntimeIdentifier")) - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(OperatingSystem.IsWindows()); - - } - - - - [TestMethod] - - public void FailswhenProjectReferenceNotFound() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var referenceProjectFile = Path.Combine("..", "reference", "myRcl.csproj"); - - var task = new MergeConfigurationProperties - - { - - BuildEngine = buildEngine.Object, - - CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(Path.GetFullPath(referenceProjectFile)) }, - - ProjectReferences = Array.Empty() - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(false); - - } - - - - [TestMethod] - - public void MergesProjectConfigurationRespectsSetTargetFramework() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var referenceProjectFile = Path.Combine("..", "reference", "myRcl.csproj"); - - var task = new MergeConfigurationProperties - - { - - BuildEngine = buildEngine.Object, - - CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(Path.GetFullPath(referenceProjectFile)) }, - - ProjectReferences = new[] { - - CreateProjectReference( - - project: Path.Combine("..", "myRcl", "myRcl.csproj"), - - msBuildSourceProjectFile: Path.GetFullPath(referenceProjectFile), - - setTargetFramework: $"TargetFramework={ToolsetInfo.CurrentTargetFramework}") - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.ProjectConfigurations.Should().HaveCount(1); - - var config = task.ProjectConfigurations[0]; - - config.GetMetadata("Source").Should().Be("myRcl"); - - config.GetMetadata("GetBuildAssetsTargets").Should().Be("GetCurrentProjectBuildStaticWebAssetItems"); - - config.GetMetadata("GetPublishAssetsTargets").Should() - - .Be("ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems"); - - config.GetMetadata("Version").Should().Be("2"); - - config.GetMetadata("AdditionalBuildProperties").Should().Be($"TargetFramework={ToolsetInfo.CurrentTargetFramework}"); - - config.GetMetadata("AdditionalBuildPropertiesToRemove").Should().Be(""); - - config.GetMetadata("AdditionalPublishProperties").Should().Be($"TargetFramework={ToolsetInfo.CurrentTargetFramework}"); - - config.GetMetadata("AdditionalPublishPropertiesToRemove").Should().Be(""); - - } - - - - [TestMethod] - - public void MergesProjectConfigurationRespectsSetPlatform() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var referenceProjectFile = Path.Combine("..", "reference", "myRcl.csproj"); - - var task = new MergeConfigurationProperties - - { - - BuildEngine = buildEngine.Object, - - CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(Path.GetFullPath(referenceProjectFile)) }, - - ProjectReferences = new[] { - - CreateProjectReference( - - project: Path.Combine("..", "myRcl", "myRcl.csproj"), - - msBuildSourceProjectFile: Path.GetFullPath(referenceProjectFile), - - setPlatform: "RuntimeIdentifier=win-x64") - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.ProjectConfigurations.Should().HaveCount(1); - - var config = task.ProjectConfigurations[0]; - - config.GetMetadata("Source").Should().Be("myRcl"); - - config.GetMetadata("GetBuildAssetsTargets").Should().Be("GetCurrentProjectBuildStaticWebAssetItems"); - - config.GetMetadata("GetPublishAssetsTargets").Should() - - .Be("ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems"); - - config.GetMetadata("Version").Should().Be("2"); - - config.GetMetadata("AdditionalBuildProperties").Should().Be("RuntimeIdentifier=win-x64"); - - config.GetMetadata("AdditionalBuildPropertiesToRemove").Should().Be(""); - - config.GetMetadata("AdditionalPublishProperties").Should().Be("RuntimeIdentifier=win-x64"); - - config.GetMetadata("AdditionalPublishPropertiesToRemove").Should().Be(""); - - } - - - - [TestMethod] - - public void MergesProjectConfigurationRespectsSetConfiguration() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var referenceProjectFile = Path.Combine("..", "reference", "myRcl.csproj"); - - var task = new MergeConfigurationProperties - - { - - BuildEngine = buildEngine.Object, - - CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(Path.GetFullPath(referenceProjectFile)) }, - - ProjectReferences = new[] { - - CreateProjectReference( - - project: Path.Combine("..", "myRcl", "myRcl.csproj"), - - msBuildSourceProjectFile: Path.GetFullPath(referenceProjectFile), - - setConfiguration: "Configuration=Release") - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.ProjectConfigurations.Should().HaveCount(1); - - var config = task.ProjectConfigurations[0]; - - config.GetMetadata("Source").Should().Be("myRcl"); - - config.GetMetadata("GetBuildAssetsTargets").Should().Be("GetCurrentProjectBuildStaticWebAssetItems"); - - config.GetMetadata("GetPublishAssetsTargets").Should() - - .Be("ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems"); - - config.GetMetadata("Version").Should().Be("2"); - - config.GetMetadata("AdditionalBuildProperties").Should().Be("Configuration=Release"); - - config.GetMetadata("AdditionalBuildPropertiesToRemove").Should().Be(""); - - config.GetMetadata("AdditionalPublishProperties").Should().Be("Configuration=Release"); - - config.GetMetadata("AdditionalPublishPropertiesToRemove").Should().Be(""); - - } - - - - [TestMethod] - - public void MergesProjectConfigurationRespectsGlobalPropertiesToRemove() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var referenceProjectFile = Path.Combine("..", "reference", "myRcl.csproj"); - - var task = new MergeConfigurationProperties - - { - - BuildEngine = buildEngine.Object, - - CandidateConfigurations = new[] { CreateCandidateProjectConfiguration(Path.GetFullPath(referenceProjectFile)) }, - - ProjectReferences = new[] { - - CreateProjectReference( - - project: Path.Combine("..", "myRcl", "myRcl.csproj"), - - msBuildSourceProjectFile: Path.GetFullPath(referenceProjectFile), - - undefineProperties: "TargetFramework", - - globalPropertiesToRemove: "RuntimeIdentifier") - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.ProjectConfigurations.Should().HaveCount(1); - - var config = task.ProjectConfigurations[0]; - - config.GetMetadata("Source").Should().Be("myRcl"); - - config.GetMetadata("GetBuildAssetsTargets").Should().Be("GetCurrentProjectBuildStaticWebAssetItems"); - - config.GetMetadata("GetPublishAssetsTargets").Should() - - .Be("ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems"); - - config.GetMetadata("Version").Should().Be("2"); - - config.GetMetadata("AdditionalBuildProperties").Should().Be(""); - - config.GetMetadata("AdditionalBuildPropertiesToRemove").Should().Be("RuntimeIdentifier;TargetFramework"); - - config.GetMetadata("AdditionalPublishProperties").Should().Be(""); - - config.GetMetadata("AdditionalPublishPropertiesToRemove").Should().Be("RuntimeIdentifier;TargetFramework"); - - } - - - - private static ITaskItem CreateCandidateProjectConfiguration(string project) - - { - - return new TaskItem(Path.GetFullPath(project), new Dictionary - - { - - ["AdditionalPublishProperties"] = "", - - ["GetBuildAssetsTargets"] = "GetCurrentProjectBuildStaticWebAssetItems", - - ["GetPublishAssetsTargets"] = "ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems", - - ["Version"] = "2", - - ["AdditionalBuildProperties"] = "", - - ["Source"] = "myRcl", - - ["AdditionalPublishPropertiesToRemove"] = "", - - ["AdditionalBuildPropertiesToRemove"] = "", - - }); - - } - - - - private static ITaskItem CreateProjectReference( - - string project, - - string msBuildSourceProjectFile, - - string undefineProperties = "", - - string setConfiguration = "", - - string setPlatform = "", - - string setTargetFramework = "", - - string globalPropertiesToRemove = "") - - { - - return new TaskItem(project, new Dictionary - - { - - ["MSBuildSourceProjectFile"] = msBuildSourceProjectFile, - - ["UndefineProperties"] = undefineProperties, - - ["SetConfiguration"] = setConfiguration, - - ["SetPlatform"] = setPlatform, - - ["SetTargetFramework"] = setTargetFramework, - - ["GlobalPropertiesToRemove"] = globalPropertiesToRemove, - - }); - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/OverrideHtmlAssetPlaceholdersTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/OverrideHtmlAssetPlaceholdersTest.cs index e8b0339b0b5e..ae436c29563f 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/OverrideHtmlAssetPlaceholdersTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/OverrideHtmlAssetPlaceholdersTest.cs @@ -2,871 +2,297 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using System.Text.RegularExpressions; - - - - namespace Microsoft.AspNetCore.Razor.Tasks; - - - - [TestClass] public class OverrideHtmlAssetPlaceholdersTest - - { - - [TestMethod] - - [DataRow( - - """ - - - - """, - - true, - - "main.js" - - )] - - [DataRow( - - """ - - - - """, - - true, - - "main.js" - - )] - - [DataRow( - - """ - - - - """, - - true, - - "main.js" - - )] - - [DataRow( - - """ - - - - """, - - true, - - "./main.js" - - )] - - [DataRow( - - """ - - - - """, - - true, - - "./folder/folder/file.name.something.js" - - )] - - [DataRow( - - """ - - - - """, - - true, - - "main.suffix.js" - - )] - - [DataRow( - - """ - - - - """, - - true, - - "/root/main.suffix.js" - - )] - - [DataRow( - - """ - - - - """, - - false - - )] - - [DataRow( - - """ - - - - """, - - false - - )] - - [DataRow( - - """ - - - - """, - - false - - )] - - [DataRow( - - """ - -

main#[.{fingerprint}].js

- - """, - - false - - )] - - [DataRow( - - """ - - - - """, - - true, - - "main.js" - - )] - - [DataRow( - - """ - - - - """, - - true, - - "./main.js" - - )] - - [DataRow( - - """ - - - - """, - - true, - - "main.js" - - )] - - public void ValidateAssetsRegex(string input, bool shouldMatch, string fileName = null) - - { - - var match = OverrideHtmlAssetPlaceholders._assetsRegex.Match(input); - - Assert.AreEqual(shouldMatch, match.Success); - - - - if (fileName != null) - - { - - Assert.AreEqual(fileName, match.Groups["fileName"].Value + match.Groups["fileExtension"].Value); - - } - - } - - - - [TestMethod] - - [DataRow( - - """ - - - - """, - - true - - )] - - [DataRow( - - """ - - - - """, - - true - - )] - - [DataRow( - - """ - - - - """, - - true - - )] - - [DataRow( - - """ - - - - """, - - true - - )] - - [DataRow( - - """ - - - - """, - - false - - )] - - [DataRow( - - """ - - - - """, - - false - - )] - - [DataRow( - - """ - - - - """, - - false - - )] - - public void ValidateImportMapRegex(string input, bool shouldMatch) - - { - - Assert.AreEqual(shouldMatch, OverrideHtmlAssetPlaceholders._importMapRegex.Match(input).Success); - - } - - - - [TestMethod] - - [DataRow( - - """ - - - - """, - - true - - )] - - [DataRow( - - """ - - - - """, - - true - - )] - - [DataRow( - - """ - - - - """, - - true - - )] - - [DataRow( - - """ - - - - """, - - false - - )] - - [DataRow( - - """ - - - - """, - - false - - )] - - [DataRow( - - """ - - " - - """, - - false - - )] - - [DataRow( - - """ - - " - - """, - - false - - )] - - [DataRow( - - """ - - - - """, - - false - - )] - - [DataRow( - - """ - - - - """, - - true, - - "webassembly" - - )] - - [DataRow( - - """ - - - - """, - - true, - - "webassembly" - - )] - - [DataRow( - - """ - - - - """, - - false - - )] - - [DataRow( - - """ - - - - """, - - false - - )] - - [DataRow( - - """ - - - - """, - - false - - )] - - [DataRow( - - """ - - - - """, - - true, - - "webassembly" - - )] - - public void ValidatePreloadRegex(string input, bool shouldMatch, string group = null) - - { - - var match = OverrideHtmlAssetPlaceholders._preloadRegex.Match(input); - - Assert.AreEqual(shouldMatch, match.Success); - - - - if (group != null) - - { - - Assert.AreEqual(group, match.Groups["group"]?.Value); - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadPackageAssetsManifestTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadPackageAssetsManifestTest.cs index 1d72ce670698..3afe23c238cb 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadPackageAssetsManifestTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadPackageAssetsManifestTest.cs @@ -2,1465 +2,495 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Text.Json; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - [TestClass] public class ReadPackageAssetsManifestTest : IDisposable - - { - - private readonly string _tempDir; - - private readonly Mock _buildEngine; - - private readonly List _errorMessages; - - private readonly List _logMessages; - - - - public ReadPackageAssetsManifestTest() - - { - - _tempDir = Path.Combine(Path.GetTempPath(), "ReadPkgManifest_" + Guid.NewGuid().ToString("N")); - - Directory.CreateDirectory(_tempDir); - - - - _errorMessages = new List(); - - _logMessages = new List(); - - _buildEngine = new Mock(); - - _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => _errorMessages.Add(args.Message)); - - _buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => _logMessages.Add(args.Message)); - - _buildEngine.Setup(e => e.LogWarningEvent(It.IsAny())) - - .Callback(args => _logMessages.Add(args.Message)); - - } - - - - public void Dispose() - - { - - if (Directory.Exists(_tempDir)) - - { - - try { Directory.Delete(_tempDir, recursive: true); } catch { } - - } - - } - - - - [TestMethod] - - public void ReadsValidManifest_EmitsAssetsAsTaskItems() - - { - - var packageRoot = SetupPackageRoot("MyLib", - - CreateManifestAsset("staticwebassets/css/site.css", "css/site.css", "_content/mylib", "")); - - - - var manifestItem = CreateManifestItem(packageRoot, "MyLib"); - - - - var task = CreateReadManifestTask(new[] { manifestItem }); - - task.Execute().Should().BeTrue(); - - - - task.Assets.Should().HaveCount(1); - - - - var emitted = task.Assets[0]; - - emitted.GetMetadata("SourceType").Should().Be("Package"); - - emitted.GetMetadata("SourceId").Should().Be("MyLib"); - - emitted.GetMetadata("BasePath").Should().Be("_content/mylib"); - - emitted.GetMetadata("RelativePath").Should().Be("css/site.css"); - - emitted.GetMetadata("Fingerprint").Should().Be("test"); - - } - - - - [TestMethod] - - public void UngroupedAssets_AlwaysIncluded() - - { - - var packageRoot = SetupPackageRoot("MyLib", - - CreateManifestAsset("staticwebassets/app.js", "app.js", "_content/mylib", "")); - - - - var manifestItem = CreateManifestItem(packageRoot, "MyLib"); - - - - var task = CreateReadManifestTask( - - new[] { manifestItem }, - - new[] { CreateGroup("SomeGroup", "SomeValue", "OtherLib") }); - - task.Execute().Should().BeTrue(); - - - - task.Assets.Should().HaveCount(1, "ungrouped assets should always be included"); - - } - - - - [TestMethod] - - public void GroupedAsset_MatchingDeclaration_IsIncluded() - - { - - var packageRoot = SetupPackageRoot("IdentityUI", - - CreateManifestAsset("staticwebassets/css/site.css", "css/site.css", "_content/id", "BootstrapVersion=V5")); - - - - var manifestItem = CreateManifestItem(packageRoot, "IdentityUI"); - - - - var task = CreateReadManifestTask( - - new[] { manifestItem }, - - new[] { CreateGroup("BootstrapVersion", "V5", "IdentityUI") }); - - task.Execute().Should().BeTrue(); - - - - task.Assets.Should().HaveCount(1); - - } - - - - [TestMethod] - - public void GroupedAsset_NoDeclarations_IsExcluded() - - { - - var packageRoot = SetupPackageRoot("IdentityUI", - - CreateManifestAsset("staticwebassets/css/site.css", "css/site.css", "_content/id", "BootstrapVersion=V5")); - - - - var manifestItem = CreateManifestItem(packageRoot, "IdentityUI"); - - - - // No StaticWebAssetGroups - - var task = CreateReadManifestTask(new[] { manifestItem }); - - task.Execute().Should().BeTrue(); - - - - task.Assets.Should().HaveCount(0, "grouped assets should be excluded with no declarations"); - - } - - - - [TestMethod] - - public void MultiGroup_PartialMatch_IsExcluded() - - { - - var packageRoot = SetupPackageRoot("IdentityUI", - - CreateManifestAsset("staticwebassets/css/site.css", "css/site.css", "_content/id", - - "BootstrapVersion=V5;DebugAssets=true")); - - - - var manifestItem = CreateManifestItem(packageRoot, "IdentityUI"); - - - - // BootstrapVersion declared but DebugAssets not declared - - var task = CreateReadManifestTask( - - new[] { manifestItem }, - - new[] { CreateGroup("BootstrapVersion", "V5", "IdentityUI") }); - - task.Execute().Should().BeTrue(); - - - - task.Assets.Should().HaveCount(0, "AND-matching: all group entries must be satisfied"); - - } - - - - [TestMethod] - - public void CascadingExclusion_RelatedAssetExcludedWithPrimary() - - { - - var primaryAsset = CreateManifestAsset( - - "staticwebassets/css/site.css", "css/site.css", "_content/id", "BootstrapVersion=V5"); - - var relatedAsset = CreateManifestAsset( - - "staticwebassets/css/site.css.gz", "css/site.css.gz", "_content/id", ""); - - relatedAsset.Value.AssetRole = "Alternative"; - - relatedAsset.Value.AssetTraitName = "Content-Encoding"; - - relatedAsset.Value.AssetTraitValue = "gzip"; - - relatedAsset.Value.RelatedAsset = "staticwebassets/css/site.css"; - - - - var packageRoot = SetupPackageRoot("IdentityUI", primaryAsset, relatedAsset); - - var manifestItem = CreateManifestItem(packageRoot, "IdentityUI"); - - - - // No group declarations → primary excluded - - var task = CreateReadManifestTask(new[] { manifestItem }); - - task.Execute().Should().BeTrue(); - - - - task.Assets.Should().HaveCount(0, "both primary and related should be excluded via cascading"); - - } - - - - [TestMethod] - - public void Endpoints_ForExcludedAssets_AreFilteredOut() - - { - - var includedAsset = CreateManifestAsset( - - "staticwebassets/app.js", "app.js", "_content/id", ""); - - var excludedAsset = CreateManifestAsset( - - "staticwebassets/css/site.css", "css/site.css", "_content/id", "BootstrapVersion=V5"); - - - - var endpoints = new[] - - { - - new StaticWebAssetEndpoint - - { - - Route = "_content/id/app.js", - - AssetFile = "staticwebassets/app.js", - - Selectors = [], - - ResponseHeaders = [], - - EndpointProperties = [] - - }, - - new StaticWebAssetEndpoint - - { - - Route = "_content/id/css/site.css", - - AssetFile = "staticwebassets/css/site.css", - - Selectors = [], - - ResponseHeaders = [], - - EndpointProperties = [] - - } - - }; - - - - var packageRoot = SetupPackageRootWithEndpoints("IdentityUI", - - new[] { includedAsset, excludedAsset }, endpoints); - - var manifestItem = CreateManifestItem(packageRoot, "IdentityUI"); - - - - // No group declarations → grouped asset excluded - - var task = CreateReadManifestTask(new[] { manifestItem }); - - task.Execute().Should().BeTrue(); - - - - task.Assets.Should().HaveCount(1, "only ungrouped asset should be included"); - - task.Endpoints.Should().HaveCount(1, "endpoint for excluded asset should be removed"); - - task.Endpoints[0].ItemSpec.Should().Be("_content/id/app.js"); - - } - - - - [TestMethod] - - public void DeferredGroups_SkippedDuringEagerFiltering() - - { - - var packageRoot = SetupPackageRoot("IdentityUI", - - CreateManifestAsset("staticwebassets/css/site.css", "css/site.css", "_content/id", - - "ServerRendering=true")); - - - - var manifestItem = CreateManifestItem(packageRoot, "IdentityUI"); - - - - var task = CreateReadManifestTask( - - new[] { manifestItem }, - - new[] { CreateGroup("ServerRendering", "", "IdentityUI", deferred: true) }); - - task.Execute().Should().BeTrue(); - - - - task.Assets.Should().HaveCount(1, - - "deferred group requirements should be skipped during eager filtering"); - - } - - - - [TestMethod] - - public void FrameworkAssets_MaterializedToIntermediateDirectory() - - { - - var packageRoot = SetupPackageRoot("MyLib", - - CreateManifestAsset("staticwebassets/js/framework.js", "js/framework.js", "_content/mylib", "", "Framework")); - - - - var manifestItem = CreateManifestItem(packageRoot, "MyLib"); - - - - var task = CreateReadManifestTask(new[] { manifestItem }); - - task.Execute().Should().BeTrue(); - - - - task.Assets.Should().HaveCount(1); - - - - var emitted = task.Assets[0]; - - // Framework assets should be materialized to the fx directory - - var expectedDir = Path.Combine(_tempDir, "obj", "fx", "MyLib"); - - emitted.ItemSpec.Should().StartWith(expectedDir); - - File.Exists(emitted.ItemSpec).Should().BeTrue(); - - - - // SourceType changes to Discovered for framework materialization - - emitted.GetMetadata("SourceType").Should().Be("Discovered"); - - emitted.GetMetadata("SourceId").Should().Be("ConsumerApp"); - - } - - - - [TestMethod] - - public void GroupedFrameworkAsset_HasAssetGroupsClearedAfterMaterialization() - - { - - // A framework asset that belongs to a group (e.g. blazor.webassembly.js in the - - // BlazorWebAssembly group) passes group filtering when the group is declared, but - - // must have its AssetGroups cleared once it is materialized as a current-project - - // asset. Otherwise downstream endpoint generation treats it as a still-grouped - - // asset, skips it, and the fingerprinted asset 404s at runtime. - - var packageRoot = SetupPackageRoot("Microsoft.AspNetCore.Components.WebAssembly", - - CreateManifestAsset("staticwebassets/_framework/blazor.webassembly.js", "_framework/blazor.webassembly.js", "/", "BlazorWebAssembly=enabled", "Framework")); - - - - var manifestItem = CreateManifestItem(packageRoot, "Microsoft.AspNetCore.Components.WebAssembly"); - - - - var task = CreateReadManifestTask( - - new[] { manifestItem }, - - new[] { CreateGroup("BlazorWebAssembly", "enabled", "Microsoft.AspNetCore.Components.WebAssembly") }); - - task.Execute().Should().BeTrue(); - - - - task.Assets.Should().HaveCount(1); - - - - var emitted = task.Assets[0]; - - emitted.GetMetadata("SourceType").Should().Be("Discovered"); - - emitted.GetMetadata("AssetGroups").Should().BeEmpty("materialized framework assets must not retain group membership or endpoint generation will skip them"); - - } - - - - [TestMethod] - - public void InvalidManifestVersion_LogsError() - - { - - var packageDir = Path.Combine(_tempDir, "packages", "BadLib"); - - var buildDir = Path.Combine(packageDir, "build"); - - Directory.CreateDirectory(buildDir); - - - - // Write a staticwebassets directory with a file - - var swDir = Path.Combine(packageDir, "staticwebassets"); - - Directory.CreateDirectory(swDir); - - - - var badManifest = new { Version = 2, ManifestType = "Package", Assets = new Dictionary(), Endpoints = Array.Empty() }; - - var manifestPath = Path.Combine(buildDir, "BadLib.PackageAssets.json"); - - File.WriteAllText(manifestPath, JsonSerializer.Serialize(badManifest)); - - - - var manifestItem = new TaskItem(manifestPath, new Dictionary - - { - - ["SourceId"] = "BadLib", - - ["ContentRoot"] = swDir + Path.DirectorySeparatorChar, - - ["PackageRoot"] = packageDir, - - }); - - - - var task = CreateReadManifestTask(new[] { manifestItem }); - - var result = task.Execute(); - - - - result.Should().BeFalse(); - - _errorMessages.Should().ContainSingle(m => m.Contains("Unsupported package manifest version")); - - } - - - - [TestMethod] - - public void MissingIntermediateOutputPath_ProducesError() - - { - - var packageRoot = SetupPackageRoot("MyLib", - - CreateManifestAsset("staticwebassets/css/site.css", "css/site.css", "_content/mylib", "")); - - var manifestItem = CreateManifestItem(packageRoot, "MyLib"); - - - - var task = new ReadPackageAssetsManifest - - { - - BuildEngine = _buildEngine.Object, - - PackageManifests = new[] { manifestItem }, - - StaticWebAssetGroups = Array.Empty(), - - IntermediateOutputPath = null, - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - var result = task.Execute(); - - - - result.Should().BeFalse(); - - _errorMessages.Should().ContainSingle(m => m.Contains("IntermediateOutputPath is required")); - - } - - - - private ReadPackageAssetsManifest CreateReadManifestTask( - - ITaskItem[] manifests, - - ITaskItem[] groups = null) - - { - - return new ReadPackageAssetsManifest - - { - - BuildEngine = _buildEngine.Object, - - PackageManifests = manifests, - - StaticWebAssetGroups = groups ?? Array.Empty(), - - IntermediateOutputPath = Path.Combine(_tempDir, "obj"), - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - } - - - - private static ITaskItem CreateGroup(string name, string value, string sourceId, bool deferred = false) - - { - - var dict = new Dictionary - - { - - ["Value"] = value, - - ["SourceId"] = sourceId, - - }; - - if (deferred) - - dict["Deferred"] = "true"; - - return new TaskItem(name, dict); - - } - - - - private string SetupPackageRoot(string packageId, params KeyValuePair[] assets) - - { - - return SetupPackageRootWithEndpoints(packageId, assets, Array.Empty()); - - } - - - - private string SetupPackageRootWithEndpoints(string packageId, KeyValuePair[] assets, StaticWebAssetEndpoint[] endpoints) - - { - - var packageDir = Path.Combine(_tempDir, "packages", packageId); - - var buildDir = Path.Combine(packageDir, "build"); - - Directory.CreateDirectory(buildDir); - - - - // Create actual files for each asset and fill in SourceId/ContentRoot - - // the way GeneratePackageAssetsManifestFile does (via copy constructor). - - var contentRoot = Path.Combine(packageDir, "staticwebassets") + Path.DirectorySeparatorChar; - - foreach (var asset in assets) - - { - - var filePath = Path.Combine(packageDir, asset.Key.Replace('/', Path.DirectorySeparatorChar)); - - Directory.CreateDirectory(Path.GetDirectoryName(filePath)); + File.WriteAllText(filePath, "content-" + asset.Key); - - File.WriteAllText(filePath, "content-" + asset.Key); - - - - - - asset.Value.SourceId = packageId; - - - asset.Value.ContentRoot = contentRoot; - - - } - - - - + asset.Value.SourceId = packageId; + asset.Value.ContentRoot = contentRoot; + } var manifest = new StaticWebAssetPackageManifest - - { - - Version = 1, - - ManifestType = "Package", - - Assets = assets.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.OrdinalIgnoreCase), - - Endpoints = endpoints, - - }; - - - - var manifestPath = Path.Combine(buildDir, packageId + ".PackageAssets.json"); - - var json = JsonSerializer.SerializeToUtf8Bytes(manifest, - - StaticWebAssetsJsonSerializerContext.Default.StaticWebAssetPackageManifest); - - File.WriteAllBytes(manifestPath, json); - - - - return packageDir; - - } - - - - private ITaskItem CreateManifestItem(string packageRoot, string sourceId) - - { - - var buildDir = Path.Combine(packageRoot, "build"); - - var manifestPath = Path.Combine(buildDir, sourceId + ".PackageAssets.json"); - - var contentRoot = Path.Combine(packageRoot, "staticwebassets") + Path.DirectorySeparatorChar; - - - - return new TaskItem(manifestPath, new Dictionary - - { - - ["SourceId"] = sourceId, - - ["ContentRoot"] = contentRoot, - - ["PackageRoot"] = packageRoot, - - }); - - } - - - - private static KeyValuePair CreateManifestAsset( - - string packagePath, string relativePath, string basePath, string assetGroups, string sourceType = "Package") - - { - - var asset = new StaticWebAsset - - { - - Identity = packagePath, - - RelativePath = relativePath, - - BasePath = basePath, - - SourceType = sourceType, - - AssetKind = "All", - - AssetMode = "All", - - AssetRole = "Primary", - - RelatedAsset = "", - - AssetTraitName = "", - - AssetTraitValue = "", - - AssetGroups = assetGroups, - - Fingerprint = "test", - - Integrity = "sha256-test", - - CopyToOutputDirectory = "Never", - - CopyToPublishDirectory = "PreserveNewest", - - FileLength = 6, - - LastWriteTime = new DateTimeOffset(1990, 11, 15, 0, 0, 0, TimeSpan.Zero), - - }; - - - - return new KeyValuePair(packagePath, asset); - - } - - - - // Scenario 6: Custom .targets author override - - // A package author disables the auto-generated .targets and manually provides - - // their own .targets that still registers a StaticWebAssetPackageManifest item. - - // The consumer's ReadPackageAssetsManifest should still work correctly. - - [TestMethod] - - public void CustomTargetsOverride_ManualManifestItem_WorksCorrectly() - - { - - // Setup: simulate a package that has its manifest at a custom location - - // (as if the author wrote their own .targets pointing to the manifest) - - var packageRoot = SetupPackageRoot("CustomLib", - - CreateManifestAsset("staticwebassets/js/custom.js", "js/custom.js", "_content/customlib", ""), - - CreateManifestAsset("staticwebassets/css/theme.css", "css/theme.css", "_content/customlib", "")); - - - - // The manifest item metadata mirrors what a hand-authored .targets would produce. - - // The key difference from auto-generated: the author controls the paths. - - var manifestItem = CreateManifestItem(packageRoot, "CustomLib"); - - - - var task = CreateReadManifestTask(new[] { manifestItem }); - - task.Execute().Should().BeTrue(); - - - - task.Assets.Should().HaveCount(2); - - - - // Both assets should have the custom package's SourceId - - task.Assets.Should().OnlyContain(a => a.GetMetadata("SourceId") == "CustomLib"); - - // Both should resolve Identity paths under the package root - - task.Assets.Should().OnlyContain(a => a.ItemSpec.StartsWith( - - Path.Combine(packageRoot, "staticwebassets"))); - - } - - - - // Scenario 7: Multiple packages contributing manifests to the same consumer - - // Two packages each provide a StaticWebAssetPackageManifest item. - - // Group filtering should be applied independently per SourceId. - - [TestMethod] - - public void MultiplePackages_IndependentGroupFilteringPerSourceId() - - { - - // Package A: has grouped assets (BootstrapVersion=V5) - - var packageRootA = SetupPackageRoot("PkgA", - - CreateManifestAsset("staticwebassets/css/a.css", "css/a.css", "_content/pkga", "BootstrapVersion=V5"), - - CreateManifestAsset("staticwebassets/js/a.js", "js/a.js", "_content/pkga", "")); - - - - // Package B: has grouped assets (Theme=Dark) and ungrouped - - var packageRootB = SetupPackageRoot("PkgB", - - CreateManifestAsset("staticwebassets/css/b.css", "css/b.css", "_content/pkgb", "Theme=Dark"), - - CreateManifestAsset("staticwebassets/js/b.js", "js/b.js", "_content/pkgb", "")); - - - - var manifestA = CreateManifestItem(packageRootA, "PkgA"); - - var manifestB = CreateManifestItem(packageRootB, "PkgB"); - - - - // Consumer declares BootstrapVersion=V5 for PkgA but NOT Theme for PkgB - - var task = CreateReadManifestTask( - - new[] { manifestA, manifestB }, - - new[] { CreateGroup("BootstrapVersion", "V5", "PkgA") }); - - task.Execute().Should().BeTrue(); - - - - // PkgA: css/a.css included (group matched), js/a.js included (ungrouped) - - // PkgB: css/b.css excluded (Theme=Dark not declared), js/b.js included (ungrouped) - - task.Assets.Should().HaveCount(3); - - - - var assetPaths = task.Assets.Select(a => a.GetMetadata("RelativePath")).ToList(); - - assetPaths.Should().Contain("css/a.css"); - - assetPaths.Should().Contain("js/a.js"); - - assetPaths.Should().Contain("js/b.js"); - - assetPaths.Should().NotContain("css/b.css"); - - - - // Verify SourceIds are correct - - task.Assets.Where(a => a.GetMetadata("SourceId") == "PkgA").Should().HaveCount(2); - - task.Assets.Where(a => a.GetMetadata("SourceId") == "PkgB").Should().HaveCount(1); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadStaticWebAssetsManifestFileTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadStaticWebAssetsManifestFileTest.cs index e3e9fd91012d..3945e8d2486e 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadStaticWebAssetsManifestFileTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ReadStaticWebAssetsManifestFileTest.cs @@ -2,1048 +2,357 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Text.Json; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class ReadStaticWebAssetsManifestFileTest - - { - - public ReadStaticWebAssetsManifestFileTest() - - { - - Directory.CreateDirectory(Path.Combine(SdkTestContext.Current.TestExecutionDirectory, nameof(ReadStaticWebAssetsManifestFileTest))); - - TempFilePath = Path.Combine(SdkTestContext.Current.TestExecutionDirectory, nameof(ReadStaticWebAssetsManifestFileTest), Guid.NewGuid().ToString("N") + ".json"); - - } - - - - public string TempFilePath { get; } - - - - [TestMethod] - - public void CanReadManifestWithoutProperties() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var emptyManifest = "{}"; - - File.WriteAllText(TempFilePath, emptyManifest); - - - - var task = new ReadStaticWebAssetsManifestFile - - { - - BuildEngine = buildEngine.Object, - - ManifestPath = TempFilePath - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.Assets.Should().BeEmpty(); - - task.Endpoints.Should().BeEmpty(); - - task.DiscoveryPatterns.Should().BeEmpty(); - - task.ReferencedProjectsConfiguration.Should().BeEmpty(); - - } - - - - [TestMethod] - - public void CanReadEmptyManifest() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var emptyManifest = @"{ - - ""Version"": 1, - - ""Hash"": ""__hash__"", - - ""Source"": ""ComponentApp"", - - ""BasePath"": ""_content/ComponentApp"", - - ""Mode"": ""Default"", - - ""ManifestType"": ""Build"", - - ""ReferencedProjectsConfiguration"": [], - - ""DiscoveryPatterns"": [], - - ""Assets"": [], - - ""Endpoints"": [] - - }"; - - File.WriteAllText(TempFilePath, emptyManifest); - - - - var task = new ReadStaticWebAssetsManifestFile - - { - - BuildEngine = buildEngine.Object, - - ManifestPath = TempFilePath - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.Assets.Should().BeEmpty(); - - task.Endpoints.Should().BeEmpty(); - - task.DiscoveryPatterns.Should().BeEmpty(); - - task.ReferencedProjectsConfiguration.Should().BeEmpty(); - - } - - - - [TestMethod] - - public void ConvertsAssetsToTaskItems() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var contentRoot = Path.GetFullPath("."); - - var encodedContentRoot = JsonEncodedText.Encode(contentRoot); - - var identity = Path.Combine(contentRoot, "ComponentApp.styles.css"); - - var encodedIdentity = JsonEncodedText.Encode(identity); - - var manifest = $@"{{ - - ""Version"": 1, - - ""Hash"": ""__hash__"", - - ""Source"": ""ComponentApp"", - - ""BasePath"": ""_content/ComponentApp"", - - ""Mode"": ""Default"", - - ""ManifestType"": ""Build"", - - ""ReferencedProjectsConfiguration"": [], - - ""DiscoveryPatterns"": [], - - ""Assets"": [ - - {{ - - ""Identity"": ""{encodedIdentity}"", - - ""SourceId"": ""ComponentApp"", - - ""SourceType"": ""Computed"", - - ""ContentRoot"": ""{encodedContentRoot}"", - - ""BasePath"": ""_content/ComponentApp"", - - ""RelativePath"": ""ComponentApp.styles.css"", - - ""AssetKind"": ""All"", - - ""AssetMode"": ""CurrentProject"", - - ""AssetRole"": ""Primary"", - - ""RelatedAsset"": """", - - ""AssetTraitName"": ""ScopedCss"", - - ""AssetTraitValue"": ""ApplicationBundle"", - - ""CopyToOutputDirectory"": ""Never"", - - ""CopyToPublishDirectory"": ""PreserveNewest"", - - ""OriginalItemSpec"": ""{encodedIdentity}"" - - }} - - ], - - ""Endpoints"": [ - - {{ - - ""AssetFile"": ""{encodedIdentity}"", - - ""Route"": ""_content/ComponentApp/ComponentApp.styles.css"", - - ""Selectors"": [ - - {{ - - ""Name"": ""Content-Encoding"", - - ""Value"": ""gzip"", - - ""Quality"": ""0.9"" - - }} - - ], - - ""ResponseHeaders"": [ - - {{ - - ""Name"": ""Content-Length"", - - ""Value"": ""__content-length__"" - - }}, - - {{ - - ""Name"": ""Content-Type"", - - ""Value"": ""text/css"" - - }}, - - {{ - - ""Name"": ""ETag"", - - ""Value"": ""__etag__"" - - }}, - - {{ - - ""Name"": ""Last-Modified"", - - ""Value"": ""__last-modified__"" - - }} - - ] - - }} - - ] - - }}"; - - File.WriteAllText(TempFilePath, manifest); - - - - var task = new ReadStaticWebAssetsManifestFile - - { - - BuildEngine = buildEngine.Object, - - ManifestPath = TempFilePath - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.ReferencedProjectsConfiguration.Should().BeEmpty(); - - task.DiscoveryPatterns.Should().BeEmpty(); - - task.Assets.Length.Should().Be(1); - - var asset = task.Assets[0]; - - asset.GetMetadata(nameof(StaticWebAsset.Identity)).Should().BeEquivalentTo($"{identity}"); - - asset.GetMetadata(nameof(StaticWebAsset.SourceId)).Should().BeEquivalentTo("ComponentApp"); - - asset.GetMetadata(nameof(StaticWebAsset.SourceType)).Should().BeEquivalentTo("Computed"); - - asset.GetMetadata(nameof(StaticWebAsset.ContentRoot)).Should().BeEquivalentTo($"{contentRoot}"); - - asset.GetMetadata(nameof(StaticWebAsset.BasePath)).Should().BeEquivalentTo("_content/ComponentApp"); - - asset.GetMetadata(nameof(StaticWebAsset.RelativePath)).Should().BeEquivalentTo("ComponentApp.styles.css"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetKind)).Should().BeEquivalentTo("All"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetMode)).Should().BeEquivalentTo("CurrentProject"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetRole)).Should().BeEquivalentTo("Primary"); - - asset.GetMetadata(nameof(StaticWebAsset.RelatedAsset)).Should().BeEquivalentTo(""); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitName)).Should().BeEquivalentTo("ScopedCss"); - - asset.GetMetadata(nameof(StaticWebAsset.AssetTraitValue)).Should().BeEquivalentTo("ApplicationBundle"); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToOutputDirectory)).Should().BeEquivalentTo("Never"); - - asset.GetMetadata(nameof(StaticWebAsset.CopyToPublishDirectory)).Should().BeEquivalentTo("PreserveNewest"); - - asset.GetMetadata(nameof(StaticWebAsset.OriginalItemSpec)).Should().BeEquivalentTo($"{identity}"); - - - - task.Endpoints.Length.Should().Be(1); - - var endpoint = task.Endpoints[0]; - - endpoint.ItemSpec.Should().BeEquivalentTo("_content/ComponentApp/ComponentApp.styles.css"); - - endpoint.GetMetadata(nameof(StaticWebAssetEndpoint.AssetFile)).Should().BeEquivalentTo($"{identity}"); - - endpoint.GetMetadata(nameof(StaticWebAssetEndpoint.Selectors)).Should().BeEquivalentTo("""[{"Name":"Content-Encoding","Value":"gzip","Quality":"0.9"}]"""); - - endpoint.GetMetadata(nameof(StaticWebAssetEndpoint.ResponseHeaders)) - - .Should() - - .BeEquivalentTo("""[{"Name":"Content-Length","Value":"__content-length__"},{"Name":"Content-Type","Value":"text/css"},{"Name":"ETag","Value":"__etag__"},{"Name":"Last-Modified","Value":"__last-modified__"}]"""); - - } - - - - [TestMethod] - - public void ConvertsReferencedProjectsConfigurationsToTaskItems() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var contentRoot = Path.GetFullPath("."); - - var identity = Path.Combine(contentRoot, "AnotherClassLib", "AnotherClassLib.csproj"); - - var encodedIdentity = JsonEncodedText.Encode(identity); - - var manifest = $@"{{ - - ""Version"": 1, - - ""Hash"": ""__hash__"", - - ""Source"": ""ComponentApp"", - - ""BasePath"": ""_content/ComponentApp"", - - ""Mode"": ""Default"", - - ""ManifestType"": ""Build"", - - ""ReferencedProjectsConfiguration"": [ - - {{ - - ""Identity"": ""{encodedIdentity}"", - - ""Version"": 2, - - ""Source"": ""AnotherClassLib"", - - ""GetPublishAssetsTargets"": ""ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems"", - - ""AdditionalPublishProperties"": "";"", - - ""AdditionalPublishPropertiesToRemove"": "";WebPublishProfileFile"", - - ""GetBuildAssetsTargets"": ""GetCurrentProjectBuildStaticWebAssetItems"", - - ""AdditionalBuildProperties"": "";"", - - ""AdditionalBuildPropertiesToRemove"": "";WebPublishProfileFile"" - - }} - - ], - - ""DiscoveryPatterns"": [], - - ""Assets"": [], - - ""Endpoints"": [] - - }}"; - - File.WriteAllText(TempFilePath, manifest); - - - - var task = new ReadStaticWebAssetsManifestFile - - { - - BuildEngine = buildEngine.Object, + ManifestPath = TempFilePath + }; - - ManifestPath = TempFilePath - - - }; - - - - - - // Act - - - var result = task.Execute(); - - - - + // Act + var result = task.Execute(); // Assert - - result.Should().Be(true); - - task.ReferencedProjectsConfiguration.Length.Should().Be(1); - - task.Assets.Should().BeEmpty(); - - task.Endpoints.Should().BeEmpty(); - - task.DiscoveryPatterns.Should().BeEmpty(); - - var projectConfiguration = task.ReferencedProjectsConfiguration[0]; - - projectConfiguration.ItemSpec.Should().BeEquivalentTo(identity); - - projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.Version)).Should().BeEquivalentTo("2"); - - projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.Source)).Should().BeEquivalentTo("AnotherClassLib"); - - projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.GetPublishAssetsTargets)).Should().BeEquivalentTo("ComputeReferencedStaticWebAssetsPublishManifest;GetCurrentProjectPublishStaticWebAssetItems"); - - projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.AdditionalPublishProperties)).Should().BeEquivalentTo(";"); - - projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.AdditionalPublishPropertiesToRemove)).Should().BeEquivalentTo(";WebPublishProfileFile"); - - projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.GetBuildAssetsTargets)).Should().BeEquivalentTo("GetCurrentProjectBuildStaticWebAssetItems"); - - projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.AdditionalBuildProperties)).Should().BeEquivalentTo(";"); - - projectConfiguration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.AdditionalBuildPropertiesToRemove)).Should().BeEquivalentTo(";WebPublishProfileFile"); - - } - - - - [TestMethod] - - public void ConvertsDiscoveryPatternsToTaskItems() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var contentRoot = Path.Combine(Path.GetFullPath("."), "AnotherClassLib", "wwwroot"); - - var encodedContentRoot = JsonEncodedText.Encode(contentRoot); - - var manifest = $@"{{ - - ""Version"": 1, - - ""Hash"": ""__hash__"", - - ""Source"": ""ComponentApp"", - - ""BasePath"": ""_content/ComponentApp"", - - ""Mode"": ""Default"", - - ""ManifestType"": ""Build"", - - ""ReferencedProjectsConfiguration"": [ ], - - ""DiscoveryPatterns"": [ - - {{ - - ""Name"": ""AnotherClassLib\\wwwroot"", - - ""Source"": ""AnotherClassLib"", - - ""ContentRoot"": ""{encodedContentRoot}"", - - ""BasePath"": ""_content/AnotherClassLib"", - - ""Pattern"": ""**"" - - }} - - ], - - ""Assets"": [], - - ""Endpoints"": [] - - }}"; - - File.WriteAllText(TempFilePath, manifest); - - - - var task = new ReadStaticWebAssetsManifestFile - - { - - BuildEngine = buildEngine.Object, - - ManifestPath = TempFilePath - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - task.DiscoveryPatterns.Length.Should().Be(1); - - task.ReferencedProjectsConfiguration.Should().BeEmpty(); - - task.Assets.Should().BeEmpty(); - - task.Endpoints.Should().BeEmpty(); - - var discoveryPattern = task.DiscoveryPatterns[0]; - - - - discoveryPattern.ItemSpec.Should().BeEquivalentTo(Path.Combine("AnotherClassLib", "wwwroot")); - - discoveryPattern.GetMetadata(nameof(StaticWebAssetsDiscoveryPattern.Source)).Should().BeEquivalentTo("AnotherClassLib"); - - discoveryPattern.GetMetadata(nameof(StaticWebAssetsDiscoveryPattern.ContentRoot)).Should().BeEquivalentTo($"{contentRoot}"); - - discoveryPattern.GetMetadata(nameof(StaticWebAssetsDiscoveryPattern.BasePath)).Should().BeEquivalentTo("_content/AnotherClassLib"); - - discoveryPattern.GetMetadata(nameof(StaticWebAssetsDiscoveryPattern.Pattern)).Should().BeEquivalentTo("**"); - - } - - - - [TestMethod] - - public void ReturnsErrorwhenManifestDoesNotExist() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ReadStaticWebAssetsManifestFile - - { - - BuildEngine = buildEngine.Object, - - ManifestPath = "nonexisting.staticwebassets.json" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(false); - - errorMessages.Count.Should().Be(1); - - errorMessages[0].Should().Be("Manifest file at 'nonexisting.staticwebassets.json' not found."); - - task.Assets.Should().BeNull(); - - task.DiscoveryPatterns.Should().BeNull(); - - task.ReferencedProjectsConfiguration.Should().BeNull(); - - } [TestMethod] @@ -1075,86 +384,30 @@ public void ReturnsErrorWhenManifestPathIsEmpty() [TestMethod] public void ReturnsErrorwhenManifestIsMalformed() - - { - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var manifest = "{"; - - File.WriteAllText(TempFilePath, manifest); - - var task = new ReadStaticWebAssetsManifestFile - - { - - BuildEngine = buildEngine.Object, - - ManifestPath = TempFilePath - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(false); - - errorMessages.Count.Should().Be(1); - - task.Assets.Should().BeNull(); - - task.Endpoints.Should().BeNull(); - - task.DiscoveryPatterns.Should().BeNull(); - - task.ReferencedProjectsConfiguration.Should().BeNull(); - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveAllScopedCssAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveAllScopedCssAssetsTest.cs index 9a96f4422cab..ed251ae21631 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveAllScopedCssAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveAllScopedCssAssetsTest.cs @@ -6,363 +6,124 @@ using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.Build.Utilities; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - namespace Microsoft.NET.Sdk.Razor.Test - - { - - [TestClass] - public class ResolveAllScopedCssAssetsTest - - { - - [TestMethod] - - public void ResolveAllScopedCssAssets_IgnoresRegularCssFiles() - - { - - // Arrange - - var taskInstance = new ResolveAllScopedCssAssets() - - { - - StaticWebAssets = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.razor.rz.scp.css", new Dictionary - - { - - ["RelativePath"] = "Pages/Counter.razor.rz.scp.css" - - }), - - new TaskItem("site.css", new Dictionary - - { - - ["RelativePath"] = "site.css" - - }), - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.ScopedCssAssets.Should().ContainSingle(); - - taskInstance.ScopedCssAssets.Should().NotContain(scopedCssAsset => scopedCssAsset.ItemSpec == "site.css"); - - } - - - - [TestMethod] - - public void ResolveAllScopedCssAssets_DetectsScopedCssFiles() - - { - - // Arrange - - var taskInstance = new ResolveAllScopedCssAssets() - - { - - StaticWebAssets = new[] - - { - - new TaskItem("TestFiles/Pages/Counter.razor.rz.scp.css", new Dictionary - - { - - ["RelativePath"] = "Pages/Counter.razor.rz.scp.css" - - }), - - new TaskItem("site.css", new Dictionary - - { - - ["RelativePath"] = "site.css" - - }), - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.ScopedCssAssets.Should().ContainSingle(); - - taskInstance.ScopedCssAssets.Should().Contain(scopedCssAsset => scopedCssAsset.ItemSpec == "TestFiles/Pages/Counter.razor.rz.scp.css"); - - } - - - - [TestMethod] - - public void ResolveAllScopedCssAssets_DetectsScopedCssProjectBundleFiles() - - { - - // Arrange - - var taskInstance = new ResolveAllScopedCssAssets() - - { - - StaticWebAssets = new[] - - { - - new TaskItem("Folder/Project.bundle.scp.css", new Dictionary - - { - - ["RelativePath"] = "Project.bundle.scp.css" - - }), - - new TaskItem("site.css", new Dictionary - - { - - ["RelativePath"] = "site.css" - - }), - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.ScopedCssProjectBundles.Should().ContainSingle(); - - taskInstance.ScopedCssProjectBundles.Should().Contain(scopedCssBundle => scopedCssBundle.ItemSpec == "Folder/Project.bundle.scp.css"); - - } - - - - [TestMethod] - - public void ResolveAllScopedCssAssets_IgnoresScopedCssApplicationBundleFiles() - - { - - // Arrange - - var taskInstance = new ResolveAllScopedCssAssets() - - { - - StaticWebAssets = new[] - - { - - new TaskItem("Folder/Project.styles.css", new Dictionary - - { - - ["RelativePath"] = "Project.styles.css" - - }), - - new TaskItem("site.css", new Dictionary - - { - - ["RelativePath"] = "site.css" - - }), - - } - - }; - - - - // Act - - var result = taskInstance.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - taskInstance.ScopedCssProjectBundles.Should().BeEmpty(); - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveCompressedAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveCompressedAssetsTest.cs index b4077791136f..33dc00d201c4 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveCompressedAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveCompressedAssetsTest.cs @@ -2,1171 +2,397 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Diagnostics.Metrics; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - using NuGet.ContentModel; - - using NuGet.Packaging.Core; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - - - [TestClass] public class ResolveCompressedAssetsTest - - { - - private readonly List _errorMessages; - - private readonly Mock _buildEngine; - - - - public string ItemSpec { get; } - - - - public string OriginalItemSpec { get; } - - - - public string OutputBasePath { get; } - - - - public ResolveCompressedAssetsTest() - - { - - OutputBasePath = Path.Combine(SdkTestContext.Current.TestExecutionDirectory, nameof(ResolveCompressedAssetsTest)); - - ItemSpec = Path.Combine(OutputBasePath, Guid.NewGuid().ToString("N") + ".tmp"); - - OriginalItemSpec = Path.Combine(OutputBasePath, Guid.NewGuid().ToString("N") + ".tmp"); - - _errorMessages = new List(); - - _buildEngine = new Mock(); - - _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => _errorMessages.Add(args.Message)); - - } - - - - [TestMethod] - - public void ResolvesExplicitlyProvidedAssets() - - { - - // Arrange - - var asset = CreatePrimaryAsset(); - - - - var gzipExplicitAsset = new TaskItem(asset.ItemSpec, asset.CloneCustomMetadata()); - - var brotliExplicitAsset = new TaskItem(asset.ItemSpec, asset.CloneCustomMetadata()); - - - - var task = new ResolveCompressedAssets() - - { - - OutputPath = OutputBasePath, - - BuildEngine = _buildEngine.Object, - - CandidateAssets = new[] { asset }, - - Formats = "gzip;brotli", - - ExplicitAssets = new[] { gzipExplicitAsset, brotliExplicitAsset }, - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(2); - - task.AssetsToCompress[0].ItemSpec.Should().EndWith(".gz"); - - task.AssetsToCompress[1].ItemSpec.Should().EndWith(".br"); - - } - - - - [TestMethod] - - public void InfersPreCompressedAssetsCorrectly() - - { - - - - var uncompressedCandidate = new StaticWebAsset - - { - - Identity = Path.Combine(Environment.CurrentDirectory, "wwwroot", "js", "site.js"), - - RelativePath = "js/site#[.{fingerprint}]?.js", - - BasePath = "_content/Test", - - AssetMode = StaticWebAsset.AssetModes.All, - - AssetKind = StaticWebAsset.AssetKinds.All, - - AssetMergeSource = string.Empty, - - SourceId = "Test", - - CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, - - Fingerprint = "xtxxf3hu2r", - - RelatedAsset = string.Empty, - - ContentRoot = Path.Combine(Environment.CurrentDirectory,"wwwroot"), - - SourceType = StaticWebAsset.SourceTypes.Discovered, - - Integrity = "hRQyftXiu1lLX2P9Ly9xa4gHJgLeR1uGN5qegUobtGo=", - - FileLength = 10, - - LastWriteTime = DateTime.UtcNow, - - AssetRole = StaticWebAsset.AssetRoles.Primary, - - AssetMergeBehavior = string.Empty, - - AssetTraitValue = string.Empty, - - AssetTraitName = string.Empty, - - OriginalItemSpec = Path.Combine("wwwroot", "js", "site.js"), - - CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest - - }; - - - - var compressedCandidate = new StaticWebAsset - - { - - Identity = Path.Combine(Environment.CurrentDirectory, "wwwroot", "js", "site.js.gz"), - - RelativePath = "js/site.js#[.{fingerprint}]?.gz", - - BasePath = "_content/Test", - - AssetMode = StaticWebAsset.AssetModes.All, - - AssetKind = StaticWebAsset.AssetKinds.All, - - AssetMergeSource = string.Empty, - - SourceId = "Test", - - CopyToOutputDirectory = StaticWebAsset.AssetCopyOptions.Never, - - Fingerprint = "es13vhk42b", - - RelatedAsset = string.Empty, - - ContentRoot = Path.Combine(Environment.CurrentDirectory, "wwwroot"), - - SourceType = StaticWebAsset.SourceTypes.Discovered, - - Integrity = "zs5Fd3XI6+g9f4N1SFLVdgghuiqdvq+nETAjTbvVxx4=", - - AssetRole = StaticWebAsset.AssetRoles.Primary, - - AssetMergeBehavior = string.Empty, - - AssetTraitValue = string.Empty, - - AssetTraitName = string.Empty, - - OriginalItemSpec = Path.Combine("wwwroot", "js", "site.js.gz"), - - CopyToPublishDirectory = StaticWebAsset.AssetCopyOptions.PreserveNewest, - - FileLength = 10, - - LastWriteTime = DateTime.UtcNow - - }; - - - - var task = new ResolveCompressedAssets - - { - - OutputPath = OutputBasePath, - - CandidateAssets = [uncompressedCandidate.ToTaskItem(), compressedCandidate.ToTaskItem()], - - Formats = "gzip", - - BuildEngine = _buildEngine.Object - - }; - - - - var result = task.Execute(); - - - - result.Should().BeTrue(); - - task.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(0); - - } - - - - [TestMethod] - - public void ResolvesAssetsMatchingIncludePattern() - - { - - // Arrange - - var asset = CreatePrimaryAsset(); - - - - var task = new ResolveCompressedAssets() - - { - - OutputPath = OutputBasePath, - - BuildEngine = _buildEngine.Object, - - CandidateAssets = new[] { asset }, - - IncludePatterns = "**\\*.tmp", - - Formats = "gzip;brotli", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(2); - - task.AssetsToCompress[0].ItemSpec.Should().EndWith(".gz"); - - task.AssetsToCompress[1].ItemSpec.Should().EndWith(".br"); - - } - - - - [TestMethod] - - public void ResolvesAssets_WithFingerprint_MatchingIncludePattern() - - { - - // Arrange - - var asset = CreatePrimaryAsset( - - Path.GetFileNameWithoutExtension(ItemSpec) + "#[.{fingerprint}]" + Path.GetExtension(ItemSpec)); - - - - var task = new ResolveCompressedAssets() - - { - - OutputPath = OutputBasePath, - - BuildEngine = _buildEngine.Object, - - CandidateAssets = new[] { asset }, - - IncludePatterns = "**\\*.tmp", - - Formats = "gzip;brotli", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(2); - - task.AssetsToCompress[0].ItemSpec.Should().EndWith(".gz"); - - var relativePath = task.AssetsToCompress[0].GetMetadata("RelativePath"); - - relativePath.Should().EndWith(".gz"); - - relativePath = Path.GetFileNameWithoutExtension(relativePath); - - relativePath.Should().EndWith(".tmp"); - - relativePath = Path.GetFileNameWithoutExtension(relativePath); - - relativePath.Should().EndWith("#[.{fingerprint=v1}]"); - - task.AssetsToCompress[1].ItemSpec.Should().EndWith(".br"); - - relativePath = task.AssetsToCompress[1].GetMetadata("RelativePath"); - - relativePath.Should().EndWith(".br"); - - relativePath = Path.GetFileNameWithoutExtension(relativePath); - - relativePath.Should().EndWith(".tmp"); - - relativePath = Path.GetFileNameWithoutExtension(relativePath); - - relativePath.Should().EndWith("#[.{fingerprint=v1}]"); - - } - - - - [TestMethod] - - public void ExcludesAssetsMatchingExcludePattern() - - { - - // Arrange - - var asset = CreatePrimaryAsset(); - - - - var task = new ResolveCompressedAssets() - - { - - OutputPath = OutputBasePath, - - BuildEngine = _buildEngine.Object, - - IncludePatterns = "**\\*", - - ExcludePatterns = "**\\*.tmp", - - CandidateAssets = new[] { asset }, - - Formats = "gzip;brotli" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.AssetsToCompress.Should().HaveCount(0); - - } - - - - [TestMethod] - - public void DeduplicatesAssetsResolvedBothExplicitlyAndFromPattern() - - { - - // Arrange - - var asset = CreatePrimaryAsset(); - - - - var gzipExplicitAsset = new TaskItem(asset.ItemSpec, asset.CloneCustomMetadata()); - - var brotliExplicitAsset = new TaskItem(asset.ItemSpec, asset.CloneCustomMetadata()); - - - - var buildTask = new ResolveCompressedAssets() - - { - - OutputPath = OutputBasePath, - - BuildEngine = _buildEngine.Object, - - CandidateAssets = new[] { asset }, - - IncludePatterns = "**\\*.tmp", - - ExplicitAssets = new[] { gzipExplicitAsset, brotliExplicitAsset }, - - Formats = "gzip;brotli" - - }; - - - - // Act - - var buildResult = buildTask.Execute(); - - - - // Assert - - buildResult.Should().BeTrue(); - - buildTask.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(2); - - buildTask.AssetsToCompress[0].ItemSpec.Should().EndWith(".gz"); - - buildTask.AssetsToCompress[1].ItemSpec.Should().EndWith(".br"); - - } - - - - [TestMethod] - - [DataRow("gzip", ".gz", "brotli", ".br")] - - [DataRow("brotli", ".br", "gzip", ".gz")] - - public void IgnoresAssetsCompressedInPreviousTaskRun( - - string phase1Format, string phase1Ext, string _, string phase2Ext) - - { - - // Arrange - - var asset = CreatePrimaryAsset(); - - - - // Act/Assert - - var task1 = new ResolveCompressedAssets() - - { - - OutputPath = OutputBasePath, - - BuildEngine = _buildEngine.Object, - - CandidateAssets = new[] { asset }, - - IncludePatterns = "**\\*.tmp", - - Formats = phase1Format, - - }; - - - - var result1 = task1.Execute(); - - - - result1.Should().BeTrue(); - - task1.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(1); - - task1.AssetsToCompress[0].ItemSpec.Should().EndWith(phase1Ext); - - task1.AssetsToCompress[0].SetMetadata("Fingerprint", "v1" + phase1Ext.TrimStart('.')); - - task1.AssetsToCompress[0].SetMetadata("Integrity", "abc" + phase1Ext.TrimStart('.')); - - - - var explicitAsset = new TaskItem(asset.ItemSpec, asset.CloneCustomMetadata()); - - explicitAsset.SetMetadata("Fingerprint", "v2"); - - explicitAsset.SetMetadata("Integrity", "def"); - - - - var task2 = new ResolveCompressedAssets() - - { - - OutputPath = OutputBasePath, - - BuildEngine = _buildEngine.Object, - - CandidateAssets = new[] { asset, task1.AssetsToCompress[0] }, - - IncludePatterns = "**\\*.tmp", - - ExplicitAssets = new[] { explicitAsset }, - - Formats = "gzip;brotli" - - }; - - - - var result2 = task2.Execute(); - - - - result2.Should().BeTrue(); - - task2.AssetsToCompress.TakeWhile(a => a != null).Should().HaveCount(1); - - task2.AssetsToCompress[0].ItemSpec.Should().EndWith(phase2Ext); - - } - - - - [TestMethod] - - public void ProducesDistinctIdentities_ForGroupVariantsWithIdenticalContent() - - { - - // Arrange — two assets that differ only in AssetGroups but have the same - - // SourceId, BasePath, AssetKind, RelativePath (after token stripping) and - - // Fingerprint (identical file content). Before the fix, these would produce - - // the same compressed asset Identity and crash in ToAssetDictionary. - - var v4ItemSpec = Path.Combine(OutputBasePath, "staticwebassets", "V4", "css", "site.css"); - - var v5ItemSpec = Path.Combine(OutputBasePath, "staticwebassets", "V5", "css", "site.css"); - - - - var v4Asset = new StaticWebAsset() - - { - - Identity = v4ItemSpec, - - OriginalItemSpec = v4ItemSpec, - - RelativePath = "#[{BootstrapVersion}/]~css/site#[.{fingerprint}]?.css", - - ContentRoot = Path.Combine(OutputBasePath, "staticwebassets"), - - SourceType = StaticWebAsset.SourceTypes.Package, - - SourceId = "Microsoft.AspNetCore.Identity.UI", - - BasePath = "_content/Microsoft.AspNetCore.Identity.UI", - - AssetKind = StaticWebAsset.AssetKinds.All, - - AssetMode = StaticWebAsset.AssetModes.All, - - AssetRole = StaticWebAsset.AssetRoles.Primary, - - AssetGroups = "BootstrapVersion=V4", - - Fingerprint = "samehash123", - - Integrity = "sameintegrity", - - FileLength = 42, - - LastWriteTime = DateTime.UtcNow - - }.ToTaskItem(); - - - - var v5Asset = new StaticWebAsset() - - { - - Identity = v5ItemSpec, - - OriginalItemSpec = v5ItemSpec, - - RelativePath = "#[{BootstrapVersion}/]~css/site#[.{fingerprint}]?.css", - - ContentRoot = Path.Combine(OutputBasePath, "staticwebassets"), - - SourceType = StaticWebAsset.SourceTypes.Package, - - SourceId = "Microsoft.AspNetCore.Identity.UI", - - BasePath = "_content/Microsoft.AspNetCore.Identity.UI", - - AssetKind = StaticWebAsset.AssetKinds.All, - - AssetMode = StaticWebAsset.AssetModes.All, - - AssetRole = StaticWebAsset.AssetRoles.Primary, - - AssetGroups = "BootstrapVersion=V5", - - Fingerprint = "samehash123", - - Integrity = "sameintegrity", - - FileLength = 42, - - LastWriteTime = DateTime.UtcNow - - }.ToTaskItem(); - - - - var task = new ResolveCompressedAssets() - - { - - OutputPath = OutputBasePath, - - BuildEngine = _buildEngine.Object, - - CandidateAssets = new[] { v4Asset, v5Asset }, - - IncludePatterns = "**/*.css", - - Formats = "gzip", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var compressed = task.AssetsToCompress.TakeWhile(a => a != null).ToArray(); - - compressed.Should().HaveCount(2); - - compressed[0].ItemSpec.Should().EndWith(".gz"); - - compressed[1].ItemSpec.Should().EndWith(".gz"); - - - - // The critical assertion: the two compressed assets must have different Identities - - // so they don't collide when added to a dictionary keyed by Identity. - - compressed[0].ItemSpec.Should().NotBe(compressed[1].ItemSpec, - - "group variants with identical content must produce distinct compressed asset identities"); - - } - - - - private ITaskItem CreatePrimaryAsset(string relativePath = null) - - { - - return new StaticWebAsset() - - { - - Identity = ItemSpec, - - OriginalItemSpec = OriginalItemSpec, - - RelativePath = relativePath ?? Path.GetFileName(ItemSpec), - - ContentRoot = Path.GetDirectoryName(ItemSpec), - - SourceType = StaticWebAsset.SourceTypes.Discovered, - - SourceId = "App", - - AssetKind = StaticWebAsset.AssetKinds.All, - - AssetMode = StaticWebAsset.AssetModes.All, - - AssetRole = StaticWebAsset.AssetRoles.Primary, - - Fingerprint = "v1", - - Integrity = "abc", - - FileLength = 10, - - LastWriteTime = DateTime.UtcNow - - }.ToTaskItem(); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveFingerprintedStaticWebAssetEndpointsForAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveFingerprintedStaticWebAssetEndpointsForAssetsTest.cs index 39796abaebcd..4917cdabb5d9 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveFingerprintedStaticWebAssetEndpointsForAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ResolveFingerprintedStaticWebAssetEndpointsForAssetsTest.cs @@ -2,886 +2,302 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; - - - - [TestClass] public class ResolveFingerprintedStaticWebAssetEndpointsForAssetsTest - - { - - [TestMethod] - - [DataRow("candidate#[.{fingerprint}]?.js", "candidate.js")] - - [DataRow("candidate#[.{fingerprint}]!.js", "candidate.asdf1234.js")] - - public void Standalone_Selects_EndpointMatching_FilePath(string pattern, string expectedRoute) - - { - - var now = DateTime.Now; - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - List candidateAssets = [ - - CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - pattern, - - "All", - - "All", - - "asdf1234", - - "integrity" - - ) - - ]; - - - - var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); - - - - var resolvedEndpoints = new ResolveFingerprintedStaticWebAssetEndpointsForAssets - - { - - CandidateAssets = [.. candidateAssets], - - CandidateEndpoints = [..endpoints.Select(e => e.ToTaskItem())], - - IsStandalone = true, - - BuildEngine = buildEngine.Object - - }; - - - - // Act - - var result = resolvedEndpoints.Execute(); - - result.Should().BeTrue(); - - - - // Assert - - resolvedEndpoints.ResolvedEndpoints.Should().HaveCount(1); - - var endpoint = StaticWebAssetEndpoint.FromTaskItem(resolvedEndpoints.ResolvedEndpoints[0]); - - - - endpoint.Route.Should().Be(expectedRoute); - - } - - - - [TestMethod] - - public void StandaloneFails_MatchingEndpointNotFound() - - { - - var now = DateTime.Now; - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - List candidateAssets = [ - - CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate#[.{fingerprint}]!.js", - - "All", - - "All", - - "asdf1234", - - "integrity" - - ) - - ]; - - - - var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); - - endpoints = endpoints.Where(e => !e.Route.Contains("asdf1234")).ToArray(); - - - - var resolvedEndpoints = new ResolveFingerprintedStaticWebAssetEndpointsForAssets - - { - - CandidateAssets = [.. candidateAssets], - - CandidateEndpoints = [.. endpoints.Select(e => e.ToTaskItem())], - - IsStandalone = true, - - BuildEngine = buildEngine.Object - - }; - - - - // Act - - var result = resolvedEndpoints.Execute(); - - result.Should().BeFalse(); - - } - - - - [TestMethod] - - [DataRow("candidate#[.{fingerprint}]?.js", "candidate.asdf1234.js")] - - [DataRow("candidate#[.{fingerprint}]!.js", "candidate.asdf1234.js")] - - public void Hosted_AlwaysPrefers_FingerprintedEndpoint(string pattern, string expectedRoute) - - { - - var now = DateTime.Now; - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - List candidateAssets = [ - - CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - pattern, - - "All", - - "All", - - "asdf1234", - - "integrity" - - ) - - ]; - - - - var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); - - - - var resolvedEndpoints = new ResolveFingerprintedStaticWebAssetEndpointsForAssets - - { - - CandidateAssets = [.. candidateAssets], - - CandidateEndpoints = [.. endpoints.Select(e => e.ToTaskItem())], - - IsStandalone = false, - - BuildEngine = buildEngine.Object - - }; - - - - // Act - - var result = resolvedEndpoints.Execute(); - - result.Should().BeTrue(); - - - - // Assert - - resolvedEndpoints.ResolvedEndpoints.Should().HaveCount(1); - - var endpoint = StaticWebAssetEndpoint.FromTaskItem(resolvedEndpoints.ResolvedEndpoints[0]); - - - - endpoint.Route.Should().Be(expectedRoute); - - } - - - - [TestMethod] - - public void Hosted_FallsBackToNonFingerprintedEndpoint_WhenFingerprintedVersionNotAvailable() - - { - - var now = DateTime.Now; - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - List candidateAssets = [ - - CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "asdf1234", - - "integrity" - - ) - - ]; - - - - var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); - - - - var resolvedEndpoints = new ResolveFingerprintedStaticWebAssetEndpointsForAssets - - { - - CandidateAssets = [.. candidateAssets], - - CandidateEndpoints = [.. endpoints.Select(e => e.ToTaskItem())], - - IsStandalone = false, - - BuildEngine = buildEngine.Object - - }; - - - - // Act - - var result = resolvedEndpoints.Execute(); - - result.Should().BeTrue(); - - - - // Assert - - resolvedEndpoints.ResolvedEndpoints.Should().HaveCount(1); - - var endpoint = StaticWebAssetEndpoint.FromTaskItem(resolvedEndpoints.ResolvedEndpoints[0]); - - - - endpoint.Route.Should().Be("candidate.js"); - - } - - - - [TestMethod] - - public void Hosted_FailsWhen_DoesnotFindMatchingEndpoint() - - { - - var now = DateTime.Now; - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - List candidateAssets = [ - - CreateCandidate( - - Path.Combine("wwwroot", "candidate.js"), - - "MyPackage", - - "Discovered", - - "candidate.js", - - "All", - - "All", - - "asdf1234", - - "integrity" - - ) - - ]; - - - - var endpoints = CreateEndpoints(candidateAssets.Select(a => StaticWebAsset.FromTaskItem(a)).ToArray()); - - endpoints = endpoints.Where(e => !e.Route.Contains("asdf1234")).ToArray(); - - endpoints[0].AssetFile = Path.GetFullPath("other.js"); - - - - var resolvedEndpoints = new ResolveFingerprintedStaticWebAssetEndpointsForAssets - - { - - CandidateAssets = [.. candidateAssets], - - CandidateEndpoints = [.. endpoints.Select(e => e.ToTaskItem())], - - IsStandalone = false, - - BuildEngine = buildEngine.Object - - }; - - - - // Act - - var result = resolvedEndpoints.Execute(); - - result.Should().BeFalse(); - - } - - - - private static ITaskItem CreateCandidate( - - string itemSpec, - - string sourceId, - - string sourceType, - - string relativePath, - - string assetKind, - - string assetMode, - - string fingerprint = "", - - string integrity = "", - - string relatedAsset = "", - - string assetTraitName = "", - - string assetTraitValue = "") - - { - - var result = new StaticWebAsset() - - { - - Identity = Path.GetFullPath(itemSpec), - - SourceId = sourceId, - - SourceType = sourceType, - - ContentRoot = Directory.GetCurrentDirectory(), - - BasePath = "base", - - RelativePath = relativePath, - - AssetKind = assetKind, - - AssetMode = assetMode, - - AssetRole = "Primary", - - RelatedAsset = relatedAsset, - - AssetTraitName = assetTraitName, - - AssetTraitValue = assetTraitValue, - - CopyToOutputDirectory = "", - - CopyToPublishDirectory = "", - - OriginalItemSpec = itemSpec, - - // Add these to avoid accessing the disk to compute them - - Integrity = integrity, - - Fingerprint = fingerprint, - - FileLength = 10, - - LastWriteTime = DateTime.UtcNow, - - }; - - - - result.ApplyDefaults(); - - result.Normalize(); - - - - return result.ToTaskItem(); - - } - - - - 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(); - - - - defineStaticWebAssetEndpoints.Execute(); - - return StaticWebAssetEndpoint.FromItemGroup(defineStaticWebAssetEndpoints.Endpoints); - - } - - - - private static TaskItem CreateContentMapping(string pattern, string contentType) - - { - - return new TaskItem(contentType, new Dictionary - - { - - { "Pattern", pattern }, - - { "Priority", "0" } - - }); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/RewriteCssTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/RewriteCssTest.cs index eac98917d57c..49654b5d7ab5 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/RewriteCssTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/RewriteCssTest.cs @@ -2,1147 +2,391 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.StaticWebAssets.Tasks; + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - - - [TestClass] public class RewriteCssTest - - { - - [TestMethod] - - public void HandlesEmptyFile() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", string.Empty, "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(string.Empty, result); - - } - - - - [TestMethod] - - public void AddsScopeAfterSelector() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - .myclass { color: red; } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - .myclass[TestScope] { color: red; } - - ", result); - - } - - - - [TestMethod] - - public void HandlesMultipleSelectors() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - .first, .second { color: red; } - - .third { color: blue; } - - :root { color: green; } - - * { color: white; } - - #some-id { color: yellow; } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - .first[TestScope], .second[TestScope] { color: red; } - - .third[TestScope] { color: blue; } - - :root[TestScope] { color: green; } - - *[TestScope] { color: white; } - - #some-id[TestScope] { color: yellow; } - - ", result); - - } - - - - [TestMethod] - - public void HandlesComplexSelectors() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - .first div > li, body .second:not(.fancy)[attr~=whatever] { color: red; } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - .first div > li[TestScope], body .second:not(.fancy)[attr~=whatever][TestScope] { color: red; } - - ", result); - - } - - - - [TestMethod] - - public void HandlesSpacesAndCommentsWithinSelectors() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - .first /* space at end {} */ div , .myclass /* comment at end */ { color: red; } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - .first /* space at end {} */ div[TestScope] , .myclass[TestScope] /* comment at end */ { color: red; } - - ", result); - - } - - - - [TestMethod] - - public void HandlesPseudoClasses() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - a:fake-pseudo-class { color: red; } - - a:focus b:hover { color: green; } - - tr:nth-child(4n + 1) { color: blue; } - - a:has(b > c) { color: yellow; } - - a:last-child > ::deep b { color: pink; } - - a:not(#something) { color: purple; } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - a:fake-pseudo-class[TestScope] { color: red; } - - a:focus b:hover[TestScope] { color: green; } - - tr:nth-child(4n + 1)[TestScope] { color: blue; } - - a:has(b > c)[TestScope] { color: yellow; } - - a:last-child[TestScope] > b { color: pink; } - - a:not(#something)[TestScope] { color: purple; } - - ", result); - - } - - - - [TestMethod] - - public void HandlesPseudoElements() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - a::before { content: ""✋""; } - - a::after::placeholder { content: ""🐯""; } - - custom-element::part(foo) { content: ""🤷‍""; } - - a::before > ::deep another { content: ""👞""; } - - a::fake-PsEuDo-element { content: ""🐔""; } - - ::selection { content: ""😾""; } - - other, ::selection { content: ""👂""; } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - a[TestScope]::before { content: ""✋""; } - - a[TestScope]::after::placeholder { content: ""🐯""; } - - custom-element[TestScope]::part(foo) { content: ""🤷‍""; } - - a[TestScope]::before > another { content: ""👞""; } - - a[TestScope]::fake-PsEuDo-element { content: ""🐔""; } - - [TestScope]::selection { content: ""😾""; } - - other[TestScope], [TestScope]::selection { content: ""👂""; } - - ", result); - - } - - - - [TestMethod] - - public void HandlesSingleColonPseudoElements() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - a:after { content: ""x""; } - - a:before { content: ""x""; } - - a:first-letter { content: ""x""; } - - a:first-line { content: ""x""; } - - a:AFTER { content: ""x""; } - - a:not(something):before { content: ""x""; } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - a[TestScope]:after { content: ""x""; } - - a[TestScope]:before { content: ""x""; } - - a[TestScope]:first-letter { content: ""x""; } - - a[TestScope]:first-line { content: ""x""; } - - a[TestScope]:AFTER { content: ""x""; } - - a:not(something)[TestScope]:before { content: ""x""; } - - ", result); - - } - - - - [TestMethod] - - public void RespectsDeepCombinator() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - .first ::deep .second { color: red; } - - a ::deep b, c ::deep d { color: blue; } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - .first[TestScope] .second { color: red; } - - a[TestScope] b, c[TestScope] d { color: blue; } - - ", result); - - } - - - - [TestMethod] - - public void RespectsDeepCombinatorWithDirectDescendant() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - a > ::deep b { color: red; } - - c ::deep > d { color: blue; } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - a[TestScope] > b { color: red; } - - c[TestScope] > d { color: blue; } - - ", result); - - } - - - - [TestMethod] - - public void RespectsDeepCombinatorWithAdjacentSibling() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - a + ::deep b { color: red; } - - c ::deep + d { color: blue; } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - a[TestScope] + b { color: red; } - - c[TestScope] + d { color: blue; } - - ", result); - - } - - - - [TestMethod] - - public void RespectsDeepCombinatorWithGeneralSibling() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - a ~ ::deep b { color: red; } - - c ::deep ~ d { color: blue; } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - a[TestScope] ~ b { color: red; } - - c[TestScope] ~ d { color: blue; } - - ", result); - - } - - - - [TestMethod] - - public void IgnoresMultipleDeepCombinators() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - .first ::deep .second ::deep .third { color:red; } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - .first[TestScope] .second ::deep .third { color:red; } - - ", result); - - } - - - - [TestMethod] - - public void RespectsDeepCombinatorWithSpacesAndComments() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - .a .b /* comment ::deep 1 */ ::deep /* comment ::deep 2 */ .c /* ::deep */ .d { color: red; } - - ::deep * { color: blue; } /* Leading deep combinator */ - - - another ::deep { color: green } /* Trailing deep combinator */ - - + another ::deep { color: green } /* Trailing deep combinator */ ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - .a .b[TestScope] /* comment ::deep 1 */ /* comment ::deep 2 */ .c /* ::deep */ .d { color: red; } - - [TestScope] * { color: blue; } /* Leading deep combinator */ - - another[TestScope] { color: green } /* Trailing deep combinator */ - - ", result); - - } - - - - [TestMethod] - - public void HandlesAtBlocks() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - .myclass { color: red; } - - - - @media only screen and (max-width: 600px) { - - .another .thing { - - content: 'This should not be a selector: .fake-selector { color: red }' - - } - - } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - .myclass[TestScope] { color: red; } - - - - @media only screen and (max-width: 600px) { - - .another .thing[TestScope] { - - content: 'This should not be a selector: .fake-selector { color: red }' - - } - - } - - ", result); - - } - - - - [TestMethod] - - public void AddsScopeToKeyframeNames() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - @keyframes my-animation { /* whatever */ } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - @keyframes my-animation-TestScope { /* whatever */ } - - ", result); - - } - - - - [TestMethod] - - public void RewritesAnimationNamesWhenMatchingKnownKeyframes() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - .myclass { - - color: red; - - animation: /* ignore comment */ my-animation 1s infinite; - - } - - - - .another-thing { animation-name: different-animation; } - - - - h1 { animation: unknown-animation; } /* Should not be scoped */ - - - - @keyframes my-animation { /* whatever */ } - - @keyframes different-animation { /* whatever */ } - - @keyframes unused-animation { /* whatever */ } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - .myclass[TestScope] { - - color: red; - - animation: /* ignore comment */ my-animation-TestScope 1s infinite; - - } - - - - .another-thing[TestScope] { animation-name: different-animation-TestScope; } - - - - h1[TestScope] { animation: unknown-animation; } /* Should not be scoped */ - - - - @keyframes my-animation-TestScope { /* whatever */ } - - @keyframes different-animation-TestScope { /* whatever */ } - - @keyframes unused-animation-TestScope { /* whatever */ } - - ", result); - - } - - - - [TestMethod] - - public void RewritesMultipleAnimationNames() - - { - - // Arrange/act - - var result = RewriteCss.AddScopeToSelectors("file.css", @" - - .myclass1 { animation-name: my-animation , different-animation } - - .myclass2 { animation: 4s linear 0s alternate my-animation infinite, different-animation 0s } - - @keyframes my-animation { } - - @keyframes different-animation { } - - ", "TestScope", out var errors); - - - - // Assert - - Assert.IsEmpty(errors); - - Assert.AreEqual(@" - - .myclass1[TestScope] { animation-name: my-animation-TestScope , different-animation-TestScope } - - .myclass2[TestScope] { animation: 4s linear 0s alternate my-animation-TestScope infinite, different-animation-TestScope 0s } - - @keyframes my-animation-TestScope { } - - @keyframes different-animation-TestScope { } - - ", result); - - } - - - - [TestMethod] - - public void RejectsImportStatements() - - { - - // Arrange/act - - RewriteCss.AddScopeToSelectors("file.css", @" - - @import ""basic-import.css""; - - @import ""import-with-media-type.css"" print; - - @import ""import-with-media-query.css"" screen and (orientation:landscape); - - @ImPoRt /* comment */ ""scheme://path/to/complex-import"" /* another-comment */ screen; - - @otheratrule ""should-not-cause-error.css""; - - /* @import ""should-be-ignored-because-it-is-in-a-comment.css""; */ - - .myclass { color: red; } - - ", "TestScope", out var errors); - - - - // Assert var errorList = errors.ToList(); Assert.HasCount(4, errorList); @@ -1150,10 +394,5 @@ public void RejectsImportStatements() Assert.AreEqual("file.css(3,5): @import rules are not supported within scoped CSS files because the loading order would be undefined. @import may only be placed in non-scoped CSS files.", errorList[1].ToString()); Assert.AreEqual("file.css(4,5): @import rules are not supported within scoped CSS files because the loading order would be undefined. @import may only be placed in non-scoped CSS files.", errorList[2].ToString()); Assert.AreEqual("file.css(5,5): @import rules are not supported within scoped CSS files because the loading order would be undefined. @import may only be placed in non-scoped CSS files.", errorList[3].ToString()); - - } - - } - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetEndpointTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetEndpointTest.cs index 8e25846079df..13303ee2adc7 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetEndpointTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetEndpointTest.cs @@ -2,190 +2,70 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - - - [TestClass] public class StaticWebAssetEndpointTest - - { - - [TestMethod] - - [DataRow("App1/css/app.css", "App1")] - - [DataRow("App1", "App1")] - - [DataRow("App1/css/styles/app.css", "App1/css")] - - [DataRow("App1/css/app.css", "")] - - [DataRow("", "")] - - [DataRow("App1\\css\\app.css", "App1")] - - [DataRow("App1/css\\app.css", "App1")] - - [DataRow("App1/App1.lib.module.js", "App1")] - - [DataRow("app1/css/app.css", "App1")] - - [DataRow("APP1/css/app.css", "app1")] - - public void RouteHasPathPrefix_ReturnsTrue_WhenRouteStartsWithPrefixAsPathSegment(string route, string prefix) - - { - - var routeSegments = new List(); - - var prefixSegments = new List(); - - - - var result = StaticWebAssetEndpoint.RouteHasPathPrefix(route, prefix, routeSegments, prefixSegments); - - - - result.Should().BeTrue(); - - } - - - - [TestMethod] - - [DataRow("App1.styles.css", "App1")] - - [DataRow("App1", "App1/css/app.css")] - - [DataRow("App1/js/app.js", "App1/css")] - - [DataRow("App12/css/app.css", "App1")] - - [DataRow("App1Bundle/app.js", "App1")] - - [DataRow("App1.lib.module.js", "App1")] - - public void RouteHasPathPrefix_ReturnsFalse_WhenRouteDoesNotStartWithPrefixAsPathSegment(string route, string prefix) - - { - - var routeSegments = new List(); - - var prefixSegments = new List(); - - - - var result = StaticWebAssetEndpoint.RouteHasPathPrefix(route, prefix, routeSegments, prefixSegments); - - - - result.Should().BeFalse(); - - } - - - - [TestMethod] - - public void RouteHasPathPrefix_ReusesSegmentLists() - - { - - var routeSegments = new List(); - - var prefixSegments = new List(); - - - - StaticWebAssetEndpoint.RouteHasPathPrefix("a/b/c", "a", routeSegments, prefixSegments); - - StaticWebAssetEndpoint.RouteHasPathPrefix("x/y/z", "x/y", routeSegments, prefixSegments); - - - - var result = StaticWebAssetEndpoint.RouteHasPathPrefix("App1/css/app.css", "App1", routeSegments, prefixSegments); - - - - result.Should().BeTrue(); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetPathPatternTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetPathPatternTest.cs index 234cee19c83b..12bdc5ed121f 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetPathPatternTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetPathPatternTest.cs @@ -2,2173 +2,731 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Globalization; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; - - - - [TestClass] public class StaticWebAssetPathPatternTest - - { - - [TestMethod] - - public void CanParse_PathWithNoExpressions() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site.css", "MyApp"); - - var expected = new StaticWebAssetPathPattern("css/site.css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "css/site.css".AsMemory(), IsLiteral = true }] } - - ] - - }; - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_ComplexFingerprintExpression_Middle() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}].css", "MyApp"); - - var expected = new StaticWebAssetPathPattern("css/site#[.{fingerprint}].css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "css/site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, - - new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_ComplexFingerprintExpression_Start() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}].css", "MyApp"); - - var expected = new StaticWebAssetPathPattern("#[.{fingerprint}].css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, - - new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_ComplexFingerprintExpression_End() - - { - - var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]", "MyApp"); - - var expected = new StaticWebAssetPathPattern("site#[.{fingerprint}]") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_ComplexFingerprintExpression_Only() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}]", "MyApp"); - - var expected = new StaticWebAssetPathPattern("#[.{fingerprint}]") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_ComplexFingerprintExpression_Multiple() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}]-#[.{version}].css", "MyApp"); - - var expected = new StaticWebAssetPathPattern("css/site#[.{fingerprint}]-#[.{version}].css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "css/site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, - - new (){ Parts = [ new() { Name = "-".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "version".AsMemory(), IsLiteral = false }] }, - - new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_ComplexFingerprintExpression_ConsecutiveExpressions() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}]#[.{version}].css", "MyApp"); - - var expected = new StaticWebAssetPathPattern("css/site#[.{fingerprint}]#[.{version}].css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "css/site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "version".AsMemory(), IsLiteral = false }] }, - - new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_SimpleFingerprintExpression_Start() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint}].css", "MyApp"); - - var expected = new StaticWebAssetPathPattern("#[{fingerprint}].css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, - - new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_SimpleFingerprintExpression_Middle() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[{fingerprint}].css", "MyApp"); - - var expected = new StaticWebAssetPathPattern("css/site#[{fingerprint}].css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "css/site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, - - new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_SimpleFingerprintExpression_End() - - { - - var pattern = StaticWebAssetPathPattern.Parse("site#[{fingerprint}]", "MyApp"); - - var expected = new StaticWebAssetPathPattern("site#[{fingerprint}]") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_SimpleFingerprintExpression_Only() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint}]", "MyApp"); - - var expected = new StaticWebAssetPathPattern("#[{fingerprint}]") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_SimpleFingerprintExpression_WithEmbeddedValues() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint=value}]", "MyApp"); - - var expected = new StaticWebAssetPathPattern("#[{fingerprint=value}]") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "fingerprint".AsMemory(), Value = "value".AsMemory(), IsLiteral = false }] } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_ComplexExpression_MultipleVariables() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}-{version}].css", "MyApp"); - - var expected = new StaticWebAssetPathPattern("css/site#[.{fingerprint}-{version}].css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "css/site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ - - new() { Name = ".".AsMemory(), IsLiteral = true }, - - new() { Name = "fingerprint".AsMemory(), IsLiteral = false }, - - new() { Name = "-".AsMemory(), IsLiteral = true }, - - new() { Name = "version".AsMemory(), IsLiteral = false } - - ] }, - - new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_ComplexExpression_MultipleConsecutiveVariables() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}{version}].css", "MyApp"); - - var expected = new StaticWebAssetPathPattern("css/site#[.{fingerprint}{version}].css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "css/site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ - - new() { Name = ".".AsMemory(), IsLiteral = true }, - - new() { Name = "fingerprint".AsMemory(), IsLiteral = false }, - - new() { Name = "version".AsMemory(), IsLiteral = false } - - ] }, - - new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_ComplexExpression_StartsWithVariable() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint}.]css", "MyApp"); - - var expected = new StaticWebAssetPathPattern("#[{fingerprint}.]css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "fingerprint".AsMemory(), IsLiteral = false }, new() { Name = ".".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = "css".AsMemory(), IsLiteral = true }] } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_OptionalExpressions_End() - - { - - var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]?", "MyApp"); - - var expected = new StaticWebAssetPathPattern("site#[.{fingerprint}]?") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }], IsOptional = true } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_OptionalPreferredExpressions() - - { - - var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]!", "MyApp"); - - var expected = new StaticWebAssetPathPattern("site#[.{fingerprint}]!") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }], IsOptional = true, IsPreferred = true } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_OptionalExpressions_Start() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}]?site", "MyApp"); - - var expected = new StaticWebAssetPathPattern("#[.{fingerprint}]?site") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }], IsOptional = true }, - - new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] - - }] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_OptionalExpressions_Middle() - - { - - var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]?site", "MyApp"); - - var expected = new StaticWebAssetPathPattern("site#[.{fingerprint}]?site") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }], IsOptional = true }, - - new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] - - }] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_OptionalExpressions_Only() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}]?", "MyApp"); - - var expected = new StaticWebAssetPathPattern("#[.{fingerprint}]?") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }], IsOptional = true } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_MultipleOptionalExpressions() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}]?site#[.{version}]?", "MyApp"); - - var expected = new StaticWebAssetPathPattern("#[.{fingerprint}]?site#[.{version}]?") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }], IsOptional = true }, - - new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }], IsOptional = false }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "version".AsMemory(), IsLiteral = false }], IsOptional = true } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanParse_ConsecutiveOptionalExpressions() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}]?#[.{version}]?", "MyApp"); - - var expected = new StaticWebAssetPathPattern("#[.{fingerprint}]?#[.{version}]?") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }], IsOptional = true }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "version".AsMemory(), IsLiteral = false }], IsOptional = true } - - ] - - }; - - - - Assert.AreEqual(expected, pattern); - - } - - - - [TestMethod] - - public void CanReplaceTokens_PathWithNoExpressions() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site.css", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234" - - }; - - var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - - - - Assert.AreEqual("css/site.css", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_ComplexFingerprintExpression_Middle() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}].css", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234" - - }; - - var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - - - - Assert.AreEqual("css/site.asdf1234.css", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_ComplexFingerprintExpression_Start() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}].css", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234" - - }; - - var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - - - - Assert.AreEqual(".asdf1234.css", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_ComplexFingerprintExpression_End() - - { - - var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234" - - }; - - var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - - - - Assert.AreEqual("site.asdf1234", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_ComplexFingerprintExpression_Only() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[.{fingerprint}]", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234" - - }; - - var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - - - - Assert.AreEqual(".asdf1234", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_ComplexFingerprintExpression_Multiple() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}]-#[.{version}].css", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234", - - }; - - var (path, _) = pattern.ReplaceTokens( - - tokens, - - CreateTestResolver(new Dictionary(StringComparer.OrdinalIgnoreCase) { ["version"] = "v1" })); - - - - Assert.AreEqual("css/site.asdf1234-.v1.css", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_ComplexFingerprintExpression_ConsecutiveExpressions() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}]#[.{version}].css", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234", - - }; - - var (path, _) = pattern.ReplaceTokens( - - tokens, - - CreateTestResolver(new Dictionary(StringComparer.OrdinalIgnoreCase) { ["version"] = "v1" })); - - - - Assert.AreEqual("css/site.asdf1234.v1.css", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_SimpleFingerprintExpression_Start() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint}].css", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234" - - }; - - var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - - - - Assert.AreEqual("asdf1234.css", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_SimpleFingerprintExpression_Middle() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[{fingerprint}].css", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234" - - }; - - var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - - - - Assert.AreEqual("css/siteasdf1234.css", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_SimpleFingerprintExpression_End() - - { - - var pattern = StaticWebAssetPathPattern.Parse("site#[{fingerprint}]", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234" - - }; - - var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - - - - Assert.AreEqual("siteasdf1234", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_SimpleFingerprintExpression_Only() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint}]", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234" - - }; - - var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - - - - Assert.AreEqual("asdf1234", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_SimpleFingerprintExpression_WithEmbeddedValues() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint=embedded}]", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234" - - }; - - var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - - - - Assert.AreEqual("embedded", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_ComplexExpression_MultipleVariables() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}-{version}].css", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234", - - }; - - var (path, _) = pattern.ReplaceTokens( - - tokens, - - CreateTestResolver(new Dictionary(StringComparer.OrdinalIgnoreCase) { ["version"] = "v1" })); - - - - Assert.AreEqual("css/site.asdf1234-v1.css", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_ComplexExpression_MultipleConsecutiveVariables() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}{version}].css", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234", - - }; - - var (path, _) = pattern.ReplaceTokens( - - tokens, - - CreateTestResolver(new Dictionary(StringComparer.OrdinalIgnoreCase) { ["version"] = "v1" })); - - - - Assert.AreEqual("css/site.asdf1234v1.css", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_ComplexExpression_StartsWithVariable() - - { - - var pattern = StaticWebAssetPathPattern.Parse("#[{fingerprint}.]css", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234" - - }; - - var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - - - - Assert.AreEqual("asdf1234.css", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_ThrowsException_IfRequiredExpressionIsValue() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}].css", "MyApp"); - - var tokens = new StaticWebAsset(); - - var exception = Assert.ThrowsExactly(() => pattern.ReplaceTokens(tokens, CreateTestResolver())); - - Assert.AreEqual("Token 'fingerprint' not provided for 'css/site#[.{fingerprint}].css'.", exception.Message); - - } - - - - [TestMethod] - - public void CanReplaceTokens_ThrowsException_MultipleTokenComplexExpression_MissingAtLeastOneValue() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}-{version}].css", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234" - - }; - - var exception = Assert.ThrowsExactly(() => pattern.ReplaceTokens(tokens, CreateTestResolver())); - - Assert.AreEqual("Token 'version' not provided for 'css/site#[.{fingerprint}-{version}].css'.", exception.Message); - - } - - - - [TestMethod] - - public void CanReplaceTokens_OptionalExpression_OmittedWhenValueNotProvided() - - { - - var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]?", "MyApp"); - - var tokens = new StaticWebAsset(); - - var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - - - - Assert.AreEqual("site", path); - - } - - - - [TestMethod] - - public void CanReplaceTokens_OptionalMultipleTokenComplexExpression_OmittedWhenMissingAtLeastOneValue() - - { - - var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}.{version}]?", "MyApp"); - - var tokens = new StaticWebAsset - - { - - Fingerprint = "asdf1234" - - }; - - var (path, _) = pattern.ReplaceTokens(tokens, CreateTestResolver()); - - - - Assert.AreEqual("site", path); - - } - - - - [TestMethod] - - public void CanExpandRoutes_LiteralPatterns() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site.css", "MyApp"); - - var routePatterns = pattern.ExpandPatternExpression(); - - - - Assert.AreSequenceEqual([pattern], routePatterns); - - } - - - - [TestMethod] - - public void CanExpandRoutes_SingleRequiredExpression() - - { - - var pattern = StaticWebAssetPathPattern.Parse("css/site#[.{fingerprint}].css", "MyApp"); - - var routePatterns = pattern.ExpandPatternExpression(); - - - - Assert.AreSequenceEqual([pattern], routePatterns); - - } - - - - [TestMethod] - - public void CanExpandRoutes_SingleOptionalExpression() - - { - - var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]?.css", "MyApp"); - - var routePatterns = pattern.ExpandPatternExpression(); - - - - var expected = new[] - - { - - new StaticWebAssetPathPattern("site.css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } - - ] - - }, - - new StaticWebAssetPathPattern("site#[.{fingerprint}].css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, - - new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } - - ] - - } - - }; - - - - Assert.AreSequenceEqual(expected, routePatterns); - - } - - - - [TestMethod] - - public void CanExpandRoutes_MultipleOptionalExpressions() - - { - - var pattern = StaticWebAssetPathPattern.Parse("site#[.{fingerprint}]?#[.{version}]?.css", "MyApp"); - - var routePatterns = pattern.ExpandPatternExpression(); - - - - var expected = new[] - - { - - new StaticWebAssetPathPattern("site.css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } - - ] - - }, - - new StaticWebAssetPathPattern("site#[.{fingerprint}].css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, - - new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } - - ] - - }, - - new StaticWebAssetPathPattern("site#[.{version}].css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "version".AsMemory(), IsLiteral = false }] }, - - new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } - - ] - - }, - - new StaticWebAssetPathPattern("site#[.{fingerprint}]#[.{version}].css") - - { - - Segments = - - [ - - new (){ Parts = [ new() { Name = "site".AsMemory(), IsLiteral = true }] }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "fingerprint".AsMemory(), IsLiteral = false }] }, - - new (){ Parts = [ new() { Name = ".".AsMemory(), IsLiteral = true }, new() { Name = "version".AsMemory(), IsLiteral = false }] }, - - new (){ Parts = [ new() { Name = ".css".AsMemory(), IsLiteral = true }] } - - ] - - } - - }; - - - - Assert.AreSequenceEqual(expected, routePatterns); - - } - - - - private static StaticWebAssetTokenResolver CreateTestResolver(Dictionary additionalTokens = null) => new(additionalTokens); - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTaskEnvironmentTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTaskEnvironmentTests.cs index 1ce3950a6e7a..abc774bc4b6c 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTaskEnvironmentTests.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTaskEnvironmentTests.cs @@ -2,776 +2,265 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - - - // Test parallelization is disabled assembly-wide via - - // [assembly:CollectionBehavior(DisableTestParallelization = true)] in - - // LegacyStaticWebAssetsV1IntegrationTest.cs, which already isolates the - - // process-CWD mutation these tests perform. - - [DoNotParallelize] [TestClass] - public class StaticWebAssetTaskEnvironmentTests - - { - - [TestMethod] - - public void NormalizeContentRootPath_WithTaskEnvironment_AbsolutizesAgainstProjectDirectory_NotProcessCurrentDirectory() - - { - - WithDecoyCwdAndProjectDirectory((projectDir, _) => - - { - - var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); - - - - var result = StaticWebAsset.NormalizeContentRootPath("wwwroot", env); - - - - result.Should().Be(Path.Combine(projectDir, "wwwroot") + Path.DirectorySeparatorChar, - - "the relative ContentRoot must be resolved against TaskEnvironment.ProjectDirectory, not the process CWD"); - - }); - - } - - - - [TestMethod] - - public void NormalizeContentRootPath_WithoutEnvOverload_StillUsesProcessCurrentDirectory_ForBackCompat() - - { - - WithDecoyCwdAndProjectDirectory((_, spawnDir) => - - { - - // The parameterless overload preserves the pre-existing behavior so unmigrated - - // call sites continue to work; only callers that opt in via the env overload get - - // the MT-safe resolution. - - var result = StaticWebAsset.NormalizeContentRootPath("wwwroot"); - - - - result.Should().Be(Path.Combine(spawnDir, "wwwroot") + Path.DirectorySeparatorChar); - - }); - - } - - - - [TestMethod] - - public void Normalize_WithTaskEnvironment_AbsolutizesContentRootAndRelatedAssetAgainstProjectDirectory() - - { - - WithDecoyCwdAndProjectDirectory((projectDir, _) => - - { - - var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); - - var asset = new StaticWebAsset - - { - - Identity = Path.Combine(projectDir, "site.css"), - - SourceId = "MyProject", - - SourceType = StaticWebAsset.SourceTypes.Discovered, - - ContentRoot = "wwwroot", - - BasePath = "/", - - RelativePath = "site.css", - - RelatedAsset = "related/asset.css", - - }; - - - - asset.Normalize(env); - - - - asset.ContentRoot.Should().Be(Path.Combine(projectDir, "wwwroot") + Path.DirectorySeparatorChar); - - asset.RelatedAsset.Should().Be(Path.Combine(projectDir, "related", "asset.css")); - - }); - - } - - - - [TestMethod] - - public void FromTaskItem_WithTaskEnvironment_HydratesAssetWithProjectDirectoryAbsolutizedPaths() - - { - - WithDecoyCwdAndProjectDirectory((projectDir, _) => - - { - - var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); - - var item = new TaskItem(Path.Combine(projectDir, "site.css"), new Dictionary - - { - - [nameof(StaticWebAsset.SourceId)] = "MyProject", - - [nameof(StaticWebAsset.SourceType)] = StaticWebAsset.SourceTypes.Discovered, - - [nameof(StaticWebAsset.ContentRoot)] = "wwwroot", - - [nameof(StaticWebAsset.BasePath)] = "/", - - [nameof(StaticWebAsset.RelativePath)] = "site.css", - - [nameof(StaticWebAsset.RelatedAsset)] = "", - - }); - - - - var asset = StaticWebAsset.FromTaskItem(item, env); - - - - asset.ContentRoot.Should().Be(Path.Combine(projectDir, "wwwroot") + Path.DirectorySeparatorChar); - - }); - - } - - - - [TestMethod] - - public void FromV1TaskItem_WithTaskEnvironment_HydratesAssetWithProjectDirectoryAbsolutizedPaths() - - { - - WithDecoyCwdAndProjectDirectory((projectDir, _) => - - { - - var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); - - var assetIdentity = Path.Combine(projectDir, "wwwroot", "site.css"); - - Directory.CreateDirectory(Path.GetDirectoryName(assetIdentity)); - - File.WriteAllText(assetIdentity, "body{}"); - - var item = new TaskItem(assetIdentity, new Dictionary - - { - - [nameof(StaticWebAsset.SourceId)] = "SomePackage", - - [nameof(StaticWebAsset.SourceType)] = StaticWebAsset.SourceTypes.Package, - - [nameof(StaticWebAsset.ContentRoot)] = "wwwroot", - - [nameof(StaticWebAsset.BasePath)] = "_content/SomePackage", - - [nameof(StaticWebAsset.RelativePath)] = "site.css", - - [nameof(StaticWebAsset.OriginalItemSpec)] = assetIdentity, - - [nameof(StaticWebAsset.Fingerprint)] = "deadbeef", - - [nameof(StaticWebAsset.Integrity)] = "sha256-fake", - - [nameof(StaticWebAsset.FileLength)] = "6", - - [nameof(StaticWebAsset.LastWriteTime)] = DateTimeOffset.UtcNow.ToString("o"), - - }); - - - - var asset = StaticWebAsset.FromV1TaskItem(item, env); - - - - asset.ContentRoot.Should().Be(Path.Combine(projectDir, "wwwroot") + Path.DirectorySeparatorChar); - - }); - - } - - - - [TestMethod] - - public void FromTaskItemGroup_WithTaskEnvironment_AbsolutizesAllAssets() - - { - - WithDecoyCwdAndProjectDirectory((projectDir, _) => - - { - - var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); - - var items = new ITaskItem[] - - { - - MakeDiscoveredItem(projectDir, contentRoot: "wwwroot", relativePath: "a.css"), - - MakeDiscoveredItem(projectDir, contentRoot: "wwwroot", relativePath: "b.css"), - - - }; - - - - + }; var assets = StaticWebAsset.FromTaskItemGroup(items, env); - - - - assets.Should().AllSatisfy(asset => - - asset.ContentRoot.Should().Be(Path.Combine(projectDir, "wwwroot") + Path.DirectorySeparatorChar)); - - }); - - } - - - - [TestMethod] - - public void ResolveFile_WithTaskEnvironment_ResolvesIdentityRelativeToProjectDirectory() - - { - - WithDecoyCwdAndProjectDirectory((projectDir, _) => - - { - - var realFile = Path.Combine(projectDir, "wwwroot", "site.css"); - - Directory.CreateDirectory(Path.GetDirectoryName(realFile)); - - File.WriteAllText(realFile, "body{}"); - - - - var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); - - - - var info = StaticWebAsset.ResolveFile(Path.Combine("wwwroot", "site.css"), originalItemSpec: "", env); - - - - info.FullName.Should().Be(realFile); - - info.Exists.Should().BeTrue(); - - }); - - } - - - - [TestMethod] - - public void HasContentRoot_WithTaskEnvironment_ComparesAgainstProjectDirectoryNormalizedForm() - - { - - WithDecoyCwdAndProjectDirectory((projectDir, _) => - - { - - var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); - - var asset = new StaticWebAsset - - { - - Identity = Path.Combine(projectDir, "site.css"), - - ContentRoot = "wwwroot", - - BasePath = "/", - - RelativePath = "site.css", - - SourceId = "X", - - SourceType = StaticWebAsset.SourceTypes.Discovered, - - }; - - asset.Normalize(env); - - - - asset.HasContentRoot("wwwroot", env).Should().BeTrue(); - - }); - - } - - - - [TestMethod] - - public void NormalizeContentRootPath_WithTaskEnvironment_PreservesCanonicalization_DotDot() - - { - - WithDecoyCwdAndProjectDirectory((projectDir, _) => - - { - - var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); - - - - var result = StaticWebAsset.NormalizeContentRootPath(Path.Combine("a", "..", "wwwroot"), env); - - - - // Outer Path.GetFullPath collapses ".." segments while the inner env.GetAbsolutePath - - // ensures the base is the project directory. The two together preserve the - - // pre-migration canonicalization semantics callers depend on for equality checks. - - result.Should().Be(Path.Combine(projectDir, "wwwroot") + Path.DirectorySeparatorChar); - - }); - - } - - - - [TestMethod] - - public void Normalize_WithTaskEnvironment_AbsolutePathInputs_ArePreservedAndCanonicalized() - - { - - // The single most common production case: upstream targets pre-absolutize. The new - - // overload must remain a no-op for already-absolute inputs (no surprise re-rooting). - - WithDecoyCwdAndProjectDirectory((projectDir, _) => - - { - - var env = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir); - - var absoluteContentRoot = Path.Combine(projectDir, "alreadyabsolute"); - - var absoluteRelatedAsset = Path.Combine(projectDir, "other", "asset.css"); - - var asset = new StaticWebAsset - - { - - Identity = Path.Combine(projectDir, "site.css"), - - SourceId = "X", - - SourceType = StaticWebAsset.SourceTypes.Discovered, - - ContentRoot = absoluteContentRoot, - - BasePath = "/", - - RelativePath = "site.css", - - RelatedAsset = absoluteRelatedAsset, - - }; - - - - asset.Normalize(env); - - - - asset.ContentRoot.Should().Be(absoluteContentRoot + Path.DirectorySeparatorChar); - - asset.RelatedAsset.Should().Be(absoluteRelatedAsset); - - }); - - } - - - - private static ITaskItem MakeDiscoveredItem(string projectDir, string contentRoot, string relativePath) - - { - - return new TaskItem(Path.Combine(projectDir, "site.css"), new Dictionary - - { - - [nameof(StaticWebAsset.SourceId)] = "MyProject", - - [nameof(StaticWebAsset.SourceType)] = StaticWebAsset.SourceTypes.Discovered, - - [nameof(StaticWebAsset.ContentRoot)] = contentRoot, - - [nameof(StaticWebAsset.BasePath)] = "/", - - [nameof(StaticWebAsset.RelativePath)] = relativePath, - - }); - - } - - - - private static void WithDecoyCwdAndProjectDirectory(Action body) - - { - - var testRoot = Path.Combine(AppContext.BaseDirectory, nameof(StaticWebAssetTaskEnvironmentTests), Guid.NewGuid().ToString("N")); - - var projectDir = Path.Combine(testRoot, "project"); - - var spawnDir = Path.Combine(testRoot, "decoy", "spawn"); - - Directory.CreateDirectory(projectDir); - - Directory.CreateDirectory(spawnDir); - - - - var originalCwd = Directory.GetCurrentDirectory(); - - try - - { - - Directory.SetCurrentDirectory(spawnDir); - - body(projectDir, spawnDir); - - } - - finally - - { - - Directory.SetCurrentDirectory(originalCwd); - - if (Directory.Exists(testRoot)) - - { - - Directory.Delete(testRoot, recursive: true); - - } - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTest.cs index 6de3bb9b4588..8cf81c9f5eeb 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetTest.cs @@ -2,1346 +2,457 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; - - - - [TestClass] public class StaticWebAssetTest - - { - - [TestMethod] - - public void ValidateAssetGroup_SingleAsset_ReturnsTrue() - - { - - var asset = CreateAsset("wwwroot/app.js", "app.js", "All", "All"); - - var group = (asset, (StaticWebAsset)null, (IReadOnlyList)null); - - - - var groupSet = new HashSet(StringComparer.Ordinal); - - var result = StaticWebAsset.ValidateAssetGroup("app.js", group, out var reason, groupSet); - - - - Assert.IsTrue(result); - - Assert.IsNull(reason); - - } - - - - [TestMethod] - - public void ValidateAssetGroup_TwoAssetsFromDifferentProjects_ReturnsFalse() - - { - - var asset1 = CreateAsset("wwwroot/app.js", "app.js", "All", "All", sourceId: "Project1"); - - var asset2 = CreateAsset("wwwroot/app.js", "app.js", "All", "All", sourceId: "Project2"); - - var group = (asset1, asset2, (IReadOnlyList)null); - - - - var groupSet2 = new HashSet(StringComparer.Ordinal); - - var result = StaticWebAsset.ValidateAssetGroup("app.js", group, out var reason, groupSet2); - - - - Assert.IsFalse(result); - - Assert.Contains("different projects", reason); - - } - - - - [TestMethod] - - public void ValidateAssetGroup_TwoAllAssetsFromSameProject_ReturnsFalse() - - { - - var asset1 = CreateAsset("wwwroot/app.js", "app.js", "All", "All"); - - var asset2 = CreateAsset("obj/app.js", "app.js", "All", "All"); - - var group = (asset1, asset2, (IReadOnlyList)null); - - - - var groupSet3 = new HashSet(StringComparer.Ordinal); - - var result = StaticWebAsset.ValidateAssetGroup("app.js", group, out var reason, groupSet3); - - - - Assert.IsFalse(result); - - Assert.Contains("'All' assets", reason); - - } - - - - [TestMethod] - - public void ValidateAssetGroup_BuildAndPublishAssetsFromSameProject_ReturnsTrue() - - { - - var buildAsset = CreateAsset("wwwroot/app.js", "app.js", "Build", "All"); - - var publishAsset = CreateAsset("obj/app.js", "app.js", "Publish", "All"); - - var group = (buildAsset, publishAsset, (IReadOnlyList)null); - - - - var groupSet4 = new HashSet(StringComparer.Ordinal); - - var result = StaticWebAsset.ValidateAssetGroup("app.js", group, out var reason, groupSet4); - - - - Assert.IsTrue(result); - - Assert.IsNull(reason); - - } - - - - [TestMethod] - - public void ComputeTargetPath_WithoutTokenResolver_KeepsTokensInPath() - - { - - var asset = CreateAsset( - - "wwwroot/MyApp.styles.css", - - "MyApp.styles#[.{fingerprint}]?.css", - - "All", - - "All"); - - asset.Fingerprint = "abc123"; - - - - var targetPath = asset.ComputeTargetPath("", '/'); - - - - Assert.AreEqual("MyApp.styles#[.{fingerprint}]?.css", targetPath); - - } - - - - [TestMethod] - - public void ComputeTargetPath_WithTokenResolver_ReplacesOptionalTokens() - - { - - var asset = CreateAsset( - - "wwwroot/MyApp.styles.css", - - "MyApp.styles#[.{fingerprint}]?.css", - - "All", - - "All"); - - asset.Fingerprint = "abc123"; - - - - var targetPath = asset.ComputeTargetPath("", '/', StaticWebAssetTokenResolver.Instance); - - - - Assert.AreEqual("MyApp.styles.css", targetPath); - - } - - - - [TestMethod] - - public void TwoAssetsWithDifferentPatternsResolveToSameTargetPath_AfterTokenReplacement() - - { - - var discoveredAsset = CreateAsset( - - "wwwroot/MyApp.styles.css", - - "MyApp.styles#[.{fingerprint}]?.css", - - "All", - - "All"); - - discoveredAsset.Fingerprint = "abc123"; - - - - var computedAsset = CreateAsset( - - "obj/scopedcss/bundle/MyApp.styles.css", - - "MyApp#[.{fingerprint}]?.styles.css", - - "All", - - "CurrentProject"); - - computedAsset.Fingerprint = "xyz789"; - - - - var path1WithTokens = discoveredAsset.ComputeTargetPath("", '/'); - - var path2WithTokens = computedAsset.ComputeTargetPath("", '/'); - - - - Assert.AreNotEqual(path1WithTokens, path2WithTokens); - - Assert.AreEqual("MyApp.styles#[.{fingerprint}]?.css", path1WithTokens); - - Assert.AreEqual("MyApp#[.{fingerprint}]?.styles.css", path2WithTokens); - - - - var path1Resolved = discoveredAsset.ComputeTargetPath("", '/', StaticWebAssetTokenResolver.Instance); - - var path2Resolved = computedAsset.ComputeTargetPath("", '/', StaticWebAssetTokenResolver.Instance); - - - - Assert.AreEqual("MyApp.styles.css", path1Resolved); - - Assert.AreEqual("MyApp.styles.css", path2Resolved); - - Assert.AreEqual(path1Resolved, path2Resolved); - - } - - - - [TestMethod] - - public void ValidateAssetGroup_DetectsConflict_WhenAssetsHaveDifferentPatterns_ButSameResolvedPath() - - { - - var discoveredAsset = CreateAsset( - - "wwwroot/MyApp.styles.css", - - "MyApp.styles#[.{fingerprint}]?.css", - - "All", - - "All"); - - discoveredAsset.Fingerprint = "abc123"; - - - - var computedAsset = CreateAsset( - - "obj/scopedcss/bundle/MyApp.styles.css", - - "MyApp#[.{fingerprint}]?.styles.css", - - "All", - - "CurrentProject"); - - computedAsset.Fingerprint = "xyz789"; - - - - var group = (discoveredAsset, computedAsset, (IReadOnlyList)null); - - var groupSet = new HashSet(StringComparer.Ordinal); - - var result = StaticWebAsset.ValidateAssetGroup("MyApp.styles.css", group, out var reason, groupSet); - - - - Assert.IsFalse(result); - - Assert.Contains("'All' assets", reason); - - } - - - - // SortByRelatedAssetInPlace tests - - - - [TestMethod] - - public void SortByRelatedAssetInPlace_EmptyArray_DoesNothing() - - { - - var assets = Array.Empty(); - - - - StaticWebAsset.SortByRelatedAssetInPlace(assets); - - - - Assert.IsEmpty(assets); - - } - - - - [TestMethod] - - public void SortByRelatedAssetInPlace_SingleElement_DoesNothing() - - { - - var a = CreateChainAsset("A"); - - var assets = new[] { a }; - - - - StaticWebAsset.SortByRelatedAssetInPlace(assets); - - - - Assert.AreSame(a, assets[0]); - - } - - - - [TestMethod] - - public void SortByRelatedAssetInPlace_AllRoots_PreservesOrder() - - { - - var a = CreateChainAsset("A"); - - var b = CreateChainAsset("B"); - - var c = CreateChainAsset("C"); - - var assets = new[] { a, b, c }; - - - - StaticWebAsset.SortByRelatedAssetInPlace(assets); - - - - Assert.AreSame(a, assets[0]); - - Assert.AreSame(b, assets[1]); - - Assert.AreSame(c, assets[2]); - - } - - - - [TestMethod] - - public void SortByRelatedAssetInPlace_AlreadySorted_Chain() - - { - - // D (root) → C → B → A (parents before children) - - var d = CreateChainAsset("D"); - - var c = CreateChainAsset("C", relatedTo: "D"); - - var b = CreateChainAsset("B", relatedTo: "C"); - - var a = CreateChainAsset("A", relatedTo: "B"); - - var assets = new[] { d, c, b, a }; - - - - StaticWebAsset.SortByRelatedAssetInPlace(assets); - - AssertParentsBeforeChildren(assets); - - } - - - - [TestMethod] - - public void SortByRelatedAssetInPlace_ReversedChain_WorstCase() - - { - - // Chain: A→B→C→D (D is root). Array in child-first order. - - var d = CreateChainAsset("D"); - - var c = CreateChainAsset("C", relatedTo: "D"); - - var b = CreateChainAsset("B", relatedTo: "C"); - - var a = CreateChainAsset("A", relatedTo: "B"); - - var assets = new[] { a, b, c, d }; - - - - StaticWebAsset.SortByRelatedAssetInPlace(assets); - - AssertParentsBeforeChildren(assets); - - } - - - - [TestMethod] - - public void SortByRelatedAssetInPlace_LongReversedChain() - - { - - // A→B→C→D→E (E is root), worst order [A, B, C, D, E] - - var e = CreateChainAsset("E"); - - var d = CreateChainAsset("D", relatedTo: "E"); - - var c = CreateChainAsset("C", relatedTo: "D"); - - var b = CreateChainAsset("B", relatedTo: "C"); - - var a = CreateChainAsset("A", relatedTo: "B"); - - var assets = new[] { a, b, c, d, e }; - - - - StaticWebAsset.SortByRelatedAssetInPlace(assets); - - AssertParentsBeforeChildren(assets); - - } - - - - [TestMethod] - - public void SortByRelatedAssetInPlace_ShuffledChain() - - { - - // A→B→C→D→E (E is root), shuffled order [C, A, D, B, E] - - var e = CreateChainAsset("E"); - - var d = CreateChainAsset("D", relatedTo: "E"); - - var c = CreateChainAsset("C", relatedTo: "D"); - - var b = CreateChainAsset("B", relatedTo: "C"); - - var a = CreateChainAsset("A", relatedTo: "B"); - - var assets = new[] { c, a, d, b, e }; - - - - StaticWebAsset.SortByRelatedAssetInPlace(assets); - - AssertParentsBeforeChildren(assets); - - } - - - - [TestMethod] - - public void SortByRelatedAssetInPlace_MultipleIndependentChains() - - { - - // Chain 1: X→Y (Y root). Chain 2: P→Q→R (R root). - - var y = CreateChainAsset("Y"); - - var x = CreateChainAsset("X", relatedTo: "Y"); - - var r = CreateChainAsset("R"); - - var q = CreateChainAsset("Q", relatedTo: "R"); - - var p = CreateChainAsset("P", relatedTo: "Q"); - - // Interleave: [X, P, Q, Y, R] - - var assets = new[] { x, p, q, y, r }; - - - - StaticWebAsset.SortByRelatedAssetInPlace(assets); - - AssertParentsBeforeChildren(assets); - - } - - - - [TestMethod] - - public void SortByRelatedAssetInPlace_Diamond_TwoChildrenOneParent() - - { - - // A→C, B→C, C is root. Order: [A, B, C] - - var c = CreateChainAsset("C"); - - var a = CreateChainAsset("A", relatedTo: "C"); - - var b = CreateChainAsset("B", relatedTo: "C"); - - var assets = new[] { a, b, c }; - - - - StaticWebAsset.SortByRelatedAssetInPlace(assets); - - AssertParentsBeforeChildren(assets); - - } - - - - [TestMethod] - - public void SortByRelatedAssetInPlace_MixedRootsAndChain() - - { - - // Roots: R1, R2. Chain: A→B→C (C root). Order: [A, R1, B, R2, C] - - var r1 = CreateChainAsset("R1"); - - var r2 = CreateChainAsset("R2"); - - var c = CreateChainAsset("C"); - - var b = CreateChainAsset("B", relatedTo: "C"); - - var a = CreateChainAsset("A", relatedTo: "B"); - - var assets = new[] { a, r1, b, r2, c }; - - - - StaticWebAsset.SortByRelatedAssetInPlace(assets); - - AssertParentsBeforeChildren(assets); - - } - - - - [TestMethod] - - public void SortByRelatedAssetInPlace_OrphanAsset_PlacedAnyway() - - { - - // A→B but B is not in the array. A should still be placed. - - var a = CreateChainAsset("A", relatedTo: "NONEXISTENT"); - - var r = CreateChainAsset("R"); - - var assets = new[] { a, r }; - - - - StaticWebAsset.SortByRelatedAssetInPlace(assets); - - - - Assert.HasCount(2, assets); - - Assert.Contains(a, assets); - - Assert.Contains(r, assets); - - } - - - - [TestMethod] - - public void SortByRelatedAssetInPlace_TwoElements_ChildBeforeParent() - - { - - var parent = CreateChainAsset("Parent"); - - var child = CreateChainAsset("Child", relatedTo: "Parent"); - - var assets = new[] { child, parent }; - - - - StaticWebAsset.SortByRelatedAssetInPlace(assets); - - - - - Assert.AreSame(parent, assets[0]); - - - Assert.AreSame(child, assets[1]); - - - } - - - - + Assert.AreSame(parent, assets[0]); + Assert.AreSame(child, assets[1]); + } [TestMethod] - - public void SortByRelatedAssetInPlace_ProducesValidOrder_OnVariousInputs() - - { - - // Verify the in-place sort produces a valid topological ordering - - // for several shuffled inputs. - - var e = CreateChainAsset("E"); - - var d = CreateChainAsset("D", relatedTo: "E"); - - var c = CreateChainAsset("C", relatedTo: "D"); - - var b = CreateChainAsset("B", relatedTo: "C"); - - var a = CreateChainAsset("A", relatedTo: "B"); - - - - var orderings = new[] - - { - - new[] { a, b, c, d, e }, - - new[] { e, d, c, b, a }, - - new[] { c, a, e, b, d }, - - new[] { b, d, a, e, c }, - - }; - - - - foreach (var order in orderings) - - { - - var copy = (StaticWebAsset[])order.Clone(); - - StaticWebAsset.SortByRelatedAssetInPlace(copy); - - AssertParentsBeforeChildren(copy); - - } - - } - - - - [TestMethod] - - public void SortByRelatedAssetInPlace_PreservesAllElements() - - { - - var e = CreateChainAsset("E"); - - var d = CreateChainAsset("D", relatedTo: "E"); - - var c = CreateChainAsset("C", relatedTo: "D"); - - var b = CreateChainAsset("B", relatedTo: "C"); - - var a = CreateChainAsset("A", relatedTo: "B"); - - var assets = new[] { a, b, c, d, e }; - - var original = new HashSet(assets); - - - - StaticWebAsset.SortByRelatedAssetInPlace(assets); - - - - new HashSet(assets).Should().BeEquivalentTo(original); - - } - - - - // Asserts that for every asset in the array, its RelatedAsset (parent) - - // appears at an earlier index. - - private static void AssertParentsBeforeChildren(StaticWebAsset[] assets) - - { - - var seen = new HashSet(StringComparer.OrdinalIgnoreCase); - - foreach (var asset in assets) - - { - - if (!string.IsNullOrEmpty(asset.RelatedAsset)) - - { - - seen.Should().Contain( asset.RelatedAsset, $"Asset '{Path.GetFileName(asset.Identity)}' appears before its parent '{Path.GetFileName(asset.RelatedAsset)}'"); - - } - - seen.Add(asset.Identity); - - } - - } - - - - private static StaticWebAsset CreateChainAsset(string name, string relatedTo = null) - - { - - var result = new StaticWebAsset - - { - - Identity = Path.GetFullPath(Path.Combine("wwwroot", name)), - - SourceId = "MyProject", - - SourceType = "Computed", - - ContentRoot = Directory.GetCurrentDirectory(), - - BasePath = "base", - - RelativePath = name, - - AssetKind = "All", - - AssetMode = "All", - - AssetRole = string.IsNullOrEmpty(relatedTo) ? "Primary" : "Related", - - AssetMergeBehavior = StaticWebAsset.MergeBehaviors.PreferTarget, - - AssetMergeSource = "", - - RelatedAsset = relatedTo == null ? "" : Path.GetFullPath(Path.Combine("wwwroot", relatedTo)), - - AssetTraitName = string.IsNullOrEmpty(relatedTo) ? "" : "Content-Encoding", - - AssetTraitValue = string.IsNullOrEmpty(relatedTo) ? "" : "gzip", - - CopyToOutputDirectory = "Never", - - CopyToPublishDirectory = "PreserveNewest", - - OriginalItemSpec = Path.Combine("wwwroot", name), - - Integrity = "integrity", - - Fingerprint = "fingerprint", - - LastWriteTime = new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero), - - FileLength = 10, - - }; - - - - return result; - - } - - - - private static StaticWebAsset CreateAsset( - - string itemSpec, - - string relativePath, - - string assetKind, - - string assetMode, - - string sourceId = "MyProject", - - string sourceType = "Computed") - - { - - var result = new StaticWebAsset - - { - - Identity = Path.GetFullPath(itemSpec), - - SourceId = sourceId, - - SourceType = sourceType, - - ContentRoot = Directory.GetCurrentDirectory(), - - BasePath = "base", - - RelativePath = relativePath, - - AssetKind = assetKind, - - AssetMode = assetMode, - - AssetRole = "Primary", - - AssetMergeBehavior = StaticWebAsset.MergeBehaviors.PreferTarget, - - AssetMergeSource = "", - - RelatedAsset = "", - - AssetTraitName = "", - - AssetTraitValue = "", - - CopyToOutputDirectory = "Never", - - CopyToPublishDirectory = "PreserveNewest", - - OriginalItemSpec = itemSpec, - - Integrity = "integrity", - - Fingerprint = "fingerprint", - - LastWriteTime = new DateTimeOffset(2023, 10, 1, 0, 0, 0, TimeSpan.Zero), - - FileLength = 10, - - }; - - - - result.ApplyDefaults(); - - result.Normalize(); - - - - return result; - - } - - } - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileMultiThreadingTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileMultiThreadingTest.cs index f814cd3d1503..defb8456f423 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileMultiThreadingTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileMultiThreadingTest.cs @@ -2,182 +2,67 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Moq; - - - - namespace Microsoft.AspNetCore.Razor.Tasks; - - - - // Test parallelization is disabled assembly-wide via - - // [assembly:CollectionBehavior(DisableTestParallelization = true)] in - - // LegacyStaticWebAssetsV1IntegrationTest.cs, which already isolates the - - // process-CWD mutation this test performs. - - [DoNotParallelize] [TestClass] - public class StaticWebAssetsGeneratePackagePropsFileMultiThreadingTest - - { - - [TestMethod] - - public void WritesPropsFileRelativeToTaskEnvironmentProjectDirectory() - - { - - var testRoot = Path.Combine(AppContext.BaseDirectory, nameof(StaticWebAssetsGeneratePackagePropsFileMultiThreadingTest), Guid.NewGuid().ToString("N")); - - var projectDir = Path.Combine(testRoot, "project"); - - var spawnDir = Path.Combine(testRoot, "spawn"); - - var projectOutputDir = Path.Combine(projectDir, "output"); - - var spawnOutputDir = Path.Combine(spawnDir, "output"); - - Directory.CreateDirectory(projectOutputDir); - - Directory.CreateDirectory(spawnOutputDir); - - - - var currentDirectory = Directory.GetCurrentDirectory(); - - try - - { - - Directory.SetCurrentDirectory(spawnDir); - - - - var buildEngine = new Mock(); - - var task = new StaticWebAssetsGeneratePackagePropsFile - - { - - BuildEngine = buildEngine.Object, - - TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir), - - PropsFileImport = "Microsoft.AspNetCore.StaticWebAssets.props", - - BuildTargetPath = Path.Combine("output", "props.xml") - - }; - - - - task.Execute().Should().BeTrue(); - - - - var expectedPath = Path.Combine(projectDir, "output", "props.xml"); - - File.Exists(expectedPath).Should().BeTrue("the file should be written under the project dir, not the process CWD"); - - - - var incorrectPath = Path.Combine(spawnDir, "output", "props.xml"); - - File.Exists(incorrectPath).Should().BeFalse(); - - } - - finally - - { - - Directory.SetCurrentDirectory(currentDirectory); - - if (Directory.Exists(testRoot)) - - { - - Directory.Delete(testRoot, recursive: true); - - } - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileTest.cs index 732ae39cbc09..60dea1e28ace 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/StaticWebAssetsGeneratePackagePropsFileTest.cs @@ -6,144 +6,51 @@ using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.Build.Framework; - - using Moq; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - namespace Microsoft.AspNetCore.Razor.Tasks - - { - - [TestClass] - public class StaticWebAssetsGeneratePackagePropsFileTest - - { - - [TestMethod] - - public void WritesPropsFile_WithProvidedImportPath() - - { - - // Arrange - - var file = Path.GetTempFileName(); - - var expectedDocument = @" - - - - "; - - - - try - - { - - var buildEngine = new Mock(); - - - - var task = new StaticWebAssetsGeneratePackagePropsFile - - { - - BuildEngine = buildEngine.Object, - - PropsFileImport = "Microsoft.AspNetCore.StaticWebAssets.props", - - BuildTargetPath = file - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - var document = File.ReadAllText(file); - - document.Should().Contain(expectedDocument); - - } - - finally - - { - - if (File.Exists(file)) - - { - - File.Delete(file); - - } - - } - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateExternallyDefinedStaticWebAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateExternallyDefinedStaticWebAssetsTest.cs index b01a07c4bd22..d39d079b3780 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateExternallyDefinedStaticWebAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateExternallyDefinedStaticWebAssetsTest.cs @@ -2,1585 +2,535 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - [TestClass] public class UpdateExternallyDefinedStaticWebAssetsTest - - { - - [TestMethod] - - public void Execute_UpdatesAssetsWithoutFingerprint() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - Directory.CreateDirectory(Path.Combine(AppContext.BaseDirectory, "dist", "assets")); - - File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"), "body { color: red; }"); - - File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), ""); - - var assets = new ITaskItem [] { - - new TaskItem( - - Path.Combine(AppContext.BaseDirectory, @"dist\assets\index-C5tBAdQX.css"), - - new Dictionary - - { - - ["RelativePath"] = "assets/index-C5tBAdQX.css", - - ["BasePath"] = "", - - ["AssetMode"] = "All", - - ["AssetKind"] = "Publish", - - ["SourceId"] = "MyProject", - - ["CopyToOutputDirectory"] = "PreserveNewest", - - ["RelatedAsset"] = "", - - ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "dist"), - - ["SourceType"] = "Discovered", - - ["AssetRole"] = "Primary", - - ["AssetTraitValue"] = "", - - ["AssetTraitName"] = "", - - ["OriginalItemSpec"] = Path.Combine(AppContext.BaseDirectory, @"dist\assets\index-C5tBAdQX.css"), - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }), - - new TaskItem( - - Path.Combine(AppContext.BaseDirectory, @"dist\index.html"), - - new Dictionary - - { - - ["RelativePath"] = "index.html", - - ["BasePath"] = "", - - ["AssetMode"] = "All", - - ["AssetKind"] = "Publish", - - ["SourceId"] = "MyProject", - - ["CopyToOutputDirectory"] = "PreserveNewest", - - ["RelatedAsset"] = "", - - ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "dist"), - - ["SourceType"] = "Discovered", - - ["AssetRole"] = "Primary", - - ["AssetTraitValue"] = "", - - ["AssetTraitName"] = "", - - ["OriginalItemSpec"] = Path.Combine(AppContext.BaseDirectory, @"dist\index.html"), - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }) - - }; - - - - var task = new UpdateExternallyDefinedStaticWebAssets - - { - - Assets = assets, - - Endpoints = [], - - BuildEngine = buildEngine.Object - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - - - task.UpdatedAssets.Should().HaveCount(2); - - task.AssetsWithoutEndpoints.Should().HaveCount(2); - - task.UpdatedAssets[0].GetMetadata("Fingerprint").Should().NotBeNullOrEmpty(); - - task.UpdatedAssets[1].GetMetadata("Fingerprint").Should().NotBeNullOrEmpty(); - - task.UpdatedAssets[0].GetMetadata("Integrity").Should().NotBeNullOrEmpty(); - - task.UpdatedAssets[1].GetMetadata("Integrity").Should().NotBeNullOrEmpty(); - - } - - - - [TestMethod] - - public void Execute_DoesNotAddAssets_WithEndpointsTo_AssetsWithoutEndpoints() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - Directory.CreateDirectory(Path.Combine(AppContext.BaseDirectory, "dist", "assets")); - - File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"), "body { color: red; }"); - - File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), ""); - - var assets = new ITaskItem[] { - - new TaskItem( - - Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"), - - new Dictionary - - { - - ["RelativePath"] = "assets/index-C5tBAdQX.css", - - ["BasePath"] = "", - - ["AssetMode"] = "All", - - ["AssetKind"] = "Publish", - - ["SourceId"] = "MyProject", - - ["CopyToOutputDirectory"] = "PreserveNewest", - - ["RelatedAsset"] = "", - - ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "dist"), - - ["SourceType"] = "Discovered", - - ["AssetRole"] = "Primary", - - ["AssetTraitValue"] = "", - - ["AssetTraitName"] = "", - - ["OriginalItemSpec"] = Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"), - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }), - - new TaskItem( - - Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), - - new Dictionary - - { - - ["RelativePath"] = "index.html", - - ["BasePath"] = "", - - ["AssetMode"] = "All", - - ["AssetKind"] = "Publish", - - ["SourceId"] = "MyProject", - - ["CopyToOutputDirectory"] = "PreserveNewest", - - ["RelatedAsset"] = "", - - ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "dist"), - - ["SourceType"] = "Discovered", - - ["AssetRole"] = "Primary", - - ["AssetTraitValue"] = "", - - ["AssetTraitName"] = "", - - ["OriginalItemSpec"] = Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }) - - }; - - - - var task = new UpdateExternallyDefinedStaticWebAssets - - { - - Assets = assets, - - Endpoints = [ - - new TaskItem( - - "index.html", - - new Dictionary - - { - - ["Route"] = "/index.html", - - ["AssetFile"] = Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), - - ["Selectors"] = "[]", - - ["ResponseHeaders"] = "[]", - - ["EndpointProperties"] = "[]" - - }) - - ], - - BuildEngine = buildEngine.Object - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - - - task.UpdatedAssets.Should().HaveCount(2); - - task.AssetsWithoutEndpoints.Should().HaveCount(1); - - task.AssetsWithoutEndpoints[0].ItemSpec.Should().Be(Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css")); - - task.UpdatedAssets[0].GetMetadata("Fingerprint").Should().NotBeNullOrEmpty(); - - task.UpdatedAssets[1].GetMetadata("Fingerprint").Should().NotBeNullOrEmpty(); - - task.UpdatedAssets[0].GetMetadata("Integrity").Should().NotBeNullOrEmpty(); - - task.UpdatedAssets[1].GetMetadata("Integrity").Should().NotBeNullOrEmpty(); - - } - - - - [TestMethod] - - public void Execute_InfersFingerprint_ForMatchingAssets() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - Directory.CreateDirectory(Path.Combine(AppContext.BaseDirectory, "dist", "assets")); - - File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"), "body { color: red; }"); - - File.WriteAllText(Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), ""); - - var assets = new ITaskItem[] { - - new TaskItem( - - Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"), - - new Dictionary - - { - - ["RelativePath"] = "assets/index-C5tBAdQX.css", - - ["BasePath"] = "", - - ["AssetMode"] = "All", - - ["AssetKind"] = "Publish", - - ["SourceId"] = "MyProject", - - ["CopyToOutputDirectory"] = "PreserveNewest", - - ["RelatedAsset"] = "", - - ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "dist"), - - ["SourceType"] = "Discovered", - - ["AssetRole"] = "Primary", - - ["AssetTraitValue"] = "", - - ["AssetTraitName"] = "", - - ["OriginalItemSpec"] = Path.Combine(AppContext.BaseDirectory, "dist", "assets", "index-C5tBAdQX.css"), - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }), - - new TaskItem( - - Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), - - new Dictionary - - { - - ["RelativePath"] = "index.html", - - ["BasePath"] = "", - - ["AssetMode"] = "All", - - ["AssetKind"] = "Publish", - - ["SourceId"] = "MyProject", - - ["CopyToOutputDirectory"] = "PreserveNewest", - - ["RelatedAsset"] = "", - - ["ContentRoot"] = Path.Combine(AppContext.BaseDirectory, "dist"), - - ["SourceType"] = "Discovered", - - ["AssetRole"] = "Primary", - - ["AssetTraitValue"] = "", - - ["AssetTraitName"] = "", - - ["OriginalItemSpec"] = Path.Combine(AppContext.BaseDirectory, "dist", "index.html"), - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }) - - }; - - - - var fingerprintExpressions = new TaskItem[] - - { - - new TaskItem( - - "React", - - new Dictionary - - { - - ["Pattern"] = "assets/.*-(?.+)\\..*", - - }) - - }; - - - - var task = new UpdateExternallyDefinedStaticWebAssets - - { - - FingerprintInferenceExpressions = fingerprintExpressions, - - Assets = assets, - - Endpoints = [], - - BuildEngine = buildEngine.Object - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.UpdatedAssets.Should().HaveCount(2); - - task.UpdatedAssets[0].GetMetadata("Fingerprint").Should().Be("C5tBAdQX"); - - task.UpdatedAssets[0].GetMetadata("RelativePath").Should().Be("assets/index-#[{fingerprint}].css"); - - task.UpdatedAssets[1].GetMetadata("Fingerprint").Should().NotBeNullOrEmpty(); - - task.UpdatedAssets[0].GetMetadata("Integrity").Should().NotBeNullOrEmpty(); - - task.UpdatedAssets[1].GetMetadata("Integrity").Should().NotBeNullOrEmpty(); - - } - - - - [TestMethod] - - public void Execute_MaterializesFrameworkAssetsFromP2PReferences() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var intermediateDir = Path.Combine(AppContext.BaseDirectory, "obj", "fxtest"); - - var sourceDir = Path.Combine(AppContext.BaseDirectory, "fxsource"); - - Directory.CreateDirectory(sourceDir); - - var sourceFile = Path.Combine(sourceDir, "framework.js"); - - File.WriteAllText(sourceFile, "// framework"); - - - - var asset = new TaskItem( - - sourceFile, - - new Dictionary - - { - - ["RelativePath"] = "framework.js", - - ["BasePath"] = "_content/SourceLib", - - ["AssetMode"] = "All", - - ["AssetKind"] = "Build", - - ["SourceId"] = "SourceLib", - - ["CopyToOutputDirectory"] = "PreserveNewest", - - ["RelatedAsset"] = "", - - ["ContentRoot"] = sourceDir + Path.DirectorySeparatorChar, - - ["SourceType"] = "Framework", - - ["AssetRole"] = "Primary", - - ["AssetTraitValue"] = "", - - ["AssetTraitName"] = "", - - ["OriginalItemSpec"] = sourceFile, - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }); - - - - var task = new UpdateExternallyDefinedStaticWebAssets - - { - - Assets = new[] { asset }, - - Endpoints = [], - - BuildEngine = buildEngine.Object, - - IntermediateOutputPath = intermediateDir, - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/ConsumerApp" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.UpdatedAssets.Should().HaveCount(1); - - var materialized = task.UpdatedAssets[0]; - - materialized.GetMetadata("SourceType").Should().Be("Discovered"); - - materialized.GetMetadata("SourceId").Should().Be("ConsumerApp"); - - materialized.GetMetadata("BasePath").Should().Be("_content/ConsumerApp"); - - materialized.GetMetadata("AssetMode").Should().Be("CurrentProject"); - - materialized.ItemSpec.Should().Contain(Path.Combine("fx", "SourceLib")); - - File.Exists(materialized.ItemSpec).Should().BeTrue(); - - - - task.OriginalFrameworkAssets.Should().HaveCount(1); - - task.OriginalFrameworkAssets[0].GetMetadata("SourceType").Should().Be("Framework"); - - } - - - - [TestMethod] - - public void Execute_RemapsEndpointRoutesForMaterializedFrameworkAssets() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var intermediateDir = Path.Combine(AppContext.BaseDirectory, "obj", "fxroute"); - - var sourceDir = Path.Combine(AppContext.BaseDirectory, "fxroutesource"); - - Directory.CreateDirectory(sourceDir); - - var sourceFile = Path.Combine(sourceDir, "framework.js"); - - File.WriteAllText(sourceFile, "// framework route test"); - - - - var asset = new TaskItem( - - sourceFile, - - new Dictionary - - { - - ["RelativePath"] = "js/framework.js", - - ["BasePath"] = "_content/SourceLib", - - ["AssetMode"] = "All", - - ["AssetKind"] = "Build", - - ["SourceId"] = "SourceLib", - - ["CopyToOutputDirectory"] = "PreserveNewest", - - ["RelatedAsset"] = "", - - ["ContentRoot"] = sourceDir + Path.DirectorySeparatorChar, - - ["SourceType"] = "Framework", - - ["AssetRole"] = "Primary", - - ["AssetTraitValue"] = "", - - ["AssetTraitName"] = "", - - ["OriginalItemSpec"] = sourceFile, - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }); - - - - // Endpoint with the old base path baked into the route. - - var endpoint = new TaskItem( - - "_content/SourceLib/js/framework.js", - - new Dictionary - - { - - ["Route"] = "_content/SourceLib/js/framework.js", - - ["AssetFile"] = sourceFile, - - ["Selectors"] = "[]", - - ["ResponseHeaders"] = "[]", - - ["EndpointProperties"] = """[{"Name":"label","Value":"_content/SourceLib/js/framework.js"}]""" - - }); - - - - var task = new UpdateExternallyDefinedStaticWebAssets - - { - - Assets = [asset], - - Endpoints = [endpoint], - - BuildEngine = buildEngine.Object, - - IntermediateOutputPath = intermediateDir, - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "/" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.UpdatedEndpoints.Should().HaveCount(1); - - var updatedEndpoint = task.UpdatedEndpoints[0]; - - - - // Route should have old base path stripped and new base path applied. - - // "/" base path means just the relative path remains. - - updatedEndpoint.ItemSpec.Should().Be("js/framework.js", - - "endpoint route should have old base path '_content/SourceLib' stripped"); - - - - // AssetFile should point to the materialized path. - - updatedEndpoint.GetMetadata("AssetFile").Should().Contain(Path.Combine("fx", "SourceLib")); - - - - // Label endpoint property should also be remapped. - - var endpointProperties = updatedEndpoint.GetMetadata("EndpointProperties"); - - endpointProperties.Should().Contain("js/framework.js"); - - endpointProperties.Should().NotContain("_content/SourceLib"); - - } - - - - - [TestMethod] - - - public void Execute_RemapsEndpointRoutesToConsumerBasePath() - - + [TestMethod] + public void Execute_RemapsEndpointRoutesToConsumerBasePath() { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var intermediateDir = Path.Combine(AppContext.BaseDirectory, "obj", "fxroutelib"); - - var sourceDir = Path.Combine(AppContext.BaseDirectory, "fxroutelibsource"); - - Directory.CreateDirectory(sourceDir); - - var sourceFile = Path.Combine(sourceDir, "lib.js"); - - File.WriteAllText(sourceFile, "// lib route test"); - - - - var asset = new TaskItem( - - sourceFile, - - new Dictionary - - { - - ["RelativePath"] = "js/lib.js", - - ["BasePath"] = "_content/SourceLib", - - ["AssetMode"] = "All", - - ["AssetKind"] = "Build", - - ["SourceId"] = "SourceLib", - - ["CopyToOutputDirectory"] = "PreserveNewest", - - ["RelatedAsset"] = "", - - ["ContentRoot"] = sourceDir + Path.DirectorySeparatorChar, - - ["SourceType"] = "Framework", - - ["AssetRole"] = "Primary", - - ["AssetTraitValue"] = "", - - ["AssetTraitName"] = "", - - ["OriginalItemSpec"] = sourceFile, - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }); - - - - var endpoint = new TaskItem( - - "_content/SourceLib/js/lib.js", - - new Dictionary - - { - - ["Route"] = "_content/SourceLib/js/lib.js", - - ["AssetFile"] = sourceFile, - - ["Selectors"] = "[]", - - ["ResponseHeaders"] = "[]", - - ["EndpointProperties"] = """[{"Name":"label","Value":"_content/SourceLib/js/lib.js"}]""" - - }); - - - - var task = new UpdateExternallyDefinedStaticWebAssets - - { - - Assets = [asset], - - Endpoints = [endpoint], - - BuildEngine = buildEngine.Object, - - IntermediateOutputPath = intermediateDir, - - ProjectPackageId = "ConsumerLib", - - ProjectBasePath = "_content/ConsumerLib" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.UpdatedEndpoints.Should().HaveCount(1); - - var updatedEndpoint = task.UpdatedEndpoints[0]; - - - - // Route should have old base path replaced with consumer's base path. - - updatedEndpoint.ItemSpec.Should().Be("_content/ConsumerLib/js/lib.js", - - "endpoint route should use consumer's base path"); - - - - // Label should also reflect the new base path. - - var endpointProperties = updatedEndpoint.GetMetadata("EndpointProperties"); - - endpointProperties.Should().Contain("_content/ConsumerLib/js/lib.js"); - - endpointProperties.Should().NotContain("_content/SourceLib"); - - } - - - - [TestMethod] - - public void Execute_PassesThroughNonFrameworkAssetsUnchanged() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var sourceDir = Path.Combine(AppContext.BaseDirectory, "normalsource"); - - Directory.CreateDirectory(sourceDir); - - var sourceFile = Path.Combine(sourceDir, "app.js"); - - File.WriteAllText(sourceFile, "// app"); - - - - var asset = new TaskItem( - - sourceFile, - - new Dictionary - - { - - ["RelativePath"] = "app.js", - - ["BasePath"] = "", - - ["AssetMode"] = "All", - - ["AssetKind"] = "Build", - - ["SourceId"] = "OtherLib", - - ["CopyToOutputDirectory"] = "PreserveNewest", - - ["RelatedAsset"] = "", - - ["ContentRoot"] = sourceDir + Path.DirectorySeparatorChar, - - ["SourceType"] = "Discovered", - - ["AssetRole"] = "Primary", - - ["AssetTraitValue"] = "", - - ["AssetTraitName"] = "", - - ["OriginalItemSpec"] = sourceFile, - - ["CopyToPublishDirectory"] = "PreserveNewest" - - }); - - - - var task = new UpdateExternallyDefinedStaticWebAssets - - { - - Assets = new[] { asset }, - - Endpoints = [], - - BuildEngine = buildEngine.Object, - - IntermediateOutputPath = Path.Combine(AppContext.BaseDirectory, "obj", "normal"), - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/ConsumerApp" - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.UpdatedAssets.Should().HaveCount(1); - - task.UpdatedAssets[0].GetMetadata("SourceType").Should().Be("Discovered"); - - task.UpdatedAssets[0].GetMetadata("SourceId").Should().Be("OtherLib"); - - task.OriginalFrameworkAssets.Should().BeEmpty(); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdatePackageStaticWebAssetsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdatePackageStaticWebAssetsTest.cs index b0e4cc5c449c..672ddb647826 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdatePackageStaticWebAssetsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdatePackageStaticWebAssetsTest.cs @@ -2,2146 +2,722 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - [TestClass] public class UpdatePackageStaticWebAssetsTest : IDisposable - - { - - private readonly string _tempDir; - - private readonly Mock _buildEngine; - - private readonly List _errorMessages; - - private readonly List _logMessages; - - - - public UpdatePackageStaticWebAssetsTest() - - { - - _tempDir = Path.Combine(Path.GetTempPath(), "UpdatePackageSWA_" + Guid.NewGuid().ToString("N")); - - Directory.CreateDirectory(_tempDir); - - - - _errorMessages = new List(); - - _logMessages = new List(); - - _buildEngine = new Mock(); - - _buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => _errorMessages.Add(args.Message)); - - _buildEngine.Setup(e => e.LogMessageEvent(It.IsAny())) - - .Callback(args => _logMessages.Add(args.Message)); - - } - - - - public void Dispose() - - { - - if (Directory.Exists(_tempDir)) - - { - - try { Directory.Delete(_tempDir, recursive: true); } catch { } - - } - - } - - - - [TestMethod] - - public void Execute_PackageAssets_ArePassedThrough() - - { - - // Arrange - - var sourceFile = CreateTempFile("pkg", "content.js", "console.log('pkg');"); - - var asset = CreatePackageAsset(sourceFile, "MyLib", "_content/mylib", "content.js"); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - IntermediateOutputPath = Path.Combine(_tempDir, "obj"), - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.UpdatedAssets.Should().HaveCount(1); - - task.OriginalAssets.Should().HaveCount(1); - - task.UpdatedAssets[0].GetMetadata("SourceType").Should().Be("Package"); - - } - - - - [TestMethod] - - public void Execute_FrameworkAssets_AreMaterialized() - - { - - // Arrange - - var sourceFile = CreateTempFile("source", "js", "framework.js", "console.log('framework');"); - - var asset = CreateFrameworkAsset(sourceFile, "FrameworkLib", "_content/frameworklib", "js/framework.js"); - - var intermediateOutput = Path.Combine(_tempDir, "obj"); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - IntermediateOutputPath = intermediateOutput, - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.UpdatedAssets.Should().HaveCount(1); - - task.OriginalAssets.Should().HaveCount(1); - - - - var updated = task.UpdatedAssets[0]; - - - - // The materialized file should exist in the fx directory - - var expectedDir = Path.Combine(intermediateOutput, "fx", "FrameworkLib"); - - var expectedPath = Path.GetFullPath(Path.Combine(expectedDir, "js", "framework.js")); - - updated.ItemSpec.Should().Be(expectedPath); - - File.Exists(expectedPath).Should().BeTrue(); - - File.ReadAllText(expectedPath).Should().Be("console.log('framework');"); - - } - - - - [TestMethod] - - public void Execute_FrameworkAssets_SourceTypeChangedToDiscovered() - - { - - // Arrange - - var sourceFile = CreateTempFile("source", "framework.js", "content"); - - var asset = CreateFrameworkAsset(sourceFile, "FrameworkLib", "_content/frameworklib", "framework.js"); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - IntermediateOutputPath = Path.Combine(_tempDir, "obj"), - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var updated = task.UpdatedAssets[0]; - - updated.GetMetadata("SourceType").Should().Be("Discovered"); - - updated.GetMetadata("SourceId").Should().Be("ConsumerApp"); - - updated.GetMetadata("BasePath").Should().Be("_content/consumerapp"); - - updated.GetMetadata("AssetMode").Should().Be("CurrentProject"); - - } - - - - [TestMethod] - - public void Execute_FrameworkAssets_ContentRootPointsToFxDirectory() - - { - - // Arrange - - var sourceFile = CreateTempFile("source", "framework.js", "content"); - - var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); - - var intermediateOutput = Path.Combine(_tempDir, "obj"); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - IntermediateOutputPath = intermediateOutput, - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var updated = task.UpdatedAssets[0]; - - var expectedContentRoot = Path.Combine(intermediateOutput, "fx", "FxLib") + Path.DirectorySeparatorChar; - - updated.GetMetadata("ContentRoot").Should().Be(expectedContentRoot); - - } - - - - [TestMethod] - - public void Execute_FrameworkAssets_MissingSourceFile_LogsError() - - { - - // Arrange - - var nonExistentFile = Path.Combine(_tempDir, "does_not_exist.js"); - - var asset = CreateFrameworkAsset(nonExistentFile, "FxLib", "_content/fxlib", "framework.js"); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - IntermediateOutputPath = Path.Combine(_tempDir, "obj"), - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeFalse(); - - _errorMessages.Should().ContainSingle(e => e.Contains("does not exist") && e.Contains("does_not_exist.js")); - - } - - - - [TestMethod] - - public void Execute_MixedAssets_ProcessesBothTypes() - - { - - // Arrange - - var pkgFile = CreateTempFile("pkg", "package.js", "console.log('pkg');"); - - var fxFile = CreateTempFile("source", "framework.js", "console.log('fx');"); - - - - var pkgAsset = CreatePackageAsset(pkgFile, "MyLib", "_content/mylib", "package.js"); - - var fxAsset = CreateFrameworkAsset(fxFile, "MyLib", "_content/mylib", "framework.js"); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { pkgAsset, fxAsset }, - - IntermediateOutputPath = Path.Combine(_tempDir, "obj"), - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.UpdatedAssets.Should().HaveCount(2); - - task.OriginalAssets.Should().HaveCount(2); - - - - // Package asset stays as Package - - task.UpdatedAssets[0].GetMetadata("SourceType").Should().Be("Package"); - - // Framework asset is converted to Discovered - - task.UpdatedAssets[1].GetMetadata("SourceType").Should().Be("Discovered"); - - } - - - - [TestMethod] - - public void Execute_FrameworkAssets_PreservesOriginalFingerprintAndIntegrity() - - { - - // Arrange - - var sourceFile = CreateTempFile("source", "framework.js", "content"); - - var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); - - - - // Get the fingerprint/integrity that were computed by FromV1TaskItem in CreateFrameworkAsset - - var originalFingerprint = asset.GetMetadata("Fingerprint"); - - var originalIntegrity = asset.GetMetadata("Integrity"); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - IntermediateOutputPath = Path.Combine(_tempDir, "obj"), - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var updated = task.UpdatedAssets[0]; - - // Fingerprint and integrity should be preserved from the original file - - updated.GetMetadata("Fingerprint").Should().Be(originalFingerprint); - - updated.GetMetadata("Integrity").Should().Be(originalIntegrity); - - } - - - - [TestMethod] - - public void Execute_FrameworkAssets_IncrementalSkipsCopy_WhenUpToDate() - - { - - // Arrange - - var sourceFile = CreateTempFile("source", "framework.js", "content"); - - var intermediateOutput = Path.Combine(_tempDir, "obj"); - - var fxDir = Path.Combine(intermediateOutput, "fx", "FxLib"); - - var destPath = Path.Combine(fxDir, "framework.js"); - - - - // Pre-create the destination so it's already up-to-date - - Directory.CreateDirectory(fxDir); - - File.Copy(sourceFile, destPath); - - // Make the dest file newer than the source - - File.SetLastWriteTimeUtc(destPath, DateTime.UtcNow.AddMinutes(1)); - - - - - var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); - - - - + var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - IntermediateOutputPath = intermediateOutput, - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.UpdatedAssets.Should().HaveCount(1); - - // Should log the "already up to date" message - - _logMessages.Should().Contain(m => m.Contains("already up to date")); - - } - - - - [TestMethod] - - public void Execute_FrameworkAssets_OverwritesStaleDestination() - - { - - // Arrange - - var sourceFile = CreateTempFile("source", "framework.js", "new content"); - - var intermediateOutput = Path.Combine(_tempDir, "obj"); - - var fxDir = Path.Combine(intermediateOutput, "fx", "FxLib"); - - var destPath = Path.Combine(fxDir, "framework.js"); - - - - // Pre-create the destination with old content and older timestamp - - Directory.CreateDirectory(fxDir); - - File.WriteAllText(destPath, "old content"); - - File.SetLastWriteTimeUtc(destPath, DateTime.UtcNow.AddMinutes(-10)); - - File.SetLastWriteTimeUtc(sourceFile, DateTime.UtcNow); - - - - var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - IntermediateOutputPath = intermediateOutput, - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - File.ReadAllText(destPath).Should().Be("new content"); - - _logMessages.Should().Contain(m => m.Contains("Materialized framework asset")); - - } - - - - [TestMethod] - - public void Execute_NoFrameworkAssets_EndpointsNotRemapped() - - { - - // Arrange - - var sourceFile = CreateTempFile("pkg", "content.js", "console.log('pkg');"); - - var pkgAsset = CreatePackageAsset(sourceFile, "MyLib", "_content/mylib", "content.js"); - - - - var endpoint = new TaskItem("content.js", new Dictionary - - { - - ["Route"] = "/content.js", - - ["AssetFile"] = sourceFile, - - ["Selectors"] = "[]", - - ["ResponseHeaders"] = "[]", - - ["EndpointProperties"] = "[]", - - }); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { pkgAsset }, - - Endpoints = new ITaskItem[] { endpoint }, - - IntermediateOutputPath = Path.Combine(_tempDir, "obj"), - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - // No framework assets => no remapping done - - task.RemappedEndpoints.Should().BeNullOrEmpty(); - - } - - - - [TestMethod] - - public void Execute_FrameworkAssets_EndpointsAreRemapped() - - { - - // Arrange - - var sourceFile = CreateTempFile("source", "framework.js", "content"); - - var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); - - var intermediateOutput = Path.Combine(_tempDir, "obj"); - - - - var endpoint = new TaskItem("framework.js", new Dictionary - - { - - ["Route"] = "/_content/fxlib/framework.js", - - ["AssetFile"] = sourceFile, - - ["Selectors"] = "[]", - - ["ResponseHeaders"] = "[]", - - ["EndpointProperties"] = "[]", - - }); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - Endpoints = new ITaskItem[] { endpoint }, - - IntermediateOutputPath = intermediateOutput, - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.RemappedEndpoints.Should().HaveCount(1); - - - - var remapped = task.RemappedEndpoints[0]; - - var expectedPath = Path.GetFullPath(Path.Combine(intermediateOutput, "fx", "FxLib", "framework.js")); - - remapped.GetMetadata("AssetFile").Should().Be(expectedPath); - - } - - - - [TestMethod] - - public void Execute_MultipleEndpoints_SameIdentity_AllRemapped() - - { - - // Arrange — two endpoints share the same Identity (e.g. same route, different selectors) - - var sourceFile = CreateTempFile("source", "framework.js", "content"); - - var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); - - var intermediateOutput = Path.Combine(_tempDir, "obj"); - - - - var endpoint1 = new TaskItem("framework.js", new Dictionary - - { - - ["Route"] = "/_content/fxlib/framework.js", - - ["AssetFile"] = sourceFile, - - ["Selectors"] = "[{\"Name\":\"Content-Encoding\",\"Value\":\"gzip\"}]", - - ["ResponseHeaders"] = "[]", - - ["EndpointProperties"] = "[]", - - }); - - - - var endpoint2 = new TaskItem("framework.js", new Dictionary - - { - - ["Route"] = "/_content/fxlib/framework.js", - - ["AssetFile"] = sourceFile, - - ["Selectors"] = "[]", - - ["ResponseHeaders"] = "[]", - - ["EndpointProperties"] = "[]", - - }); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - Endpoints = new ITaskItem[] { endpoint1, endpoint2 }, - - IntermediateOutputPath = intermediateOutput, - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.RemappedEndpoints.Should().HaveCount(2); - - - - var expectedPath = Path.GetFullPath(Path.Combine(intermediateOutput, "fx", "FxLib", "framework.js")); - - task.RemappedEndpoints[0].GetMetadata("AssetFile").Should().Be(expectedPath); - - task.RemappedEndpoints[1].GetMetadata("AssetFile").Should().Be(expectedPath); - - } - - - - [TestMethod] - - public void Execute_EndpointsNotMatchingFramework_AreNotRemapped() - - { - - // Arrange — endpoint pointing to a file that is NOT a framework asset - - var fxFile = CreateTempFile("source", "framework.js", "fx"); - - var pkgFile = CreateTempFile("pkg", "package.js", "pkg"); - - var fxAsset = CreateFrameworkAsset(fxFile, "FxLib", "_content/fxlib", "framework.js"); - - var intermediateOutput = Path.Combine(_tempDir, "obj"); - - - - var fxEndpoint = new TaskItem("framework.js", new Dictionary - - { - - ["Route"] = "/_content/fxlib/framework.js", - - ["AssetFile"] = fxFile, - - ["Selectors"] = "[]", - - ["ResponseHeaders"] = "[]", - - ["EndpointProperties"] = "[]", - - }); - - - - var pkgEndpoint = new TaskItem("package.js", new Dictionary - - { - - ["Route"] = "/_content/fxlib/package.js", - - ["AssetFile"] = pkgFile, - - ["Selectors"] = "[]", - - ["ResponseHeaders"] = "[]", - - ["EndpointProperties"] = "[]", - - }); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { fxAsset }, - - Endpoints = new ITaskItem[] { fxEndpoint, pkgEndpoint }, - - IntermediateOutputPath = intermediateOutput, - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - // Only the framework endpoint should be remapped - - task.RemappedEndpoints.Should().HaveCount(1); - - task.RemappedEndpoints[0].ItemSpec.Should().Be("framework.js"); - - } - - - - [TestMethod] - - public void Execute_NullEndpoints_DoesNotRemapAndSucceeds() - - { - - // Arrange - - var sourceFile = CreateTempFile("source", "framework.js", "content"); - - var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "framework.js"); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - Endpoints = null, - - IntermediateOutputPath = Path.Combine(_tempDir, "obj"), - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.UpdatedAssets.Should().HaveCount(1); - - task.RemappedEndpoints.Should().BeNullOrEmpty(); - - } - - - - [TestMethod] - - public void Execute_EmptyAssetsArray_Succeeds() - - { - - // Arrange - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = Array.Empty(), - - IntermediateOutputPath = Path.Combine(_tempDir, "obj"), - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.UpdatedAssets.Should().BeEmpty(); - - task.OriginalAssets.Should().BeEmpty(); - - } - - - - [TestMethod] - - public void Execute_FrameworkAssets_SubdirectoriesArePreserved() - - { - - // Arrange - - var sourceFile = CreateTempFile("source", "lib", "deep", "nested", "component.js", "content"); - - var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/fxlib", "lib/deep/nested/component.js"); - - var intermediateOutput = Path.Combine(_tempDir, "obj"); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = new[] { asset }, - - IntermediateOutputPath = intermediateOutput, - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "_content/consumerapp", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - var expectedPath = Path.GetFullPath(Path.Combine(intermediateOutput, "fx", "FxLib", "lib", "deep", "nested", "component.js")); - - task.UpdatedAssets[0].ItemSpec.Should().Be(expectedPath); - - File.Exists(expectedPath).Should().BeTrue(); - - } - - - - // Helpers - - - - private string CreateTempFile(params string[] pathParts) - - { - - // Last part is the content, everything before is path segments - - var content = pathParts[^1]; - - var segments = pathParts[..^1]; - - - - var dir = Path.Combine(new[] { _tempDir }.Concat(segments[..^1]).ToArray()); - - Directory.CreateDirectory(dir); - - var filePath = Path.Combine(dir, segments[^1]); - - File.WriteAllText(filePath, content); - - return filePath; - - } - - - - private ITaskItem CreatePackageAsset(string filePath, string sourceId, string basePath, string relativePath) - - { - - var contentRoot = Path.GetDirectoryName(filePath) + Path.DirectorySeparatorChar; - - return new TaskItem(filePath, new Dictionary - - { - - ["SourceType"] = "Package", - - ["SourceId"] = sourceId, - - ["ContentRoot"] = contentRoot, - - ["BasePath"] = basePath, - - ["RelativePath"] = relativePath, - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["OriginalItemSpec"] = filePath, - - ["Fingerprint"] = "test-fingerprint", - - ["Integrity"] = "test-integrity", - - ["FileLength"] = "10", - - ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat), - - }); - - } - - - - private ITaskItem CreateFrameworkAsset(string filePath, string sourceId, string basePath, string relativePath) - - { - - var contentRoot = Path.GetDirectoryName(filePath) + Path.DirectorySeparatorChar; - - return new TaskItem(filePath, new Dictionary - - { - - ["SourceType"] = "Framework", - - ["SourceId"] = sourceId, - - ["ContentRoot"] = contentRoot, - - ["BasePath"] = basePath, - - ["RelativePath"] = relativePath, - - ["AssetKind"] = "All", - - ["AssetMode"] = "All", - - ["AssetRole"] = "Primary", - - ["RelatedAsset"] = "", - - ["AssetTraitName"] = "", - - ["AssetTraitValue"] = "", - - ["CopyToOutputDirectory"] = "Never", - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["OriginalItemSpec"] = filePath, - - ["Fingerprint"] = "test-fingerprint", - - ["Integrity"] = "test-integrity", - - ["FileLength"] = "10", - - ["LastWriteTime"] = new DateTimeOffset(new DateTime(1990, 11, 15, 0, 0, 0, 0, DateTimeKind.Utc)).ToString(StaticWebAsset.DateTimeAssetFormat), - - }); - - } - - - - private ITaskItem CreateEndpoint(string route, string assetFile, string label = null) - - { - - var properties = label != null - - ? $$"""[{"Name":"label","Value":"{{label}}"}]""" - - : "[]"; - - - - return new TaskItem(route, new Dictionary - - { - - ["Route"] = route, - - ["AssetFile"] = assetFile, - - ["Selectors"] = "[]", - - ["ResponseHeaders"] = "[]", - - ["EndpointProperties"] = properties, - - }); - - } - - - - [TestMethod] - - public void Execute_FrameworkAssets_RemapsEndpointRoutes_StripOldBasePath() - - { - - // Arrange - - var sourceFile = CreateTempFile("source", "framework.js", "content"); - - var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/FxLib", "js/framework.js"); - - var endpoint = CreateEndpoint("_content/FxLib/js/framework.js", sourceFile, "_content/FxLib/js/framework.js"); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = [asset], - - Endpoints = [endpoint], - - IntermediateOutputPath = Path.Combine(_tempDir, "obj"), - - ProjectPackageId = "ConsumerApp", - - ProjectBasePath = "/", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.RemappedEndpoints.Should().HaveCount(1); - - var remapped = task.RemappedEndpoints[0]; - - - - // Route should have old base path stripped; "/" means just relative path. - - remapped.ItemSpec.Should().Be("js/framework.js"); - - remapped.GetMetadata("AssetFile").Should().Contain(Path.Combine("fx", "FxLib")); - - - - // Label should also be remapped. - - remapped.GetMetadata("EndpointProperties").Should().Contain("js/framework.js"); - - remapped.GetMetadata("EndpointProperties").Should().NotContain("_content/FxLib"); - - - - // Original endpoints should be output for removal. - - task.OriginalFrameworkEndpoints.Should().HaveCount(1); - - task.OriginalFrameworkEndpoints[0].ItemSpec.Should().Be("_content/FxLib/js/framework.js"); - - } - - - - [TestMethod] - - public void Execute_FrameworkAssets_RemapsEndpointRoutes_ToConsumerBasePath() - - { - - // Arrange - - var sourceFile = CreateTempFile("source2", "lib.js", "content"); - - var asset = CreateFrameworkAsset(sourceFile, "FxLib", "_content/FxLib", "js/lib.js"); - - var endpoint = CreateEndpoint("_content/FxLib/js/lib.js", sourceFile, "_content/FxLib/js/lib.js"); - - - - var task = new UpdatePackageStaticWebAssets - - { - - BuildEngine = _buildEngine.Object, - - Assets = [asset], - - Endpoints = [endpoint], - - IntermediateOutputPath = Path.Combine(_tempDir, "obj"), - - ProjectPackageId = "ConsumerLib", - - ProjectBasePath = "_content/ConsumerLib", - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().BeTrue(); - - task.RemappedEndpoints.Should().HaveCount(1); - - var remapped = task.RemappedEndpoints[0]; - - - - // Route should use consumer's base path. - - remapped.ItemSpec.Should().Be("_content/ConsumerLib/js/lib.js"); - - - - // Label should also reflect consumer's base path. - - remapped.GetMetadata("EndpointProperties").Should().Contain("_content/ConsumerLib/js/lib.js"); - - remapped.GetMetadata("EndpointProperties").Should().NotContain("_content/FxLib"); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateStaticWebAssetEndpointsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateStaticWebAssetEndpointsTest.cs index 71a5315a31e8..565d6cc16ed7 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateStaticWebAssetEndpointsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/UpdateStaticWebAssetEndpointsTest.cs @@ -2,1144 +2,388 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests.StaticWebAssets; - - - - [TestClass] public class UpdateStaticWebAssetEndpointsTest - - { - - [TestMethod] - - 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() - - }; - - - - // 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")); - - } - - } - - - - [TestMethod] - - 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() - - }; - - - - // 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)); - - } - - } - - - - [TestMethod] - - 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() - - }; - - - - // 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)); - - } - - } - - - - [TestMethod] - - 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() - - }; - - - - // 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)); - - } - - } - - - - [TestMethod] - - 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() - - }; - - - - // 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")); - - } - - } - - - - [TestMethod] - - 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() - - }; - - - - // 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 - - { - - { "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(); - - - - defineStaticWebAssetEndpoints.Execute(); - - return StaticWebAssetEndpoint.FromItemGroup(defineStaticWebAssetEndpoints.Endpoints); - - } - - - - private static TaskItem CreateContentMapping(string pattern, string contentType) - - { - - return new TaskItem(contentType, new Dictionary - - { - - { "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; - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ValidateStaticWebAssetsUniquePathsTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ValidateStaticWebAssetsUniquePathsTest.cs index f81e15c0819f..965635d12ada 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ValidateStaticWebAssetsUniquePathsTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/ValidateStaticWebAssetsUniquePathsTest.cs @@ -2,649 +2,222 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using Microsoft.Build.Framework; - - using Microsoft.Build.Utilities; - - using Moq; - - - - namespace Microsoft.AspNetCore.Razor.Tasks - - { - - [TestClass] - public class ValidateStaticWebAssetsUniquePathsTest - - { - - [TestMethod] - - public void ReturnsError_WhenStaticWebAssetsWebRootPathMatchesExistingContentItemPath() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ValidateStaticWebAssetsUniquePaths - - { - - BuildEngine = buildEngine.Object, - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwroot", "js", "project-transitive-dep.js"), new Dictionary - - { - - ["BasePath"] = "_content/ClassLibrary", - - ["RelativePath"] = "js/project-transitive-dep.js", - - ["SourceId"] = "ClassLibrary", - - ["SourceType"] = "Project", - - }), - - }, - - WebRootFiles = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot", "_content", "ClassLibrary", "js", "project-transitive-dep.js"), new Dictionary - - { - - ["CopyToPublishDirectory"] = "PreserveNewest", - - ["ExcludeFromSingleFile"] = "true", - - ["OriginalItemSpec"] = Path.Combine("wwwroot", "_content", "ClassLibrary", "js", "project-transitive-dep.js"), - - ["TargetPath"] = Path.Combine("wwwroot", "_content", "ClassLibrary", "js", "project-transitive-dep.js"), - - }) - - } - - }; - - - - var expectedMessage = $"The static web asset '{Path.Combine("wwroot", "js", "project-transitive-dep.js")}' " + - - "has a conflicting web root path '/wwwroot/_content/ClassLibrary/js/project-transitive-dep.js' with the " + - - $"project file '{Path.Combine("wwwroot", "_content", "ClassLibrary", "js", "project-transitive-dep.js")}'."; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(false); - - errorMessages.Should().NotBeEmpty().And.HaveCount(1); - - errorMessages.Should().Contain(expectedMessage); - - } - - - - [TestMethod] - - public void AllowsAssetsHavingTheSameBasePathAcrossDifferentSources_WhenTheirFinalDestinationPathIsDifferent() - - { - - // Arrange - - var task = new ValidateStaticWebAssetsUniquePaths - - { - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary - - { - - ["BasePath"] = "MyLibrary", - - ["ContentRoot"] = Path.Combine("nuget", "MyLibrary"), - - ["RelativePath"] = "sample.js", - - ["SourceId"] = "MyLibrary" - - }), - - CreateItem(Path.Combine("wwwroot", "otherLib.js"), new Dictionary - - { - - ["BasePath"] = "MyLibrary", - - ["ContentRoot"] = Path.Combine("nuget", "MyOtherLibrary"), - - ["RelativePath"] = "otherLib.js", - - ["SourceId"] = "MyOtherLibrary" - - }) - - }, - - WebRootFiles = Array.Empty() - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - } - - - - [TestMethod] - - public void AllowsAssetsHavingTheSameContentRootAndDifferentBasePathsAcrossDifferentSources_WhenTheirFinalDestinationPathIsDifferent() - - { - - // Arrange - - var task = new ValidateStaticWebAssetsUniquePaths - - { - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine("wwwroot","sample.js"), new Dictionary - - { - - ["BasePath"] = "MyLibrary", - - ["SourceId"] = "MyLibrary", - - ["RelativePath"] = "sample.js", - - ["ContentRoot"] = Path.Combine(".", "MyLibrary") - - }), - - CreateItem(Path.Combine("wwwroot", "otherLib.js"), new Dictionary - - { - - ["BasePath"] = "MyOtherLibrary", - - ["SourceId"] = "MyOtherLibrary", - - ["RelativePath"] = "otherlib.js", - - ["ContentRoot"] = Path.Combine(".", "MyLibrary") - - }) - - }, - - WebRootFiles = Array.Empty() - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - } - - - - [TestMethod] - - public void ReturnsError_WhenMultipleStaticWebAssetsHaveTheSameWebRootPath() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - buildEngine.Setup(e => e.LogErrorEvent(It.IsAny())) - - .Callback(args => errorMessages.Add(args.Message)); - - - - var task = new ValidateStaticWebAssetsUniquePaths - - { - - BuildEngine = buildEngine.Object, - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine(".", "Library", "wwwroot", "sample.js"), new Dictionary - - { - - ["BasePath"] = "/", - - ["RelativePath"] = "/sample.js", - - }), - - CreateItem(Path.Combine(".", "Library", "bin", "dist", "sample.js"), new Dictionary - - { - - ["BasePath"] = "/", - - ["RelativePath"] = "/sample.js", - - }) - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(false); - - errorMessages.Should().NotBeEmpty().And.HaveCount(1); - - errorMessages.Should().Contain($"Conflicting assets with the same path '/wwwroot/sample.js' for content root paths '{Path.Combine(".", "Library", "bin", "dist", "sample.js")}' and '{Path.Combine(".", "Library", "wwwroot", "sample.js")}'."); - - } - - - - [TestMethod] - - public void ReturnsSuccess_WhenStaticWebAssetsDontConflictWithApplicationContentItems() - - { - - // Arrange - - var errorMessages = new List(); - - var buildEngine = new Mock(); - - - - var task = new ValidateStaticWebAssetsUniquePaths - - { - - BuildEngine = buildEngine.Object, - - StaticWebAssets = new TaskItem[] - - { - - CreateItem(Path.Combine(".", "Library", "wwwroot", "sample.js"), new Dictionary - - { - - ["BasePath"] = "/_library", - - ["RelativePath"] = "/sample.js", - - }), - - CreateItem(Path.Combine(".", "Library", "wwwroot", "sample.js"), new Dictionary - - { - - ["BasePath"] = "/_library", - - ["RelativePath"] = "/sample.js", - - }) - - }, - - WebRootFiles = new TaskItem[] - - { - - CreateItem(Path.Combine(".", "App", "wwwroot", "sample.js"), new Dictionary - - { - - ["TargetPath"] = "/SAMPLE.js", - - }) - - } - - }; - - - - // Act - - var result = task.Execute(); - - - - // Assert - - result.Should().Be(true); - - } - - - - private static TaskItem CreateItem( - - string spec, - - IDictionary metadata) - - { - - var result = new TaskItem(spec); - - foreach (var (key, value) in metadata) - - { - - result.SetMetadata(key, value); - - } - - - - return result; - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineComparer.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineComparer.cs index c8ae673eabfc..cb0fe3fb703d 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineComparer.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineComparer.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; - using System.Linq; - using Microsoft.AspNetCore.StaticWebAssets.Tasks; using Microsoft.NET.TestFramework; @@ -12,1032 +10,517 @@ using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - public class StaticWebAssetsBaselineComparer - { - private static readonly string BaselineGenerationInstructions = - @"If the difference in baselines is expected, please re-generate the baselines. - Start by ensuring you're dogfooding the SDK from the current branch (dotnet --version should be '*.0.0-dev'). - If you're not on the dogfood sdk, from the root of the repository run: - 1. dotnet clean - 2. .\restore.cmd or ./restore.sh - 3. .\build.cmd ./build.sh - 4. .\eng\dogfood.cmd or . ./eng/dogfood.sh - - Then, using the dogfood SDK run the .\src\RazorSdk\update-test-baselines.ps1 script."; - - public static StaticWebAssetsBaselineComparer Instance { get; } = new(); - - internal void AssertManifest(StaticWebAssetsManifest expected, StaticWebAssetsManifest actual) - { - //Many of the properties in the manifest contain full paths, to avoid flakiness on the tests, we don't compare the full paths. - actual.Version.Should().Be(expected.Version); - actual.Source.Should().Be(expected.Source); - actual.BasePath.Should().Be(expected.BasePath); - actual.Mode.Should().Be(expected.Mode); - actual.ManifestType.Should().Be(expected.ManifestType); - - actual.ReferencedProjectsConfiguration.Should().HaveSameCount(expected.ReferencedProjectsConfiguration); - - // Relax the check for project reference configuration items see - // https://github.com/dotnet/sdk/pull/27381#issuecomment-1228764471 - // for details. - //manifest.ReferencedProjectsConfiguration.OrderBy(cm => cm.Identity) - // .Should() - // .BeEquivalentTo(expected.ReferencedProjectsConfiguration.OrderBy(cm => cm.Identity)); - - actual.DiscoveryPatterns.OrderBy(dp => dp.Name).Should().BeEquivalentTo(expected.DiscoveryPatterns.OrderBy(dp => dp.Name)); - - var actualAssets = actual.Assets - .OrderBy(a => a.BasePath) - .ThenBy(a => a.RelativePath) - .ThenBy(a => a.AssetKind) - .GroupBy(a => GetAssetGroup(a)) - .ToDictionary(a => a.Key, a => a.Order().ToArray()); - - var duplicateAssets = actual.Assets - .GroupBy(a => a) - .ToDictionary(a => a.Key, a => a.Order().ToArray()); - - var foundDuplicateAssetss = duplicateAssets.Where(a => a.Value.Length > 1).ToArray(); - duplicateAssets.Where(a => a.Value.Length > 1).Should().BeEmpty($@"no duplicate assets should exist. But found: - {string.Join($"{Environment.NewLine} ", foundDuplicateAssetss.Select(a => @$"{a.Key.Identity} - {a.Value.Length}"))}{Environment.NewLine}"); - - var expectedAssets = expected.Assets - .OrderBy(a => a.BasePath) - .ThenBy(a => a.RelativePath) - .ThenBy(a => a.AssetKind) - .GroupBy(a => GetAssetGroup(a)) - .ToDictionary(a => a.Key, a => a.Order().ToArray()); - - var actualAssetsByIdentity = actual.Assets.GroupBy(a => a.Identity).ToDictionary(a => a.Key, a => a.Order().ToArray()); - foreach (var asset in actual.Assets) - { - if (!string.IsNullOrEmpty(asset.RelatedAsset)) - { - actualAssetsByIdentity.Should().ContainKey(asset.RelatedAsset); - } - } - - foreach (var (group, actualAssetsGroup) in actualAssets) - { - var expectedAssetsGroup = expectedAssets[group]; - CompareAssetGroup(group, actualAssetsGroup, expectedAssetsGroup); - } - - var actualEndpoints = actual.Endpoints - .OrderBy(a => a.Route) - .ThenBy(a => a.AssetFile) - .GroupBy(a => GetEndpointGroup(a)) - .ToDictionary(a => a.Key, a => a.Order().ToArray()); - - SortEndpointProperties(actualEndpoints); - - var duplicateEndpoints = actual.Endpoints - .GroupBy(a => a) - .ToDictionary(a => a.Key, a => a.Order().ToArray()); - - var foundDuplicateEndpoints = duplicateEndpoints.Where(a => DuplicatesExist(a)).ToArray(); - - duplicateEndpoints.Where(a => DuplicatesExist(a)).Should().BeEmpty($@"no duplicate endpoints should exist. But found: - {string.Join($"{Environment.NewLine} ", foundDuplicateEndpoints.Select(a => @$"{a.Key.Route} - {a.Key.AssetFile} - {a.Key.Selectors.Length} - {a.Value.Length}"))}{Environment.NewLine}"); - - foreach (var endpoint in actual.Endpoints) - { - actualAssetsByIdentity.Should().ContainKey(endpoint.AssetFile); - } - - var expectedEndpoints = expected.Endpoints - .OrderBy(a => a.Route) - .ThenBy(a => a.AssetFile) - .GroupBy(a => GetEndpointGroup(a)) - .ToDictionary(a => a.Key, a => a.Order().ToArray()); - - SortEndpointProperties(expectedEndpoints); - - foreach (var (group, actualEndpointsGroup) in actualEndpoints) - { - var expectedEndpointsGroup = expectedEndpoints[group]; - CompareEndpointGroup(group, actualEndpointsGroup, expectedEndpointsGroup); - } - - static bool DuplicatesExist(KeyValuePair a) - { - var endpoint = a.Key; - if (endpoint.Route.EndsWith(".gz") || endpoint.Route.EndsWith(".br") || endpoint.Selectors.Length == 1) - { - // This is not exact, but there are situations in which our templatization process is not biyective and Build and Publish assets defined during build for - // the same asset end up having the same endpoint. To avoid issues with this, we relax the check to support finding more than one. - return a.Value.Length > 2; - } - else - { - return a.Value.Length > 1; - } - } - } - - private static void SortEndpointProperties(Dictionary endpoints) - { - foreach (var endpointGroup in endpoints.Values) - { - foreach (var endpoint in endpointGroup) - { - Array.Sort(endpoint.Selectors, (a, b) => (a.Name, a.Value).CompareTo((b.Name, b.Value))); - Array.Sort(endpoint.ResponseHeaders, (a, b) => (a.Name, a.Value).CompareTo((b.Name, b.Value))); - Array.Sort(endpoint.EndpointProperties, (a, b) => (a.Name, a.Value).CompareTo((b.Name, b.Value))); - } - } - } - - protected virtual void CompareAssetGroup(string group, StaticWebAsset[] manifestAssets, StaticWebAsset[] expectedAssets) - { - var comparisonMode = CompareAssetCounts(group, manifestAssets, expectedAssets); - Array.Sort(manifestAssets, (a, b) => a.Identity.CompareTo(b.Identity)); - Array.Sort(expectedAssets, (a, b) => a.Identity.CompareTo(b.Identity)); - - // Otherwise, do a property level comparison of all assets - switch (comparisonMode) - { - case GroupComparisonMode.Exact: - break; - case GroupComparisonMode.AllowAdditionalAssets: - break; - default: - break; - } - - var differences = new List(); - var assetDifferences = new List(); - var groupLength = Math.Min(manifestAssets.Length, expectedAssets.Length); - for (var i = 0; i < groupLength; i++) - { - var manifestAsset = manifestAssets[i]; - var expectedAsset = expectedAssets[i]; - - ComputeAssetDifferences(assetDifferences, manifestAsset, expectedAsset); - - if (assetDifferences.Any()) - { - differences.Add(@$" - ================================================== - - For {expectedAsset.Identity}: - - {string.Join(Environment.NewLine, assetDifferences)} - - =================================================="); - } - - assetDifferences.Clear(); - } - - differences.Should().BeEmpty( - @$" the generated manifest should match the expected baseline. - - {BaselineGenerationInstructions} - - "); - } - - private GroupComparisonMode CompareAssetCounts(string group, StaticWebAsset[] manifestAssets, StaticWebAsset[] expectedAssets) - { - var comparisonMode = GetGroupComparisonMode(group); - - // If there's a mismatch in the number of assets, just print the strict difference in the asset `Identity` - switch (comparisonMode) - { - case GroupComparisonMode.Exact: - if (manifestAssets.Length != expectedAssets.Length) - { - ThrowAssetCountMismatchError(manifestAssets, expectedAssets); - } - break; - case GroupComparisonMode.AllowAdditionalAssets: - if (expectedAssets.Except(manifestAssets).Any()) - { - ThrowAssetCountMismatchError(manifestAssets, expectedAssets); - } - break; - default: - break; - } - - return comparisonMode; - - static void ThrowAssetCountMismatchError(StaticWebAsset[] manifestAssets, StaticWebAsset[] expectedAssets) - { - var missingAssets = expectedAssets.Except(manifestAssets); - var unexpectedAssets = manifestAssets.Except(expectedAssets); - - var differences = new List(); - - if (missingAssets.Any()) - { - differences.Add($@"The following expected assets weren't found in the manifest: - {string.Join($"{Environment.NewLine}\t", missingAssets.Select(a => a.Identity))}"); - } - - if (unexpectedAssets.Any()) - { - differences.Add($@"The following additional unexpected assets were found in the manifest: - {string.Join($"{Environment.NewLine}\t", unexpectedAssets.Select(a => a.Identity))}"); - } - - throw new Exception($@"{string.Join(Environment.NewLine, differences)} - - {BaselineGenerationInstructions}"); - } - } - - protected virtual GroupComparisonMode GetGroupComparisonMode(string group) - { - return GroupComparisonMode.Exact; - } - - private static void ComputeAssetDifferences(List assetDifferences, StaticWebAsset manifestAsset, StaticWebAsset expectedAsset) - { - if (manifestAsset.Identity != expectedAsset.Identity) - { - assetDifferences.Add($"Expected manifest Identity of {expectedAsset.Identity} but found {manifestAsset.Identity}."); - } - if (manifestAsset.SourceType != expectedAsset.SourceType) - { - assetDifferences.Add($"Expected manifest SourceType of {expectedAsset.SourceType} but found {manifestAsset.SourceType}."); - } - if (manifestAsset.SourceId != expectedAsset.SourceId) - { - assetDifferences.Add($"Expected manifest SourceId of {expectedAsset.SourceId} but found {manifestAsset.SourceId}."); - } - if (manifestAsset.ContentRoot != expectedAsset.ContentRoot) - { - assetDifferences.Add($"Expected manifest ContentRoot of {expectedAsset.ContentRoot} but found {manifestAsset.ContentRoot}."); - } - if (manifestAsset.BasePath != expectedAsset.BasePath) - { - assetDifferences.Add($"Expected manifest BasePath of {expectedAsset.BasePath} but found {manifestAsset.BasePath}."); - } - if (manifestAsset.RelativePath != expectedAsset.RelativePath) - { - assetDifferences.Add($"Expected manifest RelativePath of {expectedAsset.RelativePath} but found {manifestAsset.RelativePath}."); - } - if (manifestAsset.AssetKind != expectedAsset.AssetKind) - { - assetDifferences.Add($"Expected manifest AssetKind of {expectedAsset.AssetKind} but found {manifestAsset.AssetKind}."); - } - if (manifestAsset.AssetMode != expectedAsset.AssetMode) - { - assetDifferences.Add($"Expected manifest AssetMode of {expectedAsset.AssetMode} but found {manifestAsset.AssetMode}."); - } - if (manifestAsset.AssetRole != expectedAsset.AssetRole) - { - assetDifferences.Add($"Expected manifest AssetRole of {expectedAsset.AssetRole} but found {manifestAsset.AssetRole}."); - } - if (manifestAsset.RelatedAsset != expectedAsset.RelatedAsset) - { - assetDifferences.Add($"Expected manifest RelatedAsset of {expectedAsset.RelatedAsset} but found {manifestAsset.RelatedAsset}."); - } - if (manifestAsset.AssetTraitName != expectedAsset.AssetTraitName) - { - assetDifferences.Add($"Expected manifest AssetTraitName of {expectedAsset.AssetTraitName} but found {manifestAsset.AssetTraitName}."); - } - if (manifestAsset.AssetTraitValue != expectedAsset.AssetTraitValue) - { - assetDifferences.Add($"Expected manifest AssetTraitValue of {expectedAsset.AssetTraitValue} but found {manifestAsset.AssetTraitValue}."); - } - if (manifestAsset.CopyToOutputDirectory != expectedAsset.CopyToOutputDirectory) - { - assetDifferences.Add($"Expected manifest CopyToOutputDirectory of {expectedAsset.CopyToOutputDirectory} but found {manifestAsset.CopyToOutputDirectory}."); - } - if (manifestAsset.CopyToPublishDirectory != expectedAsset.CopyToPublishDirectory) - { - assetDifferences.Add($"Expected manifest CopyToPublishDirectory of {expectedAsset.CopyToPublishDirectory} but found {manifestAsset.CopyToPublishDirectory}."); - } - if (manifestAsset.OriginalItemSpec != expectedAsset.OriginalItemSpec) - { - assetDifferences.Add($"Expected manifest OriginalItemSpec of {expectedAsset.OriginalItemSpec} but found {manifestAsset.OriginalItemSpec}."); - } - } - - protected virtual string GetAssetGroup(StaticWebAsset asset) - { - return Path.GetExtension(asset.Identity.TrimEnd(']')); - } - - protected virtual void CompareEndpointGroup(string group, StaticWebAssetEndpoint[] manifestAssets, StaticWebAssetEndpoint[] expectedAssets) - { - var comparisonMode = CompareEndpointCounts(group, manifestAssets, expectedAssets); - Array.Sort(manifestAssets); - Array.Sort(expectedAssets); - - // Otherwise, do a property level comparison of all assets - switch (comparisonMode) - { - case GroupComparisonMode.Exact: - break; - case GroupComparisonMode.AllowAdditionalAssets: - break; - default: - break; - } - - var differences = new List(); - var assetDifferences = new List(); - var groupLength = Math.Min(manifestAssets.Length, expectedAssets.Length); - for (var i = 0; i < groupLength; i++) - { - var manifestAsset = manifestAssets[i]; - var expectedAsset = expectedAssets[i]; - - ComputeEndpointDifferences(assetDifferences, manifestAsset, expectedAsset); - - if (assetDifferences.Any()) - { - differences.Add(@$" - ================================================== - - For {expectedAsset.AssetFile}: - - {string.Join(Environment.NewLine, assetDifferences)} - - =================================================="); - } - - assetDifferences.Clear(); - } - - differences.Should().BeEmpty( - @$" the generated manifest should match the expected baseline. - - {BaselineGenerationInstructions} - - "); - } - - private GroupComparisonMode CompareEndpointCounts(string group, StaticWebAssetEndpoint[] manifestEndpoints, StaticWebAssetEndpoint[] expectedEndpoints) - { - var comparisonMode = GetGroupComparisonMode(group); - - // If there's a mismatch in the number of assets, just print the strict difference in the asset `Identity` - switch (comparisonMode) - { - case GroupComparisonMode.Exact: - if (manifestEndpoints.Length != expectedEndpoints.Length) - { - ThrowEndpointCountMismatchError(group, manifestEndpoints, expectedEndpoints); - } - break; - case GroupComparisonMode.AllowAdditionalAssets: - if (expectedEndpoints.Except(manifestEndpoints).Any()) - { - ThrowEndpointCountMismatchError(group, manifestEndpoints, expectedEndpoints); - } - break; - default: - break; - } - - return comparisonMode; - - static void ThrowEndpointCountMismatchError(string group, StaticWebAssetEndpoint[] manifestEndpoints, StaticWebAssetEndpoint[] expectedEndpoints) - { - var missingEndpoints = expectedEndpoints.Except(manifestEndpoints); - var unexpectedEndpoints = manifestEndpoints.Except(expectedEndpoints); - - var differences = new List - { - $"Expected group '{group}' to have '{expectedEndpoints.Length}' endpoints but found '{manifestEndpoints.Length}'.", - "Expected Endpoints:", - string.Join($"{Environment.NewLine}\t", expectedEndpoints.Select(a => $"{a.Route} - {a.Selectors.Length} - {a.AssetFile}")), - "Actual Endpoints:", - string.Join($"{Environment.NewLine}\t", manifestEndpoints.Select(a => $"{a.Route} - {a.Selectors.Length} - {a.AssetFile}")) - }; - - if (missingEndpoints.Any()) - { - differences.Add($@"The following expected assets weren't found in the manifest: - {string.Join($"{Environment.NewLine}\t", missingEndpoints.Select(a => $"{a.Route} - {a.AssetFile}"))}"); - } - - if (unexpectedEndpoints.Any()) - { - differences.Add($@"The following additional unexpected assets were found in the manifest: - {string.Join($"{Environment.NewLine}\t", unexpectedEndpoints.Select(a => $"{a.Route} - {a.AssetFile}"))}"); - } - - throw new Exception($@"{string.Join(Environment.NewLine, differences)} - - {BaselineGenerationInstructions}"); - } - } - - protected virtual GroupComparisonMode GetAssetGroupComparisonMode(string group) - { - return GroupComparisonMode.Exact; - } - - private static void ComputeEndpointDifferences(List assetDifferences, StaticWebAssetEndpoint manifestAsset, StaticWebAssetEndpoint expectedAsset) - { - if (manifestAsset.Route != expectedAsset.Route) - { - assetDifferences.Add($"Expected manifest Identity of {expectedAsset.Route} but found {manifestAsset.Route}."); - } - if (manifestAsset.AssetFile != expectedAsset.AssetFile) - { - assetDifferences.Add($"Expected manifest SourceType of {expectedAsset.AssetFile} but found {manifestAsset.AssetFile}."); - } - if ((manifestAsset.Order ?? "") != (expectedAsset.Order ?? "")) - { - assetDifferences.Add($"Expected manifest Order of '{expectedAsset.Order}' but found '{manifestAsset.Order}'."); - } - - ComputeSelectorDifferences(assetDifferences, manifestAsset.Selectors, expectedAsset.Selectors); - ComputeResponseHeaderDifferences(assetDifferences, manifestAsset.ResponseHeaders, expectedAsset.ResponseHeaders); - } - - private static void ComputeResponseHeaderDifferences( - List assetDifferences, - StaticWebAssetEndpointResponseHeader[] manifestResponseHeaders, - StaticWebAssetEndpointResponseHeader[] expectedResponseHeaders) - { - if (manifestResponseHeaders.Length != expectedResponseHeaders.Length) - { - assetDifferences.Add($"Expected manifest to have {expectedResponseHeaders.Length} response headers but found {manifestResponseHeaders.Length}."); - } - - var manifest = new HashSet(manifestResponseHeaders); - var differences = new HashSet(manifestResponseHeaders); - var expected = new HashSet(expectedResponseHeaders); - differences.SymmetricExceptWith(expected); - - foreach (var difference in differences) - { - if (!manifest.Contains(difference)) - { - assetDifferences.Add($"Expected manifest to have response header '{difference.Name}={difference.Value}' but it was not found."); - } - else - { - assetDifferences.Add($"Found unexpected response header '{difference.Name}={difference.Value}'."); - } - } - } - - private static void ComputeSelectorDifferences( - List assetDifferences, - StaticWebAssetEndpointSelector[] manifestSelectors, - StaticWebAssetEndpointSelector[] expectedSelectors) - { - if (manifestSelectors.Length != expectedSelectors.Length) - { - assetDifferences.Add($"Expected manifest to have {expectedSelectors.Length} selectors but found {manifestSelectors.Length}."); - } - - var manifest = new HashSet(manifestSelectors); - var differences = new HashSet(manifestSelectors); - var expected = new HashSet(expectedSelectors); - differences.SymmetricExceptWith(expected); - - foreach (var difference in differences) - { - if (!manifest.Contains(difference)) - { - assetDifferences.Add($"Expected manifest to have selector '{difference.Name}={difference.Value};q={difference.Quality}' but it was not found."); - } - else - { - assetDifferences.Add($"Found unexpected selector '{difference.Name}={difference.Value};q={difference.Quality}'."); - } - } - } - - protected virtual string GetEndpointGroup(StaticWebAssetEndpoint asset) - { - return Path.GetExtension(asset.AssetFile.TrimEnd(']')); - } - } - - public enum GroupComparisonMode - { - // We require the same number of assets in a group for the baseline and the template. - Exact, - - // We won't fail when we check against the baseline if additional assets are present for a group. - AllowAdditionalAssets - } - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineFactory.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineFactory.cs index 7e0490d54b5f..e783d934fe8a 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineFactory.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsBaselineFactory.cs @@ -8,966 +8,484 @@ using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - using System.Runtime.Versioning; - using System.Text.RegularExpressions; - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - using NuGet.Frameworks; - using NuGet.ProjectModel; - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - public partial class StaticWebAssetsBaselineFactory - { - [GeneratedRegex("""(.*\.)([0123456789abcdefghijklmnopqrstuvwxyz]{10})(\.bundle\.scp\.css)((?:\.gz)|(?:\.br))?$""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] - private static partial Regex ScopedProjectBundleRegex(); - - [GeneratedRegex("""(.*\.)([0123456789abcdefghijklmnopqrstuvwxyz]{10})(\.styles\.css)((?:\.gz)|(?:\.br))?$""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] - private static partial Regex ScopedAppBundleRegex(); - - [GeneratedRegex("""fingerprint-site(\.)([0123456789abcdefghijklmnopqrstuvwxyz]{10})(\.css)((?:\.gz)|(?:\.br))?$""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] - private static partial Regex FingerprintedSiteCssRegex(); - - [GeneratedRegex("""(?:#\[\.{fingerprint=[0123456789abcdefghijklmnopqrstuvwxyz]{10}\}](\?|\!)?)""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] - private static partial Regex EmbeddedFingerprintExpression(); - - [GeneratedRegex("""(.*\.)([0123456789abcdefghijklmnopqrstuvwxyz]{10})(\.lib\.module\.js)((?:\.gz)|(?:\.br))?$""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] - private static partial Regex JSInitializerRegex(); - - [GeneratedRegex("""(.*\.)([0123456789abcdefghijklmnopqrstuvwxyz]{10})(\.modules\.json)((?:\.gz)|(?:\.br))?$""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] - private static partial Regex JSModuleManifestRegex(); - - private static readonly IList<(Regex expression, string replacement)> WellKnownFileNamePatternsAndReplacements = - [ - (ScopedProjectBundleRegex(),"$1__fingerprint__$3$4"), - (ScopedAppBundleRegex(),"$1__fingerprint__$3$4"), - (JSInitializerRegex(), "$1__fingerprint__$3$4"), - (JSModuleManifestRegex(), "$1__fingerprint__$3$4"), - (EmbeddedFingerprintExpression(), "#[.{fingerprint=__fingerprint__}]$1"), - (FingerprintedSiteCssRegex(), "fingerprint-site$1__fingerprint__$3$4"), - ]; - - public static StaticWebAssetsBaselineFactory Instance { get; } = new(); - - public IList KnownExtensions { get; } = - [ - // Keep this list of most specific to less specific - ".dll.gz", - ".dll.br", - ".dll", - ".wasm.gz", - ".wasm.br", - ".wasm", - ".js.gz", - ".js.br", - ".js", - ".html", - ".pdb", - ]; - - public IList KnownFilePrefixesWithHashOrVersion { get; } = - [ - "blazor.web.", - "blazor.server", - "dotnet.runtime", - "dotnet.native", - "dotnet.boot", - "dotnet" - ]; - - public void ToTemplate( - StaticWebAssetsManifest manifest, - string projectRoot, - string restorePath, - string runtimeIdentifier) - { - manifest.Hash = "__hash__"; - var assetsByIdentity = manifest.Assets.ToDictionary(a => a.Identity); - var endpointsByAssetFile = manifest.Endpoints.GroupBy(e => e.AssetFile).ToDictionary(g => g.Key, g => g.ToArray()); - foreach (var asset in manifest.Assets) - { - var relatedEndpoints = endpointsByAssetFile.GetValueOrDefault(asset.Identity); - TemplatizeAsset(projectRoot, restorePath, runtimeIdentifier, asset); - foreach (var endpoint in relatedEndpoints ?? []) - { - endpoint.AssetFile = asset.Identity; - } - if (asset.AssetTraitName == "Content-Encoding") - { - var basePath = asset.BasePath.Replace('/', Path.DirectorySeparatorChar).TrimStart(Path.DirectorySeparatorChar); - var relativePath = asset.RelativePath.Replace('/', Path.DirectorySeparatorChar); - var identity = asset.Identity.Replace('\\', Path.DirectorySeparatorChar); - var originalItemSpec = asset.OriginalItemSpec.Replace('\\', Path.DirectorySeparatorChar); - - asset.Identity = Path.Combine(Path.GetDirectoryName(identity), basePath, relativePath); - asset.Identity = asset.Identity.Replace(Path.DirectorySeparatorChar, '\\'); - foreach (var endpoint in relatedEndpoints ?? []) - { - endpoint.AssetFile = asset.Identity; - } - asset.OriginalItemSpec = Path.Combine(Path.GetDirectoryName(originalItemSpec), basePath, relativePath); - asset.OriginalItemSpec = asset.OriginalItemSpec.Replace(Path.DirectorySeparatorChar, '\\'); - } - else if ((asset.Identity.EndsWith(".gz", StringComparison.OrdinalIgnoreCase) || asset.Identity.EndsWith(".br", StringComparison.OrdinalIgnoreCase)) - && asset.AssetTraitName == "" && asset.RelatedAsset == "") - { - // Old .NET 5.0 implementation - var identity = asset.Identity.Replace('\\', Path.DirectorySeparatorChar); - var originalItemSpec = asset.OriginalItemSpec.Replace('\\', Path.DirectorySeparatorChar); - - asset.Identity = Path.Combine(Path.GetDirectoryName(identity), Path.GetFileName(originalItemSpec) + Path.GetExtension(identity)) - .Replace(Path.DirectorySeparatorChar, '\\'); - } - } - - foreach (var endpoint in manifest.Endpoints) - { - for (var i = 0; i < endpoint.ResponseHeaders.Length; i++) - { - ref var header = ref endpoint.ResponseHeaders[i]; - switch (header.Name) - { - case "Content-Length": - header.Value = "__content-length__"; - break; - case "ETag": - header.Value = "__etag__"; - break; - case "Last-Modified": - header.Value = "__last-modified__"; - break; - case "Link": - var cleaned = new List(); - var values = header.Value.Split(',').Select(v => v.Trim()); - foreach (var value in values) - { - var segments = value.Split(';').Select(v => v.Trim()).ToArray(); - var file = segments[0][1..^1]; - segments[0] = $"<{ReplaceFileName(file).Replace('\\', '/')}>"; - cleaned.Add(string.Join("; ", segments)); - } - header.Value = string.Join(", ", cleaned); - - break; - default: - break; - } - } - - for (var i = 0; i < endpoint.EndpointProperties.Length; i++) - { - ref var property = ref endpoint.EndpointProperties[i]; - switch (property.Name) - { - case "fingerprint": - property.Value = "__fingerprint__"; - endpoint.Route = endpoint.Route.Replace(property.Value, $"__{property.Name}__"); - break; - case "integrity": - property.Value = "__integrity__"; - break; - case "original-resource": - property.Value = "__original-resource__"; - break; - default: - break; - } - - ReplaceFileName(endpoint.Route); - } - - for (var i = 0; i < endpoint.Selectors.Length; i++) - { - ref var selector = ref endpoint.Selectors[i]; - selector.Quality = "__quality__"; - } - - endpoint.Route = TemplatizeFilePath(endpoint.Route, null, null, null, null, null).Replace("\\", "/"); - - endpoint.AssetFile = TemplatizeFilePath( - endpoint.AssetFile, - restorePath, - projectRoot, - null, - null, - runtimeIdentifier); - } - - foreach (var discovery in manifest.DiscoveryPatterns) - { - discovery.ContentRoot = discovery.ContentRoot.Replace(projectRoot, "${ProjectPath}", StringComparison.OrdinalIgnoreCase); - discovery.ContentRoot = discovery.ContentRoot.Replace(Path.DirectorySeparatorChar, '\\'); - - discovery.Name = discovery.Name.Replace(Path.DirectorySeparatorChar, '\\'); - discovery.Pattern = discovery.Pattern.Replace(Path.DirectorySeparatorChar, '\\'); - } - - foreach (var relatedManifest in manifest.ReferencedProjectsConfiguration) - { - relatedManifest.Identity = relatedManifest.Identity.Replace(projectRoot, "${ProjectPath}").Replace(Path.DirectorySeparatorChar, '\\'); - } - - // Sor everything now to ensure we produce stable baselines independent of the machine they were generated on. - Array.Sort(manifest.DiscoveryPatterns, (l, r) => StringComparer.Ordinal.Compare(l.Name, r.Name)); - Array.Sort(manifest.Assets); - foreach (var endpoint in manifest.Endpoints) - { - Array.Sort(endpoint.Selectors); - Array.Sort(endpoint.EndpointProperties); - Array.Sort(endpoint.ResponseHeaders); - } - Array.Sort(manifest.Endpoints); - - Array.Sort(manifest.ReferencedProjectsConfiguration, (l, r) => StringComparer.Ordinal.Compare(l.Identity, r.Identity)); - } - - private void TemplatizeAsset(string projectRoot, string restorePath, string runtimeIdentifier, StaticWebAsset asset) - { - asset.Identity = TemplatizeFilePath( - asset.Identity, - restorePath, - projectRoot, - null, - null, - runtimeIdentifier); - - asset.RelativePath = TemplatizeFilePath( - asset.RelativePath, - null, - null, - null, - null, - runtimeIdentifier).Replace('\\', '/'); - - asset.ContentRoot = TemplatizeFilePath( - asset.ContentRoot, - restorePath, - projectRoot, - null, - null, - runtimeIdentifier); - - asset.RelatedAsset = TemplatizeFilePath( - asset.RelatedAsset, - restorePath, - projectRoot, - null, - null, - runtimeIdentifier); - - asset.OriginalItemSpec = TemplatizeFilePath( - asset.OriginalItemSpec, - restorePath, - projectRoot, - null, - null, - runtimeIdentifier); - - asset.Fingerprint = string.IsNullOrEmpty(asset.Fingerprint) ? asset.Fingerprint : "__fingerprint__"; - asset.Integrity = string.IsNullOrEmpty(asset.Integrity) ? asset.Integrity : "__integrity__"; - asset.FileLength = -1; - asset.LastWriteTime = DateTimeOffset.MinValue; - } - - internal IEnumerable TemplatizeExpectedFiles( - IEnumerable files, - string restorePath, - string projectPath, - string intermediateOutputPath, - string buildOrPublishFolder) - { - foreach (var file in files) - { - var updated = TemplatizeFilePath( - file, - restorePath, - projectPath, - intermediateOutputPath, - buildOrPublishFolder, - null); - - yield return updated; - } - } - - public string TemplatizeFilePath( - string file, - string restorePath, - string projectPath, - string intermediateOutputPath, - string buildOrPublishFolder, - string runtimeIdentifier) - { - var updated = file switch - { - var processed when file.StartsWith('$') => processed, - var fromBuildOrPublishPath when buildOrPublishFolder is not null && file.StartsWith(buildOrPublishFolder, StringComparison.OrdinalIgnoreCase) => - TemplatizeBuildOrPublishPath(buildOrPublishFolder, fromBuildOrPublishPath), - var fromIntermediateOutputPath when intermediateOutputPath is not null && file.StartsWith(intermediateOutputPath, StringComparison.OrdinalIgnoreCase) => - TemplatizeIntermediatePath(intermediateOutputPath, fromIntermediateOutputPath), - var fromPackage when restorePath is not null && file.StartsWith(restorePath, StringComparison.OrdinalIgnoreCase) => - TemplatizeNugetPath(restorePath, fromPackage), - var fromProject when projectPath is not null && file.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase) => - TemplatizeProjectPath(projectPath, fromProject, runtimeIdentifier), - var fromWebAssemblySdk when file.Replace('\\', '/').Contains("/Sdks/Microsoft.NET.Sdk.WebAssembly", StringComparison.OrdinalIgnoreCase) => - TemplatizeWebAssemblySdkPath(fromWebAssemblySdk), - _ => - ReplaceSegments(file, (i, segments) => i switch - { - 2 when segments[0] is "obj" or "bin" => "${Tfm}", - var last when i == segments.Length - 1 => RemovePossibleHash(segments[last]), - _ => segments[i] - }) - }; - - return ReplaceFileName(updated).Replace('/', '\\'); - } - - private string TemplatizeWebAssemblySdkPath(string file) - { - var normalized = file.Replace('\\', '/'); - var marker = "/Sdks/Microsoft.NET.Sdk.WebAssembly"; - var idx = normalized.IndexOf(marker, StringComparison.OrdinalIgnoreCase); - if (idx < 0) - { - return file; - } - - // Replace everything up to and including the SDK folder with the token - var remainder = normalized.Substring(idx + marker.Length); - if (remainder.StartsWith('/')) - { - remainder = remainder[1..]; - } - - var templated = "${WebAssemblySdkPath}" + (string.IsNullOrEmpty(remainder) ? string.Empty : "/" + remainder); - - // Replace filename hashes if any - templated = ReplaceSegments(templated, (i, segments) => i switch - { - _ when i == segments.Length - 1 => RemovePossibleHash(segments[i]), - _ => segments[i] - }); - - return templated; - } - - private static string ReplaceFileName(string path) - { - var directory = Path.GetDirectoryName(path); - var fileName = Path.GetFileName(path); - foreach (var (expression, replacement) in WellKnownFileNamePatternsAndReplacements) - { - if (expression.IsMatch(fileName)) - { - fileName = expression.Replace(fileName, replacement); - return Path.Combine(directory, fileName); - } - } - - return path; - } - - private string TemplatizeBuildOrPublishPath(string outputPath, string file) - { - file = file.Replace(outputPath, "${OutputPath}") - .Replace('\\', '/'); - - file = ReplaceSegments(file, (i, segments) => i switch - { - _ when i == segments.Length - 1 => RemovePossibleHash(segments[i]), - _ => segments[i], - }); - - return file; - } - - private string TemplatizeIntermediatePath(string intermediatePath, string file) - { - file = file.Replace(intermediatePath, "${IntermediateOutputPath}") - .Replace('\\', '/'); - - file = ReplaceSegments(file, (i, segments) => i switch - { - 3 when segments[1] is "obj" or "bin" => "${Tfm}", - _ when i == segments.Length - 1 => RemovePossibleHash(segments[i]), - _ => segments[i] - }); - - return file; - } - - private string TemplatizeProjectPath(string projectPath, string file, string runtimeIdentifier) - { - file = file.Replace(projectPath, "${ProjectPath}") - .Replace('\\', '/'); - - file = ReplaceSegments(file, (i, segments) => i switch - { - 3 when segments[1] is "obj" or "bin" => "${Tfm}", - 4 when segments[2] is "obj" or "bin" => "${Tfm}", - 4 when segments[1] is "obj" or "bin" && segments[4] == runtimeIdentifier => "${Rid}", - 5 when segments[2] is "obj" or "bin" && segments[5] == runtimeIdentifier => "${Rid}", - _ when i == segments.Length - 1 => RemovePossibleHash(segments[i]), - _ => segments[i] - }); - - return file; - } - - private string TemplatizeNugetPath(string restorePath, string file) - { - file = file.Replace(restorePath, "${RestorePath}", StringComparison.OrdinalIgnoreCase) - .Replace('\\', '/'); - if (file.Contains("runtimes")) - { - file = ReplaceSegments(file, (i, segments) => i switch - { - 2 => "${RuntimeVersion}", - 6 when !file.Contains("native") => "${Tfm}", - _ when i == segments.Length - 1 => RemovePossibleHash(segments[i]), - _ => segments[i], - }); - } - else - { - file = ReplaceSegments(file, (i, segments) => i switch - { - 2 => "${PackageVersion}", - 4 when IsFramework(segments[4]) => "${Tfm}", - _ when i == segments.Length - 1 => RemovePossibleHash(segments[i]), - _ => segments[i], - }); - } - - return file; - - static bool IsFramework(string segment) - { - try - { - var tfm = NuGetFramework.ParseFolder(segment); - - return tfm.Framework is FrameworkConstants.FrameworkIdentifiers.NetCoreApp or - FrameworkConstants.FrameworkIdentifiers.NetStandard or - FrameworkConstants.FrameworkIdentifiers.NetCore or - FrameworkConstants.FrameworkIdentifiers.Net; - } - catch - { - return false; - } - } - } - - private static string ReplaceSegments(string file, Func selector) - { - var segments = file.Split('\\', '/'); - var newSegments = new List(); - - // Segments have the following shape `${RestorePath}/PackageName/PackageVersion/lib/Tfm/dll`. - // We want to replace PackageVersion and Tfm with tokens so that they do not cause issues. - for (var i = 0; i < segments.Length; i++) - { - newSegments.Add(selector(i, segments)); - } - - return string.Join(Path.DirectorySeparatorChar, newSegments); - } - - private string RemovePossibleHash(string fileNameAndExtension) - { - var filename = KnownFilePrefixesWithHashOrVersion.FirstOrDefault(p => fileNameAndExtension.StartsWith(p)); - if (filename != null && filename.EndsWith(".")) - { - filename = filename[..^1]; - } - var extension = KnownExtensions.FirstOrDefault(f => fileNameAndExtension.EndsWith(f, StringComparison.OrdinalIgnoreCase)); - if (filename != null && extension != null) - { - fileNameAndExtension = filename + extension; - } - - return fileNameAndExtension; - } - } - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCompressionIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCompressionIntegrationTest.cs index 3561d6164551..7482a1d5871f 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCompressionIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCompressionIntegrationTest.cs @@ -6,621 +6,209 @@ using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using System.Net.Http.Headers; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class StaticWebAssetsCompressionIntegrationTest : AspNetSdkBaselineTest - - { - - - - - [TestMethod] - - public void Build_Detects_PrecompressedAssets() - - { - - var expectedManifest = LoadBuildManifest(); - - var testAsset = "RazorAppWithP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var file = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js"); - - var gzipFile = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js.gz"); - - var brotliFile = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js.br"); - - - - // Compress file into gzip and brotli - - using (var gzipStream = new GZipStream(File.Create(gzipFile), CompressionLevel.NoCompression)) - - { - - using var stream = File.OpenRead(file); - - stream.CopyTo(gzipStream); - - } - - - - using (var brotliStream = new BrotliStream(File.Create(brotliFile), CompressionLevel.NoCompression)) - - { - - using var stream = File.OpenRead(file); - - stream.CopyTo(brotliStream); - - } - - - - var build = CreateBuildCommand(ProjectDirectory, "AppWithP2PReference"); - - 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)); - - AssertManifest(manifest, expectedManifest); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "AppWithP2PReference.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - - - var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); - - AssertManifest(manifest1, expectedManifest); - - AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); - - - - var manifest2 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); - - - - var standardEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, file, StringComparison.Ordinal)).ToArray(); - - var gzipEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, gzipFile, StringComparison.Ordinal)).ToArray(); - - var brotliEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, brotliFile, StringComparison.Ordinal)).ToArray(); - - - - var gzipAsset = manifest2.Assets.Single(a => string.Equals(a.Identity, gzipFile, StringComparison.Ordinal)); - - var brotliAsset = manifest2.Assets.Single(a => string.Equals(a.Identity, brotliFile, StringComparison.Ordinal)); - - - - standardEndpoints.Should().HaveCount(1); - - gzipEndpoints.Should().HaveCount(2); - - brotliEndpoints.Should().HaveCount(2); - - - - - - foreach (var endpoint in gzipEndpoints) - - { - - endpoint.ResponseHeaders.Where(e => e.Name == "Content-Encoding").Select(e => e.Value).Single().Should().Be("gzip"); - - - - var etags = endpoint.ResponseHeaders.Where(e => e.Name == "ETag").Select(e => EntityTagHeaderValue.Parse(e.Value)); - - etags.Where(e=> !e.IsWeak).Select(e => e.Tag).Single().Should().BeEquivalentTo($"\"{gzipAsset.Integrity}\""); - - if (endpoint.Route.EndsWith(".gz")) - - { - - continue; - - } - - } - - - - foreach (var endpoint in brotliEndpoints) - - { - - endpoint.ResponseHeaders.Where(e => e.Name == "Content-Encoding").Select(e => e.Value).Single().Should().Be("br"); - - - - var etags = endpoint.ResponseHeaders.Where(e => e.Name == "ETag").Select(e => EntityTagHeaderValue.Parse(e.Value)); - - etags.Where(e => !e.IsWeak).Select(e => e.Tag).Single().Should().BeEquivalentTo($"\"{brotliAsset.Integrity}\""); - - if (endpoint.Route.EndsWith(".br")) - - { - - continue; - - } - - } - - } - - - - [TestMethod] - - public void CanEnable_CompressionOnAllAssets() - - { - - var expectedManifest = LoadBuildManifest(); - - var testAsset = "RazorAppWithP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) - - .WithProjectChanges((project, xml) => - - { - - if (project.Contains("ClassLibrary")) - - { - - xml.Descendants("PropertyGroup") - - .First().Add(new XElement("StaticWebAssetBuildCompressAllAssets", "true")); - - } - - }); - - - - var build = CreateBuildCommand(ProjectDirectory, "AppWithP2PReference"); - - 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)); - - AssertManifest(manifest, expectedManifest); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "AppWithP2PReference.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - } - - - - [TestMethod] - - public void PublishWorks_With_PrecompressedAssets() - - { - - var expectedManifest = LoadBuildManifest(); - - var testAsset = "RazorAppWithP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var file = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js"); - - var gzipFile = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js.gz"); - - var brotliFile = Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js.br"); - - - - // Compress file into gzip and brotli - - using (var gzipStream = new GZipStream(File.Create(gzipFile), CompressionLevel.NoCompression)) - - { - - using var stream = File.OpenRead(file); - - stream.CopyTo(gzipStream); - - } - - - - using (var brotliStream = new BrotliStream(File.Create(brotliFile), CompressionLevel.NoCompression)) - - { - - using var stream = File.OpenRead(file); - - stream.CopyTo(brotliStream); - - } - - - - var build = CreatePublishCommand(ProjectDirectory, "AppWithP2PReference"); - - 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)); - - AssertManifest(manifest, expectedManifest); - - - - - - var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); - - AssertManifest(manifest1, expectedManifest); - - AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); - - - - var manifest2 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"))); - - - - var standardEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, file, StringComparison.Ordinal)).ToArray(); - - var gzipEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, gzipFile, StringComparison.Ordinal)).ToArray(); - - var brotliEndpoints = manifest2.Endpoints.Where(e => string.Equals(e.AssetFile, brotliFile, StringComparison.Ordinal)).ToArray(); - - - - var gzipAsset = manifest2.Assets.Single(a => string.Equals(a.Identity, gzipFile, StringComparison.Ordinal)); - - var brotliAsset = manifest2.Assets.Single(a => string.Equals(a.Identity, brotliFile, StringComparison.Ordinal)); - - - - standardEndpoints.Should().HaveCount(1); - - gzipEndpoints.Should().HaveCount(2); - - brotliEndpoints.Should().HaveCount(2); - - - - - - foreach (var endpoint in gzipEndpoints) - - { - - endpoint.ResponseHeaders.Where(e => e.Name == "Content-Encoding").Select(e => e.Value).Single().Should().Be("gzip"); - - - - var etags = endpoint.ResponseHeaders.Where(e => e.Name == "ETag").Select(e => EntityTagHeaderValue.Parse(e.Value)); - - etags.Where(e => !e.IsWeak).Select(e => e.Tag).Single().Should().BeEquivalentTo($"\"{gzipAsset.Integrity}\""); - - if (endpoint.Route.EndsWith(".gz")) - - { - - continue; - - } - - } - - - - foreach (var endpoint in brotliEndpoints) - - { - - endpoint.ResponseHeaders.Where(e => e.Name == "Content-Encoding").Select(e => e.Value).Single().Should().Be("br"); - - - - var etags = endpoint.ResponseHeaders.Where(e => e.Name == "ETag").Select(e => EntityTagHeaderValue.Parse(e.Value)); - - etags.Where(e => !e.IsWeak).Select(e => e.Tag).Single().Should().BeEquivalentTo($"\"{brotliAsset.Integrity}\""); - - if (endpoint.Route.EndsWith(".br")) - - { - - continue; - - } - - } - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCrossTargetingTests.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCrossTargetingTests.cs index b41d48158da1..3be4a2957dfb 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCrossTargetingTests.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsCrossTargetingTests.cs @@ -2,328 +2,116 @@ // The .NET Foundation licenses this file to you under the MIT license. // 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.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class StaticWebAssetsCrossTargetingTests : IsolatedNuGetPackageFolderAspNetSdkBaselineTest - { - protected override string RestoreNugetPackagePath => nameof(StaticWebAssetsCrossTargetingTests); - - // Build Standalone project - - [TestMethod] - [RequiresMSBuildVersion("17.12", Reason = "Needs System.Text.Json 8.0.5")] - - public void Build_CrosstargetingTests_CanIncludeBrowserAssets() - - { - - var expectedManifest = LoadBuildManifest(); - - var testAsset = "RazorComponentAppMultitarget"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - ProjectDirectory.WithProjectChanges(d => - - { - - d.Root.Element("PropertyGroup").Add( - - XElement.Parse("""/""")); - - - - d.Root.LastNode.AddBeforeSelf( - - XElement.Parse(""" - - - - - - browser - - - - - - """)); - - }); - - - - var wwwroot = Directory.CreateDirectory(Path.Combine(ProjectDirectory.TestRoot, "wwwroot")); - - File.WriteAllText(Path.Combine(wwwroot.FullName, "test.js"), "console.log('hello')"); - - - - 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)); - - AssertManifest(manifest, expectedManifest); - - AssertBuildAssets(manifest, outputPath, intermediateOutputPath); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "RazorComponentAppMultitarget.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - } - - - - [TestMethod] - - public void Publish_CrosstargetingTests_CanIncludeBrowserAssets() - - { - - var testAsset = "RazorComponentAppMultitarget"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - ProjectDirectory.WithProjectChanges(d => - - { - - d.Root.Element("PropertyGroup").Add( - - XElement.Parse("""/""")); - - - - d.Root.LastNode.AddBeforeSelf( - - XElement.Parse(""" - - - - - - browser - - - - - - """)); - - }); - - - - var wwwroot = Directory.CreateDirectory(Path.Combine(ProjectDirectory.TestRoot, "wwwroot")); - - File.WriteAllText(Path.Combine(wwwroot.FullName, "test.js"), "console.log('hello')"); - - - - var restore = CreateRestoreCommand(ProjectDirectory); - - ExecuteCommand(restore).Should().Pass(); - - - - var publish = CreatePublishCommand(ProjectDirectory); - - ExecuteCommandWithoutRestore(publish, "/bl", "/p:TargetFramework=net11.0").Should().Pass(); - - - - var publishPath = publish.GetOutputDirectory(DefaultTfm).ToString(); - - var intermediateOutputPath = publish.GetIntermediateDirectory(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)); - - AssertManifest(manifest, LoadPublishManifest()); - - - - AssertPublishAssets( - - manifest, - - publishPath, - - intermediateOutputPath); - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsDesignTimeTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsDesignTimeTest.cs index e9009e50f619..6c059da352bd 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsDesignTimeTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsDesignTimeTest.cs @@ -2,490 +2,169 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; using Microsoft.NET.TestFramework.Commands; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System; - - using System.Collections.Generic; - - using System.Linq; - - using System.Text; - - using System.Threading.Tasks; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - - - [TestClass] - public class StaticWebAssetsDesignTimeTest : AspNetSdkBaselineTest - - { - - #if DEBUG - - public const string Configuration = "Debug"; - - #else - - public const string Configuration = "Release"; - - #endif - - - - [TestMethod] - - public void CollectUpToDateCheckInputOutputsDesignTime_ReportsAddedFiles() - - { - - // Arrange - - var testAsset = "RazorAppWithP2PReference"; - - ProjectDirectory = AddIntrospection(CreateAspNetSdkTestAsset(testAsset)); - - - - var build = CreateBuildCommand(ProjectDirectory, "ClassLibrary"); - - - - build.Execute("/p:DesignTimeBuild=true", "/p:BuildingInsideVisualStudio=true", "/bl:build.binlog").Should().Pass(); - - - - File.WriteAllText(Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "file.js"), "New File"); - - - - var msbuild = CreateMSBuildCommand( - - ProjectDirectory, - - "ClassLibrary", - - "ResolveStaticWebAssetsConfiguration;ResolveProjectStaticWebAssets;CollectStaticWebAssetInputsDesignTime;CollectStaticWebAssetOutputsDesignTime"); - - - - msbuild.ExecuteWithoutRestore("/p:DesignTimeBuild=true", "/p:BuildingInsideVisualStudio=true", "/bl:design.binlog").Should().Pass(); - - - - // Check the contents of the input and output files - - var inputFilePath = Path.Combine(build.GetIntermediateDirectory().FullName, "StaticWebAssetsUTDCInput.txt"); - - new FileInfo(inputFilePath).Should().Exist(); - - var inputFiles = File.ReadAllLines(inputFilePath); - - inputFiles.Should().HaveCount(3); - - inputFiles.Should().Contain(Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "file.js")); - - inputFiles.Should().Contain(Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js")); - - inputFiles.Should().Contain(Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.v4.js")); - - - - var outputFilePath = Path.Combine(build.GetIntermediateDirectory().FullName, "StaticWebAssetsUTDCOutput.txt"); - - new FileInfo(outputFilePath).Should().Exist(); - - var outputFiles = File.ReadAllLines(outputFilePath); - - outputFiles.Should().ContainSingle(); - - Path.GetFileName(outputFiles[0]).Should().Be("staticwebassets.build.json"); - - } - - - - [TestMethod] - - public void CollectUpToDateCheckInputOutputsDesignTime_ReportsRemovedFiles_Once() - - { - - // Arrange - - var testAsset = "RazorAppWithP2PReference"; - - ProjectDirectory = AddIntrospection(CreateAspNetSdkTestAsset(testAsset)); - - - - var build = CreateBuildCommand(ProjectDirectory, "ClassLibrary"); - - - - build.Execute("/p:DesignTimeBuild=true", "/p:BuildingInsideVisualStudio=true", "/bl:build.binlog").Should().Pass(); - - - - File.Delete(Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.js")); - - - - var msbuild = CreateMSBuildCommand( - - ProjectDirectory, - - "ClassLibrary", - - "ResolveStaticWebAssetsConfiguration;ResolveProjectStaticWebAssets;CollectStaticWebAssetInputsDesignTime;CollectStaticWebAssetOutputsDesignTime"); - - - - msbuild.ExecuteWithoutRestore("/p:DesignTimeBuild=true", "/p:BuildingInsideVisualStudio=true", "/bl:design.binlog").Should().Pass(); - - - - // Check the contents of the input and output files - - var inputFilePath = Path.Combine(build.GetIntermediateDirectory().FullName, "StaticWebAssetsUTDCInput.txt"); - - new FileInfo(inputFilePath).Should().Exist(); - - var inputFiles = File.ReadAllLines(inputFilePath); - - inputFiles.Should().HaveCount(2); - - inputFiles.Should().Contain(Path.Combine(build.GetIntermediateDirectory().FullName, "staticwebassets.removed.txt")); - - inputFiles.Should().Contain(Path.Combine(ProjectDirectory.Path, "ClassLibrary", "wwwroot", "js", "project-transitive-dep.v4.js")); - - - - var outputFilePath = Path.Combine(build.GetIntermediateDirectory().FullName, "StaticWebAssetsUTDCOutput.txt"); - - new FileInfo(outputFilePath).Should().Exist(); - - var outputFiles = File.ReadAllLines(outputFilePath); - - outputFiles.Should().ContainSingle(); - - Path.GetFileName(outputFiles[0]).Should().Be("staticwebassets.build.json"); - - } - - - - [TestMethod] - - public void CollectUpToDateCheckInputOutputsDesignTime_IncludesReferencedProjectsManifests() - - { - - // Arrange - - var testAsset = "RazorAppWithP2PReference"; - - ProjectDirectory = AddIntrospection(CreateAspNetSdkTestAsset(testAsset)); - - - - var build = CreateBuildCommand(ProjectDirectory, "AppWithP2PReference"); - - - - build.Execute("/bl:build.binlog").Should().Pass(); - - build.Execute("/p:DesignTimeBuild=true", "/p:BuildingInsideVisualStudio=true", "/bl:build.binlog").Should().Pass(); - - - - var msbuild = CreateMSBuildCommand( - - ProjectDirectory, - - "AppWithP2PReference", - - "ResolveStaticWebAssetsConfiguration;ResolveProjectStaticWebAssets;CollectStaticWebAssetInputsDesignTime;CollectStaticWebAssetOutputsDesignTime"); - - - - msbuild.ExecuteWithoutRestore("/p:DesignTimeBuild=true", "/p:BuildingInsideVisualStudio=true", "/bl:design.binlog").Should().Pass(); - - - - // Check the contents of the input and output files - - var inputFilePath = Path.Combine(build.GetIntermediateDirectory().FullName, "StaticWebAssetsUTDCInput.txt"); - - new FileInfo(inputFilePath).Should().Exist(); - - var inputFiles = File.ReadAllLines(inputFilePath); - - inputFiles.Should().HaveCount(1); - - inputFiles.Should().Contain(Path.Combine(ProjectDirectory.Path, "ClassLibrary", "obj", "Debug", DefaultTfm, "staticwebassets.build.json")); - - - - var outputFilePath = Path.Combine(build.GetIntermediateDirectory().FullName, "StaticWebAssetsUTDCOutput.txt"); - - new FileInfo(outputFilePath).Should().Exist(); - - var outputFiles = File.ReadAllLines(outputFilePath); - - outputFiles.Should().ContainSingle(); - - Path.GetFileName(outputFiles[0]).Should().Be("staticwebassets.build.json"); - - } - - - - private static MSBuildCommand CreateMSBuildCommand(TestAsset testAsset, string relativeProjectPath, string targets) - - { - - return (MSBuildCommand)new MSBuildCommand(testAsset.Log, targets, testAsset.TestRoot, relativeProjectPath) - - .WithWorkingDirectory(testAsset.TestRoot); - - } - - - - private static TestAsset AddIntrospection(TestAsset testAsset) - - { - - return testAsset - - .WithProjectChanges((name, project) => - - { - - project.Document.Root.LastNode.AddAfterSelf( - - XElement.Parse(""" - - - - - - <_StaticWebAssetsUTDCInput Include="@(UpToDateCheckInput)" Condition="'%(UpToDateCheckInput.Set)' == 'StaticWebAssets'" /> - - <_StaticWebAssetsUTDCOutput Include="@(UpToDateCheckOutput)" Condition="'%(UpToDateCheckOutput.Set)' == 'StaticWebAssets'" /> - - - - - - - - - - - - - - """ - - )); - - }); - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsFingerprintingTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsFingerprintingTest.cs index 07160373e115..7f313f685987 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsFingerprintingTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsFingerprintingTest.cs @@ -2,802 +2,273 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - using System.Text.Json; - - using System.IO.Compression; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests; - - - - [TestClass] - public class StaticWebAssetsContentFingerprintingIntegrationTest : AspNetSdkBaselineTest - - { - - [TestMethod] - - public void Build_FingerprintsContent_WhenEnabled() - - { - - var expectedManifest = LoadBuildManifest(); - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) - - .WithProjectChanges(p => { - - var fingerprintContent = p.Descendants() - - .SingleOrDefault(e => e.Name.LocalName == "StaticWebAssetsFingerprintContent"); - - fingerprintContent.Value = "true"; - - }); - - - - Directory.CreateDirectory(Path.Combine(ProjectDirectory.Path, "wwwroot", "css")); - - File.WriteAllText(Path.Combine(ProjectDirectory.Path, "wwwroot", "css", "fingerprint-site.css"), "body { color: red; }"); - - - - 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)); - - AssertManifest(manifest, expectedManifest); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - - - var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); - - AssertManifest(manifest1, expectedManifest); - - AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); - - } - - - - public static TheoryData OverrideHtmlAssetPlaceholdersData => new TheoryData - - { - - { "VanillaWasm", "main.js", "main#[.{fingerprint}].js", true, true }, - - { "VanillaWasm", "main.js", null, false, false }, - - { "BlazorWasmMinimal", "_framework/blazor.webassembly.js", "_framework/blazor.webassembly#[.{fingerprint}].js", false, true } - - }; - - - - [TestMethod] - - [DynamicData(nameof(OverrideHtmlAssetPlaceholdersData))] - - public void Build_OverrideHtmlAssetPlaceholders(string testAsset, string scriptPath, string scriptPathWithFingerprintPattern, bool fingerprintUserJavascriptAssets, bool expectFingerprintOnScript) - - { - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset, identifier: $"{testAsset}_{fingerprintUserJavascriptAssets}_{expectFingerprintOnScript}"); - - ReplaceStringInIndexHtml(ProjectDirectory, scriptPath, scriptPathWithFingerprintPattern); - - FingerprintUserJavascriptAssets(fingerprintUserJavascriptAssets); - - - - var build = CreateBuildCommand(ProjectDirectory); - - ExecuteCommand(build, "-p:OverrideHtmlAssetPlaceholders=true", $"-p:FingerprintUserJavascriptAssets={fingerprintUserJavascriptAssets.ToString().ToLower()}").Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var indexHtmlPath = Directory.EnumerateFiles(Path.Combine(intermediateOutputPath, "staticwebassets", "htmlassetplaceholders", "build"), "*.html").Single(); - - var endpointsManifestPath = Path.Combine(intermediateOutputPath, $"staticwebassets.build.endpoints.json"); - - - - AssertImportMapInHtml(indexHtmlPath, endpointsManifestPath, scriptPath, expectFingerprintOnScript: expectFingerprintOnScript, expectPreloadElement: testAsset == "VanillaWasm"); - - } - - - - [TestMethod] - - public void Build_OverrideHtmlAssetPlaceholders_PreservesAdditionalEndpointDefinitions() - - { - - ProjectDirectory = CreateAspNetSdkTestAsset("VanillaWasm", identifier: nameof(Build_OverrideHtmlAssetPlaceholders_PreservesAdditionalEndpointDefinitions)); - - EnableDefaultDocumentAndSpaFallback(); - - ReplaceStringInIndexHtml(ProjectDirectory, "main.js", "main#[.{fingerprint}].js"); - - FingerprintUserJavascriptAssets(true); - - - - var build = CreateBuildCommand(ProjectDirectory); - - ExecuteCommand(build, "-p:OverrideHtmlAssetPlaceholders=true", "-p:FingerprintUserJavascriptAssets=true").Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var endpointsManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.endpoints.json"); - - AssertAdditionalEndpointDefinitionsExist(endpointsManifestPath); - - } - - - - [TestMethod] - - [DynamicData(nameof(OverrideHtmlAssetPlaceholdersData))] - - public void Publish_OverrideHtmlAssetPlaceholders(string testAsset, string scriptPath, string scriptPathWithFingerprintPattern, bool fingerprintUserJavascriptAssets, bool expectFingerprintOnScript) - - { - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset, identifier: $"{testAsset}_{fingerprintUserJavascriptAssets}_{expectFingerprintOnScript}"); - - ReplaceStringInIndexHtml(ProjectDirectory, scriptPath, scriptPathWithFingerprintPattern); - - FingerprintUserJavascriptAssets(fingerprintUserJavascriptAssets); - - - - var projectName = Path.GetFileNameWithoutExtension(Directory.EnumerateFiles(ProjectDirectory.TestRoot, "*.csproj").Single()); - - - - var publish = CreatePublishCommand(ProjectDirectory); - - ExecuteCommand(publish, "-p:OverrideHtmlAssetPlaceholders=true", $"-p:FingerprintUserJavascriptAssets={fingerprintUserJavascriptAssets.ToString().ToLower()}").Should().Pass(); - - - - var outputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - var indexHtmlOutputPath = Path.Combine(outputPath, "wwwroot", "index.html"); - - var endpointsManifestPath = Path.Combine(outputPath, $"{projectName}.staticwebassets.endpoints.json"); - - - - AssertImportMapInHtml(indexHtmlOutputPath, endpointsManifestPath, scriptPath, expectFingerprintOnScript: expectFingerprintOnScript, expectPreloadElement: testAsset == "VanillaWasm", assertHtmlCompressed: true); - - } - - - - [TestMethod] - - public void Publish_OverrideHtmlAssetPlaceholders_PreservesAdditionalEndpointDefinitions() - - { - - ProjectDirectory = CreateAspNetSdkTestAsset("VanillaWasm", identifier: nameof(Publish_OverrideHtmlAssetPlaceholders_PreservesAdditionalEndpointDefinitions)); - - EnableDefaultDocumentAndSpaFallback(); - - ReplaceStringInIndexHtml(ProjectDirectory, "main.js", "main#[.{fingerprint}].js"); - - FingerprintUserJavascriptAssets(true); - - - - var projectName = Path.GetFileNameWithoutExtension(Directory.EnumerateFiles(ProjectDirectory.TestRoot, "*.csproj").Single()); - - var publish = CreatePublishCommand(ProjectDirectory); - - ExecuteCommand(publish, "-p:OverrideHtmlAssetPlaceholders=true", "-p:FingerprintUserJavascriptAssets=true").Should().Pass(); - - - - var outputPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - var endpointsManifestPath = Path.Combine(outputPath, $"{projectName}.staticwebassets.endpoints.json"); - - AssertAdditionalEndpointDefinitionsExist(endpointsManifestPath); - - } - - - - private void EnableDefaultDocumentAndSpaFallback() - - { - - ProjectDirectory.WithProjectChanges(p => - - { - - if (p.Root != null) - - { - - p.Root.AddFirst( - - new XElement("PropertyGroup", - - new XElement("StaticWebAssetDefaultDocumentEnabled", "true"), - - new XElement("StaticWebAssetSpaFallbackEnabled", "true"))); - - } - - }); - - } - - - - private static void AssertAdditionalEndpointDefinitionsExist(string endpointsManifestPath) - - { - - var endpoints = JsonSerializer.Deserialize(File.ReadAllText(endpointsManifestPath)); - - endpoints.Should().NotBeNull(); - - - - var indexEndpoint = endpoints.Endpoints.Single(e => e.Route == "index.html" && e.Selectors.Length == 0); - - var defaultDocumentEndpoint = endpoints.Endpoints.Single(e => e.Route == "/" && e.Selectors.Length == 0); - - var spaFallbackEndpoint = endpoints.Endpoints.Single(e => e.Route == "{**fallback:nonfile}" && e.Selectors.Length == 0); - - - - defaultDocumentEndpoint.AssetFile.Should().Be(indexEndpoint.AssetFile); - - spaFallbackEndpoint.AssetFile.Should().Be(indexEndpoint.AssetFile); - - spaFallbackEndpoint.Order.Should().Be("2147483647"); - - } - - - - private void FingerprintUserJavascriptAssets(bool fingerprintUserJavascriptAssets) - - { - - if (fingerprintUserJavascriptAssets) - - { - - ProjectDirectory.WithProjectChanges(p => - - { - - if (p.Root != null) - - { - - var itemGroup = new XElement("ItemGroup"); - - var pattern = new XElement("StaticWebAssetFingerprintPattern"); - - pattern.SetAttributeValue("Include", "Js"); - - pattern.SetAttributeValue("Pattern", "*.js"); - - pattern.SetAttributeValue("Expression", "#[.{fingerprint}]!"); - - itemGroup.Add(pattern); - - p.Root.Add(itemGroup); - - } - - }); - - } - - } - - - - private void ReplaceStringInIndexHtml(TestAsset testAsset, string sourceValue, string targetValue) - - { - - if (targetValue != null) - - { - - var indexHtmlPath = Path.Combine(testAsset.TestRoot, "wwwroot", "index.html"); - - var indexHtmlContent = File.ReadAllText(indexHtmlPath); - - var newIndexHtmlContent = indexHtmlContent.Replace(sourceValue, targetValue); - - if (indexHtmlContent == newIndexHtmlContent) - - throw new Exception($"String replacement '{sourceValue}' for '{targetValue}' didn't produce any change in '{indexHtmlPath}'"); - - - - File.WriteAllText(indexHtmlPath, newIndexHtmlContent); - - } - - } - - - - private void AssertImportMapInHtml(string indexHtmlPath, string endpointsManifestPath, string scriptPath, bool expectFingerprintOnScript = true, bool expectPreloadElement = false, bool assertHtmlCompressed = false) - - { - - - - var endpoints = JsonSerializer.Deserialize(File.ReadAllText(endpointsManifestPath)); - - var fingerprintedScriptPath = GetFingerprintedPath(scriptPath); - - - - var indexHtmlContent = File.ReadAllText(indexHtmlPath); - - AssertHtmlContent(indexHtmlContent); - - - - if (assertHtmlCompressed) - - { - - var indexHtmlGzipContent = DecompressGzipFile(indexHtmlPath + ".gz"); - - AssertHtmlContent(indexHtmlGzipContent); - - - - var indexHtmlBrotliContent = DecompressBrotliFile(indexHtmlPath + ".br"); - - AssertHtmlContent(indexHtmlBrotliContent); - - } - - - - void AssertHtmlContent(string content) - - { - - if (expectFingerprintOnScript) - - { - - Assert.DoesNotContain($"src=\"{scriptPath}\"", content); - - Assert.Contains($"src=\"{fingerprintedScriptPath}\"", content); - - } - - else - - { - - Assert.Contains(scriptPath, content); - - - - if (scriptPath != fingerprintedScriptPath) - - { - - Assert.DoesNotContain(fingerprintedScriptPath, content); - - } - - } - - - - Assert.Contains(GetFingerprintedPath("_framework/dotnet.js"), content); - - Assert.Contains(GetFingerprintedPath("_framework/dotnet.native.js"), content); - - Assert.Contains(GetFingerprintedPath("_framework/dotnet.runtime.js"), content); - - - - if (expectPreloadElement) - - { - - Assert.DoesNotContain(" endpoints.Endpoints.FirstOrDefault(e => e.Route == route && e.Selectors.Length == 0)?.AssetFile ?? throw new Exception($"Missing endpoint for file '{route}' in '{endpointsManifestPath}'"); - - - - string DecompressGzipFile(string path) - - { - - if (File.Exists(path)) - - { - - using var fileStream = File.OpenRead(path); - - using var compressedStream = new GZipStream(fileStream, CompressionMode.Decompress); - - using var reader = new StreamReader(compressedStream); - - return reader.ReadToEnd(); - - } - - - - Assert.Fail($"File '{path}' does not exist."); - - return null; - - } - - - - string DecompressBrotliFile(string path) - - { - - if (File.Exists(path)) - - { - - using var fileStream = File.OpenRead(path); - - using var compressedStream = new BrotliStream(fileStream, CompressionMode.Decompress); - - using var reader = new StreamReader(compressedStream); - - return reader.ReadToEnd(); - - } - - - - Assert.Fail($"File '{path}' does not exist."); - - return null; - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs index 0d51d1fd69c5..2a1549f8a0c0 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsIntegrationTest.cs @@ -2,2926 +2,982 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - using System.Reflection; - - using Microsoft.AspNetCore.StaticWebAssets.Tasks; - - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class StaticWebAssetsIntegrationTest : AspNetSdkBaselineTest - - { - - - - // Build Standalone project - - [TestMethod] - - public void Build_GeneratesJsonManifestAndCopiesItToOutputFolder() - - { - - var expectedManifest = LoadBuildManifest(); - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - 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)); - - AssertManifest(manifest, expectedManifest); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - - - var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); - - AssertManifest(manifest1, expectedManifest); - - AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); - - } - - - - [TestMethod] - - public void Build_Can_DisableAssetCaching() - - { - - var expectedManifest = LoadBuildManifest(); - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(ProjectDirectory); - - ExecuteCommand(build, "/p:StaticWebAssetsCacheDefineStaticWebAssetsEnabled=false").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)); - - AssertManifest(manifest, expectedManifest); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - - - // The caches shouldn't exist. - - // Manifest - - new FileInfo(Path.Combine(intermediateOutputPath, "rpswa.dswa.cache.json")).Should().NotExist(); - - // Compressed assets - - new FileInfo(Path.Combine(intermediateOutputPath, "rbcswa.dswa.cache.json")).Should().NotExist(); - - // Initializers - - new FileInfo(Path.Combine(intermediateOutputPath, "rjimswa.dswa.cache.json")).Should().NotExist(); - - // JS Modules - - new FileInfo(Path.Combine(intermediateOutputPath, "rjsmcshtml.dswa.cache.json")).Should().NotExist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "rjsmrazor.dswa.cache.json")).Should().NotExist(); - - - - var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); - - AssertManifest(manifest1, expectedManifest); - - AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); - - } - - - - [TestMethod] - - public void Build_DoesNotUpdateManifest_WhenHasNotChanged() - - { - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - 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"); - - var originalObjFile = new FileInfo(path); - - originalObjFile.Should().Exist(); - - var objManifestContents = File.ReadAllText(Path.Combine(intermediateOutputPath, "staticwebassets.build.json")); - - AssertManifest( - - StaticWebAssetsManifest.FromJsonString(objManifestContents), - - LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); - - var originalFile = new FileInfo(finalPath); - - originalFile.Should().Exist(); - - var binManifestContents = File.ReadAllText(finalPath); - - - - var secondBuild = CreateBuildCommand(ProjectDirectory); - - secondBuild.Execute().Should().Pass(); - - - - var secondPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - var secondObjFile = new FileInfo(secondPath); - - secondObjFile.Should().Exist(); - - var secondObjManifest = File.ReadAllText(secondPath); - - secondObjManifest.Should().Be(objManifestContents); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var secondFinalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); - - var secondFinalFile = new FileInfo(secondFinalPath); - - secondFinalFile.Should().Exist(); - - var secondBinManifest = File.ReadAllText(secondFinalPath); - - secondBinManifest.Should().Be(binManifestContents); - - - - secondFinalFile.LastWriteTimeUtc.Should().Be(originalFile.LastWriteTimeUtc); - - } - - - - [TestMethod] - - public void Build_UpdatesManifest_WhenFilesChange() - - { - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - 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"); - - var originalObjFile = new FileInfo(path); - - originalObjFile.Should().Exist(); - - var objManifestContents = File.ReadAllText(Path.Combine(intermediateOutputPath, "staticwebassets.build.json")); - - var firstManifest = StaticWebAssetsManifest.FromJsonString(objManifestContents); - - AssertManifest(firstManifest, LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); - - var originalFile = new FileInfo(finalPath); - - originalFile.Should().Exist(); - - var binManifestContents = File.ReadAllText(finalPath); - - - - AssertBuildAssets( - - firstManifest, - - outputPath, - - intermediateOutputPath); - - - - // Second build - - Directory.CreateDirectory(Path.Combine(ProjectDirectory.Path, "wwwroot")); - - File.WriteAllText(Path.Combine(ProjectDirectory.Path, "wwwroot", "index.html"), "some html"); - - - - var secondBuild = CreateBuildCommand(ProjectDirectory); - - secondBuild.Execute().Should().Pass(); - - - - var secondPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - var secondObjFile = new FileInfo(secondPath); - - secondObjFile.Should().Exist(); - - var secondObjManifest = File.ReadAllText(secondPath); - - var secondManifest = StaticWebAssetsManifest.FromJsonString(secondObjManifest); - - AssertManifest( - - secondManifest, - - LoadBuildManifest("Updated"), - - "Updated"); - - - - secondObjManifest.Should().NotBe(objManifestContents); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var secondFinalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); - - var secondFinalFile = new FileInfo(secondFinalPath); - - secondFinalFile.Should().Exist(); - - var secondBinManifest = File.ReadAllText(secondFinalPath); - - secondBinManifest.Should().NotBe(binManifestContents); - - - - secondObjFile.LastWriteTimeUtc.Should().NotBe(originalObjFile.LastWriteTimeUtc); - - secondFinalFile.LastWriteTimeUtc.Should().NotBe(originalFile.LastWriteTimeUtc); - - - - AssertBuildAssets( - - secondManifest, - - outputPath, - - intermediateOutputPath, - - "Updated"); - - } - - - - // Rebuild - - [TestMethod] - - public void Rebuild_RegeneratesJsonManifestAndCopiesItToOutputFolder() - - { - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - 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"); - - var originalObjFile = new FileInfo(path); - - originalObjFile.Should().Exist(); - - var objManifestContents = File.ReadAllText(Path.Combine(intermediateOutputPath, "staticwebassets.build.json")); - - AssertManifest(StaticWebAssetsManifest.FromJsonString(objManifestContents), LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); - - var originalFile = new FileInfo(finalPath); - - originalFile.Should().Exist(); - - var binManifestContents = File.ReadAllText(finalPath); - - - - // rebuild build - - var rebuild = CreateRebuildCommand(ProjectDirectory); - - ExecuteCommand(rebuild).Should().Pass(); - - - - var secondPath = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - var secondObjFile = new FileInfo(secondPath); - - secondObjFile.Should().Exist(); - - var secondObjManifestContents = File.ReadAllText(secondPath); - - var secondManifest = StaticWebAssetsManifest.FromJsonString(secondObjManifestContents); - - AssertManifest( - - secondManifest, - - LoadBuildManifest("Rebuild"), - - "Rebuild"); - - - - // This is no longer true because the manifests include the timestamp for the last modified - - // time of the file, etc. - - //secondObjManifestContents.Should().Be(objManifestContents); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var secondFinalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); - - var secondFinalFile = new FileInfo(secondFinalPath); - - secondFinalFile.Should().Exist(); - - var secondBinManifest = File.ReadAllText(secondFinalPath); - - secondBinManifest.Should().Be(binManifestContents); - - - - secondObjFile.LastWriteTimeUtc.Should().NotBe(originalObjFile.LastWriteTimeUtc); - - secondFinalFile.LastWriteTimeUtc.Should().NotBe(originalFile.LastWriteTimeUtc); - - - - AssertBuildAssets( - - secondManifest, - - outputPath, - - intermediateOutputPath, - - "Rebuild"); - - } - - - - // Publish - - [TestMethod] - - public void Publish_GeneratesPublishJsonManifestAndCopiesPublishAssets() - - { - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var publish = CreatePublishCommand(ProjectDirectory); - - ExecuteCommand(publish).Should().Pass(); - - - - var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var publishPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - // GenerateStaticWebAssetsManifest should generate the build manifest file. - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - new FileInfo(path).Should().Exist(); - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - AssertManifest(manifest, LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(publishPath, "ComponentApp.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().NotExist(); - - - - // GenerateStaticWebAssetsManifest should generate the publish manifest file. - - var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - new FileInfo(path).Should().Exist(); - - var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); - - AssertManifest(publishManifest, LoadPublishManifest()); - - - - AssertPublishAssets( - - publishManifest, - - publishPath, - - intermediateOutputPath); - - } - - - - [TestMethod] - - public void Publish_PublishSingleFile_GeneratesPublishJsonManifestAndCopiesPublishAssets() - - { - - var expectedManifest = LoadBuildManifest(); - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var publish = CreatePublishCommand(ProjectDirectory); - - ExecuteCommand(publish, "/p:PublishSingleFile=true", $"/p:RuntimeIdentifier={RuntimeInformation.RuntimeIdentifier}").Should().Pass(); - - - - var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug", RuntimeInformation.RuntimeIdentifier).ToString(); - - var publishPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - // GenerateStaticWebAssetsManifest should generate the build manifest file. - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - new FileInfo(path).Should().Exist(); - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - AssertManifest(manifest, expectedManifest, runtimeIdentifier: RuntimeInformation.RuntimeIdentifier); - - - - // GenerateStaticWebAssetsManifest should not copy the file to the output folder. - - var finalPath = Path.Combine(publishPath, "ComponentApp.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().NotExist(); - - - - // GenerateStaticWebAssetsManifest should generate the publish manifest file. - - var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - new FileInfo(path).Should().Exist(); - - var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); - - AssertManifest( - - publishManifest, - - LoadPublishManifest(), - - runtimeIdentifier: RuntimeInformation.RuntimeIdentifier); - - - - AssertPublishAssets( - - publishManifest, - - publishPath, - - intermediateOutputPath); - - } - - - - [TestMethod] - - public void Publish_NoBuild_GeneratesPublishJsonManifestAndCopiesPublishAssets() - - { - - var expectedManifest = LoadBuildManifest(); - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(ProjectDirectory); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var publishPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - // GenerateStaticWebAssetsManifest should generate the manifest file. - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - var objManifestFile = new FileInfo(path); - - objManifestFile.Should().Exist(); - - var objManifestFileTimeStamp = objManifestFile.LastWriteTimeUtc; - - - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - AssertManifest(manifest, expectedManifest); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(publishPath, "ComponentApp.staticwebassets.runtime.json"); - - var binManifestFile = new FileInfo(finalPath); - - binManifestFile.Should().Exist(); - - var binManifestTimeStamp = binManifestFile.LastWriteTimeUtc; - - - - var finalManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); - - AssertManifest(finalManifest, expectedManifest); - - - - // Publish no build - - - - var publish = CreatePublishCommand(ProjectDirectory); - - ExecuteCommand(publish, "/p:NoBuild=true").Should().Pass(); - - - - var secondObjTimeStamp = new FileInfo(path).LastWriteTimeUtc; - - - - secondObjTimeStamp.Should().Be(objManifestFileTimeStamp); - - - - var seconbObjManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - AssertManifest(seconbObjManifest, expectedManifest); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var seconBinManifestPath = Path.Combine(publishPath, "ComponentApp.staticwebassets.runtime.json"); - - var secondBinManifestFile = new FileInfo(seconBinManifestPath); - - secondBinManifestFile.Should().Exist(); - - - - secondBinManifestFile.LastWriteTimeUtc.Should().Be(binManifestTimeStamp); - - - - var secondBinManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); - - AssertManifest(secondBinManifest, expectedManifest); - - - - // GenerateStaticWebAssetsManifest should generate the publish manifest file. - - var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - new FileInfo(path).Should().Exist(); - - var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); - - AssertManifest( - - publishManifest, - - LoadPublishManifest()); - - - - AssertPublishAssets( - - publishManifest, - - publishPath, - - intermediateOutputPath); - - } - - - - [TestMethod] - - public void Build_DeployOnBuild_GeneratesPublishJsonManifestAndCopiesPublishAssets() - - { - - var expectedManifest = LoadBuildManifest(); - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var build = CreateBuildCommand(ProjectDirectory); - - build.Execute("/p:DeployOnBuild=true").Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - // GenerateStaticWebAssetsManifest should generate the build manifest file. - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - new FileInfo(path).Should().Exist(); - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - AssertManifest(manifest, LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - - - // GenerateStaticWebAssetsManifest should generate the publish manifest file. - - var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - new FileInfo(path).Should().Exist(); - - var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); - - AssertManifest(publishManifest, LoadPublishManifest()); - - - - AssertPublishAssets( - - publishManifest, - - Path.Combine(outputPath, "publish"), - - intermediateOutputPath); - - } - - - - // Clean - - [TestMethod] - - public void Clean_RemovesManifestFrom_BuildAndIntermediateOutput() - - { - - var expectedManifest = LoadBuildManifest(); - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - 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)); - - AssertManifest(manifest, expectedManifest); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - var finalManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); - - AssertManifest(finalManifest, expectedManifest); - - - - var clean = new CleanCommand(Log, ProjectDirectory.Path); - - clean.Execute().Should().Pass(); - - - - // Obj folder manifest does not exist - - new FileInfo(path).Should().NotExist(); - - - - // Bin folder manifest does not exist - - new FileInfo(finalPath).Should().NotExist(); - - } - - - - [TestMethod] - - public void Publish_WithExternalProjectReference_UpdatesAssets() - - { - - var testAsset = "RazorAppWithP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) - - .WithProjectChanges((name, project) => - - { - - if (Path.GetFileName(name).Equals("ClassLibrary.csproj", StringComparison.Ordinal)) - - { - - var sdkAttribute = project.Root.Attribute("Sdk"); - - if (sdkAttribute == null) - - { - - sdkAttribute = new XAttribute("Sdk", "Microsoft.NET.Sdk"); - - project.Root.Add(sdkAttribute); - - } - - else - - { - - sdkAttribute.Value = "Microsoft.NET.Sdk"; - - } - - project.Root.AddFirst(new XElement("Import", new XAttribute("Project", "ExternalStaticAssets.targets"))); - - - - using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Microsoft.NET.Sdk.StaticWebAssets.Tests.content.ExternalStaticAssets.targets"); - - using var destination = File.OpenWrite(Path.Combine(Path.GetDirectoryName(name), "ExternalStaticAssets.targets")); - - stream.CopyTo(destination); - - } - - }); - - - - var publish = CreatePublishCommand(ProjectDirectory, "AppWithP2PReference"); - - ExecuteCommand(publish).Should().Pass(); - - - - var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var publishPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - // GenerateStaticWebAssetsManifest should generate the build manifest file. - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - new FileInfo(path).Should().Exist(); - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - AssertManifest(manifest, LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(publishPath, "ComponentApp.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().NotExist(); - - - - // GenerateStaticWebAssetsManifest should generate the publish manifest file. - - var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - new FileInfo(path).Should().Exist(); - - var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); - - AssertManifest(publishManifest, LoadPublishManifest()); - - - - AssertPublishAssets( - - publishManifest, - - publishPath, - - intermediateOutputPath); - - } - - - - [TestMethod] - - public void Build_WithExternalProjectReference_UpdatesAssets() - - { - - var testAsset = "RazorAppWithP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) - - .WithProjectChanges((name, project) => - - { - - if (Path.GetFileName(name).Equals("ClassLibrary.csproj", StringComparison.Ordinal)) - - { - - var sdkAttribute = project.Root.Attribute("Sdk"); - - if (sdkAttribute == null) - - { - - sdkAttribute = new XAttribute("Sdk", "Microsoft.NET.Sdk"); - - project.Root.Add(sdkAttribute); - - } - - else - - { - - sdkAttribute.Value = "Microsoft.NET.Sdk"; - - } - - project.Root.AddFirst(new XElement("Import", new XAttribute("Project", "ExternalStaticAssets.targets"))); - - - - using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Microsoft.NET.Sdk.StaticWebAssets.Tests.content.ExternalStaticAssets.targets"); - - using var destination = File.OpenWrite(Path.Combine(Path.GetDirectoryName(name), "ExternalStaticAssets.targets")); - - stream.CopyTo(destination); - - } - - - - if (Path.GetFileName(name).Equals("AppWithP2PReference.csproj", StringComparison.Ordinal)) - - { - - project.Root.AddFirst(new XElement("ItemGroup", - - new XElement( - - "StaticWebAssetFingerprintInferenceExpression", - - new XAttribute("Include", "Version"), - - new XAttribute("Pattern", ".*(?v\\d{1})\\.js$")))); - - } - - }); - - - - var build = CreateBuildCommand(ProjectDirectory, "AppWithP2PReference"); - - ExecuteCommand(build).Should().Pass(); - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var buildPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - // GenerateStaticWebAssetsManifest should generate the build manifest file. - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - new FileInfo(path).Should().Exist(); - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - AssertManifest(manifest, LoadBuildManifest()); - - - - AssertBuildAssets( - - manifest, - - buildPath, - - intermediateOutputPath); - - } - - - - [TestMethod] - - public void Build_DoesNotFailToCompress_TwoAssetsWith_TheSameContent() - - { - - var expectedManifest = LoadBuildManifest(); - - var testAsset = "RazorComponentApp"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset) - - .WithProjectChanges(document => - - { - - document.Root.AddFirst(new XElement("ItemGroup", - - new XElement("Content", - - new XAttribute("Update", "wwwroot\\file.build.txt"), - - new XAttribute("TargetPath", "wwwroot\\file.txt"), - - new XAttribute("CopyToPublishDirectory", "Never")), - - new XElement("Content", - - new XAttribute("Update", "wwwroot\\file.publish.txt"), - - new XAttribute("TargetPath", "wwwroot\\file.txt"), - - new XAttribute("CopyToOutputDirectory", "Never")))); - - }); - - - - Directory.CreateDirectory(Path.Combine(ProjectDirectory.Path, "wwwroot")); - - File.WriteAllText(Path.Combine(ProjectDirectory.Path, "wwwroot", "file.build.txt"), "file1"); - - File.WriteAllText(Path.Combine(ProjectDirectory.Path, "wwwroot", "file.publish.txt"), "file1"); - - - - 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)); - - AssertManifest(manifest, expectedManifest); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "ComponentApp.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - - - var manifest1 = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); - - AssertManifest(manifest1, expectedManifest); - - AssertBuildAssets(manifest1, outputPath, intermediateOutputPath); - - } - - } - [TestClass] - - - - public class StaticWebAssetsAppWithPackagesIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest - { - protected override string RestoreNugetPackagePath => nameof(StaticWebAssetsAppWithPackagesIntegrationTest); - - [TestMethod] - - public void Build_Fails_WhenConflictingAssetsFoundBetweenAStaticWebAssetAndAFileInTheWebRootFolder() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - Directory.CreateDirectory(Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "wwwroot", "_content", "ClassLibrary", "js")); - - File.WriteAllText(Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "wwwroot", "_content", "ClassLibrary", "js", "project-transitive-dep.js"), "console.log('transitive-dep');"); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); + var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + ExecuteCommand(build).Should().Fail(); + } + [TestMethod] + public void BuildProjectWithReferences_DeployOnBuild_GeneratesPublishJsonManifestAndCopiesPublishAssets() + { + var testAsset = "RazorAppWithPackageAndP2PReference"; + ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); + EnsureLocalPackagesExists(); + var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + ExecuteCommand(restore).Should().Pass(); var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); + build.Execute("/p:DeployOnBuild=true").Should().Pass(); - - ExecuteCommand(build).Should().Fail(); - - - } - - - - - - [TestMethod] - - - public void BuildProjectWithReferences_DeployOnBuild_GeneratesPublishJsonManifestAndCopiesPublishAssets() - - - { - - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - - - EnsureLocalPackagesExists(); - - - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - - ExecuteCommand(restore).Should().Pass(); - - - - - - var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - - build.Execute("/p:DeployOnBuild=true").Should().Pass(); - - - - - - var intermediateOutputPath = build.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - - var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - + 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(); - - - - AssertManifest( - - StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)), - - LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - - - // GenerateStaticWebAssetsManifest should generate the publish manifest file. - - var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - new FileInfo(path).Should().Exist(); - - var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); - - AssertManifest(publishManifest, LoadPublishManifest()); - - - - AssertPublishAssets( - - publishManifest, - - Path.Combine(outputPath, "publish"), - - intermediateOutputPath); - - } - - - - [TestMethod] - - public void BuildProjectWithReferences_GeneratesJsonManifestAndCopiesItToOutputFolder() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - 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)); - - AssertManifest( - - manifest, - - LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - - - AssertBuildAssets( - - manifest, - - outputPath, - - intermediateOutputPath); - - } - - - - [TestMethod] - - public void BuildProjectWithReferences_NoDependencies_GeneratesJsonManifestAndCopiesItToOutputFolder() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - 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(); - - AssertManifest( - - StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)), - - LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().Exist(); - - var manifestContents = File.ReadAllText(finalPath); - - var initialManifest = StaticWebAssetsManifest.FromJsonString(File.ReadAllText(path)); - - AssertManifest( - - initialManifest, - - LoadBuildManifest()); - - - - // Second build - - var secondBuild = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(secondBuild,"/p:BuildProjectReferences=false").Should().Pass(); - - - - // GenerateStaticWebAssetsManifest should generate the manifest file. - - new FileInfo(path).Should().Exist(); - - var manifestNoDeps = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - AssertManifest( - - manifestNoDeps, - - LoadBuildManifest("NoDependencies"), - - "NoDependencies"); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - new FileInfo(finalPath).Should().Exist(); - - var manifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(Path.Combine(intermediateOutputPath, "staticwebassets.build.json"))); - - AssertManifest( - - manifest, - - LoadBuildManifest("NoDependencies"), - - "NoDependencies"); - - - - AssertBuildAssets( - - manifest, - - outputPath, - - intermediateOutputPath, - - "NoDependencies"); - - - - // Check that the two manifests are the same - - manifestContents.Should().Be(File.ReadAllText(finalPath)); - - } - - - - [TestMethod] - - public void PublishProjectWithReferences_GeneratesPublishJsonManifestAndCopiesPublishAssets() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - var publish = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(publish).Should().Pass(); - - - - var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var publishPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - // GenerateStaticWebAssetsManifest should generate the manifest file. - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - new FileInfo(path).Should().Exist(); - - AssertManifest( - - StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)), - - LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(publishPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().NotExist(); - - - - // GenerateStaticWebAssetsPublishManifest should generate the publish manifest file. - - var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - new FileInfo(path).Should().Exist(); - - var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); - - AssertManifest( - - publishManifest, - - LoadPublishManifest()); - - - - AssertPublishAssets( - - publishManifest, - - publishPath, - - intermediateOutputPath); - - } - - - - [TestMethod] - - public void PublishProjectWithReferences_PublishSingleFile_GeneratesPublishJsonManifestAndCopiesPublishAssets() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - var publish = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(publish, "/p:PublishSingleFile=true", $"/p:RuntimeIdentifier={RuntimeInformation.RuntimeIdentifier}").Should().Pass(); - - - - var intermediateOutputPath = publish.GetIntermediateDirectory(DefaultTfm, "Debug", RuntimeInformation.RuntimeIdentifier).ToString(); - - var publishPath = publish.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - // GenerateStaticWebAssetsManifest should generate the manifest file. - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - new FileInfo(path).Should().Exist(); - - AssertManifest( - - StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)), - - LoadBuildManifest(), - - runtimeIdentifier: RuntimeInformation.RuntimeIdentifier); - - - - // GenerateStaticWebAssetsManifest should not copy the file to the output folder. - - var finalPath = Path.Combine(publishPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().NotExist(); - - - - // GenerateStaticWebAssetsPublishManifest should generate the publish manifest file. - - var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - new FileInfo(path).Should().Exist(); - - var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); - - AssertManifest(publishManifest, LoadPublishManifest(), runtimeIdentifier: RuntimeInformation.RuntimeIdentifier); - - - - AssertPublishAssets( - - publishManifest, - - publishPath, - - intermediateOutputPath); - - } - - - - [TestMethod] - - public void PublishProjectWithReferences_NoBuild_GeneratesPublishJsonManifestAndCopiesPublishAssets() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - var build = CreateBuildCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - 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"); - - var objManifestFile = new FileInfo(path); - - objManifestFile.Should().Exist(); - - var objManifestFileTimeStamp = objManifestFile.LastWriteTimeUtc; - - - - AssertManifest( - - StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)), - - LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); - - var binManifestFile = new FileInfo(finalPath); - - binManifestFile.Should().Exist(); - - var binManifestTimeStamp = binManifestFile.LastWriteTimeUtc; - - - - AssertManifest( - - StaticWebAssetsManifest.FromJsonString(File.ReadAllText(path)), - - LoadBuildManifest()); - - - - // Publish no build - - var publish = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - var publishResult = ExecuteCommand(publish, "/p:NoBuild=true", "/p:ErrorOnDuplicatePublishOutputFiles=false"); - - var publishPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - publishResult.Should().Pass(); - - - - new FileInfo(path).LastWriteTimeUtc.Should().Be(objManifestFileTimeStamp); - - - - var seconbObjManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)); - - AssertManifest(seconbObjManifest, LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var seconBinManifestPath = Path.Combine(outputPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); - - var secondBinManifestFile = new FileInfo(seconBinManifestPath); - - secondBinManifestFile.Should().Exist(); - - - - secondBinManifestFile.LastWriteTimeUtc.Should().Be(binManifestTimeStamp); - - - - // GenerateStaticWebAssetsManifest should generate the publish manifest file. - - var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - new FileInfo(path).Should().Exist(); - - var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); - - AssertManifest(publishManifest, LoadPublishManifest()); - - - - AssertPublishAssets( - - publishManifest, - - publishPath, - - intermediateOutputPath); - - } - - - - [TestMethod] - - public void PublishProjectWithReferences_AppendTargetFrameworkToOutputPathFalse_GeneratesPublishJsonManifestAndCopiesPublishAssets() - - { - - var testAsset = "RazorAppWithPackageAndP2PReference"; - - ProjectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - EnsureLocalPackagesExists(); - - - - var restore = CreateRestoreCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(restore).Should().Pass(); - - - - var publish = CreatePublishCommand(ProjectDirectory, "AppWithPackageAndP2PReference"); - - ExecuteCommand(publish, "/p:AppendTargetFrameworkToOutputPath=false").Should().Pass(); - - - - // Hard code output paths here to account for AppendTargetFrameworkToOutputPath=false - - var intermediateOutputPath = Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "obj", "Debug"); - - var publishPath = Path.Combine(ProjectDirectory.Path, "AppWithPackageAndP2PReference", "bin", "Debug", "publish"); - - - - // GenerateStaticWebAssetsManifest should generate the manifest file. - - var path = Path.Combine(intermediateOutputPath, "staticwebassets.build.json"); - - new FileInfo(path).Should().Exist(); - - AssertManifest( - - StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(path)), - - LoadBuildManifest()); - - - - // GenerateStaticWebAssetsManifest should copy the file to the output folder. - - var finalPath = Path.Combine(publishPath, "AppWithPackageAndP2PReference.staticwebassets.runtime.json"); - - new FileInfo(finalPath).Should().NotExist(); - - - - // GenerateStaticWebAssetsPublishManifest should generate the publish manifest file. - - var intermediatePublishManifestPath = Path.Combine(intermediateOutputPath, "staticwebassets.publish.json"); - - new FileInfo(path).Should().Exist(); - - var publishManifest = StaticWebAssetsManifest.FromJsonBytes(File.ReadAllBytes(intermediatePublishManifestPath)); - - AssertManifest( - - publishManifest, - - LoadPublishManifest()); - - - - AssertPublishAssets( - - publishManifest, - - publishPath, - - intermediateOutputPath); - - } - - } - - } - - diff --git a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsPackIntegrationTest.cs b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsPackIntegrationTest.cs index 0d8ca43390d8..368efd4987f7 100644 --- a/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsPackIntegrationTest.cs +++ b/test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssetsPackIntegrationTest.cs @@ -2,4061 +2,1360 @@ // The .NET Foundation licenses this file to you under the MIT license. #nullable disable + using Microsoft.NET.TestFramework; using Microsoft.NET.TestFramework.Commands; using Microsoft.NET.TestFramework.Assertions; using Microsoft.NET.TestFramework.Utilities; - - using Microsoft.VisualStudio.TestTools.UnitTesting; - - - namespace Microsoft.NET.Sdk.StaticWebAssets.Tests - - { - - [TestClass] - public class StaticWebAssetsPackIntegrationTest : IsolatedNuGetPackageFolderAspNetSdkBaselineTest - { - protected override string RestoreNugetPackagePath => nameof(StaticWebAssetsPackIntegrationTest); - - [TestMethod] - - public void Pack_FailsWhenStaticWebAssetsHaveConflictingPaths() - - { - - var testAsset = "PackageLibraryDirectDependency"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages") - - .WithProjectChanges(project => - - { - - var ns = project.Root.Name.Namespace; - - var itemGroup = new XElement(ns + "ItemGroup"); - - var element = new XElement("StaticWebAsset", new XAttribute("Include", @"bundle\js\pkg-direct-dep.js")); - - element.Add(new XElement("SourceType")); - - element.Add(new XElement("SourceId", "PackageLibraryDirectDependency")); - - element.Add(new XElement("ContentRoot", "$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)\\bundle\\'))")); - - element.Add(new XElement("BasePath", "_content/PackageLibraryDirectDependency")); - - element.Add(new XElement("RelativePath", "js/pkg-direct-dep.js")); - - itemGroup.Add(element); - - project.Root.Add(itemGroup); - - }); - - - - Directory.CreateDirectory(Path.Combine(projectDirectory.Path, "bundle", "js")); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "bundle", "js", "pkg-direct-dep.js"), "console.log('bundle');"); - - - - var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - - ExecuteCommand(pack).Should().Fail(); - - } - - - - // If you modify this test, make sure you also modify the test below this one to assert that things are not included as content. - - [TestMethod] - - public void Pack_IncludesStaticWebAssets() - - { - - var testAsset = "PackageLibraryDirectDependency"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result.Should().NuPkgContainsPatterns( - - Path.Combine(pack.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePatterns: new[] - - { - - Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), - - Path.Combine("staticwebassets", "css", "site.css"), - - Path.Combine("staticwebassets", "PackageLibraryDirectDependency.*.bundle.scp.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), - - Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), - - Path.Combine("build", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") - - }); - - } - - - - [TestMethod] - - public void Pack_NoAssets_DoesNothing() - - { - - var testAsset = "PackageLibraryNoStaticAssets"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - var pack = CreatePackCommand(projectDirectory); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryNoStaticAssets.dll")).Should().Exist(); - - - - result.Should().NuPkgDoesNotContain( - - Path.Combine(pack.GetPackageDirectory().FullName, "PackageLibraryNoStaticAssets.1.0.0.nupkg"), - - filePaths: new[] - - { - - Path.Combine("staticwebassets"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), - - Path.Combine("build", "PackageLibraryNoStaticAssets.props"), - - Path.Combine("buildMultiTargeting", "PackageLibraryNoStaticAssets.props"), - - Path.Combine("buildTransitive", "PackageLibraryNoStaticAssets.props") - - }); - - } - - - - [TestMethod] - - public void Pack_NoAssets_Multitargeting_DoesNothing() - - { - - var testAsset = "PackageLibraryNoStaticAssets"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - projectDirectory.WithProjectChanges(project => - - { - - var tfm = project.Root.Descendants("TargetFramework").Single(); - - tfm.Name = "TargetFrameworks"; - - tfm.Value = "net6.0;" + DefaultTfm; - - }); - - - - var pack = CreatePackCommand(projectDirectory); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryNoStaticAssets.dll")).Should().Exist(); - - - - result.Should().NuPkgDoesNotContain( - - Path.Combine(projectDirectory.Path, "bin", "Debug", "PackageLibraryNoStaticAssets.1.0.0.nupkg"), - - filePaths: new[] - - { - - Path.Combine("staticwebassets"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), - - Path.Combine("build", "PackageLibraryNoStaticAssets.props"), - - Path.Combine("buildMultiTargeting", "PackageLibraryNoStaticAssets.props"), - - Path.Combine("buildTransitive", "PackageLibraryNoStaticAssets.props") - - }); - - } - - - - [TestMethod] - - public void Pack_Incremental_IncludesStaticWebAssets() - - { - - var testAsset = "PackageLibraryDirectDependency"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var pack2 = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result2 = ExecuteCommand(pack2); - - - - result2.Should().Pass(); - - - - var outputPath = pack2.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result2.Should().NuPkgContainsPatterns( - - Path.Combine(pack2.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePatterns: new[] - - { - - Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), - - Path.Combine("staticwebassets", "css", "site.css"), - - Path.Combine("staticwebassets", "PackageLibraryDirectDependency.*.bundle.scp.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), - - Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), - - Path.Combine("build", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") - - }); - - } - - - - [TestMethod] - - public void Pack_StaticWebAssets_WithoutFileExtension_AreCorrectlyPacked() - - { - - var testAsset = "PackageLibraryDirectDependency"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - File.WriteAllText(Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "wwwroot", "LICENSE"), "license file contents"); - - - - var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result.Should().NuPkgContainsPatterns( - - Path.Combine(pack.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePatterns: new[] - - { - - Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), - - Path.Combine("staticwebassets", "css", "site.css"), - - Path.Combine("staticwebassets", "LICENSE"), - - Path.Combine("staticwebassets", "PackageLibraryDirectDependency.*.bundle.scp.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), - - Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), - - Path.Combine("build", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") - - }); - - } - - - - [TestMethod] - - public void Pack_MultipleTargetFrameworks_Works() - - { - - var projectDirectory = SetupMultiTargetProject(); - - - - var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result.Should().NuPkgContain( - - Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "bin", "Debug", "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePaths: new[] - - { - - Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), - - Path.Combine("staticwebassets", "css", "site.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), - - Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), - - Path.Combine("build", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") - - }); - - } - - - - [TestMethod] - - public void Pack_MultipleTargetFrameworks_NoBuild_IncludesStaticWebAssets() - - { - - var projectDirectory = SetupMultiTargetProject(); - - - - var build = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var buildResult = build.Execute(); - - - - var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = pack.Execute("/p:NoBuild=true"); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result.Should().NuPkgContain( - - Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "bin", "Debug", "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePaths: new[] - - { - - Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), - - Path.Combine("staticwebassets", "css", "site.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), - - Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), - - Path.Combine("build", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") - - }); - - } - - - - [TestMethod] - - public void Pack_MultipleTargetFrameworks_NoBuild_DoesNotIncludeAssetsAsContent() - - { - - var projectDirectory = SetupMultiTargetProject(); - - - - var build = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var buildResult = build.Execute(); - - - - var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = pack.Execute("/p:NoBuild=true"); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result.Should().NuPkgDoesNotContain( - - Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "bin", "Debug", "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePaths: new[] - - { - - Path.Combine("content", "wwwroot", "js", "pkg-direct-dep.js"), - - Path.Combine("content", "wwwroot", "css", "site.css"), - - Path.Combine("contentFiles", "wwwroot", "js", "pkg-direct-dep.js"), - - Path.Combine("contentFiles", "wwwroot", "css", "site.css"), - - }); - - } - - - - [TestMethod] - - public void Pack_MultipleTargetFrameworks_GeneratePackageOnBuild_IncludesStaticWebAssets() - - { - - var projectDirectory = SetupMultiTargetProject(); - - - - var build = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = build.Execute("/p:GeneratePackageOnBuild=true"); - - - - result.Should().Pass(); - - - - var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result.Should().NuPkgContain( - - Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "bin", "Debug", "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePaths: new[] - - { - - Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), - - Path.Combine("staticwebassets", "css", "site.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), - - Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), - - Path.Combine("build", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") - - }); - - } - - - - [TestMethod] - - public void Pack_MultipleTargetFrameworks_GeneratePackageOnBuild_DoesNotIncludeAssetsAsContent() - - { - - var projectDirectory = SetupMultiTargetProject(); - - - - var build = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = build.Execute("/p:GeneratePackageOnBuild=true"); - - - - result.Should().Pass(); - - - - var outputPath = build.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result.Should().NuPkgDoesNotContain( - - Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "bin", "Debug", "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePaths: new[] - - { - - Path.Combine("content", "wwwroot", "js", "pkg-direct-dep.js"), - - Path.Combine("content", "wwwroot", "css", "site.css"), - - Path.Combine("contentFiles", "wwwroot", "js", "pkg-direct-dep.js"), - - Path.Combine("contentFiles", "wwwroot", "css", "site.css"), - - }); - - } - - - - [TestMethod] - - public void Pack_BeforeNet60_MultipleTargetFrameworks_WithScopedCss_IncludesAssetsAndProjectBundle() - - { - - var projectDirectory = SetupBeforeNet60ScopedCssProject(); - - - - var pack = CreatePackCommand(projectDirectory); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - result.Should().NuPkgContainsPatterns( - - packagePath, - - filePatterns: new[] - - { - - Path.Combine("staticwebassets", "exampleJsInterop.js"), - - Path.Combine("staticwebassets", "background.png"), - - Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), - - Path.Combine("build", "PackageLibraryTransitiveDependency.props"), - - Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.props"), - - Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.props") - - }); - - } - - - - [TestMethod] - - public void Pack_BeforeNet60_MultipleTargetFrameworks_WithScopedCss_DoesNotIncludeAssetsAsContent() - - { - - var projectDirectory = SetupBeforeNet60ScopedCssProject(); - - - - var pack = CreatePackCommand(projectDirectory); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - result.Should().NuPkgDoesNotContain( - - packagePath, - - filePaths: new[] - - { - - Path.Combine("content", "exampleJsInterop.js"), - - Path.Combine("content", "background.png"), - - Path.Combine("content", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - Path.Combine("contentFiles", "exampleJsInterop.js"), - - Path.Combine("contentFiles", "background.png"), - - Path.Combine("contentFiles", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - }); - - } - - - - [TestMethod] - - public void Pack_BeforeNet60_MultipleTargetFrameworks_NoBuild_WithScopedCss_IncludesAssetsAndProjectBundle() - - { - - var projectDirectory = SetupBeforeNet60ScopedCssProject(); - - - - var build = CreateBuildCommand(projectDirectory); - - var buildResult = build.Execute(); - - - - buildResult.Should().Pass(); - - - - var pack = CreatePackCommand(projectDirectory); - - var result = pack.Execute("/p:NoBuild=true"); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - result.Should().NuPkgContain( - - packagePath, - - filePaths: new[] - - { - - Path.Combine("staticwebassets", "exampleJsInterop.js"), - - Path.Combine("staticwebassets", "background.png"), - - Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), - - Path.Combine("build", "PackageLibraryTransitiveDependency.props"), - - Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.props"), - - Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.props") - - }); - - } - - - - [TestMethod] - - public void Pack_BeforeNet60_MultipleTargetFrameworks_NoBuild_WithScopedCss_DoesNotIncludeAssetsAsContent() - - { - - var projectDirectory = SetupBeforeNet60ScopedCssProject(); - - - - var build = CreateBuildCommand(projectDirectory); - - var buildResult = build.Execute(); - - - - buildResult.Should().Pass(); - - - - var pack = CreatePackCommand(projectDirectory); - - var result = pack.Execute("/p:NoBuild=true"); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - result.Should().NuPkgDoesNotContain( - - packagePath, - - filePaths: new[] - - { - - Path.Combine("content", "exampleJsInterop.js"), - - Path.Combine("content", "background.png"), - - Path.Combine("content", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - Path.Combine("contentFiles", "exampleJsInterop.js"), - - Path.Combine("contentFiles", "background.png"), - - Path.Combine("contentFiles", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - }); - - } - - - - [TestMethod] - - public void Pack_BeforeNet60_MultipleTargetFrameworks_GeneratePackageOnBuild_WithScopedCss_IncludesAssetsAndProjectBundle() - - { - - var projectDirectory = SetupBeforeNet60ScopedCssProject(); - - - - var build = CreateBuildCommand(projectDirectory); - - var result = build.Execute("/p:GeneratePackageOnBuild=true"); - - - - result.Should().Pass(); - - - - var outputPath = build.GetOutputDirectory("net5.0", "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - result.Should().NuPkgContain( - - packagePath, - - filePaths: new[] - - { - - Path.Combine("staticwebassets", "exampleJsInterop.js"), - - Path.Combine("staticwebassets", "background.png"), - - Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), - - Path.Combine("build", "PackageLibraryTransitiveDependency.props"), - - Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.props"), - - Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.props") - - }); - - } - - - - [TestMethod] - - public void Pack_BeforeNet60_MultipleTargetFrameworks_GeneratePackageOnBuild_WithScopedCss_DoesNotIncludeAssetsAsContent() - - { - - var projectDirectory = SetupBeforeNet60ScopedCssProject(); - - - - var build = CreateBuildCommand(projectDirectory); - - var result = build.Execute("/p:GeneratePackageOnBuild=true"); - - - - result.Should().Pass(); - - - - var outputPath = build.GetOutputDirectory("net5.0", "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - result.Should().NuPkgDoesNotContain( - - packagePath, - - filePaths: new[] - - { - - Path.Combine("content", "exampleJsInterop.js"), - - Path.Combine("content", "background.png"), - - Path.Combine("content", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - Path.Combine("contentFiles", "exampleJsInterop.js"), - - Path.Combine("contentFiles", "background.png"), - - Path.Combine("contentFiles", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - }); - - } - - - - [TestMethod] - - public void Pack_Net50_WithScopedCss_IncludesAssetsAndProjectBundle() - - { - - var projectDirectory = SetupNet50ScopedCssProject(); - - - - var pack = CreatePackCommand(projectDirectory); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - result.Should().NuPkgContain( - - packagePath, - - filePaths: new[] - - { - - Path.Combine("staticwebassets", "exampleJsInterop.js"), - - Path.Combine("staticwebassets", "background.png"), - - Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), - - Path.Combine("build", "PackageLibraryTransitiveDependency.props"), - - Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.props"), - - Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.props") - - }); - - } - - - - [TestMethod] - - public void Pack_Net50_WithScopedCss_DoesNotIncludeAssetsAsContent() - - { - - var projectDirectory = SetupNet50ScopedCssProject(); - - - - var pack = CreatePackCommand(projectDirectory); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - result.Should().NuPkgDoesNotContain( - - packagePath, - - filePaths: new[] - - { - - Path.Combine("content", "exampleJsInterop.js"), - - Path.Combine("content", "background.png"), - - Path.Combine("content", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - Path.Combine("contentFiles", "exampleJsInterop.js"), - - Path.Combine("contentFiles", "background.png"), - - Path.Combine("contentFiles", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - }); - - } - - - - [TestMethod] - - public void Pack_Net50_NoBuild_WithScopedCss_IncludesAssetsAndProjectBundle() - - { - - var projectDirectory = SetupNet50ScopedCssProject(); - - - - var build = CreateBuildCommand(projectDirectory); - - var buildResult = build.Execute(); - - - - buildResult.Should().Pass(); - - - - var pack = CreatePackCommand(projectDirectory); - - var result = pack.Execute("/p:NoBuild=true"); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - result.Should().NuPkgContain( - - packagePath, - - filePaths: new[] - - { - - Path.Combine("staticwebassets", "exampleJsInterop.js"), - - Path.Combine("staticwebassets", "background.png"), - - Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), - - Path.Combine("build", "PackageLibraryTransitiveDependency.props"), - - Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.props"), - - Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.props") - - }); - - } - - - - [TestMethod] - - public void Pack_Net50_NoBuild_WithScopedCss_DoesNotIncludeAssetsAsContent() - - { - - var projectDirectory = SetupNet50ScopedCssProject(); - - - - var build = CreateBuildCommand(projectDirectory); - - var buildResult = build.Execute(); - - - - buildResult.Should().Pass(); - - - - var pack = CreatePackCommand(projectDirectory); - - var result = pack.Execute("/p:NoBuild=true"); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory("net5.0", "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - result.Should().NuPkgDoesNotContain( - - packagePath, - - filePaths: new[] - - { - - Path.Combine("content", "exampleJsInterop.js"), - - Path.Combine("content", "background.png"), - - Path.Combine("content", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - Path.Combine("contentFiles", "exampleJsInterop.js"), - - Path.Combine("contentFiles", "background.png"), - - Path.Combine("contentFiles", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - }); - - } - - - - [TestMethod] - - public void Pack_Net50_GeneratePackageOnBuild_WithScopedCss_IncludesAssetsAndProjectBundle() - - { - - var projectDirectory = SetupNet50ScopedCssProject(); - - - - var build = CreateBuildCommand(projectDirectory); - - var result = build.Execute("/p:GeneratePackageOnBuild=true"); - - - - result.Should().Pass(); - - - - var outputPath = build.GetOutputDirectory("net5.0", "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - result.Should().NuPkgContain( - - packagePath, - - filePaths: new[] - - { - - Path.Combine("staticwebassets", "exampleJsInterop.js"), - - Path.Combine("staticwebassets", "background.png"), - - Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), - - Path.Combine("build", "PackageLibraryTransitiveDependency.props"), - - Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.props"), - - Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.props") - - }); - - } - - - - [TestMethod] - - public void Pack_Net50_GeneratePackageOnBuild_WithScopedCss_DoesNotIncludeAssetsAsContent() - - { - - var projectDirectory = SetupNet50ScopedCssProject(); - - - - var build = CreateBuildCommand(projectDirectory); - - var result = build.Execute("/p:GeneratePackageOnBuild=true"); - - - - result.Should().Pass(); - - - - var outputPath = build.GetOutputDirectory("net5.0", "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - result.Should().NuPkgDoesNotContain( - - packagePath, - - filePaths: new[] - - { - - Path.Combine("content", "exampleJsInterop.js"), - - Path.Combine("content", "background.png"), - - Path.Combine("content", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - Path.Combine("contentFiles", "exampleJsInterop.js"), - - Path.Combine("contentFiles", "background.png"), - - Path.Combine("contentFiles", "PackageLibraryTransitiveDependency.bundle.scp.css"), - - }); - - } - - - - [TestMethod] - - public void Pack_MultipleTargetFrameworks_WithScopedCssAndJsModules_IncludesAssetsAndProjectBundle() - - { - - var testAsset = "PackageLibraryTransitiveDependency"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - projectDirectory.WithProjectChanges(document => - - { - - var parse = XDocument.Parse($@" - - - - - - {ToolsetInfo.CurrentTargetFramework};net8.0;net7.0;net6.0;net5.0 - - - - - - - - - - - - - - - - - - - - - - - - "); - - document.Root.ReplaceWith(parse.Root); - - }); - - - - Directory.Delete(Path.Combine(projectDirectory.Path, "wwwroot"), recursive: true); - - - - var componentText = @"
- - This component is defined in the razorclasslibrarypack library. - -
"; - - - - // This mimics the structure of our default template project - - Directory.CreateDirectory(Path.Combine(projectDirectory.Path, "wwwroot")); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "_Imports.razor"), "@using Microsoft.AspNetCore.Components.Web" + Environment.NewLine); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor"), componentText); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor.css"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor.js"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "ExampleJsInterop.cs"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "background.png"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "PackageLibraryTransitiveDependency.lib.module.js"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "exampleJsInterop.js"), ""); - - - - var pack = CreatePackCommand(projectDirectory); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - result.Should().NuPkgContainsPatterns( - - packagePath, - - filePatterns: new[] - - { - - Path.Combine("staticwebassets", "exampleJsInterop.js"), - - Path.Combine("staticwebassets", "background.png"), - - Path.Combine("staticwebassets", "Component1.razor.js"), - - Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.*.bundle.scp.css"), - - Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.*.lib.module.js"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), - - Path.Combine("build", "PackageLibraryTransitiveDependency.PackageAssets.json"), - - Path.Combine("build", "PackageLibraryTransitiveDependency.targets"), - - Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.targets"), - - Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.targets") - - }); - - } - - - - [TestMethod] - - public void Pack_Incremental_MultipleTargetFrameworks_WithScopedCssAndJsModules_IncludesAssetsAndProjectBundle() - - { - - var testAsset = "PackageLibraryTransitiveDependency"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - projectDirectory.WithProjectChanges(document => - - { - - var parse = XDocument.Parse($@" - - - - - - {ToolsetInfo.CurrentTargetFramework};net8.0;net7.0;net6.0;net5.0 - - - - - - - - - - - - - - - - - - - - - - - - "); - - document.Root.ReplaceWith(parse.Root); - - }); - - - - Directory.Delete(Path.Combine(projectDirectory.Path, "wwwroot"), recursive: true); - - - - var componentText = @"
- - This component is defined in the razorclasslibrarypack library. - -
"; - - - - // This mimics the structure of our default template project - - Directory.CreateDirectory(Path.Combine(projectDirectory.Path, "wwwroot")); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "_Imports.razor"), "@using Microsoft.AspNetCore.Components.Web" + Environment.NewLine); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor"), componentText); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor.css"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor.js"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "ExampleJsInterop.cs"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "background.png"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "PackageLibraryTransitiveDependency.lib.module.js"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "exampleJsInterop.js"), ""); - - - - var pack = CreatePackCommand(projectDirectory); - - - - var pack2 = CreatePackCommand(projectDirectory); - - var result2 = pack2.Execute(); - - - - result2.Should().Pass(); - - - - var outputPath = pack2.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - result2.Should().NuPkgContainsPatterns( - - packagePath, - - filePatterns: new[] - - { - - Path.Combine("staticwebassets", "exampleJsInterop.js"), - - Path.Combine("staticwebassets", "background.png"), - - Path.Combine("staticwebassets", "Component1.razor.js"), - - Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.*.bundle.scp.css"), - - Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.*.lib.module.js"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), - - Path.Combine("build", "PackageLibraryTransitiveDependency.PackageAssets.json"), - - Path.Combine("build", "PackageLibraryTransitiveDependency.targets"), - - Path.Combine("buildMultiTargeting", "PackageLibraryTransitiveDependency.targets"), - - Path.Combine("buildTransitive", "PackageLibraryTransitiveDependency.targets") - - }); - - } - - - - [TestMethod] - - public void Pack_MultipleTargetFrameworks_WithScopedCssAndJsModules_DoesNotIncludeApplicationBundleNorModulesManifest() - - { - - var testAsset = "PackageLibraryTransitiveDependency"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - projectDirectory.WithProjectChanges(document => - - { - - var parse = XDocument.Parse($@" - - - - - - {ToolsetInfo.CurrentTargetFramework};net8.0;net7.0;net6.0;net5.0 - - - - - - - - - - - - - - - - - - - - - - - - "); - - document.Root.ReplaceWith(parse.Root); - - }); - - - - Directory.Delete(Path.Combine(projectDirectory.Path, "wwwroot"), recursive: true); - - - - var componentText = @"
- - This component is defined in the razorclasslibrarypack library. - -
"; - - - - // This mimics the structure of our default template project - - Directory.CreateDirectory(Path.Combine(projectDirectory.Path, "wwwroot")); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "_Imports.razor"), "@using Microsoft.AspNetCore.Components.Web" + Environment.NewLine); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor"), componentText); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor.css"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "ExampleJsInterop.cs"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "background.png"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "exampleJsInterop.js"), ""); - - - - var pack = CreatePackCommand(projectDirectory); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - var packagePath = Path.Combine( - - projectDirectory.Path, - - "bin", - - "Debug", - - "PackageLibraryTransitiveDependency.1.0.0.nupkg"); - - - - - result.Should().NuPkgDoesNotContain( - - - packagePath, - - + result.Should().NuPkgDoesNotContain( + packagePath, filePaths: new[] - - { - - Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.styles.css"), - - Path.Combine("staticwebassets", "PackageLibraryTransitiveDependency.modules.json"), - - }); - - } - - - - [TestMethod] - - public void Pack_MultipleTargetFrameworks_DoesNotIncludeAssetsAsContent() - - { - - var testAsset = "PackageLibraryDirectDependency"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - projectDirectory.WithProjectChanges((project, document) => - - { - - var tfm = document.Descendants("TargetFramework").Single(); - - tfm.Name = "TargetFrameworks"; - - tfm.FirstNode.ReplaceWith(tfm.FirstNode.ToString() + ";netstandard2.1"); - - - - document.Descendants("AddRazorSupportForMvc").SingleOrDefault()?.Remove(); - - document.Descendants("FrameworkReference").SingleOrDefault()?.Remove(); - - }); - - - - Directory.Delete(Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "Components"), recursive: true); - - - - var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result.Should().NuPkgDoesNotContain( - - Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "bin", "Debug", "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePaths: new[] - - { - - Path.Combine("content", "wwwroot", "js", "pkg-direct-dep.js"), - - Path.Combine("content", "wwwroot", "css", "site.css"), - - Path.Combine("contentFiles", "wwwroot", "js", "pkg-direct-dep.js"), - - Path.Combine("contentFiles", "wwwroot", "css", "site.css"), - - }); - - } - - - - [TestMethod] - - public void Pack_DoesNotInclude_TransitiveBundleOrScopedCssAsStaticWebAsset() - - { - - var testAsset = "PackageLibraryDirectDependency"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result.Should().NuPkgDoesNotContain( - - Path.Combine(pack.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePaths: new[] - - { - - // This is to make sure we don't include the scoped css files on the package when bundling is enabled. - - Path.Combine("staticwebassets", "Components", "App.razor.rz.scp.css"), - - Path.Combine("staticwebassets", "PackageLibraryDirectDependency.styles.css"), - - }); - - } - - - - [TestMethod] - - public void Pack_DoesNotIncludeStaticWebAssetsAsContent() - - { - - var testAsset = "PackageLibraryDirectDependency"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = ExecuteCommand(pack); - - - - result.Should().Pass(); - - - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result.Should().NuPkgDoesNotContain( - - Path.Combine(pack.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePaths: new[] - - { - - Path.Combine("content", "wwwroot", "js", "pkg-direct-dep.js"), - - Path.Combine("content", "wwwroot", "css", "site.css"), - - Path.Combine("content", "Components", "App.razor.css"), - - // This is to make sure we don't include the unscoped css file on the package. - - Path.Combine("content", "Components", "App.razor.css"), - - Path.Combine("content", "Components", "App.razor.rz.scp.css"), - - Path.Combine("contentFiles", "wwwroot", "js", "pkg-direct-dep.js"), - - Path.Combine("contentFiles", "wwwroot", "css", "site.css"), - - Path.Combine("contentFiles", "Components", "App.razor.css"), - - Path.Combine("contentFiles", "Components", "App.razor.rz.scp.css"), - - }); - - } - - - - [TestMethod] - - public void Pack_NoBuild_IncludesStaticWebAssets() - - { - - var testAsset = "PackageLibraryDirectDependency"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - var build = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); - - build.Execute().Should().Pass(); - - - - var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = pack.Execute("/p:NoBuild=true"); - - - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result.Should().NuPkgContainsPatterns( - - Path.Combine(build.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePatterns: new[] - - { - - Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), - - Path.Combine("staticwebassets", "PackageLibraryDirectDependency.*.bundle.scp.css"), - - Path.Combine("staticwebassets", "css", "site.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), - - Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), - - Path.Combine("build", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") - - }); - - } - - - - [TestMethod] - - public void Pack_NoBuild_DoesNotIncludeFilesAsContent() - - { - - var testAsset = "PackageLibraryDirectDependency"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - var build = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); - - build.Execute().Should().Pass(); - - - - var pack = CreatePackCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = pack.Execute("/p:NoBuild=true"); - - - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result.Should().NuPkgDoesNotContain( - - Path.Combine(pack.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePaths: new[] - - { - - Path.Combine("content", "wwwroot", "js", "pkg-direct-dep.js"), - - Path.Combine("content", "PackageLibraryDirectDependency.bundle.scp.css"), - - Path.Combine("content", "wwwroot", "css", "site.css"), - - Path.Combine("contentFiles", "wwwroot", "js", "pkg-direct-dep.js"), - - Path.Combine("contentFiles", "PackageLibraryDirectDependency.bundle.scp.css"), - - Path.Combine("contentFiles", "wwwroot", "css", "site.css"), - - }); - - } - - - - [TestMethod] - - public void Pack_DoesNotIncludeAnyCustomPropsFiles_WhenNoStaticAssetsAreAvailable() - - { - - var testAsset = "RazorComponentLibrary"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset); - - - - var pack = CreatePackCommand(projectDirectory); - - var result = ExecuteCommand(pack); - - - - var outputPath = pack.GetOutputDirectory("netstandard2.0", "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "ComponentLibrary.dll")).Should().Exist(); - - - - result.Should().NuPkgDoesNotContain( - - Path.Combine(projectDirectory.Path, "bin", "Debug", "ComponentLibrary.1.0.0.nupkg"), - - filePaths: new[] - - { - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.props"), - - Path.Combine("build", "ComponentLibrary.props"), - - Path.Combine("buildMultiTargeting", "ComponentLibrary.props"), - - Path.Combine("buildTransitive", "ComponentLibrary.props") - - }); - - } - - - - [TestMethod] - - public void Pack_Incremental_DoesNotRegenerateCacheAndPropsFiles() - - { - - var testAsset = "PackageLibraryTransitiveDependency"; - - var projectDirectory = TestAssetsManager - - .CopyTestAsset(testAsset, testAssetSubdirectory: "TestPackages") - - .WithSource(); - - - - var pack = CreatePackCommand(projectDirectory); - - var result = ExecuteCommand(pack); - - - - var intermediateOutputPath = pack.GetIntermediateDirectory(DefaultTfm, "Debug").ToString(); - - var outputPath = pack.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryTransitiveDependency.dll")).Should().Exist(); - - - - new FileInfo(Path.Combine(intermediateOutputPath, "staticwebassets", "msbuild.PackageLibraryTransitiveDependency.Microsoft.AspNetCore.StaticWebAssets.targets")).Should().Exist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "staticwebassets", "msbuild.build.PackageLibraryTransitiveDependency.targets")).Should().Exist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "staticwebassets", "msbuild.buildMultiTargeting.PackageLibraryTransitiveDependency.targets")).Should().Exist(); - - new FileInfo(Path.Combine(intermediateOutputPath, "staticwebassets", "msbuild.buildTransitive.PackageLibraryTransitiveDependency.targets")).Should().Exist(); - - - - var directoryPath = Path.Combine(intermediateOutputPath, "staticwebassets"); - - var thumbPrints = new Dictionary(); - - var thumbPrintFiles = new[] - - { - - Path.Combine(directoryPath, "msbuild.PackageLibraryTransitiveDependency.Microsoft.AspNetCore.StaticWebAssets.targets"), - - Path.Combine(directoryPath, "msbuild.build.PackageLibraryTransitiveDependency.targets"), - - Path.Combine(directoryPath, "msbuild.buildMultiTargeting.PackageLibraryTransitiveDependency.targets"), - - Path.Combine(directoryPath, "msbuild.buildTransitive.PackageLibraryTransitiveDependency.targets"), - - }; - - - - foreach (var file in thumbPrintFiles) - - { - - var thumbprint = FileThumbPrint.Create(file); - - thumbPrints[file] = thumbprint; - - } - - - - // Act - - var incremental = CreatePackCommand(projectDirectory); - - incremental.Execute().Should().Pass(); - - foreach (var file in thumbPrintFiles) - - { - - var thumbprint = FileThumbPrint.Create(file); - - Assert.AreEqual(thumbPrints[file], thumbprint); - - } - - } - - - - [TestMethod] - - public void Build_StaticWebAssets_GeneratePackageOnBuild_PacksStaticWebAssets() - - { - - var testAsset = "PackageLibraryDirectDependency"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - File.WriteAllText(Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "wwwroot", "LICENSE"), "license file contents"); - - - - var buildCommand = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = buildCommand.Execute("/p:GeneratePackageOnBuild=true"); - - - - result.Should().Pass(); - - - - var outputPath = buildCommand.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result.Should().NuPkgContainsPatterns( - - Path.Combine(buildCommand.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePatterns: new[] - - { - - Path.Combine("staticwebassets", "js", "pkg-direct-dep.js"), - - Path.Combine("staticwebassets", "css", "site.css"), - - Path.Combine("staticwebassets", "PackageLibraryDirectDependency.*.bundle.scp.css"), - - Path.Combine("build", "Microsoft.AspNetCore.StaticWebAssets.targets"), - - Path.Combine("build", "PackageLibraryDirectDependency.PackageAssets.json"), - - Path.Combine("build", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildMultiTargeting", "PackageLibraryDirectDependency.targets"), - - Path.Combine("buildTransitive", "PackageLibraryDirectDependency.targets") - - }); - - } - - - - [TestMethod] - - public void Build_StaticWebAssets_GeneratePackageOnBuild_DoesNotIncludeAssetsAsContent() - - { - - var testAsset = "PackageLibraryDirectDependency"; - - var projectDirectory = CreateAspNetSdkTestAsset(testAsset, subdirectory: "TestPackages"); - - - - File.WriteAllText(Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "wwwroot", "LICENSE"), "license file contents"); - - - - var buildCommand = CreateBuildCommand(projectDirectory, "PackageLibraryDirectDependency"); - - var result = buildCommand.Execute("/p:GeneratePackageOnBuild=true"); - - - - result.Should().Pass(); - - - - var outputPath = buildCommand.GetOutputDirectory(DefaultTfm, "Debug").ToString(); - - - - new FileInfo(Path.Combine(outputPath, "PackageLibraryDirectDependency.dll")).Should().Exist(); - - - - result.Should().NuPkgDoesNotContainPatterns( - - Path.Combine(buildCommand.GetPackageDirectory().FullName, "PackageLibraryDirectDependency.1.0.0.nupkg"), - - filePatterns: new[] - - { - - Path.Combine("content", "js", "pkg-direct-dep.js"), - - Path.Combine("content", "css", "site.css"), - - Path.Combine("content", "PackageLibraryDirectDependency.*.bundle.scp.css"), - - Path.Combine("contentFiles", "js", "pkg-direct-dep.js"), - - Path.Combine("contentFiles", "css", "site.css"), - - Path.Combine("contentFiles", "PackageLibraryDirectDependency.bundle.scp.css"), - - }); - - } - - - - private TestAsset SetupMultiTargetProject() - - { - - var projectDirectory = CreateAspNetSdkTestAsset("PackageLibraryDirectDependency", subdirectory: "TestPackages"); - - - - projectDirectory.WithProjectChanges((project, document) => - - { - - var tfm = document.Descendants("TargetFramework").Single(); - - tfm.Name = "TargetFrameworks"; - - tfm.FirstNode.ReplaceWith(tfm.FirstNode.ToString() + ";netstandard2.1"); - - - - document.Descendants("AddRazorSupportForMvc").SingleOrDefault()?.Remove(); - - document.Descendants("FrameworkReference").SingleOrDefault()?.Remove(); - - }); - - - - Directory.Delete(Path.Combine(projectDirectory.Path, "PackageLibraryDirectDependency", "Components"), recursive: true); - - return projectDirectory; - - } - - - - private TestAsset SetupBeforeNet60ScopedCssProject() - - { - - var projectDirectory = CreateAspNetSdkTestAsset("PackageLibraryTransitiveDependency", subdirectory: "TestPackages"); - - - - projectDirectory.WithProjectChanges(document => - - { - - var parse = XDocument.Parse($@" - - - - - - netstandard2.0;net5.0 - - 3.0 - - - - - - - - - - - - - - - - - - "); - - document.Root.ReplaceWith(parse.Root); - - }); - - - - SetupScopedCssFiles(projectDirectory); - - return projectDirectory; - - } - - - - private TestAsset SetupNet50ScopedCssProject() - - { - - var projectDirectory = CreateAspNetSdkTestAsset("PackageLibraryTransitiveDependency", subdirectory: "TestPackages"); - - - - projectDirectory.WithProjectChanges(document => - - { - - var parse = XDocument.Parse($@" - - - - - - net5.0 - - - - - - - - - - - - - - - - "); - - document.Root.ReplaceWith(parse.Root); - - }); - - - - SetupScopedCssFiles(projectDirectory); - - return projectDirectory; - - } - - - - private static void SetupScopedCssFiles(TestAsset projectDirectory) - - { - - Directory.Delete(Path.Combine(projectDirectory.Path, "wwwroot"), recursive: true); - - - - var componentText = @"
- - This component is defined in the razorclasslibrarypack library. - -
"; - - - - Directory.CreateDirectory(Path.Combine(projectDirectory.Path, "wwwroot")); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "_Imports.razor"), "@using Microsoft.AspNetCore.Components.Web" + Environment.NewLine); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor"), componentText); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "Component1.razor.css"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "ExampleJsInterop.cs"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "background.png"), ""); - - File.WriteAllText(Path.Combine(projectDirectory.Path, "wwwroot", "exampleJsInterop.js"), ""); - - } - - } - - } - -