using System.Net; using System.Net.Http.Json; using System.Text.Json; using FluentAssertions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Nostos.Backend.Services.Library; using Nostos.Backend.Services.Portability; using Nostos.Backend.Services.Portability.Activation; namespace Nostos.Backend.Tests.Portability; /// /// Real in-process SelfHosted host (full Program pipeline) over the same /// files an activation test bed just switched, so the recovery endpoints serve /// a genuine retained copy created by the real activation coordinator. /// internal sealed class RecoveryHttpHarness : IAsyncDisposable { internal ActivationCoordinatorTestBed Bed { get; } private WebApplicationFactory? _factory; internal HttpClient Client { get; private set; } = null!; private RecoveryHttpHarness(ActivationCoordinatorTestBed bed) { Bed = bed; } internal static async Task StartAsync() { var bed = await ActivationCoordinatorTestBed.CreateAsync(populated: true); (await bed.ActivateAsync(confirm: true)).Outcome.Should().Be(SelfHostedActivationOutcome.Completed); var harness = new RecoveryHttpHarness(bed); harness._factory = new HostFactory(harness); harness._factory.UseKestrel(0); harness.Client = harness._factory.CreateClient(); return harness; } internal T GetService() where T : notnull => _factory!.Services.GetRequiredService(); internal async Task RevisionTokenAsync() { await using var scope = _factory!.Services.CreateAsyncScope(); return await scope.ServiceProvider.GetRequiredService() .GetCurrentAsync(default); } internal async Task AwaitRestoreAsync(Guid recoveryId) { var dispatcher = GetService(); await dispatcher.AwaitRestoreFinishedAsync(recoveryId, default); } internal async Task<(HttpStatusCode Status, JsonDocument Body)> SendAsync( HttpMethod method, string path, HttpContent? content = null) { using var request = new HttpRequestMessage(method, path) { Content = content }; using var response = await Client.SendAsync(request); return (response.StatusCode, await MigrationHttpHarness.ReadJsonAsync(response)); } internal async Task<(HttpStatusCode Status, JsonDocument Body)> PostJsonAsync( string path, T payload) { using var content = JsonContent.Create(payload, options: MigrationHttpHarness.Json); return await SendAsync(HttpMethod.Post, path, content); } public async ValueTask DisposeAsync() { Client?.Dispose(); if (_factory is not null) await _factory.DisposeAsync(); await Bed.DisposeAsync(); } private sealed class HostFactory(RecoveryHttpHarness harness) : WebApplicationFactory { protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment("Testing"); builder.UseContentRoot(harness.Bed.Root); builder.UseSetting("Persistence:DatabasePath", harness.Bed.Paths.LiveDatabase); builder.UseSetting("Storage:BooksRoot", harness.Bed.Paths.LiveMedia); builder.UseSetting("Storage:TransferPath", harness.Bed.TransferRoot); builder.UseSetting("Storage:BackupsRoot", Path.Combine(harness.Bed.Root, "backups")); builder.UseSetting("Storage:DiskSafetyMarginBytes", "0"); builder.UseSetting("Storage:DiskSafetyMarginPercent", "0"); builder.ConfigureTestServices(services => { // Retained copies were created with the bed's clock. The HTTP // host must use it too, or fixtures expire as real time passes. services.RemoveAll(); services.AddSingleton(harness.Bed.Clock); }); } } }