## What & why S-15b, second of the S-15 (#16) split, on top of S-15a (#133). A beheerder edits the ACL's ZGW **default-fill** values from the beheer portal, and the next zaak is stamped with the new values — no restart. Closes #131 ### The vertical portal → BFF `GET/PUT /beheer/default-fill` (medewerker realm + `beheerder` role) → ACL `GET/PUT /default-fill` → a runtime-mutable in-memory store the ACL reads **per zaak**. - **ACL**: `IDefaultFillStore` / `InMemoryDefaultFillStore` (thread-safe, seeded from `Acl:Defaults`); `AclService` reads `fill.Current` per zaak (not cached at construction); `GET`/`PUT /default-fill` with required-field validation. - **BFF**: `IAclClient` gains `GetDefaultFillAsync`/`UpdateDefaultFillAsync`; `GET`/`PUT /beheer/default-fill` behind the `beheerder` policy. OpenAPI + generated client regenerated. - **Frontend**: a *Default-fill* editor page in the beheer app (load → edit → save, with saved/failure states) + nav between Catalogus and Default-fill. ### Scope decision → ADR-0026 Only the **three ZGW fill fields** (bronorganisatie, verantwoordelijke organisatie, vertrouwelijkheidaanduiding) are editable. The S-27 catalog-resolution keys stay **static config** — editing them would desync the zaaktype-URL cache (ADR-0021), and they're catalogus wiring, not "default fill". The store is **in-memory** (seeded from config): an edit reverts on restart. That's the reference-app-appropriate ceiling (no DB added to the stateless ACL); upgrade path documented. Recorded in **ADR-0026**. ## Verified locally lint (`dotnet format`) ✓ · .NET unit — acl 60 / bff 45 / domain 152 / event-subscriber 19 / acceptance 17 ✓ · frontend lint+test (8 projects) ✓ · beheer build ✓. Clean full-solution build (caught + fixed the acceptance `AclService` ctor drift). TDD red→green per layer (ACL store, ACL endpoints, BFF, frontend). ## Definition of Done - [x] Failing test committed before each implementation (red→green per layer). - [x] Conventional Commits referencing #131. - [ ] CI green — see note below. - [x] Docs: ADR-0026 + S-15b demo note. - [x] Demo note in `docs/demo-script.md`. ## Note on CI The bulk validates in the fast jobs (lint/build/unit/frontend/mutation). The **verify-stack e2e** (incl. the new `default-fill.spec.ts`) can't go green until the pre-existing **verify-stack bring-up failure on the 1.27/2.0.0 runner** is resolved (that fails on plain `main` too — unrelated to this PR). Additive change; no existing e2e touched. 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #138
This commit was merged in pull request #138.
This commit is contained in:
@@ -1 +1,5 @@
|
||||
<nav aria-label="Beheer" class="utrecht-theme">
|
||||
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Catalogus</a>
|
||||
<a routerLink="/default-fill" routerLinkActive="active">Default-fill</a>
|
||||
</nav>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Route } from '@angular/router';
|
||||
import { authenticatedGuard } from 'auth';
|
||||
import { CatalogusPage } from './catalogus/catalogus-page';
|
||||
import { DefaultFillPage } from './default-fill/default-fill-page';
|
||||
|
||||
export const appRoutes: Route[] = [
|
||||
{ path: '', component: CatalogusPage, canActivate: [authenticatedGuard] },
|
||||
{ path: 'default-fill', component: DefaultFillPage, canActivate: [authenticatedGuard] },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<main utrecht-document class="utrecht-theme">
|
||||
<utrecht-article>
|
||||
<utrecht-heading-1>Default-fill</utrecht-heading-1>
|
||||
<p utrecht-paragraph>
|
||||
De ZGW-standaardwaarden die de ACL op elke nieuwe zaak invult (ADR-0003). Een wijziging geldt
|
||||
voor de eerstvolgende zaak.
|
||||
</p>
|
||||
|
||||
@if (loading()) {
|
||||
<p utrecht-paragraph role="status">Bezig met laden…</p>
|
||||
} @else if (loaded()) {
|
||||
<form (submit)="save(); $event.preventDefault()">
|
||||
<p>
|
||||
<label for="bronorganisatie">Bronorganisatie</label><br />
|
||||
<input
|
||||
id="bronorganisatie"
|
||||
name="bronorganisatie"
|
||||
[value]="bronorganisatie()"
|
||||
(input)="bronorganisatie.set($any($event.target).value)"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<label for="verantwoordelijkeOrganisatie">Verantwoordelijke organisatie</label><br />
|
||||
<input
|
||||
id="verantwoordelijkeOrganisatie"
|
||||
name="verantwoordelijkeOrganisatie"
|
||||
[value]="verantwoordelijkeOrganisatie()"
|
||||
(input)="verantwoordelijkeOrganisatie.set($any($event.target).value)"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<label for="vertrouwelijkheidaanduiding">Vertrouwelijkheidaanduiding</label><br />
|
||||
<input
|
||||
id="vertrouwelijkheidaanduiding"
|
||||
name="vertrouwelijkheidaanduiding"
|
||||
[value]="vertrouwelijkheidaanduiding()"
|
||||
(input)="vertrouwelijkheidaanduiding.set($any($event.target).value)"
|
||||
/>
|
||||
</p>
|
||||
<button utrecht-button appearance="primary-action-button" type="submit" [disabled]="saving()">
|
||||
Opslaan
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@if (saved()) {
|
||||
<p utrecht-paragraph role="status">De standaardwaarden zijn opgeslagen.</p>
|
||||
}
|
||||
@if (failed()) {
|
||||
<p utrecht-paragraph role="alert">
|
||||
Opslaan is niet gelukt. Controleer of je als beheerder bent ingelogd en probeer het opnieuw.
|
||||
</p>
|
||||
}
|
||||
} @else if (failed()) {
|
||||
<p utrecht-paragraph role="alert">
|
||||
Kon de standaardwaarden niet laden. Controleer of je als beheerder bent ingelogd en probeer
|
||||
het opnieuw.
|
||||
</p>
|
||||
}
|
||||
</utrecht-article>
|
||||
</main>
|
||||
@@ -0,0 +1,90 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { fireEvent, render, screen } from '@testing-library/angular';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { BeheerDefaultFill, BffApiV1Service } from 'api-client';
|
||||
import { AuthService } from 'auth';
|
||||
import { axe } from 'vitest-axe';
|
||||
import { DefaultFillPage } from './default-fill-page';
|
||||
|
||||
const current: BeheerDefaultFill = {
|
||||
bronorganisatie: '517439943',
|
||||
verantwoordelijkeOrganisatie: '517439943',
|
||||
vertrouwelijkheidaanduiding: 'openbaar',
|
||||
};
|
||||
|
||||
class FakeAuth extends AuthService {
|
||||
readonly isAuthenticated = signal(true);
|
||||
readonly bsn = signal<string | undefined>(undefined);
|
||||
override readonly roles = signal<readonly string[]>(['beheerder']);
|
||||
login(): void {
|
||||
/* not exercised */
|
||||
}
|
||||
logout(): void {
|
||||
/* not exercised */
|
||||
}
|
||||
}
|
||||
|
||||
function setup(
|
||||
overrides: {
|
||||
getBeheerDefaultFill?: ReturnType<typeof vi.fn>;
|
||||
putBeheerDefaultFill?: ReturnType<typeof vi.fn>;
|
||||
} = {},
|
||||
) {
|
||||
const getBeheerDefaultFill = overrides.getBeheerDefaultFill ?? vi.fn().mockReturnValue(of(current));
|
||||
const putBeheerDefaultFill = overrides.putBeheerDefaultFill ?? vi.fn().mockReturnValue(of(undefined));
|
||||
return {
|
||||
getBeheerDefaultFill,
|
||||
putBeheerDefaultFill,
|
||||
providers: [
|
||||
{ provide: BffApiV1Service, useValue: { getBeheerDefaultFill, putBeheerDefaultFill } },
|
||||
{ provide: AuthService, useClass: FakeAuth },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe('DefaultFillPage', () => {
|
||||
it('loads the current default-fill into the form on open', async () => {
|
||||
const { getBeheerDefaultFill, providers } = setup();
|
||||
await render(DefaultFillPage, { providers });
|
||||
|
||||
expect(getBeheerDefaultFill).toHaveBeenCalled();
|
||||
const bron = (await screen.findByLabelText('Bronorganisatie')) as HTMLInputElement;
|
||||
expect(bron.value).toBe('517439943');
|
||||
});
|
||||
|
||||
it('saves the edited values via the BFF', async () => {
|
||||
const { putBeheerDefaultFill, providers } = setup();
|
||||
await render(DefaultFillPage, { providers });
|
||||
|
||||
const bron = (await screen.findByLabelText('Bronorganisatie')) as HTMLInputElement;
|
||||
fireEvent.input(bron, { target: { value: '999999999' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /opslaan/i }));
|
||||
|
||||
expect(putBeheerDefaultFill).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ bronorganisatie: '999999999', vertrouwelijkheidaanduiding: 'openbaar' }),
|
||||
);
|
||||
expect(await screen.findByText(/standaardwaarden zijn opgeslagen/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('surfaces a save failure instead of swallowing it', async () => {
|
||||
const { providers } = setup({
|
||||
putBeheerDefaultFill: vi.fn().mockReturnValue(throwError(() => new Error('403'))),
|
||||
});
|
||||
await render(DefaultFillPage, { providers });
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /opslaan/i }));
|
||||
|
||||
expect(await screen.findByText(/opslaan is niet gelukt/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('has no WCAG 2.1 AA violations', async () => {
|
||||
document.documentElement.lang = 'nl';
|
||||
const { container } = await render(DefaultFillPage, { providers: setup().providers });
|
||||
|
||||
const results = await axe(container, {
|
||||
runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'] },
|
||||
});
|
||||
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { BeheerDefaultFill, BffApiV1Service } from 'api-client';
|
||||
import { UtrechtComponentsModule } from 'ui';
|
||||
|
||||
/**
|
||||
* The beheer default-fill editor (S-15b): a beheerder reads and edits the ZGW default-fill values the
|
||||
* ACL stamps on every zaak (ADR-0003). Load and save go through the BFF (`/beheer/default-fill`),
|
||||
* which proxies the ACL (ADR-0025). A save takes effect on the next zaak (the ACL reads it per zaak).
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-default-fill-page',
|
||||
imports: [UtrechtComponentsModule],
|
||||
templateUrl: './default-fill-page.html',
|
||||
})
|
||||
export class DefaultFillPage {
|
||||
private readonly bff = inject(BffApiV1Service);
|
||||
|
||||
protected readonly bronorganisatie = signal('');
|
||||
protected readonly verantwoordelijkeOrganisatie = signal('');
|
||||
protected readonly vertrouwelijkheidaanduiding = signal('');
|
||||
protected readonly loading = signal(false);
|
||||
protected readonly loaded = signal(false);
|
||||
protected readonly saving = signal(false);
|
||||
protected readonly failed = signal(false);
|
||||
protected readonly saved = signal(false);
|
||||
|
||||
constructor() {
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading.set(true);
|
||||
this.failed.set(false);
|
||||
this.saved.set(false);
|
||||
this.bff.getBeheerDefaultFill().subscribe({
|
||||
next: (d: BeheerDefaultFill) => {
|
||||
this.bronorganisatie.set(d.bronorganisatie);
|
||||
this.verantwoordelijkeOrganisatie.set(d.verantwoordelijkeOrganisatie);
|
||||
this.vertrouwelijkheidaanduiding.set(d.vertrouwelijkheidaanduiding);
|
||||
this.loading.set(false);
|
||||
this.loaded.set(true);
|
||||
},
|
||||
error: () => {
|
||||
this.loading.set(false);
|
||||
this.loaded.set(true);
|
||||
this.failed.set(true);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.saving.set(true);
|
||||
this.failed.set(false);
|
||||
this.saved.set(false);
|
||||
this.bff
|
||||
.putBeheerDefaultFill({
|
||||
bronorganisatie: this.bronorganisatie(),
|
||||
verantwoordelijkeOrganisatie: this.verantwoordelijkeOrganisatie(),
|
||||
vertrouwelijkheidaanduiding: this.vertrouwelijkheidaanduiding(),
|
||||
})
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.saving.set(false);
|
||||
this.saved.set(true);
|
||||
},
|
||||
error: () => {
|
||||
this.saving.set(false);
|
||||
this.failed.set(true);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
# ADR-0026: Runtime-mutable ACL default-fill (in-memory store, seeded from config)
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-07-24
|
||||
- **Deciders:** Respellion engineering
|
||||
- **Slice:** S-15b (#131), second of the S-15 (#16) split
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0003 made the ACL *default-fill* the ZGW-mandatory fields it stamps on every
|
||||
zaak, supplied as static configuration (`Acl:Defaults`, read once at startup as an
|
||||
immutable singleton). S-15b lets a beheerder **edit** those values from the portal
|
||||
and have the next zaak reflect them — so the defaults must become mutable at runtime.
|
||||
|
||||
Two questions: **what** is editable, and **where** the mutable state lives.
|
||||
|
||||
## Decision
|
||||
|
||||
**Make the three ZGW default-fill fields a runtime-mutable, in-memory store
|
||||
(`IDefaultFillStore`), seeded from `Acl:Defaults` at startup. The ACL reads it per
|
||||
zaak; the beheer `PUT /default-fill` replaces it.**
|
||||
|
||||
### Only the three ZGW fill fields are editable
|
||||
|
||||
`Acl:Defaults` also carries the S-27 catalog-resolution keys (`ZaaktypeIdentificatie`,
|
||||
`InformatieobjecttypeOmschrijving`). Those feed the resolved-URL cache
|
||||
(`CachedZaaktypeCatalog`, ADR-0021); editing them at runtime would leave a stale cache
|
||||
and is catalogus *wiring*, not "default fill". So they **stay static config** and are
|
||||
out of scope for the CRUD. The editable set is exactly `Bronorganisatie`,
|
||||
`VerantwoordelijkeOrganisatie`, `Vertrouwelijkheidaanduiding` (`DefaultFillSettings`).
|
||||
|
||||
### In-memory, not persisted
|
||||
|
||||
The store is a thread-safe in-memory singleton. **An edit is lost on restart**, when it
|
||||
reverts to the configured env. That is acceptable for this reference app: the slice
|
||||
demonstrates the *pattern* (beheer edits config that the ACL honours), not durable
|
||||
config management. The ACL stays stateless — no DB, no EF, no migration, no extra
|
||||
compose service.
|
||||
|
||||
- ponytail ceiling: no persistence, no audit trail, no optimistic concurrency.
|
||||
- Upgrade path: back `IDefaultFillStore` with a DB (or an Objecten record) if durable,
|
||||
audited, multi-instance config is needed — the port stays the same.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive**
|
||||
|
||||
- Demoable end to end (edit in portal → next zaak reflects it) with minimal moving parts.
|
||||
- The read path is per-zaak, so no restart and no cache concerns for the ZGW fields.
|
||||
|
||||
**Negative / costs**
|
||||
|
||||
- Edits don't survive a restart and aren't shared across replicas (single-instance
|
||||
assumption). Documented ceiling above.
|
||||
- Two sources of default config now (static keys on `AclDefaults`, mutable fields in the
|
||||
store) — a deliberate split by editability.
|
||||
|
||||
## Coupling rules touched (CLAUDE.md §8)
|
||||
|
||||
None new. The BFF→ACL edge already exists (ADR-0025); this adds a read/write pair on it.
|
||||
The ACL remains the owner of the ZGW-facing config.
|
||||
@@ -5,6 +5,28 @@ copy-pasteable walkthrough against a local `make up` stack.
|
||||
|
||||
---
|
||||
|
||||
## S-15b — Beheer-portal: default-fill configuration editor (#131, ADR-0026)
|
||||
|
||||
**Outcome:** a beheerder edits the ACL's ZGW **default-fill** values (bronorganisatie,
|
||||
verantwoordelijke organisatie, vertrouwelijkheidaanduiding) from the beheer portal, and the next zaak
|
||||
is stamped with the new values — no restart. Path: portal → BFF `GET/PUT /beheer/default-fill`
|
||||
(beheerder role) → ACL `GET/PUT /default-fill` → a runtime-mutable in-memory store the ACL reads per
|
||||
zaak (ADR-0026). The S-27 catalog-resolution keys stay static config (editing them would desync the
|
||||
zaaktype cache). Store is in-memory: an edit reverts to the configured env on restart.
|
||||
|
||||
```bash
|
||||
make up
|
||||
# 1. Log in as bram-beheerder / test123 → "Default-fill" tab → change a value → Opslaan.
|
||||
open http://localhost:8143/default-fill
|
||||
#
|
||||
# 2. Automated: the ACL uses the current default-fill per zaak (unit) and the endpoints are behind the
|
||||
# beheerder role (BFF unit):
|
||||
# Acl.Tests → AclServiceTests.Opening_a_zaak_reflects_a_default_fill_update
|
||||
# Bff.Tests → BeheerDefaultFillEndpointTests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## S-15a — Beheer-portal: read-only catalogus viewer (#130, ADR-0025)
|
||||
|
||||
**Outcome:** a new **beheer** portal (medewerker realm, like behandel) shows the ZTC catalogus —
|
||||
|
||||
@@ -24,6 +24,12 @@ import {
|
||||
Observable
|
||||
} from 'rxjs';
|
||||
|
||||
export interface BeheerDefaultFill {
|
||||
bronorganisatie: string;
|
||||
verantwoordelijkeOrganisatie: string;
|
||||
vertrouwelijkheidaanduiding: string;
|
||||
}
|
||||
|
||||
export interface BeheerZaaktype {
|
||||
identificatie: string;
|
||||
omschrijving: string;
|
||||
@@ -446,4 +452,69 @@ export class BffApiV1Service {
|
||||
);
|
||||
}
|
||||
|
||||
getBeheerDefaultFill<TData = BeheerDefaultFill>( options?: HttpClientBodyOptions): Observable<TData>;
|
||||
getBeheerDefaultFill<TData = BeheerDefaultFill>( options?: HttpClientEventOptions): Observable<HttpEvent<TData>>;
|
||||
getBeheerDefaultFill<TData = BeheerDefaultFill>( options?: HttpClientResponseOptions): Observable<AngularHttpResponse<TData>>;
|
||||
getBeheerDefaultFill<TData = BeheerDefaultFill>(
|
||||
options?: HttpClientObserveOptions): Observable<TData | HttpEvent<TData> | AngularHttpResponse<TData>> {
|
||||
if (options?.observe === 'events') {
|
||||
return this.http.get<TData>(
|
||||
`/beheer/default-fill`,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'events',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.observe === 'response') {
|
||||
return this.http.get<TData>(
|
||||
`/beheer/default-fill`,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'response',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return this.http.get<TData>(
|
||||
`/beheer/default-fill`,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'body',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
putBeheerDefaultFill<TData = void>(beheerDefaultFill: BeheerDefaultFill, options?: HttpClientBodyOptions): Observable<TData>;
|
||||
putBeheerDefaultFill<TData = void>(beheerDefaultFill: BeheerDefaultFill, options?: HttpClientEventOptions): Observable<HttpEvent<TData>>;
|
||||
putBeheerDefaultFill<TData = void>(beheerDefaultFill: BeheerDefaultFill, options?: HttpClientResponseOptions): Observable<AngularHttpResponse<TData>>;
|
||||
putBeheerDefaultFill<TData = void>(
|
||||
beheerDefaultFill: BeheerDefaultFill, options?: HttpClientObserveOptions): Observable<TData | HttpEvent<TData> | AngularHttpResponse<TData>> {
|
||||
if (options?.observe === 'events') {
|
||||
return this.http.put<TData>(
|
||||
`/beheer/default-fill`,
|
||||
beheerDefaultFill,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'events',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.observe === 'response') {
|
||||
return this.http.put<TData>(
|
||||
`/beheer/default-fill`,
|
||||
beheerDefaultFill,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'response',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return this.http.put<TData>(
|
||||
`/beheer/default-fill`,
|
||||
beheerDefaultFill,{
|
||||
...(options as Omit<NonNullable<typeof options>, 'observe'>),
|
||||
observe: 'body',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -33,6 +33,15 @@ builder.Services.AddSingleton(sp => sp.GetRequiredService<IConfiguration>()
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<IConfiguration>()
|
||||
.GetSection("Acl:OpenZaak").Get<OpenZaakOptions>()
|
||||
?? throw new InvalidOperationException("Missing configuration section 'Acl:OpenZaak'"));
|
||||
// The default-fill values are held in a runtime-mutable store (S-15b, ADR-0026), seeded from the
|
||||
// configured Acl:Defaults. The beheer portal edits it; the worker reads it per zaak. The S-27
|
||||
// resolution keys stay on AclDefaults (static) — see DefaultFillSettings.
|
||||
builder.Services.AddSingleton<IDefaultFillStore>(sp =>
|
||||
{
|
||||
var d = sp.GetRequiredService<AclDefaults>();
|
||||
return new InMemoryDefaultFillStore(
|
||||
new DefaultFillSettings(d.Bronorganisatie, d.VerantwoordelijkeOrganisatie, d.Vertrouwelijkheidaanduiding));
|
||||
});
|
||||
builder.Services.AddHttpClient<IZaakGateway, OpenZaakGateway>();
|
||||
// Singleton so the resolved zaaktype/informatieobjecttype URLs are cached across requests (S-27).
|
||||
builder.Services.AddSingleton<IZaaktypeCatalog, CachedZaaktypeCatalog>();
|
||||
@@ -91,6 +100,22 @@ app.MapPost("/documenten", async (StoreDocumentRequest body, AclService acl, Can
|
||||
app.MapGet("/catalogi/zaaktypen", async (AclService acl, CancellationToken ct) =>
|
||||
Results.Ok(await acl.ListZaaktypenAsync(ct)));
|
||||
|
||||
// Read the current default-fill settings (beheer config viewer, S-15b).
|
||||
app.MapGet("/default-fill", (AclService acl) => Results.Ok(acl.GetDefaultFill()));
|
||||
|
||||
// Update the default-fill settings from the beheer portal (S-15b). Behind beheerder authorization at
|
||||
// the BFF; the ACL validates the values are present (the three ZGW-mandatory fields).
|
||||
app.MapPut("/default-fill", (DefaultFillSettings body, AclService acl) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(body.Bronorganisatie) ||
|
||||
string.IsNullOrWhiteSpace(body.VerantwoordelijkeOrganisatie) ||
|
||||
string.IsNullOrWhiteSpace(body.Vertrouwelijkheidaanduiding))
|
||||
return Results.BadRequest(new { error = "bronorganisatie, verantwoordelijkeOrganisatie and vertrouwelijkheidaanduiding are all required." });
|
||||
|
||||
acl.UpdateDefaultFill(body);
|
||||
return Results.NoContent();
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
public sealed record OpenZaakRequest(string Bsn, string Reference);
|
||||
|
||||
@@ -2,12 +2,15 @@ namespace Acl.Application;
|
||||
|
||||
/// <summary>The ACL's single operation: open a zaak from a domain payload,
|
||||
/// 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, IDefaultFillStore fill, IZaaktypeCatalog catalog, IClock clock)
|
||||
{
|
||||
public async Task<Uri> OpenZaakAsync(DomainRegistration registration, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(registration);
|
||||
|
||||
// Read the current default-fill per zaak (not at construction), so a beheerder edit (S-15b)
|
||||
// takes effect on the next zaak without a restart.
|
||||
var defaults = fill.Current;
|
||||
var request = new ZaakRequest(
|
||||
defaults.Bronorganisatie,
|
||||
defaults.VerantwoordelijkeOrganisatie,
|
||||
@@ -47,6 +50,17 @@ public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, IZaak
|
||||
public Task<IReadOnlyList<ZaaktypeSummary>> ListZaaktypenAsync(CancellationToken ct = default) =>
|
||||
gateway.ListZaaktypenAsync(ct);
|
||||
|
||||
/// <summary>The current default-fill settings, for the beheer config viewer (S-15b).</summary>
|
||||
public DefaultFillSettings GetDefaultFill() => fill.Current;
|
||||
|
||||
/// <summary>Replace the default-fill settings from the beheer portal (S-15b). Takes effect on the
|
||||
/// next zaak (the fill is read per zaak, not cached).</summary>
|
||||
public void UpdateDefaultFill(DefaultFillSettings settings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
fill.Update(settings);
|
||||
}
|
||||
|
||||
/// <summary>The zaak's reference (its ZGW identificatie), for the read projection (#78).</summary>
|
||||
public Task<string> GetZaakReferenceAsync(Uri zaakUrl, CancellationToken ct = default)
|
||||
{
|
||||
@@ -68,6 +82,7 @@ public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, IZaak
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(contentType);
|
||||
|
||||
var defaults = fill.Current;
|
||||
var request = new DocumentRequest(
|
||||
defaults.Bronorganisatie,
|
||||
await catalog.GetInformatieobjecttypeUrlAsync(ct),
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Acl.Application;
|
||||
|
||||
/// <summary>The ZGW default-fill values a beheerder can edit at runtime (S-15b) — the mandatory fields
|
||||
/// the ACL stamps on every zaak (ADR-0003). The S-27 catalog-resolution keys (zaaktype identificatie,
|
||||
/// informatieobjecttype omschrijving) stay static config: editing them would desync the resolved-URL
|
||||
/// cache, and they're catalogus wiring rather than "default fill".</summary>
|
||||
public sealed record DefaultFillSettings(
|
||||
string Bronorganisatie,
|
||||
string VerantwoordelijkeOrganisatie,
|
||||
string Vertrouwelijkheidaanduiding);
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Acl.Application;
|
||||
|
||||
/// <summary>Holds the ACL's current default-fill values, editable at runtime through the beheer portal
|
||||
/// (S-15b). Seeded from config at startup.
|
||||
///
|
||||
/// ponytail: in-memory only — an edit is lost on restart, when it reverts to the configured env
|
||||
/// (ADR-0026). Adequate for the reference demo; back it with a DB if durable, audited config is needed.
|
||||
/// </summary>
|
||||
public interface IDefaultFillStore
|
||||
{
|
||||
DefaultFillSettings Current { get; }
|
||||
|
||||
void Update(DefaultFillSettings settings);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Acl.Application;
|
||||
|
||||
/// <summary>In-memory <see cref="IDefaultFillStore"/> (ADR-0026), seeded from config. Thread-safe: the
|
||||
/// hosted worker reads <see cref="Current"/> per zaak while the beheer endpoint may update it.</summary>
|
||||
public sealed class InMemoryDefaultFillStore(DefaultFillSettings seed) : IDefaultFillStore
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private DefaultFillSettings _current = seed;
|
||||
|
||||
public DefaultFillSettings Current
|
||||
{
|
||||
get { lock (_gate) return _current; }
|
||||
}
|
||||
|
||||
public void Update(DefaultFillSettings settings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
lock (_gate) _current = settings;
|
||||
}
|
||||
}
|
||||
@@ -84,8 +84,11 @@ public class AclServiceTests
|
||||
InformatieobjecttypeOmschrijving = "Diploma",
|
||||
};
|
||||
|
||||
private static InMemoryDefaultFillStore FillFrom(AclDefaults d) =>
|
||||
new(new DefaultFillSettings(d.Bronorganisatie, d.VerantwoordelijkeOrganisatie, d.Vertrouwelijkheidaanduiding));
|
||||
|
||||
private static AclService ServiceWith(FakeGateway gateway, AclDefaults defaults, DateOnly today) =>
|
||||
new(gateway, defaults, new CachedZaaktypeCatalog(gateway, defaults), new FixedClock(today));
|
||||
new(gateway, FillFrom(defaults), new CachedZaaktypeCatalog(gateway, defaults), new FixedClock(today));
|
||||
|
||||
private sealed class FixedClock(DateOnly today) : IClock
|
||||
{
|
||||
@@ -113,6 +116,22 @@ public class AclServiceTests
|
||||
Assert.Equal("reg-77", req.Identificatie);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Opening_a_zaak_reflects_a_default_fill_update(/* S-15b */)
|
||||
{
|
||||
var gateway = new FakeGateway();
|
||||
var service = ServiceWith(gateway, Defaults(), new DateOnly(2026, 6, 4));
|
||||
|
||||
// A beheerder edits the default-fill; the very next zaak must use the new values (read per zaak).
|
||||
service.UpdateDefaultFill(new DefaultFillSettings("999999999", "888888888", "vertrouwelijk"));
|
||||
await service.OpenZaakAsync(new DomainRegistration("123456782", "reg-1"));
|
||||
|
||||
var req = gateway.Captured!;
|
||||
Assert.Equal("999999999", req.Bronorganisatie);
|
||||
Assert.Equal("888888888", req.VerantwoordelijkeOrganisatie);
|
||||
Assert.Equal("vertrouwelijk", req.Vertrouwelijkheidaanduiding);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Rejects_a_null_registration_without_calling_the_gateway()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using Acl.Application;
|
||||
|
||||
namespace Acl.Tests;
|
||||
|
||||
public class DefaultFillStoreTests
|
||||
{
|
||||
private static DefaultFillSettings Seed() => new("517439943", "517439943", "openbaar");
|
||||
|
||||
[Fact]
|
||||
public void Seeds_from_the_supplied_settings()
|
||||
{
|
||||
var store = new InMemoryDefaultFillStore(Seed());
|
||||
|
||||
Assert.Equal("517439943", store.Current.Bronorganisatie);
|
||||
Assert.Equal("openbaar", store.Current.Vertrouwelijkheidaanduiding);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Updating_replaces_the_current_settings()
|
||||
{
|
||||
var store = new InMemoryDefaultFillStore(Seed());
|
||||
|
||||
store.Update(new DefaultFillSettings("999999999", "888888888", "vertrouwelijk"));
|
||||
|
||||
Assert.Equal("999999999", store.Current.Bronorganisatie);
|
||||
Assert.Equal("888888888", store.Current.VerantwoordelijkeOrganisatie);
|
||||
Assert.Equal("vertrouwelijk", store.Current.Vertrouwelijkheidaanduiding);
|
||||
}
|
||||
}
|
||||
@@ -59,12 +59,26 @@ public interface IProjectionClient
|
||||
/// internal reference, not shown in the portal.</summary>
|
||||
public sealed record BeheerZaaktype(string Identificatie, string Omschrijving);
|
||||
|
||||
/// <summary>Port to the ACL for read-only catalogus queries (beheer portal, S-15a). The BFF reaches the
|
||||
/// ACL directly for this read: the catalogus isn't a domain concern, and the ACL is the only code
|
||||
/// allowed to read the ZGW Catalogi API (§8.1, ADR-0025).</summary>
|
||||
/// <summary>The ACL default-fill settings the beheer portal reads + edits (S-15b): the three ZGW-mandatory
|
||||
/// fields the ACL stamps on every zaak (ADR-0003).</summary>
|
||||
public sealed record BeheerDefaultFill(
|
||||
string Bronorganisatie,
|
||||
string VerantwoordelijkeOrganisatie,
|
||||
string Vertrouwelijkheidaanduiding);
|
||||
|
||||
/// <summary>Port to the ACL for beheer queries (beheer portal). The BFF reaches the ACL directly: these
|
||||
/// aren't a domain concern, and the ACL is the only code allowed to read/own the ZGW-facing config
|
||||
/// (§8.1, ADR-0025).</summary>
|
||||
public interface IAclClient
|
||||
{
|
||||
/// <summary>The published catalogus zaaktypen, read-only (S-15a).</summary>
|
||||
Task<IReadOnlyList<BeheerZaaktype>> GetZaaktypenAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>The current default-fill settings (S-15b).</summary>
|
||||
Task<BeheerDefaultFill> GetDefaultFillAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>Replace the default-fill settings (S-15b).</summary>
|
||||
Task UpdateDefaultFillAsync(BeheerDefaultFill settings, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>Calls the Domain Service's <c>POST /registrations</c>.</summary>
|
||||
@@ -141,4 +155,14 @@ public sealed class AclClient(HttpClient http) : IAclClient
|
||||
{
|
||||
public async Task<IReadOnlyList<BeheerZaaktype>> GetZaaktypenAsync(CancellationToken ct = default)
|
||||
=> await http.GetFromJsonAsync<List<BeheerZaaktype>>("catalogi/zaaktypen", ct) ?? [];
|
||||
|
||||
public async Task<BeheerDefaultFill> GetDefaultFillAsync(CancellationToken ct = default)
|
||||
=> await http.GetFromJsonAsync<BeheerDefaultFill>("default-fill", ct)
|
||||
?? throw new InvalidOperationException("The ACL returned an empty default-fill response.");
|
||||
|
||||
public async Task UpdateDefaultFillAsync(BeheerDefaultFill settings, CancellationToken ct = default)
|
||||
{
|
||||
using var response = await http.PutAsJsonAsync("default-fill", settings, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +228,25 @@ app.MapGet("/beheer/catalogi/zaaktypen", async (IAclClient acl, CancellationToke
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces(StatusCodes.Status403Forbidden);
|
||||
|
||||
// Beheer default-fill config (S-15b): read + edit the ACL's default-fill values. Behind medewerker-
|
||||
// realm + beheerder authorization; the BFF proxies the ACL (ADR-0025). The ACL validates the values.
|
||||
app.MapGet("/beheer/default-fill", async (IAclClient acl, CancellationToken ct) =>
|
||||
Results.Ok(await acl.GetDefaultFillAsync(ct)))
|
||||
.RequireAuthorization(BeheerAuth.Policy)
|
||||
.Produces<BeheerDefaultFill>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces(StatusCodes.Status403Forbidden);
|
||||
|
||||
app.MapPut("/beheer/default-fill", async (BeheerDefaultFill body, IAclClient acl, CancellationToken ct) =>
|
||||
{
|
||||
await acl.UpdateDefaultFillAsync(body, ct);
|
||||
return Results.NoContent();
|
||||
})
|
||||
.RequireAuthorization(BeheerAuth.Policy)
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces(StatusCodes.Status401Unauthorized)
|
||||
.Produces(StatusCodes.Status403Forbidden);
|
||||
|
||||
app.Run();
|
||||
|
||||
/// <summary>The behandelaar's decision on a registration.</summary>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using Bff.Api;
|
||||
|
||||
namespace Bff.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The beheer default-fill config endpoints (S-15b): read (GET) and edit (PUT) the ACL's default-fill,
|
||||
/// reached only with a medewerker-realm token carrying the <c>beheerder</c> role. Missing token → 401;
|
||||
/// a medewerker without the role → 403; a beheerder reads and updates via the ACL client.
|
||||
/// </summary>
|
||||
public class BeheerDefaultFillEndpointTests
|
||||
{
|
||||
private static HttpRequestMessage Get(string? bearer)
|
||||
{
|
||||
var r = new HttpRequestMessage(HttpMethod.Get, "/beheer/default-fill");
|
||||
if (bearer is not null) r.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer);
|
||||
return r;
|
||||
}
|
||||
|
||||
private static HttpRequestMessage Put(string? bearer, object body)
|
||||
{
|
||||
var r = new HttpRequestMessage(HttpMethod.Put, "/beheer/default-fill") { Content = JsonContent.Create(body) };
|
||||
if (bearer is not null) r.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer);
|
||||
return r;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Rejects_read_without_a_token()
|
||||
{
|
||||
using var factory = new BffFactory();
|
||||
var response = await factory.CreateClient().SendAsync(Get(bearer: null));
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Rejects_a_medewerker_without_the_beheerder_role()
|
||||
{
|
||||
using var factory = new BffFactory();
|
||||
var response = await factory.CreateClient().SendAsync(Get(TestTokens.Medewerker("behandelaar")));
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Serves_the_current_default_fill_to_a_beheerder()
|
||||
{
|
||||
using var factory = new BffFactory();
|
||||
factory.Acl.DefaultFill = new BeheerDefaultFill("517439943", "517439943", "openbaar");
|
||||
|
||||
var response = await factory.CreateClient().SendAsync(Get(TestTokens.Medewerker("beheerder")));
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadFromJsonAsync<BeheerDefaultFill>();
|
||||
Assert.Equal("517439943", body!.Bronorganisatie);
|
||||
Assert.Equal("openbaar", body.Vertrouwelijkheidaanduiding);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Updates_the_default_fill_via_the_acl_for_a_beheerder()
|
||||
{
|
||||
using var factory = new BffFactory();
|
||||
|
||||
var response = await factory.CreateClient().SendAsync(
|
||||
Put(TestTokens.Medewerker("beheerder"),
|
||||
new { bronorganisatie = "999999999", verantwoordelijkeOrganisatie = "888888888", vertrouwelijkheidaanduiding = "vertrouwelijk" }));
|
||||
|
||||
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
|
||||
Assert.Equal("999999999", factory.Acl.Updated!.Bronorganisatie);
|
||||
Assert.Equal("vertrouwelijk", factory.Acl.Updated.Vertrouwelijkheidaanduiding);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Rejects_an_update_from_a_non_beheerder()
|
||||
{
|
||||
using var factory = new BffFactory();
|
||||
|
||||
var response = await factory.CreateClient().SendAsync(
|
||||
Put(TestTokens.Medewerker("behandelaar"), new { bronorganisatie = "1", verantwoordelijkeOrganisatie = "2", vertrouwelijkheidaanduiding = "openbaar" }));
|
||||
|
||||
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
|
||||
Assert.Null(factory.Acl.Updated);
|
||||
}
|
||||
}
|
||||
@@ -142,11 +142,24 @@ internal sealed class FakeProjectionClient : IProjectionClient
|
||||
=> Task.FromResult<IReadOnlyList<ProjectionEntry>>(Entries);
|
||||
}
|
||||
|
||||
/// <summary>Serves a configurable set of catalogus zaaktypen (beheer viewer, S-15a).</summary>
|
||||
/// <summary>Serves catalogus zaaktypen (S-15a) and holds the default-fill settings (S-15b).</summary>
|
||||
internal sealed class FakeAclClient : IAclClient
|
||||
{
|
||||
public List<BeheerZaaktype> Zaaktypen { get; } = [];
|
||||
|
||||
public Task<IReadOnlyList<BeheerZaaktype>> GetZaaktypenAsync(CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<BeheerZaaktype>>(Zaaktypen);
|
||||
|
||||
public BeheerDefaultFill DefaultFill { get; set; } = new("517439943", "517439943", "openbaar");
|
||||
public BeheerDefaultFill? Updated { get; private set; }
|
||||
|
||||
public Task<BeheerDefaultFill> GetDefaultFillAsync(CancellationToken ct = default)
|
||||
=> Task.FromResult(DefaultFill);
|
||||
|
||||
public Task UpdateDefaultFillAsync(BeheerDefaultFill settings, CancellationToken ct = default)
|
||||
{
|
||||
Updated = settings;
|
||||
DefaultFill = settings;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,10 +255,80 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/beheer/default-fill": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Bff.Api"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/BeheerDefaultFill"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"Bff.Api"
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/BeheerDefaultFill"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"BeheerDefaultFill": {
|
||||
"required": [
|
||||
"bronorganisatie",
|
||||
"verantwoordelijkeOrganisatie",
|
||||
"vertrouwelijkheidaanduiding"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bronorganisatie": {
|
||||
"type": "string"
|
||||
},
|
||||
"verantwoordelijkeOrganisatie": {
|
||||
"type": "string"
|
||||
},
|
||||
"vertrouwelijkheidaanduiding": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"BeheerZaaktype": {
|
||||
"required": [
|
||||
"identificatie",
|
||||
|
||||
@@ -44,7 +44,9 @@ public sealed class EenZaakOpenenSteps
|
||||
[When("the domain asks the ACL to open a zaak")]
|
||||
public async Task WhenTheDomainAsksTheAclToOpenAZaak()
|
||||
{
|
||||
var service = new AclService(_gateway, _defaults!, new CachedZaaktypeCatalog(_gateway, _defaults!), new FixedClock(_today));
|
||||
var fill = new InMemoryDefaultFillStore(new DefaultFillSettings(
|
||||
_defaults!.Bronorganisatie, _defaults.VerantwoordelijkeOrganisatie, _defaults.Vertrouwelijkheidaanduiding));
|
||||
var service = new AclService(_gateway, fill, new CachedZaaktypeCatalog(_gateway, _defaults!), new FixedClock(_today));
|
||||
_returnedUrl = await service.OpenZaakAsync(_registration!);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
// S-15b: a beheerder edits the ACL default-fill in the beheer portal and gets a saved confirmation.
|
||||
// Runs against the shared verify stack; it edits + saves (the ACL store is in-memory, ADR-0026) and
|
||||
// asserts the confirmation, without depending on another test's state.
|
||||
test('a beheerder edits and saves the default-fill', async ({ page }) => {
|
||||
await page.goto('http://beheer/');
|
||||
|
||||
// Keycloak medewerker-realm login (same realm as behandel).
|
||||
await page.locator('#username').fill('bram-beheerder');
|
||||
await page.locator('#password').fill('test123');
|
||||
await page.locator('#kc-login').click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: /Catalogus/i })).toBeVisible();
|
||||
|
||||
// Navigate to the default-fill editor and change a value.
|
||||
await page.getByRole('link', { name: /Default-fill/i }).click();
|
||||
await expect(page.getByRole('heading', { name: /Default-fill/i })).toBeVisible();
|
||||
|
||||
const bron = page.getByLabel('Bronorganisatie');
|
||||
await expect(bron).toBeVisible();
|
||||
await bron.fill('517439943');
|
||||
await page.getByRole('button', { name: /Opslaan/i }).click();
|
||||
|
||||
await expect(page.getByText(/standaardwaarden zijn opgeslagen/i)).toBeVisible();
|
||||
});
|
||||
Reference in New Issue
Block a user