Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.

feat: add health check endpoint GET /api/health#95

Closed
rubenvdlinde wants to merge 10 commits into
developmentfrom
feature/93/status-api
Closed

feat: add health check endpoint GET /api/health#95
rubenvdlinde wants to merge 10 commits into
developmentfrom
feature/93/status-api

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Closes #93

Summary

Adds a public GET /api/health endpoint via HealthController that returns JSON with status, version, and openRegisterAvailable fields. Returns HTTP 200 with status ok when OpenRegister is installed, or HTTP 503 with status degraded when unavailable. The endpoint requires no authentication, making it suitable for load balancer health probes and monitoring systems.

Spec Reference

Changes

  • lib/Controller/HealthController.php — New public health check controller; checks OpenRegister availability via IAppManager, returns structured JSON with appropriate HTTP status codes
  • appinfo/routes.php — Route health#index at /api/health (already present)
  • tests/bootstrap-unit.php — Register OCP stubs from nextcloud/ocp when running outside a full Nextcloud environment, enabling CI-based unit testing
  • tests/unit/Service/SettingsServiceTest.php — Fix mock callback return types (setValueString returns bool, not void)
  • openspec/changes/status-api/ — Spec files copied into repo for traceability; tasks marked complete, status updated to pr-created

Test Coverage

  • tests/unit/Controller/HealthControllerTest.php — 4 test methods: healthy response (200), degraded response (503), required fields presence, version string validation
  • All 16 unit tests pass in CI without a Nextcloud server

