Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions lib/Settings/softwarecatalogus_register.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"info": {
"title": "Software Catalog Register",
"description": "Register containing AMEF and Voorzieningen schemas for the VNG Software Catalog application. This configuration includes schemas for applications, services, organizations, and compliance tracking.",
"version": "2.4.3",
"changelog": "2.4.3: Re-authored Dutch schema-level titles to English (dienst, kwetsbaarheid, contactpersoon, organisatie, gebruik, koppeling, beoordeeling, module, bioMaatregel, moduleVersie, sbomComponent); schema keys unchanged, Dutch labels now come from the app's l10n translation files. 2.4.2: Moved SBOM provenance properties (sbomLastImportedAt, sbomFormat, sbomFileName, sbomComponents) from the organisatie schema to moduleVersie, where SBOM imports actually record them; without this the moduleVersie magic table lacked the columns so recordProvenance() writes were silently dropped and the import-status endpoint always reported 'never imported'. 2.4.1: Re-authored Dutch schema property titles to English (property keys unchanged); Dutch labels now come from the app's l10n translation files."
"version": "2.4.4",
"changelog": "2.4.4: organization.status was left behind by #520's enum translation — its `default` was still 'Concept' and its whole x-openregister-lifecycle block still named Concept/Actief/Deactief, while the enum and the migrated rows are Draft/Active/Inactive/merged. A default outside its own enum makes every newly created organisation fall out of the Organisations index filter, and a lifecycle whose from/to values match no row offers no transition at all — neither raises an error. The schema version is bumped with it because a deployed version >= the declared one makes the import SKIP, and OpenRegister's schemaContentDiffers() escape hatch compares only properties/required/authorization — never `configuration` — so a lifecycle-only edit would never have deployed. 2.4.3: Re-authored Dutch schema-level titles to English (dienst, kwetsbaarheid, contactpersoon, organisatie, gebruik, koppeling, beoordeeling, module, bioMaatregel, moduleVersie, sbomComponent); schema keys unchanged, Dutch labels now come from the app's l10n translation files. 2.4.2: Moved SBOM provenance properties (sbomLastImportedAt, sbomFormat, sbomFileName, sbomComponents) from the organisatie schema to moduleVersie, where SBOM imports actually record them; without this the moduleVersie magic table lacked the columns so recordProvenance() writes were silently dropped and the import-status endpoint always reported 'never imported'. 2.4.1: Re-authored Dutch schema property titles to English (property keys unchanged); Dutch labels now come from the app's l10n translation files."
},
"x-openregister": {
"type": "application",
Expand Down Expand Up @@ -2023,7 +2023,7 @@
"x-schema-org": "schema:Organization",
"title": "Organization",
"description": "An organisation that offers provisions. Absorbs the former ArchiMate `organization` schema: its identity and statutory identifiers (name, summary, description, oin, tooi, rsin, pki, image) and its ArchiMate round-trip `xml` are declared here, so there is one organisation schema rather than two that shared no property.",
"version": "0.5.0",
"version": "0.5.1",
"omschrijving": "",
"icon": "OfficeBuildingOutline",
"required": [
Expand Down Expand Up @@ -2318,7 +2318,7 @@
"description": "Geeft aan of de VNG de organisatie positief beoordeeld heeft voor toegang tot de Softwarecatalogus",
"title": "Status",
"type": "string",
"default": "Concept",
"default": "Draft",
"visible": false,
"hideOnCollection": true,
"facetable": false,
Expand Down Expand Up @@ -2618,32 +2618,33 @@
"fair": 0.5
}
},
"$comment-lifecycle": "🔴 THE STATE NAMES HERE ARE THE ENUM'S VALUES, NOT LABELS. #520 translated the status enum (Concept/Actief/Deactief -> Draft/Active/Inactive) and migrated the stored rows, but left this block in Dutch, so `initial`, `final` and every `from`/`to` named a value no row can hold: the initial state wrote an out-of-enum value and no transition could ever match. Nothing errors — a transition whose `from` matches nothing is simply never offered — so it reads as a lifecycle nobody uses.",
"x-openregister-lifecycle": {
"field": "status",
"initial": "Concept",
"initial": "Draft",
"final": [
"Deactief"
"Inactive"
],
"transitions": {
"activate": {
"from": [
"Concept"
"Draft"
],
"to": "Actief",
"to": "Active",
"description": "Activate the organisation."
},
"deactivate": {
"from": [
"Actief"
"Active"
],
"to": "Deactief",
"to": "Inactive",
"description": "Deactivate the organisation."
},
"reactivate": {
"from": [
"Deactief"
"Inactive"
],
"to": "Actief",
"to": "Active",
"description": "Re-activate a deactivated organisation."
}
}
Expand Down
33 changes: 21 additions & 12 deletions src/components/sbom/SbomComponentsPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ import { matchComponents } from '../../utils/sbomVulnerabilityMatch.js'
* @license EUPL-1.2
*
* ModuleversieDetail "Components" sidebar tab: renders the imported
* `sbomComponent` set for a `moduleVersie` (name/version/purl/licenses) with
* `sbomComponent` set for a `moduleVersion` (name/version/purl/licenses) with
* summary counts (total, distinct licenses, matched vulnerabilities) and an
* upload control that posts a CycloneDX/SPDX JSON file to `SbomController`.
* Re-importing REPLACES the previous set server-side (design Decision 3);
Expand All @@ -205,7 +205,7 @@ export default {
},

props: {
/** The moduleVersie OR object uuid (passed by CnObjectSidebar as `objectId`). */
/** The moduleVersion OR object uuid (passed by CnObjectSidebar as `objectId`). */
objectId: {
type: [String, Number],
default: null,
Expand Down Expand Up @@ -248,13 +248,22 @@ export default {

computed: {
/**
* The moduleVersie being inspected: the active object, else looked up
* The module version being inspected: the active object, else looked up
* by objectId in the fetched collection.
*
* @return {object|null} The moduleVersie record.
* ⚠️ THE NAME IS A CONTRACT. `moduleVersionData` below reads this
* property by name. When the schema slug `moduleVersie` was translated
* to `moduleVersion` the consumer was renamed and this producer was not,
* so the consumer resolved `undefined`, returned an empty data bag, and
* every derived value went quietly empty — no error, no warning, and the
* provenance line simply never rendered. Renaming one half of a
* producer/consumer pair is a silent break; `tests/vitest/
* sbomProvenanceLabel.spec.js` fails when the pair drifts again.
*
* @return {object|null} The module version record.
* @spec openspec/specs/sbom-import/spec.md#requirement-moduleversie-records-sbom-import-provenance
*/
moduleVersie() {
moduleVersion() {
const active =
typeof objectStore.getActiveObject === 'function'
? objectStore.getActiveObject('moduleVersion')
Expand All @@ -279,31 +288,31 @@ export default {
},

/**
* The moduleVersie's raw data bag.
* The module version's raw data bag.
*
* @return {object} The property bag.
* @spec openspec/specs/sbom-import/spec.md#requirement-imported-components-persist-as-openregister-objects-scoped-to-a-moduleversie
*/
moduleVersieData() {
moduleVersionData() {
if (!this.moduleVersion) {
return {}
}
return this.moduleVersion.object || this.moduleVersion
},

/**
* The moduleVersie's parent module uuid — scopes the possible-match
* The module version's parent module uuid — scopes the possible-match
* heuristic (design Decision 6, never a catalogue-wide scan).
*
* @return {string} The parent module uuid, or ''.
* @spec openspec/specs/sbom-import/spec.md#requirement-components-are-matched-against-existing-kwetsbaarheden-without-external-calls
*/
parentModuleId() {
return resolveUuid(this.moduleVersieData.module)
return resolveUuid(this.moduleVersionData.module)
},

/**
* The imported `sbomComponent` set for this moduleVersie, sorted by name.
* The imported `sbomComponent` set for this moduleVersion, sorted by name.
*
* @return {Array<object>} The component records.
* @spec openspec/specs/sbom-import/spec.md#requirement-imported-components-persist-as-openregister-objects-scoped-to-a-moduleversie
Expand Down Expand Up @@ -428,7 +437,7 @@ export default {
* @spec openspec/specs/sbom-import/spec.md#requirement-moduleversie-records-sbom-import-provenance
*/
lastImportedLabel() {
const data = this.moduleVersieData
const data = this.moduleVersionData
if (!data.sbomLastImportedAt) {
return ''
}
Expand Down Expand Up @@ -547,7 +556,7 @@ export default {

/**
* Upload the selected file to `SbomController::importSbom`. On success,
* refetches the sbomComponent and moduleVersie collections so the
* refetches the sbomComponent and moduleVersion collections so the
* table/summary/provenance reflect the REPLACED set (design Decision
* 3) with no page reload.
*
Expand Down
4 changes: 2 additions & 2 deletions src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,11 @@
"viewMode": "cards",
"cardComponent": "OrganisatieCard",
"columns": ["name", "type", "status", "website"],
"filter": { "status": ["Concept", "Actief", "Deactief"] },
"filter": { "status": ["Draft", "Active", "Inactive"] },
"sidebar": { "enabled": true, "showMetadata": true },
"documentationUrl": "https://softwarecatalog.conduction.nl"
},
"_note": "Decomposed from the bespoke OrganisatieIndexView to a standard type:index (Phase 8): renders organisatie OR objects as a card grid via config.cardComponent=OrganisatieCard. The card keeps its inline contactpersoon toggle internally; CnIndexPage provides the toolbar, search, view-toggle and create/edit/delete dialogs. config.filter excludes organisation-merge tombstones (status='samengevoegd') from the default listing per the organisation-merge spec — a merged-away source stays readable by direct UUID lookup (OrganisatieDetail route) but never appears in this index."
"_note": "Decomposed from the bespoke OrganisatieIndexView to a standard type:index (Phase 8): renders organisatie OR objects as a card grid via config.cardComponent=OrganisatieCard. The card keeps its inline contactpersoon toggle internally; CnIndexPage provides the toolbar, search, view-toggle and create/edit/delete dialogs. config.filter excludes organisation-merge tombstones (status='merged') from the default listing per the organisation-merge spec — a merged-away source stays readable by direct UUID lookup (OrganisatieDetail route) but never appears in this index. 🔴 THIS FILTER IS COUPLED TO THE SCHEMA'S status ENUM AND WENT STALE: #520 translated the stored enum values and migrated the rows (Concept/Actief/Deactief/samengevoegd -> Draft/Active/Inactive/merged) but did NOT translate this list, so the index filtered on three values no row can hold any more. A value filter that matches nothing is not an error — OpenRegister answers 200 with total=0 — so the Organisations index rendered 'No items found' for every user and read as an empty catalogue. Measured on the running instance: ?status[]=Draft&status[]=Active returns the seeded row, ?status[]=Concept&status[]=Actief&status[]=Deactief returns total=0. Any future enum rename must move this list in the same commit."
},
{
"id": "OrganisatieDetail",
Expand Down
31 changes: 26 additions & 5 deletions tests/e2e/spec-coverage/dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,19 +99,40 @@ test('dashboard: "Ga naar Organisaties" quick-nav button is clickable and error-

// The organisaties index is genuinely reachable via the real app nav entry
// "Organisations" — this is the user's actual navigation path and lands on the
// CnIndexPage list surface (Add Organisatie + Cards/Table toggle).
// CnIndexPage list surface (Add button + Cards/Table toggle).
test('dashboard: "Organisations" nav entry reaches the organisaties index', async ({
page,
}) => {
const bag = collectAppErrors(page)
await navClickTo(page, 'Organisations')
const main = page.locator(APP_MAIN).first()
// Organisations is a `type: custom` page (OrganisatieIndexView), not a
// CnIndexPage — its create action reads "Add organisation" and it has no
// Cards/Table toggle. Assert the custom surface's primary create action.

// ⚠️ THIS USED TO ASSERT A SURFACE THE PRODUCT NO LONGER RENDERS. The
// comment here claimed Organisations was a `type: custom` page
// (OrganisatieIndexView) "with no Cards/Table toggle" whose create action
// read "Add organisation". src/manifest.json decomposed it into a standard
// `type: index` page (its own `_note` records the change), so the surface
// is CnIndexPage: a heading, a Cards/Table view toggle, and an Add button
// whose label CnIndexPage derives as `'Add ' + schema.title`.
//
// The schema title is authored "Organization" while every other string in
// this app is British ("Organisations" nav entry, "Organisation
// relationships" page title) — and a deployed instance can still carry the
// older "Organisation" title, because OpenRegister's import skips a schema
// whose deployed version is not older and its escape hatch never compares
// the title. Accept either spelling of the one word rather than pin the
// test to whichever an environment happens to hold; the assertion still
// names the action and the entity, so it cannot match another page.
await expect(
main.getByRole('heading', { name: 'Organisation relationships' }).first(),
).toBeVisible({ timeout: 30000 })
await expect(
main.getByRole('button', { name: /Add organisation/i }).first(),
main.getByRole('button', { name: /^Add Organi[sz]ation$/i }).first(),
).toBeVisible({ timeout: 30000 })
// The view toggle exists only on the index surface — it is what
// distinguishes "landed on the index" from "landed on any page with a
// create button".
await expect(main.getByRole('button', { name: 'Table' }).first()).toBeVisible()
expectNoAppErrors(bag)
})

Expand Down
17 changes: 13 additions & 4 deletions tests/e2e/spec-coverage/gemma-faceted-search.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,19 +146,28 @@ test('facets: an unsupported schema is rejected with 400 naming the supported on
const message = String(body?.message ?? '')
// The scenario requires the error to NAME the supported schemas, not merely
// to reject — so both names are asserted, not just a non-2xx.
//
// ⚠️ The supported set is `FacetService::SUPPORTED_SCHEMAS = ['module',
// 'service']`. This assertion used to look for `dienst`, the pre-#518 Dutch
// slug, and so did the 200 control below — a slug rename moved the API and
// left the test naming a schema the service has never heard of.
expect(
message,
`error message did not name the supported schemas: ${message}`,
).toMatch(/module/)
expect(message).toMatch(/dienst/)
expect(message).toMatch(/service/)

// The supported set is also machine-readable, and must be exactly the two.
expect(body?.supportedSchemas?.sort?.()).toEqual(['service', 'module'])
// ⚠️ `Array.prototype.sort()` sorts IN PLACE and returns the sorted array,
// so the expected literal has to be sorted too — comparing a sorted actual
// against `['service', 'module']` could never have held whichever names the
// service used. Copy before sorting so the response body is not mutated.
expect([...(body?.supportedSchemas ?? [])].sort()).toEqual(['module', 'service'])

// Control: the same endpoint shape with a SUPPORTED schema is a 200, so the
// 400 above is about the schema and not about the route being broken.
const ok = await ctx.get(`${FACETS}/dienst`)
expect(ok.status(), `GET ${FACETS}/dienst returned ${ok.status()}`).toBe(200)
const ok = await ctx.get(`${FACETS}/service`)
expect(ok.status(), `GET ${FACETS}/service returned ${ok.status()}`).toBe(200)
})

// ⚠️ `no-text-query-returns-facets-over-the-full-rbac-scoped-set` IS DELIBERATELY
Expand Down
Loading
Loading