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
1 change: 0 additions & 1 deletion .phpunit.cache/test-results

This file was deleted.

1 change: 0 additions & 1 deletion .phpunit.result.cache

This file was deleted.

4 changes: 2 additions & 2 deletions lib/Service/SbomImportService.php
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ public function userCanReadModule(string $moduleUuid): bool
*
* @return array<string, mixed> `{sbomLastImportedAt, sbomFormat, sbomFileName, progress}`.
*
* @spec openspec/specs/sbom-import/spec.md#requirement-modulversie-records-sbom-import-provenance
* @spec openspec/specs/sbom-import/spec.md#requirement-moduleversie-records-sbom-import-provenance
*/
public function getStatus(string $moduleVersieUuid, ?string $operationId=null): array
{
Expand Down Expand Up @@ -450,7 +450,7 @@ private function createComponentSet(
*
* @return void
*
* @spec openspec/specs/sbom-import/spec.md#requirement-modulversie-records-sbom-import-provenance
* @spec openspec/specs/sbom-import/spec.md#requirement-moduleversie-records-sbom-import-provenance
*/
private function recordProvenance(
ObjectService $objectService,
Expand Down
14 changes: 8 additions & 6 deletions lib/Settings/softwarecatalogus_register.json
Original file line number Diff line number Diff line change
Expand Up @@ -4192,7 +4192,7 @@
"$ref": "",
"items": {
"cascadeDelete": true,
"$ref": "#/components/schemas/property",
"$ref": "#/components/schemas/property-definition",
"type": "object"
},
"objectConfiguration": {
Expand Down Expand Up @@ -5356,7 +5356,7 @@
"$ref": "",
"items": {
"cascadeDelete": true,
"$ref": "#/components/schemas/property",
"$ref": "#/components/schemas/property-definition",
"type": "object"
},
"objectConfiguration": {
Expand Down Expand Up @@ -5803,7 +5803,7 @@
"$ref": "",
"items": {
"cascadeDelete": true,
"$ref": "#/components/schemas/property",
"$ref": "#/components/schemas/property-definition",
"type": "object"
},
"objectConfiguration": {
Expand Down Expand Up @@ -6632,7 +6632,7 @@
"$ref": "",
"items": {
"cascadeDelete": true,
"$ref": "#/components/schemas/property",
"$ref": "#/components/schemas/property-definition",
"type": "object"
},
"objectConfiguration": {
Expand Down Expand Up @@ -8018,11 +8018,13 @@
"properties": {
"alg": {
"type": "string",
"title": "Algorithm"
"title": "Algorithm",
"description": "Naam van het hash-algoritme zoals CycloneDX het aanlevert, bijvoorbeeld SHA-256 of SHA-512."
},
"value": {
"type": "string",
"title": "Value"
"title": "Value",
"description": "De hexadecimale hashwaarde die met het genoemde algoritme over het componentbestand is berekend."
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions openspec/specs/sbom-import/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,23 @@ unset.
- **THEN** existing `moduleVersie` objects without the new fields load and
save unchanged


## Notes

### SPDX 2.x is a second parser entry point, not a second requirement

`SbomParserService` exposes two explicit entry points rather than one
auto-detecting `parse()`: `parse()` for CycloneDX and `parseSpdx()` for SPDX
2.x JSON. Both normalise to the same component DTO shape
(`name`/`version`/`purl`/`licenses`); SPDX carries no VEX-equivalent block in
this app's scope, so `parseSpdx()` always returns an empty `vulnerabilities`
list and no component can arrive with a confirmed CVE match through that path.
`spdxVersion` values outside `SPDX-2.*` are rejected with
`UnsupportedSbomFormatException`, mirroring the CycloneDX `bomFormat` /
`specVersion` rejection above.

This is recorded as a note rather than as a Requirement because the SPDX path
has unit coverage (`tests/Unit/SbomParserServiceTest.php`) but no browser-level
proof; promoting it to a Requirement means writing the Scenarios and the
Playwright test that assert them, which is follow-up work rather than something
to declare satisfied here.
28 changes: 0 additions & 28 deletions src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,6 @@
"route": "FeaturesRoadmap",
"section": "footer",
"order": 95
},
{
"id": "SettingsMenu",
"label": "Settings",
"icon": "CogOutline",
"route": "Settings",
"section": "settings",
"order": 99
}
],
"pages": [
Expand Down Expand Up @@ -827,26 +819,6 @@
"config": {
"documentationUrl": "https://softwarecatalog.conduction.nl"
}
},
{
"id": "Settings",
"route": "/settings",
"type": "settings",
"title": "Settings",
"config": {
"saveEndpoint": "/index.php/apps/softwarecatalog/api/settings",
"sections": [
{
"title": "Algemeen",
"widgets": [
{
"type": "component",
"componentName": "SoftwareCatalogSettingsPage"
}
]
}
]
}
}
]
}
4 changes: 1 addition & 3 deletions src/menu-layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,5 @@
"ComplianceMatrix": "ReportsCompliance"
},
"removals": [],
"settingsSection": [
"SettingsMenu"
]
"settingsSection": []
}
2 changes: 1 addition & 1 deletion src/utils/suiteWizard.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function summarizeApplications(applications) {
*
* @return {Array<{uuid: string, label: string, raw: object}>} Option list.
*
* @spec openspec/specs/suite-wizard/spec.md#requirement-the-wizard-must-let-the-user-attach-one-or-more-existing-applications-to-the-new-suite
* @spec openspec/specs/suite-wizard/spec.md#requirement-the-wizard-shall-guide-suite-creation-through-details-application-attachment-and-confirmation-steps
*/
export function mapApplicationOptions(collection) {
const modules = Array.isArray(collection) ? collection : (collection?.results || [])
Expand Down
2 changes: 1 addition & 1 deletion test-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ else:
echo ""
echo "--- Step 8b: Importing AMEF test data ---"

AMEF_FILE="/var/www/html/custom_apps/softwarecatalog/data/GEMMA release.xml"
AMEF_FILE="/var/www/html/custom_apps/softwarecatalog/tests/fixtures/amef/GEMMA release.xml"
if docker exec nextcloud test -f "$AMEF_FILE"; then
# Check if AMEF views already exist (full GEMMA release has 248 views)
VIEW_COUNT=$(curl -s -u "${ADMIN_USER}:${ADMIN_PASS}" \
Expand Down
58 changes: 38 additions & 20 deletions tests/e2e/manifest-pages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,38 +209,56 @@ for (const p of DETAIL_PAGES) {
}

// ---------------------------------------------------------------------------
// Settings page (type: settings) — in-app settings surface
// Settings — the Nextcloud admin settings section
// ---------------------------------------------------------------------------
// These tests used to drive the in-app `#/settings` route, a manifest
// `type: "settings"` page. ADR-079 D1 removed it: app-level configuration has
// exactly one home, `/settings/admin/<app>`, where Nextcloud authorizes the
// caller server-side before the section renders. The SAME component
// (`src/views/settings/SoftwareCatalogSettings.vue`) renders there, mounted by
// `src/settings.js` into `templates/settings/admin.php`, so these assertions
// are unchanged in substance — only the door they walk through moved.
//
// `domcontentloaded`, not `networkidle`: Nextcloud keeps long-lived polls open,
// so the network never goes idle (ADR-074 rule 4). The visibility assertions
// below are the real readiness signal.
const ADMIN_SETTINGS_URL = '/settings/admin/softwarecatalog'

/**
* Open the app's Nextcloud admin settings section and return its host element.
* Auth is injected from storageState (see playwright.config.ts).
*/
async function gotoAdminSettings(page: Page) {
await page.goto(ADMIN_SETTINGS_URL, { waitUntil: 'domcontentloaded' })
const host = page.locator('#softwarecatalog-settings')
await expect(host).toBeVisible({ timeout: 30000 })
return host
}

// The settings shell (SoftwareCatalogSettings.vue) renders its section
// navigation and the configuration status — fe-settings-ui "Open settings".
// @e2e fe-settings-ui::open-settings
test('manifest settings: in-app settings page renders', async ({ page }) => {
await gotoAppRoute(page, '/settings')
const main = page.locator(APP_MAIN).first()
await expect(main).toBeVisible()
// Scope to main so the assertion can't match a transient notification toast
// elsewhere in the DOM (which also contains app/section words but is hidden).
await expect(main.getByText('SoftwareCatalog', { exact: false }).first()).toBeVisible({ timeout: 30000 })
test('admin settings: the settings section renders', async ({ page }) => {
const host = await gotoAdminSettings(page)
// Scope to the app's own settings host so the assertion can't match a
// transient notification toast elsewhere in the DOM.
await expect(host.getByText('SoftwareCatalog', { exact: false }).first()).toBeVisible({ timeout: 30000 })
})

// The settings shell renders the Statistics overview section (StatisticsOverview.vue),
// which loads and displays aggregate object counts — fe-settings-ui "View statistics".
// @e2e fe-settings-ui::view-statistics
test('manifest settings: statistics section renders', async ({ page }) => {
await gotoAppRoute(page, '/settings')
const main = page.locator(APP_MAIN).first()
await expect(main).toBeVisible()
await expect(main.getByText('Statistics', { exact: false }).first()).toBeVisible({ timeout: 30000 })
test('admin settings: statistics section renders', async ({ page }) => {
const host = await gotoAdminSettings(page)
await expect(host.getByText('Statistics', { exact: false }).first()).toBeVisible({ timeout: 30000 })
})

// The settings shell renders the Version information section (VersionInformation.vue),
// which loads and displays the app version — fe-settings-ui "View version information".
// @e2e fe-settings-ui::view-version-information
test('manifest settings: version information section renders', async ({ page }) => {
await gotoAppRoute(page, '/settings')
const main = page.locator(APP_MAIN).first()
await expect(main).toBeVisible()
// Match the "Version Information" section heading inside main, not a hidden
// "Application Version was updated" notification toast in the DOM.
await expect(main.getByText('Version Information', { exact: false }).first()).toBeVisible({ timeout: 30000 })
test('admin settings: version information section renders', async ({ page }) => {
const host = await gotoAdminSettings(page)
// Match the "Version Information" section heading inside the settings host,
// not a hidden "Application Version was updated" notification toast.
await expect(host.getByText('Version Information', { exact: false }).first()).toBeVisible({ timeout: 30000 })
})
24 changes: 19 additions & 5 deletions tests/e2e/spec-coverage/settings.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2026 Conduction B.V.
/**
* Behavioural UI coverage for the in-app Settings page (manifest page
* `Settings` → SoftwareCatalogSettings.vue).
* Behavioural UI coverage for the app's settings surface
* (`/settings/admin/softwarecatalog` → SoftwareCatalogSettings.vue).
*
* This drove the in-app manifest page `Settings` until ADR-079 D1 removed it:
* app-level configuration has exactly one home, the Nextcloud admin settings
* section, which the platform authorizes server-side before it renders. The
* SAME component renders there — mounted by `src/settings.js` into
* `templates/settings/admin.php` — so every assertion below is unchanged; only
* the door they walk through moved.
*
* The existing manifest-pages smoke asserts only three section TITLES exist.
* This suite drives the real settings surface and its interactions:
Expand All @@ -19,11 +26,18 @@
* longer filters that message, so this suite asserts it is genuinely absent.
*/
import { test, expect } from '@playwright/test'
import { gotoAppRoute, collectAppErrors, expectNoAppErrors, APP_MAIN } from './_helpers'
import { collectAppErrors, expectNoAppErrors } from './_helpers'

/**
* Open the app's Nextcloud admin settings section and return its host element.
*
* `domcontentloaded`, not `networkidle`: Nextcloud keeps long-lived polls open,
* so the network never goes idle (ADR-074 rule 4). The banner assertion below
* is the real readiness signal.
*/
async function gotoSettings(page) {
await gotoAppRoute(page, '/settings')
const main = page.locator(APP_MAIN).first()
await page.goto('/settings/admin/softwarecatalog', { waitUntil: 'domcontentloaded' })
const main = page.locator('#softwarecatalog-settings')
// The settings shell renders the app name banner first.
await expect(main.getByText('SoftwareCatalog', { exact: false }).first())
.toBeVisible({ timeout: 30000 })
Expand Down
File renamed without changes.
Loading