@
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> @
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user