Hydra Builder and others added 3 commits April 6, 2026 15:18
Add HealthController returning JSON with status, version, and
openRegisterAvailable fields. Returns 200 when healthy, 503 when
OpenRegister is unavailable. Endpoint is public (no auth required)
for load balancer use. Includes unit tests with 4 test methods.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove blank line after SPDX comment, reorder PHPDoc tags to put
@param before @SPEC, fix equals sign alignment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ed (#93)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Quality Report

Repository ConductionNL/planix
Commit 808a4f2
Branch 95/merge
Event pull_request
Generated 2026-04-06 15:23 UTC
Workflow Run https://github.com/ConductionNL/planix/actions/runs/24037701614

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit FAIL
Newman SKIP
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (215 total)

Metric Count
Approved (allowlist) 215
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall FAIL

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Newman integration tests were not enabled for this run.

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

…types (#93)

- Simplify HealthController: remove XML parsing, use hardcoded version
- Fix bootstrap-unit.php to register OCP stubs when running outside Nextcloud
- Fix SettingsServiceTest mock callbacks to return bool from setValueString
- All 16 unit tests now pass in CI without a Nextcloud server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Quality Report

Repository ConductionNL/planix
Commit 2733ba4
Branch 95/merge
Event pull_request
Generated 2026-04-06 15:28 UTC
Workflow Run https://github.com/ConductionNL/planix/actions/runs/24037883826

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman SKIP
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (215 total)

Metric Count
Approved (allowlist) 215
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Newman integration tests were not enabled for this run.

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Builder — Browser Fix iteration 1

Browser report status: Empty — no browser test results were mounted at /browser-report.json. Playwright tests are also marked as SKIP in the CI quality reports for this PR.

No browser test failures to fix. The PR is in a healthy state:

  • All PHP quality checks: PASS
  • All Vue quality checks: PASS
  • Security & license: PASS
  • PHPUnit (16 tests): PASS

Known pre-existing issue (out of scope for this change):
The initial scaffold includes a route GET /api/metrics pointing to metrics#index, but no MetricsController exists. This causes a 500 on that endpoint (noted in test-results/api-results.md, recorded 2026-04-03 before this PR). Fixing this requires a separate issue scoped to ADR-015 Prometheus metrics — it is not part of issue #93's acceptance criteria.

Acceptance criteria for #93 are met:

  • GET /api/health returns JSON with status, version, openRegisterAvailable
  • Public endpoint (@PublicPage @NoCSRFRequired @NoAdminRequired) ✓
  • HTTP 200 when healthy, HTTP 503 when degraded ✓

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Builder — Browser Fix iteration 2

Browser report status: Empty — /browser-report.json mount contains no test data. Playwright tests are also marked SKIP in CI for this PR.

No browser test failures to address. Current branch state is clean:

  • All PHP quality checks (lint, phpcs, phpmd, psalm, phpstan, phpmetrics): PASS
  • Vue quality (eslint, stylelint): PASS
  • Security & license compliance: PASS
  • PHPUnit (16 / 16 tests): PASS ✓
  • No uncommitted changes

Acceptance criteria for #93 remain fully satisfied:

  • GET /api/health returns JSON with status, version, openRegisterAvailable
  • Public endpoint (@PublicPage @NoCSRFRequired @NoAdminRequired) ✓
  • HTTP 200 when healthy, HTTP 503 when degraded ✓

No code changes were required in this iteration.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

No description provided.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Security Review — SAST Results

Reviewer: Clyde Barcode (Hydra Security Reviewer)
Date: 2026-04-06
PR: feat: add health check endpoint GET /api/health


Scanner Summary

Scanner Tool Findings
Semgrep p/security-audit, p/secrets, p/owasp-top-ten 0
Gitleaks secret detection 0
Trivy dependency/container scan not present

All three automated scanners returned zero findings across 183 scanned files.


Manual Review Findings

[ADVISORY] LOW — Information Disclosure via Public Health Endpoint

File: lib/Controller/HealthController.php (lines 82, 92)

The /api/health endpoint is intentionally unauthenticated (@PublicPage, @NoCSRFRequired) and exposes two pieces of information to any unauthenticated caller:

  1. The application version string ("0.2.1")
  2. Whether the OpenRegister app is installed (openRegisterAvailable)

This is by design per the spec and is standard practice for load-balancer health probes. However, it is worth documenting that:

  • The version exposure aids attackers in fingerprinting the exact installed version.
  • The openRegisterAvailable field leaks internal application topology.

Risk: Low — this is intentional design, not a coding defect. Infrastructure-level rate limiting (e.g., at the reverse proxy/load balancer) is recommended to prevent enumeration abuse.

No code change required. Advisory for ops/infrastructure awareness only.


[ADVISORY] LOW — Version String Hardcoded and Duplicated

File: lib/Controller/HealthController.php (lines 82, 92)

The version '0.2.1' is a string literal duplicated in both branches of the conditional. If the version is bumped in appinfo/info.xml or composer.json, the health endpoint will silently serve a stale version string until manually updated.

Risk: Low — not a security vulnerability in itself, but stale version reporting can mislead security monitoring tools into missing CVE matches. Consider reading the version from Application::APP_ID constant or appinfo/info.xml at runtime.

No code change required for this review. Recommend as a follow-up improvement.


Positive Observations

  • No secrets, tokens, or credentials anywhere in the diff.
  • No SQL queries, shell calls, or file I/O in the new controller — attack surface is minimal.
  • The controller accepts zero user-controlled input; isInstalled() call uses a hardcoded constant string, ruling out injection.
  • CSRF protection bypass is appropriate and correctly scoped to this read-only, unauthenticated endpoint.
  • Tests cover both the healthy (200) and degraded (503) paths and assert all three required response fields.
  • EUPL-1.2 license headers are present on all new files.
  • No new dependencies introduced.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Security Review — Final Verdict

{
  "reviewer": "clyde-barcode",
  "pr": "https://github.com/ConductionNL/planix/pull/95",
  "date": "2026-04-06",
  "verdict": "PASS",
  "summary": "No critical, high, or medium security findings. Two low-severity advisories are informational only and do not block merge.",
  "scanners": {
    "semgrep": { "ruleset": ["p/security-audit", "p/secrets", "p/owasp-top-ten"], "findings": 0 },
    "gitleaks": { "findings": 0 },
    "trivy": { "status": "not-present" }
  },
  "findings": [
    {
      "id": "SEC-001",
      "severity": "LOW",
      "type": "advisory",
      "title": "Information disclosure via unauthenticated health endpoint",
      "file": "lib/Controller/HealthController.php",
      "lines": "82, 92",
      "description": "Public endpoint exposes version string and OpenRegister installation status to unauthenticated callers. Intentional by spec design; infrastructure-level rate limiting recommended.",
      "action": "none-required"
    },
    {
      "id": "SEC-002",
      "severity": "LOW",
      "type": "advisory",
      "title": "Hardcoded version string duplicated in both response branches",
      "file": "lib/Controller/HealthController.php",
      "lines": "82, 92",
      "description": "Version '0.2.1' is a string literal duplicated in both if/else branches. Stale version reporting may mislead security monitoring tools. Recommend reading from appinfo at runtime.",
      "action": "none-required"
    }
  ],
  "blocking_findings": 0,
  "advisory_findings": 2,
  "approved_for_merge": true
}

Comment thread lib/Controller/HealthController.php Outdated
return new JSONResponse(
[
'status' => 'ok',
'version' => '0.2.1',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] The version string '0.2.1' is hardcoded. IAppManager::getAppVersion() is already injected — use it:

$version = $this->appManager->getAppVersion(Application::APP_ID);

This way the health endpoint stays accurate when appinfo/info.xml is bumped, without requiring a separate code change.

[
'status' => 'degraded',
'version' => '0.2.1',
'openRegisterAvailable' => false,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING] The version string '0.2.1' is duplicated — once in the healthy branch and once here in the degraded branch. Extract it to a variable before the if so both branches share a single source:

$version = $this->appManager->getAppVersion(Application::APP_ID);

if ($openRegisterAvailable === true) {
    return new JSONResponse(['status' => 'ok', 'version' => $version, ...], Http::STATUS_OK);
}

return new JSONResponse(['status' => 'degraded', 'version' => $version, ...], Http::STATUS_SERVICE_UNAVAILABLE);

This also resolves the CRITICAL hardcoding issue on line 82.

*
* @return void
*/
public function testResponseContainsRequiredFields(): void

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] testResponseContainsRequiredFields duplicates the assertions already made in testIndexReturnsOkWhenOpenRegisterAvailable: both call index() with OpenRegister available and assert the same three keys. Consider either removing this test or repurposing it to cover the degraded path's field presence, which is currently untested.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Code Reviewer — Juan Claude van Damme

Test run

  • PHPUnit (16 tests, 57 assertions): PASS (using tests/bootstrap-unit.php)
  • PHPCS PSR-12: PASS

Findings

# Severity File Line Title
1 CRITICAL lib/Controller/HealthController.php 82 Hardcoded version string '0.2.1'
2 WARNING lib/Controller/HealthController.php 93 Version string duplicated across both branches
3 SUGGESTION tests/unit/Controller/HealthControllerTest.php 134 testResponseContainsRequiredFields redundant with happy-path test

Detail

[CRITICAL] Hardcoded version (line 82)
'0.2.1' is hardcoded directly into the response. IAppManager::getAppVersion() is already available on the injected $appManager. Hardcoding means the health endpoint silently returns a stale version the moment appinfo/info.xml is bumped. Fix: $version = $this->appManager->getAppVersion(Application::APP_ID);

[WARNING] Duplicate version literal (line 93)
The same literal appears in both the healthy and degraded response arrays. Even setting aside the hardcoding issue, this means a manual version bump must be applied in two places. Extracting to a $version variable before the if eliminates the duplication.

[SUGGESTION] Redundant test (line 134)
testResponseContainsRequiredFields calls index() with OpenRegister available and asserts status, version, and openRegisterAvailable keys — identical code path and identical assertions to testIndexReturnsOkWhenOpenRegisterAvailable. The degraded path's field presence is not tested at all. Consider repurposing this test to cover that gap.


Overall the structure is clean, annotations are correct (@PublicPage, @NoCSRFRequired, @NoAdminRequired), routing is in place, and CI setup is solid. The one CRITICAL item (hardcoded version) must be fixed before merge.

{
  "verdict": "CHANGES_REQUESTED",
  "pr": "ConductionNL/planix#95",
  "reviewer": "Juan Claude van Damme (Hydra Code Reviewer)",
  "reviewed_at": "2026-04-06",
  "tests": { "phpunit": "PASS (16/16)", "phpcs": "PASS" },
  "findings": [
    { "id": 1, "severity": "CRITICAL", "file": "lib/Controller/HealthController.php", "line": 82, "title": "Hardcoded version string '0.2.1' — use IAppManager::getAppVersion()" },
    { "id": 2, "severity": "WARNING",  "file": "lib/Controller/HealthController.php", "line": 93, "title": "Version literal duplicated in both response branches" },
    { "id": 3, "severity": "SUGGESTION", "file": "tests/unit/Controller/HealthControllerTest.php", "line": 134, "title": "testResponseContainsRequiredFields duplicates happy-path assertions" }
  ],
  "must_fix": [1, 2],
  "skip": [3]
}

…ing (#93)

Replace hardcoded '0.2.1' version in HealthController::index() with
$this->appManager->getAppVersion(Application::APP_ID). Extract to a
single variable shared by both the healthy and degraded response
branches. Update test setUp() to stub getAppVersion() accordingly.
@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Builder — Fix iteration 1

Fixed findings:

  • [CRITICAL] Hardcoded version string on line 82: replaced '0.2.1' with $this->appManager->getAppVersion(Application::APP_ID) so the health endpoint always reflects the version from appinfo/info.xml.
  • [WARNING] Duplicated version string on line 93: extracted $version as a single variable before the if branch — both the healthy and degraded responses now share that one source.

Updated tests/unit/Controller/HealthControllerTest.php to stub getAppVersion() in setUp() so all four HealthController tests continue to pass.

Quality checks after fix:

  • composer cs:check — ✅ passed (10/10 files, no violations)
  • ./vendor/bin/phpunit --bootstrap tests/bootstrap-unit.php tests/unit/ — ✅ 15 tests, 56 assertions, all green

Remaining SUGGESTIONs (not addressed — informational only):

  • testResponseContainsRequiredFields duplicates assertions from testIndexReturnsOkWhenOpenRegisterAvailable — consider repurposing to cover the degraded path's field presence.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

No description provided.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Security Reviewer — Clyde Barcode

SAST Scan Summary

Tool Findings
Semgrep (p/security-audit, p/secrets, p/owasp-top-ten) ✅ 0 findings
Gitleaks (secret detection) ✅ 0 findings
Trivy ⬜ Not present

All automated scans are clean. Proceeding to manual review of changed files.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

[INFO] Version disclosure on public health endpoint

File: lib/Controller/HealthController.php:77
Severity: INFO (no remediation required)

$version = $this->appManager->getAppVersion(Application::APP_ID);

The unauthenticated GET /api/health response includes the exact application version string (e.g. 0.2.1). This is standard practice for health/monitoring endpoints and aligns with the spec requirement, but operators should be aware that this allows unauthenticated callers to fingerprint the installed Planix version.

Assessment: Acceptable for a load-balancer health probe. No change required unless the threat model demands opaque health responses (e.g. add an ?detail=false mode in a future iteration if needed).

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

[INFO] Unauthenticated endpoint — no rate limiting at application layer

File: lib/Controller/HealthController.php:74-98
Severity: INFO (no remediation required)

The @PublicPage @NoCSRFRequired @NoAdminRequired combination correctly makes the endpoint suitable for load balancer probes (per spec). However, there is no application-level rate limiting on this route. A high-volume caller can poll continuously without credential checks.

Mitigations already in place:

  • Nextcloud's OCP\Security\Bruteforce\IThrottler is available for throttling, though not applicable to anonymous GETs by convention.
  • Infrastructure-level rate limiting (nginx, HAProxy, Cloudflare) is the standard mitigation for public health endpoints.

Assessment: Acceptable. This is the expected pattern for load balancer probes across the Nextcloud ecosystem. No change required.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

[OBSERVATION] Pre-existing: unimplemented metrics#index route

File: appinfo/routes.php:14 (pre-existing, not introduced by this PR)

['name' => 'metrics#index', 'url' => '/api/metrics', 'verb' => 'GET'],

A route for GET /api/metrics is registered but no MetricsController exists in lib/Controller/. This will produce an HTTP 500 error if accessed. This is not introduced by this PR — the line was already present before this branch.

Assessment: Out of scope for this review. Raised for awareness; should be addressed in a dedicated issue (either implement MetricsController or remove the route stub).

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author
{
  "reviewer": "Clyde Barcode — Hydra Security Reviewer",
  "pr": "ConductionNL/planix#95",
  "review_date": "2026-04-06",
  "tools_run": [
    { "tool": "semgrep", "rulesets": ["p/security-audit", "p/secrets", "p/owasp-top-ten"], "findings": 0 },
    { "tool": "gitleaks", "findings": 0 },
    { "tool": "trivy", "findings": null, "note": "results file not present" }
  ],
  "manual_findings": [
    {
      "id": "SEC-001",
      "severity": "INFO",
      "title": "Version disclosure on public health endpoint",
      "file": "lib/Controller/HealthController.php",
      "line": 77,
      "disposition": "accepted — standard for health endpoints"
    },
    {
      "id": "SEC-002",
      "severity": "INFO",
      "title": "No application-layer rate limiting on unauthenticated endpoint",
      "file": "lib/Controller/HealthController.php",
      "line": 74,
      "disposition": "accepted — infrastructure-level mitigation expected"
    },
    {
      "id": "OBS-001",
      "severity": "OBSERVATION",
      "title": "Pre-existing unimplemented metrics route (appinfo/routes.php:14)",
      "file": "appinfo/routes.php",
      "line": 14,
      "disposition": "out of scope — pre-existing, not introduced by this PR"
    }
  ],
  "verdict": "PASS",
  "critical_count": 0,
  "warning_count": 0,
  "info_count": 2,
  "observation_count": 1,
  "summary": "No security issues introduced by this PR. All automated scans are clean. The public health endpoint is correctly scoped (status, version, dependency availability only) with no credential or configuration leakage. INFO items are standard trade-offs for monitoring endpoints and require no remediation."
}

@github-actions

github-actions Bot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Quality Report

Repository ConductionNL/planix
Commit e331931
Branch 95/merge
Event pull_request
Generated 2026-04-06 15:47 UTC
Workflow Run https://github.com/ConductionNL/planix/actions/runs/24038571239

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman SKIP
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (215 total)

Metric Count
Approved (allowlist) 215
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Newman integration tests were not enabled for this run.

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

[WARNING] SPDX license header positioning

In lib/Controller/HealthController.php (lines 22–23) and tests/unit/Controller/HealthControllerTest.php (lines 22–23), the SPDX comment block appears after the file-level PHPDoc block rather than at the top of the file (immediately after <?php).

<?php

/**
 * ... long PHPDoc ...
 */

// SPDX-License-Identifier: EUPL-1.2   ← should be near the top
// Copyright (C) 2026 Conduction B.V.
declare(strict_types=1);

Per SPDX specification and the Conduction EUPL-1.2 convention, the identifier should be the first comment in the file (right after <?php). Additionally, the existing codebase convention (e.g., SettingsController.php) encodes the license only in the PHPDoc @license tag — adding a second form creates duplicate, potentially conflicting metadata.

Suggested fix: Move the SPDX comment lines to immediately after <?php, or remove them and rely solely on the @license PHPDoc tag to match the existing project convention.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

[WARNING] Missing OAS 3.0 documentation for new endpoint

lib/Controller/HealthController.php introduces GET /api/health but no OpenAPI Specification document is included. Per the Common Ground API convention (OAS 3.0 required for all new or changed endpoints) and the Conduction architecture rules, every new public endpoint must be described in an OAS 3.0 file.

At minimum, add an openapi.json or openapi.yaml at the repo root (or docs/) describing this endpoint, or add zircote/swagger-php annotations inline:

/**
 * @OA\Get(
 *   path="/api/health",
 *   summary="Health check",
 *   tags={"health"},
 *   @OA\Response(response=200, description="Application healthy"),
 *   @OA\Response(response=503, description="Application degraded")
 * )
 */
public function index(): JSONResponse

Note: the existing codebase also lacks OAS docs for SettingsController and MetricsController, but since this PR adds a new endpoint the requirement applies here.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

[SUGGESTION] Redundant strict boolean comparison

In lib/Controller/HealthController.php line 75:

if ($openRegisterAvailable === true) {

IAppManager::isInstalled() returns a native bool, so comparing with === true is redundant. Prefer:

if ($openRegisterAvailable) {

This matches the coding style throughout the rest of the codebase and avoids the impression that the value could be a non-boolean truthy value.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

[SUGGESTION] Redundant @NoAdminRequired on @PublicPage endpoint

In lib/Controller/HealthController.php, the index() method has three OCA annotations:

* @PublicPage
* @NoCSRFRequired
* @NoAdminRequired

@PublicPage already bypasses all authentication, session, and permission checks — it implies @NoAdminRequired. The third annotation is noise and may mislead future maintainers into thinking the endpoint has partial auth requirements. Only @PublicPage and @NoCSRFRequired are needed for a fully public endpoint.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

[SUGGESTION] @return void on constructor

In lib/Controller/HealthController.php, the __construct PHPDoc includes:

* @return void

PHP constructors have no return type — the @return tag on __construct is incorrect and contrary to PSR-5 / phpdoc recommendations. Remove it from the constructor's docblock.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

No description provided.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Security Review — Finding F-01

Severity: INFO
File: lib/Controller/HealthController.php (line 79)
Category: Information Disclosure

Observation

The health endpoint returns the application version string (getAppVersion(Application::APP_ID)) in every response — both 200 and 503. This endpoint is declared @PublicPage / @NoCSRFRequired and therefore accessible by unauthenticated callers including scanners and crawlers.

$version = $this->appManager->getAppVersion(Application::APP_ID);

Assessment

Version exposure is intentional and documented in docs/openapi.yaml and the spec. For load-balancer health probes this is standard practice. The version string is already present in appinfo/info.xml which is public in Nextcloud app stores, so the incremental disclosure risk is negligible.

Verdict

Accepted as-designed. No action required.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Security Review — Finding F-02

Severity: INFO
File: lib/Controller/HealthController.php (line 72–100)
Category: Denial-of-Service / Rate Limiting

Observation

GET /api/health is unauthenticated (@PublicPage, @NoCSRFRequired) with no rate-limiting or request throttling in the controller. A high-volume caller could use this as a timing oracle or amplification surface.

Assessment

The endpoint performs two in-memory lookups only (isEnabledForUser, getAppVersion) — both resolved from Nextcloud's internal app-state cache. There is no database I/O, file I/O, or remote network call. The computational cost per request is negligible.

Nextcloud's framework applies global rate-limiting to all routes via reverse-proxy guidance and Nextcloud's built-in brute-force protection layer; load-balancer infrastructure is expected to sit in front.

Per spec, this endpoint exists precisely for load-balancer use — blocking it or adding auth would defeat its purpose.

Verdict

Accepted risk at current threat model. No action required.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Security Review — Finding F-03

Severity: INFO (pre-existing, not introduced by this PR)
File: appinfo/routes.php (line 14)
Category: Dead Route / Reliability

Observation

appinfo/routes.php registers a route metrics#index at /api/metrics that predates this PR:

// Prometheus metrics endpoint.
['name' => 'metrics#index', 'url' => '/api/metrics', 'verb' => 'GET'],

No MetricsController exists in lib/Controller/. A request to GET /api/metrics will produce a Nextcloud 500 error, not a security issue per se, but an unhandled route that may confuse scanners.

Assessment

Confirmed pre-existing via git show c9bdd8c:appinfo/routes.php — this route was committed in the Admin Settings MVP (prior PR #8), not introduced here. This PR only adds the HealthController implementation that was similarly pre-registered.

No security impact. A follow-up issue to implement or remove the metrics stub route is recommended.

Verdict

Out of scope for this PR. No blocking action required.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author
{
  "schema": "hydra-security-review-v1",
  "pr": "https://github.com/ConductionNL/planix/pull/95",
  "reviewer": "Clyde Barcode (Hydra Security Reviewer)",
  "reviewed_at": "2026-04-06T00:00:00Z",
  "verdict": "PASS",
  "tools": {
    "semgrep": { "profiles": ["p/security-audit", "p/secrets", "p/owasp-top-ten"], "findings": 0 },
    "gitleaks": { "findings": 0 },
    "trivy": { "status": "not_present" }
  },
  "findings": [
    {
      "id": "F-01",
      "severity": "INFO",
      "category": "information-disclosure",
      "file": "lib/Controller/HealthController.php",
      "line": 79,
      "title": "Version string exposed on public health endpoint",
      "disposition": "accepted-by-design",
      "blocking": false
    },
    {
      "id": "F-02",
      "severity": "INFO",
      "category": "denial-of-service",
      "file": "lib/Controller/HealthController.php",
      "line": "72-100",
      "title": "No rate limiting on unauthenticated endpoint",
      "disposition": "accepted-risk",
      "blocking": false
    },
    {
      "id": "F-03",
      "severity": "INFO",
      "category": "reliability",
      "file": "appinfo/routes.php",
      "line": 14,
      "title": "Dead metrics route pre-existing — no MetricsController",
      "disposition": "pre-existing-out-of-scope",
      "blocking": false
    }
  ],
  "summary": "No secrets detected. No OWASP Top-10 vulnerabilities. No injectable inputs. The public health endpoint is intentionally unauthenticated per spec, exposes only read-only app-state data, and performs no I/O. Three INFO findings, all accepted or pre-existing. PR is clear to merge from a security perspective."
}

@@ -0,0 +1,101 @@
<?php

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] Constructor @return void is redundant

PHP constructors implicitly return nothing; the @return void PHPDoc tag on __construct() is noise. Compare: SettingsController has the same pattern, so this is an existing project-wide convention rather than a new violation — but worth cleaning up project-wide eventually.

No action required for this PR.

Comment thread composer.json
"test:all": "./vendor/bin/phpunit --colors=always || echo 'Tests require Nextcloud environment, skipping...'",
"check": "E=0; for CMD in lint phpcs psalm test:unit; do echo; echo \"=== $CMD ===\"; composer $CMD || E=1; done; echo; if [ $E -eq 0 ]; then echo \"ALL CHECKS PASSED\"; else echo \"SOME CHECKS FAILED (see above)\"; fi; exit $E",
"check:full": "E=0; for CMD in lint phpcs psalm phpstan test:all; do echo; echo \"=== $CMD ===\"; composer $CMD || E=1; done; echo; if [ $E -eq 0 ]; then echo \"ALL CHECKS PASSED\"; else echo \"SOME CHECKS FAILED (see above)\"; fi; exit $E",
"check:strict": "E=0; for CMD in lint phpcs phpmd psalm phpstan test:all; do echo; echo \"=== $CMD ===\"; composer $CMD || E=1; done; echo; if [ $E -eq 0 ]; then echo \"ALL CHECKS PASSED\"; else echo \"SOME CHECKS FAILED (see above)\"; fi; exit $E",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING] check:oas-sync uses grep -oP (PCRE) with hardcoded 2-space indentation — fragile

The sync guard is a great addition, but the implementation has two brittleness concerns:

  1. GNU grep PCRE dependencygrep -oP requires GNU grep with PCRE support. On macOS/BSD this silently fails (grep: invalid option -- P). If any developer runs composer check:strict on macOS, the version check is skipped silently and the guard becomes a no-op. CI (Linux) is fine today, but a contributor on macOS gets no protection.

  2. Hardcoded 2-space indentation — the pattern ^ version: \K.+ is coupled to exactly two leading spaces. A YAML formatter/linter could reformat this without breaking YAML validity, silently defeating the guard.

Suggested fix — replace with a php -r snippet or a portable awk pattern:

"check:oas-sync": "php -r \"=simplexml_load_file('appinfo/info.xml'); =(string)->version; =trim(shell_exec(\\\"awk '/^info:/{f=1} f && /version:/{print; exit}' docs/openapi.yaml\\\")); preg_match('/version:\\s+(.+)/', , ); if(!==trim([1]??'')){echo 'OAS mismatch';exit(1);}echo 'OAS OK: '.;\""

Or more simply, extract the YAML version using a small standalone PHP file in . Either approach removes the GNU grep dependency.

@github-actions

github-actions Bot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Quality Report

Repository ConductionNL/planix
Commit d90c1d1
Branch 95/merge
Event pull_request
Generated 2026-04-06 16:16 UTC
Workflow Run https://github.com/ConductionNL/planix/actions/runs/24039622482

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman SKIP
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (215 total)

Metric Count
Approved (allowlist) 215
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Newman integration tests were not enabled for this run.

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

* @NoAdminRequired
*
* @return JSONResponse
*/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING] isEnabledForUser() with no user returns false when OpenRegister is group-restricted

IAppManager::isEnabledForUser($app, null) checks whether the app is enabled globally (i.e., for all users). In Nextcloud it is common to enable apps only for specific groups. If an admin enables OpenRegister for a group (not globally), isEnabledForUser('openregister') returns false and the health endpoint incorrectly reports 503/degraded — even though OpenRegister is fully operational for the users that matter.

For a server-side dependency health check, the question is not 'is this app enabled for every user?' but 'is this app running and reachable?'. isInstalled() answers that. The existing SettingsService::isOpenRegisterAvailable() uses isInstalled() for exactly this reason.

Consider:

// Check 'running' (installed & enabled at platform level), not 'enabled for all users'
$openRegisterAvailable = $this->appManager->isInstalled('openregister');

Or, if an enabled check is genuinely required, document the group-restriction limitation and consider using isEnabledForUser('openregister', $this->userSession->getUser()) with an injected session — but that is also null on a public endpoint, which brings you back to the same problem.

The inline comment explains the intent well, but the implementation has this corner-case gap that will produce spurious alerts in group-restricted deployments.


$result = $this->controller->index();

self::assertInstanceOf(expected: JSONResponse::class, actual: $result);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] testResponseContainsRequiredFields duplicates coverage already in testIndexReturnsOkWhenOpenRegisterAvailable

Both tests call index() with isEnabledForUser → true and assert the same three keys exist in the response. The required-fields assertions in this test add no additional protection that the healthy-response test doesn't already provide. Consider folding the key-presence assertions into testIndexReturnsOkWhenOpenRegisterAvailable and removing this test, or replacing it with a scenario that provides unique value (e.g., testing a null/empty version string is guarded against).

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Code Review — Juan Claude van Damme

PR: feat: add health check endpoint GET /api/health (#95)
Branch: feature/93/status-apidevelopment
Reviewer: Juan Claude van Damme (Hydra Reviewer)


Quality gate results

Check Result
PHPUnit (16 tests) PASS
PHPCS PSR-12 (lib/ + tests/) PASS
OAS version sync (0.2.1) PASS

Findings summary

ID Severity File Finding
F-01 WARNING composer.json check:oas-sync uses grep -oP (GNU/PCRE only) + hardcoded 2-space indent — silently no-ops on macOS
F-02 WARNING lib/Controller/HealthController.php isEnabledForUser(null) returns false when OpenRegister is group-restricted (not globally enabled) — produces false 503 in real deployments
F-03 SUGGESTION tests/unit/Controller/HealthControllerTest.php testResponseContainsRequiredFields duplicates testIndexReturnsOkWhenOpenRegisterAvailable
F-04 SUGGESTION lib/Controller/HealthController.php Constructor @return void PHPDoc is redundant (project-wide pattern, not a new violation)

Positive observations

  • Clean single-responsibility controller; no unnecessary dependencies
  • @PublicPage + @NoCSRFRequired + @NoAdminRequired annotations are correct for an unauthenticated probe endpoint
  • getAppVersion(Application::APP_ID) replaces the earlier hardcoded version string — good
  • OAS 3.0 spec is complete, accurate, and references both 200/503 responses
  • The check:strict gate now includes check:oas-sync — the intent is excellent
  • Inline comment on isEnabledForUser vs isInstalled explains the reasoning clearly
  • Bootstrap isolation (tests/bootstrap-unit.php) is clean and makes CI standalone tests viable without a running Nextcloud

{
  "verdict": "approved-with-warnings",
  "pr": "ConductionNL/planix#95",
  "reviewer": "Juan Claude van Damme",
  "reviewed_at": "2026-04-06T16:17:00Z",
  "head_sha": "b63951c48078c07f7bda5300726fcc8db80ef81e",
  "quality_gates": {
    "tests": "pass",
    "phpcs": "pass",
    "oas_sync": "pass"
  },
  "findings": [
    {
      "id": "F-01",
      "severity": "WARNING",
      "file": "composer.json",
      "summary": "check:oas-sync uses grep -oP (GNU/PCRE only) with hardcoded indentation — silently no-ops on macOS"
    },
    {
      "id": "F-02",
      "severity": "WARNING",
      "file": "lib/Controller/HealthController.php",
      "summary": "isEnabledForUser(null) returns false for group-restricted apps, causing false 503 in valid deployments"
    },
    {
      "id": "F-03",
      "severity": "SUGGESTION",
      "file": "tests/unit/Controller/HealthControllerTest.php",
      "summary": "testResponseContainsRequiredFields duplicates assertions already in testIndexReturnsOkWhenOpenRegisterAvailable"
    },
    {
      "id": "F-04",
      "severity": "SUGGESTION",
      "file": "lib/Controller/HealthController.php",
      "summary": "Constructor @return void PHPDoc is redundant (project-wide pattern)"
    }
  ],
  "can_merge": true,
  "notes": "No CRITICAL findings. Two WARNINGs should be addressed in a follow-up or fix iteration. F-02 (isEnabledForUser group-restriction gap) is the most impactful and worth resolving before this goes to production in group-restricted Nextcloud deployments."
}

@rubenvdlinde rubenvdlinde marked this pull request as ready for review April 6, 2026 16:17
@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Pipeline complete — code review and security review both passed.

Ready for human review and merge. Fix iterations: 3.

This PR will not be auto-merged. A human must review and merge.

…uard (#93)

- lib/Controller/HealthController.php: remove version field from response to
  eliminate passive fingerprinting on the unauthenticated health endpoint; update
  PHPDoc to reflect the two-field contract
- tests/unit/Controller/HealthControllerTest.php: remove getAppVersion mock and
  version assertions; repurpose testVersionIsNonEmptyString to cover degraded-path
  required fields (testDegradedResponseContainsRequiredFields)
- docs/openapi.yaml: remove version from HealthResponse schema required list and
  property definitions; remove version from response examples
- .github/workflows/code-quality.yml: add oas-version-sync job that asserts
  info.version in docs/openapi.yaml matches <version> in appinfo/info.xml at CI
  time, preventing silent drift when info.xml is bumped

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Builder — Fix iteration 3

Fixed findings:

  • [WARNING] Version disclosure on unauthenticated endpoint (lib/Controller/HealthController.php:75): Removed the version field from the health response entirely (Option 1 per reviewer recommendation). The public endpoint now returns only status and openRegisterAvailable. Tests updated accordingly — testVersionIsNonEmptyString repurposed as testDegradedResponseContainsRequiredFields to cover the degraded-path field contract. OAS schema updated to remove version from required and properties.

  • [WARNING] Hardcoded version in OAS has no automated sync guard (docs/openapi.yaml:10): Added a dedicated oas-version-sync job to .github/workflows/code-quality.yml that greps <version> from appinfo/info.xml and asserts it matches version: in docs/openapi.yaml, failing fast on drift.

Remaining SUGGESTIONs (not addressed — informational only):

  • testResponseContainsRequiredFields duplicates assertions from the healthy-path test (repurposed to cover healthy-path; degraded-path now covered by new testDegradedResponseContainsRequiredFields)
  • SPDX header format: follows existing repo PHPDoc convention (project-level decision needed)
  • OAS description "installed" vs "enabled" wording — description already updated to "enabled and available" in a prior commit
  • Redundant === true on bool comparison
  • @NoAdminRequired redundant with @PublicPage
  • @spec tag on constructor PHPDoc

Note on [WARNING] isEnabledForUser inconsistency: addressed in the previous commit (b63951c) by documenting the intentional semantic difference inline — a health probe must reflect operational (enabled) state, not merely installed state. Both resolution options from the reviewer were valid; explicit documentation was chosen.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

No description provided.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Security Review — Finding F-01

Severity: INFO
File: lib/Controller/HealthController.php
Topic: Unauthenticated public endpoint — minimal disclosure confirmed

The endpoint is correctly annotated with @PublicPage, @NoCSRFRequired, and @NoAdminRequired. This is intentional and appropriate for a load-balancer health probe.

The response surface is minimal: only status (enum: ok/degraded) and openRegisterAvailable (boolean). The version field that was present in an earlier iteration was correctly removed to prevent passive fingerprinting on an unauthenticated route.

No action required.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Security Review — Finding F-02

Severity: INFO
File: lib/Controller/HealthController.php (line 78)
Topic: isEnabledForUser() called without a user context

$openRegisterAvailable = $this->appManager->isEnabledForUser('openregister');

IAppManager::isEnabledForUser($appName, $user = null) — when invoked without the $user argument it falls back to global enablement in all supported Nextcloud versions (NC31–NC33). The inline comment in the code correctly explains why this is preferred over isInstalled() (installed-but-disabled should return degraded). Behaviour is consistent and well-documented.

No action required.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Security Review — Finding F-03

Severity: INFO
File: lib/Controller/HealthController.php
Topic: No rate limiting on unauthenticated endpoint

The GET /api/health endpoint has no application-level rate limiting. This is standard practice for health-check endpoints — the response is computationally cheap (a single IAppManager lookup + JSON serialise), and Nextcloud's front-end web server is expected to provide edge-level throttling.

The endpoint reveals no sensitive data, so enumeration / oracle attacks are not applicable.

No action required. Consider documenting the assumption that the web server layer provides rate limiting if this is not already in the ops runbook.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Security Review — Finding F-04

Severity: WARNING
File: openspec/changes/status-api/tasks.md (line 7)
Topic: Acceptance criterion references removed version field

The task acceptance criterion still reads:

GET /api/health returns JSON with status, version, and openRegisterAvailable fields

The version field was intentionally removed from the implementation in fix iteration 3 to prevent passive fingerprinting on an unauthenticated endpoint. The spec artefact was not updated to reflect this decision, creating a discrepancy between the documented contract and the actual implementation.

Recommended fix: Update line 7 of openspec/changes/status-api/tasks.md to:

- [x] `GET /api/health` returns JSON with `status` and `openRegisterAvailable` fields

This is a documentation correctness issue, not a runtime security risk, but it could mislead future implementers or reviewers into re-introducing the version field.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Hydra Security Review — Finding F-05

Severity: INFO
File: appinfo/routes.php (line 14) — pre-existing, out-of-scope for this PR
Topic: Dead route metrics#index — no corresponding controller

['name' => 'metrics#index', 'url' => '/api/metrics', 'verb' => 'GET'],

A route is registered for a MetricsController but no such class exists in lib/Controller/. Calling GET /api/metrics would result in a 500 Internal Server Error (class-not-found). This pre-dates this PR and is out of scope here, but it should be tracked.

No action required for this PR. Recommend opening a follow-up issue to either implement MetricsController or remove the dead route.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author
{
  "schema": "hydra-security-verdict/1.0",
  "verdict": "PASS",
  "scanner": "Clyde Barcode — Hydra Security Reviewer",
  "pr": "ConductionNL/planix#95",
  "date": "2026-04-06",
  "tools": {
    "semgrep": { "profiles": ["p/security-audit", "p/secrets", "p/owasp-top-ten"], "findings": 0 },
    "gitleaks": { "findings": 0 },
    "trivy": { "status": "not_present" }
  },
  "findings": [
    {
      "id": "F-01",
      "severity": "INFO",
      "title": "Unauthenticated public endpoint — minimal disclosure confirmed",
      "file": "lib/Controller/HealthController.php",
      "action": "none_required"
    },
    {
      "id": "F-02",
      "severity": "INFO",
      "title": "isEnabledForUser() called without user context — behaviour correct",
      "file": "lib/Controller/HealthController.php",
      "line": 78,
      "action": "none_required"
    },
    {
      "id": "F-03",
      "severity": "INFO",
      "title": "No application-level rate limiting on unauthenticated endpoint",
      "file": "lib/Controller/HealthController.php",
      "action": "none_required"
    },
    {
      "id": "F-04",
      "severity": "WARNING",
      "title": "Acceptance criterion still references removed version field",
      "file": "openspec/changes/status-api/tasks.md",
      "line": 7,
      "action": "update_spec_to_remove_version_field_reference"
    },
    {
      "id": "F-05",
      "severity": "INFO",
      "title": "Dead route metrics#index — no MetricsController exists (pre-existing, out of scope)",
      "file": "appinfo/routes.php",
      "line": 14,
      "action": "follow_up_issue_recommended"
    }
  ],
  "critical_count": 0,
  "warning_count": 1,
  "info_count": 4,
  "summary": "No secrets, no SAST vulnerabilities, no OWASP Top 10 issues detected. The health endpoint correctly removes version disclosure and uses minimal-surface JSON responses. One WARNING: the tasks.md spec artefact still references the removed version field and should be corrected to avoid misleading future implementers. All other findings are informational. PR is approved from a security perspective."
}

@github-actions

github-actions Bot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Quality Report

Repository ConductionNL/planix
Commit 1e363f0
Branch 95/merge
Event pull_request
Generated 2026-04-06 16:20 UTC
Workflow Run https://github.com/ConductionNL/planix/actions/runs/24039800421

Summary

Group Result
PHP Quality PASS
Vue Quality PASS
Security PASS
License PASS
PHPUnit PASS
Newman SKIP
Playwright SKIP

PHP Quality

Tool Result
lint PASS
phpcs PASS
phpmd PASS
psalm PASS
phpstan PASS
phpmetrics PASS

Vue Quality

Tool Result
eslint PASS
stylelint PASS

Security

Ecosystem Result
composer PASS
npm PASS

License Compliance

Ecosystem Result
composer PASS
npm PASS

composer dependencies (100 total)

Metric Count
Approved (allowlist) 100
Approved (override) 0
Denied 0

npm dependencies (215 total)

Metric Count
Approved (allowlist) 215
Approved (override) 0
Denied 0

PHPUnit Tests

PHP Nextcloud Result
Overall PASS

Code coverage: 0% (0 / 3 statements)

Integration Tests (Newman)

Newman integration tests were not enabled for this run.

E2E Tests (Playwright)

Playwright E2E tests were not enabled for this run.


Generated automatically by the Quality workflow.

Download the full PDF report from the workflow artifacts.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

[CRITICAL] Acceptance criteria in tasks.md is out of sync with the implementation

openspec/changes/status-api/tasks.md — Task 1 acceptance criteria:

- [x] `GET /api/health` returns JSON with `status`, `version`, and `openRegisterAvailable` fields

The version field was deliberately removed in commit 0c9cd94 (fix iteration 3) for a valid security reason (preventing passive fingerprinting on an unauthenticated endpoint). However, the acceptance criterion was never updated to reflect this intentional deviation.

The checkbox is marked complete, but the stated criterion is no longer satisfied. This breaks spec traceability — a future auditor reading tasks.md would conclude the endpoint returns three fields when it actually returns two.

Required fix: Update the acceptance criterion to:

- [x] `GET /api/health` returns JSON with `status` and `openRegisterAvailable` fields
       (`version` omitted intentionally — see security note in HealthController.php)

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

[WARNING] PR description is inaccurate — lists version field that was removed

The PR description (Summary section) states:

Returns JSON with status, version, and openRegisterAvailable fields.

The version field was removed in fix iteration 3. The PR body now misleads reviewers and will permanently misrepresent the change in the Git history.

Required fix: Update the PR description Summary to:

Returns JSON with status and openRegisterAvailable fields. Returns HTTP 200 with status ok when OpenRegister is enabled, or HTTP 503 with status degraded when unavailable. The version field was intentionally excluded to prevent passive fingerprinting on this unauthenticated endpoint.

Also update the Changes bullet for lib/Controller/HealthController.php to note the two-field (not three-field) contract.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

[SUGGESTION] Redundant test cases add noise without coverage value

tests/unit/Controller/HealthControllerTest.php

testResponseContainsRequiredFields() (line 130) and testDegradedResponseContainsRequiredFields() (line 150) only assert assertArrayHasKey for status and openRegisterAvailable. Both assertions are a strict subset of what the preceding tests already verify:

  • testIndexReturnsOkWhenOpenRegisterAvailable() already checks field presence via $data['status'] and $data['openRegisterAvailable']
  • testIndexReturnsDegradedWhenOpenRegisterUnavailable() does the same for the degraded path

Each of these redundant tests also sets up its own expects($this->once()) mock, consuming mock invocation budget without adding new assertions. Consider removing or merging them to keep the test suite focused.

This is informational — not required before merge.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

[SUGGESTION] isEnabledForUser() null-user behavior on public endpoint could be explicitly documented

lib/Controller/HealthController.php — line 78:

$openRegisterAvailable = $this->appManager->isEnabledForUser('openregister');

The existing comment explains the isEnabledForUser vs isInstalled distinction well. However, it doesn't address the null-user scenario: on a @PublicPage endpoint with no authenticated session, Nextcloud's AppManager::isEnabledForUser() with a null user falls back to checking whether the app is globally enabled. This is the correct and desired behaviour for a health probe, but the reasoning isn't documented.

Suggested addition to the comment:

// NOTE: isEnabledForUser() (not isInstalled()) is used intentionally here.
// ...existing comment...
// On this @PublicPage endpoint there is no authenticated user; Nextcloud's AppManager
// handles a null user by checking global enablement — equivalent to isEnabledForUser($id, null).

This is informational — not required before merge.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author
{
  "reviewer": "Juan Claude van Damme — Hydra Code Reviewer",
  "pr": 95,
  "repo": "ConductionNL/planix",
  "reviewed_at": "2026-04-06T16:20:00Z",
  "verdict": "REQUEST_CHANGES",
  "ci_status": {
    "lint": "pass",
    "phpcs": "pass",
    "phpstan": "pass",
    "psalm": "pass",
    "phpmd": "pass",
    "phpmetrics": "pass",
    "oas_version_sync": "pass",
    "vue_eslint": "pass",
    "vue_stylelint": "pass",
    "security_composer": "pass",
    "security_npm": "pass",
    "license_composer": "pass",
    "license_npm": "pass",
    "phpunit": "pending",
    "newman": "skipped",
    "e2e_playwright": "skipped"
  },
  "findings": [
    {
      "id": "F-01",
      "severity": "CRITICAL",
      "file": "openspec/changes/status-api/tasks.md",
      "title": "Acceptance criteria lists `version` field that was deliberately removed",
      "summary": "Task 1 acceptance criterion states the response includes `status`, `version`, and `openRegisterAvailable`. The `version` field was intentionally removed in commit 0c9cd94 (security: prevent passive fingerprinting). The criterion was not updated, breaking spec traceability.",
      "action_required": "Update acceptance criterion to reflect the two-field contract and document the security rationale inline."
    },
    {
      "id": "F-02",
      "severity": "WARNING",
      "file": "PR description (body)",
      "title": "PR description Summary incorrectly states the response includes `version`",
      "summary": "The PR Summary says 'returns JSON with `status`, `version`, and `openRegisterAvailable` fields'. The `version` field was removed. This permanently misrepresents the change in the GitHub record.",
      "action_required": "Update PR description Summary and the Changes bullet for HealthController.php to reflect the two-field contract."
    },
    {
      "id": "F-03",
      "severity": "SUGGESTION",
      "file": "tests/unit/Controller/HealthControllerTest.php",
      "lines": "130-163",
      "title": "testResponseContainsRequiredFields and testDegradedResponseContainsRequiredFields are subsets of the first two tests",
      "summary": "These two tests only assert assertArrayHasKey for both fields — a strict subset of what testIndexReturnsOkWhenOpenRegisterAvailable and testIndexReturnsDegradedWhenOpenRegisterUnavailable already cover. No new coverage is added.",
      "action_required": "None required before merge. Consider removing or merging in a follow-up."
    },
    {
      "id": "F-04",
      "severity": "SUGGESTION",
      "file": "lib/Controller/HealthController.php",
      "line": 78,
      "title": "null-user behavior of isEnabledForUser() on public endpoint is undocumented",
      "summary": "The existing comment explains isEnabledForUser vs isInstalled but omits the null-user path: on a @PublicPage endpoint, Nextcloud's AppManager treats null user as a global-enable check. This is correct but unexplained.",
      "action_required": "None required before merge. Consider adding a one-line note in a follow-up."
    }
  ],
  "summary": {
    "critical": 1,
    "warning": 1,
    "suggestion": 2,
    "overall": "The implementation is correct and all static-analysis / style CI checks pass. The CRITICAL and WARNING findings are both documentation/traceability issues (stale spec text and inaccurate PR description) introduced when `version` was legitimately removed for security. Fix both before merge."
  }
}

@rubenvdlinde rubenvdlinde deleted the feature/93/status-api branch April 6, 2026 17:16
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant