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;
}
}