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);
}