276 lines
10 KiB
C#
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();
|
|
}
|
|
}
|