workspace = Workspace::factory()->create(); $this->service = app(AgentApiKeyService::class); } // ========================================================================= // Key Creation Tests // ========================================================================= public function test_create_returns_agent_api_key(): void { $key = $this->service->create( $this->workspace, 'Test Key' ); $this->assertInstanceOf(AgentApiKey::class, $key); $this->assertNotNull($key->plainTextKey); } public function test_create_with_workspace_id(): void { $key = $this->service->create( $this->workspace->id, 'Test Key' ); $this->assertEquals($this->workspace->id, $key->workspace_id); } public function test_create_with_permissions(): void { $permissions = [ AgentApiKey::PERM_PLANS_READ, AgentApiKey::PERM_PLANS_WRITE, ]; $key = $this->service->create( $this->workspace, 'Test Key', $permissions ); $this->assertEquals($permissions, $key->permissions); } public function test_create_with_custom_rate_limit(): void { $key = $this->service->create( $this->workspace, 'Test Key', [], 500 ); $this->assertEquals(500, $key->rate_limit); } public function test_create_with_expiry(): void { $expiresAt = Carbon::now()->addMonth(); $key = $this->service->create( $this->workspace, 'Test Key', [], 100, $expiresAt ); $this->assertEquals( $expiresAt->toDateTimeString(), $key->expires_at->toDateTimeString() ); } // ========================================================================= // Key Validation Tests // ========================================================================= public function test_validate_returns_key_for_valid_key(): void { $key = $this->service->create($this->workspace, 'Test Key'); $plainKey = $key->plainTextKey; $result = $this->service->validate($plainKey); $this->assertNotNull($result); $this->assertEquals($key->id, $result->id); } public function test_validate_returns_null_for_invalid_key(): void { $result = $this->service->validate('ak_invalid_key_here'); $this->assertNull($result); } public function test_validate_returns_null_for_revoked_key(): void { $key = $this->service->create($this->workspace, 'Test Key'); $plainKey = $key->plainTextKey; $key->revoke(); $result = $this->service->validate($plainKey); $this->assertNull($result); } public function test_validate_returns_null_for_expired_key(): void { $key = $this->service->create( $this->workspace, 'Test Key', [], 100, Carbon::now()->subDay() ); $plainKey = $key->plainTextKey; $result = $this->service->validate($plainKey); $this->assertNull($result); } // ========================================================================= // Permission Check Tests // ========================================================================= public function test_check_permission_returns_true_when_granted(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $result = $this->service->checkPermission($key, AgentApiKey::PERM_PLANS_READ); $this->assertTrue($result); } public function test_check_permission_returns_false_when_not_granted(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $result = $this->service->checkPermission($key, AgentApiKey::PERM_PLANS_WRITE); $this->assertFalse($result); } public function test_check_permission_returns_false_for_inactive_key(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $key->revoke(); $result = $this->service->checkPermission($key, AgentApiKey::PERM_PLANS_READ); $this->assertFalse($result); } public function test_check_permissions_returns_true_when_all_granted(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ, AgentApiKey::PERM_PLANS_WRITE] ); $result = $this->service->checkPermissions($key, [ AgentApiKey::PERM_PLANS_READ, AgentApiKey::PERM_PLANS_WRITE, ]); $this->assertTrue($result); } public function test_check_permissions_returns_false_when_missing_one(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $result = $this->service->checkPermissions($key, [ AgentApiKey::PERM_PLANS_READ, AgentApiKey::PERM_PLANS_WRITE, ]); $this->assertFalse($result); } // ========================================================================= // Rate Limiting Tests // ========================================================================= public function test_record_usage_increments_cache_counter(): void { $key = $this->service->create($this->workspace, 'Test Key'); Cache::forget("agent_api_key_rate:{$key->id}"); $this->service->recordUsage($key); $this->service->recordUsage($key); $this->service->recordUsage($key); $this->assertEquals(3, Cache::get("agent_api_key_rate:{$key->id}")); } public function test_record_usage_records_client_ip(): void { $key = $this->service->create($this->workspace, 'Test Key'); $this->service->recordUsage($key, '192.168.1.100'); $this->assertEquals('192.168.1.100', $key->fresh()->last_used_ip); } public function test_record_usage_updates_last_used_at(): void { $key = $this->service->create($this->workspace, 'Test Key'); $this->service->recordUsage($key); $this->assertNotNull($key->fresh()->last_used_at); } public function test_is_rate_limited_returns_false_under_limit(): void { $key = $this->service->create($this->workspace, 'Test Key', [], 100); Cache::put("agent_api_key_rate:{$key->id}", 50, 60); $result = $this->service->isRateLimited($key); $this->assertFalse($result); } public function test_is_rate_limited_returns_true_at_limit(): void { $key = $this->service->create($this->workspace, 'Test Key', [], 100); Cache::put("agent_api_key_rate:{$key->id}", 100, 60); $result = $this->service->isRateLimited($key); $this->assertTrue($result); } public function test_is_rate_limited_returns_true_over_limit(): void { $key = $this->service->create($this->workspace, 'Test Key', [], 100); Cache::put("agent_api_key_rate:{$key->id}", 150, 60); $result = $this->service->isRateLimited($key); $this->assertTrue($result); } public function test_get_rate_limit_status_returns_correct_values(): void { $key = $this->service->create($this->workspace, 'Test Key', [], 100); Cache::put("agent_api_key_rate:{$key->id}", 30, 60); $status = $this->service->getRateLimitStatus($key); $this->assertEquals(100, $status['limit']); $this->assertEquals(70, $status['remaining']); $this->assertEquals(30, $status['used']); $this->assertArrayHasKey('reset_in_seconds', $status); } // ========================================================================= // Key Management Tests // ========================================================================= public function test_revoke_sets_revoked_at(): void { $key = $this->service->create($this->workspace, 'Test Key'); $this->service->revoke($key); $this->assertNotNull($key->fresh()->revoked_at); } public function test_revoke_clears_rate_limit_cache(): void { $key = $this->service->create($this->workspace, 'Test Key'); Cache::put("agent_api_key_rate:{$key->id}", 50, 60); $this->service->revoke($key); $this->assertNull(Cache::get("agent_api_key_rate:{$key->id}")); } public function test_update_permissions_changes_permissions(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $this->service->updatePermissions($key, [AgentApiKey::PERM_SESSIONS_WRITE]); $fresh = $key->fresh(); $this->assertFalse($fresh->hasPermission(AgentApiKey::PERM_PLANS_READ)); $this->assertTrue($fresh->hasPermission(AgentApiKey::PERM_SESSIONS_WRITE)); } public function test_update_rate_limit_changes_limit(): void { $key = $this->service->create($this->workspace, 'Test Key', [], 100); $this->service->updateRateLimit($key, 500); $this->assertEquals(500, $key->fresh()->rate_limit); } public function test_extend_expiry_updates_expiry(): void { $key = $this->service->create( $this->workspace, 'Test Key', [], 100, Carbon::now()->addDay() ); $newExpiry = Carbon::now()->addMonth(); $this->service->extendExpiry($key, $newExpiry); $this->assertEquals( $newExpiry->toDateTimeString(), $key->fresh()->expires_at->toDateTimeString() ); } public function test_remove_expiry_clears_expiry(): void { $key = $this->service->create( $this->workspace, 'Test Key', [], 100, Carbon::now()->addDay() ); $this->service->removeExpiry($key); $this->assertNull($key->fresh()->expires_at); } // ========================================================================= // IP Restriction Tests // ========================================================================= public function test_update_ip_restrictions_sets_values(): void { $key = $this->service->create($this->workspace, 'Test Key'); $this->service->updateIpRestrictions($key, true, ['192.168.1.1', '10.0.0.0/8']); $fresh = $key->fresh(); $this->assertTrue($fresh->ip_restriction_enabled); $this->assertEquals(['192.168.1.1', '10.0.0.0/8'], $fresh->ip_whitelist); } public function test_enable_ip_restrictions_enables_with_whitelist(): void { $key = $this->service->create($this->workspace, 'Test Key'); $this->service->enableIpRestrictions($key, ['192.168.1.1']); $fresh = $key->fresh(); $this->assertTrue($fresh->ip_restriction_enabled); $this->assertEquals(['192.168.1.1'], $fresh->ip_whitelist); } public function test_disable_ip_restrictions_disables(): void { $key = $this->service->create($this->workspace, 'Test Key'); $this->service->enableIpRestrictions($key, ['192.168.1.1']); $this->service->disableIpRestrictions($key); $this->assertFalse($key->fresh()->ip_restriction_enabled); } public function test_is_ip_allowed_returns_true_when_restrictions_disabled(): void { $key = $this->service->create($this->workspace, 'Test Key'); $result = $this->service->isIpAllowed($key, '192.168.1.100'); $this->assertTrue($result); } public function test_is_ip_allowed_returns_true_when_ip_in_whitelist(): void { $key = $this->service->create($this->workspace, 'Test Key'); $this->service->enableIpRestrictions($key, ['192.168.1.100']); $result = $this->service->isIpAllowed($key->fresh(), '192.168.1.100'); $this->assertTrue($result); } public function test_is_ip_allowed_returns_false_when_ip_not_in_whitelist(): void { $key = $this->service->create($this->workspace, 'Test Key'); $this->service->enableIpRestrictions($key, ['192.168.1.100']); $result = $this->service->isIpAllowed($key->fresh(), '10.0.0.1'); $this->assertFalse($result); } public function test_is_ip_allowed_supports_cidr_ranges(): void { $key = $this->service->create($this->workspace, 'Test Key'); $this->service->enableIpRestrictions($key, ['192.168.1.0/24']); $fresh = $key->fresh(); $this->assertTrue($this->service->isIpAllowed($fresh, '192.168.1.50')); $this->assertTrue($this->service->isIpAllowed($fresh, '192.168.1.254')); $this->assertFalse($this->service->isIpAllowed($fresh, '192.168.2.1')); } public function test_parse_ip_whitelist_input_parses_valid_input(): void { $input = "192.168.1.1\n192.168.1.2\n10.0.0.0/8"; $result = $this->service->parseIpWhitelistInput($input); $this->assertEmpty($result['errors']); $this->assertCount(3, $result['entries']); $this->assertContains('192.168.1.1', $result['entries']); $this->assertContains('192.168.1.2', $result['entries']); $this->assertContains('10.0.0.0/8', $result['entries']); } public function test_parse_ip_whitelist_input_returns_errors_for_invalid(): void { $input = "192.168.1.1\ninvalid_ip\n10.0.0.0/8"; $result = $this->service->parseIpWhitelistInput($input); $this->assertCount(1, $result['errors']); $this->assertCount(2, $result['entries']); } // ========================================================================= // Workspace Query Tests // ========================================================================= public function test_get_active_keys_for_workspace_returns_active_only(): void { $active = $this->service->create($this->workspace, 'Active Key'); $revoked = $this->service->create($this->workspace, 'Revoked Key'); $revoked->revoke(); $this->service->create( $this->workspace, 'Expired Key', [], 100, Carbon::now()->subDay() ); $keys = $this->service->getActiveKeysForWorkspace($this->workspace); $this->assertCount(1, $keys); $this->assertEquals('Active Key', $keys->first()->name); } public function test_get_active_keys_for_workspace_filters_by_workspace(): void { $otherWorkspace = Workspace::factory()->create(); $this->service->create($this->workspace, 'Our Key'); $this->service->create($otherWorkspace, 'Their Key'); $keys = $this->service->getActiveKeysForWorkspace($this->workspace); $this->assertCount(1, $keys); $this->assertEquals('Our Key', $keys->first()->name); } public function test_get_all_keys_for_workspace_returns_all(): void { $this->service->create($this->workspace, 'Active Key'); $revoked = $this->service->create($this->workspace, 'Revoked Key'); $revoked->revoke(); $this->service->create( $this->workspace, 'Expired Key', [], 100, Carbon::now()->subDay() ); $keys = $this->service->getAllKeysForWorkspace($this->workspace); $this->assertCount(3, $keys); } // ========================================================================= // Validate With Permission Tests // ========================================================================= public function test_validate_with_permission_returns_key_when_valid(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $plainKey = $key->plainTextKey; $result = $this->service->validateWithPermission($plainKey, AgentApiKey::PERM_PLANS_READ); $this->assertNotNull($result); $this->assertEquals($key->id, $result->id); } public function test_validate_with_permission_returns_null_for_invalid_key(): void { $result = $this->service->validateWithPermission( 'ak_invalid_key', AgentApiKey::PERM_PLANS_READ ); $this->assertNull($result); } public function test_validate_with_permission_returns_null_without_permission(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_SESSIONS_READ] ); $plainKey = $key->plainTextKey; $result = $this->service->validateWithPermission($plainKey, AgentApiKey::PERM_PLANS_READ); $this->assertNull($result); } public function test_validate_with_permission_returns_null_when_rate_limited(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ], 100 ); $plainKey = $key->plainTextKey; Cache::put("agent_api_key_rate:{$key->id}", 150, 60); $result = $this->service->validateWithPermission($plainKey, AgentApiKey::PERM_PLANS_READ); $this->assertNull($result); } // ========================================================================= // Full Authentication Flow Tests // ========================================================================= public function test_authenticate_returns_success_for_valid_key(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $plainKey = $key->plainTextKey; $result = $this->service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ); $this->assertTrue($result['success']); $this->assertInstanceOf(AgentApiKey::class, $result['key']); $this->assertEquals($this->workspace->id, $result['workspace_id']); $this->assertArrayHasKey('rate_limit', $result); } public function test_authenticate_returns_error_for_invalid_key(): void { $result = $this->service->authenticate( 'ak_invalid_key', AgentApiKey::PERM_PLANS_READ ); $this->assertFalse($result['success']); $this->assertEquals('invalid_key', $result['error']); } public function test_authenticate_returns_error_for_revoked_key(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $plainKey = $key->plainTextKey; $key->revoke(); $result = $this->service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ); $this->assertFalse($result['success']); $this->assertEquals('key_revoked', $result['error']); } public function test_authenticate_returns_error_for_expired_key(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ], 100, Carbon::now()->subDay() ); $plainKey = $key->plainTextKey; $result = $this->service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ); $this->assertFalse($result['success']); $this->assertEquals('key_expired', $result['error']); } public function test_authenticate_returns_error_for_missing_permission(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_SESSIONS_READ] ); $plainKey = $key->plainTextKey; $result = $this->service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ); $this->assertFalse($result['success']); $this->assertEquals('permission_denied', $result['error']); } public function test_authenticate_returns_error_when_rate_limited(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ], 100 ); $plainKey = $key->plainTextKey; Cache::put("agent_api_key_rate:{$key->id}", 150, 60); $result = $this->service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ); $this->assertFalse($result['success']); $this->assertEquals('rate_limited', $result['error']); $this->assertArrayHasKey('rate_limit', $result); } public function test_authenticate_checks_ip_restrictions(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $plainKey = $key->plainTextKey; $this->service->enableIpRestrictions($key, ['192.168.1.100']); $result = $this->service->authenticate( $plainKey, AgentApiKey::PERM_PLANS_READ, '10.0.0.1' ); $this->assertFalse($result['success']); $this->assertEquals('ip_not_allowed', $result['error']); } public function test_authenticate_allows_whitelisted_ip(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $plainKey = $key->plainTextKey; $this->service->enableIpRestrictions($key, ['192.168.1.100']); $result = $this->service->authenticate( $plainKey, AgentApiKey::PERM_PLANS_READ, '192.168.1.100' ); $this->assertTrue($result['success']); $this->assertEquals('192.168.1.100', $result['client_ip']); } public function test_authenticate_records_usage_on_success(): void { $key = $this->service->create( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $plainKey = $key->plainTextKey; Cache::forget("agent_api_key_rate:{$key->id}"); $this->service->authenticate( $plainKey, AgentApiKey::PERM_PLANS_READ, '192.168.1.50' ); $fresh = $key->fresh(); $this->assertEquals(1, $fresh->call_count); $this->assertNotNull($fresh->last_used_at); $this->assertEquals('192.168.1.50', $fresh->last_used_ip); } public function test_authenticate_does_not_record_usage_on_failure(): void { $key = $this->service->create( $this->workspace, 'Test Key', [] ); $plainKey = $key->plainTextKey; $this->service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ); $this->assertEquals(0, $key->fresh()->call_count); } }