Added tests to verify fallback source content is same as the CDN content

This commit is contained in:
Kiran Challa 2018-05-16 14:57:44 -07:00
parent f2df6763aa
commit ac595c0c51
3 changed files with 135 additions and 59 deletions

View File

@ -3,6 +3,7 @@
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> <MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Package Versions"> <PropertyGroup Label="Package Versions">
<AngleSharpPackageVersion>0.9.9</AngleSharpPackageVersion>
<DotNetEfPackageVersion>2.2.0-preview1-34184</DotNetEfPackageVersion> <DotNetEfPackageVersion>2.2.0-preview1-34184</DotNetEfPackageVersion>
<InternalAspNetCoreSdkPackageVersion>2.2.0-preview1-17051</InternalAspNetCoreSdkPackageVersion> <InternalAspNetCoreSdkPackageVersion>2.2.0-preview1-17051</InternalAspNetCoreSdkPackageVersion>
<MicrosoftAspNetCoreAllPackageVersion>2.2.0-preview1-34184</MicrosoftAspNetCoreAllPackageVersion> <MicrosoftAspNetCoreAllPackageVersion>2.2.0-preview1-34184</MicrosoftAspNetCoreAllPackageVersion>

View File

@ -1,107 +1,171 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using AngleSharp.Dom.Html;
using AngleSharp.Parser.Html;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace Templates.Test namespace Templates.Test
{ {
public class CdnScriptTagTests public class CdnScriptTagTests : IDisposable
{ {
private readonly ITestOutputHelper _output; private readonly ITestOutputHelper _output;
private readonly HttpClient _httpClient;
private static readonly string _solutionDir;
private static readonly string _artifactsDir;
private static List<ScriptTag> _scriptTags;
static CdnScriptTagTests()
{
_solutionDir = GetSolutionDir();
_artifactsDir = Path.Combine(_solutionDir, "artifacts", "build");
var packages = Directory.GetFiles(_artifactsDir, "*.nupkg");
_scriptTags = new List<ScriptTag>();
foreach (var packagePath in packages)
{
_scriptTags.AddRange(GetScriptTags(packagePath));
}
}
public CdnScriptTagTests(ITestOutputHelper output) public CdnScriptTagTests(ITestOutputHelper output)
{ {
_output = output; _output = output;
_httpClient = new HttpClient();
} }
[Fact] public static IEnumerable<object[]> SubresourceIntegrityCheckData
public async Task CheckSubresourceIntegrity()
{ {
var dir = GetSolutionDir(); get
var artifactsDir = Path.Combine(dir, "artifacts", "build");
var packages = Directory.GetFiles(artifactsDir, "*.nupkg");
var scriptTags = new List<ScriptTag>();
foreach (var packagePath in packages)
{ {
scriptTags.AddRange(GetScriptTags(packagePath)); var scriptTags = _scriptTags
.Where(st => st.Integrity != null)
.Select(st => new object[] { st });
Assert.NotEmpty(scriptTags);
return scriptTags;
} }
Assert.NotEmpty(scriptTags);
var shasum = new Dictionary<string, string>();
var client = new HttpClient();
foreach (var script in scriptTags)
{
if (shasum.ContainsKey(script.Src))
{
continue;
}
using (var resp = await client.GetStreamAsync(script.Src))
using (var alg = SHA384.Create())
{
var hash = alg.ComputeHash(resp);
shasum.Add(script.Src, "sha384-" + Convert.ToBase64String(hash));
}
}
Assert.All(scriptTags, t =>
{
Assert.True(shasum[t.Src] == t.Integrity, userMessage: $"Expected integrity on script tag to be {shasum[t.Src]} but it was {t.Integrity}. {t.FileName}:{t.Entry}");
});
} }
private struct ScriptTag [Theory]
[MemberData(nameof(SubresourceIntegrityCheckData))]
public async Task CheckSubresourceIntegrity(ScriptTag scriptTag)
{
string expectedIntegrity;
using (var responseStream = await _httpClient.GetStreamAsync(scriptTag.Src))
using (var alg = SHA384.Create())
{
var hash = alg.ComputeHash(responseStream);
expectedIntegrity = "sha384-" + Convert.ToBase64String(hash);
}
Assert.Equal(expectedIntegrity, scriptTag.Integrity);
}
public static IEnumerable<object[]> FallbackSrcCheckData
{
get
{
var scriptTags = _scriptTags
.Where(st => st.FallbackSrc != null)
.Select(st => new object[] { st });
Assert.NotEmpty(scriptTags);
return scriptTags;
}
}
[Theory]
[MemberData(nameof(FallbackSrcCheckData))]
public async Task FallbackSrcContent_Matches_CDNContent(ScriptTag scriptTag)
{
var fallbackSrc = scriptTag.FallbackSrc
.TrimStart('~')
.TrimStart('/');
var cdnContent = await _httpClient.GetStringAsync(scriptTag.Src);
var fallbackSrcContent = GetFileContentFromArchive(scriptTag, fallbackSrc);
Assert.Equal(RemoveLineEndings(cdnContent), RemoveLineEndings(fallbackSrcContent));
}
public struct ScriptTag
{ {
public string Src; public string Src;
public string Integrity; public string Integrity;
public string FallbackSrc;
public string FileName; public string FileName;
public string Entry; public string Entry;
public override string ToString()
{
return $"{Src}, {Entry}";
}
} }
private static readonly Regex _scriptRegex = new Regex(@"<script[^>]*src=""(?'src'http[^""]+)""[^>]*integrity=""(?'integrity'[^""]+)""([^>]*)>", RegexOptions.Multiline); private static string GetFileContentFromArchive(ScriptTag scriptTag, string relativeFilePath)
private IEnumerable<ScriptTag> GetScriptTags(string zipFile)
{ {
var file = Path.Combine(_artifactsDir, scriptTag.FileName);
using (var zip = new ZipArchive(File.OpenRead(file), ZipArchiveMode.Read, leaveOpen: false))
{
var entry = zip.Entries
.Where(e => e.FullName.EndsWith(relativeFilePath, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();
if (entry != null)
{
using (var reader = new StreamReader(entry.Open()))
{
return reader.ReadToEnd();
}
}
}
return null;
}
private static List<ScriptTag> GetScriptTags(string zipFile)
{
var scriptTags = new List<ScriptTag>();
using (var zip = new ZipArchive(File.OpenRead(zipFile), ZipArchiveMode.Read, leaveOpen: false)) using (var zip = new ZipArchive(File.OpenRead(zipFile), ZipArchiveMode.Read, leaveOpen: false))
{ {
foreach (var entry in zip.Entries) foreach (var entry in zip.Entries)
{ {
if (string.Equals(".cshtml", Path.GetExtension(entry.Name), StringComparison.OrdinalIgnoreCase)) if (!string.Equals(".cshtml", Path.GetExtension(entry.Name), StringComparison.OrdinalIgnoreCase))
{ {
string contents; continue;
using (var reader = new StreamReader(entry.Open())) }
{
contents = reader.ReadToEnd();
}
var match = _scriptRegex.Match(contents); IHtmlDocument htmlDocument;
while (match != null && match != Match.Empty) var htmlParser = new HtmlParser();
using (var reader = new StreamReader(entry.Open()))
{
htmlDocument = htmlParser.Parse(entry.Open());
}
foreach (var scriptElement in htmlDocument.Scripts)
{
var fallbackSrcAttribute = scriptElement.Attributes
.FirstOrDefault(attr => string.Equals("asp-fallback-src", attr.Name, StringComparison.OrdinalIgnoreCase));
scriptTags.Add(new ScriptTag
{ {
var tag = new ScriptTag Src = scriptElement.Source,
{ Integrity = scriptElement.Integrity,
Src = match.Groups["src"].Value, FallbackSrc = fallbackSrcAttribute?.Value,
Integrity = match.Groups["integrity"].Value, FileName = Path.GetFileName(zipFile),
FileName = Path.GetFileName(zipFile), Entry = entry.FullName
Entry = entry.FullName, });
};
yield return tag;
_output.WriteLine($"Found script tag in {tag.FileName}:{tag.Entry}, src='{tag.Src}' integrity='{tag.Integrity}'");
match = match.NextMatch();
}
} }
} }
} }
return scriptTags;
} }
private static string GetSolutionDir() private static string GetSolutionDir()
@ -117,5 +181,15 @@ namespace Templates.Test
} }
return dir.FullName; return dir.FullName;
} }
private static string RemoveLineEndings(string originalString)
{
return originalString.Replace("\r\n", "").Replace("\n", "");
}
public void Dispose()
{
_httpClient.Dispose();
}
} }
} }

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework> <TargetFramework>netcoreapp2.2</TargetFramework>
<DefineConstants>$(DefineConstants);XPLAT</DefineConstants> <DefineConstants>$(DefineConstants);XPLAT</DefineConstants>
@ -9,6 +9,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AngleSharp" Version="$(AngleSharpPackageVersion)" />
<PackageReference Include="Internal.AspNetCore.Sdk" Version="$(InternalAspNetCoreSdkPackageVersion)" /> <PackageReference Include="Internal.AspNetCore.Sdk" Version="$(InternalAspNetCoreSdkPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Certificates.Generation.Sources" Version="$(MicrosoftAspNetCoreCertificatesGenerationSourcesPackageVersion)" /> <PackageReference Include="Microsoft.AspNetCore.Certificates.Generation.Sources" Version="$(MicrosoftAspNetCoreCertificatesGenerationSourcesPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" /> <PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />