diff --git a/samples/MvcSandbox/MvcSandbox.csproj b/samples/MvcSandbox/MvcSandbox.csproj index 478e0db801..0cb47cf757 100644 --- a/samples/MvcSandbox/MvcSandbox.csproj +++ b/samples/MvcSandbox/MvcSandbox.csproj @@ -1,8 +1,8 @@ - + - net452;netcoreapp1.1 + netcoreapp1.1;net452 netcoreapp1.1 diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProject.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProject.cs index c814102ba7..3508f90bca 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProject.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorProject.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using Microsoft.AspNetCore.Razor.Evolution; using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.Razor.Internal { @@ -29,21 +28,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal public override IEnumerable EnumerateItems(string path) { - if (path == null) - { - throw new ArgumentNullException(nameof(path)); - } - - if (path.Length == 0 || path[0] != '/') - { - throw new ArgumentException(Resources.RazorProject_PathMustStartWithForwardSlash); - } - - return EnumerateFiles(_provider.GetDirectoryContents(path), path, ""); + EnsureValidPath(path); + return EnumerateFiles(_provider.GetDirectoryContents(path), path, prefix: string.Empty); } - public virtual IChangeToken Watch(string pattern) => _provider.Watch(pattern); - private IEnumerable EnumerateFiles(IDirectoryContents directory, string basePath, string prefix) { if (directory.Exists) @@ -53,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal if (file.IsDirectory) { var relativePath = prefix + "/" + file.Name; - var subDirectory = _provider.GetDirectoryContents(relativePath); + var subDirectory = _provider.GetDirectoryContents(JoinPath(basePath, relativePath)); var children = EnumerateFiles(subDirectory, basePath, relativePath); foreach (var child in children) { @@ -67,5 +55,21 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal } } } + + private static string JoinPath(string path1, string path2) + { + var hasTrailingSlash = path1.EndsWith("/", StringComparison.Ordinal); + var hasLeadingSlash = path2.StartsWith("/", StringComparison.Ordinal); + if (hasLeadingSlash && hasTrailingSlash) + { + return path1 + path2.Substring(1); + } + else if (hasLeadingSlash || hasTrailingSlash) + { + return path1 + path2; + } + + return path1 + "/" + path2; + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs index f367926d83..1e06952d10 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageActionDescriptorProvider.cs @@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure public void OnProvidersExecuting(ActionDescriptorProviderContext context) { - foreach (var item in _project.EnumerateItems("/")) + foreach (var item in _project.EnumerateItems(_pagesOptions.RootDirectory)) { if (item.Filename.StartsWith("_")) { @@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure private void AddActionDescriptors(IList actions, RazorProjectItem item, string template) { var model = new PageApplicationModel(item.CombinedPath, item.PathWithoutExtension); - var routePrefix = item.BasePath == "/" ? item.PathWithoutExtension : item.BasePath + item.PathWithoutExtension; + var routePrefix = item.PathWithoutExtension; model.Selectors.Add(CreateSelectorModel(routePrefix, template)); if (string.Equals(IndexFileName, item.Filename, StringComparison.OrdinalIgnoreCase)) diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionDescriptorChangeProvider.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionDescriptorChangeProvider.cs index e80fb8c6c7..7e5ea4a82a 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionDescriptorChangeProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageActionDescriptorChangeProvider.cs @@ -1,22 +1,38 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Razor.Internal; -using Microsoft.AspNetCore.Razor.Evolution; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal { public class PageActionDescriptorChangeProvider : IActionDescriptorChangeProvider { - private readonly RazorProject _razorProject; + private readonly IFileProvider _fileProvider; + private readonly string _searchPattern; - public PageActionDescriptorChangeProvider(RazorProject razorProject) + public PageActionDescriptorChangeProvider( + IRazorViewEngineFileProviderAccessor fileProviderAccessor, + IOptions razorPagesOptions) { - _razorProject = razorProject; + if (fileProviderAccessor == null) + { + throw new ArgumentNullException(nameof(fileProviderAccessor)); + } + + if (razorPagesOptions == null) + { + throw new ArgumentNullException(nameof(razorPagesOptions)); + } + + _fileProvider = fileProviderAccessor.FileProvider; + _searchPattern = razorPagesOptions.Value.RootDirectory.TrimEnd('/') + "/**/*.cshtml"; } - public IChangeToken GetChangeToken() => ((DefaultRazorProject)_razorProject).Watch("**/*.cshtml"); + public IChangeToken GetChangeToken() => _fileProvider.Watch(_searchPattern); } } diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs index 2167d4a4c6..4c5ab25565 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Properties/Resources.Designer.cs @@ -91,7 +91,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages } /// - /// Unsupported handler method type '{0}'. + /// Unsupported handler method return type '{0}'. /// internal static string UnsupportedHandlerMethodType { @@ -99,13 +99,29 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages } /// - /// Unsupported handler method type '{0}'. + /// Unsupported handler method return type '{0}'. /// internal static string FormatUnsupportedHandlerMethodType(object p0) { return string.Format(CultureInfo.CurrentCulture, GetString("UnsupportedHandlerMethodType"), p0); } + /// + /// Path must be an application relative path that starts with a forward slash '/'. + /// + internal static string PathMustBeAnAppRelativePath + { + get { return GetString("PathMustBeAnAppRelativePath"); } + } + + /// + /// Path must be an application relative path that starts with a forward slash '/'. + /// + internal static string FormatPathMustBeAnAppRelativePath() + { + return GetString("PathMustBeAnAppRelativePath"); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs index e7a5d5b37e..5207e7bbf0 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/RazorPagesOptions.cs @@ -12,10 +12,34 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages /// public class RazorPagesOptions { + private string _root = "/"; + /// /// Gets a list of instances that will be applied to /// the when discovering Razor Pages. /// public IList Conventions { get; } = new List(); + + /// + /// Application relative path used as the root of discovery for Razor Page files. + /// + public string RootDirectory + { + get => _root; + set + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(value)); + } + + if (value[0] != '/') + { + throw new ArgumentException(Resources.PathMustBeAnAppRelativePath, nameof(value)); + } + + _root = value; + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx index 406a421a44..95d5df2bae 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Resources.resx @@ -135,4 +135,7 @@ Unsupported handler method return type '{0}'. + + Path must be an application relative path that starts with a forward slash '/'. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs index 35feac6179..1985d99ac5 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesTest.cs @@ -35,6 +35,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public async Task HelloWorld_CanGetContent() { // Arrange + // Note: If the route in this test case ever changes, the negative test case + // RazorPagesWithBasePathTest.PageOutsideBasePath_IsNotRouteable needs to be updated too. var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost/HelloWorld"); // Act diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs new file mode 100644 index 0000000000..13294a214c --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/RazorPagesWithBasePathTest.cs @@ -0,0 +1,117 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class RazorPagesWithBasePathTest : IClassFixture> + { + public RazorPagesWithBasePathTest(MvcTestFixture fixture) + { + Client = fixture.Client; + } + + public HttpClient Client { get; } + + [Fact] + public async Task PageOutsideBasePath_IsNotRouteable() + { + // Act + var response = await Client.GetAsync("/HelloWorld"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task IndexAtBasePath_IsRouteableAtRoot() + { + // Act + var response = await Client.GetAsync("/"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello from /Index", content.Trim()); + } + + [Fact] + public async Task IndexAtBasePath_IsRouteableViaIndex() + { + // Act + var response = await Client.GetAsync("/Index"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello from /Index", content.Trim()); + } + + [Fact] + public async Task IndexInSubdirectory_IsRouteableViaDirectoryName() + { + // Act + var response = await Client.GetAsync("/Admin/Index"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello from /Admin/Index", content.Trim()); + } + + [Fact] + public async Task PageWithRouteTemplateInSubdirectory_IsRouteable() + { + // Act + var response = await Client.GetAsync("/Admin/RouteTemplate/1/MyRouteSuffix/"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello from /Admin/RouteTemplate 1", content.Trim()); + } + + [Fact] + public async Task PageWithRouteTemplateInSubdirectory_IsRouteable_WithOptionalParameters() + { + // Act + var response = await Client.GetAsync("/Admin/RouteTemplate/my-user-id/MyRouteSuffix/4"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Equal("Hello from /Admin/RouteTemplate my-user-id 4", content.Trim()); + } + + [Fact] + public async Task AuthConvention_IsAppliedOnBasePathRelativePaths_ForFiles() + { + // Act + var response = await Client.GetAsync("/Conventions/Auth"); + + // Assert + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + Assert.Equal("/Login?ReturnUrl=%2FConventions%2FAuth", response.Headers.Location.PathAndQuery); + } + + [Fact] + public async Task AuthConvention_IsAppliedOnBasePathRelativePaths_For_Folders() + { + // Act + var response = await Client.GetAsync("/Conventions/AuthFolder"); + + // Assert + Assert.Equal(HttpStatusCode.Redirect, response.StatusCode); + Assert.Equal("/Login?ReturnUrl=%2FConventions%2FAuthFolder", response.Headers.Location.PathAndQuery); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorProjectTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorProjectTest.cs new file mode 100644 index 0000000000..c955aae32c --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorProjectTest.cs @@ -0,0 +1,129 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Microsoft.Extensions.FileProviders; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Internal +{ + public class DefaultRazorProjectTest + { + [Fact] + public void EnumerateFiles_ReturnsEmptySequenceIfNoCshtmlFilesArePresent() + { + // Arrange + var fileProvider = new TestFileProvider(); + var file1 = fileProvider.AddFile("File1.txt", "content"); + var file2 = fileProvider.AddFile("File2.js", "content"); + fileProvider.AddDirectoryContent("/", new IFileInfo[] { file1, file2 }); + + var razorProject = new DefaultRazorProject(fileProvider); + + // Act + var razorFiles = razorProject.EnumerateItems("/"); + + // Assert + Assert.Empty(razorFiles); + } + + [Fact] + public void EnumerateFiles_ReturnsCshtmlFiles() + { + // Arrange + var fileProvider = new TestFileProvider(); + var file1 = fileProvider.AddFile("File1.cshtml", "content"); + var file2 = fileProvider.AddFile("File2.js", "content"); + var file3 = fileProvider.AddFile("File3.cshtml", "content"); + fileProvider.AddDirectoryContent("/", new IFileInfo[] { file1, file2, file3 }); + + var razorProject = new DefaultRazorProject(fileProvider); + + // Act + var razorFiles = razorProject.EnumerateItems("/"); + + // Assert + Assert.Collection(razorFiles.OrderBy(f => f.Path), + file => Assert.Equal("/File1.cshtml", file.Path), + file => Assert.Equal("/File3.cshtml", file.Path)); + } + + [Fact] + public void EnumerateFiles_IteratesOverAllCshtmlUnderRoot() + { + // Arrange + var fileProvider = new TestFileProvider(); + var directory1 = new TestDirectoryFileInfo + { + Name = "Level1-Dir1", + }; + var file1 = fileProvider.AddFile("File1.cshtml", "content"); + var directory2 = new TestDirectoryFileInfo + { + Name = "Level1-Dir2", + }; + fileProvider.AddDirectoryContent("/", new IFileInfo[] { directory1, file1, directory2 }); + + var file2 = fileProvider.AddFile("Level1-Dir1/File2.cshtml", "content"); + var file3 = fileProvider.AddFile("Level1-Dir1/File3.cshtml", "content"); + var file4 = fileProvider.AddFile("Level1-Dir1/File4.txt", "content"); + var directory3 = new TestDirectoryFileInfo + { + Name = "Level2-Dir1" + }; + fileProvider.AddDirectoryContent("/Level1-Dir1", new IFileInfo[] { file2, directory3, file3, file4 }); + var file5 = fileProvider.AddFile("Level1-Dir2/File5.cshtml", "content"); + fileProvider.AddDirectoryContent("/Level1-Dir2", new IFileInfo[] { file5 }); + fileProvider.AddDirectoryContent("/Level1/Level2", new IFileInfo[0]); + var razorProject = new DefaultRazorProject(fileProvider); + + // Act + var razorFiles = razorProject.EnumerateItems("/"); + + // Assert + Assert.Collection(razorFiles.OrderBy(f => f.Path), + file => Assert.Equal("/File1.cshtml", file.Path), + file => Assert.Equal("/Level1-Dir1/File2.cshtml", file.Path), + file => Assert.Equal("/Level1-Dir1/File3.cshtml", file.Path), + file => Assert.Equal("/Level1-Dir2/File5.cshtml", file.Path)); + } + + [Fact] + public void EnumerateFiles_IteratesOverAllCshtmlUnderPath() + { + // Arrange + var fileProvider = new TestFileProvider(); + var directory1 = new TestDirectoryFileInfo + { + Name = "Level1-Dir1", + }; + var file1 = fileProvider.AddFile("File1.cshtml", "content"); + var directory2 = new TestDirectoryFileInfo + { + Name = "Level1-Dir2", + }; + fileProvider.AddDirectoryContent("/", new IFileInfo[] { directory1, file1, directory2 }); + + var file2 = fileProvider.AddFile("Level1-Dir1/File2.cshtml", "content"); + var file3 = fileProvider.AddFile("Level1-Dir1/File3.cshtml", "content"); + var file4 = fileProvider.AddFile("Level1-Dir1/File4.txt", "content"); + var directory3 = new TestDirectoryFileInfo + { + Name = "Level2-Dir1" + }; + fileProvider.AddDirectoryContent("/Level1-Dir1", new IFileInfo[] { file2, directory3, file3, file4 }); + var file5 = fileProvider.AddFile("Level1-Dir2/File5.cshtml", "content"); + fileProvider.AddDirectoryContent("/Level1-Dir2", new IFileInfo[] { file5 }); + fileProvider.AddDirectoryContent("/Level1/Level2", new IFileInfo[0]); + var razorProject = new DefaultRazorProject(fileProvider); + + // Act + var razorFiles = razorProject.EnumerateItems("/Level1-Dir1"); + + // Assert + Assert.Collection(razorFiles.OrderBy(f => f.Path), + file => Assert.Equal("/File2.cshtml", file.Path), + file => Assert.Equal("/File3.cshtml", file.Path)); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs index a87c14f25f..0fc3a36677 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Infrastructure/PageActionDescriptorProviderTest.cs @@ -95,6 +95,75 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure Assert.Equal("Test/Home", descriptor.AttributeRouteInfo.Template); } + [Fact] + public void GetDescriptors_GeneratesRouteTemplate() + { + // Arrange + var razorProject = new Mock(MockBehavior.Strict); + razorProject.Setup(p => p.EnumerateItems("/")) + .Returns(new[] + { + GetProjectItem("/", "/base-path/Test.cshtml", $"@page \"Home\" {Environment.NewLine}

Hello world

"), + GetProjectItem("/", "/base-path/Index.cshtml", $"@page {Environment.NewLine}"), + GetProjectItem("/", "/base-path/Admin/Index.cshtml", $"@page{Environment.NewLine}"), + GetProjectItem("/", "/base-path/Admin/User.cshtml", $"@page{Environment.NewLine}"), + }); + var options = GetRazorPagesOptions(); + + var provider = new PageActionDescriptorProvider( + razorProject.Object, + GetAccessor(), + options); + var context = new ActionDescriptorProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection(context.Results, + result => Assert.Equal("base-path/Test/Home", result.AttributeRouteInfo.Template), + result => Assert.Equal("base-path/Index", result.AttributeRouteInfo.Template), + result => Assert.Equal("base-path", result.AttributeRouteInfo.Template), + result => Assert.Equal("base-path/Admin/Index", result.AttributeRouteInfo.Template), + result => Assert.Equal("base-path/Admin", result.AttributeRouteInfo.Template), + result => Assert.Equal("base-path/Admin/User", result.AttributeRouteInfo.Template)); + } + + [Fact] + public void GetDescriptors_UsesBasePathOption_WhenGeneratingRouteTemplate() + { + // Arrange + var razorProject = new Mock(MockBehavior.Strict); + razorProject.Setup(p => p.EnumerateItems("/base-path")) + .Returns(new[] + { + GetProjectItem("/base-path", "/Test.cshtml", $"@page \"Home\" {Environment.NewLine}

Hello world

"), + GetProjectItem("/base-path", "/Index.cshtml", $"@page {Environment.NewLine}"), + GetProjectItem("/base-path", "/Admin/Index.cshtml", $"@page{Environment.NewLine}"), + GetProjectItem("/base-path", "/Admin/User.cshtml", $"@page{Environment.NewLine}"), + }); + var options = GetRazorPagesOptions(); + options.Value.RootDirectory = "/base-path"; + var provider = new PageActionDescriptorProvider( + razorProject.Object, + GetAccessor(), + options); + var context = new ActionDescriptorProviderContext(); + + // Act + provider.OnProvidersExecuting(context); + + // Assert + Assert.Collection(context.Results, + result => Assert.Equal("Test/Home", result.AttributeRouteInfo.Template), + result => Assert.Equal("Index", result.AttributeRouteInfo.Template), + result => Assert.Equal("", result.AttributeRouteInfo.Template), + result => Assert.Equal("Admin/Index", result.AttributeRouteInfo.Template), + result => Assert.Equal("Admin", result.AttributeRouteInfo.Template), + result => Assert.Equal("Admin/User", result.AttributeRouteInfo.Template)); + + } + [Theory] [InlineData("/Path1")] [InlineData("~/Path1")] @@ -232,7 +301,6 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure }); } - [Fact] public void GetDescriptors_AddsGlobalFilters() { diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionDescriptorChangeProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionDescriptorChangeProviderTest.cs new file mode 100644 index 0000000000..6ffc1d7039 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/PageActionDescriptorChangeProviderTest.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Mvc.Razor.Internal; +using Microsoft.Extensions.FileProviders; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal +{ + public class PageActionDescriptorChangeProviderTest + { + [Fact] + public void GetChangeToken_WatchesAllCshtmlFilesUnderFileSystemRoot() + { + // Arrange + var options = new TestOptionsManager(); + var fileProvider = new Mock(); + var fileProviderAccessor = new Mock(); + fileProviderAccessor + .Setup(f => f.FileProvider) + .Returns(fileProvider.Object); + var changeProvider = new PageActionDescriptorChangeProvider(fileProviderAccessor.Object, options); + + // Act + var changeToken = changeProvider.GetChangeToken(); + + // Assert + fileProvider.Verify(f => f.Watch("/**/*.cshtml")); + } + + [Theory] + [InlineData("/pages-base-dir")] + [InlineData("/pages-base-dir/")] + public void GetChangeToken_WatchesAllCshtmlFilesUnderSpecifiedRootDirectory(string rootDirectory) + { + // Arrange + var options = new TestOptionsManager(); + options.Value.RootDirectory = rootDirectory; + var fileProvider = new Mock(); + var fileProviderAccessor = new Mock(); + fileProviderAccessor + .Setup(f => f.FileProvider) + .Returns(fileProvider.Object); + var changeProvider = new PageActionDescriptorChangeProvider(fileProviderAccessor.Object, options); + + // Act + var changeToken = changeProvider.GetChangeToken(); + + // Assert + fileProvider.Verify(f => f.Watch("/pages-base-dir/**/*.cshtml")); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestDirectoryContent.cs b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestDirectoryContent.cs new file mode 100644 index 0000000000..d50a72c3bc --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestDirectoryContent.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Mvc.TestCommon +{ + public class TestDirectoryContent : IDirectoryContents + { + private readonly IEnumerable _files; + + public TestDirectoryContent(IEnumerable files) + { + _files = files; + } + + public bool Exists => true; + + public IEnumerator GetEnumerator() => _files.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestDirectoryFileInfo.cs b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestDirectoryFileInfo.cs new file mode 100644 index 0000000000..4183025503 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestDirectoryFileInfo.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using Microsoft.Extensions.FileProviders; + +namespace Microsoft.AspNetCore.Mvc.Razor +{ + public class TestDirectoryFileInfo : IFileInfo + { + public bool IsDirectory => true; + + public long Length { get; set; } + + public string Name { get; set; } + + public string PhysicalPath { get; set; } + + public bool Exists => true; + + public DateTimeOffset LastModified => throw new NotImplementedException(); + + public Stream CreateReadStream() + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestFileProvider.cs b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestFileProvider.cs index 54786c231b..c432ff5ad3 100644 --- a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestFileProvider.cs +++ b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestFileProvider.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using Microsoft.AspNetCore.Mvc.TestCommon; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; @@ -13,12 +14,20 @@ namespace Microsoft.AspNetCore.Mvc.Razor { private readonly Dictionary _lookup = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary _directoryContentsLookup = + new Dictionary(); + private readonly Dictionary _fileTriggers = new Dictionary(StringComparer.Ordinal); public virtual IDirectoryContents GetDirectoryContents(string subpath) { - throw new NotSupportedException(); + if (_directoryContentsLookup.TryGetValue(subpath, out var value)) + { + return value; + } + + return new NotFoundDirectoryContents(); } public TestFileInfo AddFile(string path, string contents) @@ -36,6 +45,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor return fileInfo; } + public TestDirectoryContent AddDirectoryContent(string path, IEnumerable files) + { + var directoryContent = new TestDirectoryContent(files); + _directoryContentsLookup[path] = directoryContent; + return directoryContent; + } + public void AddFile(string path, IFileInfo contents) { _lookup[path] = contents; diff --git a/test/WebSites/RazorPagesWebSite/Pages/Admin/Index.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Admin/Index.cshtml new file mode 100644 index 0000000000..10cd33bb09 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Admin/Index.cshtml @@ -0,0 +1,2 @@ +@page +Hello from @ViewContext.RouteData.Values["page"] \ No newline at end of file diff --git a/test/WebSites/RazorPagesWebSite/Pages/Admin/RouteTemplate.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Admin/RouteTemplate.cshtml new file mode 100644 index 0000000000..d3b837de51 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Admin/RouteTemplate.cshtml @@ -0,0 +1,2 @@ +@page "{id}/MyRouteSuffix/{value:int?}" +Hello from @ViewContext.RouteData.Values["page"] @ViewContext.RouteData.Values["id"] @ViewContext.RouteData.Values["value"] \ No newline at end of file diff --git a/test/WebSites/RazorPagesWebSite/Pages/Conventions/Auth.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Conventions/Auth.cshtml new file mode 100644 index 0000000000..010bf8732a --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Conventions/Auth.cshtml @@ -0,0 +1 @@ +@page diff --git a/test/WebSites/RazorPagesWebSite/Pages/Conventions/AuthFolder/Index.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Conventions/AuthFolder/Index.cshtml new file mode 100644 index 0000000000..010bf8732a --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Conventions/AuthFolder/Index.cshtml @@ -0,0 +1 @@ +@page diff --git a/test/WebSites/RazorPagesWebSite/Pages/Index.cshtml b/test/WebSites/RazorPagesWebSite/Pages/Index.cshtml new file mode 100644 index 0000000000..10cd33bb09 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/Pages/Index.cshtml @@ -0,0 +1,2 @@ +@page +Hello from @ViewContext.RouteData.Values["page"] \ No newline at end of file diff --git a/test/WebSites/RazorPagesWebSite/RazorPagesWebSite.csproj b/test/WebSites/RazorPagesWebSite/RazorPagesWebSite.csproj index 31acae6182..0f45dcb1e9 100644 --- a/test/WebSites/RazorPagesWebSite/RazorPagesWebSite.csproj +++ b/test/WebSites/RazorPagesWebSite/RazorPagesWebSite.csproj @@ -1,4 +1,4 @@ - + diff --git a/test/WebSites/RazorPagesWebSite/StartupWithBasePath.cs b/test/WebSites/RazorPagesWebSite/StartupWithBasePath.cs new file mode 100644 index 0000000000..1881cca258 --- /dev/null +++ b/test/WebSites/RazorPagesWebSite/StartupWithBasePath.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace RazorPagesWebSite +{ + public class StartupWithBasePath + { + public void ConfigureServices(IServiceCollection services) + { + services + .AddMvc() + .AddCookieTempDataProvider() + .AddRazorPagesOptions(options => + { + options.RootDirectory = "/Pages"; + options.AuthorizePage("/Conventions/Auth", string.Empty); + options.AuthorizeFolder("/Conventions/AuthFolder", string.Empty); + }); + } + + public void Configure(IApplicationBuilder app) + { + app.UseCultureReplacer(); + + app.UseCookieAuthentication(new CookieAuthenticationOptions + { + LoginPath = "/Login", + AutomaticAuthenticate = true, + AutomaticChallenge = true + }); + + app.UseStaticFiles(); + + app.UseMvc(); + } + } +}