Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcd24d17a3 | ||
|
|
a693137c7c | ||
|
|
a940a6c9ce | ||
|
|
97ef3b9535 |
@@ -21,20 +21,16 @@ function providers(
|
|||||||
post = vi.fn().mockReturnValue(of({ registrationId: 'reg-9', status: 'Ingediend' })),
|
post = vi.fn().mockReturnValue(of({ registrationId: 'reg-9', status: 'Ingediend' })),
|
||||||
withdraw = vi.fn().mockReturnValue(of(undefined)),
|
withdraw = vi.fn().mockReturnValue(of(undefined)),
|
||||||
provideDocuments = vi.fn().mockReturnValue(of(undefined)),
|
provideDocuments = vi.fn().mockReturnValue(of(undefined)),
|
||||||
// Resume lookup (S-26): default to 204/empty — no in-flight registration, so the submit form shows.
|
|
||||||
getCurrent = vi.fn().mockReturnValue(of(undefined)),
|
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
post,
|
post,
|
||||||
withdraw,
|
withdraw,
|
||||||
provideDocuments,
|
provideDocuments,
|
||||||
getCurrent,
|
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: AuthService, useClass: FakeAuth },
|
{ provide: AuthService, useClass: FakeAuth },
|
||||||
{
|
{
|
||||||
provide: BffApiV1Service,
|
provide: BffApiV1Service,
|
||||||
useValue: {
|
useValue: {
|
||||||
getSelfServiceRegistrations: getCurrent,
|
|
||||||
postSelfServiceRegistrations: post,
|
postSelfServiceRegistrations: post,
|
||||||
postSelfServiceRegistrationsIdWithdraw: withdraw,
|
postSelfServiceRegistrationsIdWithdraw: withdraw,
|
||||||
postSelfServiceRegistrationsIdDocuments: provideDocuments,
|
postSelfServiceRegistrationsIdDocuments: provideDocuments,
|
||||||
@@ -60,21 +56,6 @@ describe('RegistrationPage', () => {
|
|||||||
expect(await screen.findByText(/ontvangen/i)).toBeTruthy();
|
expect(await screen.findByText(/ontvangen/i)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resumes an existing registration on load, without submitting again (S-26)', async () => {
|
|
||||||
const { post, providers: p } = providers(
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
vi.fn().mockReturnValue(of({ registrationId: 'reg-77', status: 'Ingediend' })),
|
|
||||||
);
|
|
||||||
await render(RegistrationPage, { providers: p });
|
|
||||||
|
|
||||||
// The confirmation view is restored from the in-flight registration — no submit click.
|
|
||||||
expect(await screen.findByText(/ontvangen/i)).toBeTruthy();
|
|
||||||
expect(screen.getByText(/reg-77/)).toBeTruthy();
|
|
||||||
expect(post).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows an error and keeps the submit available when the BFF call fails', async () => {
|
it('shows an error and keeps the submit available when the BFF call fails', async () => {
|
||||||
const { post, providers: p } = providers(vi.fn().mockReturnValue(throwError(() => new Error('BFF rejected'))));
|
const { post, providers: p } = providers(vi.fn().mockReturnValue(throwError(() => new Error('BFF rejected'))));
|
||||||
await render(RegistrationPage, { providers: p });
|
await render(RegistrationPage, { providers: p });
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Component, inject, type OnInit, signal } from '@angular/core';
|
import { Component, inject, signal } from '@angular/core';
|
||||||
import { BffApiV1Service, type CurrentRegistration, type SubmitAccepted } from 'api-client';
|
import { BffApiV1Service, type SubmitAccepted } from 'api-client';
|
||||||
import { AuthService } from 'auth';
|
import { AuthService } from 'auth';
|
||||||
import { UtrechtComponentsModule } from 'ui';
|
import { UtrechtComponentsModule } from 'ui';
|
||||||
|
|
||||||
@@ -8,16 +8,13 @@ import { UtrechtComponentsModule } from 'ui';
|
|||||||
* registration. The bsn comes from the DigiD token (not a form field), so this is a confirm-and-
|
* registration. The bsn comes from the DigiD token (not a form field), so this is a confirm-and-
|
||||||
* submit flow that posts to the BFF and shows the returned reference (ADR-0010; S-08c). After
|
* submit flow that posts to the BFF and shows the returned reference (ADR-0010; S-08c). After
|
||||||
* submitting they can withdraw it — "trek aanvraag in" — keyed by that reference (S-11c).
|
* submitting they can withdraw it — "trek aanvraag in" — keyed by that reference (S-11c).
|
||||||
*
|
|
||||||
* On load it asks the BFF for the caller's current open registration and restores the submitted view
|
|
||||||
* if there is one, so a page refresh no longer strands an in-flight registration (S-26).
|
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-registration-page',
|
selector: 'app-registration-page',
|
||||||
imports: [UtrechtComponentsModule],
|
imports: [UtrechtComponentsModule],
|
||||||
templateUrl: './registration-page.html',
|
templateUrl: './registration-page.html',
|
||||||
})
|
})
|
||||||
export class RegistrationPage implements OnInit {
|
export class RegistrationPage {
|
||||||
private readonly auth = inject(AuthService);
|
private readonly auth = inject(AuthService);
|
||||||
private readonly bff = inject(BffApiV1Service);
|
private readonly bff = inject(BffApiV1Service);
|
||||||
|
|
||||||
@@ -34,23 +31,6 @@ export class RegistrationPage implements OnInit {
|
|||||||
protected readonly provideDocumentsFailed = signal(false);
|
protected readonly provideDocumentsFailed = signal(false);
|
||||||
protected readonly selectedFile = signal<File | undefined>(undefined);
|
protected readonly selectedFile = signal<File | undefined>(undefined);
|
||||||
|
|
||||||
/** Resume an existing in-flight registration after a refresh (S-26): the BFF returns the caller's
|
|
||||||
* current open registration, or 204 (empty body) when there is none — in which case we show the
|
|
||||||
* submit form as before. Failures are non-fatal for the same reason. */
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.bff.getSelfServiceRegistrations().subscribe({
|
|
||||||
next: (current: CurrentRegistration | void) => {
|
|
||||||
if (current && current.registrationId) {
|
|
||||||
this.reference.set(current.registrationId);
|
|
||||||
this.submitted.set(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
// No resumable registration (or the lookup failed) — fall back to the submit form.
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
submit(): void {
|
submit(): void {
|
||||||
this.submitting.set(true);
|
this.submitting.set(true);
|
||||||
this.failed.set(false);
|
this.failed.set(false);
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
# ADR-0021: The ACL resolves its zaaktype by identificatie, not a pinned URL
|
|
||||||
|
|
||||||
- **Status:** Accepted
|
|
||||||
- **Date:** 2026-07-22
|
|
||||||
- **Deciders:** Respellion engineering
|
|
||||||
- **Relates to:** S-27 (#113), proposed in #117. The cleaner design deliberately split out of S-B04
|
|
||||||
(#110, ADR-0020), which fixed the local stack with an infra-only bootstrap.
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
The ACL was handed a **pinned zaaktype URL** (`Acl__Defaults__ZaaktypeUrl`) and diploma
|
|
||||||
informatieobjecttype URL. OpenZaak assigns those UUIDs at creation, so the URL is not knowable when
|
|
||||||
the compose file is written — every stack had to seed the catalogus and then capture + inject the
|
|
||||||
resulting URLs out of band: `run-domain-check.sh` for CI, and the `local-seed` → `acl.env` bootstrap
|
|
||||||
(ADR-0020) for `make local`. Brittle, and a stale/placeholder URL failed opaquely (OpenZaak 400).
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**The ACL resolves its zaaktype (by `identificatie`) and diploma informatieobjecttype (by
|
|
||||||
`omschrijving`) from OpenZaak's Catalogi API, instead of being handed the URLs.**
|
|
||||||
|
|
||||||
- **Config:** `AclDefaults.ZaaktypeUrl`/`InformatieobjecttypeUrl` → `ZaaktypeIdentificatie`
|
|
||||||
(`BIG-REGISTRATIE`) / `InformatieobjecttypeOmschrijving` (`Diploma`).
|
|
||||||
- **Lookup (gateway, §8.1):** `GET /catalogi/api/v1/zaaktypen?status=definitief&identificatie=…` →
|
|
||||||
the published zaaktype URL; `GET /catalogi/api/v1/informatieobjecttypen?status=definitief` matched
|
|
||||||
on `omschrijving`. Reuses the gateway's existing catalogus-query machinery.
|
|
||||||
- **Timing = lazy + cached (`CachedZaaktypeCatalog`).** Resolve on first use (first zaak open /
|
|
||||||
document store) and cache for the process lifetime. Lazy avoids a startup ordering coupling — the
|
|
||||||
ACL never crash-loops when it boots before the catalogus is published. A **failed** resolution is
|
|
||||||
not cached, so it is retried on the next call (e.g. once the zaaktype is published); a restart
|
|
||||||
re-resolves.
|
|
||||||
- **Failure mode:** no published match → a clear "No published zaaktype with identificatie '…' found
|
|
||||||
in OpenZaak — is the BIG catalogus seeded and published?" error, replacing the opaque placeholder
|
|
||||||
400.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
**Positive**
|
|
||||||
|
|
||||||
- No stack captures or injects a server-assigned URL any more: `run-domain-check.sh` drops the
|
|
||||||
`ACL_ZAAKTYPE_URL`/`ACL_INFORMATIEOBJECTTYPE_URL` capture+inject, `docker-compose.yml`/`.local.yml`
|
|
||||||
drop the placeholder URL env, and `local-seed`/`acl.env` shrink to a single line. The ACL
|
|
||||||
self-configures from the catalogus it already talks to.
|
|
||||||
- The failure mode is legible (a named error instead of a 400 on a zeros-UUID).
|
|
||||||
|
|
||||||
**Negative / costs**
|
|
||||||
|
|
||||||
- The ACL still needs its OpenZaak **BaseUrl** pointed at a **URL-valid host (a container IP)**, so
|
|
||||||
the base-URL injection from ADR-0020 stays (the local `acl.env` now carries only that; CI keeps
|
|
||||||
`ACL_OPENZAAK_BASEURL`). This is **not** something S-27 can remove: OpenZaak validates the
|
|
||||||
`zaaktype` field on zaak-create with Django's URLValidator and **rejects a single-label host**
|
|
||||||
(`http://openzaak:8000/…` → `zaaktype: bad-url, "Voer een geldige URL in."`, confirmed empirically).
|
|
||||||
So ADR-0020's `seed-env` volume + ACL entrypoint shim are **simplified, not deleted**.
|
|
||||||
- New branching in the gateway/resolver → unit + integration test surface; the mutation ratchet
|
|
||||||
covers it (§5).
|
|
||||||
- A seed step still **creates + publishes** the zaaktype (this ADR changes only discovery). Reaching
|
|
||||||
OpenZaak's Catalogi API to *seed* likewise needs the IP host (its query params hit the same
|
|
||||||
URLValidator) — unchanged from before.
|
|
||||||
|
|
||||||
## Alternatives considered
|
|
||||||
|
|
||||||
- **Resolve at startup** (eager). Simpler cache, but reintroduces the ordering coupling (crash-loop
|
|
||||||
if the catalogus isn't published yet). Rejected in favour of lazy.
|
|
||||||
- **Per-request resolution** (no cache). No stale-cache risk, but a Catalogi lookup on every ACL
|
|
||||||
operation. Rejected; a process-lifetime cache with restart-to-refresh is enough here.
|
|
||||||
- **Keep the pinned URL** (status quo / ADR-0020 only). Rejected — the brittleness this ADR removes is
|
|
||||||
exactly what S-27 was carved out to fix.
|
|
||||||
+3
-25
@@ -26,31 +26,9 @@ make verify-local # → "OK — a fresh local stack completed the flow with
|
|||||||
# test123); it shows as INGESCHREVEN in the openbaar register at http://localhost:8141.
|
# test123); it shows as INGESCHREVEN in the openbaar register at http://localhost:8141.
|
||||||
```
|
```
|
||||||
|
|
||||||
> The zaaktype is discovered by the ACL itself since S-27 (below); `local-seed`'s `acl.env` now
|
> The zaaktype UUID is server-assigned, so `local-seed` writes the real URL into a shared volume as
|
||||||
> carries only OpenZaak's IP base URL, which the ACL still needs because OpenZaak rejects a
|
> `acl.env` and the ACL sources it on startup (ADR-0020). The cleaner long-term fix — the ACL
|
||||||
> single-label host on zaak-create (ADR-0020 + ADR-0021).
|
> resolving its zaaktype by `identificatie` — is tracked separately as S-27 (#113).
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## S-27 — ACL resolves its zaaktype by identificatie, not a pinned URL (#113, ADR-0021)
|
|
||||||
|
|
||||||
**Outcome:** the ACL discovers its BIG zaaktype (by `identificatie`) and diploma informatieobjecttype
|
|
||||||
(by `omschrijving`) from OpenZaak's Catalogi API, instead of being handed the server-assigned URLs.
|
|
||||||
No user-visible behaviour change — the flow runs exactly as before — but no stack captures/injects a
|
|
||||||
zaaktype URL any more, and a missing catalogus now fails with a clear message instead of an opaque 400.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# The live ACL↔OpenZaak integration test proves resolution against a real seeded OpenZaak:
|
|
||||||
make verify-acl # → "resolves the published BIG-REGISTRATIE zaaktype + Diploma informatieobjecttype by business key"
|
|
||||||
|
|
||||||
# End-to-end unchanged (the ACL self-discovers the zaaktype during the flow):
|
|
||||||
make verify-local # local stack — still green, now with no zaaktype-URL injection
|
|
||||||
make verify-domain # CI stack — recreates the ACL pointed only at OpenZaak's IP (no URL to inject)
|
|
||||||
```
|
|
||||||
|
|
||||||
> The ACL still needs its OpenZaak base URL at a URL-valid host (a container IP): OpenZaak's
|
|
||||||
> URLValidator rejects a single-label host like `openzaak:8000` on zaak-create. So ADR-0020's base-URL
|
|
||||||
> injection stays; only the zaaktype/informatieobjecttype **URL** injection is gone (ADR-0021).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ All test users share the password **`test123`**.
|
|||||||
| Realm | Mimics | User | Identifying claim |
|
| Realm | Mimics | User | Identifying claim |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `digid` | DigiD (burgers) | `jan-burger` | `bsn` = `123456782` |
|
| `digid` | DigiD (burgers) | `jan-burger` | `bsn` = `123456782` |
|
||||||
| `digid` | DigiD (burgers) | `sanne-burger` | `bsn` = `231477813` (S-26 resume e2e — its own user so it can leave an open registration) |
|
|
||||||
| `eherkenning` | eHerkenning (bedrijven) | `acme-ondernemer` | `kvk` = `12345678` |
|
| `eherkenning` | eHerkenning (bedrijven) | `acme-ondernemer` | `kvk` = `12345678` |
|
||||||
| `eidas` | eIDAS (EU) | `pierre-dupont` | `eidas_id` = `FR/NL/AB-1234-5678` |
|
| `eidas` | eIDAS (EU) | `pierre-dupont` | `eidas_id` = `FR/NL/AB-1234-5678` |
|
||||||
| `medewerker` | Internal staff | `merel-behandelaar` | role `behandelaar` |
|
| `medewerker` | Internal staff | `merel-behandelaar` | role `behandelaar` |
|
||||||
|
|||||||
@@ -315,22 +315,21 @@ services:
|
|||||||
context: ../services/acl
|
context: ../services/acl
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: register-referentie/acl:dev
|
image: register-referentie/acl:dev
|
||||||
# The ACL discovers its zaaktype + informatieobjecttype URLs from the Catalogi API by the business
|
# The base/zaaktype/informatieobjecttype below are PLACEHOLDERS. The real, server-assigned
|
||||||
# keys below (S-27, ADR-0021), so no URL is injected. It still needs its OpenZaak BaseUrl pointed at
|
# values are written by the local-seed one-shot into seed-env:/seed/acl.env, which the entrypoint
|
||||||
# a URL-valid host (OpenZaak rejects a single-label host like `openzaak` on zaak-create), so the
|
# sources (set -a) so they override these before the app starts (S-B04, #110, ADR-0020). Sourcing
|
||||||
# local-seed one-shot writes that IP base into seed-env:/seed/acl.env, which the entrypoint sources
|
# a runtime-generated env file is why we override the entrypoint here rather than use `env_file:`
|
||||||
# (set -a) before the app starts. A runtime-generated env file is why we override the entrypoint here
|
# (which compose reads at parse time, before the seed has run).
|
||||||
# rather than use `env_file:` (which compose reads at parse time, before the seed has run).
|
|
||||||
entrypoint: ["/bin/sh", "-c", "set -a; . /seed/acl.env; set +a; exec dotnet Acl.Api.dll"]
|
entrypoint: ["/bin/sh", "-c", "set -a; . /seed/acl.env; set +a; exec dotnet Acl.Api.dll"]
|
||||||
environment:
|
environment:
|
||||||
Acl__OpenZaak__BaseUrl: http://openzaak:8000/ # placeholder; seed-env/acl.env supplies the IP base
|
Acl__OpenZaak__BaseUrl: http://openzaak:8000/
|
||||||
Acl__OpenZaak__ClientId: big-reference-seed
|
Acl__OpenZaak__ClientId: big-reference-seed
|
||||||
Acl__OpenZaak__Secret: insecure-dev-secret-change-me
|
Acl__OpenZaak__Secret: insecure-dev-secret-change-me
|
||||||
Acl__Defaults__Bronorganisatie: "517439943"
|
Acl__Defaults__Bronorganisatie: "517439943"
|
||||||
Acl__Defaults__VerantwoordelijkeOrganisatie: "517439943"
|
Acl__Defaults__VerantwoordelijkeOrganisatie: "517439943"
|
||||||
Acl__Defaults__Vertrouwelijkheidaanduiding: openbaar
|
Acl__Defaults__Vertrouwelijkheidaanduiding: openbaar
|
||||||
Acl__Defaults__ZaaktypeIdentificatie: BIG-REGISTRATIE
|
Acl__Defaults__ZaaktypeUrl: http://openzaak:8000/catalogi/api/v1/zaaktypen/00000000-0000-0000-0000-000000000000
|
||||||
Acl__Defaults__InformatieobjecttypeOmschrijving: Diploma
|
Acl__Defaults__InformatieobjecttypeUrl: http://openzaak:8000/catalogi/api/v1/informatieobjecttypen/00000000-0000-0000-0000-000000000000
|
||||||
ports:
|
ports:
|
||||||
- "8100:8080"
|
- "8100:8080"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+11
-12
@@ -15,12 +15,12 @@
|
|||||||
#
|
#
|
||||||
# docker compose -f infra/docker-compose.yml up -d --build --wait
|
# docker compose -f infra/docker-compose.yml up -d --build --wait
|
||||||
#
|
#
|
||||||
# After first boot, seed + publish the BIG catalogus:
|
# After first boot, seed the BIG catalogus and note the zaaktype URL:
|
||||||
# OZ_PUBLISH=1 python infra/openzaak/seed_catalogus.py
|
# python infra/openzaak/seed_catalogus.py
|
||||||
# The ACL discovers the zaaktype by identificatie (S-27, ADR-0021), so there is no URL to inject —
|
# Then set ACL_ZAAKTYPE_URL in a .env file or your shell and re-up the acl
|
||||||
# just point its BaseUrl at an OpenZaak host OpenZaak accepts on zaak-create (a container IP; a
|
# service:
|
||||||
# single-label host is rejected):
|
# export ACL_ZAAKTYPE_URL=http://openzaak:8000/catalogi/api/v1/zaaktypen/<uuid>
|
||||||
# ACL_OPENZAAK_BASEURL=http://<openzaak-ip>:8000/ docker compose -f infra/docker-compose.yml up -d acl
|
# docker compose -f infra/docker-compose.yml up -d acl
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
@@ -304,12 +304,11 @@ services:
|
|||||||
Acl__Defaults__Bronorganisatie: "517439943"
|
Acl__Defaults__Bronorganisatie: "517439943"
|
||||||
Acl__Defaults__VerantwoordelijkeOrganisatie: "517439943"
|
Acl__Defaults__VerantwoordelijkeOrganisatie: "517439943"
|
||||||
Acl__Defaults__Vertrouwelijkheidaanduiding: openbaar
|
Acl__Defaults__Vertrouwelijkheidaanduiding: openbaar
|
||||||
# The ACL resolves the (server-assigned) zaaktype + diploma informatieobjecttype URLs from the
|
# Override with the real zaaktype URL after running seed_catalogus.py.
|
||||||
# Catalogi API by these stable business keys (S-27, ADR-0021) — no URL to capture and inject.
|
Acl__Defaults__ZaaktypeUrl: ${ACL_ZAAKTYPE_URL:-http://openzaak:8000/catalogi/api/v1/zaaktypen/00000000-0000-0000-0000-000000000000}
|
||||||
# BaseUrl above stays overridable because OpenZaak rejects a single-label host on zaak creation,
|
# The informatieobjecttype a diploma is filed under (S-10b). Placeholder until seed_catalogus.py
|
||||||
# so verify-domain still points the ACL at OpenZaak's container IP.
|
# (OZ_PUBLISH=1) reports the real URL, which verify-domain injects like the zaaktype URL.
|
||||||
Acl__Defaults__ZaaktypeIdentificatie: BIG-REGISTRATIE
|
Acl__Defaults__InformatieobjecttypeUrl: ${ACL_INFORMATIEOBJECTTYPE_URL:-http://openzaak:8000/catalogi/api/v1/informatieobjecttypen/00000000-0000-0000-0000-000000000000}
|
||||||
Acl__Defaults__InformatieobjecttypeOmschrijving: Diploma
|
|
||||||
ports:
|
ports:
|
||||||
- "8100:8080"
|
- "8100:8080"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -38,36 +38,6 @@
|
|||||||
"emailVerified": true,
|
"emailVerified": true,
|
||||||
"credentials": [{ "type": "password", "value": "test123", "temporary": false }],
|
"credentials": [{ "type": "password", "value": "test123", "temporary": false }],
|
||||||
"attributes": { "bsn": ["123456782"] }
|
"attributes": { "bsn": ["123456782"] }
|
||||||
},
|
|
||||||
{
|
|
||||||
"username": "sanne-burger",
|
|
||||||
"enabled": true,
|
|
||||||
"firstName": "Sanne",
|
|
||||||
"lastName": "Burger",
|
|
||||||
"email": "sanne.burger@example.nl",
|
|
||||||
"emailVerified": true,
|
|
||||||
"credentials": [{ "type": "password", "value": "test123", "temporary": false }],
|
|
||||||
"attributes": { "bsn": ["231477813"] }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"username": "emma-burger",
|
|
||||||
"enabled": true,
|
|
||||||
"firstName": "Emma",
|
|
||||||
"lastName": "Burger",
|
|
||||||
"email": "emma.burger@example.nl",
|
|
||||||
"emailVerified": true,
|
|
||||||
"credentials": [{ "type": "password", "value": "test123", "temporary": false }],
|
|
||||||
"attributes": { "bsn": ["231477805"] }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"username": "lars-burger",
|
|
||||||
"enabled": true,
|
|
||||||
"firstName": "Lars",
|
|
||||||
"lastName": "Burger",
|
|
||||||
"email": "lars.burger@example.nl",
|
|
||||||
"emailVerified": true,
|
|
||||||
"credentials": [{ "type": "password", "value": "test123", "temporary": false }],
|
|
||||||
"attributes": { "bsn": ["231477821"] }
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,15 @@ echo ">> seeding + publishing the BIG zaaktype at ${OZ_BASE} (idempotent)"
|
|||||||
out="$(python3 /work/seed_catalogus.py)"
|
out="$(python3 /work/seed_catalogus.py)"
|
||||||
echo "$out"
|
echo "$out"
|
||||||
|
|
||||||
# Sanity-check that the zaaktype was actually published (the ACL discovers it by identificatie, S-27).
|
zt="$(printf '%s\n' "$out" | sed -n 's/^ZAAKTYPE_URL //p' | head -1)"
|
||||||
printf '%s\n' "$out" | grep -q '^ZAAKTYPE_URL ' || { echo "ERROR: seed did not publish the zaaktype" >&2; exit 1; }
|
iot="$(printf '%s\n' "$out" | sed -n 's/^INFORMATIEOBJECTTYPE_URL //p' | head -1)"
|
||||||
|
[ -n "$zt" ] || { echo "ERROR: seed did not report a ZAAKTYPE_URL" >&2; exit 1; }
|
||||||
|
[ -n "$iot" ] || { echo "ERROR: seed did not report an INFORMATIEOBJECTTYPE_URL" >&2; exit 1; }
|
||||||
|
|
||||||
# The ACL resolves the zaaktype/informatieobjecttype URLs itself (S-27, ADR-0021); the only value it
|
# The ACL entrypoint sources this; these keys override the placeholder defaults in the compose file.
|
||||||
# still needs injected is the OpenZaak base URL at a URL-valid host (the container IP), because OpenZaak
|
|
||||||
# rejects a single-label host on zaak-create. The ACL entrypoint sources this.
|
|
||||||
cat > /out/acl.env <<EOF
|
cat > /out/acl.env <<EOF
|
||||||
Acl__OpenZaak__BaseUrl=${OZ_BASE}/
|
Acl__OpenZaak__BaseUrl=${OZ_BASE}/
|
||||||
|
Acl__Defaults__ZaaktypeUrl=${zt}
|
||||||
|
Acl__Defaults__InformatieobjecttypeUrl=${iot}
|
||||||
EOF
|
EOF
|
||||||
echo ">> wrote /out/acl.env (base=${OZ_BASE}/)"
|
echo ">> wrote /out/acl.env (base=${OZ_BASE}/ zaaktype=${zt})"
|
||||||
|
|||||||
Binary file not shown.
@@ -30,18 +30,21 @@ oz_ip="$(ip "$oz")"; dom_ip="$(ip "$dom")"
|
|||||||
oz_base="http://$oz_ip:8000"
|
oz_base="http://$oz_ip:8000"
|
||||||
echo ">> openzaak=$oz_ip domain=$dom_ip network=$net"
|
echo ">> openzaak=$oz_ip domain=$dom_ip network=$net"
|
||||||
|
|
||||||
echo ">> seeding + publishing a BIG zaaktype (idempotent)"
|
echo ">> seeding a published BIG zaaktype (idempotent) and capturing its URL"
|
||||||
sid="$(docker create --network "$net" -e "OZ_BASE=$oz_base" -e OZ_PUBLISH=1 python:3-slim python /seed.py)"
|
sid="$(docker create --network "$net" -e "OZ_BASE=$oz_base" -e OZ_PUBLISH=1 python:3-slim python /seed.py)"
|
||||||
docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed.py" >/dev/null
|
docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed.py" >/dev/null
|
||||||
seed_out="$(docker start -a "$sid")"
|
seed_out="$(docker start -a "$sid")"
|
||||||
|
zt_url="$(printf '%s\n' "$seed_out" | sed -n 's/^ZAAKTYPE_URL //p' | head -1)"
|
||||||
|
iot_url="$(printf '%s\n' "$seed_out" | sed -n 's/^INFORMATIEOBJECTTYPE_URL //p' | head -1)"
|
||||||
docker rm -f "$sid" >/dev/null
|
docker rm -f "$sid" >/dev/null
|
||||||
printf '%s\n' "$seed_out" | grep -q '^ZAAKTYPE_URL ' || { echo "ERROR: seed did not publish the zaaktype" >&2; exit 1; }
|
[ -n "$zt_url" ] || { echo "ERROR: seed did not report a ZAAKTYPE_URL" >&2; exit 1; }
|
||||||
|
[ -n "$iot_url" ] || { echo "ERROR: seed did not report an INFORMATIEOBJECTTYPE_URL" >&2; exit 1; }
|
||||||
|
echo ">> zaaktype: $zt_url"
|
||||||
|
echo ">> informatieobjecttype: $iot_url"
|
||||||
|
|
||||||
# The ACL resolves the zaaktype + informatieobjecttype by identificatie/omschrijving (S-27, ADR-0021),
|
echo ">> recreating the acl service pointed at the seeded zaaktype + informatieobjecttype (host-consistent)"
|
||||||
# so there is no URL to inject — only the OpenZaak base URL, pointed at the same host's container IP
|
ACL_ZAAKTYPE_URL="$zt_url" ACL_INFORMATIEOBJECTTYPE_URL="$iot_url" ACL_OPENZAAK_BASEURL="$oz_base/" \
|
||||||
# (OpenZaak rejects a single-label host on zaak-create).
|
docker compose -f "$compose" up -d acl
|
||||||
echo ">> recreating the acl service pointed at OpenZaak's IP (it resolves the zaaktype itself, S-27)"
|
|
||||||
ACL_OPENZAAK_BASEURL="$oz_base/" docker compose -f "$compose" up -d acl
|
|
||||||
WAIT_TIMEOUT="${WAIT_TIMEOUT:-120}" bash "$here/wait-healthy.sh" acl
|
WAIT_TIMEOUT="${WAIT_TIMEOUT:-120}" bash "$here/wait-healthy.sh" acl
|
||||||
|
|
||||||
echo ">> submitting a registration to the domain"
|
echo ">> submitting a registration to the domain"
|
||||||
|
|||||||
@@ -24,11 +24,6 @@ import {
|
|||||||
Observable
|
Observable
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
export interface CurrentRegistration {
|
|
||||||
registrationId: string;
|
|
||||||
status: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DecideRequest {
|
export interface DecideRequest {
|
||||||
besluit: string;
|
besluit: string;
|
||||||
}
|
}
|
||||||
@@ -205,37 +200,6 @@ export class BffApiV1Service {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSelfServiceRegistrations<TData = CurrentRegistration | void>( options?: HttpClientBodyOptions): Observable<TData>;
|
|
||||||
getSelfServiceRegistrations<TData = CurrentRegistration | void>( options?: HttpClientEventOptions): Observable<HttpEvent<TData>>;
|
|
||||||
getSelfServiceRegistrations<TData = CurrentRegistration | void>( options?: HttpClientResponseOptions): Observable<AngularHttpResponse<TData>>;
|
|
||||||
getSelfServiceRegistrations<TData = CurrentRegistration | void>(
|
|
||||||
options?: HttpClientObserveOptions): Observable<TData | HttpEvent<TData> | AngularHttpResponse<TData>> {
|
|
||||||
if (options?.observe === 'events') {
|
|
||||||
return this.http.get<TData>(
|
|
||||||
`/self-service/registrations`,{
|
|
||||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
|
||||||
observe: 'events',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.observe === 'response') {
|
|
||||||
return this.http.get<TData>(
|
|
||||||
`/self-service/registrations`,{
|
|
||||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
|
||||||
observe: 'response',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.http.get<TData>(
|
|
||||||
`/self-service/registrations`,{
|
|
||||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
|
||||||
observe: 'body',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
postSelfServiceRegistrationsIdWithdraw<TData = void>(id: string, options?: HttpClientBodyOptions): Observable<TData>;
|
postSelfServiceRegistrationsIdWithdraw<TData = void>(id: string, options?: HttpClientBodyOptions): Observable<TData>;
|
||||||
postSelfServiceRegistrationsIdWithdraw<TData = void>(id: string, options?: HttpClientEventOptions): Observable<HttpEvent<TData>>;
|
postSelfServiceRegistrationsIdWithdraw<TData = void>(id: string, options?: HttpClientEventOptions): Observable<HttpEvent<TData>>;
|
||||||
postSelfServiceRegistrationsIdWithdraw<TData = void>(id: string, options?: HttpClientResponseOptions): Observable<AngularHttpResponse<TData>>;
|
postSelfServiceRegistrationsIdWithdraw<TData = void>(id: string, options?: HttpClientResponseOptions): Observable<AngularHttpResponse<TData>>;
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ builder.Services.AddSingleton(sp => sp.GetRequiredService<IConfiguration>()
|
|||||||
.GetSection("Acl:OpenZaak").Get<OpenZaakOptions>()
|
.GetSection("Acl:OpenZaak").Get<OpenZaakOptions>()
|
||||||
?? throw new InvalidOperationException("Missing configuration section 'Acl:OpenZaak'"));
|
?? throw new InvalidOperationException("Missing configuration section 'Acl:OpenZaak'"));
|
||||||
builder.Services.AddHttpClient<IZaakGateway, OpenZaakGateway>();
|
builder.Services.AddHttpClient<IZaakGateway, OpenZaakGateway>();
|
||||||
// Singleton so the resolved zaaktype/informatieobjecttype URLs are cached across requests (S-27).
|
|
||||||
builder.Services.AddSingleton<IZaaktypeCatalog, CachedZaaktypeCatalog>();
|
|
||||||
builder.Services.AddScoped<AclService>();
|
builder.Services.AddScoped<AclService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ public sealed class AclDefaults
|
|||||||
public required string Bronorganisatie { get; init; }
|
public required string Bronorganisatie { get; init; }
|
||||||
public required string VerantwoordelijkeOrganisatie { get; init; }
|
public required string VerantwoordelijkeOrganisatie { get; init; }
|
||||||
public required string Vertrouwelijkheidaanduiding { get; init; }
|
public required string Vertrouwelijkheidaanduiding { get; init; }
|
||||||
|
public required Uri ZaaktypeUrl { get; init; }
|
||||||
|
|
||||||
/// <summary>The BIG zaaktype's stable business key. The ACL resolves the (server-assigned) zaaktype
|
/// <summary>The informatieobjecttype an uploaded diploma is filed under (S-10b). Seeded in the
|
||||||
/// URL from this via the Catalogi API instead of being handed a pinned URL (S-27, ADR-0021).</summary>
|
/// catalogus and injected like <see cref="ZaaktypeUrl"/>.</summary>
|
||||||
public required string ZaaktypeIdentificatie { get; init; }
|
public required Uri InformatieobjecttypeUrl { get; init; }
|
||||||
|
|
||||||
/// <summary>The omschrijving of the informatieobjecttype an uploaded diploma is filed under (S-10b);
|
|
||||||
/// resolved to a URL by the Catalogi API, like <see cref="ZaaktypeIdentificatie"/>.</summary>
|
|
||||||
public required string InformatieobjecttypeOmschrijving { get; init; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ namespace Acl.Application;
|
|||||||
|
|
||||||
/// <summary>The ACL's single operation: open a zaak from a domain payload,
|
/// <summary>The ACL's single operation: open a zaak from a domain payload,
|
||||||
/// default-filling the ZGW-mandatory fields (ADR-0003).</summary>
|
/// default-filling the ZGW-mandatory fields (ADR-0003).</summary>
|
||||||
public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, IZaaktypeCatalog catalog, IClock clock)
|
public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, IClock clock)
|
||||||
{
|
{
|
||||||
public async Task<Uri> OpenZaakAsync(DomainRegistration registration, CancellationToken ct = default)
|
public Task<Uri> OpenZaakAsync(DomainRegistration registration, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(registration);
|
ArgumentNullException.ThrowIfNull(registration);
|
||||||
|
|
||||||
@@ -12,34 +12,34 @@ public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, IZaak
|
|||||||
defaults.Bronorganisatie,
|
defaults.Bronorganisatie,
|
||||||
defaults.VerantwoordelijkeOrganisatie,
|
defaults.VerantwoordelijkeOrganisatie,
|
||||||
defaults.Vertrouwelijkheidaanduiding,
|
defaults.Vertrouwelijkheidaanduiding,
|
||||||
await catalog.GetZaaktypeUrlAsync(ct),
|
defaults.ZaaktypeUrl,
|
||||||
clock.Today,
|
clock.Today,
|
||||||
registration.Reference);
|
registration.Reference);
|
||||||
|
|
||||||
return await gateway.OpenZaakAsync(request, ct);
|
return gateway.OpenZaakAsync(request, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Approve a zaak: set it to the eindstatus of the BIG zaaktype (resolved by identificatie, S-27).
|
/// Approve a zaak: set it to the eindstatus of the configured BIG zaaktype (ADR-0003 default). The
|
||||||
/// The domain hands over only the zaak URL; the ACL owns which statustype means "approved" (§8.1).
|
/// domain hands over only the zaak URL; the ACL owns which statustype means "approved" (§8.1).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task ApproveZaakAsync(Uri zaakUrl, CancellationToken ct = default)
|
public Task ApproveZaakAsync(Uri zaakUrl, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(zaakUrl);
|
ArgumentNullException.ThrowIfNull(zaakUrl);
|
||||||
|
|
||||||
await gateway.SetZaakToEindstatusAsync(zaakUrl, await catalog.GetZaaktypeUrlAsync(ct), clock.Today, ct);
|
return gateway.SetZaakToEindstatusAsync(zaakUrl, defaults.ZaaktypeUrl, clock.Today, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cancel a zaak on document-timeout expiry (S-10c): set it to the BIG zaaktype's cancellation
|
/// Cancel a zaak on document-timeout expiry (S-10c): set it to the configured BIG zaaktype's
|
||||||
/// statustype + resultaat. The domain hands over only the zaak URL; the ACL owns which
|
/// cancellation statustype + resultaat. The domain hands over only the zaak URL; the ACL owns which
|
||||||
/// statustype/resultaat means "cancelled" (§8.1).
|
/// statustype/resultaat means "cancelled" (§8.1).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task CancelZaakAsync(Uri zaakUrl, CancellationToken ct = default)
|
public Task CancelZaakAsync(Uri zaakUrl, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(zaakUrl);
|
ArgumentNullException.ThrowIfNull(zaakUrl);
|
||||||
|
|
||||||
await gateway.SetZaakToCancellationStatusAsync(zaakUrl, await catalog.GetZaaktypeUrlAsync(ct), clock.Today, ct);
|
return gateway.SetZaakToCancellationStatusAsync(zaakUrl, defaults.ZaaktypeUrl, clock.Today, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>The zaak's reference (its ZGW identificatie), for the read projection (#78).</summary>
|
/// <summary>The zaak's reference (its ZGW identificatie), for the read projection (#78).</summary>
|
||||||
@@ -56,7 +56,7 @@ public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, IZaak
|
|||||||
/// and hand the file to the gateway, which creates the informatieobject and relates it to the zaak.
|
/// and hand the file to the gateway, which creates the informatieobject and relates it to the zaak.
|
||||||
/// The domain supplies only the zaak, the bytes, and the file's name/type (§8.1).
|
/// The domain supplies only the zaak, the bytes, and the file's name/type (§8.1).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<Uri> StoreDiplomaAsync(Uri zaakUrl, byte[] content, string fileName, string contentType, CancellationToken ct = default)
|
public Task<Uri> StoreDiplomaAsync(Uri zaakUrl, byte[] content, string fileName, string contentType, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(zaakUrl);
|
ArgumentNullException.ThrowIfNull(zaakUrl);
|
||||||
ArgumentNullException.ThrowIfNull(content);
|
ArgumentNullException.ThrowIfNull(content);
|
||||||
@@ -65,7 +65,7 @@ public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, IZaak
|
|||||||
|
|
||||||
var request = new DocumentRequest(
|
var request = new DocumentRequest(
|
||||||
defaults.Bronorganisatie,
|
defaults.Bronorganisatie,
|
||||||
await catalog.GetInformatieobjecttypeUrlAsync(ct),
|
defaults.InformatieobjecttypeUrl,
|
||||||
defaults.Vertrouwelijkheidaanduiding,
|
defaults.Vertrouwelijkheidaanduiding,
|
||||||
zaakUrl,
|
zaakUrl,
|
||||||
clock.Today,
|
clock.Today,
|
||||||
@@ -76,6 +76,6 @@ public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, IZaak
|
|||||||
Formaat: contentType,
|
Formaat: contentType,
|
||||||
Inhoud: content);
|
Inhoud: content);
|
||||||
|
|
||||||
return await gateway.StoreDocumentAsync(request, ct);
|
return gateway.StoreDocumentAsync(request, ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
namespace Acl.Application;
|
|
||||||
|
|
||||||
/// <summary>Resolves the zaaktype + diploma-informatieobjecttype URLs from the Catalogi API on first
|
|
||||||
/// use and caches them for the process lifetime (S-27, ADR-0021). Lazy (not at startup) so the ACL
|
|
||||||
/// never crash-loops when it boots before the catalogus is seeded/published; a <em>failed</em>
|
|
||||||
/// resolution is not cached, so it is retried on the next call (e.g. once the zaaktype is published).
|
|
||||||
/// A process restart re-resolves.</summary>
|
|
||||||
public sealed class CachedZaaktypeCatalog(IZaakGateway gateway, AclDefaults defaults) : IZaaktypeCatalog
|
|
||||||
{
|
|
||||||
private readonly SemaphoreSlim gate = new(1, 1);
|
|
||||||
private Uri? zaaktype;
|
|
||||||
private Uri? informatieobjecttype;
|
|
||||||
|
|
||||||
public Task<Uri> GetZaaktypeUrlAsync(CancellationToken ct = default) =>
|
|
||||||
ResolveOnceAsync(
|
|
||||||
() => zaaktype, value => zaaktype = value,
|
|
||||||
() => gateway.ResolveZaaktypeUrlAsync(defaults.ZaaktypeIdentificatie, ct), ct);
|
|
||||||
|
|
||||||
public Task<Uri> GetInformatieobjecttypeUrlAsync(CancellationToken ct = default) =>
|
|
||||||
ResolveOnceAsync(
|
|
||||||
() => informatieobjecttype, value => informatieobjecttype = value,
|
|
||||||
() => gateway.ResolveInformatieobjecttypeUrlAsync(defaults.InformatieobjecttypeOmschrijving, ct), ct);
|
|
||||||
|
|
||||||
// Double-checked, single-flight resolution: return the cache if set; otherwise resolve under the
|
|
||||||
// gate and cache only on success (a throw leaves the cache empty so the next call retries).
|
|
||||||
private async Task<Uri> ResolveOnceAsync(Func<Uri?> read, Action<Uri> store, Func<Task<Uri>> resolve, CancellationToken ct)
|
|
||||||
{
|
|
||||||
if (read() is { } cached)
|
|
||||||
return cached;
|
|
||||||
|
|
||||||
await gate.WaitAsync(ct);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (read() is { } existing)
|
|
||||||
return existing;
|
|
||||||
|
|
||||||
var resolved = await resolve();
|
|
||||||
store(resolved);
|
|
||||||
return resolved;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
gate.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -32,12 +32,4 @@ public interface IZaakGateway
|
|||||||
/// the created informatieobject.
|
/// the created informatieobject.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<Uri> StoreDocumentAsync(DocumentRequest request, CancellationToken ct = default);
|
Task<Uri> StoreDocumentAsync(DocumentRequest request, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>Resolve the URL of the published zaaktype with the given <paramref name="identificatie"/>
|
|
||||||
/// from the Catalogi API (S-27). Throws if no published zaaktype matches.</summary>
|
|
||||||
Task<Uri> ResolveZaaktypeUrlAsync(string identificatie, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>Resolve the URL of the published informatieobjecttype with the given
|
|
||||||
/// <paramref name="omschrijving"/> from the Catalogi API (S-27). Throws if none matches.</summary>
|
|
||||||
Task<Uri> ResolveInformatieobjecttypeUrlAsync(string omschrijving, CancellationToken ct = default);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace Acl.Application;
|
|
||||||
|
|
||||||
/// <summary>Supplies the ACL's zaaktype + diploma-informatieobjecttype URLs, resolved from OpenZaak's
|
|
||||||
/// Catalogi API by their stable business keys (<see cref="AclDefaults.ZaaktypeIdentificatie"/> /
|
|
||||||
/// <see cref="AclDefaults.InformatieobjecttypeOmschrijving"/>) rather than pinned in config (S-27,
|
|
||||||
/// ADR-0021). Implementations resolve lazily on first use and cache the result.</summary>
|
|
||||||
public interface IZaaktypeCatalog
|
|
||||||
{
|
|
||||||
Task<Uri> GetZaaktypeUrlAsync(CancellationToken ct = default);
|
|
||||||
|
|
||||||
Task<Uri> GetInformatieobjecttypeUrlAsync(CancellationToken ct = default);
|
|
||||||
}
|
|
||||||
@@ -142,48 +142,6 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) :
|
|||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Uri> ResolveZaaktypeUrlAsync(string identificatie, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(identificatie);
|
|
||||||
|
|
||||||
// The published zaaktype with this identificatie; status=definitief excludes concepts.
|
|
||||||
var page = await GetAsync<ZaaktypePage>(
|
|
||||||
"/catalogi/api/v1/zaaktypen?status=definitief&identificatie=" + Uri.EscapeDataString(identificatie),
|
|
||||||
"zaaktypen", ct);
|
|
||||||
var match = (page.Results ?? []).FirstOrDefault()
|
|
||||||
?? throw new InvalidOperationException(
|
|
||||||
$"No published zaaktype with identificatie '{identificatie}' found in OpenZaak — is the BIG catalogus seeded and published?");
|
|
||||||
return new Uri(match.Url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Uri> ResolveInformatieobjecttypeUrlAsync(string omschrijving, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(omschrijving);
|
|
||||||
|
|
||||||
// The informatieobjecttypen collection has no omschrijving filter, so match client-side over the
|
|
||||||
// published ones.
|
|
||||||
var page = await GetAsync<InformatieobjecttypePage>(
|
|
||||||
"/catalogi/api/v1/informatieobjecttypen?status=definitief", "informatieobjecttypen", ct);
|
|
||||||
var match = (page.Results ?? []).FirstOrDefault(i => i.Omschrijving == omschrijving)
|
|
||||||
?? throw new InvalidOperationException(
|
|
||||||
$"No published informatieobjecttype '{omschrijving}' found in OpenZaak — is the BIG catalogus seeded and published?");
|
|
||||||
return new Uri(match.Url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// GETs an absolute-by-path ZGW resource with auth (no CRS — catalogi is not a geo API).
|
|
||||||
private async Task<T> GetAsync<T>(string pathAndQuery, string label, CancellationToken ct)
|
|
||||||
{
|
|
||||||
using var message = new HttpRequestMessage(HttpMethod.Get, new Uri(options.BaseUrl, pathAndQuery));
|
|
||||||
message.Headers.Authorization =
|
|
||||||
new AuthenticationHeaderValue("Bearer", ZgwToken.Mint(options.ClientId, options.Secret));
|
|
||||||
|
|
||||||
using var response = await http.SendAsync(message, ct);
|
|
||||||
await EnsureSuccessAsync(response, $"Querying {label}", ct);
|
|
||||||
|
|
||||||
return await response.Content.ReadFromJsonAsync<T>(ct)
|
|
||||||
?? throw new InvalidOperationException($"OpenZaak returned an empty {label} response");
|
|
||||||
}
|
|
||||||
|
|
||||||
// POSTs a non-geo ZGW resource (resultaat/status — no CRS headers). Buffers the body so uwsgi gets
|
// POSTs a non-geo ZGW resource (resultaat/status — no CRS headers). Buffers the body so uwsgi gets
|
||||||
// a Content-Length instead of a chunked body (as with zaak-create).
|
// a Content-Length instead of a chunked body (as with zaak-create).
|
||||||
private async Task PostAsync(string path, object dto, string action, CancellationToken ct)
|
private async Task PostAsync(string path, object dto, string action, CancellationToken ct)
|
||||||
@@ -340,18 +298,4 @@ public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) :
|
|||||||
private sealed record ZaakInformatieobjectDto(
|
private sealed record ZaakInformatieobjectDto(
|
||||||
[property: JsonPropertyName("zaak")] string Zaak,
|
[property: JsonPropertyName("zaak")] string Zaak,
|
||||||
[property: JsonPropertyName("informatieobject")] string Informatieobject);
|
[property: JsonPropertyName("informatieobject")] string Informatieobject);
|
||||||
|
|
||||||
private sealed record ZaaktypePage(
|
|
||||||
[property: JsonPropertyName("results")] IReadOnlyList<ZaaktypeDto>? Results);
|
|
||||||
|
|
||||||
private sealed record ZaaktypeDto(
|
|
||||||
[property: JsonPropertyName("url")] string Url,
|
|
||||||
[property: JsonPropertyName("identificatie")] string? Identificatie);
|
|
||||||
|
|
||||||
private sealed record InformatieobjecttypePage(
|
|
||||||
[property: JsonPropertyName("results")] IReadOnlyList<InformatieobjecttypeDto>? Results);
|
|
||||||
|
|
||||||
private sealed record InformatieobjecttypeDto(
|
|
||||||
[property: JsonPropertyName("url")] string Url,
|
|
||||||
[property: JsonPropertyName("omschrijving")] string? Omschrijving);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,32 +161,4 @@ public sealed class OpenZaakGatewayIntegrationTests(OpenZaakFixture stack)
|
|||||||
Assert.Contains(relations.EnumerateArray(),
|
Assert.Contains(relations.EnumerateArray(),
|
||||||
r => r.GetProperty("zaak").GetString() == zaakUrl.ToString());
|
r => r.GetProperty("zaak").GetString() == zaakUrl.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Resolves_the_published_zaaktype_and_diploma_informatieobjecttype_by_business_key()
|
|
||||||
{
|
|
||||||
var expectedZaaktype = await stack.FindPublishedBigZaaktypeAsync();
|
|
||||||
Assert.True(expectedZaaktype is not null,
|
|
||||||
"No published BIG-REGISTRATIE zaaktype found — seed the stack with OZ_PUBLISH=1.");
|
|
||||||
var expectedInformatieobjecttype = await stack.FindPublishedDiplomaInformatieobjecttypeAsync();
|
|
||||||
Assert.True(expectedInformatieobjecttype is not null,
|
|
||||||
"No published Diploma informatieobjecttype found — seed the stack with OZ_PUBLISH=1.");
|
|
||||||
|
|
||||||
var gateway = new OpenZaakGateway(stack.Http, stack.Options);
|
|
||||||
|
|
||||||
// The ACL discovers both URLs from the live Catalogi API by their stable business keys (S-27),
|
|
||||||
// matching what the fixture found independently — no pinned URL needed.
|
|
||||||
Assert.Equal(expectedZaaktype, await gateway.ResolveZaaktypeUrlAsync("BIG-REGISTRATIE"));
|
|
||||||
Assert.Equal(expectedInformatieobjecttype, await gateway.ResolveInformatieobjecttypeUrlAsync("Diploma"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Resolving_an_unknown_zaaktype_identificatie_throws_a_clear_error()
|
|
||||||
{
|
|
||||||
var gateway = new OpenZaakGateway(stack.Http, stack.Options);
|
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
|
||||||
() => gateway.ResolveZaaktypeUrlAsync("NO-SUCH-ZAAKTYPE"));
|
|
||||||
Assert.Contains("NO-SUCH-ZAAKTYPE", ex.Message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ public class AclServiceTests
|
|||||||
{
|
{
|
||||||
private sealed class FakeGateway : IZaakGateway
|
private sealed class FakeGateway : IZaakGateway
|
||||||
{
|
{
|
||||||
// The URLs the catalogus resolves the configured identificatie/omschrijving to (S-27).
|
|
||||||
public Uri ResolvedZaaktype { get; } = new("http://openzaak/catalogi/api/v1/zaaktypen/big");
|
|
||||||
public Uri ResolvedInformatieobjecttype { get; } = new("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip");
|
|
||||||
public string? ResolvedByIdentificatie;
|
|
||||||
public string? ResolvedByOmschrijving;
|
|
||||||
|
|
||||||
public ZaakRequest? Captured;
|
public ZaakRequest? Captured;
|
||||||
public Uri Result { get; } = new("http://openzaak/zaken/api/v1/zaken/abc");
|
public Uri Result { get; } = new("http://openzaak/zaken/api/v1/zaken/abc");
|
||||||
|
|
||||||
@@ -53,18 +47,6 @@ public class AclServiceTests
|
|||||||
StoredDocument = request;
|
StoredDocument = request;
|
||||||
return Task.FromResult(DocumentResult);
|
return Task.FromResult(DocumentResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Uri> ResolveZaaktypeUrlAsync(string identificatie, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
ResolvedByIdentificatie = identificatie;
|
|
||||||
return Task.FromResult(ResolvedZaaktype);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Uri> ResolveInformatieobjecttypeUrlAsync(string omschrijving, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
ResolvedByOmschrijving = omschrijving;
|
|
||||||
return Task.FromResult(ResolvedInformatieobjecttype);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AclDefaults Defaults() => new()
|
private static AclDefaults Defaults() => new()
|
||||||
@@ -72,23 +54,28 @@ public class AclServiceTests
|
|||||||
Bronorganisatie = "517439943",
|
Bronorganisatie = "517439943",
|
||||||
VerantwoordelijkeOrganisatie = "517439943",
|
VerantwoordelijkeOrganisatie = "517439943",
|
||||||
Vertrouwelijkheidaanduiding = "openbaar",
|
Vertrouwelijkheidaanduiding = "openbaar",
|
||||||
ZaaktypeIdentificatie = "BIG-REGISTRATIE",
|
ZaaktypeUrl = new("http://openzaak/catalogi/api/v1/zaaktypen/big"),
|
||||||
InformatieobjecttypeOmschrijving = "Diploma",
|
InformatieobjecttypeUrl = new("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip"),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static AclService ServiceWith(FakeGateway gateway, AclDefaults defaults, DateOnly today) =>
|
|
||||||
new(gateway, defaults, new CachedZaaktypeCatalog(gateway, defaults), new FixedClock(today));
|
|
||||||
|
|
||||||
private sealed class FixedClock(DateOnly today) : IClock
|
private sealed class FixedClock(DateOnly today) : IClock
|
||||||
{
|
{
|
||||||
public DateOnly Today { get; } = today;
|
public DateOnly Today { get; } = today;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Opening_a_zaak_default_fills_zgw_fields_and_uses_the_resolved_zaaktype()
|
public async Task Opening_a_zaak_default_fills_zgw_fields_and_returns_the_zaak_url()
|
||||||
{
|
{
|
||||||
var gateway = new FakeGateway();
|
var gateway = new FakeGateway();
|
||||||
var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4));
|
var defaults = new AclDefaults
|
||||||
|
{
|
||||||
|
Bronorganisatie = "517439943",
|
||||||
|
VerantwoordelijkeOrganisatie = "517439943",
|
||||||
|
Vertrouwelijkheidaanduiding = "openbaar",
|
||||||
|
ZaaktypeUrl = new("http://openzaak/catalogi/api/v1/zaaktypen/big"),
|
||||||
|
InformatieobjecttypeUrl = new("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip"),
|
||||||
|
};
|
||||||
|
var service = new AclService(gateway, defaults, new FixedClock(new DateOnly(2026, 6, 4)));
|
||||||
|
|
||||||
var url = await service.OpenZaakAsync(new DomainRegistration("123456782", "reg-77"));
|
var url = await service.OpenZaakAsync(new DomainRegistration("123456782", "reg-77"));
|
||||||
|
|
||||||
@@ -97,9 +84,7 @@ public class AclServiceTests
|
|||||||
Assert.Equal("517439943", req.Bronorganisatie);
|
Assert.Equal("517439943", req.Bronorganisatie);
|
||||||
Assert.Equal("517439943", req.VerantwoordelijkeOrganisatie);
|
Assert.Equal("517439943", req.VerantwoordelijkeOrganisatie);
|
||||||
Assert.Equal("openbaar", req.Vertrouwelijkheidaanduiding);
|
Assert.Equal("openbaar", req.Vertrouwelijkheidaanduiding);
|
||||||
// The zaaktype is resolved from the configured identificatie, not a pinned URL (S-27).
|
Assert.Equal(defaults.ZaaktypeUrl, req.Zaaktype);
|
||||||
Assert.Equal("BIG-REGISTRATIE", gateway.ResolvedByIdentificatie);
|
|
||||||
Assert.Equal(gateway.ResolvedZaaktype, req.Zaaktype);
|
|
||||||
Assert.Equal(new DateOnly(2026, 6, 4), req.Startdatum);
|
Assert.Equal(new DateOnly(2026, 6, 4), req.Startdatum);
|
||||||
// The registration reference becomes the zaak identificatie (#78).
|
// The registration reference becomes the zaak identificatie (#78).
|
||||||
Assert.Equal("reg-77", req.Identificatie);
|
Assert.Equal("reg-77", req.Identificatie);
|
||||||
@@ -109,24 +94,33 @@ public class AclServiceTests
|
|||||||
public async Task Rejects_a_null_registration_without_calling_the_gateway()
|
public async Task Rejects_a_null_registration_without_calling_the_gateway()
|
||||||
{
|
{
|
||||||
var gateway = new FakeGateway();
|
var gateway = new FakeGateway();
|
||||||
var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4));
|
var defaults = new AclDefaults
|
||||||
|
{
|
||||||
|
Bronorganisatie = "517439943",
|
||||||
|
VerantwoordelijkeOrganisatie = "517439943",
|
||||||
|
Vertrouwelijkheidaanduiding = "openbaar",
|
||||||
|
ZaaktypeUrl = new("http://openzaak/catalogi/api/v1/zaaktypen/big"),
|
||||||
|
InformatieobjecttypeUrl = new("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip"),
|
||||||
|
};
|
||||||
|
var service = new AclService(gateway, defaults, new FixedClock(new DateOnly(2026, 6, 4)));
|
||||||
|
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>(() => service.OpenZaakAsync(null!));
|
await Assert.ThrowsAsync<ArgumentNullException>(() => service.OpenZaakAsync(null!));
|
||||||
Assert.Null(gateway.Captured);
|
Assert.Null(gateway.Captured);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Approving_a_zaak_sets_it_to_its_resolved_zaaktypes_eindstatus_dated_today()
|
public async Task Approving_a_zaak_sets_it_to_its_zaaktypes_eindstatus_dated_today()
|
||||||
{
|
{
|
||||||
var gateway = new FakeGateway();
|
var gateway = new FakeGateway();
|
||||||
var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4));
|
var defaults = Defaults();
|
||||||
|
var service = new AclService(gateway, defaults, new FixedClock(new DateOnly(2026, 6, 4)));
|
||||||
var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc");
|
var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc");
|
||||||
|
|
||||||
await service.ApproveZaakAsync(zaak);
|
await service.ApproveZaakAsync(zaak);
|
||||||
|
|
||||||
Assert.NotNull(gateway.Approved);
|
Assert.NotNull(gateway.Approved);
|
||||||
Assert.Equal(zaak, gateway.Approved!.Value.Zaak);
|
Assert.Equal(zaak, gateway.Approved!.Value.Zaak);
|
||||||
Assert.Equal(gateway.ResolvedZaaktype, gateway.Approved.Value.Zaaktype);
|
Assert.Equal(defaults.ZaaktypeUrl, gateway.Approved.Value.Zaaktype);
|
||||||
Assert.Equal(new DateOnly(2026, 6, 4), gateway.Approved.Value.Datum);
|
Assert.Equal(new DateOnly(2026, 6, 4), gateway.Approved.Value.Datum);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +128,7 @@ public class AclServiceTests
|
|||||||
public async Task Approving_a_null_zaak_is_rejected_without_touching_the_gateway()
|
public async Task Approving_a_null_zaak_is_rejected_without_touching_the_gateway()
|
||||||
{
|
{
|
||||||
var gateway = new FakeGateway();
|
var gateway = new FakeGateway();
|
||||||
var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4));
|
var service = new AclService(gateway, Defaults(), new FixedClock(new DateOnly(2026, 6, 4)));
|
||||||
|
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>(() => service.ApproveZaakAsync(null!));
|
await Assert.ThrowsAsync<ArgumentNullException>(() => service.ApproveZaakAsync(null!));
|
||||||
Assert.Null(gateway.Approved);
|
Assert.Null(gateway.Approved);
|
||||||
@@ -144,14 +138,15 @@ public class AclServiceTests
|
|||||||
public async Task Cancelling_a_zaak_sets_it_to_the_cancellation_status_dated_today()
|
public async Task Cancelling_a_zaak_sets_it_to_the_cancellation_status_dated_today()
|
||||||
{
|
{
|
||||||
var gateway = new FakeGateway();
|
var gateway = new FakeGateway();
|
||||||
var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4));
|
var defaults = Defaults();
|
||||||
|
var service = new AclService(gateway, defaults, new FixedClock(new DateOnly(2026, 6, 4)));
|
||||||
var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc");
|
var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc");
|
||||||
|
|
||||||
await service.CancelZaakAsync(zaak);
|
await service.CancelZaakAsync(zaak);
|
||||||
|
|
||||||
Assert.NotNull(gateway.Cancelled);
|
Assert.NotNull(gateway.Cancelled);
|
||||||
Assert.Equal(zaak, gateway.Cancelled!.Value.Zaak);
|
Assert.Equal(zaak, gateway.Cancelled!.Value.Zaak);
|
||||||
Assert.Equal(gateway.ResolvedZaaktype, gateway.Cancelled.Value.Zaaktype);
|
Assert.Equal(defaults.ZaaktypeUrl, gateway.Cancelled.Value.Zaaktype);
|
||||||
Assert.Equal(new DateOnly(2026, 6, 4), gateway.Cancelled.Value.Datum);
|
Assert.Equal(new DateOnly(2026, 6, 4), gateway.Cancelled.Value.Datum);
|
||||||
// Cancellation must not touch the approval path.
|
// Cancellation must not touch the approval path.
|
||||||
Assert.Null(gateway.Approved);
|
Assert.Null(gateway.Approved);
|
||||||
@@ -161,17 +156,18 @@ public class AclServiceTests
|
|||||||
public async Task Cancelling_a_null_zaak_is_rejected_without_touching_the_gateway()
|
public async Task Cancelling_a_null_zaak_is_rejected_without_touching_the_gateway()
|
||||||
{
|
{
|
||||||
var gateway = new FakeGateway();
|
var gateway = new FakeGateway();
|
||||||
var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4));
|
var service = new AclService(gateway, Defaults(), new FixedClock(new DateOnly(2026, 6, 4)));
|
||||||
|
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>(() => service.CancelZaakAsync(null!));
|
await Assert.ThrowsAsync<ArgumentNullException>(() => service.CancelZaakAsync(null!));
|
||||||
Assert.Null(gateway.Cancelled);
|
Assert.Null(gateway.Cancelled);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Storing_a_diploma_default_fills_the_document_fields_and_uses_the_resolved_informatieobjecttype()
|
public async Task Storing_a_diploma_default_fills_the_document_fields_and_returns_its_url()
|
||||||
{
|
{
|
||||||
var gateway = new FakeGateway();
|
var gateway = new FakeGateway();
|
||||||
var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4));
|
var defaults = Defaults();
|
||||||
|
var service = new AclService(gateway, defaults, new FixedClock(new DateOnly(2026, 6, 4)));
|
||||||
var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc");
|
var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc");
|
||||||
|
|
||||||
var url = await service.StoreDiplomaAsync(zaak, [1, 2, 3], "diploma.pdf", "application/pdf");
|
var url = await service.StoreDiplomaAsync(zaak, [1, 2, 3], "diploma.pdf", "application/pdf");
|
||||||
@@ -179,9 +175,7 @@ public class AclServiceTests
|
|||||||
Assert.Equal(gateway.DocumentResult, url);
|
Assert.Equal(gateway.DocumentResult, url);
|
||||||
var req = gateway.StoredDocument!;
|
var req = gateway.StoredDocument!;
|
||||||
Assert.Equal(zaak, req.Zaak);
|
Assert.Equal(zaak, req.Zaak);
|
||||||
// The informatieobjecttype is resolved from the configured omschrijving (S-27).
|
Assert.Equal(defaults.InformatieobjecttypeUrl, req.Informatieobjecttype);
|
||||||
Assert.Equal("Diploma", gateway.ResolvedByOmschrijving);
|
|
||||||
Assert.Equal(gateway.ResolvedInformatieobjecttype, req.Informatieobjecttype);
|
|
||||||
Assert.Equal("517439943", req.Bronorganisatie);
|
Assert.Equal("517439943", req.Bronorganisatie);
|
||||||
Assert.Equal("openbaar", req.Vertrouwelijkheidaanduiding);
|
Assert.Equal("openbaar", req.Vertrouwelijkheidaanduiding);
|
||||||
Assert.Equal(new DateOnly(2026, 6, 4), req.Creatiedatum);
|
Assert.Equal(new DateOnly(2026, 6, 4), req.Creatiedatum);
|
||||||
@@ -194,7 +188,7 @@ public class AclServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task Storing_a_diploma_rejects_null_or_blank_arguments()
|
public async Task Storing_a_diploma_rejects_null_or_blank_arguments()
|
||||||
{
|
{
|
||||||
var service = ServiceWith(new FakeGateway(), Defaults(), new DateOnly(2026, 6, 4));
|
var service = new AclService(new FakeGateway(), Defaults(), new FixedClock(new DateOnly(2026, 6, 4)));
|
||||||
var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc");
|
var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc");
|
||||||
|
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>(() => service.StoreDiplomaAsync(null!, [1], "d.pdf", "application/pdf"));
|
await Assert.ThrowsAsync<ArgumentNullException>(() => service.StoreDiplomaAsync(null!, [1], "d.pdf", "application/pdf"));
|
||||||
@@ -207,7 +201,7 @@ public class AclServiceTests
|
|||||||
public async Task Reading_a_zaak_reference_returns_the_zaaks_identificatie()
|
public async Task Reading_a_zaak_reference_returns_the_zaaks_identificatie()
|
||||||
{
|
{
|
||||||
var gateway = new FakeGateway();
|
var gateway = new FakeGateway();
|
||||||
var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4));
|
var service = new AclService(gateway, Defaults(), new FixedClock(new DateOnly(2026, 6, 4)));
|
||||||
var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc");
|
var zaak = new Uri("http://openzaak/zaken/api/v1/zaken/abc");
|
||||||
|
|
||||||
var reference = await service.GetZaakReferenceAsync(zaak);
|
var reference = await service.GetZaakReferenceAsync(zaak);
|
||||||
@@ -220,7 +214,7 @@ public class AclServiceTests
|
|||||||
public async Task Reading_a_null_zaak_reference_is_rejected()
|
public async Task Reading_a_null_zaak_reference_is_rejected()
|
||||||
{
|
{
|
||||||
var gateway = new FakeGateway();
|
var gateway = new FakeGateway();
|
||||||
var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4));
|
var service = new AclService(gateway, Defaults(), new FixedClock(new DateOnly(2026, 6, 4)));
|
||||||
|
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>(() => service.GetZaakReferenceAsync(null!));
|
await Assert.ThrowsAsync<ArgumentNullException>(() => service.GetZaakReferenceAsync(null!));
|
||||||
Assert.Null(gateway.ReadReferenceFor);
|
Assert.Null(gateway.ReadReferenceFor);
|
||||||
|
|||||||
@@ -675,153 +675,4 @@ public class OpenZaakGatewayTests
|
|||||||
|
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>(() => Gateway(handler).StoreDocumentAsync(null!));
|
await Assert.ThrowsAsync<ArgumentNullException>(() => Gateway(handler).StoreDocumentAsync(null!));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Catalogi resolution by business key (S-27) ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Resolves_the_published_zaaktype_url_by_identificatie()
|
|
||||||
{
|
|
||||||
HttpRequestMessage? seen = null;
|
|
||||||
var handler = new StubHandler(req =>
|
|
||||||
{
|
|
||||||
seen = req;
|
|
||||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
|
||||||
{
|
|
||||||
Content = JsonContent.Create(new
|
|
||||||
{
|
|
||||||
results = new[] { new { url = "http://openzaak/catalogi/api/v1/zaaktypen/big", identificatie = "BIG-REGISTRATIE" } },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var url = await Gateway(handler).ResolveZaaktypeUrlAsync("BIG-REGISTRATIE");
|
|
||||||
|
|
||||||
Assert.Equal("http://openzaak/catalogi/api/v1/zaaktypen/big", url.ToString());
|
|
||||||
Assert.Equal(HttpMethod.Get, seen!.Method);
|
|
||||||
// Filters to the published zaaktype with that identificatie, and authenticates.
|
|
||||||
Assert.Contains("/catalogi/api/v1/zaaktypen", seen.RequestUri!.ToString());
|
|
||||||
Assert.Contains("status=definitief", seen.RequestUri!.Query);
|
|
||||||
Assert.Contains("identificatie=BIG-REGISTRATIE", seen.RequestUri!.Query);
|
|
||||||
Assert.Equal("Bearer", seen.Headers.Authorization!.Scheme);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Resolving_a_zaaktype_throws_a_clear_error_when_none_is_published()
|
|
||||||
{
|
|
||||||
var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
|
||||||
{
|
|
||||||
Content = JsonContent.Create(new { results = Array.Empty<object>() }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
|
||||||
() => Gateway(handler).ResolveZaaktypeUrlAsync("BIG-REGISTRATIE"));
|
|
||||||
Assert.Contains("BIG-REGISTRATIE", ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Resolves_the_informatieobjecttype_url_by_omschrijving()
|
|
||||||
{
|
|
||||||
HttpRequestMessage? seen = null;
|
|
||||||
var handler = new StubHandler(req =>
|
|
||||||
{
|
|
||||||
seen = req;
|
|
||||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
|
||||||
{
|
|
||||||
Content = JsonContent.Create(new
|
|
||||||
{
|
|
||||||
results = new[]
|
|
||||||
{
|
|
||||||
new { url = "http://openzaak/catalogi/api/v1/informatieobjecttypen/other", omschrijving = "Overig" },
|
|
||||||
new { url = "http://openzaak/catalogi/api/v1/informatieobjecttypen/dip", omschrijving = "Diploma" },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var url = await Gateway(handler).ResolveInformatieobjecttypeUrlAsync("Diploma");
|
|
||||||
|
|
||||||
// Queries the published informatieobjecttypen collection, and matches on omschrijving (not position).
|
|
||||||
Assert.Contains("/catalogi/api/v1/informatieobjecttypen", seen!.RequestUri!.ToString());
|
|
||||||
Assert.Contains("status=definitief", seen.RequestUri!.Query);
|
|
||||||
Assert.Equal("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip", url.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Resolving_a_zaaktype_throws_when_the_response_carries_no_results()
|
|
||||||
{
|
|
||||||
// No "results" property → the page's Results is null; the gateway must treat that as "none
|
|
||||||
// found" (not dereference null).
|
|
||||||
var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
|
||||||
{
|
|
||||||
Content = JsonContent.Create(new { count = 0 }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
|
||||||
() => Gateway(handler).ResolveZaaktypeUrlAsync("BIG-REGISTRATIE"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Resolving_an_informatieobjecttype_throws_when_the_response_carries_no_results()
|
|
||||||
{
|
|
||||||
var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
|
||||||
{
|
|
||||||
Content = JsonContent.Create(new { count = 0 }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
|
||||||
() => Gateway(handler).ResolveInformatieobjecttypeUrlAsync("Diploma"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Resolving_a_zaaktype_surfaces_a_non_success_catalogi_response()
|
|
||||||
{
|
|
||||||
var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
|
||||||
{
|
|
||||||
Content = new StringContent("boom"),
|
|
||||||
}));
|
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<HttpRequestException>(
|
|
||||||
() => Gateway(handler).ResolveZaaktypeUrlAsync("BIG-REGISTRATIE"));
|
|
||||||
// The error names the resource being queried and includes OpenZaak's body.
|
|
||||||
Assert.Contains("zaaktypen", ex.Message);
|
|
||||||
Assert.Contains("boom", ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Resolving_an_informatieobjecttype_surfaces_a_non_success_catalogi_response()
|
|
||||||
{
|
|
||||||
var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.InternalServerError)
|
|
||||||
{
|
|
||||||
Content = new StringContent("boom"),
|
|
||||||
}));
|
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<HttpRequestException>(
|
|
||||||
() => Gateway(handler).ResolveInformatieobjecttypeUrlAsync("Diploma"));
|
|
||||||
Assert.Contains("informatieobjecttypen", ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Resolving_an_informatieobjecttype_throws_when_no_omschrijving_matches()
|
|
||||||
{
|
|
||||||
var handler = new StubHandler(_ => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
|
|
||||||
{
|
|
||||||
Content = JsonContent.Create(new
|
|
||||||
{
|
|
||||||
results = new[] { new { url = "http://openzaak/catalogi/api/v1/informatieobjecttypen/other", omschrijving = "Overig" } },
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
|
||||||
() => Gateway(handler).ResolveInformatieobjecttypeUrlAsync("Diploma"));
|
|
||||||
Assert.Contains("Diploma", ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Resolving_rejects_a_blank_business_key_without_calling_openzaak()
|
|
||||||
{
|
|
||||||
var handler = new StubHandler(_ => throw new InvalidOperationException("should not be sent"));
|
|
||||||
|
|
||||||
await Assert.ThrowsAnyAsync<ArgumentException>(() => Gateway(handler).ResolveZaaktypeUrlAsync(" "));
|
|
||||||
await Assert.ThrowsAnyAsync<ArgumentException>(() => Gateway(handler).ResolveInformatieobjecttypeUrlAsync(" "));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
using Acl.Application;
|
|
||||||
|
|
||||||
namespace Acl.Tests;
|
|
||||||
|
|
||||||
public class ZaaktypeCatalogTests
|
|
||||||
{
|
|
||||||
// A gateway that only supports resolution; the other members are unused here.
|
|
||||||
private sealed class ResolvingGateway : IZaakGateway
|
|
||||||
{
|
|
||||||
public int ZaaktypeCalls;
|
|
||||||
public int InformatieobjecttypeCalls;
|
|
||||||
public string? LastIdentificatie;
|
|
||||||
public string? LastOmschrijving;
|
|
||||||
public int ThrowZaaktypeTimes;
|
|
||||||
public Uri ZaaktypeUrl { get; } = new("http://openzaak/catalogi/api/v1/zaaktypen/big");
|
|
||||||
public Uri InformatieobjecttypeUrl { get; } = new("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip");
|
|
||||||
|
|
||||||
public Task<Uri> ResolveZaaktypeUrlAsync(string identificatie, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
ZaaktypeCalls++;
|
|
||||||
LastIdentificatie = identificatie;
|
|
||||||
if (ZaaktypeCalls <= ThrowZaaktypeTimes)
|
|
||||||
throw new InvalidOperationException("no published zaaktype yet");
|
|
||||||
return Task.FromResult(ZaaktypeUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Uri> ResolveInformatieobjecttypeUrlAsync(string omschrijving, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
InformatieobjecttypeCalls++;
|
|
||||||
LastOmschrijving = omschrijving;
|
|
||||||
return Task.FromResult(InformatieobjecttypeUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Uri> OpenZaakAsync(ZaakRequest request, CancellationToken ct = default) => throw new NotSupportedException();
|
|
||||||
public Task SetZaakToEindstatusAsync(Uri z, Uri zt, DateOnly d, CancellationToken ct = default) => throw new NotSupportedException();
|
|
||||||
public Task SetZaakToCancellationStatusAsync(Uri z, Uri zt, DateOnly d, CancellationToken ct = default) => throw new NotSupportedException();
|
|
||||||
public Task<string> GetZaakIdentificatieAsync(Uri z, CancellationToken ct = default) => throw new NotSupportedException();
|
|
||||||
public Task<Uri> StoreDocumentAsync(DocumentRequest r, CancellationToken ct = default) => throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AclDefaults Defaults() => new()
|
|
||||||
{
|
|
||||||
Bronorganisatie = "517439943",
|
|
||||||
VerantwoordelijkeOrganisatie = "517439943",
|
|
||||||
Vertrouwelijkheidaanduiding = "openbaar",
|
|
||||||
ZaaktypeIdentificatie = "BIG-REGISTRATIE",
|
|
||||||
InformatieobjecttypeOmschrijving = "Diploma",
|
|
||||||
};
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Resolves_the_zaaktype_and_informatieobjecttype_by_their_configured_business_keys()
|
|
||||||
{
|
|
||||||
var gateway = new ResolvingGateway();
|
|
||||||
var catalog = new CachedZaaktypeCatalog(gateway, Defaults());
|
|
||||||
|
|
||||||
Assert.Equal(gateway.ZaaktypeUrl, await catalog.GetZaaktypeUrlAsync());
|
|
||||||
Assert.Equal(gateway.InformatieobjecttypeUrl, await catalog.GetInformatieobjecttypeUrlAsync());
|
|
||||||
Assert.Equal("BIG-REGISTRATIE", gateway.LastIdentificatie);
|
|
||||||
Assert.Equal("Diploma", gateway.LastOmschrijving);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Caches_the_resolved_urls_so_the_gateway_is_hit_once()
|
|
||||||
{
|
|
||||||
var gateway = new ResolvingGateway();
|
|
||||||
var catalog = new CachedZaaktypeCatalog(gateway, Defaults());
|
|
||||||
|
|
||||||
for (var i = 0; i < 3; i++)
|
|
||||||
{
|
|
||||||
await catalog.GetZaaktypeUrlAsync();
|
|
||||||
await catalog.GetInformatieobjecttypeUrlAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert.Equal(1, gateway.ZaaktypeCalls);
|
|
||||||
Assert.Equal(1, gateway.InformatieobjecttypeCalls);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Does_not_cache_a_failed_resolution_so_it_is_retried()
|
|
||||||
{
|
|
||||||
// The zaaktype is not published yet on the first call; the catalog must retry (not cache the
|
|
||||||
// failure) so a later call succeeds once it is published.
|
|
||||||
var gateway = new ResolvingGateway { ThrowZaaktypeTimes = 1 };
|
|
||||||
var catalog = new CachedZaaktypeCatalog(gateway, Defaults());
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(() => catalog.GetZaaktypeUrlAsync());
|
|
||||||
var url = await catalog.GetZaaktypeUrlAsync();
|
|
||||||
|
|
||||||
Assert.Equal(gateway.ZaaktypeUrl, url);
|
|
||||||
Assert.Equal(2, gateway.ZaaktypeCalls);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,10 +5,6 @@ namespace Bff.Api;
|
|||||||
/// <summary>What the self-service submit returns to the portal (the domain's registration id + status).</summary>
|
/// <summary>What the self-service submit returns to the portal (the domain's registration id + status).</summary>
|
||||||
public sealed record SubmitAccepted(string RegistrationId, string Status);
|
public sealed record SubmitAccepted(string RegistrationId, string Status);
|
||||||
|
|
||||||
/// <summary>The caller's current open registration, for resuming the self-service portal after a
|
|
||||||
/// refresh (S-26): the reference (registration id) + its status.</summary>
|
|
||||||
public sealed record CurrentRegistration(string RegistrationId, string Status);
|
|
||||||
|
|
||||||
/// <summary>A projection row as the projection-api serves it. <c>Bsn</c>/<c>NaamPlaceholder</c> are
|
/// <summary>A projection row as the projection-api serves it. <c>Bsn</c>/<c>NaamPlaceholder</c> are
|
||||||
/// read but never surfaced by the openbaar endpoint (public-safe filtering, ADR-0010/S-09).
|
/// read but never surfaced by the openbaar endpoint (public-safe filtering, ADR-0010/S-09).
|
||||||
/// <c>Reference</c> is the public-safe citizen reference (the zaak identificatie, #78).</summary>
|
/// <c>Reference</c> is the public-safe citizen reference (the zaak identificatie, #78).</summary>
|
||||||
@@ -26,10 +22,6 @@ public interface IDomainClient
|
|||||||
{
|
{
|
||||||
Task<SubmitAccepted> SubmitRegistrationAsync(string bsn, CancellationToken ct = default);
|
Task<SubmitAccepted> SubmitRegistrationAsync(string bsn, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>The caller's current open registration (resume after refresh, S-26), or <c>null</c>
|
|
||||||
/// when they have none in flight. Owner-scoped by <paramref name="bsn"/>.</summary>
|
|
||||||
Task<CurrentRegistration?> GetCurrentRegistrationAsync(string bsn, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>Withdraw the caller's own registration ("trek aanvraag in"). Owner-scoped by
|
/// <summary>Withdraw the caller's own registration ("trek aanvraag in"). Owner-scoped by
|
||||||
/// <paramref name="bsn"/>. Returns <c>false</c> when the domain reports the registration is
|
/// <paramref name="bsn"/>. Returns <c>false</c> when the domain reports the registration is
|
||||||
/// unknown or not the caller's (404), so the BFF can relay a 404 rather than a 500.</summary>
|
/// unknown or not the caller's (404), so the BFF can relay a 404 rather than a 500.</summary>
|
||||||
@@ -66,18 +58,6 @@ public sealed class DomainClient(HttpClient http) : IDomainClient
|
|||||||
return new SubmitAccepted(dto.RegistrationId, dto.Status);
|
return new SubmitAccepted(dto.RegistrationId, dto.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CurrentRegistration?> GetCurrentRegistrationAsync(string bsn, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
using var response = await http.GetAsync($"registrations/current?bsn={Uri.EscapeDataString(bsn)}", ct);
|
|
||||||
// The domain 404s when the citizen has no open registration — that's "none", not an error.
|
|
||||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
|
||||||
return null;
|
|
||||||
response.EnsureSuccessStatusCode();
|
|
||||||
var dto = await response.Content.ReadFromJsonAsync<DomainResponse>(ct)
|
|
||||||
?? throw new InvalidOperationException("The Domain Service returned an empty registration response.");
|
|
||||||
return new CurrentRegistration(dto.RegistrationId, dto.Status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> WithdrawRegistrationAsync(string registrationId, string bsn, CancellationToken ct = default)
|
public async Task<bool> WithdrawRegistrationAsync(string registrationId, string bsn, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
using var response = await http.PostAsJsonAsync(
|
using var response = await http.PostAsJsonAsync(
|
||||||
|
|||||||
@@ -86,24 +86,6 @@ app.MapPost("/self-service/registrations", async (ClaimsPrincipal user, IDomainC
|
|||||||
.Produces(StatusCodes.Status400BadRequest)
|
.Produces(StatusCodes.Status400BadRequest)
|
||||||
.Produces(StatusCodes.Status401Unauthorized);
|
.Produces(StatusCodes.Status401Unauthorized);
|
||||||
|
|
||||||
// Self-service resume (S-26): the signed-in zorgprofessional's current open registration, so the
|
|
||||||
// portal can restore its reference + actions after a page refresh. The bsn comes from the DigiD token;
|
|
||||||
// 204 when the citizen has none in flight (so the portal shows the submit form).
|
|
||||||
app.MapGet("/self-service/registrations", async (ClaimsPrincipal user, IDomainClient domain, CancellationToken ct) =>
|
|
||||||
{
|
|
||||||
var bsn = user.FindFirstValue("bsn");
|
|
||||||
if (string.IsNullOrWhiteSpace(bsn))
|
|
||||||
return Results.BadRequest("The token carries no bsn claim.");
|
|
||||||
|
|
||||||
var current = await domain.GetCurrentRegistrationAsync(bsn, ct);
|
|
||||||
return current is null ? Results.NoContent() : Results.Ok(current);
|
|
||||||
})
|
|
||||||
.RequireAuthorization()
|
|
||||||
.Produces<CurrentRegistration>(StatusCodes.Status200OK)
|
|
||||||
.Produces(StatusCodes.Status204NoContent)
|
|
||||||
.Produces(StatusCodes.Status400BadRequest)
|
|
||||||
.Produces(StatusCodes.Status401Unauthorized);
|
|
||||||
|
|
||||||
// Self-service withdrawal (S-11): the signed-in zorgprofessional withdraws their own registration.
|
// Self-service withdrawal (S-11): the signed-in zorgprofessional withdraws their own registration.
|
||||||
// The bsn comes from the DigiD token and is forwarded to the domain, which owner-scopes the action;
|
// The bsn comes from the DigiD token and is forwarded to the domain, which owner-scopes the action;
|
||||||
// a registration that is unknown or not the caller's comes back 404 (ownership is not revealed).
|
// a registration that is unknown or not the caller's comes back 404 (ownership is not revealed).
|
||||||
|
|||||||
@@ -82,18 +82,6 @@ internal sealed class FakeDomainClient : IDomainClient
|
|||||||
return Task.FromResult(Result);
|
return Task.FromResult(Result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string? CurrentQueriedBsn { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>The current open registration the fake domain returns (null → the citizen has none in
|
|
||||||
/// flight, so the BFF replies 204). Tests set this to exercise resume.</summary>
|
|
||||||
public CurrentRegistration? Current { get; set; }
|
|
||||||
|
|
||||||
public Task<CurrentRegistration?> GetCurrentRegistrationAsync(string bsn, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
CurrentQueriedBsn = bsn;
|
|
||||||
return Task.FromResult(Current);
|
|
||||||
}
|
|
||||||
|
|
||||||
public (string RegistrationId, string Bsn)? Withdrawn { get; private set; }
|
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
|
/// <summary>Whether the fake domain reports the withdrawal as done (true → 204) or not-found/not-owned
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using Bff.Api;
|
|
||||||
|
|
||||||
namespace Bff.Tests;
|
namespace Bff.Tests;
|
||||||
|
|
||||||
@@ -169,52 +168,5 @@ public class SelfServiceEndpointTests
|
|||||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpRequestMessage Current(string? bearer)
|
|
||||||
{
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Get, "/self-service/registrations");
|
|
||||||
if (bearer is not null)
|
|
||||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer);
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Rejects_the_current_registration_lookup_without_a_token()
|
|
||||||
{
|
|
||||||
using var factory = new BffFactory();
|
|
||||||
|
|
||||||
var response = await factory.CreateClient().SendAsync(Current(bearer: null));
|
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Returns_no_content_when_the_caller_has_no_open_registration()
|
|
||||||
{
|
|
||||||
using var factory = new BffFactory();
|
|
||||||
factory.Domain.Current = null;
|
|
||||||
|
|
||||||
var response = await factory.CreateClient().SendAsync(Current(TestTokens.Valid("123456782")));
|
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
|
||||||
Assert.Equal("123456782", factory.Domain.CurrentQueriedBsn);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Returns_the_callers_current_registration_when_one_is_open()
|
|
||||||
{
|
|
||||||
using var factory = new BffFactory();
|
|
||||||
factory.Domain.Current = new CurrentRegistration("reg-77", "Ingediend");
|
|
||||||
|
|
||||||
var response = await factory.CreateClient().SendAsync(Current(TestTokens.Valid("123456782")));
|
|
||||||
|
|
||||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
||||||
Assert.Equal("123456782", factory.Domain.CurrentQueriedBsn);
|
|
||||||
var body = await response.Content.ReadFromJsonAsync<CurrentRegistrationDto>();
|
|
||||||
Assert.Equal("reg-77", body!.RegistrationId);
|
|
||||||
Assert.Equal("Ingediend", body.Status);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed record SubmitAcceptedDto(string RegistrationId, string Status);
|
private sealed record SubmitAcceptedDto(string RegistrationId, string Status);
|
||||||
|
|
||||||
private sealed record CurrentRegistrationDto(string RegistrationId, string Status);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,32 +28,6 @@
|
|||||||
"description": "Unauthorized"
|
"description": "Unauthorized"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"get": {
|
|
||||||
"tags": [
|
|
||||||
"Bff.Api"
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/CurrentRegistration"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"204": {
|
|
||||||
"description": "No Content"
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request"
|
|
||||||
},
|
|
||||||
"401": {
|
|
||||||
"description": "Unauthorized"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/self-service/registrations/{id}/withdraw": {
|
"/self-service/registrations/{id}/withdraw": {
|
||||||
@@ -231,21 +205,6 @@
|
|||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
"CurrentRegistration": {
|
|
||||||
"required": [
|
|
||||||
"registrationId",
|
|
||||||
"status"
|
|
||||||
],
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"registrationId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"DecideRequest": {
|
"DecideRequest": {
|
||||||
"required": [
|
"required": [
|
||||||
"besluit"
|
"besluit"
|
||||||
|
|||||||
@@ -141,21 +141,6 @@ app.MapGet("/behandel/werkbak", async (Werkbak werkbak, CancellationToken ct) =>
|
|||||||
Results.Ok(await werkbak.GetAsync(ct)));
|
Results.Ok(await werkbak.GetAsync(ct)));
|
||||||
|
|
||||||
// Read a registration. Its zaak URL appears once the worker has opened the zaak (eventually).
|
// Read a registration. Its zaak URL appears once the worker has opened the zaak (eventually).
|
||||||
// The citizen's current open registration, looked up by bsn — lets the self-service portal resume
|
|
||||||
// after a refresh (S-26). The BFF forwards the bsn from the DigiD token; the domain trusts its
|
|
||||||
// callers (§8.3). 404 when the citizen has none in flight.
|
|
||||||
app.MapGet("/registrations/current", async (string bsn, IRegistrationStore store, CancellationToken ct) =>
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(bsn))
|
|
||||||
return Results.BadRequest("A bsn is required.");
|
|
||||||
|
|
||||||
var registration = await store.FindOpenByBsnAsync(bsn, ct);
|
|
||||||
return registration is null
|
|
||||||
? Results.NotFound()
|
|
||||||
: Results.Ok(new RegistrationResponse(
|
|
||||||
registration.Id.ToString(), registration.Status.ToString(), registration.ZaakUrl?.ToString()));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.MapGet("/registrations/{id}", async (string id, IRegistrationStore store, CancellationToken ct) =>
|
app.MapGet("/registrations/{id}", async (string id, IRegistrationStore store, CancellationToken ct) =>
|
||||||
{
|
{
|
||||||
if (!Guid.TryParse(id, out var guid))
|
if (!Guid.TryParse(id, out var guid))
|
||||||
|
|||||||
@@ -102,11 +102,6 @@ public interface IRegistrationStore
|
|||||||
|
|
||||||
/// <summary>Load a registration by id, or <c>null</c> if none exists.</summary>
|
/// <summary>Load a registration by id, or <c>null</c> if none exists.</summary>
|
||||||
Task<Registration?> GetAsync(RegistrationId id, CancellationToken ct = default);
|
Task<Registration?> GetAsync(RegistrationId id, CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>The citizen's current <em>open</em> (non-terminal: INGEDIEND/IN_BEHANDELING)
|
|
||||||
/// registration, or <c>null</c> if they have none in flight. Lets the self-service portal resume
|
|
||||||
/// an existing registration after a refresh (S-26); terminal registrations are not resumed.</summary>
|
|
||||||
Task<Registration?> FindOpenByBsnAsync(string bsn, CancellationToken ct = default);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -22,8 +22,4 @@ public sealed class InMemoryRegistrationStore : IRegistrationStore
|
|||||||
|
|
||||||
public Task<Registration?> GetAsync(RegistrationId id, CancellationToken ct = default)
|
public Task<Registration?> GetAsync(RegistrationId id, CancellationToken ct = default)
|
||||||
=> Task.FromResult(_byId.GetValueOrDefault(id));
|
=> Task.FromResult(_byId.GetValueOrDefault(id));
|
||||||
|
|
||||||
public Task<Registration?> FindOpenByBsnAsync(string bsn, CancellationToken ct = default)
|
|
||||||
=> Task.FromResult(_byId.Values.FirstOrDefault(r =>
|
|
||||||
r.Bsn == bsn && r.Status is RegistrationStatus.Ingediend or RegistrationStatus.InBehandeling));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,6 @@ internal sealed class FakeRegistrationStore : IRegistrationStore
|
|||||||
public Task<Registration?> GetAsync(RegistrationId id, CancellationToken ct = default)
|
public Task<Registration?> GetAsync(RegistrationId id, CancellationToken ct = default)
|
||||||
=> Task.FromResult(_byId.GetValueOrDefault(id));
|
=> Task.FromResult(_byId.GetValueOrDefault(id));
|
||||||
|
|
||||||
public Task<Registration?> FindOpenByBsnAsync(string bsn, CancellationToken ct = default)
|
|
||||||
=> Task.FromResult(_byId.Values.FirstOrDefault(r =>
|
|
||||||
r.Bsn == bsn && r.Status is RegistrationStatus.Ingediend or RegistrationStatus.InBehandeling));
|
|
||||||
|
|
||||||
public void Seed(Registration registration) => _byId[registration.Id] = registration;
|
public void Seed(Registration registration) => _byId[registration.Id] = registration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,59 +41,4 @@ public class InMemoryRegistrationStoreTests
|
|||||||
|
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>(() => store.SaveAsync(null!));
|
await Assert.ThrowsAsync<ArgumentNullException>(() => store.SaveAsync(null!));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Finds_the_open_registration_for_a_bsn()
|
|
||||||
{
|
|
||||||
var store = new InMemoryRegistrationStore();
|
|
||||||
var open = Registration.Submit("123456782");
|
|
||||||
await store.SaveAsync(open);
|
|
||||||
|
|
||||||
var found = await store.FindOpenByBsnAsync("123456782");
|
|
||||||
|
|
||||||
Assert.NotNull(found);
|
|
||||||
Assert.Equal(open.Id, found.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task An_in_behandeling_registration_is_still_open()
|
|
||||||
{
|
|
||||||
var store = new InMemoryRegistrationStore();
|
|
||||||
var registration = Registration.Submit("123456782");
|
|
||||||
registration.TakeIntoBehandeling();
|
|
||||||
await store.SaveAsync(registration);
|
|
||||||
|
|
||||||
Assert.NotNull(await store.FindOpenByBsnAsync("123456782"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(nameof(Registration.Withdraw))]
|
|
||||||
[InlineData(nameof(Registration.Approve))]
|
|
||||||
[InlineData(nameof(Registration.Reject))]
|
|
||||||
[InlineData(nameof(Registration.Expire))]
|
|
||||||
public async Task A_terminal_registration_is_not_returned_as_open(string transition)
|
|
||||||
{
|
|
||||||
var store = new InMemoryRegistrationStore();
|
|
||||||
var registration = Registration.Submit("123456782");
|
|
||||||
registration.AttachZaak(new Uri("http://openzaak/zaken/api/v1/zaken/abc")); // Approve requires an opened zaak
|
|
||||||
switch (transition)
|
|
||||||
{
|
|
||||||
case nameof(Registration.Withdraw): registration.Withdraw(); break;
|
|
||||||
case nameof(Registration.Approve): registration.Approve(); break;
|
|
||||||
case nameof(Registration.Reject): registration.Reject(); break;
|
|
||||||
case nameof(Registration.Expire): registration.Expire(); break;
|
|
||||||
}
|
|
||||||
await store.SaveAsync(registration);
|
|
||||||
|
|
||||||
Assert.Null(await store.FindOpenByBsnAsync("123456782"));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Does_not_return_another_bsns_registration_or_an_unknown_bsn()
|
|
||||||
{
|
|
||||||
var store = new InMemoryRegistrationStore();
|
|
||||||
await store.SaveAsync(Registration.Submit("111111110"));
|
|
||||||
|
|
||||||
Assert.Null(await store.FindOpenByBsnAsync("123456782"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,9 @@ public sealed class EenZaakOpenenSteps
|
|||||||
Bronorganisatie = values["bronorganisatie"],
|
Bronorganisatie = values["bronorganisatie"],
|
||||||
VerantwoordelijkeOrganisatie = values["verantwoordelijkeOrganisatie"],
|
VerantwoordelijkeOrganisatie = values["verantwoordelijkeOrganisatie"],
|
||||||
Vertrouwelijkheidaanduiding = values["vertrouwelijkheidaanduiding"],
|
Vertrouwelijkheidaanduiding = values["vertrouwelijkheidaanduiding"],
|
||||||
ZaaktypeIdentificatie = "BIG-REGISTRATIE",
|
ZaaktypeUrl = new Uri(values["zaaktype"]),
|
||||||
InformatieobjecttypeOmschrijving = "Diploma",
|
InformatieobjecttypeUrl = new Uri("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip"),
|
||||||
};
|
};
|
||||||
// The ACL resolves the zaaktype by identificatie (S-27); the scenario's zaaktype URL is what
|
|
||||||
// the catalogus resolves it to, so the created zaak still carries that URL.
|
|
||||||
_gateway.ResolvedZaaktypeUrl = new Uri(values["zaaktype"]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Given("today is \"(.*)\"")]
|
[Given("today is \"(.*)\"")]
|
||||||
@@ -44,7 +41,7 @@ public sealed class EenZaakOpenenSteps
|
|||||||
[When("the domain asks the ACL to open a zaak")]
|
[When("the domain asks the ACL to open a zaak")]
|
||||||
public async Task WhenTheDomainAsksTheAclToOpenAZaak()
|
public async Task WhenTheDomainAsksTheAclToOpenAZaak()
|
||||||
{
|
{
|
||||||
var service = new AclService(_gateway, _defaults!, new CachedZaaktypeCatalog(_gateway, _defaults!), new FixedClock(_today));
|
var service = new AclService(_gateway, _defaults!, new FixedClock(_today));
|
||||||
_returnedUrl = await service.OpenZaakAsync(_registration!);
|
_returnedUrl = await service.OpenZaakAsync(_registration!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,6 @@ public sealed class CapturingDomainClient : IDomainClient
|
|||||||
return Task.FromResult(new SubmitAccepted("reg-acc-1", "Ingediend"));
|
return Task.FromResult(new SubmitAccepted("reg-acc-1", "Ingediend"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<CurrentRegistration?> GetCurrentRegistrationAsync(string bsn, CancellationToken ct = default)
|
|
||||||
=> Task.FromResult<CurrentRegistration?>(null);
|
|
||||||
|
|
||||||
public Task<bool> WithdrawRegistrationAsync(string registrationId, string bsn, CancellationToken ct = default)
|
public Task<bool> WithdrawRegistrationAsync(string registrationId, string bsn, CancellationToken ct = default)
|
||||||
=> Task.FromResult(true);
|
=> Task.FromResult(true);
|
||||||
|
|
||||||
|
|||||||
@@ -217,8 +217,4 @@ public sealed class InMemoryRegistrationStore : IRegistrationStore
|
|||||||
|
|
||||||
public Task<Registration?> GetAsync(RegistrationId id, CancellationToken ct = default)
|
public Task<Registration?> GetAsync(RegistrationId id, CancellationToken ct = default)
|
||||||
=> Task.FromResult(_byId.GetValueOrDefault(id));
|
=> Task.FromResult(_byId.GetValueOrDefault(id));
|
||||||
|
|
||||||
public Task<Registration?> FindOpenByBsnAsync(string bsn, CancellationToken ct = default)
|
|
||||||
=> Task.FromResult(_byId.Values.FirstOrDefault(r =>
|
|
||||||
r.Bsn == bsn && r.Status is RegistrationStatus.Ingediend or RegistrationStatus.InBehandeling));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,6 @@ public sealed class InMemoryZaakGateway : IZaakGateway
|
|||||||
public (Uri Zaak, Uri Zaaktype, DateOnly Datum)? Approved { get; private set; }
|
public (Uri Zaak, Uri Zaaktype, DateOnly Datum)? Approved { get; private set; }
|
||||||
public (Uri Zaak, Uri Zaaktype, DateOnly Datum)? Cancelled { get; private set; }
|
public (Uri Zaak, Uri Zaaktype, DateOnly Datum)? Cancelled { get; private set; }
|
||||||
|
|
||||||
// The URLs the catalogus resolves the configured identificatie/omschrijving to (S-27); settable so
|
|
||||||
// a scenario can pin the zaaktype the ACL should default-fill.
|
|
||||||
public Uri ResolvedZaaktypeUrl { get; set; } = new("http://openzaak/catalogi/api/v1/zaaktypen/big");
|
|
||||||
public Uri ResolvedInformatieobjecttypeUrl { get; set; } = new("http://openzaak/catalogi/api/v1/informatieobjecttypen/dip");
|
|
||||||
|
|
||||||
public Task<Uri> OpenZaakAsync(ZaakRequest request, CancellationToken ct = default)
|
public Task<Uri> OpenZaakAsync(ZaakRequest request, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
Captured = request;
|
Captured = request;
|
||||||
@@ -42,10 +37,4 @@ public sealed class InMemoryZaakGateway : IZaakGateway
|
|||||||
|
|
||||||
public Task<Uri> StoreDocumentAsync(DocumentRequest request, CancellationToken ct = default)
|
public Task<Uri> StoreDocumentAsync(DocumentRequest request, CancellationToken ct = default)
|
||||||
=> Task.FromResult(new Uri("http://openzaak/documenten/api/v1/enkelvoudiginformatieobjecten/acc-doc"));
|
=> Task.FromResult(new Uri("http://openzaak/documenten/api/v1/enkelvoudiginformatieobjecten/acc-doc"));
|
||||||
|
|
||||||
public Task<Uri> ResolveZaaktypeUrlAsync(string identificatie, CancellationToken ct = default)
|
|
||||||
=> Task.FromResult(ResolvedZaaktypeUrl);
|
|
||||||
|
|
||||||
public Task<Uri> ResolveInformatieobjecttypeUrlAsync(string omschrijving, CancellationToken ct = default)
|
|
||||||
=> Task.FromResult(ResolvedInformatieobjecttypeUrl);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,8 @@ test('DigiD submit → public INGEDIEND → documenten → behandelaar goedkeurt
|
|||||||
// Visiting the guarded page redirects to the Keycloak (mock DigiD) login.
|
// Visiting the guarded page redirects to the Keycloak (mock DigiD) login.
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
// Keycloak's default login form (stable ids across themes). Its own DigiD user: the verify-* API
|
// Keycloak's default login form (stable ids across themes).
|
||||||
// checks submit as jan-burger (bsn 123456782) before the e2e runs on the shared stack, and
|
await page.locator('#username').fill('jan-burger');
|
||||||
// resume-on-load (S-26) would otherwise restore one of those on login — so each self-service spec
|
|
||||||
// uses a dedicated citizen no other actor touches.
|
|
||||||
await page.locator('#username').fill('emma-burger');
|
|
||||||
await page.locator('#password').fill('test123');
|
await page.locator('#password').fill('test123');
|
||||||
await page.locator('#kc-login').click();
|
await page.locator('#kc-login').click();
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
|
||||||
|
|
||||||
// S-26: a zorgprofessional submits, then reloads the self-service portal. On load the portal asks the
|
|
||||||
// BFF for the caller's current open registration (owner-scoped by the DigiD token's bsn) and restores
|
|
||||||
// the submitted view — so a refresh no longer strands the in-flight registration and its actions.
|
|
||||||
test('DigiD submit → reload → self-service restores the existing registration', async ({ page }) => {
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
// Its own DigiD user (like every self-service spec): on the shared verify stack, resume-on-load
|
|
||||||
// (S-26) restores any open registration for the bsn, so each spec uses a dedicated citizen that no
|
|
||||||
// other spec or verify-* check touches. This one in particular leaves an open registration.
|
|
||||||
await page.locator('#username').fill('sanne-burger');
|
|
||||||
await page.locator('#password').fill('test123');
|
|
||||||
await page.locator('#kc-login').click();
|
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: /Zelfservice/i })).toBeVisible();
|
|
||||||
await page.getByRole('button', { name: /indienen/i }).click();
|
|
||||||
|
|
||||||
const confirmation = page.getByText(/ontvangen/i);
|
|
||||||
await expect(confirmation).toBeVisible();
|
|
||||||
const reference = (await confirmation.textContent())?.match(/Referentie:\s*([0-9a-fA-F-]+)/)?.[1];
|
|
||||||
expect(reference, 'the confirmation shows a registration reference').toBeTruthy();
|
|
||||||
|
|
||||||
// Reload: the component's in-memory submitted state is gone, but the DigiD session persists and the
|
|
||||||
// portal resumes from the BFF instead of dropping back to the blank submit form.
|
|
||||||
await page.reload();
|
|
||||||
|
|
||||||
await expect(page.getByText(/ontvangen/i)).toBeVisible();
|
|
||||||
// The same reference the citizen saw before the reload is restored...
|
|
||||||
await expect(page.getByText(new RegExp(reference!))).toBeVisible();
|
|
||||||
// ...and its actions are reachable again (e.g. "trek aanvraag in").
|
|
||||||
await expect(page.getByRole('button', { name: /trek aanvraag in/i })).toBeVisible();
|
|
||||||
});
|
|
||||||
@@ -8,9 +8,7 @@ test('DigiD submit → trek aanvraag in → self-service confirms ingetrokken',
|
|||||||
// Visiting the guarded page redirects to the Keycloak (mock DigiD) login.
|
// Visiting the guarded page redirects to the Keycloak (mock DigiD) login.
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
// Its own DigiD user — isolated from the verify-* checks (jan-burger/123456782) so resume-on-load
|
await page.locator('#username').fill('jan-burger');
|
||||||
// (S-26) can't restore someone else's registration on the shared stack.
|
|
||||||
await page.locator('#username').fill('lars-burger');
|
|
||||||
await page.locator('#password').fill('test123');
|
await page.locator('#password').fill('test123');
|
||||||
await page.locator('#kc-login').click();
|
await page.locator('#kc-login').click();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user