Issue #2141 - Script & Link tag helpers should support generating cache-busting file version hash in URL.
This commit is contained in:
parent
0462dd6be3
commit
0e783ace58
|
|
@ -88,7 +88,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
|||
{
|
||||
if (!context.AllAttributes.ContainsKey(attribute) ||
|
||||
context.AllAttributes[attribute] == null ||
|
||||
string.IsNullOrWhiteSpace(context.AllAttributes[attribute] as string))
|
||||
(typeof(string).IsAssignableFrom(context.AllAttributes[attribute].GetType()) &&
|
||||
string.IsNullOrWhiteSpace(context.AllAttributes[attribute] as string)))
|
||||
{
|
||||
// Missing attribute!
|
||||
missingAttributes.Add(attribute);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.Security.Cryptography;
|
||||
using Microsoft.AspNet.FileProviders;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.WebUtilities;
|
||||
using Microsoft.Framework.Caching.Memory;
|
||||
using Microsoft.Framework.Internal;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides version hash for a specified file.
|
||||
/// </summary>
|
||||
public class FileVersionProvider
|
||||
{
|
||||
private const string VersionKey = "v";
|
||||
private readonly IFileProvider _fileProvider;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly PathString _requestPathBase;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="FileVersionProvider"/>.
|
||||
/// </summary>
|
||||
/// <param name="fileProvider">The file provider to get and watch files.</param>
|
||||
/// <param name="applicationName">Name of the application.</param>
|
||||
/// <param name="cache"><see cref="IMemoryCache"/> where versioned urls of files are cached.</param>
|
||||
public FileVersionProvider(
|
||||
[NotNull] IFileProvider fileProvider,
|
||||
[NotNull] IMemoryCache cache,
|
||||
[NotNull] PathString requestPathBase)
|
||||
{
|
||||
_fileProvider = fileProvider;
|
||||
_cache = cache;
|
||||
_requestPathBase = requestPathBase;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds version query parameter to the specified file path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the file to which version should be added.</param>
|
||||
/// <returns>Path containing the version query string.</returns>
|
||||
/// <remarks>
|
||||
/// The version query string is appended as with the key "v".
|
||||
/// </remarks>
|
||||
public string AddFileVersionToPath([NotNull] string path)
|
||||
{
|
||||
var resolvedPath = path;
|
||||
var fileInfo = _fileProvider.GetFileInfo(resolvedPath);
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
if (_requestPathBase.HasValue &&
|
||||
resolvedPath.StartsWith(_requestPathBase.Value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
resolvedPath = resolvedPath.Substring(_requestPathBase.Value.Length);
|
||||
fileInfo = _fileProvider.GetFileInfo(resolvedPath);
|
||||
}
|
||||
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
// if the file is not in the current server.
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return _cache.GetOrSet(path, cacheGetOrSetContext =>
|
||||
{
|
||||
var trigger = _fileProvider.Watch(resolvedPath);
|
||||
cacheGetOrSetContext.AddExpirationTrigger(trigger);
|
||||
return QueryHelpers.AddQueryString(path, VersionKey, GetHashForFile(fileInfo));
|
||||
});
|
||||
}
|
||||
|
||||
private string GetHashForFile(IFileInfo fileInfo)
|
||||
{
|
||||
using (var sha256 = SHA256.Create())
|
||||
{
|
||||
var hash = sha256.ComputeHash(fileInfo.CreateReadStream());
|
||||
return WebEncoders.Base64UrlEncode(hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,12 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.AspNet.Hosting;
|
||||
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.Framework.Caching.Memory;
|
||||
using Microsoft.Framework.Logging;
|
||||
using Microsoft.Framework.Runtime;
|
||||
using Microsoft.Framework.WebEncoders;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
|
|
@ -29,6 +29,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
[TargetElement("link", Attributes = FallbackTestClassAttributeName)]
|
||||
[TargetElement("link", Attributes = FallbackTestPropertyAttributeName)]
|
||||
[TargetElement("link", Attributes = FallbackTestValueAttributeName)]
|
||||
[TargetElement("link", Attributes = FileVersionAttributeName)]
|
||||
public class LinkTagHelper : TagHelper
|
||||
{
|
||||
private const string HrefIncludeAttributeName = "asp-href-include";
|
||||
|
|
@ -40,8 +41,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
private const string FallbackTestPropertyAttributeName = "asp-fallback-test-property";
|
||||
private const string FallbackTestValueAttributeName = "asp-fallback-test-value";
|
||||
private const string FallbackJavaScriptResourceName = "compiler/resources/LinkTagHelper_FallbackJavaScript.js";
|
||||
private const string FileVersionAttributeName = "asp-file-version";
|
||||
private const string HrefAttributeName = "href";
|
||||
|
||||
private FileVersionProvider _fileVersionProvider;
|
||||
|
||||
private static readonly ModeAttributes<Mode>[] ModeDetails = new[] {
|
||||
// Regular src with file version alone
|
||||
ModeAttributes.Create(Mode.FileVersion, new[] { FileVersionAttributeName }),
|
||||
// Globbed Href (include only) no static href
|
||||
ModeAttributes.Create(Mode.GlobbedHref, new [] { HrefIncludeAttributeName }),
|
||||
// Globbed Href (include & exclude), no static href
|
||||
|
|
@ -76,15 +83,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
|
||||
private enum Mode
|
||||
{
|
||||
/// <summary>
|
||||
/// Just adding a file version for the generated urls.
|
||||
/// </summary>
|
||||
FileVersion = 0,
|
||||
/// <summary>
|
||||
/// Just performing file globbing search for the href, rendering a separate <link> for each match.
|
||||
/// </summary>
|
||||
GlobbedHref = 0,
|
||||
GlobbedHref = 1,
|
||||
/// <summary>
|
||||
/// Rendering a fallback block if primary stylesheet fails to load. Will also do globbing for both the
|
||||
/// primary and fallback hrefs if the appropriate properties are set.
|
||||
/// </summary>
|
||||
Fallback = 1,
|
||||
Fallback = 2,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -108,6 +119,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
[HtmlAttributeName(FallbackHrefAttributeName)]
|
||||
public string FallbackHref { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Value indicating if file version should be appended to the href urls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <c>true</c> then a query string "v" with the encoded content of the file is added.
|
||||
/// </remarks>
|
||||
[HtmlAttributeName(FileVersionAttributeName)]
|
||||
public bool? FileVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A comma separated list of globbed file patterns of CSS stylesheets to fallback to in the case the primary
|
||||
/// one fails.
|
||||
|
|
@ -149,7 +169,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
[HtmlAttributeName(FallbackTestValueAttributeName)]
|
||||
public string FallbackTestValue { get; set; }
|
||||
|
||||
// Properties are protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
|
||||
// Properties are protected to ensure subclasses are correctly activated.
|
||||
// Internal for ease of use when testing.
|
||||
[Activate]
|
||||
protected internal ILoggerFactory LoggerFactory { get; set; }
|
||||
|
||||
|
|
@ -194,9 +215,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
|
||||
var builder = new DefaultTagHelperContent();
|
||||
|
||||
if (mode == Mode.Fallback && string.IsNullOrEmpty(HrefInclude))
|
||||
if (mode == Mode.Fallback && string.IsNullOrEmpty(HrefInclude) || mode == Mode.FileVersion)
|
||||
{
|
||||
// No globbing to do, just build a <link /> tag to match the original one in the source file
|
||||
// No globbing to do, just build a <link /> tag to match the original one in the source file.
|
||||
// Or just add file version to the link tag.
|
||||
BuildLinkTag(attributes, builder);
|
||||
}
|
||||
else
|
||||
|
|
@ -218,14 +240,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
{
|
||||
// Build a <link /> tag for each matched href as well as the original one in the source file
|
||||
string staticHref;
|
||||
attributes.TryGetValue("href", out staticHref);
|
||||
attributes.TryGetValue(HrefAttributeName, out staticHref);
|
||||
|
||||
EnsureGlobbingUrlBuilder();
|
||||
var urls = GlobbingUrlBuilder.BuildUrlList(staticHref, HrefInclude, HrefExclude);
|
||||
|
||||
foreach (var url in urls)
|
||||
{
|
||||
attributes["href"] = HtmlEncoder.HtmlEncode(url);
|
||||
attributes[HrefAttributeName] = url;
|
||||
BuildLinkTag(attributes, builder);
|
||||
}
|
||||
}
|
||||
|
|
@ -233,10 +255,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
private void BuildFallbackBlock(TagHelperContent builder)
|
||||
{
|
||||
EnsureGlobbingUrlBuilder();
|
||||
var fallbackHrefs = GlobbingUrlBuilder.BuildUrlList(FallbackHref, FallbackHrefInclude, FallbackHrefExclude);
|
||||
var fallbackHrefs =
|
||||
GlobbingUrlBuilder.BuildUrlList(FallbackHref, FallbackHrefInclude, FallbackHrefExclude).ToArray();
|
||||
|
||||
if (fallbackHrefs.Any())
|
||||
if (fallbackHrefs.Length > 0)
|
||||
{
|
||||
if (ShouldAddFileVersion())
|
||||
{
|
||||
for (var i=0; i < fallbackHrefs.Length; i++)
|
||||
{
|
||||
fallbackHrefs[i] = _fileVersionProvider.AddFileVersionToPath(fallbackHrefs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append(Environment.NewLine);
|
||||
|
||||
// Build the <meta /> tag that's used to test for the presence of the stylesheet
|
||||
|
|
@ -269,17 +300,46 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
}
|
||||
}
|
||||
|
||||
private static void BuildLinkTag(IDictionary<string, string> attributes, TagHelperContent builder)
|
||||
private void EnsureFileVersionProvider()
|
||||
{
|
||||
if (_fileVersionProvider == null)
|
||||
{
|
||||
_fileVersionProvider = new FileVersionProvider(
|
||||
HostingEnvironment.WebRootFileProvider,
|
||||
Cache,
|
||||
ViewContext.HttpContext.Request.PathBase);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildLinkTag(IDictionary<string, string> attributes, TagHelperContent builder)
|
||||
{
|
||||
EnsureFileVersionProvider();
|
||||
builder.Append("<link ");
|
||||
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
builder.Append(
|
||||
string.Format(CultureInfo.InvariantCulture, "{0}=\"{1}\" ", attribute.Key, attribute.Value));
|
||||
var attributeValue = attribute.Value;
|
||||
if (string.Equals(attribute.Key, HrefAttributeName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
attributeValue = HtmlEncoder.HtmlEncode(
|
||||
ShouldAddFileVersion() ?
|
||||
_fileVersionProvider.AddFileVersionToPath(attributeValue) :
|
||||
attributeValue);
|
||||
}
|
||||
|
||||
builder
|
||||
.Append(attribute.Key)
|
||||
.Append("=\"")
|
||||
.Append(attributeValue)
|
||||
.Append("\" ");
|
||||
}
|
||||
|
||||
builder.Append("/>");
|
||||
}
|
||||
|
||||
private bool ShouldAddFileVersion()
|
||||
{
|
||||
return FileVersion ?? false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Hosting;
|
||||
|
|
@ -11,6 +10,7 @@ using Microsoft.AspNet.Mvc.TagHelpers.Internal;
|
|||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.Framework.Caching.Memory;
|
||||
using Microsoft.Framework.Logging;
|
||||
using Microsoft.Framework.Runtime;
|
||||
using Microsoft.Framework.WebEncoders;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers
|
||||
|
|
@ -27,6 +27,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
[TargetElement("script", Attributes = FallbackSrcIncludeAttributeName)]
|
||||
[TargetElement("script", Attributes = FallbackSrcExcludeAttributeName)]
|
||||
[TargetElement("script", Attributes = FallbackTestExpressionAttributeName)]
|
||||
[TargetElement("script", Attributes = FileVersionAttributeName)]
|
||||
public class ScriptTagHelper : TagHelper
|
||||
{
|
||||
private const string SrcIncludeAttributeName = "asp-src-include";
|
||||
|
|
@ -36,8 +37,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
private const string FallbackSrcExcludeAttributeName = "asp-fallback-src-exclude";
|
||||
private const string FallbackTestExpressionAttributeName = "asp-fallback-test";
|
||||
private const string SrcAttributeName = "src";
|
||||
private const string FileVersionAttributeName = "asp-file-version";
|
||||
|
||||
private FileVersionProvider _fileVersionProvider;
|
||||
|
||||
private static readonly ModeAttributes<Mode>[] ModeDetails = new[] {
|
||||
// Regular src with file version alone
|
||||
ModeAttributes.Create(Mode.FileVersion, new[] { FileVersionAttributeName }),
|
||||
// Globbed src (include only)
|
||||
ModeAttributes.Create(Mode.GlobbedSrc, new [] { SrcIncludeAttributeName }),
|
||||
// Globbed src (include & exclude)
|
||||
|
|
@ -66,15 +72,19 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
|
||||
private enum Mode
|
||||
{
|
||||
/// <summary>
|
||||
/// Just adding a file version for the generated urls.
|
||||
/// </summary>
|
||||
FileVersion = 0,
|
||||
/// <summary>
|
||||
/// Just performing file globbing search for the src, rendering a separate <script> for each match.
|
||||
/// </summary>
|
||||
GlobbedSrc = 0,
|
||||
GlobbedSrc = 1,
|
||||
/// <summary>
|
||||
/// Rendering a fallback block if primary javascript fails to load. Will also do globbing for both the
|
||||
/// primary and fallback srcs if the appropriate properties are set.
|
||||
/// </summary>
|
||||
Fallback = 1
|
||||
Fallback = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -98,6 +108,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
[HtmlAttributeName(FallbackSrcAttributeName)]
|
||||
public string FallbackSrc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Value indicating if file version should be appended to src urls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A query string "v" with the encoded content of the file is added.
|
||||
/// </remarks>
|
||||
[HtmlAttributeName(FileVersionAttributeName)]
|
||||
public bool? FileVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A comma separated list of globbed file patterns of JavaScript scripts to fallback to in the case the
|
||||
/// primary one fails.
|
||||
|
|
@ -137,12 +156,12 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
[Activate]
|
||||
protected internal IMemoryCache Cache { get; set; }
|
||||
|
||||
// Internal for ease of use when testing.
|
||||
protected internal GlobbingUrlBuilder GlobbingUrlBuilder { get; set; }
|
||||
|
||||
[Activate]
|
||||
protected internal IHtmlEncoder HtmlEncoder { get; set; }
|
||||
|
||||
// Internal for ease of use when testing.
|
||||
protected internal GlobbingUrlBuilder GlobbingUrlBuilder { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
|
|
@ -167,9 +186,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var builder = new DefaultTagHelperContent();
|
||||
var originalContent = await context.GetChildContentAsync();
|
||||
|
||||
if (mode == Mode.Fallback && string.IsNullOrEmpty(SrcInclude))
|
||||
if (mode == Mode.Fallback && string.IsNullOrEmpty(SrcInclude) || mode == Mode.FileVersion)
|
||||
{
|
||||
// No globbing to do, just build a <script /> tag to match the original one in the source file
|
||||
// Or just add file version to the script tag.
|
||||
BuildScriptTag(originalContent, attributes, builder);
|
||||
}
|
||||
else
|
||||
|
|
@ -194,14 +214,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
{
|
||||
// Build a <script> tag for each matched src as well as the original one in the source file
|
||||
string staticSrc;
|
||||
attributes.TryGetValue("src", out staticSrc);
|
||||
attributes.TryGetValue(SrcAttributeName, out staticSrc);
|
||||
|
||||
EnsureGlobbingUrlBuilder();
|
||||
var urls = GlobbingUrlBuilder.BuildUrlList(staticSrc, SrcInclude, SrcExclude);
|
||||
|
||||
foreach (var url in urls)
|
||||
{
|
||||
attributes["src"] = HtmlEncoder.HtmlEncode(url);
|
||||
attributes[SrcAttributeName] = url;
|
||||
var content =
|
||||
string.Equals(url, staticSrc, StringComparison.OrdinalIgnoreCase) ? originalContent : null;
|
||||
BuildScriptTag(content, attributes, builder);
|
||||
|
|
@ -211,6 +231,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
private void BuildFallbackBlock(IDictionary<string, string> attributes, DefaultTagHelperContent builder)
|
||||
{
|
||||
EnsureGlobbingUrlBuilder();
|
||||
EnsureFileVersionProvider();
|
||||
|
||||
var fallbackSrcs = GlobbingUrlBuilder.BuildUrlList(FallbackSrc, FallbackSrcInclude, FallbackSrcExclude);
|
||||
|
||||
|
|
@ -226,9 +247,13 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
{
|
||||
builder.Append("<script");
|
||||
|
||||
if (!attributes.ContainsKey("src"))
|
||||
if (!attributes.ContainsKey(SrcAttributeName))
|
||||
{
|
||||
AppendSrc(builder, "src", src);
|
||||
AppendAttribute(
|
||||
builder,
|
||||
SrcAttributeName,
|
||||
HtmlEncoder.HtmlEncode(ShouldAddFileVersion() ? _fileVersionProvider.AddFileVersionToPath(src) : src),
|
||||
escapteQuotes: true);
|
||||
}
|
||||
|
||||
foreach (var attribute in attributes)
|
||||
|
|
@ -238,21 +263,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var encodedKey = JavaScriptStringEncoder.Default.JavaScriptStringEncode(attribute.Key);
|
||||
var encodedValue = JavaScriptStringEncoder.Default.JavaScriptStringEncode(attribute.Value);
|
||||
|
||||
builder.Append(string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" {0}=\\\"{1}\\\"",
|
||||
encodedKey,
|
||||
encodedValue));
|
||||
AppendAttribute(builder, encodedKey, encodedValue, escapteQuotes: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
AppendSrc(builder, attribute.Key, src);
|
||||
AppendAttribute(
|
||||
builder,
|
||||
attribute.Key,
|
||||
HtmlEncoder.HtmlEncode(
|
||||
ShouldAddFileVersion() ? _fileVersionProvider.AddFileVersionToPath(src) : src),
|
||||
escapteQuotes: true);
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append("><\\/script>");
|
||||
}
|
||||
|
||||
|
||||
builder.Append("\"));</script>");
|
||||
}
|
||||
}
|
||||
|
|
@ -268,17 +294,37 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
}
|
||||
}
|
||||
|
||||
private static void BuildScriptTag(
|
||||
private void EnsureFileVersionProvider()
|
||||
{
|
||||
if (_fileVersionProvider == null)
|
||||
{
|
||||
_fileVersionProvider = new FileVersionProvider(
|
||||
HostingEnvironment.WebRootFileProvider,
|
||||
Cache,
|
||||
ViewContext.HttpContext.Request.PathBase);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildScriptTag(
|
||||
TagHelperContent content,
|
||||
IDictionary<string, string> attributes,
|
||||
TagHelperContent builder)
|
||||
{
|
||||
EnsureFileVersionProvider();
|
||||
builder.Append("<script");
|
||||
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
builder.Append(
|
||||
string.Format(CultureInfo.InvariantCulture, " {0}=\"{1}\"", attribute.Key, attribute.Value));
|
||||
string attributeValue = attribute.Value;
|
||||
if (string.Equals(attribute.Key, SrcAttributeName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
attributeValue = HtmlEncoder.HtmlEncode(
|
||||
ShouldAddFileVersion() ?
|
||||
_fileVersionProvider.AddFileVersionToPath(attribute.Value) :
|
||||
attributeValue);
|
||||
}
|
||||
|
||||
AppendAttribute(builder, attribute.Key, attributeValue, escapteQuotes: false);
|
||||
}
|
||||
|
||||
builder.Append(">")
|
||||
|
|
@ -286,15 +332,20 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
.Append("</script>");
|
||||
}
|
||||
|
||||
private void AppendSrc(TagHelperContent content, string srcKey, string srcValue)
|
||||
private bool ShouldAddFileVersion()
|
||||
{
|
||||
// Append src attribute in the original place and replace the content the fallback content
|
||||
// No need to encode the key because we know it is "src".
|
||||
content.Append(" ")
|
||||
.Append(srcKey)
|
||||
.Append("=\\\"")
|
||||
.Append(HtmlEncoder.HtmlEncode(srcValue))
|
||||
.Append("\\\"");
|
||||
return FileVersion ?? false;
|
||||
}
|
||||
|
||||
private void AppendAttribute(TagHelperContent content, string key, string value, bool escapteQuotes)
|
||||
{
|
||||
content
|
||||
.Append(" ")
|
||||
.Append(key)
|
||||
.Append(escapteQuotes ? "=\\\"" : "=\"")
|
||||
.Append(value)
|
||||
.Append(escapteQuotes ? "\\\"" : "\"");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,19 @@
|
|||
|
||||
<!-- Fallback with missing attribute -->
|
||||
<link href="/site.min.css" rel="stylesheet" data-extra="test" />
|
||||
|
||||
<!-- Plain link tag with file version -->
|
||||
<link href="/site.css?v=XY7YsMemPf8AGU4SIX9ED9eOjK1LOQWu2dmCNmh-pQc" rel="stylesheet" />
|
||||
|
||||
<!-- Globbed link tag with existing file and file version -->
|
||||
<link rel="stylesheet" href="/site.css?v=XY7YsMemPf8AGU4SIX9ED9eOjK1LOQWu2dmCNmh-pQc" />
|
||||
|
||||
<!-- Fallback with file version -->
|
||||
<link href="/site.min.css" rel="stylesheet" data-extra="test" />
|
||||
<meta name="x-stylesheet-fallback-test" class="hidden" /><script>!function(a,b,c){var d,e=document,f=e.getElementsByTagName("SCRIPT"),g=f[f.length-1].previousElementSibling,h=e.defaultView&&e.defaultView.getComputedStyle?e.defaultView.getComputedStyle(g):g.currentStyle;if(h&&h[a]!==b)for(d=0;d<c.length;d++)e.write('<link rel="stylesheet" href="'+c[d]+'"/>')}("visibility","hidden",["\/site.css?v=XY7YsMemPf8AGU4SIX9ED9eOjK1LOQWu2dmCNmh-pQc"]);</script>
|
||||
|
||||
<!-- Globbed link tag with existing file, static href and file version -->
|
||||
<link href="/site.css?v=XY7YsMemPf8AGU4SIX9ED9eOjK1LOQWu2dmCNmh-pQc" rel="stylesheet" /><link href="/sub/site2.css?v=30cxPex0tA9xEatW7f1Qhnn8tVLAHgE6xwIZhESq0y0" rel="stylesheet" /><link href="/sub/site3.css?v=fSxxOr1Q4Dq2uPuzlju5UYGuK0SKABI-ghvaIGEsZDc" rel="stylesheet" /><link href="/sub/site3.min.css?v=s8JMmAZxBn0dzuhRtQ0wgOvNBK4XRJRWEC2wfzsVF9M" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
|||
|
|
@ -92,5 +92,25 @@
|
|||
<script src="/site.js">
|
||||
// Globbed script tag with existing file and static src should dedupe
|
||||
</script>
|
||||
|
||||
<script src="/blank.js">
|
||||
// TagHelper script with comment in body, and file version.
|
||||
</script>
|
||||
<script>(false||document.write("<script src=\"/site.js?v=jx1PJjLX32-xgQQx2BxnckU9QH9DVKkm4-M5bSK869I\"><\/script>"));</script>
|
||||
|
||||
<script src="/blank.js">
|
||||
// Fallback to globbed src with file version.
|
||||
</script>
|
||||
<script>(false||document.write("<script src=\"/site.js?v=jx1PJjLX32-xgQQx2BxnckU9QH9DVKkm4-M5bSK869I\"><\/script>"));</script>
|
||||
|
||||
<script src="/site.js?v=jx1PJjLX32-xgQQx2BxnckU9QH9DVKkm4-M5bSK869I">
|
||||
// Regular script with comment in body, and file version.
|
||||
</script>
|
||||
|
||||
<!-- Globbed script tag with existing files and version -->
|
||||
<script src="/site.js?v=jx1PJjLX32-xgQQx2BxnckU9QH9DVKkm4-M5bSK869I"></script><script src="/sub/site2.js?v=pwJaxaQxnb-rPAdF2JlAp4xiPNq1XuJFd6TyOOfNF-0"></script><script src="/sub/site3.js?v=lmeAMiqm76lnGyqHhu6PIBHAC0Vt46mgVB_KaG_gGdA"></script>
|
||||
|
||||
<!-- Globbed script tag with existing file, exclude and version -->
|
||||
<script src="/site.js?v=jx1PJjLX32-xgQQx2BxnckU9QH9DVKkm4-M5bSK869I"></script><script src="/sub/site2.js?v=pwJaxaQxnb-rPAdF2JlAp4xiPNq1XuJFd6TyOOfNF-0"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,214 @@
|
|||
// Copyright (c) Microsoft Open Technologies, Inc. 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.IO;
|
||||
using System.Text;
|
||||
using Microsoft.AspNet.FileProviders;
|
||||
using Microsoft.AspNet.Hosting;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.Framework.Caching.Memory;
|
||||
using Microsoft.Framework.Expiration.Interfaces;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.TagHelpers.Internal
|
||||
{
|
||||
public class FileVersionProviderTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("/hello/world", "/hello/world?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk")]
|
||||
[InlineData("/hello/world?q=test", "/hello/world?q=test&v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk")]
|
||||
[InlineData("/hello/world?q=foo&bar", "/hello/world?q=foo&bar&v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk")]
|
||||
public void AddsVersionToFiles_WhenCacheIsAbsent(string filePath, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var hostingEnvironment = GetMockHostingEnvironment(filePath);
|
||||
var fileVersionProvider = new FileVersionProvider(
|
||||
hostingEnvironment.WebRootFileProvider,
|
||||
GetMockCache(),
|
||||
GetRequestPathBase());
|
||||
|
||||
// Act
|
||||
var result = fileVersionProvider.AddFileVersionToPath(filePath);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/testApp/hello/world", true, "/testApp")]
|
||||
[InlineData("/testApp/foo/bar/hello/world", true, "/testApp/foo/bar")]
|
||||
[InlineData("/test/testApp/hello/world", false, "/testApp")]
|
||||
public void AddsVersionToFiles_PathContainingAppName(
|
||||
string filePath,
|
||||
bool pathStartsWithAppBase,
|
||||
string requestPathBase)
|
||||
{
|
||||
// Arrange
|
||||
var hostingEnvironment = GetMockHostingEnvironment(filePath, pathStartsWithAppBase);
|
||||
var fileVersionProvider = new FileVersionProvider(
|
||||
hostingEnvironment.WebRootFileProvider,
|
||||
GetMockCache(),
|
||||
GetRequestPathBase(requestPathBase));
|
||||
|
||||
// Act
|
||||
var result = fileVersionProvider.AddFileVersionToPath(filePath);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(filePath + "?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotAddVersion_IfFileNotFound()
|
||||
{
|
||||
// Arrange
|
||||
var filePath = "http://contoso.com/hello/world";
|
||||
var hostingEnvironment = GetMockHostingEnvironment(filePath, false, true);
|
||||
var fileVersionProvider = new FileVersionProvider(
|
||||
hostingEnvironment.WebRootFileProvider,
|
||||
GetMockCache(),
|
||||
GetRequestPathBase());
|
||||
|
||||
// Act
|
||||
var result = fileVersionProvider.AddFileVersionToPath(filePath);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("http://contoso.com/hello/world", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReturnsValueFromCache()
|
||||
{
|
||||
// Arrange
|
||||
var filePath = "/hello/world";
|
||||
var hostingEnvironment = GetMockHostingEnvironment(filePath);
|
||||
var fileVersionProvider = new FileVersionProvider(
|
||||
hostingEnvironment.WebRootFileProvider,
|
||||
GetMockCache("FromCache"),
|
||||
GetRequestPathBase());
|
||||
|
||||
// Act
|
||||
var result = fileVersionProvider.AddFileVersionToPath(filePath);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("FromCache", result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("/hello/world", "/hello/world", null)]
|
||||
[InlineData("/testApp/hello/world", "/hello/world", "/testApp")]
|
||||
[InlineData("/hello/world", "/hello/world", null)]
|
||||
public void SetsValueInCache(string filePath, string watchPath, string requestPathBase)
|
||||
{
|
||||
// Arrange
|
||||
var trigger = new Mock<IExpirationTrigger>();
|
||||
var hostingEnvironment = GetMockHostingEnvironment(filePath, requestPathBase != null);
|
||||
Mock.Get(hostingEnvironment.WebRootFileProvider)
|
||||
.Setup(f => f.Watch(watchPath)).Returns(trigger.Object);
|
||||
|
||||
object cacheValue = null;
|
||||
var cache = new Mock<IMemoryCache>();
|
||||
cache.CallBase = true;
|
||||
cache.Setup(c => c.TryGetValue(It.IsAny<string>(), It.IsAny<IEntryLink>(), out cacheValue))
|
||||
.Returns(cacheValue != null);
|
||||
var cacheSetContext = new Mock<ICacheSetContext>();
|
||||
cacheSetContext.Setup(c => c.AddExpirationTrigger(trigger.Object)).Verifiable();
|
||||
cache.Setup(c => c.Set(
|
||||
/*key*/ filePath,
|
||||
/*link*/ It.IsAny<IEntryLink>(),
|
||||
/*state*/ It.IsAny<object>(),
|
||||
/*create*/ It.IsAny<Func<ICacheSetContext, object>>()))
|
||||
.Returns<string, IEntryLink, object, Func<ICacheSetContext, object>>(
|
||||
(key, link, state, create) =>
|
||||
{
|
||||
cacheSetContext.Setup(c => c.State).Returns(state);
|
||||
return create(cacheSetContext.Object);
|
||||
})
|
||||
.Verifiable();
|
||||
var fileVersionProvider = new FileVersionProvider(
|
||||
hostingEnvironment.WebRootFileProvider,
|
||||
cache.Object,
|
||||
GetRequestPathBase(requestPathBase));
|
||||
|
||||
// Act
|
||||
var result = fileVersionProvider.AddFileVersionToPath(filePath);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(filePath + "?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk", result);
|
||||
cacheSetContext.VerifyAll();
|
||||
cache.VerifyAll();
|
||||
}
|
||||
|
||||
private IHostingEnvironment GetMockHostingEnvironment(
|
||||
string filePath,
|
||||
bool pathStartsWithAppName = false,
|
||||
bool fileDoesNotExist = false)
|
||||
{
|
||||
var existingMockFile = new Mock<IFileInfo>();
|
||||
existingMockFile.SetupGet(f => f.Exists).Returns(true);
|
||||
existingMockFile
|
||||
.Setup(m => m.CreateReadStream())
|
||||
.Returns(() => new MemoryStream(Encoding.UTF8.GetBytes("Hello World!")));
|
||||
|
||||
var nonExistingMockFile = new Mock<IFileInfo>();
|
||||
nonExistingMockFile.SetupGet(f => f.Exists).Returns(false);
|
||||
nonExistingMockFile
|
||||
.Setup(m => m.CreateReadStream())
|
||||
.Returns(() => new MemoryStream(Encoding.UTF8.GetBytes("Hello World!")));
|
||||
|
||||
var mockFileProvider = new Mock<IFileProvider>();
|
||||
if (pathStartsWithAppName)
|
||||
{
|
||||
mockFileProvider.Setup(fp => fp.GetFileInfo(filePath)).Returns(nonExistingMockFile.Object);
|
||||
mockFileProvider.Setup(fp => fp.GetFileInfo(It.Is<string>(str => str != filePath)))
|
||||
.Returns(existingMockFile.Object);
|
||||
}
|
||||
else
|
||||
{
|
||||
mockFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny<string>()))
|
||||
.Returns(fileDoesNotExist? nonExistingMockFile.Object : existingMockFile.Object);
|
||||
}
|
||||
|
||||
var hostingEnvironment = new Mock<IHostingEnvironment>();
|
||||
hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(mockFileProvider.Object);
|
||||
|
||||
return hostingEnvironment.Object;
|
||||
}
|
||||
|
||||
private static IMemoryCache GetMockCache(object result = null)
|
||||
{
|
||||
var cache = new Mock<IMemoryCache>();
|
||||
cache.CallBase = true;
|
||||
cache.Setup(c => c.TryGetValue(It.IsAny<string>(), It.IsAny<IEntryLink>(), out result))
|
||||
.Returns(result != null);
|
||||
|
||||
var cacheSetContext = new Mock<ICacheSetContext>();
|
||||
cacheSetContext.Setup(c => c.AddExpirationTrigger(It.IsAny<IExpirationTrigger>()));
|
||||
cache
|
||||
.Setup(
|
||||
c => c.Set(
|
||||
/*key*/ It.IsAny<string>(),
|
||||
/*link*/ It.IsAny<IEntryLink>(),
|
||||
/*state*/ It.IsAny<object>(),
|
||||
/*create*/ It.IsAny<Func<ICacheSetContext, object>>()))
|
||||
.Returns((
|
||||
string input,
|
||||
IEntryLink entryLink,
|
||||
object state,
|
||||
Func<ICacheSetContext, object> create) =>
|
||||
{
|
||||
{
|
||||
cacheSetContext.Setup(c => c.State).Returns(state);
|
||||
return create(cacheSetContext.Object);
|
||||
}
|
||||
});
|
||||
return cache.Object;
|
||||
}
|
||||
|
||||
private static PathString GetRequestPathBase(string requestPathBase = null)
|
||||
{
|
||||
return new PathString(requestPathBase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.FileProviders;
|
||||
using Microsoft.AspNet.Hosting;
|
||||
|
|
@ -14,7 +15,10 @@ using Microsoft.AspNet.Mvc.Rendering;
|
|||
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.Framework.Caching.Memory;
|
||||
using Microsoft.Framework.Expiration.Interfaces;
|
||||
using Microsoft.Framework.Logging;
|
||||
using Microsoft.Framework.Runtime;
|
||||
using Microsoft.Framework.WebEncoders;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
|
@ -82,6 +86,79 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
tagHelper.FallbackTestProperty = "visibility";
|
||||
tagHelper.FallbackTestValue = "hidden";
|
||||
}
|
||||
},
|
||||
// File Version
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-file-version"] = "true"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.FileVersion = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-href-include"] = "*.css",
|
||||
["asp-file-version"] = "true"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.HrefInclude = "*.css";
|
||||
tagHelper.FileVersion = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-href-include"] = "*.css",
|
||||
["asp-href-exclude"] = "*.min.css",
|
||||
["asp-file-version"] = "true"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.HrefInclude = "*.css";
|
||||
tagHelper.HrefExclude = "*.min.css";
|
||||
tagHelper.FileVersion = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-fallback-href"] = "test.css",
|
||||
["asp-fallback-test-class"] = "hidden",
|
||||
["asp-fallback-test-property"] = "visibility",
|
||||
["asp-fallback-test-value"] = "hidden",
|
||||
["asp-file-version"] = "true"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.FallbackHref = "test.css";
|
||||
tagHelper.FallbackTestClass = "hidden";
|
||||
tagHelper.FallbackTestProperty = "visibility";
|
||||
tagHelper.FallbackTestValue = "hidden";
|
||||
tagHelper.FileVersion = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-fallback-href-include"] = "*.css",
|
||||
["asp-fallback-test-class"] = "hidden",
|
||||
["asp-fallback-test-property"] = "visibility",
|
||||
["asp-fallback-test-value"] = "hidden",
|
||||
["asp-file-version"] = "true"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.FallbackHrefInclude = "*.css";
|
||||
tagHelper.FallbackTestClass = "hidden";
|
||||
tagHelper.FallbackTestProperty = "visibility";
|
||||
tagHelper.FallbackTestValue = "hidden";
|
||||
tagHelper.FileVersion = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -105,6 +182,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
Cache = MakeCache()
|
||||
};
|
||||
setProperties(helper);
|
||||
|
||||
|
|
@ -151,7 +229,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
FallbackHref = "test.css",
|
||||
FallbackTestClass = "hidden",
|
||||
FallbackTestProperty = "visibility",
|
||||
FallbackTestValue = "hidden"
|
||||
FallbackTestValue = "hidden",
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -250,7 +329,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
{
|
||||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext
|
||||
ViewContext = viewContext,
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
setProperties(helper);
|
||||
|
||||
|
|
@ -275,7 +355,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
{
|
||||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext
|
||||
ViewContext = viewContext,
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -315,7 +396,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
HrefInclude = "**/*.css"
|
||||
HrefInclude = "**/*.css",
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -355,7 +437,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
HrefInclude = "**/*.css"
|
||||
HrefInclude = "**/*.css",
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -366,9 +449,134 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
"<link href=\"HtmlEncode[[/base.css]]\" rel=\"stylesheet\" />", output.Content.GetContent());
|
||||
}
|
||||
|
||||
private static ViewContext MakeViewContext()
|
||||
[Fact]
|
||||
public void RendersLinkTags_AddsFileVersion()
|
||||
{
|
||||
// Arrange
|
||||
var context = MakeTagHelperContext(
|
||||
attributes: new Dictionary<string, object>
|
||||
{
|
||||
["href"] = "/css/site.css",
|
||||
["rel"] = "stylesheet",
|
||||
["asp-file-version"] = "true"
|
||||
});
|
||||
var output = MakeTagHelperOutput("link", attributes: new Dictionary<string, string>
|
||||
{
|
||||
["href"] = "/css/site.css",
|
||||
["rel"] = "stylesheet"
|
||||
});
|
||||
var logger = new Mock<ILogger<LinkTagHelper>>();
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var helper = new LinkTagHelper
|
||||
{
|
||||
HtmlEncoder = new TestHtmlEncoder(),
|
||||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
HrefInclude = "**/*.css",
|
||||
FileVersion = true,
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
helper.Process(context, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("<link href=\"HtmlEncode[[/css/site.css?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk]]\"" +
|
||||
" rel=\"stylesheet\" />", output.Content.GetContent());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersLinkTags_AddsFileVersion_WithRequestPathBase()
|
||||
{
|
||||
// Arrange
|
||||
var context = MakeTagHelperContext(
|
||||
attributes: new Dictionary<string, object>
|
||||
{
|
||||
["href"] = "/bar/css/site.css",
|
||||
["rel"] = "stylesheet",
|
||||
["asp-file-version"] = "true"
|
||||
});
|
||||
var output = MakeTagHelperOutput("link", attributes: new Dictionary<string, string>
|
||||
{
|
||||
["href"] = "/bar/css/site.css",
|
||||
["rel"] = "stylesheet"
|
||||
});
|
||||
var logger = new Mock<ILogger<LinkTagHelper>>();
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext("/bar");
|
||||
var helper = new LinkTagHelper
|
||||
{
|
||||
HtmlEncoder = new TestHtmlEncoder(),
|
||||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
HrefInclude = "**/*.css",
|
||||
FileVersion = true,
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
helper.Process(context, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("<link href=\"HtmlEncode[[/bar/css/site.css?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-" +
|
||||
"j1ncoSt3SABJtkGk]]\" rel=\"stylesheet\" />", output.Content.GetContent());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendersLinkTags_GlobbedHref_AddsFileVersion()
|
||||
{
|
||||
// Arrange
|
||||
var context = MakeTagHelperContext(
|
||||
attributes: new Dictionary<string, object>
|
||||
{
|
||||
["href"] = "/css/site.css",
|
||||
["rel"] = "stylesheet",
|
||||
["asp-href-include"] = "**/*.css",
|
||||
["asp-file-version"] = "true"
|
||||
});
|
||||
var output = MakeTagHelperOutput("link", attributes: new Dictionary<string, string>
|
||||
{
|
||||
["href"] = "/css/site.css",
|
||||
["rel"] = "stylesheet"
|
||||
});
|
||||
var logger = new Mock<ILogger<LinkTagHelper>>();
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>();
|
||||
globbingUrlBuilder.Setup(g => g.BuildUrlList("/css/site.css", "**/*.css", null))
|
||||
.Returns(new[] { "/css/site.css", "/base.css" });
|
||||
var helper = new LinkTagHelper
|
||||
{
|
||||
HtmlEncoder = new TestHtmlEncoder(),
|
||||
GlobbingUrlBuilder = globbingUrlBuilder.Object,
|
||||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
HrefInclude = "**/*.css",
|
||||
FileVersion = true,
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
helper.Process(context, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("<link href=\"HtmlEncode[[/css/site.css?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk]]\"" +
|
||||
" rel=\"stylesheet\" /><link href=\"HtmlEncode[[/base.css" +
|
||||
"?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk]]\" rel=\"stylesheet\" />", output.Content.GetContent());
|
||||
}
|
||||
|
||||
private static ViewContext MakeViewContext(string requestPathBase = null)
|
||||
{
|
||||
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
|
||||
if (requestPathBase != null)
|
||||
{
|
||||
actionContext.HttpContext.Request.PathBase = new Http.PathString(requestPathBase);
|
||||
}
|
||||
|
||||
var metadataProvider = new EmptyModelMetadataProvider();
|
||||
var viewData = new ViewDataDictionary(metadataProvider);
|
||||
var viewContext = new ViewContext(
|
||||
|
|
@ -411,15 +619,59 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var emptyDirectoryContents = new Mock<IDirectoryContents>();
|
||||
emptyDirectoryContents.Setup(dc => dc.GetEnumerator())
|
||||
.Returns(Enumerable.Empty<IFileInfo>().GetEnumerator());
|
||||
var mockFile = new Mock<IFileInfo>();
|
||||
mockFile.SetupGet(f => f.Exists).Returns(true);
|
||||
mockFile
|
||||
.Setup(m => m.CreateReadStream())
|
||||
.Returns(() => new MemoryStream(Encoding.UTF8.GetBytes("Hello World!")));
|
||||
var mockFileProvider = new Mock<IFileProvider>();
|
||||
mockFileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny<string>()))
|
||||
.Returns(emptyDirectoryContents.Object);
|
||||
mockFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny<string>()))
|
||||
.Returns(mockFile.Object);
|
||||
var hostingEnvironment = new Mock<IHostingEnvironment>();
|
||||
hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(mockFileProvider.Object);
|
||||
|
||||
return hostingEnvironment.Object;
|
||||
}
|
||||
|
||||
private static IApplicationEnvironment MakeApplicationEnvironment(string applicationName = "testApplication")
|
||||
{
|
||||
var applicationEnvironment = new Mock<IApplicationEnvironment>();
|
||||
applicationEnvironment.Setup(a => a.ApplicationName).Returns(applicationName);
|
||||
return applicationEnvironment.Object;
|
||||
}
|
||||
|
||||
private static IMemoryCache MakeCache(object result = null)
|
||||
{
|
||||
var cache = new Mock<IMemoryCache>();
|
||||
cache.CallBase = true;
|
||||
cache.Setup(c => c.TryGetValue(It.IsAny<string>(), It.IsAny<IEntryLink>(), out result))
|
||||
.Returns(result != null);
|
||||
|
||||
var cacheSetContext = new Mock<ICacheSetContext>();
|
||||
cacheSetContext.Setup(c => c.AddExpirationTrigger(It.IsAny<IExpirationTrigger>()));
|
||||
cache
|
||||
.Setup(
|
||||
c => c.Set(
|
||||
/*key*/ It.IsAny<string>(),
|
||||
/*link*/ It.IsAny<IEntryLink>(),
|
||||
/*state*/ It.IsAny<object>(),
|
||||
/*create*/ It.IsAny<Func<ICacheSetContext, object>>()))
|
||||
.Returns((
|
||||
string input,
|
||||
IEntryLink entryLink,
|
||||
object state,
|
||||
Func<ICacheSetContext, object> create) =>
|
||||
{
|
||||
{
|
||||
cacheSetContext.Setup(c => c.State).Returns(state);
|
||||
return create(cacheSetContext.Object);
|
||||
}
|
||||
});
|
||||
return cache.Object;
|
||||
}
|
||||
|
||||
private class TestHtmlEncoder : IHtmlEncoder
|
||||
{
|
||||
public string HtmlEncode(string value)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.FileProviders;
|
||||
using Microsoft.AspNet.Hosting;
|
||||
|
|
@ -14,7 +15,10 @@ using Microsoft.AspNet.Mvc.Rendering;
|
|||
using Microsoft.AspNet.Mvc.TagHelpers.Internal;
|
||||
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.Framework.Caching.Memory;
|
||||
using Microsoft.Framework.Expiration.Interfaces;
|
||||
using Microsoft.Framework.Logging;
|
||||
using Microsoft.Framework.Runtime;
|
||||
using Microsoft.Framework.WebEncoders;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
|
@ -102,6 +106,103 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
tagHelper.FallbackSrcExclude = "*.min.css";
|
||||
tagHelper.FallbackTestExpression = "isavailable()";
|
||||
}
|
||||
},
|
||||
// File Version
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-file-version"] = "true"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.FileVersion = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-src-include"] = "*.js",
|
||||
["asp-file-version"] = "true"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.SrcInclude = "*.js";
|
||||
tagHelper.FileVersion = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-src-include"] = "*.js",
|
||||
["asp-src-exclude"] = "*.min.js",
|
||||
["asp-file-version"] = "true"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.SrcInclude = "*.js";
|
||||
tagHelper.SrcExclude = "*.min.js";
|
||||
tagHelper.FileVersion = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-fallback-src"] = "test.js",
|
||||
["asp-fallback-test"] = "isavailable()",
|
||||
["asp-file-version"] = "true"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.FallbackSrc = "test.js";
|
||||
tagHelper.FallbackTestExpression = "isavailable()";
|
||||
tagHelper.FileVersion = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-fallback-src-include"] = "*.js",
|
||||
["asp-fallback-test"] = "isavailable()",
|
||||
["asp-file-version"] = "true"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.FallbackSrcInclude = "*.css";
|
||||
tagHelper.FallbackTestExpression = "isavailable()";
|
||||
tagHelper.FileVersion = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-fallback-src"] = "test.js",
|
||||
["asp-fallback-src-include"] = "*.js",
|
||||
["asp-fallback-test"] = "isavailable()",
|
||||
["asp-file-version"] = "true"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.FallbackSrc = "test.js";
|
||||
tagHelper.FallbackSrcInclude = "*.css";
|
||||
tagHelper.FallbackTestExpression = "isavailable()";
|
||||
tagHelper.FileVersion = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
["asp-fallback-src-include"] = "*.js",
|
||||
["asp-fallback-src-exclude"] = "*.min.js",
|
||||
["asp-fallback-test"] = "isavailable()",
|
||||
["asp-file-version"] = "true"
|
||||
},
|
||||
tagHelper =>
|
||||
{
|
||||
tagHelper.FallbackSrcInclude = "*.css";
|
||||
tagHelper.FallbackSrcExclude = "*.min.css";
|
||||
tagHelper.FallbackTestExpression = "isavailable()";
|
||||
tagHelper.FileVersion = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -125,6 +226,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
Logger = logger,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
setProperties(helper);
|
||||
|
||||
|
|
@ -213,7 +315,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
{
|
||||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext
|
||||
ViewContext = viewContext,
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
setProperties(helper);
|
||||
|
||||
|
|
@ -241,7 +344,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
{
|
||||
Logger = logger,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext
|
||||
ViewContext = viewContext,
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
setProperties(helper);
|
||||
|
||||
|
|
@ -277,7 +381,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var helper = new ScriptTagHelper
|
||||
{
|
||||
Logger = logger,
|
||||
ViewContext = viewContext
|
||||
ViewContext = viewContext,
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -300,7 +405,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var helper = new ScriptTagHelper
|
||||
{
|
||||
Logger = logger,
|
||||
ViewContext = viewContext
|
||||
ViewContext = viewContext,
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -353,6 +459,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
HostingEnvironment = hostingEnvironment,
|
||||
FallbackSrc = "~/blank.js",
|
||||
FallbackTestExpression = "http://www.example.com/blank.js",
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -391,7 +498,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
SrcInclude = "**/*.js",
|
||||
HtmlEncoder = new HtmlEncoder()
|
||||
HtmlEncoder = new HtmlEncoder(),
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -428,7 +536,8 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
SrcInclude = "**/*.js",
|
||||
HtmlEncoder = new TestHtmlEncoder()
|
||||
HtmlEncoder = new TestHtmlEncoder(),
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
|
|
@ -439,6 +548,168 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
"<script src=\"HtmlEncode[[/common.js]]\"></script>", output.Content.GetContent());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RenderScriptTags_WithFileVersion()
|
||||
{
|
||||
// Arrange
|
||||
var context = MakeTagHelperContext(
|
||||
attributes: new Dictionary<string, object>
|
||||
{
|
||||
["src"] = "/js/site.js",
|
||||
["asp-file-version"] = "true"
|
||||
});
|
||||
var output = MakeTagHelperOutput("script", attributes: new Dictionary<string, string>
|
||||
{
|
||||
["src"] = "/js/site.js"
|
||||
});
|
||||
|
||||
var logger = new Mock<ILogger<ScriptTagHelper>>();
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
|
||||
var helper = new ScriptTagHelper
|
||||
{
|
||||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
FileVersion = true,
|
||||
HtmlEncoder = new TestHtmlEncoder(),
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
await helper.ProcessAsync(context, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"<script src=\"HtmlEncode[[/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk]]\">" +
|
||||
"</script>", output.Content.GetContent());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RenderScriptTags_WithFileVersion_AndRequestPathBase()
|
||||
{
|
||||
// Arrange
|
||||
var context = MakeTagHelperContext(
|
||||
attributes: new Dictionary<string, object>
|
||||
{
|
||||
["src"] = "/bar/js/site.js",
|
||||
["asp-file-version"] = "true"
|
||||
});
|
||||
var output = MakeTagHelperOutput("script", attributes: new Dictionary<string, string>
|
||||
{
|
||||
["src"] = "/bar/js/site.js"
|
||||
});
|
||||
|
||||
var logger = new Mock<ILogger<ScriptTagHelper>>();
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext("/bar");
|
||||
|
||||
var helper = new ScriptTagHelper
|
||||
{
|
||||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
FileVersion = true,
|
||||
HtmlEncoder = new TestHtmlEncoder(),
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
await helper.ProcessAsync(context, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"<script src=\"HtmlEncode[[/bar/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk]]\">" +
|
||||
"</script>", output.Content.GetContent());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RenderScriptTags_FallbackSrc_WithFileVersion()
|
||||
{
|
||||
// Arrange
|
||||
var context = MakeTagHelperContext(
|
||||
attributes: new Dictionary<string, object>
|
||||
{
|
||||
["src"] = "/js/site.js",
|
||||
["asp-fallback-src-include"] = "fallback.js",
|
||||
["asp-fallback-test"] = "isavailable()",
|
||||
["asp-file-version"] = "true"
|
||||
});
|
||||
var output = MakeTagHelperOutput("script", attributes: new Dictionary<string, string>
|
||||
{
|
||||
["src"] = "/js/site.js"
|
||||
});
|
||||
|
||||
var logger = new Mock<ILogger<ScriptTagHelper>>();
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
|
||||
var helper = new ScriptTagHelper
|
||||
{
|
||||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
FallbackSrc = "fallback.js",
|
||||
FallbackTestExpression = "isavailable()",
|
||||
FileVersion = true,
|
||||
HtmlEncoder = new TestHtmlEncoder(),
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
await helper.ProcessAsync(context, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"<script src=\"HtmlEncode[[/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk]]\">" +
|
||||
"</script>\r\n<script>(isavailable()||document.write(\"<script src=\\\"HtmlEncode[[fallback.js" +
|
||||
"?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk]]\\\"><\\/script>\"));</script>",
|
||||
output.Content.GetContent());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RenderScriptTags_GlobbedSrc_WithFileVersion()
|
||||
{
|
||||
// Arrange
|
||||
var context = MakeTagHelperContext(
|
||||
attributes: new Dictionary<string, object>
|
||||
{
|
||||
["src"] = "/js/site.js",
|
||||
["asp-src-include"] = "*.js",
|
||||
["asp-file-version"] = "true"
|
||||
});
|
||||
var output = MakeTagHelperOutput("script", attributes: new Dictionary<string, string>
|
||||
{
|
||||
["src"] = "/js/site.js"
|
||||
});
|
||||
var logger = new Mock<ILogger<ScriptTagHelper>>();
|
||||
var hostingEnvironment = MakeHostingEnvironment();
|
||||
var viewContext = MakeViewContext();
|
||||
var globbingUrlBuilder = new Mock<GlobbingUrlBuilder>();
|
||||
globbingUrlBuilder.Setup(g => g.BuildUrlList("/js/site.js", "*.js", null))
|
||||
.Returns(new[] { "/js/site.js", "/common.js" });
|
||||
var helper = new ScriptTagHelper
|
||||
{
|
||||
GlobbingUrlBuilder = globbingUrlBuilder.Object,
|
||||
Logger = logger.Object,
|
||||
HostingEnvironment = hostingEnvironment,
|
||||
ViewContext = viewContext,
|
||||
SrcInclude = "*.js",
|
||||
FileVersion = true,
|
||||
HtmlEncoder = new TestHtmlEncoder(),
|
||||
Cache = MakeCache(),
|
||||
};
|
||||
|
||||
// Act
|
||||
await helper.ProcessAsync(context, output);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("<script src=\"HtmlEncode[[/js/site.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk]]\">" +
|
||||
"</script><script src=\"HtmlEncode[[/common.js?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk]]\">" +
|
||||
"</script>", output.Content.GetContent());
|
||||
}
|
||||
|
||||
private TagHelperContext MakeTagHelperContext(
|
||||
IDictionary<string, object> attributes = null,
|
||||
string content = null)
|
||||
|
|
@ -457,9 +728,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
});
|
||||
}
|
||||
|
||||
private static ViewContext MakeViewContext()
|
||||
private static ViewContext MakeViewContext(string requestPathBase = null)
|
||||
{
|
||||
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
|
||||
if (requestPathBase != null)
|
||||
{
|
||||
actionContext.HttpContext.Request.PathBase = new Http.PathString(requestPathBase);
|
||||
}
|
||||
|
||||
var metadataProvider = new EmptyModelMetadataProvider();
|
||||
var viewData = new ViewDataDictionary(metadataProvider);
|
||||
var viewContext = new ViewContext(
|
||||
|
|
@ -489,15 +765,59 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
|
|||
var emptyDirectoryContents = new Mock<IDirectoryContents>();
|
||||
emptyDirectoryContents.Setup(dc => dc.GetEnumerator())
|
||||
.Returns(Enumerable.Empty<IFileInfo>().GetEnumerator());
|
||||
var mockFile = new Mock<IFileInfo>();
|
||||
mockFile.SetupGet(f => f.Exists).Returns(true);
|
||||
mockFile
|
||||
.Setup(m => m.CreateReadStream())
|
||||
.Returns(() => new MemoryStream(Encoding.UTF8.GetBytes("Hello World!")));
|
||||
var mockFileProvider = new Mock<IFileProvider>();
|
||||
mockFileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny<string>()))
|
||||
.Returns(emptyDirectoryContents.Object);
|
||||
mockFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny<string>()))
|
||||
.Returns(mockFile.Object);
|
||||
var hostingEnvironment = new Mock<IHostingEnvironment>();
|
||||
hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(mockFileProvider.Object);
|
||||
|
||||
return hostingEnvironment.Object;
|
||||
}
|
||||
|
||||
private static IApplicationEnvironment MakeApplicationEnvironment(string applicationName = "testApplication")
|
||||
{
|
||||
var applicationEnvironment = new Mock<IApplicationEnvironment>();
|
||||
applicationEnvironment.Setup(a => a.ApplicationName).Returns(applicationName);
|
||||
return applicationEnvironment.Object;
|
||||
}
|
||||
|
||||
private static IMemoryCache MakeCache(object result = null)
|
||||
{
|
||||
var cache = new Mock<IMemoryCache>();
|
||||
cache.CallBase = true;
|
||||
cache.Setup(c => c.TryGetValue(It.IsAny<string>(), It.IsAny<IEntryLink>(), out result))
|
||||
.Returns(result != null);
|
||||
|
||||
var cacheSetContext = new Mock<ICacheSetContext>();
|
||||
cacheSetContext.Setup(c => c.AddExpirationTrigger(It.IsAny<IExpirationTrigger>()));
|
||||
cache
|
||||
.Setup(
|
||||
c => c.Set(
|
||||
/*key*/ It.IsAny<string>(),
|
||||
/*link*/ It.IsAny<IEntryLink>(),
|
||||
/*state*/ It.IsAny<object>(),
|
||||
/*create*/ It.IsAny<Func<ICacheSetContext, object>>()))
|
||||
.Returns((
|
||||
string input,
|
||||
IEntryLink entryLink,
|
||||
object state,
|
||||
Func<ICacheSetContext, object> create) =>
|
||||
{
|
||||
{
|
||||
cacheSetContext.Setup(c => c.State).Returns(state);
|
||||
return create(cacheSetContext.Object);
|
||||
}
|
||||
});
|
||||
return cache.Object;
|
||||
}
|
||||
|
||||
private class TestHtmlEncoder : IHtmlEncoder
|
||||
{
|
||||
public string HtmlEncode(string value)
|
||||
|
|
|
|||
|
|
@ -181,6 +181,23 @@
|
|||
asp-fallback-href="~/site.css"
|
||||
asp-fallback-test-class="hidden"
|
||||
asp-fallback-test-property="visibility" />
|
||||
|
||||
<!-- Plain link tag with file version -->
|
||||
<link href="~/site.css" rel="stylesheet" asp-file-version="true" />
|
||||
|
||||
<!-- Globbed link tag with existing file and file version -->
|
||||
<link asp-href-include="**/site.css" rel="stylesheet" asp-file-version="true" />
|
||||
|
||||
<!-- Fallback with file version -->
|
||||
<link href="~/site.min.css" rel="stylesheet" data-extra="test"
|
||||
asp-fallback-href="~/site.css"
|
||||
asp-fallback-test-class="hidden"
|
||||
asp-fallback-test-property="visibility"
|
||||
asp-fallback-test-value="hidden"
|
||||
asp-file-version="true" />
|
||||
|
||||
<!-- Globbed link tag with existing file, static href and file version -->
|
||||
<link href="~/site.css" asp-href-include="**/*.css" rel="stylesheet" asp-file-version="true" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
|||
|
|
@ -105,5 +105,27 @@
|
|||
<script src="~/site.js" asp-src-include="**/site.js">
|
||||
// Globbed script tag with existing file and static src should dedupe
|
||||
</script>
|
||||
|
||||
<script src="~/blank.js" asp-fallback-src="~/site.js" asp-fallback-test="false" asp-file-version="true">
|
||||
// TagHelper script with comment in body, and file version.
|
||||
</script>
|
||||
|
||||
<script src="~/blank.js" asp-fallback-src-include="**/site.js" asp-fallback-test="false" asp-file-version="true">
|
||||
// Fallback to globbed src with file version.
|
||||
</script>
|
||||
|
||||
<script src="~/site.js" asp-file-version="true">
|
||||
// Regular script with comment in body, and file version.
|
||||
</script>
|
||||
|
||||
<!-- Globbed script tag with existing files and version -->
|
||||
<script asp-src-include="**/*.js" asp-file-version="true">
|
||||
// This comment shouldn't be emitted
|
||||
</script>
|
||||
|
||||
<!-- Globbed script tag with existing file, exclude and version -->
|
||||
<script asp-src-include="**/*.js" asp-src-exclude="**/site3.js" asp-file-version="true">
|
||||
// This comment shouldn't be emitted
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue