diff --git a/agent-system/runs/TEST-COVERAGE-POLICIES-001/execution-report.json b/agent-system/runs/TEST-COVERAGE-POLICIES-001/execution-report.json new file mode 100644 index 0000000..7ed3ea2 --- /dev/null +++ b/agent-system/runs/TEST-COVERAGE-POLICIES-001/execution-report.json @@ -0,0 +1,65 @@ +{ + "task_id": "TEST-COVERAGE-POLICIES-001", + "plan_ref": "agent-system/runs/TEST-COVERAGE-POLICIES-001/plan.json", + "status": "done", + "changed_files": [ + { + "path": "tests/Service/Access/OperationsAuthorizationPolicyTest.php", + "summary": "CREATED — S2: 17 tests covering supports() (known + unknown abilities), authorize() with zero/negative/missing actor_user_id, unsupported ability (500), allowIfHas pattern (5 tests across different abilities), authorizeUsersCreateCustomFields compound AND logic (3 tests), authorizeApiTokensSelfManage OR logic (3 tests). Uses AuthorizationPolicyTestSupport trait." + } + ], + "guard_evidence": [ + { + "guard_id": "GR-TEST-001", + "status": "pass", + "evidence": "17 test methods covering every public method (supports, authorize) with allowed + denied scenarios for all 3 authorization paths (allowIfHas, authorizeUsersCreateCustomFields, authorizeApiTokensSelfManage)." + }, + { + "guard_id": "GR-TEST-002", + "status": "pass", + "evidence": "Edge cases covered: zero actor_user_id, negative actor_user_id, missing actor_user_id, unsupported ability, missing both compound permissions, missing one of two required permissions, OR-logic with only first permission, OR-logic with only second permission." + }, + { + "guard_id": "GR-CORE-005", + "status": "pass", + "evidence": "OperationsAuthorizationPolicy follows the same pattern as the 6 existing tested policies: constructor-injected PermissionService, supports() + authorize() public interface, returns AuthorizationDecision. Test uses identical mock pattern (permissionGatewayAllowing)." + }, + { + "guard_id": "GR-LANG-001", + "status": "pass", + "evidence": "Test file follows PSR-4 namespace (MintyPHP\\Tests\\Service\\Access). CS Fixer reports 0 of 532 files need fixing." + }, + { + "guard_id": "GR-LANG-002", + "status": "pass", + "evidence": "PHPStan level 5: 0 new errors. 4 pre-existing errors in AuthzUiContractTest.php — verified pre-existing in prior tasks." + } + ], + "commands": [ + { "cmd": "docker compose exec php vendor/bin/phpunit tests/Service/Access/ --no-coverage", "result": "pass" }, + { "cmd": "docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress", "result": "pass" }, + { "cmd": "docker compose exec php vendor/bin/php-cs-fixer fix --dry-run --config=.php-cs-fixer.dist.php", "result": "pass" } + ], + "quality_gate_results": [ + { + "gate_id": "QG-001", + "result": "pass", + "notes": "93 tests in tests/Service/Access/ — all green. 17 new tests added for OperationsAuthorizationPolicy. All 7 policy test files pass (100% policy test coverage). 1 pre-existing failure (TranslationKeysTest) out of scope. This task introduced zero test failures." + }, + { + "gate_id": "QG-002", + "result": "pass", + "notes": "0 new PHPStan errors. 4 pre-existing errors in AuthzUiContractTest.php — verified pre-existing in prior tasks. This task introduced zero PHPStan errors." + }, + { + "gate_id": "QG-006", + "result": "pass", + "notes": "php-cs-fixer fix --dry-run: 0 of 532 files need fixing." + } + ], + "test_results": [ + { "name": "tests/Service/Access/OperationsAuthorizationPolicyTest.php", "result": "pass", "count": 17 }, + { "name": "tests/Service/Access/ (all 7 policy files)", "result": "pass", "count": 93 } + ], + "open_items": [] +} diff --git a/agent-system/runs/TEST-COVERAGE-POLICIES-001/finalize.json b/agent-system/runs/TEST-COVERAGE-POLICIES-001/finalize.json new file mode 100644 index 0000000..bf512bb --- /dev/null +++ b/agent-system/runs/TEST-COVERAGE-POLICIES-001/finalize.json @@ -0,0 +1,10 @@ +{ + "task_id": "TEST-COVERAGE-POLICIES-001", + "ready_to_finalize": true, + "guard_review": "pass", + "acceptance_review": "pass", + "ci_status": "pass", + "final_action": "commit", + "commit_message": "test(access): finalize operations authorization policy coverage", + "notes": "review-guards und review-acceptance sind PASS; execution-report dokumentiert QG-001/QG-002/QG-006 als PASS ohne task-introduzierte Fehler." +} diff --git a/agent-system/runs/TEST-COVERAGE-POLICIES-001/review-acceptance.json b/agent-system/runs/TEST-COVERAGE-POLICIES-001/review-acceptance.json new file mode 100644 index 0000000..1463c30 --- /dev/null +++ b/agent-system/runs/TEST-COVERAGE-POLICIES-001/review-acceptance.json @@ -0,0 +1,29 @@ +{ + "task_id": "TEST-COVERAGE-POLICIES-001", + "verdict": "pass", + "checked_criterion_ids": [ + "SC-001", + "SC-002", + "SC-003" + ], + "checks": [ + { + "criterion_id": "SC-001", + "criterion": "OperationsAuthorizationPolicyTest exists and covers every public method with allowed + denied scenarios.", + "result": "pass", + "evidence": "tests/Service/Access/OperationsAuthorizationPolicyTest.php exists and contains 17 test methods, including supports() true/false and authorize() allow/deny paths (simple permission checks, compound AND, OR, unsupported ability, and actor_user_id edge cases)." + }, + { + "criterion_id": "SC-002", + "criterion": "Test uses AuthorizationPolicyTestSupport trait for consistent assertion patterns.", + "result": "pass", + "evidence": "The test class explicitly uses AuthorizationPolicyTestSupport and applies trait assertions like assertForbiddenDecision and assertDeniedDecision." + }, + { + "criterion_id": "SC-003", + "criterion": "All 7 policy test files pass green (100% policy coverage).", + "result": "pass", + "evidence": "execution-report.json shows QG-001 pass with 93 passing tests in tests/Service/Access/ and explicitly states all 7 policy test files are green with 100% policy test coverage." + } + ] +} diff --git a/agent-system/runs/TEST-COVERAGE-POLICIES-001/review-guards.json b/agent-system/runs/TEST-COVERAGE-POLICIES-001/review-guards.json new file mode 100644 index 0000000..8f2fd27 --- /dev/null +++ b/agent-system/runs/TEST-COVERAGE-POLICIES-001/review-guards.json @@ -0,0 +1,17 @@ +{ + "task_id": "TEST-COVERAGE-POLICIES-001", + "verdict": "pass", + "checked_guard_ids": [ + "GR-TEST-001", + "GR-TEST-002", + "GR-CORE-005", + "GR-LANG-001", + "GR-LANG-002" + ], + "checked_quality_gate_ids": [ + "QG-001", + "QG-002", + "QG-006" + ], + "findings": [] +} diff --git a/tests/Service/Access/OperationsAuthorizationPolicyTest.php b/tests/Service/Access/OperationsAuthorizationPolicyTest.php new file mode 100644 index 0000000..a02c777 --- /dev/null +++ b/tests/Service/Access/OperationsAuthorizationPolicyTest.php @@ -0,0 +1,237 @@ +createMock(PermissionService::class)); + + $this->assertTrue($policy->supports(OperationsAuthorizationPolicy::ABILITY_ADDRESS_BOOK_VIEW)); + $this->assertTrue($policy->supports(OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_VIEW)); + $this->assertTrue($policy->supports(OperationsAuthorizationPolicy::ABILITY_API_TENANTS_DELETE)); + $this->assertTrue($policy->supports(OperationsAuthorizationPolicy::ABILITY_API_TOKENS_SELF_MANAGE)); + $this->assertTrue($policy->supports(OperationsAuthorizationPolicy::ABILITY_ADMIN_USERS_CREATE_EDIT_CUSTOM_FIELDS)); + } + + public function testSupportsReturnsFalseForUnknownAbility(): void + { + $policy = new OperationsAuthorizationPolicy($this->createMock(PermissionService::class)); + + $this->assertFalse($policy->supports('ops.nonexistent')); + $this->assertFalse($policy->supports('admin.roles.view')); + $this->assertFalse($policy->supports('')); + } + + // --- authorize: zero/negative actor_user_id --- + + public function testAuthorizeDeniesWhenActorUserIdIsZero(): void + { + $permissionService = $this->createMock(PermissionService::class); + $permissionService->expects($this->never())->method('userHas'); + + $policy = new OperationsAuthorizationPolicy($permissionService); + $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_ADDRESS_BOOK_VIEW, [ + 'actor_user_id' => 0, + ]); + + $this->assertForbiddenDecision($decision); + } + + public function testAuthorizeDeniesWhenActorUserIdIsMissing(): void + { + $permissionService = $this->createMock(PermissionService::class); + $permissionService->expects($this->never())->method('userHas'); + + $policy = new OperationsAuthorizationPolicy($permissionService); + $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_ADMIN_STATS_VIEW, []); + + $this->assertForbiddenDecision($decision); + } + + public function testAuthorizeDeniesWhenActorUserIdIsNegative(): void + { + $permissionService = $this->createMock(PermissionService::class); + $permissionService->expects($this->never())->method('userHas'); + + $policy = new OperationsAuthorizationPolicy($permissionService); + $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_ADMIN_STATS_VIEW, [ + 'actor_user_id' => -1, + ]); + + $this->assertForbiddenDecision($decision); + } + + // --- authorize: unsupported ability --- + + public function testAuthorizeReturns500ForUnsupportedAbility(): void + { + $policy = new OperationsAuthorizationPolicy($this->createMock(PermissionService::class)); + $decision = $policy->authorize('ops.nonexistent', ['actor_user_id' => 1]); + + $this->assertDeniedDecision($decision, 500, 'authorization_ability_not_supported'); + } + + // --- authorize: allowIfHas (simple permission check) --- + + public function testAddressBookViewAllowedWithPermission(): void + { + $permissionService = $this->permissionGatewayAllowing([ + 5 => [PermissionService::ADDRESS_BOOK_VIEW], + ]); + + $policy = new OperationsAuthorizationPolicy($permissionService); + $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_ADDRESS_BOOK_VIEW, [ + 'actor_user_id' => 5, + ]); + + $this->assertTrue($decision->isAllowed()); + } + + public function testAddressBookViewDeniedWithoutPermission(): void + { + $permissionService = $this->permissionGatewayAllowing([5 => []]); + + $policy = new OperationsAuthorizationPolicy($permissionService); + $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_ADDRESS_BOOK_VIEW, [ + 'actor_user_id' => 5, + ]); + + $this->assertForbiddenDecision($decision); + } + + public function testJobsViewAllowedWithPermission(): void + { + $permissionService = $this->permissionGatewayAllowing([ + 10 => [PermissionService::JOBS_VIEW], + ]); + + $policy = new OperationsAuthorizationPolicy($permissionService); + $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_ADMIN_JOBS_VIEW, [ + 'actor_user_id' => 10, + ]); + + $this->assertTrue($decision->isAllowed()); + } + + public function testApiTenantsDeleteDeniedWithoutPermission(): void + { + $permissionService = $this->permissionGatewayAllowing([8 => []]); + + $policy = new OperationsAuthorizationPolicy($permissionService); + $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_API_TENANTS_DELETE, [ + 'actor_user_id' => 8, + ]); + + $this->assertForbiddenDecision($decision); + } + + public function testSystemAuditPurgeAllowedWithPermission(): void + { + $permissionService = $this->permissionGatewayAllowing([ + 3 => [PermissionService::SYSTEM_AUDIT_PURGE], + ]); + + $policy = new OperationsAuthorizationPolicy($permissionService); + $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_ADMIN_SYSTEM_AUDIT_PURGE, [ + 'actor_user_id' => 3, + ]); + + $this->assertTrue($decision->isAllowed()); + } + + // --- authorize: authorizeUsersCreateCustomFields (requires TWO permissions) --- + + public function testUsersCreateCustomFieldsAllowedWithBothPermissions(): void + { + $permissionService = $this->permissionGatewayAllowing([ + 7 => [PermissionService::CUSTOM_FIELDS_EDIT_VALUES, PermissionService::USERS_UPDATE_ASSIGNMENTS], + ]); + + $policy = new OperationsAuthorizationPolicy($permissionService); + $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_ADMIN_USERS_CREATE_EDIT_CUSTOM_FIELDS, [ + 'actor_user_id' => 7, + ]); + + $this->assertTrue($decision->isAllowed()); + } + + public function testUsersCreateCustomFieldsDeniedWithoutCustomFieldsPermission(): void + { + $permissionService = $this->permissionGatewayAllowing([ + 7 => [PermissionService::USERS_UPDATE_ASSIGNMENTS], + ]); + + $policy = new OperationsAuthorizationPolicy($permissionService); + $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_ADMIN_USERS_CREATE_EDIT_CUSTOM_FIELDS, [ + 'actor_user_id' => 7, + ]); + + $this->assertForbiddenDecision($decision); + } + + public function testUsersCreateCustomFieldsDeniedWithoutAssignmentsPermission(): void + { + $permissionService = $this->permissionGatewayAllowing([ + 7 => [PermissionService::CUSTOM_FIELDS_EDIT_VALUES], + ]); + + $policy = new OperationsAuthorizationPolicy($permissionService); + $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_ADMIN_USERS_CREATE_EDIT_CUSTOM_FIELDS, [ + 'actor_user_id' => 7, + ]); + + $this->assertForbiddenDecision($decision); + } + + // --- authorize: authorizeApiTokensSelfManage (requires EITHER permission) --- + + public function testApiTokensSelfManageAllowedWithSelfUpdatePermission(): void + { + $permissionService = $this->permissionGatewayAllowing([ + 9 => [PermissionService::USERS_SELF_UPDATE], + ]); + + $policy = new OperationsAuthorizationPolicy($permissionService); + $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_API_TOKENS_SELF_MANAGE, [ + 'actor_user_id' => 9, + ]); + + $this->assertTrue($decision->isAllowed()); + } + + public function testApiTokensSelfManageAllowedWithApiTokensManagePermission(): void + { + $permissionService = $this->permissionGatewayAllowing([ + 9 => [PermissionService::API_TOKENS_MANAGE], + ]); + + $policy = new OperationsAuthorizationPolicy($permissionService); + $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_API_TOKENS_SELF_MANAGE, [ + 'actor_user_id' => 9, + ]); + + $this->assertTrue($decision->isAllowed()); + } + + public function testApiTokensSelfManageDeniedWithoutEitherPermission(): void + { + $permissionService = $this->permissionGatewayAllowing([9 => []]); + + $policy = new OperationsAuthorizationPolicy($permissionService); + $decision = $policy->authorize(OperationsAuthorizationPolicy::ABILITY_API_TOKENS_SELF_MANAGE, [ + 'actor_user_id' => 9, + ]); + + $this->assertForbiddenDecision($decision); + } +}