Ctrl K
ComunidadGuías…

Nostos.Backend.Tests/Portability/MigrationHttpAcceptanceTests.cs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
using System.Net;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Xunit;

namespace Nostos.Backend.Tests.Portability;

/// <summary>
/// Slice 11 end-to-end acceptance over the real Program host (Kestrel, real
/// SQLite, real transfer root, real worker) with the production phase handlers:
/// a browser upload becomes a durable prepared import that survives a host
/// restart, and an export job produces a sealed artifact that streams back with
/// HTTP ranges. Engine-level crash seams remain the exhaustive proof; these
/// tests close the transport-to-real-handler gap.
/// </summary>
public sealed class MigrationHttpAcceptanceTests
{
    [Fact]
    public async Task Upload_complete_restart_then_real_worker_reaches_ready_to_activate()
    {
        await using var h = new MigrationHttpHarness { UseRealPhaseHandlers = true }.Start();
        var archive = await MigrationArchiveJobTestSupport.ExportRepresentativeAsync();

        var jobId = await h.CreateImportJobAsync("acceptance-import");
        var (sessionStatus, _, _) = await h.CreateSessionAsync(jobId, archive);
        sessionStatus.Should().Be(HttpStatusCode.Created);

        for (var index = 0; index < MigrationHttpHarness.ChunkCount(archive.LongLength, 4 * 1024 * 1024); index++)
        {
            (await h.UploadChunkAsync(jobId, archive, index)).Status.Should().Be(HttpStatusCode.OK);
        }

        (await h.CompleteAsync(jobId)).Status.Should().Be(HttpStatusCode.OK);

        // A full host restart between a sealed upload and preparation: the
        // durable session and the real worker must finish the job afterwards.
        await h.RestartAsync();

        var ready = await h.WaitForJobStateAsync(
            jobId,
            "ReadyToActivate",
            timeout: TimeSpan.FromSeconds(60));
        ready.RootElement.GetProperty("job").GetProperty("state").GetString().Should().Be("ReadyToActivate");
        ready.RootElement.GetProperty("session").GetProperty("state").GetString().Should().Be("Complete");

        // The prepared descriptor is durable and the live library stayed empty.
        var status = await h.JobStatusAsync(jobId);
        status.RootElement.GetProperty("preparedImport").ValueKind.Should().NotBe(
            System.Text.Json.JsonValueKind.Null,
            "a ReadyToActivate import carries its committed prepared descriptor");
        (await h.WithDb(db => db.Works.CountAsync())).Should().Be(0);
    }

    [Fact]
    public async Task Real_export_job_completes_and_streams_range_downloads()
    {
        await using var h = new MigrationHttpHarness { UseRealPhaseHandlers = true }.Start();

        var (createStatus, body) = await h.CreateJobAsync("Export", "acceptance-export", null);
        createStatus.Should().Be(HttpStatusCode.Created);
        var jobId = body.RootElement.GetProperty("job").GetProperty("id").GetGuid();

        await h.WaitForJobStateAsync(jobId, "Completed", timeout: TimeSpan.FromSeconds(60));

        var full = await h.Client.GetAsync(
            $"/api/portability/migration/jobs/{jobId}/export-download",
            HttpCompletionOption.ResponseHeadersRead);
        full.StatusCode.Should().Be(HttpStatusCode.OK);
        full.Headers.AcceptRanges.Should().ContainSingle("bytes");
        var bytes = await full.Content.ReadAsByteArrayAsync();
        bytes.Length.Should().BeGreaterThan(0);

        using var rangedRequest = new HttpRequestMessage(
            HttpMethod.Get,
            $"/api/portability/migration/jobs/{jobId}/export-download");
        rangedRequest.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(0, 31);
        using var ranged = await h.Client.SendAsync(rangedRequest, HttpCompletionOption.ResponseHeadersRead);
        ranged.StatusCode.Should().Be(HttpStatusCode.PartialContent);
        (await ranged.Content.ReadAsByteArrayAsync()).Should().Equal(bytes[..32]);
    }
}