test(domain): Workflow Client, ACL client, store and job processor (refs #6)
Failing infrastructure unit tests (stub HttpMessageHandler, fakes): - FlowableWorkflowClient starts a process with the registrationId variable and returns the instance id; acquires OpenZaakAanmaken jobs (topic/workerId/lock) and parses their registrationId; completes a job with the zaakUrl variable — request URIs match flowable-rest's service/ and external-job-api/ paths. - AclHttpClient POSTs the bsn to the ACL and returns the zaak URL. - InMemoryRegistrationStore saves/reads/upserts by id. - OpenZaakJobProcessor acquires, opens a zaak, completes the job; leaves a failing job uncompleted for redelivery; polls harmlessly when idle. Adapters are stubs so the tests compile and fail on their assertions; the green commit implements them against the REST contract verified on a live Flowable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
17
services/domain/Big.Infrastructure/AclHttpClient.cs
Normal file
17
services/domain/Big.Infrastructure/AclHttpClient.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Big.Application;
|
||||
|
||||
namespace Big.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP client to the ACL service — the boundary the domain crosses to open a zaak (§8.1). It POSTs
|
||||
/// the bsn to the ACL's <c>/zaken</c> endpoint and returns the created zaak URL; it never constructs
|
||||
/// ZGW URLs or talks to OpenZaak itself.
|
||||
/// </summary>
|
||||
public sealed class AclHttpClient(HttpClient http, AclOptions options) : IAclClient
|
||||
{
|
||||
public Task<Uri> OpenZaakAsync(string bsn, CancellationToken ct = default)
|
||||
{
|
||||
// STUB (red).
|
||||
return Task.FromResult(new Uri("about:blank"));
|
||||
}
|
||||
}
|
||||
24
services/domain/Big.Infrastructure/Big.Infrastructure.csproj
Normal file
24
services/domain/Big.Infrastructure/Big.Infrastructure.csproj
Normal file
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- The infrastructure layer (CLAUDE.md §9): adapters implementing the Application ports.
|
||||
The Workflow Client (Flowable REST) and the ACL HTTP client live here — the only code
|
||||
that talks to Flowable (§8.2) and to the ACL respectively. -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Big.Application\Big.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- The external-task job worker is a hosted BackgroundService (ADR-0009); it logs and
|
||||
resolves a per-tick scope. Abstractions only — the host (Api) brings the implementations. -->
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
31
services/domain/Big.Infrastructure/FlowableWorkflowClient.cs
Normal file
31
services/domain/Big.Infrastructure/FlowableWorkflowClient.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Big.Application;
|
||||
using Big.Domain;
|
||||
|
||||
namespace Big.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// The Workflow Client — the only code that talks to Flowable (§8.2). It starts registratie process
|
||||
/// instances and drives the <c>OpenZaakAanmaken</c> external-worker jobs over Flowable's REST API
|
||||
/// (start: <c>service/runtime/process-instances</c>; acquire/complete: <c>external-job-api/…</c>).
|
||||
/// </summary>
|
||||
public sealed class FlowableWorkflowClient(HttpClient http, FlowableOptions options)
|
||||
: IWorkflowClient, IExternalWorkerClient
|
||||
{
|
||||
public Task<string> StartRegistrationProcessAsync(RegistrationId registrationId, CancellationToken ct = default)
|
||||
{
|
||||
// STUB (red).
|
||||
return Task.FromResult("");
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<OpenZaakJob>> AcquireOpenZaakJobsAsync(int maxJobs, CancellationToken ct = default)
|
||||
{
|
||||
// STUB (red).
|
||||
return Task.FromResult<IReadOnlyList<OpenZaakJob>>([]);
|
||||
}
|
||||
|
||||
public Task CompleteOpenZaakJobAsync(string jobId, Uri zaakUrl, CancellationToken ct = default)
|
||||
{
|
||||
// STUB (red).
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
18
services/domain/Big.Infrastructure/IExternalWorkerClient.cs
Normal file
18
services/domain/Big.Infrastructure/IExternalWorkerClient.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Big.Application;
|
||||
|
||||
namespace Big.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// The worker-facing side of the Workflow Client: acquiring and completing Flowable external-worker
|
||||
/// jobs (ADR-0009). Kept separate from the Application's <see cref="IWorkflowClient"/> because job
|
||||
/// acquisition is a Flowable-specific polling mechanic the application never needs to know about.
|
||||
/// Implemented by <see cref="FlowableWorkflowClient"/> — the only code that talks to Flowable (§8.2).
|
||||
/// </summary>
|
||||
public interface IExternalWorkerClient
|
||||
{
|
||||
/// <summary>Acquire and lock up to <paramref name="maxJobs"/> <c>OpenZaakAanmaken</c> jobs.</summary>
|
||||
Task<IReadOnlyList<OpenZaakJob>> AcquireOpenZaakJobsAsync(int maxJobs, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Complete an acquired job, passing the opened zaak URL back into the process.</summary>
|
||||
Task CompleteOpenZaakJobAsync(string jobId, Uri zaakUrl, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Big.Application;
|
||||
using Big.Domain;
|
||||
|
||||
namespace Big.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory <see cref="IRegistrationStore"/> for the minimal slice (ADR-0009). The walking
|
||||
/// skeleton's read path is the projection (S-06), not this store, so durable domain persistence is a
|
||||
/// documented follow-up. Registered as a singleton so the submit endpoint and the worker share it.
|
||||
/// </summary>
|
||||
public sealed class InMemoryRegistrationStore : IRegistrationStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<RegistrationId, Registration> _byId = new();
|
||||
|
||||
public Task SaveAsync(Registration registration, CancellationToken ct = default)
|
||||
{
|
||||
// STUB (red).
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<Registration?> GetAsync(RegistrationId id, CancellationToken ct = default)
|
||||
{
|
||||
// STUB (red).
|
||||
return Task.FromResult<Registration?>(null);
|
||||
}
|
||||
}
|
||||
26
services/domain/Big.Infrastructure/OpenZaakJobProcessor.cs
Normal file
26
services/domain/Big.Infrastructure/OpenZaakJobProcessor.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Big.Application;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Big.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// One poll tick of the external-task worker (ADR-0009): acquire the parked <c>OpenZaakAanmaken</c>
|
||||
/// jobs, hand each to the <see cref="OpenZaakWorker"/> to open a zaak via the ACL, and complete the
|
||||
/// Flowable job with the resulting zaak URL. A job that fails is logged and left un-completed so
|
||||
/// Flowable redelivers it (§8.6). Split out from the hosted <see cref="OpenZaakJobPump"/> so the
|
||||
/// acquire→process→complete logic is unit-testable without a running host.
|
||||
/// </summary>
|
||||
public sealed class OpenZaakJobProcessor(
|
||||
IExternalWorkerClient client,
|
||||
OpenZaakWorker worker,
|
||||
ILogger<OpenZaakJobProcessor> logger)
|
||||
{
|
||||
/// <summary>Process up to <paramref name="maxJobs"/> jobs. Returns the number completed.</summary>
|
||||
public async Task<int> PumpOnceAsync(int maxJobs, CancellationToken ct = default)
|
||||
{
|
||||
// STUB (red).
|
||||
await Task.CompletedTask;
|
||||
_ = logger;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
29
services/domain/Big.Infrastructure/Options.cs
Normal file
29
services/domain/Big.Infrastructure/Options.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace Big.Infrastructure;
|
||||
|
||||
/// <summary>Configuration for the Flowable Workflow Client. <see cref="BaseUrl"/> is the flowable-rest
|
||||
/// root and must end with a slash (e.g. <c>http://flowable-rest:8080/flowable-rest/</c>) so the
|
||||
/// <c>service/…</c> and <c>external-job-api/…</c> sub-paths resolve correctly.</summary>
|
||||
public sealed class FlowableOptions
|
||||
{
|
||||
public Uri BaseUrl { get; set; } = null!;
|
||||
public string Username { get; set; } = "";
|
||||
public string Password { get; set; } = "";
|
||||
|
||||
/// <summary>The id this worker locks jobs under, so two workers don't process the same job.</summary>
|
||||
public string WorkerId { get; set; } = "big-domain-worker";
|
||||
|
||||
/// <summary>How long an acquired job stays locked to this worker (ISO-8601 duration).</summary>
|
||||
public string LockDuration { get; set; } = "PT5M";
|
||||
|
||||
/// <summary>How many OpenZaakAanmaken jobs to acquire per poll.</summary>
|
||||
public int MaxJobsPerPoll { get; set; } = 5;
|
||||
|
||||
/// <summary>How often the worker polls Flowable for new jobs.</summary>
|
||||
public TimeSpan PollInterval { get; set; } = TimeSpan.FromSeconds(2);
|
||||
}
|
||||
|
||||
/// <summary>Configuration for the ACL HTTP client. <see cref="BaseUrl"/> is the ACL service root.</summary>
|
||||
public sealed class AclOptions
|
||||
{
|
||||
public Uri BaseUrl { get; set; } = null!;
|
||||
}
|
||||
Reference in New Issue
Block a user