stabilize tests
This commit is contained in:
@@ -5,25 +5,32 @@
|
||||
// Description: Integration tests for evidence bundle export API endpoints.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
using EvidenceLockerProgram = StellaOps.EvidenceLocker.WebService.Program;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moq;
|
||||
using StellaOps.EvidenceLocker.Api;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.EvidenceLocker.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for export API endpoints.
|
||||
/// Uses the shared EvidenceLockerWebApplicationFactory (via Collection fixture)
|
||||
/// instead of raw WebApplicationFactory<Program> to avoid loading real
|
||||
/// infrastructure services (database, auth, background services) which causes
|
||||
/// the test process to hang and consume excessive memory.
|
||||
/// </summary>
|
||||
public sealed class ExportEndpointsTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
[Collection(EvidenceLockerTestCollection.Name)]
|
||||
public sealed class ExportEndpointsTests
|
||||
{
|
||||
private readonly WebApplicationFactory<Program> _factory;
|
||||
private readonly EvidenceLockerWebApplicationFactory _factory;
|
||||
|
||||
public ExportEndpointsTests(WebApplicationFactory<Program> factory)
|
||||
public ExportEndpointsTests(EvidenceLockerWebApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
@@ -45,11 +52,11 @@ public sealed class ExportEndpointsTests : IClassFixture<WebApplicationFactory<P
|
||||
EstimatedSize = 1024
|
||||
});
|
||||
|
||||
var client = CreateClientWithMock(mockService.Object);
|
||||
using var scope = CreateClientWithMock(mockService.Object);
|
||||
var request = new ExportTriggerRequest();
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsJsonAsync("/api/v1/bundles/bundle-123/export", request);
|
||||
var response = await scope.Client.PostAsJsonAsync("/api/v1/bundles/bundle-123/export", request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
@@ -72,11 +79,11 @@ public sealed class ExportEndpointsTests : IClassFixture<WebApplicationFactory<P
|
||||
It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new ExportJobResult { IsNotFound = true });
|
||||
|
||||
var client = CreateClientWithMock(mockService.Object);
|
||||
using var scope = CreateClientWithMock(mockService.Object);
|
||||
var request = new ExportTriggerRequest();
|
||||
|
||||
// Act
|
||||
var response = await client.PostAsJsonAsync("/api/v1/bundles/nonexistent/export", request);
|
||||
var response = await scope.Client.PostAsJsonAsync("/api/v1/bundles/nonexistent/export", request);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
@@ -97,10 +104,10 @@ public sealed class ExportEndpointsTests : IClassFixture<WebApplicationFactory<P
|
||||
CompletedAt = DateTimeOffset.Parse("2026-01-07T12:00:00Z")
|
||||
});
|
||||
|
||||
var client = CreateClientWithMock(mockService.Object);
|
||||
using var scope = CreateClientWithMock(mockService.Object);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/v1/bundles/bundle-123/export/exp-123");
|
||||
var response = await scope.Client.GetAsync("/api/v1/bundles/bundle-123/export/exp-123");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
@@ -126,10 +133,10 @@ public sealed class ExportEndpointsTests : IClassFixture<WebApplicationFactory<P
|
||||
EstimatedTimeRemaining = "30s"
|
||||
});
|
||||
|
||||
var client = CreateClientWithMock(mockService.Object);
|
||||
using var scope = CreateClientWithMock(mockService.Object);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/v1/bundles/bundle-123/export/exp-123");
|
||||
var response = await scope.Client.GetAsync("/api/v1/bundles/bundle-123/export/exp-123");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
|
||||
@@ -149,10 +156,10 @@ public sealed class ExportEndpointsTests : IClassFixture<WebApplicationFactory<P
|
||||
.Setup(s => s.GetExportStatusAsync("bundle-123", "nonexistent", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((ExportJobStatus?)null);
|
||||
|
||||
var client = CreateClientWithMock(mockService.Object);
|
||||
using var scope = CreateClientWithMock(mockService.Object);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/v1/bundles/bundle-123/export/nonexistent");
|
||||
var response = await scope.Client.GetAsync("/api/v1/bundles/bundle-123/export/nonexistent");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
@@ -174,10 +181,10 @@ public sealed class ExportEndpointsTests : IClassFixture<WebApplicationFactory<P
|
||||
FileName = "evidence-bundle-123.tar.gz"
|
||||
});
|
||||
|
||||
var client = CreateClientWithMock(mockService.Object);
|
||||
using var scope = CreateClientWithMock(mockService.Object);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/v1/bundles/bundle-123/export/exp-123/download");
|
||||
var response = await scope.Client.GetAsync("/api/v1/bundles/bundle-123/export/exp-123/download");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
@@ -196,10 +203,10 @@ public sealed class ExportEndpointsTests : IClassFixture<WebApplicationFactory<P
|
||||
Status = ExportJobStatusEnum.Processing
|
||||
});
|
||||
|
||||
var client = CreateClientWithMock(mockService.Object);
|
||||
using var scope = CreateClientWithMock(mockService.Object);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/v1/bundles/bundle-123/export/exp-123/download");
|
||||
var response = await scope.Client.GetAsync("/api/v1/bundles/bundle-123/export/exp-123/download");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
|
||||
@@ -214,10 +221,10 @@ public sealed class ExportEndpointsTests : IClassFixture<WebApplicationFactory<P
|
||||
.Setup(s => s.GetExportFileAsync("bundle-123", "nonexistent", It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((ExportFileResult?)null);
|
||||
|
||||
var client = CreateClientWithMock(mockService.Object);
|
||||
using var scope = CreateClientWithMock(mockService.Object);
|
||||
|
||||
// Act
|
||||
var response = await client.GetAsync("/api/v1/bundles/bundle-123/export/nonexistent/download");
|
||||
var response = await scope.Client.GetAsync("/api/v1/bundles/bundle-123/export/nonexistent/download");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
@@ -241,7 +248,7 @@ public sealed class ExportEndpointsTests : IClassFixture<WebApplicationFactory<P
|
||||
Status = "pending"
|
||||
});
|
||||
|
||||
var client = CreateClientWithMock(mockService.Object);
|
||||
using var scope = CreateClientWithMock(mockService.Object);
|
||||
var request = new ExportTriggerRequest
|
||||
{
|
||||
CompressionLevel = 9,
|
||||
@@ -250,7 +257,7 @@ public sealed class ExportEndpointsTests : IClassFixture<WebApplicationFactory<P
|
||||
};
|
||||
|
||||
// Act
|
||||
await client.PostAsJsonAsync("/api/v1/bundles/bundle-123/export", request);
|
||||
await scope.Client.PostAsJsonAsync("/api/v1/bundles/bundle-123/export", request);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(capturedRequest);
|
||||
@@ -259,9 +266,37 @@ public sealed class ExportEndpointsTests : IClassFixture<WebApplicationFactory<P
|
||||
Assert.False(capturedRequest.IncludeRekorProofs);
|
||||
}
|
||||
|
||||
private HttpClient CreateClientWithMock(IExportJobService mockService)
|
||||
/// <summary>
|
||||
/// Wraps a derived WebApplicationFactory and HttpClient so both are disposed together.
|
||||
/// Previously, WithWebHostBuilder() created a new factory per test that was never disposed,
|
||||
/// leaking TestServer instances and consuming gigabytes of memory.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Wraps a derived WebApplicationFactory and HttpClient so both are disposed together.
|
||||
/// Previously, WithWebHostBuilder() created a new factory per test that was never disposed,
|
||||
/// leaking TestServer instances and consuming gigabytes of memory.
|
||||
/// </summary>
|
||||
private sealed class MockScope : IDisposable
|
||||
{
|
||||
return _factory.WithWebHostBuilder(builder =>
|
||||
private readonly WebApplicationFactory<EvidenceLockerProgram> _derivedFactory;
|
||||
public HttpClient Client { get; }
|
||||
|
||||
public MockScope(WebApplicationFactory<EvidenceLockerProgram> derivedFactory)
|
||||
{
|
||||
_derivedFactory = derivedFactory;
|
||||
Client = derivedFactory.CreateClient();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Client.Dispose();
|
||||
_derivedFactory.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private MockScope CreateClientWithMock(IExportJobService mockService)
|
||||
{
|
||||
var derivedFactory = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
@@ -276,6 +311,7 @@ public sealed class ExportEndpointsTests : IClassFixture<WebApplicationFactory<P
|
||||
// Add mock
|
||||
services.AddSingleton(mockService);
|
||||
});
|
||||
}).CreateClient();
|
||||
});
|
||||
return new MockScope(derivedFactory);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user