39a979b1a7
backend phase 3: identity profiles, patients & nurse bank accounts Add the role-attached identity layer on top of the b2 auth spine: nurse seller profiles (guarded is_verified, read-only aggregates), thin customer payer profiles, first-class patients (tenancy-scoped), and nurse payout bank accounts hardened with an iban_hash uniqueness guard and an automated استعلام شبا IBAN-ownership inquiry. - Four usr tables via one migration (1:1 uniques, UNIQUE(iban_hash), filtered UNIQUE(nurse_id) WHERE is_primary=1, guarded is_verified, encrypted PII, soft-delete on nurse_profiles) - 15 CQRS slices + 4 role-scoped controllers; reads projected + paginated, IBAN masked (last-4); ownership-inquiry endpoints rate-limited - New IBankAccountOwnershipVerifier seam (mock deterministic شبا match) + per-domain repositories on IUnitOfWork + encrypted-PII value converters - Activate FluentValidation repo-wide (validators were never registered) - Handler unit tests + WebApplicationFactory integration tests (76 pass); contract identity-profiles.md + swagger snapshot; docs, handoff & report Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
91 lines
3.8 KiB
C#
91 lines
3.8 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
|
|
namespace Baya.Test.Api;
|
|
|
|
public class PatientsApiTests(BayaApiFactory factory) : IClassFixture<BayaApiFactory>
|
|
{
|
|
private static object PatientBody(string name, string gender = "female") => new
|
|
{
|
|
displayName = name,
|
|
firstName = "F",
|
|
lastName = "L",
|
|
birthDate = "1950-03-01",
|
|
gender,
|
|
bloodType = "O+",
|
|
initialMedicalNotes = "diabetic"
|
|
};
|
|
|
|
[Fact]
|
|
public async Task Create_List_Get_Update_Archive_Lifecycle()
|
|
{
|
|
var client = factory.CreateClient();
|
|
await ProfileTestClient.AuthenticateAsync(factory, client, "09123000001", "customer");
|
|
|
|
var create = await client.PostAsJsonAsync("/api/v1/patients/create", PatientBody("Mother"));
|
|
Assert.Equal(HttpStatusCode.OK, create.StatusCode);
|
|
var created = await AuthTestClient.ReadDataAsync(create);
|
|
var id = created.GetProperty("id").GetInt64();
|
|
Assert.Equal("female", created.GetProperty("gender").GetString());
|
|
Assert.Equal("diabetic", created.GetProperty("initialMedicalNotes").GetString());
|
|
|
|
var list = await client.GetAsync("/api/v1/patients/list");
|
|
var listData = await AuthTestClient.ReadDataAsync(list);
|
|
Assert.Equal(1, listData.GetProperty("total").GetInt32());
|
|
|
|
var get = await client.GetAsync($"/api/v1/patients/get/{id}");
|
|
Assert.Equal(HttpStatusCode.OK, get.StatusCode);
|
|
|
|
var update = await client.PostAsJsonAsync($"/api/v1/patients/update/{id}", PatientBody("Mother Renamed", "female"));
|
|
Assert.Equal(HttpStatusCode.OK, update.StatusCode);
|
|
var updated = await AuthTestClient.ReadDataAsync(update);
|
|
Assert.Equal("Mother Renamed", updated.GetProperty("displayName").GetString());
|
|
|
|
var archive = await client.PostAsJsonAsync($"/api/v1/patients/archive/{id}", new { });
|
|
Assert.Equal(HttpStatusCode.OK, archive.StatusCode);
|
|
|
|
var afterArchive = await client.GetAsync($"/api/v1/patients/get/{id}");
|
|
var archivedData = await AuthTestClient.ReadDataAsync(afterArchive);
|
|
Assert.False(archivedData.GetProperty("isActive").GetBoolean());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Get_And_Update_OfAnotherCustomersPatient_Return404()
|
|
{
|
|
// Customer A creates a patient.
|
|
var clientA = factory.CreateClient();
|
|
await ProfileTestClient.AuthenticateAsync(factory, clientA, "09123000002", "customer");
|
|
var create = await clientA.PostAsJsonAsync("/api/v1/patients/create", PatientBody("A's patient"));
|
|
var aPatientId = (await AuthTestClient.ReadDataAsync(create)).GetProperty("id").GetInt64();
|
|
|
|
// Customer B can neither read nor mutate A's patient — existence is not leaked.
|
|
var clientB = factory.CreateClient();
|
|
await ProfileTestClient.AuthenticateAsync(factory, clientB, "09123000003", "customer");
|
|
|
|
var get = await clientB.GetAsync($"/api/v1/patients/get/{aPatientId}");
|
|
Assert.Equal(HttpStatusCode.NotFound, get.StatusCode);
|
|
|
|
var update = await clientB.PostAsJsonAsync($"/api/v1/patients/update/{aPatientId}", PatientBody("hijack"));
|
|
Assert.Equal(HttpStatusCode.NotFound, update.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task List_Unauthenticated_Returns401()
|
|
{
|
|
var client = factory.CreateClient();
|
|
var response = await client.GetAsync("/api/v1/patients/list");
|
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_MissingGender_Returns400()
|
|
{
|
|
var client = factory.CreateClient();
|
|
await ProfileTestClient.AuthenticateAsync(factory, client, "09123000004", "customer");
|
|
|
|
var response = await client.PostAsJsonAsync("/api/v1/patients/create", PatientBody("No gender", gender: ""));
|
|
|
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
|
}
|
|
}
|