|
// 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 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;
public class DefineStaticWebAssetEndpointsTest
{
[Theory]
[InlineData(StaticWebAsset.SourceTypes.Discovered)]
[InlineData(StaticWebAsset.SourceTypes.Computed)]
public void DefinesEndpointsForAssets(string sourceType)
{
var errorMessages = new List<string>();
var buildEngine = new Mock<IBuildEngine>();
buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
.Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message));
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]
public void CanDefineFingerprintedEndpoints()
{
var errorMessages = new List<string>();
var buildEngine = new Mock<IBuildEngine>();
buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
.Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message));
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]
public void CanDefineFingerprintedEndpoints_WithEmbeddedFingerprint()
{
var errorMessages = new List<string>();
var buildEngine = new Mock<IBuildEngine>();
buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
.Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message));
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]
public void DoesNotDefineNewEndpointsWhenAnExistingEndpointAlreadyExists()
{
var errorMessages = new List<string>();
var buildEngine = new Mock<IBuildEngine>();
buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
.Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message));
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]
public void ResolvesContentType_ForCompressedAssets()
{
var errorMessages = new List<string>();
var buildEngine = new Mock<IBuildEngine>();
buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
.Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message));
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<string, string>
{
["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]
public void ResolvesContentType_ForFingerprintedAssets()
{
var errorMessages = new List<string>();
var buildEngine = new Mock<IBuildEngine>();
buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
.Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message));
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<string, string>
{
["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]
public void Produces_TheExpectedEndpoint_ForExternalAssets()
{
var errorMessages = new List<string>();
var buildEngine = new Mock<IBuildEngine>();
buildEngine.Setup(e => e.LogErrorEvent(It.IsAny<BuildErrorEventArgs>()))
.Callback<BuildErrorEventArgs>(args => errorMessages.Add(args.Message));
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<string, string>
{
["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<string, string>
{
{ "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();
}
}
|