Files

276 lines
10 KiB
C#

using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Tests for batch cancellation and sort parameters.
/// </summary>
[Collection("Database")]
public class BatchOperationsTests : IAsyncLifetime
{
private readonly ApiFixture _fixture;
private HttpClient _adminClient = null!;
public BatchOperationsTests(ApiFixture fixture) => _fixture = fixture;
public async Task InitializeAsync()
{
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await DbResetHelper.ResetAsync(db);
await DataSeeder.SeedAsync(db);
_adminClient = await AuthHelper.LoginAsync(_fixture, "admin1");
}
public Task DisposeAsync() => Task.CompletedTask;
// ---------------------------------------------------------------
// Cancellation tests
// ---------------------------------------------------------------
[Fact]
public async Task Cancel_UploadedBatch_Returns200AndTransitionsToCancelled()
{
// Arrange: upload a batch
var batchId = await UploadBatchAsync();
// Act
var response = await _adminClient.PostAsJsonAsync(
$"/api/v1/digitization-batches/{batchId}/cancel",
new { Reason = "Wrong document scanned" });
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
body.GetProperty("data").GetProperty("status").GetString()
.Should().Be("CANCELLED");
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var batch = await db.DigitizationBatches.FindAsync(batchId);
batch!.Status.Should().Be(BatchStatus.Cancelled);
var cancelEvent = await db.DigitizationEvents
.FirstOrDefaultAsync(e => e.BatchId == batchId
&& e.EventType == DigitizationEventType.Cancelled);
cancelEvent.Should().NotBeNull();
}
[Fact]
public async Task Cancel_InEntryBatch_Returns200()
{
// Arrange: upload + assign (transitions to IN_ENTRY)
var batchId = await UploadBatchAsync();
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var entryUserId = await BatchSeedHelper.UserIdAsync(db, "entry1");
await _adminClient.PatchAsJsonAsync(
$"/api/v1/digitization-batches/{batchId}/assign",
new { EntryClerkUserId = entryUserId });
// Act
var response = await _adminClient.PostAsJsonAsync(
$"/api/v1/digitization-batches/{batchId}/cancel",
new { Reason = "Test upload during training" });
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
// Reload to verify
db.ChangeTracker.Clear();
var batch = await db.DigitizationBatches.FindAsync(batchId);
batch!.Status.Should().Be(BatchStatus.Cancelled);
}
[Fact]
public async Task Cancel_RejectedBatch_Returns200()
{
// Arrange: seed a rejected batch
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var entryUserId = await BatchSeedHelper.UserIdAsync(db, "entry1");
var rejectedBatch = await BatchSeedHelper.SeedBatchInRejectedAsync(db, entryUserId);
// Act
var response = await _adminClient.PostAsJsonAsync(
$"/api/v1/digitization-batches/{rejectedBatch.Id}/cancel",
new { Reason = "Patient linked incorrectly" });
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
[Fact]
public async Task Cancel_PromotedBatch_Returns409()
{
// Arrange: drive batch to promoted
var batchId = await BatchPipelineHelper.CreateAndVerifyBatchAsync(_fixture);
var approverClient = await AuthHelper.LoginAsync(_fixture, "approver1");
approverClient.DefaultRequestHeaders.Add("Idempotency-Key", Guid.NewGuid().ToString());
await approverClient.PostAsJsonAsync(
$"/api/v1/digitization-batches/{batchId}/approve",
new ApproveRequest());
approverClient.DefaultRequestHeaders.Remove("Idempotency-Key");
// Act
var response = await _adminClient.PostAsJsonAsync(
$"/api/v1/digitization-batches/{batchId}/cancel",
new { Reason = "Should not work" });
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Conflict);
}
[Fact]
public async Task Cancel_NonAdmin_Returns403()
{
var batchId = await UploadBatchAsync();
var entryClient = await AuthHelper.LoginAsync(_fixture, "entry1");
var response = await entryClient.PostAsJsonAsync(
$"/api/v1/digitization-batches/{batchId}/cancel",
new { Reason = "Should not be allowed" });
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
}
[Fact]
public async Task Cancel_CancelledBatchExcludedFromEntryQueue()
{
// Arrange: seed an IN_ENTRY batch and cancel it
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var entryUserId = await BatchSeedHelper.UserIdAsync(db, "entry1");
var batchId = await UploadBatchAsync();
await _adminClient.PatchAsJsonAsync(
$"/api/v1/digitization-batches/{batchId}/assign",
new { EntryClerkUserId = entryUserId });
await _adminClient.PostAsJsonAsync(
$"/api/v1/digitization-batches/{batchId}/cancel",
new { Reason = "Cleanup" });
// Act
var entryClient = await AuthHelper.LoginAsync(_fixture, "entry1");
var response = await entryClient.GetAsync("/api/v1/work-queue/entry");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
var items = body.GetProperty("data").GetProperty("items");
var batchIds = Enumerable.Range(0, items.GetArrayLength())
.Select(i => items[i].GetProperty("batchId").GetString())
.ToList();
batchIds.Should().NotContain(batchId.ToString());
}
// ---------------------------------------------------------------
// Sort parameter tests
// ---------------------------------------------------------------
[Fact]
public async Task List_SortByCreatedAtAsc_ReturnsOldestFirst()
{
// Arrange: create two batches
var batchId1 = await UploadBatchAsync();
await Task.Delay(50);
var batchId2 = await UploadBatchAsync();
// Act
var response = await _adminClient.GetAsync(
"/api/v1/digitization-batches?sortBy=createdAt&sortDirection=asc");
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
var items = body.GetProperty("data").GetProperty("items");
var ids = Enumerable.Range(0, items.GetArrayLength())
.Select(i => items[i].GetProperty("id").GetGuid())
.ToList();
var idx1 = ids.IndexOf(batchId1);
var idx2 = ids.IndexOf(batchId2);
idx1.Should().BeLessThan(idx2, "oldest batch should come first with asc sort");
}
[Fact]
public async Task List_InvalidSortField_Returns422()
{
var response = await _adminClient.GetAsync(
"/api/v1/digitization-batches?sortBy=invalidField");
response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
}
[Fact]
public async Task WorkQueue_SortByCreatedAtDesc_ReturnsNewestFirst()
{
// Arrange: seed two pending verification batches with different ages
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var entryUserId = await BatchSeedHelper.UserIdAsync(db, "entry1");
var olderBatch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(db, entryUserId);
var newerBatch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(db, entryUserId);
// Make the "older" batch actually older
olderBatch.CreatedAt = DateTimeOffset.UtcNow.AddHours(-2);
olderBatch.UpdatedAt = DateTimeOffset.UtcNow.AddHours(-2);
newerBatch.CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5);
newerBatch.UpdatedAt = DateTimeOffset.UtcNow.AddMinutes(-5);
await db.SaveChangesAsync();
// Act
var verifierClient = await AuthHelper.LoginAsync(_fixture, "verifier1");
var response = await verifierClient.GetAsync(
"/api/v1/work-queue/verification?sortBy=createdAt&sortDirection=desc");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
var items = body.GetProperty("data").GetProperty("items");
var batchIds = Enumerable.Range(0, items.GetArrayLength())
.Select(i => items[i].GetProperty("batchId").GetString())
.ToList();
var idxNewer = batchIds.IndexOf(newerBatch.Id.ToString());
var idxOlder = batchIds.IndexOf(olderBatch.Id.ToString());
idxNewer.Should().BeLessThan(idxOlder, "newest batch should come first with desc sort");
}
// ---------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------
private async Task<Guid> UploadBatchAsync()
{
var intakeClient = await AuthHelper.LoginAsync(_fixture, "intake1");
var fileContent = new ByteArrayContent(
System.Text.Encoding.ASCII.GetBytes(
$"%PDF-1.4\n%%EOF\n%test-{Guid.NewGuid()}"));
fileContent.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf");
var formData = new MultipartFormDataContent
{
{ fileContent, "file", "test.pdf" },
{ new StringContent("VITALS_SHEET"), "batchType" }
};
var response = await intakeClient.PostAsync("/api/v1/digitization-batches", formData);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadFromJsonAsync<JsonElement>();
return body.GetProperty("data").GetProperty("id").GetGuid();
}
}