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 PrivaPub.Infrastructure.Statistics; using System.Net; namespace PrivaPub.Tests.Support.Host { public class PrivaPubHost : WebApplicationFactory { 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), typeof(StatisticsSchedule) }; static PrivaPubHost _shared; readonly string _mediaRoot = Path.Combine(Path.GetTempPath(), $"privapub-tests-{Guid.NewGuid():N}"); public IReadOnlyList Registered { get; private set; } public static async Task Shared() { await Boot.WaitAsync(); try { if (_shared == default) { var host = new PrivaPubHost(); _ = host.Services; _shared = host; } return _shared; } finally { Boot.Release(); } } protected virtual IEnumerable> Settings() => new Dictionary { ["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", ["Registrations:Mode"] = "Open", ["Statistics:Geo:AutoUpdate"] = "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(); 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() where T : notnull => Services.GetRequiredService(); 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 Configure(Action 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 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> Settings() => base.Settings().Append(new KeyValuePair("Federation:SecureMode", "true")); } }