|
// 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.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;
public class ApplyCompressionNegotiationTest
{
[Fact]
public void AppliesContentNegotiationRules_ForExistingAssets()
{
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 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]
public void AppliesContentNegotiationRules_ForExistingAssets_WithFingerprints()
{
var now = DateTime.Now;
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));
List<ITaskItem> 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]
public void AppliesContentNegotiationRules_ToAllRelatedAssetEndpoints()
{
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 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]
public void AppliesContentNegotiationRules_IgnoresAlreadyProcessedEndpoints()
{
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 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]
public void AppliesContentNegotiationRules_ProcessesNewCompressedFormatsWhenAvailable()
{
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 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]
public void AppliesContentNegotiationRules_AddsVaryHeaderToEndpointsWithSameRouteButDifferentAssets()
{
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 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]
public void AppliesContentNegotiationRules_AttachesWeakETagAsResponseHeader()
{
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 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]
public void AppliesContentNegotiationRules_AttachesWeakETagAsEndpointProperty()
{
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 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]
public void AppliesContentNegotiationRules_DoesNotAttachETagWhenModeIsEmpty()
{
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 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");
}
}
|