Files
SocialPub/PrivaPub.Tests/Support/Host/PrivaPubHost.cs
T
thepraandClaude Opus 5.5 5e34517e73 T3: the whole server under test
PrivaPubHost is a WebApplicationFactory<Program> on the fixture's database, configured only
through UseSetting (visible before Build, unlike ConfigureAppConfiguration). It drops the
background workers so tests run the jobs they queue (Jobs.Run, RunInbox), gives each client
its own address for the rate limiter, and has a SecureMode variant. Accounts signs up roots,
adds personas and gets Mastodon tokens through the real /oauth code flow. RemoteActor signs
HttpRequestMessages for the real /peasants routes; Peer records bodies and headers and
serves files with ranges and text pages.

Program registers the Guid serializer with TryRegisterSerializer, so a second host in one
process starts; the fixture runs the migrations in production's order before any test.

HostBootTests: the host shares the fixture database; the harness handles exactly the
activities the server registers; every job kind has one handler; every controller and page
model can be made; the service graph validates with ValidateOnBuild and ValidateScopes;
Swagger is 404 outside Development; a persona's token never names its root; a signed DM
through the real /mouth route is queued, processed and stored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
2026-10-03 10:43:03 +02:00

161 lines
5.2 KiB
C#

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using PrivaPub.Api.Mastodon.Auth;
using PrivaPub.Domain.Media;
using PrivaPub.Infrastructure.Jobs;
using System.Net;
namespace PrivaPub.Tests.Support.Host
{
public class PrivaPubHost : WebApplicationFactory<Program>
{
public const string Host = "privapub.test";
public const string Base = "https://" + Host;
public const string ClientHeader = "X-Test-Client";
static readonly SemaphoreSlim Boot = new(1, 1);
static readonly Type[] Unwanted = { typeof(JobWorker), typeof(MediaJanitor), typeof(OAuthPruner) };
static PrivaPubHost _shared;
readonly string _mediaRoot = Path.Combine(Path.GetTempPath(), $"privapub-tests-{Guid.NewGuid():N}");
public IReadOnlyList<ServiceDescriptor> Registered { get; private set; }
public static async Task<PrivaPubHost> Shared()
{
await Boot.WaitAsync();
try
{
if (_shared == default)
{
var host = new PrivaPubHost();
_ = host.Services;
_shared = host;
}
return _shared;
}
finally
{
Boot.Release();
}
}
protected virtual IEnumerable<KeyValuePair<string, string>> Settings() => new Dictionary<string, string>
{
["MongoSettings:ConnectionString"] = MongoFixture.Connection,
["MongoSettings:Database"] = MongoFixture.Database,
["MongoSettings:LogsDatabase"] = "logs",
["AppConfiguration:Version"] = "0.0.0-test",
["AppConfiguration:BackendBaseAddress"] = Base,
["AppConfiguration:MaxAllowedUploadFiles"] = "3",
["AppConfiguration:MaxAllowedFileSize"] = "2097152",
["AppConfiguration:SupportedLanguages:0"] = "en",
["AppConfiguration:SupportedLanguages:1"] = "it",
["AppConfiguration:HashingOptions:Iterations"] = "1000",
["AppConfiguration:Jwt:Key"] = "privapub-tests-only-signing-key-0123456789abcdef0123456789abcdef",
["AppConfiguration:Jwt:Issuer"] = Base,
["AppConfiguration:Jwt:Audience"] = Base,
["AppConfiguration:Jwt:HoursTimeout"] = "1",
["AppConfiguration:EmailConfiguration:SmtpServer"] = "127.0.0.1",
["AppConfiguration:EmailConfiguration:SmtpPort"] = "9",
["AppConfiguration:EmailConfiguration:UseSSL"] = "false",
["AppConfiguration:EmailConfiguration:SmtpUsername"] = "nobody@privapub.test",
["AppConfiguration:EmailConfiguration:SmtpPassword"] = "none",
["Federation:AllowPrivateNetworks"] = "true",
["Federation:AllowPlainHttp"] = "true",
["Federation:FetchLinkPreviews"] = "false",
["Media:Root"] = _mediaRoot,
["Logging:LogLevel:Default"] = "Warning",
["Serilog:MinimumLevel:Default"] = Environment.GetEnvironmentVariable("PRIVAPUB_TEST_LOGS") == "1" ? "Information" : "Fatal"
};
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
foreach (var (key, value) in Settings())
builder.UseSetting(key, value);
builder.ConfigureTestServices(services =>
{
foreach (var hosted in services.Where(s => s.ServiceType == typeof(IHostedService) && Unwanted.Contains(s.ImplementationType)).ToList())
services.Remove(hosted);
services.AddDataProtection().UseEphemeralDataProtectionProvider();
services.AddSingleton<IStartupFilter, ClientAddressFilter>();
Registered = services.ToList();
});
}
protected override void ConfigureClient(HttpClient client)
{
client.BaseAddress = new Uri(Base + "/");
client.DefaultRequestHeaders.Add(ClientHeader, $"10.{Random.Shared.Next(256)}.{Random.Shared.Next(256)}.{Random.Shared.Next(1, 255)}");
}
public HttpClient Client(bool cookies = false) => CreateClient(new WebApplicationFactoryClientOptions
{
BaseAddress = new Uri(Base + "/"),
AllowAutoRedirect = false,
HandleCookies = cookies
});
public T Get<T>() where T : notnull => Services.GetRequiredService<T>();
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing && Directory.Exists(_mediaRoot))
Directory.Delete(_mediaRoot, recursive: true);
}
sealed class ClientAddressFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) => app =>
{
app.Use(async (context, call) =>
{
if (IPAddress.TryParse(context.Request.Headers[ClientHeader].ToString(), out var address))
context.Connection.RemoteIpAddress = address;
await call(context);
});
next(app);
};
}
}
public sealed class SecureModeHost : PrivaPubHost
{
static readonly SemaphoreSlim Boot = new(1, 1);
static SecureModeHost _shared;
public static new async Task<SecureModeHost> Shared()
{
await PrivaPubHost.Shared();
await Boot.WaitAsync();
try
{
if (_shared == default)
{
var host = new SecureModeHost();
_ = host.Services;
_shared = host;
}
return _shared;
}
finally
{
Boot.Release();
}
}
protected override IEnumerable<KeyValuePair<string, string>> Settings() =>
base.Settings().Append(new KeyValuePair<string, string>("Federation:SecureMode", "true"));
}
}