diff --git a/src/Hosting/TestHost/src/PublicAPI.Shipped.txt b/src/Hosting/TestHost/src/PublicAPI.Shipped.txt index 4333342f4f5d..cd5a4bbda9b7 100644 --- a/src/Hosting/TestHost/src/PublicAPI.Shipped.txt +++ b/src/Hosting/TestHost/src/PublicAPI.Shipped.txt @@ -56,8 +56,10 @@ static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.ConfigureTestConta static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.ConfigureTestServices(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! webHostBuilder, System.Action! servicesConfiguration) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.GetTestClient(this Microsoft.AspNetCore.Hosting.IWebHost! host) -> System.Net.Http.HttpClient! static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.GetTestServer(this Microsoft.AspNetCore.Hosting.IWebHost! host) -> Microsoft.AspNetCore.TestHost.TestServer! -static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! applicationBasePath, string! solutionName = "*.sln") -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! -static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! solutionName = "*.sln") -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! applicationBasePath, string! solutionName) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! applicationBasePath, System.ReadOnlySpan solutionNames = default(System.ReadOnlySpan)) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseSolutionRelativeContentRoot(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, string! solutionRelativePath, string! solutionName) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseTestServer(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! static Microsoft.AspNetCore.TestHost.WebHostBuilderExtensions.UseTestServer(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, System.Action! configureOptions) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! static Microsoft.AspNetCore.TestHost.WebHostBuilderFactory.CreateFromAssemblyEntryPoint(System.Reflection.Assembly! assembly, string![]! args) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder? diff --git a/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs b/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs index cbdb7e06e989..e38f390326a2 100644 --- a/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs +++ b/src/Hosting/TestHost/src/WebHostBuilderExtensions.cs @@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.TestHost; /// public static class WebHostBuilderExtensions { + private static readonly string[] _defaultSolutionNames = ["*.sln", "*.slnx"]; + /// /// Enables the service. /// @@ -115,6 +117,19 @@ public static IWebHostBuilder ConfigureTestContainer(this IWebHostBu return webHostBuilder; } + /// + /// Sets the content root of relative to the . + /// + /// The . + /// The directory of the solution file. + /// The . + public static IWebHostBuilder UseSolutionRelativeContentRoot( + this IWebHostBuilder builder, + string solutionRelativePath) + { + return builder.UseSolutionRelativeContentRoot(solutionRelativePath, AppContext.BaseDirectory, _defaultSolutionNames); + } + /// /// Sets the content root of relative to the . /// @@ -122,13 +137,12 @@ public static IWebHostBuilder ConfigureTestContainer(this IWebHostBu /// The directory of the solution file. /// The name of the solution file to make the content root relative to. /// The . - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] public static IWebHostBuilder UseSolutionRelativeContentRoot( this IWebHostBuilder builder, string solutionRelativePath, - string solutionName = "*.sln") + string solutionName) { - return builder.UseSolutionRelativeContentRoot(solutionRelativePath, AppContext.BaseDirectory, solutionName); + return builder.UseSolutionRelativeContentRoot(solutionRelativePath, AppContext.BaseDirectory, [solutionName]); } /// @@ -144,19 +158,45 @@ public static IWebHostBuilder UseSolutionRelativeContentRoot( this IWebHostBuilder builder, string solutionRelativePath, string applicationBasePath, - string solutionName = "*.sln") + string solutionName) + { + return builder.UseSolutionRelativeContentRoot(solutionRelativePath, applicationBasePath, [solutionName]); + } + + /// + /// Sets the content root of relative to the . + /// + /// The . + /// The directory of the solution file. + /// The root of the app's directory. + /// The names of the solution files to make the content root relative to. If empty, defaults to *.sln and *.slnx. + /// The . + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static IWebHostBuilder UseSolutionRelativeContentRoot( + this IWebHostBuilder builder, + string solutionRelativePath, + string applicationBasePath, + ReadOnlySpan solutionNames = default) { ArgumentNullException.ThrowIfNull(solutionRelativePath); ArgumentNullException.ThrowIfNull(applicationBasePath); + if (solutionNames.IsEmpty) + { + solutionNames = _defaultSolutionNames; + } + var directoryInfo = new DirectoryInfo(applicationBasePath); do { - var solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, solutionName).FirstOrDefault(); - if (solutionPath != null) + foreach (var solutionName in solutionNames) { - builder.UseContentRoot(Path.GetFullPath(Path.Combine(directoryInfo.FullName, solutionRelativePath))); - return builder; + var solutionPath = Directory.EnumerateFiles(directoryInfo.FullName, solutionName).FirstOrDefault(); + if (solutionPath != null) + { + builder.UseContentRoot(Path.GetFullPath(Path.Combine(directoryInfo.FullName, solutionRelativePath))); + return builder; + } } directoryInfo = directoryInfo.Parent; diff --git a/src/Hosting/TestHost/test/UseSolutionRelativeContentRootTests.cs b/src/Hosting/TestHost/test/UseSolutionRelativeContentRootTests.cs new file mode 100644 index 000000000000..26ce35e10f1a --- /dev/null +++ b/src/Hosting/TestHost/test/UseSolutionRelativeContentRootTests.cs @@ -0,0 +1,196 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.TestHost; + +public class UseSolutionRelativeContentRootTests : IDisposable +{ + private readonly string _tempDirectory; + private readonly string _contentDirectory; + + public UseSolutionRelativeContentRootTests() + { + _tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")[..8]); + _contentDirectory = Path.Combine(_tempDirectory, "src"); + Directory.CreateDirectory(_contentDirectory); + } + + [Fact] + public void UseSolutionRelativeContentRoot_FindsSlnFile() + { + var solutionFile = Path.Combine(_tempDirectory, "TestApp.sln"); + File.WriteAllText(solutionFile, "Microsoft Visual Studio Solution File, Format Version 12.00"); + + var builder = new WebHostBuilder() + .UseTestServer() + .Configure(app => { }); + + builder.UseSolutionRelativeContentRoot("src", applicationBasePath: _tempDirectory); + + using var host = builder.Build(); + var environment = host.Services.GetRequiredService(); + + Assert.Equal(_contentDirectory, environment.ContentRootPath); + } + + [Fact] + public void UseSolutionRelativeContentRoot_FindsSlnxFile() + { + var solutionFile = Path.Combine(_tempDirectory, "TestApp.slnx"); + File.WriteAllText(solutionFile, """ + + + + + + + """); + + var builder = new WebHostBuilder() + .UseTestServer() + .Configure(app => { }); + + builder.UseSolutionRelativeContentRoot("src", applicationBasePath: _tempDirectory); + + using var host = builder.Build(); + var environment = host.Services.GetRequiredService(); + + Assert.Equal(_contentDirectory, environment.ContentRootPath); + } + + [Fact] + public void UseSolutionRelativeContentRoot_WithSolutionName_FindsSpecifiedFile() + { + var subDirectory = Path.Combine(_tempDirectory, "sub"); + Directory.CreateDirectory(subDirectory); + + var slnFile = Path.Combine(subDirectory, "TestApp.sln"); + var slnxFile = Path.Combine(_tempDirectory, "TestApp.slnx"); + File.WriteAllText(slnFile, "Microsoft Visual Studio Solution File, Format Version 12.00"); + File.WriteAllText(slnxFile, """ + + + + + + """); + + var builder = new WebHostBuilder() + .UseTestServer() + .Configure(app => { }); + + builder.UseSolutionRelativeContentRoot("src", _tempDirectory, "*.slnx"); + + using var host = builder.Build(); + var environment = host.Services.GetRequiredService(); + + Assert.Equal(_contentDirectory, environment.ContentRootPath); + } + + [Fact] + public void UseSolutionRelativeContentRoot_WithMultipleSolutionNames_FindsInCurrentDirectoryFirst() + { + var expectedPath = Path.Combine(_contentDirectory, "sub"); + Directory.CreateDirectory(expectedPath); + + var slnFile = Path.Combine(_tempDirectory, "TestApp.sln"); + var slnxFile = Path.Combine(_contentDirectory, "TestApp.slnx"); + File.WriteAllText(slnFile, "Microsoft Visual Studio Solution File, Format Version 12.00"); + File.WriteAllText(slnxFile, """ + + + + + + """); + + var builder = new WebHostBuilder() + .UseTestServer() + .Configure(app => { }); + + builder.UseSolutionRelativeContentRoot("sub", _contentDirectory, ["*.sln", "*.slnx"]); + + using var host = builder.Build(); + var environment = host.Services.GetRequiredService(); + + Assert.Equal(expectedPath, environment.ContentRootPath); + } + + [Fact] + public void UseSolutionRelativeContentRoot_WithMultipleSolutionNames_WorksWithMultipleFiles() + { + var slnFile = Path.Combine(_tempDirectory, "TestApp.sln"); + var slnxFile = Path.Combine(_tempDirectory, "TestApp.slnx"); + File.WriteAllText(slnFile, "Microsoft Visual Studio Solution File, Format Version 12.00"); + File.WriteAllText(slnxFile, """ + + + + + + """); + + var builder = new WebHostBuilder() + .UseTestServer() + .Configure(app => { }); + + builder.UseSolutionRelativeContentRoot("src", applicationBasePath: _tempDirectory, solutionNames: ["*.sln", "*.slnx"]); + + using var host = builder.Build(); + var environment = host.Services.GetRequiredService(); + + Assert.Equal(_contentDirectory, environment.ContentRootPath); + } + + [Fact] + public void UseSolutionRelativeContentRoot_ThrowsWhenSolutionNotFound() + { + var builder = new WebHostBuilder() + .UseTestServer() + .Configure(app => { }); + + var exception = Assert.Throws(() => + builder.UseSolutionRelativeContentRoot("src", applicationBasePath: _tempDirectory)); + + Assert.Contains("Solution root could not be located", exception.Message); + Assert.Contains(_tempDirectory, exception.Message); + } + + [Fact] + public void UseSolutionRelativeContentRoot_WithSolutionName_SearchesParentDirectories() + { + var subDirectory = Path.Combine(_tempDirectory, "sub", "folder"); + Directory.CreateDirectory(subDirectory); + + var solutionFile = Path.Combine(_tempDirectory, "TestApp.slnx"); + File.WriteAllText(solutionFile, """ + + + + + + """); + + var builder = new WebHostBuilder() + .UseTestServer() + .Configure(app => { }); + + builder.UseSolutionRelativeContentRoot("src", subDirectory, "*.slnx"); + + using var host = builder.Build(); + var environment = host.Services.GetRequiredService(); + + Assert.Equal(_contentDirectory, environment.ContentRootPath); + } + + public void Dispose() + { + if (Directory.Exists(_tempDirectory)) + { + Directory.Delete(_tempDirectory, recursive: true); + } + } +} diff --git a/src/Mvc/test/Mvc.FunctionalTests/WebApplicationFactorySlnxTests.cs b/src/Mvc/test/Mvc.FunctionalTests/WebApplicationFactorySlnxTests.cs new file mode 100644 index 000000000000..60de72aa2e55 --- /dev/null +++ b/src/Mvc/test/Mvc.FunctionalTests/WebApplicationFactorySlnxTests.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using BasicWebSite; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests; + +public class WebApplicationFactorySlnxTests : IClassFixture>, IDisposable +{ + private readonly string _tempDirectory; + private readonly string _contentDirectory; + + public WebApplicationFactorySlnxTests(WebApplicationFactory factory) + { + Factory = factory; + _tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")[..8]); + _contentDirectory = Path.Combine(_tempDirectory, "BasicWebSite"); + + Directory.CreateDirectory(_tempDirectory); + Directory.CreateDirectory(_contentDirectory); + + // Create a minimal wwwroot directory to satisfy content root expectations + var wwwrootDir = Path.Combine(_contentDirectory, "wwwroot"); + Directory.CreateDirectory(wwwrootDir); + } + + public WebApplicationFactory Factory { get; } + + [Fact] + public async Task WebApplicationFactory_UsesSlnxForSolutionRelativeContentRoot() + { + // Create .slnx file in temp directory + var slnxFile = Path.Combine(_tempDirectory, "TestSolution.slnx"); + File.WriteAllText(slnxFile, """ + + + + + + + + + + """); + + var factory = Factory.WithWebHostBuilder(builder => + { + builder.UseSolutionRelativeContentRoot("BasicWebSite", _tempDirectory, "TestSolution.slnx"); + }); + + using var client = factory.CreateClient(); + + // Verify that the content root was set correctly by accessing the environment + var environment = factory.Services.GetRequiredService(); + Assert.Equal(_contentDirectory, environment.ContentRootPath); + Assert.True(Directory.Exists(environment.ContentRootPath)); + + // Verify the factory is functional with the .slnx-resolved content root + var response = await client.GetAsync("/"); + Assert.True(response.IsSuccessStatusCode); + } + + public void Dispose() + { + if (Directory.Exists(_tempDirectory)) + { + Directory.Delete(_tempDirectory, recursive: true); + } + } +}