feature: Ward Gateway Service (Local-First Clinical Path)

This commit is contained in:
voltsrage
2026-06-23 16:45:38 +08:00
parent d8e142fffe
commit 1bf8359097
100 changed files with 5474 additions and 4 deletions
@@ -0,0 +1,138 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
[Collection("GatewayIntegration")]
public class WardGatewayLocalPathTests : IAsyncLifetime
{
private readonly GatewayApiFixture _fixture;
private readonly HttpClient _client;
private Guid _encounterId;
public WardGatewayLocalPathTests(GatewayApiFixture fixture)
{
_fixture = fixture;
_client = fixture.CreateClient();
}
public async Task InitializeAsync()
{
(_, _encounterId) = await GatewayTestSeeder.SeedActiveEncounterAsync(_fixture.Services);
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task CriticalObservation_CreatesAlertLocally()
{
var resp = await _client.PostAsJsonAsync(
$"/api/v1/encounters/{_encounterId}/observations",
new
{
observationCode = "HEART_RATE",
value = 160,
unit = "bpm",
source = "DEVICE",
recordedAt = DateTimeOffset.UtcNow,
idempotencyKey = Guid.NewGuid().ToString()
});
resp.StatusCode.Should().Be(HttpStatusCode.Created);
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<GatewayDbContext>();
(await db.ClinicalAlerts.CountAsync()).Should().BeGreaterThan(0);
(await db.BufferedSyncItems.CountAsync(b => !b.Synced)).Should().BeGreaterThan(0);
}
[Fact]
public async Task DuplicateIdempotencyKey_ReturnsExisting()
{
var key = Guid.NewGuid().ToString();
var body = new
{
observationCode = "HEART_RATE",
value = 80,
unit = "bpm",
source = "DEVICE",
recordedAt = DateTimeOffset.UtcNow,
idempotencyKey = key
};
await _client.PostAsJsonAsync($"/api/v1/encounters/{_encounterId}/observations", body);
var dup = await _client.PostAsJsonAsync($"/api/v1/encounters/{_encounterId}/observations", body);
dup.StatusCode.Should().Be(HttpStatusCode.OK);
var json = await dup.Content.ReadFromJsonAsync<JsonDocument>();
json!.RootElement.GetProperty("data").GetProperty("duplicate").GetBoolean().Should().BeTrue();
}
[Fact]
public async Task Acknowledge_WritesBufferedAck()
{
await _client.PostAsJsonAsync(
$"/api/v1/encounters/{_encounterId}/observations",
new
{
observationCode = "HEART_RATE",
value = 160,
unit = "bpm",
source = "DEVICE",
recordedAt = DateTimeOffset.UtcNow,
idempotencyKey = Guid.NewGuid().ToString()
});
var alertsResp = await _client.GetAsync($"/api/v1/encounters/{_encounterId}/alerts");
alertsResp.StatusCode.Should().Be(HttpStatusCode.OK);
var alertsDoc = await alertsResp.Content.ReadFromJsonAsync<JsonDocument>();
var alertId = alertsDoc!.RootElement
.GetProperty("data").GetProperty("items")[0].GetProperty("id").GetGuid();
var ackResp = await _client.PostAsJsonAsync(
$"/api/v1/alerts/{alertId}/acknowledge",
new AcknowledgeAlertRequest("Reviewing at bedside."));
ackResp.StatusCode.Should().Be(HttpStatusCode.OK);
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<GatewayDbContext>();
(await db.BufferedSyncItems.CountAsync(b =>
b.ItemType == BufferedSyncItemType.Ack && !b.Synced)).Should().Be(1);
}
[Fact]
public async Task CentralDown_ReachabilityReportsDegraded()
{
await Task.Delay(TimeSpan.FromSeconds(2));
using var scope = _fixture.Services.CreateScope();
var reachability = scope.ServiceProvider.GetRequiredService<CentralReachabilityService>();
reachability.IsCentralReachable.Should().BeFalse();
}
[Fact]
public async Task WarningObservation_CreatesWarningNoPage()
{
var resp = await _client.PostAsJsonAsync(
$"/api/v1/encounters/{_encounterId}/observations",
new
{
observationCode = "HEART_RATE",
value = 105,
unit = "bpm",
source = "DEVICE",
recordedAt = DateTimeOffset.UtcNow,
idempotencyKey = Guid.NewGuid().ToString()
});
resp.StatusCode.Should().Be(HttpStatusCode.Created);
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<GatewayDbContext>();
var alert = await db.ClinicalAlerts.SingleAsync();
alert.Severity.Should().Be(AlertSeverity.Warning);
alert.AlertType.Should().Be(AlertType.WarningHeartRate);
alert.Status.Should().Be(AlertStatus.Open);
}
}