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
This commit is contained in:
Steve Sanderson 2020-04-01 21:35:30 +01:00 committed by GitHub
parent a8bd551d99
commit ae569e2b48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 177 additions and 3 deletions

View File

@ -210,6 +210,10 @@
</ItemGroup>
</Target>
<!--
Note that the VS-specific condition below is a workaround for https://github.com/dotnet/aspnetcore/issues/19822
For more details, see https://github.com/dotnet/aspnetcore/issues/20413
-->
<UsingTask TaskName="BlazorILLink" AssemblyFile="$(_BlazorTasksPath)" />
<Target
Name="_LinkBlazorApplication"
@ -217,7 +221,8 @@
@(_BlazorManagedRuntimeAssembly);
@(BlazorLinkerDescriptor);
$(MSBuildAllProjects)"
Outputs="$(_BlazorLinkerOutputCache)">
Outputs="$(_BlazorLinkerOutputCache)"
Condition="'$(BuildingInsideVisualStudio)' != 'true' OR '$(DeployOnBuild)' != 'true'">
<PropertyGroup>
<_BlazorLinkerAdditionalOptions>-l $(MonoLinkerI18NAssemblies) $(AdditionalMonoLinkerOptions)</_BlazorLinkerAdditionalOptions>

View File

@ -89,7 +89,7 @@
</PropertyGroup>
</Target>
<Target Name="_OmitServiceWorkerContent" BeforeTargets="AssignTargetPaths">
<Target Name="_OmitServiceWorkerContent" BeforeTargets="AssignTargetPaths; ResolveCurrentProjectStaticWebAssetsInputs">
<ItemGroup>
<!-- Don't emit the service worker source files to the output -->
<Content Remove="@(ServiceWorker)" />
@ -113,6 +113,7 @@
<ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' == ''">%(ServiceWorker.Identity)</ContentSourcePath>
<TargetOutputPath>%(ServiceWorker.Identity)</TargetOutputPath>
<TargetOutputPath Condition="$([System.String]::Copy('%(ServiceWorker.Identity)').StartsWith('wwwroot\'))">$([System.String]::Copy('%(ServiceWorker.Identity)').Substring(8))</TargetOutputPath>
<TargetOutputPath Condition="$([System.String]::Copy('%(ServiceWorker.Identity)').StartsWith('wwwroot/'))">$([System.String]::Copy('%(ServiceWorker.Identity)').Substring(8))</TargetOutputPath>
</_ServiceWorkerIntermediateFile>
</ItemGroup>
</Target>

View File

@ -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)

View File

@ -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<string, string>;
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<GenerateBlazorBootJson.BootJsonData>(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<string, string> 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<string, string>)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<GenerateServiceWorkerAssetsManifest.AssetsManifestFile>(json);
}
}
}

View File

@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="$(ReferenceBlazorBuildFromSourceProps)" />
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
<ServiceWorkerAssetsManifest>custom-service-worker-assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>
<!-- Test Placeholder -->
@ -32,6 +33,8 @@
<!-- This asset should not be treated as a static web asset as it is being linked out of the wwwroot folder. -->
<Content Update="wwwroot\Fake-License.txt" Link="Excluded-Static-Web-Assets\Fake-License.txt" />
<!-- The content from my-prod-service-worker.js should be published under the name my-service-worker.js -->
<ServiceWorker Include="wwwroot\serviceworkers\my-service-worker.js" PublishedContent="wwwroot\serviceworkers\my-prod-service-worker.js" />
</ItemGroup>
</Project>

View File

@ -0,0 +1 @@
// This is the production service worker

View File

@ -0,0 +1 @@
// This is the development service worker