diff --git a/services/bff/Bff.Tests/BffFactory.cs b/services/bff/Bff.Tests/BffFactory.cs index 3cf1e67..f108da8 100644 --- a/services/bff/Bff.Tests/BffFactory.cs +++ b/services/bff/Bff.Tests/BffFactory.cs @@ -82,6 +82,18 @@ internal sealed class FakeDomainClient : IDomainClient return Task.FromResult(Result); } + public (string RegistrationId, string Bsn)? Withdrawn { get; private set; } + + /// Whether the fake domain reports the withdrawal as done (true → 204) or not-found/not-owned + /// (false → 404). Tests set this to exercise the relay. + public bool WithdrawSucceeds { get; set; } = true; + + public Task WithdrawRegistrationAsync(string registrationId, string bsn, CancellationToken ct = default) + { + Withdrawn = (registrationId, bsn); + return Task.FromResult(WithdrawSucceeds); + } + public (string RegistrationId, string Besluit)? Decided { get; private set; } public Task> GetWerkbakAsync(CancellationToken ct = default) diff --git a/services/bff/Bff.Tests/SelfServiceEndpointTests.cs b/services/bff/Bff.Tests/SelfServiceEndpointTests.cs index fed9825..f7adaa9 100644 --- a/services/bff/Bff.Tests/SelfServiceEndpointTests.cs +++ b/services/bff/Bff.Tests/SelfServiceEndpointTests.cs @@ -71,5 +71,46 @@ public class SelfServiceEndpointTests Assert.Equal("reg-123", body!.RegistrationId); } + private static HttpRequestMessage Withdraw(string? bearer, string id = "reg-123") + { + var request = new HttpRequestMessage(HttpMethod.Post, $"/self-service/registrations/{id}/withdraw"); + if (bearer is not null) + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer); + return request; + } + + [Fact] + public async Task Rejects_a_withdrawal_without_a_token() + { + using var factory = new BffFactory(); + + var response = await factory.CreateClient().SendAsync(Withdraw(bearer: null)); + + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Null(factory.Domain.Withdrawn); + } + + [Fact] + public async Task Withdraws_the_callers_registration_forwarding_the_id_and_bsn() + { + using var factory = new BffFactory(); + + var response = await factory.CreateClient().SendAsync(Withdraw(TestTokens.Valid("123456782"), "reg-9")); + + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + Assert.Equal(("reg-9", "123456782"), factory.Domain.Withdrawn); + } + + [Fact] + public async Task Relays_not_found_when_the_registration_is_unknown_or_not_the_callers() + { + using var factory = new BffFactory(); + factory.Domain.WithdrawSucceeds = false; + + var response = await factory.CreateClient().SendAsync(Withdraw(TestTokens.Valid("123456782"))); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + private sealed record SubmitAcceptedDto(string RegistrationId, string Status); } diff --git a/services/domain/Big.Tests/WithdrawRegistrationTests.cs b/services/domain/Big.Tests/WithdrawRegistrationTests.cs index 99de8b9..7e73c69 100644 --- a/services/domain/Big.Tests/WithdrawRegistrationTests.cs +++ b/services/domain/Big.Tests/WithdrawRegistrationTests.cs @@ -5,13 +5,17 @@ namespace Big.Tests; public class WithdrawRegistrationTests { + private const string Bsn = "123456782"; + private static Registration Submitted(string processInstanceId = "proc-1") { - var registration = Registration.Submit("123456782"); + var registration = Registration.Submit(Bsn); registration.RecordProcessStarted(processInstanceId); return registration; } + private static WithdrawRegistrationCommand Command(RegistrationId id, string bsn = Bsn) => new(id, bsn); + [Fact] public async Task Withdrawing_marks_the_registration_ingetrokken_and_persists_it() { @@ -20,8 +24,9 @@ public class WithdrawRegistrationTests store.Seed(registration); var handler = new WithdrawRegistration(store, new FakeWorkflowClient()); - await handler.HandleAsync(new WithdrawRegistrationCommand(registration.Id)); + var outcome = await handler.HandleAsync(Command(registration.Id)); + Assert.Equal(WithdrawOutcome.Withdrawn, outcome); var saved = await store.GetAsync(registration.Id); Assert.Equal(RegistrationStatus.Ingetrokken, saved!.Status); Assert.Equal(1, store.SaveCount); @@ -36,7 +41,7 @@ public class WithdrawRegistrationTests var workflow = new FakeWorkflowClient(); var handler = new WithdrawRegistration(store, workflow); - await handler.HandleAsync(new WithdrawRegistrationCommand(registration.Id)); + await handler.HandleAsync(Command(registration.Id)); // The process the registration recorded at submit is cancelled (ADR-0014). Assert.Equal("proc-42", workflow.WithdrawnProcessInstanceId); @@ -45,20 +50,37 @@ public class WithdrawRegistrationTests [Fact] public async Task Withdrawing_before_a_process_was_started_still_marks_ingetrokken() { - // A registration with no recorded process (e.g. start never happened) can still be withdrawn; - // there is simply no process to cancel. var store = new FakeRegistrationStore(); - var registration = Registration.Submit("123456782"); // no RecordProcessStarted + var registration = Registration.Submit(Bsn); // no RecordProcessStarted store.Seed(registration); var workflow = new FakeWorkflowClient(); var handler = new WithdrawRegistration(store, workflow); - await handler.HandleAsync(new WithdrawRegistrationCommand(registration.Id)); + await handler.HandleAsync(Command(registration.Id)); Assert.Equal(RegistrationStatus.Ingetrokken, (await store.GetAsync(registration.Id))!.Status); Assert.Null(workflow.WithdrawnProcessInstanceId); } + [Fact] + public async Task A_different_bsn_cannot_withdraw_the_registration() + { + // Owner-scoping: only the zorgprofessional who submitted may withdraw. Another bsn is told + // NotFound (we don't reveal the registration exists) and nothing is changed. + var store = new FakeRegistrationStore(); + var registration = Submitted(); + store.Seed(registration); + var workflow = new FakeWorkflowClient(); + var handler = new WithdrawRegistration(store, workflow); + + var outcome = await handler.HandleAsync(Command(registration.Id, bsn: "999999990")); + + Assert.Equal(WithdrawOutcome.NotFound, outcome); + Assert.Equal(RegistrationStatus.Ingediend, (await store.GetAsync(registration.Id))!.Status); + Assert.Equal(0, store.SaveCount); + Assert.Null(workflow.WithdrawnProcessInstanceId); + } + [Fact] public async Task Rejects_a_null_command_without_touching_the_store() { @@ -70,14 +92,14 @@ public class WithdrawRegistrationTests } [Fact] - public async Task Withdrawing_an_unknown_registration_throws() + public async Task Withdrawing_an_unknown_registration_is_not_found() { var store = new FakeRegistrationStore(); var handler = new WithdrawRegistration(store, new FakeWorkflowClient()); - var ex = await Assert.ThrowsAsync( - () => handler.HandleAsync(new WithdrawRegistrationCommand(RegistrationId.New()))); - Assert.Contains("No registration", ex.Message); + var outcome = await handler.HandleAsync(Command(RegistrationId.New())); + + Assert.Equal(WithdrawOutcome.NotFound, outcome); Assert.Equal(0, store.SaveCount); } @@ -90,10 +112,11 @@ public class WithdrawRegistrationTests var workflow = new FakeWorkflowClient(); var handler = new WithdrawRegistration(store, workflow); - await handler.HandleAsync(new WithdrawRegistrationCommand(registration.Id)); - await handler.HandleAsync(new WithdrawRegistrationCommand(registration.Id)); + await handler.HandleAsync(Command(registration.Id)); + var second = await handler.HandleAsync(Command(registration.Id)); - // The second withdrawal is a no-op: the aggregate is not persisted again. + // The second withdrawal is a no-op: still Withdrawn, but the aggregate is not persisted again. + Assert.Equal(WithdrawOutcome.Withdrawn, second); Assert.Equal(1, store.SaveCount); Assert.Equal(RegistrationStatus.Ingetrokken, (await store.GetAsync(registration.Id))!.Status); }