Do Pagination missing sortBy and sortDirection parameters
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -434,10 +434,11 @@ public class VerificationTests : IAsyncLifetime
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test: Entry queue returns Uploaded, InEntry, and Rejected batches.
|
||||
/// Test: Entry queue returns InEntry and Rejected batches, but NOT Uploaded
|
||||
/// (Uploaded batches must be assigned first, which transitions them to InEntry).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task EntryQueue_ReturnsUploadedInEntryAndRejectedBatches()
|
||||
public async Task EntryQueue_ReturnsInEntryAndRejectedBatches_ExcludesUploaded()
|
||||
{
|
||||
// Arrange
|
||||
using var scope = _fixture.Services.CreateScope();
|
||||
@@ -451,7 +452,7 @@ public class VerificationTests : IAsyncLifetime
|
||||
var pendingBatch = await BatchSeedHelper.SeedBatchInPendingVerificationAsync(
|
||||
db, (await BatchSeedHelper.UserIdAsync(db, "entry2")));
|
||||
|
||||
// Seed an Uploaded batch directly
|
||||
// Seed an Uploaded batch directly (should NOT appear — must be assigned first)
|
||||
var uploadedBatchId = Guid.NewGuid();
|
||||
db.DigitizationBatches.Add(new DigitizationBatch
|
||||
{
|
||||
@@ -495,12 +496,12 @@ public class VerificationTests : IAsyncLifetime
|
||||
.ToList();
|
||||
|
||||
batchIds.Should().Contain(rejectedBatch.Id.ToString());
|
||||
batchIds.Should().Contain(uploadedBatchId.ToString());
|
||||
batchIds.Should().NotContain(uploadedBatchId.ToString(),
|
||||
"Uploaded batches must be assigned before appearing in the entry queue");
|
||||
batchIds.Should().NotContain(pendingBatch.Id.ToString());
|
||||
|
||||
var entryStatuses = new[]
|
||||
{
|
||||
BatchStatus.Uploaded.ToDbString(),
|
||||
BatchStatus.InEntry.ToDbString(),
|
||||
BatchStatus.Rejected.ToDbString()
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user