From 0e783ace5898c761435541aa937085a8593a75d0 Mon Sep 17 00:00:00 2001 From: sornaks Date: Fri, 20 Mar 2015 13:35:14 -0700 Subject: [PATCH] Issue #2141 - Script & Link tag helpers should support generating cache-busting file version hash in URL. --- .../Internal/AttributeMatcher.cs | 3 +- .../Internal/FileVersionProvider.cs | 85 +++++ .../LinkTagHelper.cs | 86 ++++- .../ScriptTagHelper.cs | 109 ++++-- ...HelpersWebSite.MvcTagHelper_Home.Link.html | 13 + ...lpersWebSite.MvcTagHelper_Home.Script.html | 20 ++ .../Internal/FileVersionProviderTest.cs | 214 +++++++++++ .../LinkTagHelperTest.cs | 264 +++++++++++++- .../ScriptTagHelperTest.cs | 334 +++++++++++++++++- .../Views/MvcTagHelper_Home/Link.cshtml | 17 + .../Views/MvcTagHelper_Home/Script.cshtml | 22 ++ 11 files changed, 1111 insertions(+), 56 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.TagHelpers/Internal/FileVersionProvider.cs create mode 100644 test/Microsoft.AspNet.Mvc.TagHelpers.Test/Internal/FileVersionProviderTest.cs diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs index 331cffc9cf..4152f2fa21 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/AttributeMatcher.cs @@ -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); diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/FileVersionProvider.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/FileVersionProvider.cs new file mode 100644 index 0000000000..d5ef0eda93 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Internal/FileVersionProvider.cs @@ -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 +{ + /// + /// Provides version hash for a specified file. + /// + public class FileVersionProvider + { + private const string VersionKey = "v"; + private readonly IFileProvider _fileProvider; + private readonly IMemoryCache _cache; + private readonly PathString _requestPathBase; + + /// + /// Creates a new instance of . + /// + /// The file provider to get and watch files. + /// Name of the application. + /// where versioned urls of files are cached. + public FileVersionProvider( + [NotNull] IFileProvider fileProvider, + [NotNull] IMemoryCache cache, + [NotNull] PathString requestPathBase) + { + _fileProvider = fileProvider; + _cache = cache; + _requestPathBase = requestPathBase; + } + + /// + /// Adds version query parameter to the specified file path. + /// + /// The path of the file to which version should be added. + /// Path containing the version query string. + /// + /// The version query string is appended as with the key "v". + /// + 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); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs index 3db20ddc7e..1bf94f9e9d 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs @@ -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[] 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 { + /// + /// Just adding a file version for the generated urls. + /// + FileVersion = 0, /// /// Just performing file globbing search for the href, rendering a separate <link> for each match. /// - GlobbedHref = 0, + GlobbedHref = 1, /// /// 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. /// - Fallback = 1, + Fallback = 2, } /// @@ -108,6 +119,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers [HtmlAttributeName(FallbackHrefAttributeName)] public string FallbackHref { get; set; } + /// + /// Value indicating if file version should be appended to the href urls. + /// + /// + /// If true then a query string "v" with the encoded content of the file is added. + /// + [HtmlAttributeName(FileVersionAttributeName)] + public bool? FileVersion { get; set; } + /// /// 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 tag to match the original one in the source file + // No globbing to do, just build a 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 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 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 attributes, TagHelperContent builder) + private void EnsureFileVersionProvider() { + if (_fileVersionProvider == null) + { + _fileVersionProvider = new FileVersionProvider( + HostingEnvironment.WebRootFileProvider, + Cache, + ViewContext.HttpContext.Request.PathBase); + } + } + + private void BuildLinkTag(IDictionary attributes, TagHelperContent builder) + { + EnsureFileVersionProvider(); builder.Append(""); } + + private bool ShouldAddFileVersion() + { + return FileVersion ?? false; + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs index 226949f725..e3a2e4d8b4 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/ScriptTagHelper.cs @@ -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[] 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 { + /// + /// Just adding a file version for the generated urls. + /// + FileVersion = 0, /// /// Just performing file globbing search for the src, rendering a separate <script> for each match. /// - GlobbedSrc = 0, + GlobbedSrc = 1, /// /// 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. /// - Fallback = 1 + Fallback = 2 } /// @@ -98,6 +108,15 @@ namespace Microsoft.AspNet.Mvc.TagHelpers [HtmlAttributeName(FallbackSrcAttributeName)] public string FallbackSrc { get; set; } + /// + /// Value indicating if file version should be appended to src urls. + /// + /// + /// A query string "v" with the encoded content of the file is added. + /// + [HtmlAttributeName(FileVersionAttributeName)] + public bool? FileVersion { get; set; } + /// /// 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; } + /// 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 "); } } @@ -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 attributes, TagHelperContent builder) { + EnsureFileVersionProvider(); builder.Append("") @@ -286,15 +332,20 @@ namespace Microsoft.AspNet.Mvc.TagHelpers .Append(""); } - 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 ? "\\\"" : "\""); + } + } } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html index 9dd1127c76..be221d21d0 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Link.html @@ -110,6 +110,19 @@ + + + + + + + + + + + + + diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Script.html b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Script.html index 70102a9ac7..1aa41653c6 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Script.html +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/Compiler/Resources/MvcTagHelpersWebSite.MvcTagHelper_Home.Script.html @@ -92,5 +92,25 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/Internal/FileVersionProviderTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/Internal/FileVersionProviderTest.cs new file mode 100644 index 0000000000..e07fe43af0 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/Internal/FileVersionProviderTest.cs @@ -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(); + 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(); + cache.CallBase = true; + cache.Setup(c => c.TryGetValue(It.IsAny(), It.IsAny(), out cacheValue)) + .Returns(cacheValue != null); + var cacheSetContext = new Mock(); + cacheSetContext.Setup(c => c.AddExpirationTrigger(trigger.Object)).Verifiable(); + cache.Setup(c => c.Set( + /*key*/ filePath, + /*link*/ It.IsAny(), + /*state*/ It.IsAny(), + /*create*/ It.IsAny>())) + .Returns>( + (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(); + existingMockFile.SetupGet(f => f.Exists).Returns(true); + existingMockFile + .Setup(m => m.CreateReadStream()) + .Returns(() => new MemoryStream(Encoding.UTF8.GetBytes("Hello World!"))); + + var nonExistingMockFile = new Mock(); + nonExistingMockFile.SetupGet(f => f.Exists).Returns(false); + nonExistingMockFile + .Setup(m => m.CreateReadStream()) + .Returns(() => new MemoryStream(Encoding.UTF8.GetBytes("Hello World!"))); + + var mockFileProvider = new Mock(); + if (pathStartsWithAppName) + { + mockFileProvider.Setup(fp => fp.GetFileInfo(filePath)).Returns(nonExistingMockFile.Object); + mockFileProvider.Setup(fp => fp.GetFileInfo(It.Is(str => str != filePath))) + .Returns(existingMockFile.Object); + } + else + { + mockFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny())) + .Returns(fileDoesNotExist? nonExistingMockFile.Object : existingMockFile.Object); + } + + var hostingEnvironment = new Mock(); + hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(mockFileProvider.Object); + + return hostingEnvironment.Object; + } + + private static IMemoryCache GetMockCache(object result = null) + { + var cache = new Mock(); + cache.CallBase = true; + cache.Setup(c => c.TryGetValue(It.IsAny(), It.IsAny(), out result)) + .Returns(result != null); + + var cacheSetContext = new Mock(); + cacheSetContext.Setup(c => c.AddExpirationTrigger(It.IsAny())); + cache + .Setup( + c => c.Set( + /*key*/ It.IsAny(), + /*link*/ It.IsAny(), + /*state*/ It.IsAny(), + /*create*/ It.IsAny>())) + .Returns(( + string input, + IEntryLink entryLink, + object state, + Func 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); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs index e895df6f07..c0d529d624 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LinkTagHelperTest.cs @@ -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 + { + ["asp-file-version"] = "true" + }, + tagHelper => + { + tagHelper.FileVersion = true; + } + }, + { + new Dictionary + { + ["asp-href-include"] = "*.css", + ["asp-file-version"] = "true" + }, + tagHelper => + { + tagHelper.HrefInclude = "*.css"; + tagHelper.FileVersion = true; + } + }, + { + new Dictionary + { + ["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 + { + ["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 + { + ["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 "", output.Content.GetContent()); } - private static ViewContext MakeViewContext() + [Fact] + public void RendersLinkTags_AddsFileVersion() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new Dictionary + { + ["href"] = "/css/site.css", + ["rel"] = "stylesheet", + ["asp-file-version"] = "true" + }); + var output = MakeTagHelperOutput("link", attributes: new Dictionary + { + ["href"] = "/css/site.css", + ["rel"] = "stylesheet" + }); + var logger = new Mock>(); + 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("", output.Content.GetContent()); + } + + [Fact] + public void RendersLinkTags_AddsFileVersion_WithRequestPathBase() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new Dictionary + { + ["href"] = "/bar/css/site.css", + ["rel"] = "stylesheet", + ["asp-file-version"] = "true" + }); + var output = MakeTagHelperOutput("link", attributes: new Dictionary + { + ["href"] = "/bar/css/site.css", + ["rel"] = "stylesheet" + }); + var logger = new Mock>(); + 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("", output.Content.GetContent()); + } + + [Fact] + public void RendersLinkTags_GlobbedHref_AddsFileVersion() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new Dictionary + { + ["href"] = "/css/site.css", + ["rel"] = "stylesheet", + ["asp-href-include"] = "**/*.css", + ["asp-file-version"] = "true" + }); + var output = MakeTagHelperOutput("link", attributes: new Dictionary + { + ["href"] = "/css/site.css", + ["rel"] = "stylesheet" + }); + var logger = new Mock>(); + var hostingEnvironment = MakeHostingEnvironment(); + var viewContext = MakeViewContext(); + var globbingUrlBuilder = new Mock(); + 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("", 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(); emptyDirectoryContents.Setup(dc => dc.GetEnumerator()) .Returns(Enumerable.Empty().GetEnumerator()); + var mockFile = new Mock(); + mockFile.SetupGet(f => f.Exists).Returns(true); + mockFile + .Setup(m => m.CreateReadStream()) + .Returns(() => new MemoryStream(Encoding.UTF8.GetBytes("Hello World!"))); var mockFileProvider = new Mock(); mockFileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny())) .Returns(emptyDirectoryContents.Object); + mockFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny())) + .Returns(mockFile.Object); var hostingEnvironment = new Mock(); hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(mockFileProvider.Object); return hostingEnvironment.Object; } + private static IApplicationEnvironment MakeApplicationEnvironment(string applicationName = "testApplication") + { + var applicationEnvironment = new Mock(); + applicationEnvironment.Setup(a => a.ApplicationName).Returns(applicationName); + return applicationEnvironment.Object; + } + + private static IMemoryCache MakeCache(object result = null) + { + var cache = new Mock(); + cache.CallBase = true; + cache.Setup(c => c.TryGetValue(It.IsAny(), It.IsAny(), out result)) + .Returns(result != null); + + var cacheSetContext = new Mock(); + cacheSetContext.Setup(c => c.AddExpirationTrigger(It.IsAny())); + cache + .Setup( + c => c.Set( + /*key*/ It.IsAny(), + /*link*/ It.IsAny(), + /*state*/ It.IsAny(), + /*create*/ It.IsAny>())) + .Returns(( + string input, + IEntryLink entryLink, + object state, + Func create) => + { + { + cacheSetContext.Setup(c => c.State).Returns(state); + return create(cacheSetContext.Object); + } + }); + return cache.Object; + } + private class TestHtmlEncoder : IHtmlEncoder { public string HtmlEncode(string value) diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs index 21b27e019c..720aa801f3 100644 --- a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ScriptTagHelperTest.cs @@ -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 + { + ["asp-file-version"] = "true" + }, + tagHelper => + { + tagHelper.FileVersion = true; + } + }, + { + new Dictionary + { + ["asp-src-include"] = "*.js", + ["asp-file-version"] = "true" + }, + tagHelper => + { + tagHelper.SrcInclude = "*.js"; + tagHelper.FileVersion = true; + } + }, + { + new Dictionary + { + ["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 + { + ["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 + { + ["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 + { + ["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 + { + ["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 "", output.Content.GetContent()); } + [Fact] + public async Task RenderScriptTags_WithFileVersion() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new Dictionary + { + ["src"] = "/js/site.js", + ["asp-file-version"] = "true" + }); + var output = MakeTagHelperOutput("script", attributes: new Dictionary + { + ["src"] = "/js/site.js" + }); + + var logger = new Mock>(); + 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( + "", output.Content.GetContent()); + } + + [Fact] + public async Task RenderScriptTags_WithFileVersion_AndRequestPathBase() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new Dictionary + { + ["src"] = "/bar/js/site.js", + ["asp-file-version"] = "true" + }); + var output = MakeTagHelperOutput("script", attributes: new Dictionary + { + ["src"] = "/bar/js/site.js" + }); + + var logger = new Mock>(); + 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( + "", output.Content.GetContent()); + } + + [Fact] + public async Task RenderScriptTags_FallbackSrc_WithFileVersion() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new Dictionary + { + ["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 + { + ["src"] = "/js/site.js" + }); + + var logger = new Mock>(); + 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( + "\r\n", + output.Content.GetContent()); + } + + [Fact] + public async Task RenderScriptTags_GlobbedSrc_WithFileVersion() + { + // Arrange + var context = MakeTagHelperContext( + attributes: new Dictionary + { + ["src"] = "/js/site.js", + ["asp-src-include"] = "*.js", + ["asp-file-version"] = "true" + }); + var output = MakeTagHelperOutput("script", attributes: new Dictionary + { + ["src"] = "/js/site.js" + }); + var logger = new Mock>(); + var hostingEnvironment = MakeHostingEnvironment(); + var viewContext = MakeViewContext(); + var globbingUrlBuilder = new Mock(); + 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("", output.Content.GetContent()); + } + private TagHelperContext MakeTagHelperContext( IDictionary 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(); emptyDirectoryContents.Setup(dc => dc.GetEnumerator()) .Returns(Enumerable.Empty().GetEnumerator()); + var mockFile = new Mock(); + mockFile.SetupGet(f => f.Exists).Returns(true); + mockFile + .Setup(m => m.CreateReadStream()) + .Returns(() => new MemoryStream(Encoding.UTF8.GetBytes("Hello World!"))); var mockFileProvider = new Mock(); mockFileProvider.Setup(fp => fp.GetDirectoryContents(It.IsAny())) .Returns(emptyDirectoryContents.Object); + mockFileProvider.Setup(fp => fp.GetFileInfo(It.IsAny())) + .Returns(mockFile.Object); var hostingEnvironment = new Mock(); hostingEnvironment.Setup(h => h.WebRootFileProvider).Returns(mockFileProvider.Object); return hostingEnvironment.Object; } + private static IApplicationEnvironment MakeApplicationEnvironment(string applicationName = "testApplication") + { + var applicationEnvironment = new Mock(); + applicationEnvironment.Setup(a => a.ApplicationName).Returns(applicationName); + return applicationEnvironment.Object; + } + + private static IMemoryCache MakeCache(object result = null) + { + var cache = new Mock(); + cache.CallBase = true; + cache.Setup(c => c.TryGetValue(It.IsAny(), It.IsAny(), out result)) + .Returns(result != null); + + var cacheSetContext = new Mock(); + cacheSetContext.Setup(c => c.AddExpirationTrigger(It.IsAny())); + cache + .Setup( + c => c.Set( + /*key*/ It.IsAny(), + /*link*/ It.IsAny(), + /*state*/ It.IsAny(), + /*create*/ It.IsAny>())) + .Returns(( + string input, + IEntryLink entryLink, + object state, + Func create) => + { + { + cacheSetContext.Setup(c => c.State).Returns(state); + return create(cacheSetContext.Object); + } + }); + return cache.Object; + } + private class TestHtmlEncoder : IHtmlEncoder { public string HtmlEncode(string value) diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Link.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Link.cshtml index f003ff1967..08f42bc575 100644 --- a/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Link.cshtml +++ b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Link.cshtml @@ -181,6 +181,23 @@ asp-fallback-href="~/site.css" asp-fallback-test-class="hidden" asp-fallback-test-property="visibility" /> + + + + + + + + + + + + diff --git a/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Script.cshtml b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Script.cshtml index 3f7c98c7cb..861d1a9779 100644 --- a/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Script.cshtml +++ b/test/WebSites/MvcTagHelpersWebSite/Views/MvcTagHelper_Home/Script.cshtml @@ -105,5 +105,27 @@ + + + + + + + + + + + + \ No newline at end of file