feat(bff): owner-scoped self-service withdraw endpoint (S-11c-1, refs #12) #90
@@ -82,6 +82,18 @@ internal sealed class FakeDomainClient : IDomainClient
|
|||||||
return Task.FromResult(Result);
|
return Task.FromResult(Result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public (string RegistrationId, string Bsn)? Withdrawn { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>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.</summary>
|
||||||
|
public bool WithdrawSucceeds { get; set; } = true;
|
||||||
|
|
||||||
|
public Task<bool> 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 (string RegistrationId, string Besluit)? Decided { get; private set; }
|
||||||
|
|
||||||
public Task<IReadOnlyList<WerkbakItem>> GetWerkbakAsync(CancellationToken ct = default)
|
public Task<IReadOnlyList<WerkbakItem>> GetWerkbakAsync(CancellationToken ct = default)
|
||||||
|
|||||||
@@ -71,5 +71,46 @@ public class SelfServiceEndpointTests
|
|||||||
Assert.Equal("reg-123", body!.RegistrationId);
|
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);
|
private sealed record SubmitAcceptedDto(string RegistrationId, string Status);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,17 @@ namespace Big.Tests;
|
|||||||
|
|
||||||
public class WithdrawRegistrationTests
|
public class WithdrawRegistrationTests
|
||||||
{
|
{
|
||||||
|
private const string Bsn = "123456782";
|
||||||
|
|
||||||
private static Registration Submitted(string processInstanceId = "proc-1")
|
private static Registration Submitted(string processInstanceId = "proc-1")
|
||||||
{
|
{
|
||||||
var registration = Registration.Submit("123456782");
|
var registration = Registration.Submit(Bsn);
|
||||||
registration.RecordProcessStarted(processInstanceId);
|
registration.RecordProcessStarted(processInstanceId);
|
||||||
return registration;
|
return registration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static WithdrawRegistrationCommand Command(RegistrationId id, string bsn = Bsn) => new(id, bsn);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Withdrawing_marks_the_registration_ingetrokken_and_persists_it()
|
public async Task Withdrawing_marks_the_registration_ingetrokken_and_persists_it()
|
||||||
{
|
{
|
||||||
@@ -20,8 +24,9 @@ public class WithdrawRegistrationTests
|
|||||||
store.Seed(registration);
|
store.Seed(registration);
|
||||||
var handler = new WithdrawRegistration(store, new FakeWorkflowClient());
|
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);
|
var saved = await store.GetAsync(registration.Id);
|
||||||
Assert.Equal(RegistrationStatus.Ingetrokken, saved!.Status);
|
Assert.Equal(RegistrationStatus.Ingetrokken, saved!.Status);
|
||||||
Assert.Equal(1, store.SaveCount);
|
Assert.Equal(1, store.SaveCount);
|
||||||
@@ -36,7 +41,7 @@ public class WithdrawRegistrationTests
|
|||||||
var workflow = new FakeWorkflowClient();
|
var workflow = new FakeWorkflowClient();
|
||||||
var handler = new WithdrawRegistration(store, workflow);
|
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).
|
// The process the registration recorded at submit is cancelled (ADR-0014).
|
||||||
Assert.Equal("proc-42", workflow.WithdrawnProcessInstanceId);
|
Assert.Equal("proc-42", workflow.WithdrawnProcessInstanceId);
|
||||||
@@ -45,20 +50,37 @@ public class WithdrawRegistrationTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Withdrawing_before_a_process_was_started_still_marks_ingetrokken()
|
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 store = new FakeRegistrationStore();
|
||||||
var registration = Registration.Submit("123456782"); // no RecordProcessStarted
|
var registration = Registration.Submit(Bsn); // no RecordProcessStarted
|
||||||
store.Seed(registration);
|
store.Seed(registration);
|
||||||
var workflow = new FakeWorkflowClient();
|
var workflow = new FakeWorkflowClient();
|
||||||
var handler = new WithdrawRegistration(store, workflow);
|
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.Equal(RegistrationStatus.Ingetrokken, (await store.GetAsync(registration.Id))!.Status);
|
||||||
Assert.Null(workflow.WithdrawnProcessInstanceId);
|
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]
|
[Fact]
|
||||||
public async Task Rejects_a_null_command_without_touching_the_store()
|
public async Task Rejects_a_null_command_without_touching_the_store()
|
||||||
{
|
{
|
||||||
@@ -70,14 +92,14 @@ public class WithdrawRegistrationTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Withdrawing_an_unknown_registration_throws()
|
public async Task Withdrawing_an_unknown_registration_is_not_found()
|
||||||
{
|
{
|
||||||
var store = new FakeRegistrationStore();
|
var store = new FakeRegistrationStore();
|
||||||
var handler = new WithdrawRegistration(store, new FakeWorkflowClient());
|
var handler = new WithdrawRegistration(store, new FakeWorkflowClient());
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
var outcome = await handler.HandleAsync(Command(RegistrationId.New()));
|
||||||
() => handler.HandleAsync(new WithdrawRegistrationCommand(RegistrationId.New())));
|
|
||||||
Assert.Contains("No registration", ex.Message);
|
Assert.Equal(WithdrawOutcome.NotFound, outcome);
|
||||||
Assert.Equal(0, store.SaveCount);
|
Assert.Equal(0, store.SaveCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,10 +112,11 @@ public class WithdrawRegistrationTests
|
|||||||
var workflow = new FakeWorkflowClient();
|
var workflow = new FakeWorkflowClient();
|
||||||
var handler = new WithdrawRegistration(store, workflow);
|
var handler = new WithdrawRegistration(store, workflow);
|
||||||
|
|
||||||
await handler.HandleAsync(new WithdrawRegistrationCommand(registration.Id));
|
await handler.HandleAsync(Command(registration.Id));
|
||||||
await handler.HandleAsync(new WithdrawRegistrationCommand(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(1, store.SaveCount);
|
||||||
Assert.Equal(RegistrationStatus.Ingetrokken, (await store.GetAsync(registration.Id))!.Status);
|
Assert.Equal(RegistrationStatus.Ingetrokken, (await store.GetAsync(registration.Id))!.Status);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user