feature: Site & Gateway Registry + Clinical Sync Contracts

This commit is contained in:
voltsrage
2026-06-23 04:31:15 +08:00
parent c9994b1ba2
commit efe5419da4
24 changed files with 547 additions and 19 deletions
@@ -40,10 +40,18 @@ public class ClinicalRefactorEndToEndTests : IAsyncLifetime
await ScenarioReplayHelper.WaitForAlertTypeAsync(
_fixture.Services, encounterId, AlertType.SofaSepsis, TimeSpan.FromSeconds(30));
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var bundle = await db.SepsisBundles.SingleAsync(b => b.EncounterId == encounterId);
bundle.TriggeringAlertType.Should().Be("SOFA_SEPSIS");
SepsisBundle? bundle = null;
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(10);
while (DateTime.UtcNow < deadline)
{
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
bundle = await db.SepsisBundles.FirstOrDefaultAsync(b => b.EncounterId == encounterId);
if (bundle is not null) break;
await Task.Delay(500);
}
bundle.Should().NotBeNull("expected sepsis bundle for encounter after SOFA_SEPSIS alert");
bundle!.TriggeringAlertType.Should().Be("SOFA_SEPSIS");
}
[Fact]
@@ -0,0 +1,97 @@
using System.Net;
using System.Net.Http.Json;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
[Collection("Integration")]
public class GatewayRegistryTests : IAsyncLifetime
{
private readonly ApiFixture _fixture;
private readonly HttpClient _client;
private Guid _siteId;
private Guid _gatewayId;
public GatewayRegistryTests(ApiFixture fixture)
{
_fixture = fixture;
_client = fixture.CreateClient();
}
public async Task InitializeAsync()
{
_client.AsAdmin();
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await DbResetHelper.ResetAsync(db);
await GatewayRegistrySeeder.SeedAsync(db);
_siteId = GatewayRegistrySeeder.DemoSiteId;
_gatewayId = GatewayRegistrySeeder.DemoGatewayId;
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task RegisterGateway_UnderSite_Returns201()
{
var resp = await _client.PostAsJsonAsync(
$"/api/v1/sites/{_siteId}/gateways",
new { gatewayCode = "GW-ICU-1A", department = "ICU" });
resp.StatusCode.Should().Be(HttpStatusCode.Created);
var body = await resp.Content.ReadFromJsonAsync<ApiResponse<WardGatewayResponse>>();
body!.Data!.GatewayCode.Should().Be("GW-ICU-1A");
}
[Fact]
public async Task Heartbeat_UpdatesStatusAndBufferDepth()
{
_client.WithGatewayApiKey(_gatewayId);
var resp = await _client.PatchAsJsonAsync(
$"/api/v1/gateways/{_gatewayId}/heartbeat",
new { status = "ONLINE", bufferDepth = 42, reportedAtUtc = DateTimeOffset.UtcNow });
resp.StatusCode.Should().Be(HttpStatusCode.OK);
_client.AsAdmin();
var get = await _client.GetAsync($"/api/v1/gateways/{_gatewayId}");
var body = await get.Content.ReadFromJsonAsync<ApiResponse<WardGatewayResponse>>();
body!.Data!.Status.Should().Be("ONLINE");
body.Data.ReportedBufferDepth.Should().Be(42);
body.Data.LastHeartbeatAt.Should().NotBeNull();
}
[Fact]
public async Task Heartbeat_InvalidApiKey_Returns401()
{
_client.WithGatewayApiKey(_gatewayId, apiKey: "wrong-key");
var resp = await _client.PatchAsJsonAsync(
$"/api/v1/gateways/{_gatewayId}/heartbeat",
new { status = "ONLINE", bufferDepth = 0, reportedAtUtc = DateTimeOffset.UtcNow });
resp.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
[Fact]
public async Task Heartbeat_DegradedStatus_Persisted()
{
_client.WithGatewayApiKey(_gatewayId);
await _client.PatchAsJsonAsync(
$"/api/v1/gateways/{_gatewayId}/heartbeat",
new { status = "DEGRADED", bufferDepth = 847, reportedAtUtc = DateTimeOffset.UtcNow });
_client.AsAdmin();
var get = await _client.GetAsync($"/api/v1/gateways/{_gatewayId}");
var body = await get.Content.ReadFromJsonAsync<ApiResponse<WardGatewayResponse>>();
body!.Data!.Status.Should().Be("DEGRADED");
body.Data.ReportedBufferDepth.Should().Be(847);
}
[Fact]
public async Task ListGateways_FilterByDepartment()
{
await _client.PostAsJsonAsync(
$"/api/v1/sites/{_siteId}/gateways",
new { gatewayCode = "GW-GM-8B", department = "GENERAL_MEDICINE" });
var resp = await _client.GetAsync($"/api/v1/sites/{_siteId}/gateways?department=ICU");
var body = await resp.Content.ReadFromJsonAsync<ApiResponse<List<WardGatewayResponse>>>();
body!.Data.Should().OnlyContain(g => g.Department == "ICU");
}
}
@@ -11,14 +11,23 @@ public static class AuthHelper
}
}
public static void AsAdmin(this HttpClient client) =>
public static void AsAdmin(this HttpClient client)
{
client.DefaultRequestHeaders.Remove("X-Test-Role");
client.DefaultRequestHeaders.Add("X-Test-Role", "ADMIN");
}
public static void AsPhysician(this HttpClient client) =>
public static void AsPhysician(this HttpClient client)
{
client.DefaultRequestHeaders.Remove("X-Test-Role");
client.DefaultRequestHeaders.Add("X-Test-Role", "PHYSICIAN");
}
public static void AsIntegration(this HttpClient client) =>
public static void AsIntegration(this HttpClient client)
{
client.DefaultRequestHeaders.Remove("X-Test-Role");
client.DefaultRequestHeaders.Add("X-Test-Role", "INTEGRATION");
}
public static void ClearAuth(this HttpClient client)
{