diff --git a/lib/Controller/HealthController.php b/lib/Controller/HealthController.php index 42ebb8b2..b54dbf71 100644 --- a/lib/Controller/HealthController.php +++ b/lib/Controller/HealthController.php @@ -33,6 +33,8 @@ /** * Controller for health check endpoints. * + * @spec openspec/changes/prometheus-metrics/tasks.md#task-4 + * * @psalm-suppress UnusedClass */ class HealthController extends Controller @@ -57,6 +59,8 @@ public function __construct( /** * Health check endpoint. * + * @spec openspec/changes/prometheus-metrics/tasks.md#task-4 + * * @NoCSRFRequired * * @return JSONResponse Health status @@ -126,6 +130,8 @@ private function checkDatabase(): string * OpenRegister is a hard dependency for Procest. If it is not enabled, * the overall health status MUST be "error". * + * @spec openspec/changes/prometheus-metrics/tasks.md#task-4 + * * @return string 'ok' or error message */ private function checkOpenRegister(): string diff --git a/lib/Controller/MetricsController.php b/lib/Controller/MetricsController.php index 56ecbb54..0f9aa60f 100644 --- a/lib/Controller/MetricsController.php +++ b/lib/Controller/MetricsController.php @@ -33,6 +33,10 @@ /** * Controller for exposing Prometheus metrics. * + * @spec openspec/changes/prometheus-metrics/tasks.md#task-1 + * @spec openspec/changes/prometheus-metrics/tasks.md#task-2 + * @spec openspec/changes/prometheus-metrics/tasks.md#task-3 + * * @psalm-suppress UnusedClass */ class MetricsController extends Controller @@ -67,6 +71,10 @@ public function __construct( /** * Return Prometheus metrics in text exposition format. * + * @spec openspec/changes/prometheus-metrics/tasks.md#task-1 + * @spec openspec/changes/prometheus-metrics/tasks.md#task-2 + * @spec openspec/changes/prometheus-metrics/tasks.md#task-3 + * * @NoCSRFRequired * * @return TextPlainResponse Prometheus-formatted metrics @@ -83,6 +91,10 @@ public function index(): TextPlainResponse /** * Collect all metrics and format as Prometheus text. * + * @spec openspec/changes/prometheus-metrics/tasks.md#task-1 + * @spec openspec/changes/prometheus-metrics/tasks.md#task-2 + * @spec openspec/changes/prometheus-metrics/tasks.md#task-3 + * * @return string Prometheus exposition format text */ private function collectMetrics(): string @@ -200,6 +212,8 @@ private function collectMetrics(): string * @param callable $compute Callable that computes the value on cache miss * * @return mixed The cached or freshly computed value + * + * @spec openspec/changes/prometheus-metrics/tasks.md#task-3 */ private function getCached(string $key, int $ttl, callable $compute): mixed { diff --git a/openspec/changes/prometheus-metrics/hydra.json b/openspec/changes/prometheus-metrics/hydra.json new file mode 100644 index 00000000..fd86d6e6 --- /dev/null +++ b/openspec/changes/prometheus-metrics/hydra.json @@ -0,0 +1,523 @@ +{ + "spec_slug": "prometheus-metrics", + "app": "procest", + "repo": "ConductionNL/procest", + "issue": 212, + "depends_on": [], + "schema_version": 2, + "cycles": [ + { + "cycle": 1, + "trigger": "build:queued", + "started_at": "2026-04-18T20:32:47Z", + "ended_at": null, + "outcome": "aborted", + "outcome_reason": "no terminal label seen in timeline", + "pattern_tags": [ + "reviewer-skipped-full-suite" + ], + "stages": [ + { + "stage": "build", + "persona": "Al Gorithm", + "model": "haiku", + "container": "hydra-builder", + "started_at": "2026-04-18T20:32:47Z", + "ended_at": "2026-04-18T20:32:48Z", + "turns_used": 53, + "turns_budget": 200, + "cost_usd": 0.3818, + "checks_run": [ + "composer check:strict" + ], + "checks_skipped": [], + "findings": [ + { + "id": "b-builder-build-0", + "severity": "WARNING", + "gate": null, + "rule": "builder output flagged failure language", + "status": "open", + "note": "Perfect! I have successfully implemented the prometheus-metrics OpenSpec change. Here's what was completed:\n\n## Implementation Summary\n\n**PR Created:** [#251 on ConductionNL/procest](https://github.com/ConductionNL/procest/pull/251)\n\n### Changes Made:\n\n1. **MetricsController Enhancements**\n - Added `nextcloud_version` label to `procest_info` gauge\n - Implemented `procest_cases_created_today` metric\n - Added `getCached()` method with APCu caching (30s TTL for standard queries, 60s for overd", + "autofixable": false + } + ], + "decisions": [], + "verdict": "fail" + }, + { + "stage": "pre-review-quality", + "persona": "orchestrator", + "container": "hydra-quality-runner", + "started_at": "2026-04-18T20:32:47Z", + "ended_at": "2026-04-18T20:32:48Z", + "exit_code": 0, + "checks_run": [ + "php-lint", + "phpcs", + "phpmd", + "psalm", + "phpstan", + "phpmetrics", + "composer-audit", + "spdx-headers", + "forbidden-patterns", + "eslint", + "stylelint", + "npm-audit", + "phpunit" + ], + "checks_skipped": [ + "publiccode", + "gitleaks", + "trivy", + "newman" + ], + "gates": { + "php-lint": { + "pass": true, + "failures": 0 + }, + "phpcs": { + "pass": true, + "failures": 0 + }, + "phpmd": { + "pass": true, + "failures": 0 + }, + "psalm": { + "pass": true, + "failures": 0 + }, + "phpstan": { + "pass": true, + "failures": 0 + }, + "phpmetrics": { + "pass": true, + "failures": 0 + }, + "composer-audit": { + "pass": true, + "failures": 0 + }, + "spdx-headers": { + "pass": true, + "failures": 0 + }, + "forbidden-patterns": { + "pass": true, + "failures": 0 + }, + "eslint": { + "pass": true, + "failures": 0 + }, + "stylelint": { + "pass": true, + "failures": 0 + }, + "npm-audit": { + "pass": true, + "failures": 0 + }, + "phpunit": { + "pass": true, + "failures": 0 + } + }, + "findings": [], + "verdict": "pass" + }, + { + "stage": "code-review", + "persona": "Juan Claude van Damme", + "model": "sonnet", + "container": "hydra-reviewer", + "started_at": "2026-04-18T20:41:58Z", + "ended_at": "2026-04-18T20:41:59Z", + "turns_used": 41, + "turns_budget": 40, + "cost_usd": 1.254, + "checks_run": [ + "composer check:strict", + "phpcs" + ], + "checks_skipped": [ + "hydra-gates" + ], + "findings": [], + "verdict": "none" + }, + { + "stage": "security-review", + "persona": "Clyde Barcode", + "model": "sonnet", + "container": "hydra-security", + "started_at": "2026-04-18T20:48:55Z", + "ended_at": "2026-04-18T20:48:56Z", + "turns_used": 34, + "turns_budget": 40, + "cost_usd": 0.8284, + "checks_run": [], + "checks_skipped": [ + "hydra-gates", + "composer check:strict" + ], + "findings": [ + { + "id": "SEC-01", + "severity": "CRITICAL", + "gate": "OWASP A01:2021", + "file": "lib/Controller/HealthController.php", + "line": 66, + "rule": "OWASP A01:2021", + "status": "open", + "note": "Without @PublicPage, Nextcloud auth middleware blocks all unauthenticated requests. Container-orchestration health probes receive a 302 redirect to the login page, making the endpoint non-functional. Fix: add @PublicPage above @NoCSRFRequired in the method docblock. Could not apply: container has no write access to repository files (root-owned).", + "autofixable": false + }, + { + "id": "SEC-02", + "severity": "CRITICAL", + "gate": "OWASP A01:2021", + "file": "lib/Controller/MetricsController.php", + "line": 74, + "rule": "OWASP A01:2021", + "status": "open", + "note": "Without @PublicPage, Nextcloud auth middleware blocks all Prometheus scrape requests. The entire /api/metrics endpoint is non-functional for unauthenticated scrapers. Fix: add @PublicPage above @NoCSRFRequired. Could not apply: container has no write access to repository files (root-owned).", + "autofixable": false + }, + { + "id": "SEC-03", + "severity": "WARNING", + "gate": "CWE-209 / OWASP A05:2021", + "file": "lib/Controller/HealthController.php", + "line": 121, + "rule": "CWE-209 / OWASP A05:2021", + "status": "open", + "note": "checkDatabase() (line 121), checkOpenRegister() (line 145), and checkFilesystem() (line 167) return $e->getMessage() verbatim in the response body. Once @PublicPage is applied, any unauthenticated caller can observe DB connection-string fragments, table names, or file paths. Fix: return generic strings ('failed: database unavailable', etc.) and ensure $this->logger records the full error. checkFilesystem catch block is also missing a logger call. Could not apply: container has no write access to", + "autofixable": false + } + ], + "verdict": "fail" + } + ] + }, + { + "cycle": 2, + "trigger": "build:queued", + "started_at": "2026-04-18T20:53:35Z", + "ended_at": "2026-04-20T21:52:50Z", + "outcome": "aborted", + "outcome_reason": "rebuild:queued \u2014 human wiped prior cycle", + "pattern_tags": [], + "stages": [ + { + "stage": "quality-recheck", + "persona": "orchestrator", + "container": "hydra-quality-runner", + "started_at": "2026-04-18T20:53:35Z", + "ended_at": "2026-04-18T20:53:36Z", + "exit_code": 1, + "checks_run": [ + "phpcs", + "phpmd", + "psalm", + "phpstan", + "phpmetrics", + "composer-audit", + "spdx-headers", + "forbidden-patterns", + "eslint", + "npm-audit", + "phpunit" + ], + "checks_skipped": [ + "php-lint", + "publiccode", + "stylelint", + "gitleaks", + "trivy", + "newman" + ], + "gates": { + "phpcs": { + "pass": false, + "failures": 1 + }, + "phpmd": { + "pass": true, + "failures": 0 + }, + "psalm": { + "pass": true, + "failures": 0 + }, + "phpstan": { + "pass": true, + "failures": 0 + }, + "phpmetrics": { + "pass": true, + "failures": 0 + }, + "composer-audit": { + "pass": true, + "failures": 0 + }, + "spdx-headers": { + "pass": true, + "failures": 0 + }, + "forbidden-patterns": { + "pass": false, + "failures": 1 + }, + "eslint": { + "pass": false, + "failures": 1 + }, + "npm-audit": { + "pass": true, + "failures": 0 + }, + "phpunit": { + "pass": false, + "failures": 1 + } + }, + "findings": [ + { + "id": "qr-quality-recheck-phpcs", + "severity": "CRITICAL", + "gate": "phpcs", + "rule": "phpcs gate failing", + "status": "open", + "note": "phpcs reported status=fail in quality-recheck.json", + "autofixable": true + }, + { + "id": "qr-quality-recheck-forbidden-patterns", + "severity": "CRITICAL", + "gate": "forbidden-patterns", + "rule": "forbidden-patterns gate failing", + "status": "open", + "note": "forbidden-patterns reported status=fail in quality-recheck.json", + "autofixable": true + }, + { + "id": "qr-quality-recheck-eslint", + "severity": "CRITICAL", + "gate": "eslint", + "rule": "eslint gate failing", + "status": "open", + "note": "eslint reported status=fail in quality-recheck.json", + "autofixable": true + }, + { + "id": "qr-quality-recheck-phpunit", + "severity": "CRITICAL", + "gate": "phpunit", + "rule": "phpunit gate failing", + "status": "open", + "note": "phpunit reported status=fail in quality-recheck.json", + "autofixable": false + } + ], + "verdict": "fail" + } + ] + }, + { + "cycle": 3, + "trigger": "build:queued", + "started_at": "2026-04-21T04:44:27Z", + "ended_at": "2026-04-21T07:54:07Z", + "outcome": "needs-input", + "outcome_reason": "deterministic checks still failing \u2014 reviewers ran but applied no fixes, builder output remains broken", + "pattern_tags": [ + "browser-test-nc-setup-failed", + "reviewer-ran-applied-no-fixes" + ], + "stages": [ + { + "stage": "build", + "persona": "Al Gorithm", + "model": "haiku", + "container": "hydra-builder", + "started_at": "2026-04-20T21:53:32Z", + "ended_at": "2026-04-21T04:44:24Z", + "exit_code": 0, + "turns_used": 125, + "turns_budget": 40, + "checks_run": [], + "checks_skipped": [], + "findings": [], + "decisions": [], + "verdict": "pass" + }, + { + "stage": "pre-review-quality", + "persona": "orchestrator", + "container": "hydra-quality-runner", + "started_at": "2026-04-21T07:27:08Z", + "ended_at": "2026-04-21T07:27:08Z", + "exit_code": 1, + "checks_run": [ + "php -l", + "composer check:strict (phpcs)", + "composer check:strict (phpmd)", + "composer check:strict (psalm)", + "composer check:strict (phpstan)", + "phpmetrics", + "composer audit", + "spdx-headers", + "forbidden-patterns", + "npm run lint (eslint)", + "npm run lint (stylelint)", + "npm audit" + ], + "checks_skipped": [ + "publiccode", + "stub-scan", + "gitleaks", + "trivy", + "composer test:unit (phpunit)", + "newman" + ], + "gates": { + "php-lint": "pass", + "phpcs": "fail", + "phpmd": "pass", + "psalm": "pass", + "phpstan": "pass", + "phpmetrics": "pass", + "composer-audit": "pass", + "spdx-headers": "pass", + "publiccode": "skip", + "forbidden-patterns": "pass", + "eslint": "fail", + "stylelint": "fail", + "npm-audit": "pass", + "stub-scan": "skip", + "gitleaks": "skip", + "trivy": "skip", + "phpunit": "skip", + "newman": "skip" + }, + "findings": [ + { + "id": "prq-phpcs", + "severity": "WARNING", + "gate": "phpcs", + "rule": "composer check:strict (phpcs) failing", + "status": "open", + "note": "...\nThe repository at \"/server/apps/app\" does not have the correct ownership and git refuses to use it:\n\nfatal: detected dubious ownership in repository at '/server/apps/app'\nTo add an exception for this directory, call:\n\ngit config --global --add safe.directory /server/apps/app\n\nComposer could not detect the root package (conductionnl/procest) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version\n\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[31mE\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m", + "autofixable": true + }, + { + "id": "prq-eslint", + "severity": "WARNING", + "gate": "eslint", + "rule": "npm run lint (eslint) failing", + "status": "open", + "note": "...\n\n> procest@0.1.0 lint\n> eslint src\n\nsh: 1: eslint: not found", + "autofixable": true + }, + { + "id": "prq-stylelint", + "severity": "WARNING", + "gate": "stylelint", + "rule": "npm run lint (stylelint) failing", + "status": "open", + "note": "...\n\n> procest@0.1.0 stylelint\n> stylelint src/**/*.vue src/**/*.scss src/**/*.css\n\nsh: 1: stylelint: not found", + "autofixable": true + } + ], + "verdict": "fail" + }, + { + "stage": "quality-recheck", + "persona": "orchestrator", + "container": "hydra-quality-runner", + "started_at": "2026-04-21T07:53:58Z", + "ended_at": "2026-04-21T07:53:58Z", + "exit_code": 1, + "checks_run": [ + "php -l", + "composer check:strict (phpcs)", + "composer check:strict (phpmd)", + "composer check:strict (psalm)", + "composer check:strict (phpstan)", + "phpmetrics", + "composer audit", + "spdx-headers", + "forbidden-patterns", + "npm run lint (eslint)", + "npm run lint (stylelint)", + "npm audit" + ], + "checks_skipped": [ + "publiccode", + "stub-scan", + "gitleaks", + "trivy", + "composer test:unit (phpunit)", + "newman" + ], + "gates": { + "php-lint": "pass", + "phpcs": "fail", + "phpmd": "pass", + "psalm": "pass", + "phpstan": "pass", + "phpmetrics": "pass", + "composer-audit": "pass", + "spdx-headers": "pass", + "publiccode": "skip", + "forbidden-patterns": "pass", + "eslint": "fail", + "stylelint": "fail", + "npm-audit": "pass", + "stub-scan": "skip", + "gitleaks": "skip", + "trivy": "skip", + "phpunit": "skip", + "newman": "skip" + }, + "findings": [ + { + "id": "qrc-phpcs", + "severity": "WARNING", + "gate": "phpcs", + "rule": "composer check:strict (phpcs) failing", + "status": "open", + "note": "...\nThe repository at \"/server/apps/repo\" does not have the correct ownership and git refuses to use it:\n\nfatal: detected dubious ownership in repository at '/server/apps/repo'\nTo add an exception for this directory, call:\n\ngit config --global --add safe.directory /server/apps/repo\n\nComposer could not detect the root package (conductionnl/procest) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version\n\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m.\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m.\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33mW\u001b[0m\u001b[33m", + "autofixable": true + }, + { + "id": "qrc-eslint", + "severity": "WARNING", + "gate": "eslint", + "rule": "npm run lint (eslint) failing", + "status": "open", + "note": "...\n\n> procest@0.1.0 lint\n> eslint src\n\nsh: 1: eslint: not found", + "autofixable": true + }, + { + "id": "qrc-stylelint", + "severity": "WARNING", + "gate": "stylelint", + "rule": "npm run lint (stylelint) failing", + "status": "open", + "note": "...\n\n> procest@0.1.0 stylelint\n> stylelint src/**/*.vue src/**/*.scss src/**/*.css\n\nsh: 1: stylelint: not found", + "autofixable": true + } + ], + "verdict": "fail" + } + ] + } + ] +} diff --git a/tests/Unit/Controller/HealthControllerTest.php b/tests/Unit/Controller/HealthControllerTest.php index d14025a0..c5de6ccf 100644 --- a/tests/Unit/Controller/HealthControllerTest.php +++ b/tests/Unit/Controller/HealthControllerTest.php @@ -32,6 +32,8 @@ /** * Unit tests for the HealthController class. * + * @spec openspec/changes/prometheus-metrics/tasks.md#task-6 + * * @covers \OCA\Procest\Controller\HealthController */ class HealthControllerTest extends TestCase @@ -72,7 +74,6 @@ class HealthControllerTest extends TestCase */ private HealthController $controller; - /** * Set up test fixtures. * @@ -80,21 +81,20 @@ class HealthControllerTest extends TestCase */ protected function setUp(): void { - $this->request = $this->createMock(IRequest::class); - $this->db = $this->createMock(IDBConnection::class); - $this->appManager = $this->createMock(IAppManager::class); - $this->logger = $this->createMock(LoggerInterface::class); + $this->request = $this->createMock(originalClassName: IRequest::class); + $this->db = $this->createMock(originalClassName: IDBConnection::class); + $this->appManager = $this->createMock(originalClassName: IAppManager::class); + $this->logger = $this->createMock(originalClassName: LoggerInterface::class); $this->controller = new HealthController( - $this->request, - $this->db, - $this->appManager, - $this->logger, + request: $this->request, + db: $this->db, + appManager: $this->appManager, + logger: $this->logger, ); }//end setUp() - /** * Test healthy system returns 200 with ok status. * @@ -102,8 +102,8 @@ protected function setUp(): void */ public function testHealthySystemReturnsOk(): void { - $qbMock = $this->createMock(\OCP\DB\QueryBuilder\IQueryBuilder::class); - $resultMock = $this->createMock(\OCP\DB\IResult::class); + $qbMock = $this->createMock(originalClassName: \OCP\DB\QueryBuilder\IQueryBuilder::class); + $resultMock = $this->createMock(originalClassName: \OCP\DB\IResult::class); $qbMock->method('select')->willReturnSelf(); $qbMock->method('createFunction')->willReturn('1'); @@ -122,15 +122,14 @@ public function testHealthySystemReturnsOk(): void $response = $this->controller->index(); $data = $response->getData(); - $this->assertSame(Http::STATUS_OK, $response->getStatus()); - $this->assertSame('ok', $data['status']); - $this->assertSame('ok', $data['checks']['database']); - $this->assertSame('ok', $data['checks']['openregister']); - $this->assertSame('ok', $data['checks']['filesystem']); + $this->assertSame(expected: Http::STATUS_OK, actual: $response->getStatus()); + $this->assertSame(expected: 'ok', actual: $data['status']); + $this->assertSame(expected: 'ok', actual: $data['checks']['database']); + $this->assertSame(expected: 'ok', actual: $data['checks']['openregister']); + $this->assertSame(expected: 'ok', actual: $data['checks']['filesystem']); }//end testHealthySystemReturnsOk() - /** * Test that OpenRegister unavailable results in error status. * @@ -138,8 +137,8 @@ public function testHealthySystemReturnsOk(): void */ public function testOpenRegisterUnavailableReturnsError(): void { - $qbMock = $this->createMock(\OCP\DB\QueryBuilder\IQueryBuilder::class); - $resultMock = $this->createMock(\OCP\DB\IResult::class); + $qbMock = $this->createMock(originalClassName: \OCP\DB\QueryBuilder\IQueryBuilder::class); + $resultMock = $this->createMock(originalClassName: \OCP\DB\IResult::class); $qbMock->method('select')->willReturnSelf(); $qbMock->method('createFunction')->willReturn('1'); @@ -158,14 +157,13 @@ public function testOpenRegisterUnavailableReturnsError(): void $response = $this->controller->index(); $data = $response->getData(); - $this->assertSame(Http::STATUS_SERVICE_UNAVAILABLE, $response->getStatus()); - $this->assertSame('error', $data['status']); - $this->assertSame('ok', $data['checks']['database']); - $this->assertSame('failed: app not enabled', $data['checks']['openregister']); + $this->assertSame(expected: Http::STATUS_SERVICE_UNAVAILABLE, actual: $response->getStatus()); + $this->assertSame(expected: 'error', actual: $data['status']); + $this->assertSame(expected: 'ok', actual: $data['checks']['database']); + $this->assertSame(expected: 'failed: app not enabled', actual: $data['checks']['openregister']); }//end testOpenRegisterUnavailableReturnsError() - /** * Test that database unreachable results in error status. * @@ -186,13 +184,12 @@ public function testDatabaseUnreachableReturnsError(): void $response = $this->controller->index(); $data = $response->getData(); - $this->assertSame(Http::STATUS_SERVICE_UNAVAILABLE, $response->getStatus()); - $this->assertSame('error', $data['status']); - $this->assertStringContainsString('failed:', $data['checks']['database']); + $this->assertSame(expected: Http::STATUS_SERVICE_UNAVAILABLE, actual: $response->getStatus()); + $this->assertSame(expected: 'error', actual: $data['status']); + $this->assertStringContainsString(needle: 'failed:', haystack: $data['checks']['database']); }//end testDatabaseUnreachableReturnsError() - /** * Test that the response includes version information. * @@ -212,9 +209,7 @@ public function testResponseIncludesVersion(): void $response = $this->controller->index(); $data = $response->getData(); - $this->assertSame('1.2.3', $data['version']); + $this->assertSame(expected: '1.2.3', actual: $data['version']); }//end testResponseIncludesVersion() - - }//end class diff --git a/tests/Unit/Controller/MetricsControllerTest.php b/tests/Unit/Controller/MetricsControllerTest.php index 8ae1dc05..9bf1b7bb 100644 --- a/tests/Unit/Controller/MetricsControllerTest.php +++ b/tests/Unit/Controller/MetricsControllerTest.php @@ -31,6 +31,8 @@ /** * Unit tests for the MetricsController class. * + * @spec openspec/changes/prometheus-metrics/tasks.md#task-5 + * * @covers \OCA\Procest\Controller\MetricsController */ class MetricsControllerTest extends TestCase @@ -71,7 +73,6 @@ class MetricsControllerTest extends TestCase */ private MetricsController $controller; - /** * Set up test fixtures. * @@ -79,21 +80,20 @@ class MetricsControllerTest extends TestCase */ protected function setUp(): void { - $this->request = $this->createMock(IRequest::class); - $this->db = $this->createMock(IDBConnection::class); - $this->appManager = $this->createMock(IAppManager::class); - $this->logger = $this->createMock(LoggerInterface::class); + $this->request = $this->createMock(originalClassName: IRequest::class); + $this->db = $this->createMock(originalClassName: IDBConnection::class); + $this->appManager = $this->createMock(originalClassName: IAppManager::class); + $this->logger = $this->createMock(originalClassName: LoggerInterface::class); $this->controller = new MetricsController( - $this->request, - $this->db, - $this->appManager, - $this->logger, + request: $this->request, + db: $this->db, + appManager: $this->appManager, + logger: $this->logger, ); }//end setUp() - /** * Test that the index method returns a TextPlainResponse. * @@ -110,15 +110,14 @@ public function testIndexReturnsTextPlainResponse(): void $response = $this->controller->index(); - $this->assertSame(200, $response->getStatus()); + $this->assertSame(expected: 200, actual: $response->getStatus()); $headers = $response->getHeaders(); - $this->assertArrayHasKey('Content-Type', $headers); - $this->assertSame('text/plain; version=0.0.4; charset=utf-8', $headers['Content-Type']); + $this->assertArrayHasKey(key: 'Content-Type', array: $headers); + $this->assertSame(expected: 'text/plain; version=0.0.4; charset=utf-8', actual: $headers['Content-Type']); }//end testIndexReturnsTextPlainResponse() - /** * Test that the metrics output contains the expected metric families. * @@ -136,21 +135,20 @@ public function testMetricsContainsExpectedFamilies(): void $content = $response->render(); // Verify required metric families are present. - $this->assertStringContainsString('# HELP procest_info Application information', $content); - $this->assertStringContainsString('# TYPE procest_info gauge', $content); - $this->assertStringContainsString('# HELP procest_up Whether the application is healthy', $content); - $this->assertStringContainsString('# TYPE procest_up gauge', $content); - $this->assertStringContainsString('# HELP procest_cases_total', $content); - $this->assertStringContainsString('# TYPE procest_cases_total gauge', $content); - $this->assertStringContainsString('# HELP procest_cases_overdue_total', $content); - $this->assertStringContainsString('# HELP procest_cases_created_today', $content); - $this->assertStringContainsString('# TYPE procest_cases_created_today gauge', $content); - $this->assertStringContainsString('# HELP procest_tasks_total', $content); - $this->assertStringContainsString('# HELP procest_tasks_overdue_total', $content); + $this->assertStringContainsString(needle: '# HELP procest_info Application information', haystack: $content); + $this->assertStringContainsString(needle: '# TYPE procest_info gauge', haystack: $content); + $this->assertStringContainsString(needle: '# HELP procest_up Whether the application is healthy', haystack: $content); + $this->assertStringContainsString(needle: '# TYPE procest_up gauge', haystack: $content); + $this->assertStringContainsString(needle: '# HELP procest_cases_total', haystack: $content); + $this->assertStringContainsString(needle: '# TYPE procest_cases_total gauge', haystack: $content); + $this->assertStringContainsString(needle: '# HELP procest_cases_overdue_total', haystack: $content); + $this->assertStringContainsString(needle: '# HELP procest_cases_created_today', haystack: $content); + $this->assertStringContainsString(needle: '# TYPE procest_cases_created_today gauge', haystack: $content); + $this->assertStringContainsString(needle: '# HELP procest_tasks_total', haystack: $content); + $this->assertStringContainsString(needle: '# HELP procest_tasks_overdue_total', haystack: $content); }//end testMetricsContainsExpectedFamilies() - /** * Test that the info gauge includes the nextcloud_version label. * @@ -169,13 +167,12 @@ public function testInfoGaugeIncludesNextcloudVersion(): void // The info line should contain nextcloud_version label. $this->assertMatchesRegularExpression( - '/procest_info\{.*nextcloud_version="[^"]*".*\} 1/', - $content + pattern: '/procest_info\{.*nextcloud_version="[^"]*".*\} 1/', + string: $content ); }//end testInfoGaugeIncludesNextcloudVersion() - /** * Test that procest_up is 0 when database is unreachable. * @@ -192,11 +189,10 @@ public function testUpGaugeReflectsDatabaseHealth(): void $response = $this->controller->index(); $content = $response->render(); - $this->assertStringContainsString('procest_up 0', $content); + $this->assertStringContainsString(needle: 'procest_up 0', haystack: $content); }//end testUpGaugeReflectsDatabaseHealth() - /** * Test that the cases_created_today metric has a valid format. * @@ -214,9 +210,7 @@ public function testCasesCreatedTodayMetricFormat(): void $content = $response->render(); // Should have a valid integer value (0 since DB is mocked to fail). - $this->assertStringContainsString('procest_cases_created_today 0', $content); + $this->assertStringContainsString(needle: 'procest_cases_created_today 0', haystack: $content); }//end testCasesCreatedTodayMetricFormat() - - }//end class