From ae569e2b48f66a2c4833319e82fcc74876c763ce Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 1 Apr 2020 21:35:30 +0100 Subject: [PATCH] Add some extra publish integration tests, plus fix publish-from-VS-with-RID (#20410) * Add detailed integration tests for publishing service workers, assets manifests, and blazor.boot.json * Fix publishing from VS with non-portable RID --- .../src/targets/Blazor.MonoRuntime.targets | 7 +- .../ServiceWorkerAssetsManifest.targets | 3 +- .../test/BuildIntegrationTests/Assert.cs | 18 +++ .../PublishIntegrationTest.cs | 145 ++++++++++++++++++ .../testassets/standalone/standalone.csproj | 5 +- .../serviceworkers/my-prod-service-worker.js | 1 + .../serviceworkers/my-service-worker.js | 1 + 7 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 src/Components/WebAssembly/Build/testassets/standalone/wwwroot/serviceworkers/my-prod-service-worker.js create mode 100644 src/Components/WebAssembly/Build/testassets/standalone/wwwroot/serviceworkers/my-service-worker.js diff --git a/src/Components/WebAssembly/Build/src/targets/Blazor.MonoRuntime.targets b/src/Components/WebAssembly/Build/src/targets/Blazor.MonoRuntime.targets index 8058d0d8e6..2129568bac 100644 --- a/src/Components/WebAssembly/Build/src/targets/Blazor.MonoRuntime.targets +++ b/src/Components/WebAssembly/Build/src/targets/Blazor.MonoRuntime.targets @@ -210,6 +210,10 @@ + + Outputs="$(_BlazorLinkerOutputCache)" + Condition="'$(BuildingInsideVisualStudio)' != 'true' OR '$(DeployOnBuild)' != 'true'"> <_BlazorLinkerAdditionalOptions>-l $(MonoLinkerI18NAssemblies) $(AdditionalMonoLinkerOptions) diff --git a/src/Components/WebAssembly/Build/src/targets/ServiceWorkerAssetsManifest.targets b/src/Components/WebAssembly/Build/src/targets/ServiceWorkerAssetsManifest.targets index 15cb4372ae..1cac810b55 100644 --- a/src/Components/WebAssembly/Build/src/targets/ServiceWorkerAssetsManifest.targets +++ b/src/Components/WebAssembly/Build/src/targets/ServiceWorkerAssetsManifest.targets @@ -89,7 +89,7 @@ - + @@ -113,6 +113,7 @@ %(ServiceWorker.Identity) %(ServiceWorker.Identity) $([System.String]::Copy('%(ServiceWorker.Identity)').Substring(8)) + $([System.String]::Copy('%(ServiceWorker.Identity)').Substring(8)) diff --git a/src/Components/WebAssembly/Build/test/BuildIntegrationTests/Assert.cs b/src/Components/WebAssembly/Build/test/BuildIntegrationTests/Assert.cs index 75639b0228..c5329f644d 100644 --- a/src/Components/WebAssembly/Build/test/BuildIntegrationTests/Assert.cs +++ b/src/Components/WebAssembly/Build/test/BuildIntegrationTests/Assert.cs @@ -9,6 +9,7 @@ using System.IO.Compression; using System.Linq; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; +using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -238,6 +239,23 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build } } + public static void FileHashEquals(MSBuildResult result, string filePath, string expectedSha256Base64) + { + if (result == null) + { + throw new ArgumentNullException(nameof(result)); + } + + filePath = Path.Combine(result.Project.DirectoryPath, filePath); + FileExists(result, filePath); + + var actual = File.ReadAllBytes(filePath); + using var algorithm = SHA256.Create(); + var actualSha256 = algorithm.ComputeHash(actual); + var actualSha256Base64 = Convert.ToBase64String(actualSha256); + Assert.Equal(expectedSha256Base64, actualSha256Base64); + } + public static void FileEquals(MSBuildResult result, string expected, string actual) { if (result == null) diff --git a/src/Components/WebAssembly/Build/test/BuildIntegrationTests/PublishIntegrationTest.cs b/src/Components/WebAssembly/Build/test/BuildIntegrationTests/PublishIntegrationTest.cs index 691a17b69b..c727b1dad5 100644 --- a/src/Components/WebAssembly/Build/test/BuildIntegrationTests/PublishIntegrationTest.cs +++ b/src/Components/WebAssembly/Build/test/BuildIntegrationTests/PublishIntegrationTest.cs @@ -1,9 +1,15 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Text.Json; using System.Threading.Tasks; +using Microsoft.AspNetCore.Blazor.Build; using Xunit; +using ResourceHashesByNameDictionary = System.Collections.Generic.Dictionary; using static Microsoft.AspNetCore.Components.WebAssembly.Build.WebAssemblyRuntimePackage; namespace Microsoft.AspNetCore.Components.WebAssembly.Build @@ -44,6 +50,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); + + VerifyBootManifestHashes(result, blazorPublishDirectory); + VerifyServiceWorkerFiles(result, blazorPublishDirectory, + serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), + serviceWorkerContent: "// This is the production service worker", + assetsManifestPath: "custom-service-worker-assets.js"); } [Fact] @@ -81,6 +93,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); + + VerifyBootManifestHashes(result, blazorPublishDirectory); + VerifyServiceWorkerFiles(result, blazorPublishDirectory, + serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), + serviceWorkerContent: "// This is the production service worker", + assetsManifestPath: "custom-service-worker-assets.js"); } [Fact] @@ -116,6 +134,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); + + VerifyBootManifestHashes(result, blazorPublishDirectory); + VerifyServiceWorkerFiles(result, blazorPublishDirectory, + serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), + serviceWorkerContent: "// This is the production service worker", + assetsManifestPath: "custom-service-worker-assets.js"); } [Fact] @@ -145,6 +169,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build var bootJsonPath = Path.Combine(blazorPublishDirectory, "_framework", "blazor.boot.json"); Assert.FileContains(result, bootJsonPath, "\"Microsoft.CodeAnalysis.CSharp.dll\""); Assert.FileContains(result, bootJsonPath, "\"fr\\/Microsoft.CodeAnalysis.CSharp.resources.dll\""); + + VerifyBootManifestHashes(result, blazorPublishDirectory); + VerifyServiceWorkerFiles(result, blazorPublishDirectory, + serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), + serviceWorkerContent: "// This is the production service worker", + assetsManifestPath: "custom-service-worker-assets.js"); } [Fact] @@ -178,6 +208,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); + + VerifyBootManifestHashes(result, blazorPublishDirectory); + VerifyServiceWorkerFiles(result, blazorPublishDirectory, + serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), + serviceWorkerContent: "// This is the production service worker", + assetsManifestPath: "custom-service-worker-assets.js"); } [Fact] @@ -226,6 +262,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); + VerifyBootManifestHashes(result, blazorPublishDirectory); + VerifyServiceWorkerFiles(result, blazorPublishDirectory, + serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), + serviceWorkerContent: "// This is the production service worker", + assetsManifestPath: "custom-service-worker-assets.js"); + void AddSiblingProjectFileContent(string content) { var path = Path.Combine(project.SolutionPath, "standalone", "standalone.csproj"); @@ -269,6 +311,109 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Build // Verify web.config Assert.FileExists(result, publishDirectory, "web.config"); + + VerifyBootManifestHashes(result, blazorPublishDirectory); + VerifyServiceWorkerFiles(result, blazorPublishDirectory, + serviceWorkerPath: Path.Combine("serviceworkers", "my-service-worker.js"), + serviceWorkerContent: "// This is the production service worker", + assetsManifestPath: "custom-service-worker-assets.js"); + } + + private static void VerifyBootManifestHashes(MSBuildResult result, string blazorPublishDirectory) + { + var bootManifestResolvedPath = Assert.FileExists(result, blazorPublishDirectory, "_framework", "blazor.boot.json"); + var bootManifestJson = File.ReadAllText(bootManifestResolvedPath); + var bootManifest = JsonSerializer.Deserialize(bootManifestJson); + + VerifyBootManifestHashes(result, blazorPublishDirectory, bootManifest.resources.assembly, r => $"_framework/_bin/{r}"); + VerifyBootManifestHashes(result, blazorPublishDirectory, bootManifest.resources.runtime, r => $"_framework/wasm/{r}"); + + if (bootManifest.resources.pdb != null) + { + VerifyBootManifestHashes(result, blazorPublishDirectory, bootManifest.resources.pdb, r => $"_framework/_bin/{r}"); + } + + if (bootManifest.resources.satelliteResources != null) + { + foreach (var resourcesForCulture in bootManifest.resources.satelliteResources.Values) + { + VerifyBootManifestHashes(result, blazorPublishDirectory, resourcesForCulture, r => $"_framework/_bin/{r}"); + } + } + } + + private static void VerifyBootManifestHashes(MSBuildResult result, string blazorPublishDirectory, ResourceHashesByNameDictionary resources, Func relativePathFunc) + { + foreach (var (name, hash) in resources) + { + var relativePath = Path.Combine(blazorPublishDirectory, relativePathFunc(name)); + Assert.FileHashEquals(result, relativePath, ParseWebFormattedHash(hash)); + } + } + + private static void VerifyServiceWorkerFiles(MSBuildResult result, string blazorPublishDirectory, string serviceWorkerPath, string serviceWorkerContent, string assetsManifestPath) + { + // Check the expected files are there + var serviceWorkerResolvedPath = Assert.FileExists(result, blazorPublishDirectory, serviceWorkerPath); + var assetsManifestResolvedPath = Assert.FileExists(result, blazorPublishDirectory, assetsManifestPath); + + // Check the service worker contains the expected content (which comes from the PublishedContent file) + Assert.FileContains(result, serviceWorkerResolvedPath, serviceWorkerContent); + + // Check the assets manifest version was added to the published service worker + var assetsManifest = ReadServiceWorkerAssetsManifest(assetsManifestResolvedPath); + Assert.FileContains(result, serviceWorkerResolvedPath, $"/* Manifest version: {assetsManifest.version} */"); + + // Check the assets manifest contains correct entries for all static content we're publishing + var resolvedPublishDirectory = Path.Combine(result.Project.DirectoryPath, blazorPublishDirectory); + var publishedStaticFiles = Directory.GetFiles(resolvedPublishDirectory, "*", new EnumerationOptions { RecurseSubdirectories = true }); + var assetsManifestHashesByUrl = (IReadOnlyDictionary)assetsManifest.assets.ToDictionary(x => x.url, x => x.hash); + foreach (var publishedFilePath in publishedStaticFiles) + { + var publishedFileRelativePath = Path.GetRelativePath(resolvedPublishDirectory, publishedFilePath); + + // We don't list compressed files in the SWAM, as these are transparent to the client, + // nor do we list the service worker itself or its assets manifest, as these don't need to be fetched in the same way + if (IsCompressedFile(publishedFileRelativePath) + || string.Equals(publishedFileRelativePath, serviceWorkerPath, StringComparison.Ordinal) + || string.Equals(publishedFileRelativePath, assetsManifestPath, StringComparison.Ordinal)) + { + continue; + } + + // Verify hash + var publishedFileUrl = publishedFileRelativePath.Replace('\\', '/'); + var expectedHash = ParseWebFormattedHash(assetsManifestHashesByUrl[publishedFileUrl]); + Assert.Contains(publishedFileUrl, assetsManifestHashesByUrl); + Assert.FileHashEquals(result, publishedFilePath, expectedHash); + } + } + + private static string ParseWebFormattedHash(string webFormattedHash) + { + Assert.StartsWith("sha256-", webFormattedHash); + return webFormattedHash.Substring(7); + } + + private static bool IsCompressedFile(string path) + { + switch (Path.GetExtension(path)) + { + case ".br": + case ".gz": + return true; + default: + return false; + } + } + + private static GenerateServiceWorkerAssetsManifest.AssetsManifestFile ReadServiceWorkerAssetsManifest(string assetsManifestResolvedPath) + { + var jsContents = File.ReadAllText(assetsManifestResolvedPath); + var jsonStart = jsContents.IndexOf("{"); + var jsonLength = jsContents.LastIndexOf("}") - jsonStart + 1; + var json = jsContents.Substring(jsonStart, jsonLength); + return JsonSerializer.Deserialize(json); } } } diff --git a/src/Components/WebAssembly/Build/testassets/standalone/standalone.csproj b/src/Components/WebAssembly/Build/testassets/standalone/standalone.csproj index 8fc5d5cfb8..bbeafa77c2 100644 --- a/src/Components/WebAssembly/Build/testassets/standalone/standalone.csproj +++ b/src/Components/WebAssembly/Build/testassets/standalone/standalone.csproj @@ -1,9 +1,10 @@ - + netstandard2.1 3.0 + custom-service-worker-assets.js @@ -32,6 +33,8 @@ + + diff --git a/src/Components/WebAssembly/Build/testassets/standalone/wwwroot/serviceworkers/my-prod-service-worker.js b/src/Components/WebAssembly/Build/testassets/standalone/wwwroot/serviceworkers/my-prod-service-worker.js new file mode 100644 index 0000000000..a2ecc1b349 --- /dev/null +++ b/src/Components/WebAssembly/Build/testassets/standalone/wwwroot/serviceworkers/my-prod-service-worker.js @@ -0,0 +1 @@ +// This is the production service worker diff --git a/src/Components/WebAssembly/Build/testassets/standalone/wwwroot/serviceworkers/my-service-worker.js b/src/Components/WebAssembly/Build/testassets/standalone/wwwroot/serviceworkers/my-service-worker.js new file mode 100644 index 0000000000..c42d1c8475 --- /dev/null +++ b/src/Components/WebAssembly/Build/testassets/standalone/wwwroot/serviceworkers/my-service-worker.js @@ -0,0 +1 @@ +// This is the development service worker