From 44249c38e00da27ec83f6b9e8dbca6644b3161b9 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 15 Jan 2026 03:52:00 -0500 Subject: [PATCH] Add some integration tests for the Server project (#6839) * Add some integration tests for the Server project * Not sure why this project got removed? * Format * capture debug output * Update tests to work with the now legacy WebHostBuilder - I accidentally had the updated Program locally and that was why tests were working for me locally * Formatting...again --- bitwarden-server.sln | 8 ++ .../Properties/AssemblyInfo.cs | 1 + .../Server.IntegrationTest.csproj | 23 ++++ test/Server.IntegrationTest/Server.cs | 45 ++++++++ test/Server.IntegrationTest/ServerTests.cs | 102 ++++++++++++++++++ util/Server/Program.cs | 10 +- 6 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 test/Server.IntegrationTest/Properties/AssemblyInfo.cs create mode 100644 test/Server.IntegrationTest/Server.IntegrationTest.csproj create mode 100644 test/Server.IntegrationTest/Server.cs create mode 100644 test/Server.IntegrationTest/ServerTests.cs diff --git a/bitwarden-server.sln b/bitwarden-server.sln index ae9571a4a5..409906e2d0 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -140,10 +140,13 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeederApi", "util\SeederApi\SeederApi.csproj", "{9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeederApi.IntegrationTest", "test\SeederApi.IntegrationTest\SeederApi.IntegrationTest.csproj", "{A2E067EF-609C-4D13-895A-E054C61D48BB}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SSO.Test", "bitwarden_license\test\SSO.Test\SSO.Test.csproj", "{7D98784C-C253-43FB-9873-25B65C6250D6}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sso.IntegrationTest", "bitwarden_license\test\Sso.IntegrationTest\Sso.IntegrationTest.csproj", "{FFB09376-595B-6F93-36F0-70CAE90AFECB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server.IntegrationTest", "test\Server.IntegrationTest\Server.IntegrationTest.csproj", "{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -372,6 +375,10 @@ Global {FFB09376-595B-6F93-36F0-70CAE90AFECB}.Debug|Any CPU.Build.0 = Debug|Any CPU {FFB09376-595B-6F93-36F0-70CAE90AFECB}.Release|Any CPU.ActiveCfg = Release|Any CPU {FFB09376-595B-6F93-36F0-70CAE90AFECB}.Release|Any CPU.Build.0 = Release|Any CPU + {E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -432,6 +439,7 @@ Global {A2E067EF-609C-4D13-895A-E054C61D48BB} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {7D98784C-C253-43FB-9873-25B65C6250D6} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B} {FFB09376-595B-6F93-36F0-70CAE90AFECB} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B} + {E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/test/Server.IntegrationTest/Properties/AssemblyInfo.cs b/test/Server.IntegrationTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..80afc76e2e --- /dev/null +++ b/test/Server.IntegrationTest/Properties/AssemblyInfo.cs @@ -0,0 +1 @@ +[assembly: CaptureTrace] diff --git a/test/Server.IntegrationTest/Server.IntegrationTest.csproj b/test/Server.IntegrationTest/Server.IntegrationTest.csproj new file mode 100644 index 0000000000..362ada84a0 --- /dev/null +++ b/test/Server.IntegrationTest/Server.IntegrationTest.csproj @@ -0,0 +1,23 @@ + + + + Exe + enable + + + + + + + + + + + + + + + + + + diff --git a/test/Server.IntegrationTest/Server.cs b/test/Server.IntegrationTest/Server.cs new file mode 100644 index 0000000000..073dbffb5a --- /dev/null +++ b/test/Server.IntegrationTest/Server.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; + +namespace Bit.Server.IntegrationTest; + +public class Server : WebApplicationFactory +{ + public string? ContentRoot { get; set; } + public string? WebRoot { get; set; } + public bool ServeUnknown { get; set; } + public bool? WebVault { get; set; } + public string? AppIdLocation { get; set; } + + protected override IWebHostBuilder? CreateWebHostBuilder() + { + var args = new List + { + "/contentRoot", + ContentRoot ?? "", + "/webRoot", + WebRoot ?? "", + "/serveUnknown", + ServeUnknown.ToString().ToLowerInvariant(), + }; + + if (WebVault.HasValue) + { + args.Add("/webVault"); + args.Add(WebVault.Value.ToString().ToLowerInvariant()); + } + + if (!string.IsNullOrEmpty(AppIdLocation)) + { + args.Add("/appIdLocation"); + args.Add(AppIdLocation); + } + + var builder = WebHostBuilderFactory.CreateFromTypesAssemblyEntryPoint([.. args]) + ?? throw new InvalidProgramException("Could not create builder from assembly."); + + builder.UseSetting("TEST_CONTENTROOT_SERVER", ContentRoot); + return builder; + } +} diff --git a/test/Server.IntegrationTest/ServerTests.cs b/test/Server.IntegrationTest/ServerTests.cs new file mode 100644 index 0000000000..e432f53775 --- /dev/null +++ b/test/Server.IntegrationTest/ServerTests.cs @@ -0,0 +1,102 @@ +using System.Net; +using System.Runtime.CompilerServices; + +namespace Bit.Server.IntegrationTest; + +public class ServerTests +{ + [Fact] + public async Task AttachmentsStyleUse() + { + using var tempDir = new TempDir(); + + await tempDir.WriteAsync("my-file.txt", "Hello!"); + + using var server = new Server + { + ContentRoot = tempDir.Info.FullName, + WebRoot = ".", + ServeUnknown = true, + }; + + var client = server.CreateClient(); + + var response = await client.GetAsync("/my-file.txt", TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Hello!", await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task WebVaultStyleUse() + { + using var tempDir = new TempDir(); + + await tempDir.WriteAsync("index.html", ""); + await tempDir.WriteAsync(Path.Join("app", "file.js"), "AppStuff"); + await tempDir.WriteAsync(Path.Join("locales", "file.json"), "LocalesStuff"); + await tempDir.WriteAsync(Path.Join("fonts", "file.ttf"), "FontsStuff"); + await tempDir.WriteAsync(Path.Join("connectors", "file.js"), "ConnectorsStuff"); + await tempDir.WriteAsync(Path.Join("scripts", "file.js"), "ScriptsStuff"); + await tempDir.WriteAsync(Path.Join("images", "file.avif"), "ImagesStuff"); + await tempDir.WriteAsync(Path.Join("test", "file.json"), "{}"); + + using var server = new Server + { + ContentRoot = tempDir.Info.FullName, + WebRoot = ".", + ServeUnknown = false, + WebVault = true, + AppIdLocation = Path.Join(tempDir.Info.FullName, "test", "file.json"), + }; + + var client = server.CreateClient(); + + // Going to root should return the default file + var response = await client.GetAsync("", TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("", await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken)); + // No caching on the default document + Assert.Null(response.Headers.CacheControl?.MaxAge); + + await ExpectMaxAgeAsync("app/file.js", TimeSpan.FromDays(14)); + await ExpectMaxAgeAsync("locales/file.json", TimeSpan.FromDays(14)); + await ExpectMaxAgeAsync("fonts/file.ttf", TimeSpan.FromDays(14)); + await ExpectMaxAgeAsync("connectors/file.js", TimeSpan.FromDays(14)); + await ExpectMaxAgeAsync("scripts/file.js", TimeSpan.FromDays(14)); + await ExpectMaxAgeAsync("images/file.avif", TimeSpan.FromDays(7)); + + response = await client.GetAsync("app-id.json", TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("application/json", response.Content.Headers.ContentType?.MediaType); + + async Task ExpectMaxAgeAsync(string path, TimeSpan maxAge) + { + response = await client.GetAsync(path); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Headers.CacheControl); + Assert.Equal(maxAge, response.Headers.CacheControl.MaxAge); + } + } + + private class TempDir([CallerMemberName] string test = null!) : IDisposable + { + public DirectoryInfo Info { get; } = Directory.CreateTempSubdirectory(test); + + public void Dispose() + { + Info.Delete(recursive: true); + } + + public async Task WriteAsync(string fileName, string content) + { + var fullPath = Path.Join(Info.FullName, fileName); + var directory = Path.GetDirectoryName(fullPath); + if (directory != null) + { + Directory.CreateDirectory(directory); + } + + await File.WriteAllTextAsync(fullPath, content, TestContext.Current.CancellationToken); + } + } +} diff --git a/util/Server/Program.cs b/util/Server/Program.cs index a2d7e5f687..3d563830ab 100644 --- a/util/Server/Program.cs +++ b/util/Server/Program.cs @@ -6,6 +6,13 @@ namespace Bit.Server; public class Program { public static void Main(string[] args) + { + var builder = CreateWebHostBuilder(args); + var host = builder.Build(); + host.Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) { var config = new ConfigurationBuilder() .AddCommandLine(args) @@ -37,7 +44,6 @@ public class Program builder.UseWebRoot(webRoot); } - var host = builder.Build(); - host.Run(); + return builder; } }