workspace = Workspace::factory()->create(); } // ========================================================================= // Key Generation Tests // ========================================================================= public function test_it_generates_key_with_correct_prefix(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $this->assertStringStartsWith('ak_', $key->plainTextKey); $this->assertEquals(35, strlen($key->plainTextKey)); // ak_ + 32 random chars } public function test_it_stores_hashed_key_with_argon2id(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); // Argon2id hashes start with $argon2id$ $this->assertStringStartsWith('$argon2id$', $key->key); } public function test_plaintext_key_is_only_available_once(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $plainKey = $key->plainTextKey; $this->assertNotNull($plainKey); // After fetching from database, plaintext should be null $freshKey = AgentApiKey::find($key->id); $this->assertNull($freshKey->plainTextKey); } public function test_it_generates_key_with_workspace_id(): void { $key = AgentApiKey::generate( $this->workspace->id, 'Test Key' ); $this->assertEquals($this->workspace->id, $key->workspace_id); } public function test_it_generates_key_with_workspace_model(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $this->assertEquals($this->workspace->id, $key->workspace_id); } public function test_it_generates_key_with_permissions(): void { $permissions = [ AgentApiKey::PERM_PLANS_READ, AgentApiKey::PERM_PLANS_WRITE, ]; $key = AgentApiKey::generate( $this->workspace, 'Test Key', $permissions ); $this->assertEquals($permissions, $key->permissions); } public function test_it_generates_key_with_custom_rate_limit(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 500 ); $this->assertEquals(500, $key->rate_limit); } public function test_it_generates_key_with_expiry(): void { $expiresAt = Carbon::now()->addDays(30); $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 100, $expiresAt ); $this->assertEquals($expiresAt->toDateTimeString(), $key->expires_at->toDateTimeString()); } public function test_it_initialises_call_count_to_zero(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $this->assertEquals(0, $key->call_count); } // ========================================================================= // Key Lookup Tests // ========================================================================= public function test_find_by_key_returns_correct_key(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $plainKey = $key->plainTextKey; $found = AgentApiKey::findByKey($plainKey); $this->assertNotNull($found); $this->assertEquals($key->id, $found->id); } public function test_find_by_key_returns_null_for_invalid_key(): void { AgentApiKey::generate( $this->workspace, 'Test Key' ); $found = AgentApiKey::findByKey('ak_invalid_key_that_does_not_exist'); $this->assertNull($found); } public function test_find_by_key_returns_null_for_malformed_key(): void { $this->assertNull(AgentApiKey::findByKey('')); $this->assertNull(AgentApiKey::findByKey('invalid')); $this->assertNull(AgentApiKey::findByKey('ak_short')); } public function test_find_by_key_does_not_find_revoked_keys(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $plainKey = $key->plainTextKey; $key->revoke(); $found = AgentApiKey::findByKey($plainKey); $this->assertNull($found); } public function test_find_by_key_does_not_find_expired_keys(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 100, Carbon::now()->subDay() ); $plainKey = $key->plainTextKey; $found = AgentApiKey::findByKey($plainKey); $this->assertNull($found); } public function test_verify_key_returns_true_for_matching_key(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $plainKey = $key->plainTextKey; $this->assertTrue($key->verifyKey($plainKey)); } public function test_verify_key_returns_false_for_non_matching_key(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $this->assertFalse($key->verifyKey('ak_wrong_key_entirely')); } // ========================================================================= // Status Tests // ========================================================================= public function test_is_active_returns_true_for_fresh_key(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $this->assertTrue($key->isActive()); } public function test_is_active_returns_false_for_revoked_key(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $key->revoke(); $this->assertFalse($key->isActive()); } public function test_is_active_returns_false_for_expired_key(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 100, Carbon::now()->subDay() ); $this->assertFalse($key->isActive()); } public function test_is_active_returns_true_for_key_with_future_expiry(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 100, Carbon::now()->addDay() ); $this->assertTrue($key->isActive()); } public function test_is_revoked_returns_correct_value(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $this->assertFalse($key->isRevoked()); $key->revoke(); $this->assertTrue($key->isRevoked()); } public function test_is_expired_returns_correct_value(): void { $notExpired = AgentApiKey::generate( $this->workspace, 'Not Expired', [], 100, Carbon::now()->addDay() ); $expired = AgentApiKey::generate( $this->workspace, 'Expired', [], 100, Carbon::now()->subDay() ); $noExpiry = AgentApiKey::generate( $this->workspace, 'No Expiry' ); $this->assertFalse($notExpired->isExpired()); $this->assertTrue($expired->isExpired()); $this->assertFalse($noExpiry->isExpired()); } // ========================================================================= // Permission Tests // ========================================================================= public function test_has_permission_returns_true_when_granted(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ, AgentApiKey::PERM_PLANS_WRITE] ); $this->assertTrue($key->hasPermission(AgentApiKey::PERM_PLANS_READ)); $this->assertTrue($key->hasPermission(AgentApiKey::PERM_PLANS_WRITE)); } public function test_has_permission_returns_false_when_not_granted(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $this->assertFalse($key->hasPermission(AgentApiKey::PERM_PLANS_WRITE)); } public function test_has_any_permission_returns_true_when_one_matches(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $this->assertTrue($key->hasAnyPermission([ AgentApiKey::PERM_PLANS_READ, AgentApiKey::PERM_PLANS_WRITE, ])); } public function test_has_any_permission_returns_false_when_none_match(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [AgentApiKey::PERM_TEMPLATES_READ] ); $this->assertFalse($key->hasAnyPermission([ AgentApiKey::PERM_PLANS_READ, AgentApiKey::PERM_PLANS_WRITE, ])); } public function test_has_all_permissions_returns_true_when_all_granted(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ, AgentApiKey::PERM_PLANS_WRITE, AgentApiKey::PERM_SESSIONS_READ] ); $this->assertTrue($key->hasAllPermissions([ AgentApiKey::PERM_PLANS_READ, AgentApiKey::PERM_PLANS_WRITE, ])); } public function test_has_all_permissions_returns_false_when_missing_one(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $this->assertFalse($key->hasAllPermissions([ AgentApiKey::PERM_PLANS_READ, AgentApiKey::PERM_PLANS_WRITE, ])); } public function test_update_permissions_changes_permissions(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $key->updatePermissions([AgentApiKey::PERM_SESSIONS_WRITE]); $this->assertFalse($key->hasPermission(AgentApiKey::PERM_PLANS_READ)); $this->assertTrue($key->hasPermission(AgentApiKey::PERM_SESSIONS_WRITE)); } // ========================================================================= // Rate Limiting Tests // ========================================================================= public function test_is_rate_limited_returns_false_when_under_limit(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 100 ); Cache::put("agent_api_key_rate:{$key->id}", 50, 60); $this->assertFalse($key->isRateLimited()); } public function test_is_rate_limited_returns_true_when_at_limit(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 100 ); Cache::put("agent_api_key_rate:{$key->id}", 100, 60); $this->assertTrue($key->isRateLimited()); } public function test_is_rate_limited_returns_true_when_over_limit(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 100 ); Cache::put("agent_api_key_rate:{$key->id}", 150, 60); $this->assertTrue($key->isRateLimited()); } public function test_get_recent_call_count_returns_cache_value(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); Cache::put("agent_api_key_rate:{$key->id}", 42, 60); $this->assertEquals(42, $key->getRecentCallCount()); } public function test_get_recent_call_count_returns_zero_when_not_cached(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $this->assertEquals(0, $key->getRecentCallCount()); } public function test_get_remaining_calls_returns_correct_value(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 100 ); Cache::put("agent_api_key_rate:{$key->id}", 30, 60); $this->assertEquals(70, $key->getRemainingCalls()); } public function test_get_remaining_calls_returns_zero_when_over_limit(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 100 ); Cache::put("agent_api_key_rate:{$key->id}", 150, 60); $this->assertEquals(0, $key->getRemainingCalls()); } public function test_update_rate_limit_changes_limit(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 100 ); $key->updateRateLimit(200); $this->assertEquals(200, $key->fresh()->rate_limit); } // ========================================================================= // IP Restriction Tests // ========================================================================= public function test_ip_restrictions_disabled_by_default(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $this->assertFalse($key->ip_restriction_enabled); } public function test_enable_ip_restriction_sets_flag(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $key->enableIpRestriction(); $this->assertTrue($key->fresh()->ip_restriction_enabled); } public function test_disable_ip_restriction_clears_flag(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $key->enableIpRestriction(); $key->disableIpRestriction(); $this->assertFalse($key->fresh()->ip_restriction_enabled); } public function test_update_ip_whitelist_sets_list(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $key->updateIpWhitelist(['192.168.1.1', '10.0.0.0/8']); $this->assertEquals(['192.168.1.1', '10.0.0.0/8'], $key->fresh()->ip_whitelist); } public function test_add_to_ip_whitelist_adds_entry(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $key->updateIpWhitelist(['192.168.1.1']); $key->addToIpWhitelist('10.0.0.1'); $this->assertContains('192.168.1.1', $key->fresh()->ip_whitelist); $this->assertContains('10.0.0.1', $key->fresh()->ip_whitelist); } public function test_add_to_ip_whitelist_does_not_duplicate(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $key->updateIpWhitelist(['192.168.1.1']); $key->addToIpWhitelist('192.168.1.1'); $this->assertCount(1, $key->fresh()->ip_whitelist); } public function test_remove_from_ip_whitelist_removes_entry(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $key->updateIpWhitelist(['192.168.1.1', '10.0.0.1']); $key->removeFromIpWhitelist('192.168.1.1'); $whitelist = $key->fresh()->ip_whitelist; $this->assertNotContains('192.168.1.1', $whitelist); $this->assertContains('10.0.0.1', $whitelist); } public function test_has_ip_restrictions_returns_correct_value(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); // No restrictions $this->assertFalse($key->hasIpRestrictions()); // Enabled but no whitelist $key->enableIpRestriction(); $this->assertFalse($key->fresh()->hasIpRestrictions()); // Enabled with whitelist $key->updateIpWhitelist(['192.168.1.1']); $this->assertTrue($key->fresh()->hasIpRestrictions()); } public function test_get_ip_whitelist_count_returns_correct_value(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $this->assertEquals(0, $key->getIpWhitelistCount()); $key->updateIpWhitelist(['192.168.1.1', '10.0.0.0/8', '172.16.0.0/12']); $this->assertEquals(3, $key->fresh()->getIpWhitelistCount()); } public function test_record_last_used_ip_stores_ip(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $key->recordLastUsedIp('192.168.1.100'); $this->assertEquals('192.168.1.100', $key->fresh()->last_used_ip); } // ========================================================================= // Actions Tests // ========================================================================= public function test_revoke_sets_revoked_at(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $key->revoke(); $this->assertNotNull($key->fresh()->revoked_at); } public function test_record_usage_increments_count(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $key->recordUsage(); $key->recordUsage(); $key->recordUsage(); $this->assertEquals(3, $key->fresh()->call_count); } public function test_record_usage_updates_last_used_at(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $this->assertNull($key->last_used_at); $key->recordUsage(); $this->assertNotNull($key->fresh()->last_used_at); } public function test_extend_expiry_updates_expiry(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 100, Carbon::now()->addDay() ); $newExpiry = Carbon::now()->addMonth(); $key->extendExpiry($newExpiry); $this->assertEquals( $newExpiry->toDateTimeString(), $key->fresh()->expires_at->toDateTimeString() ); } public function test_remove_expiry_clears_expiry(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 100, Carbon::now()->addDay() ); $key->removeExpiry(); $this->assertNull($key->fresh()->expires_at); } // ========================================================================= // Scope Tests // ========================================================================= public function test_active_scope_filters_correctly(): void { AgentApiKey::generate($this->workspace, 'Active Key'); $revoked = AgentApiKey::generate($this->workspace, 'Revoked Key'); $revoked->revoke(); AgentApiKey::generate($this->workspace, 'Expired Key', [], 100, Carbon::now()->subDay()); $activeKeys = AgentApiKey::active()->get(); $this->assertCount(1, $activeKeys); $this->assertEquals('Active Key', $activeKeys->first()->name); } public function test_for_workspace_scope_filters_correctly(): void { $otherWorkspace = Workspace::factory()->create(); AgentApiKey::generate($this->workspace, 'Our Key'); AgentApiKey::generate($otherWorkspace, 'Their Key'); $ourKeys = AgentApiKey::forWorkspace($this->workspace)->get(); $this->assertCount(1, $ourKeys); $this->assertEquals('Our Key', $ourKeys->first()->name); } public function test_revoked_scope_filters_correctly(): void { AgentApiKey::generate($this->workspace, 'Active Key'); $revoked = AgentApiKey::generate($this->workspace, 'Revoked Key'); $revoked->revoke(); $revokedKeys = AgentApiKey::revoked()->get(); $this->assertCount(1, $revokedKeys); $this->assertEquals('Revoked Key', $revokedKeys->first()->name); } public function test_expired_scope_filters_correctly(): void { AgentApiKey::generate($this->workspace, 'Active Key'); AgentApiKey::generate($this->workspace, 'Expired Key', [], 100, Carbon::now()->subDay()); $expiredKeys = AgentApiKey::expired()->get(); $this->assertCount(1, $expiredKeys); $this->assertEquals('Expired Key', $expiredKeys->first()->name); } // ========================================================================= // Display Helper Tests // ========================================================================= public function test_get_masked_key_returns_masked_format(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $masked = $key->getMaskedKey(); $this->assertStringStartsWith('ak_', $masked); $this->assertStringEndsWith('...', $masked); } public function test_get_status_label_returns_correct_label(): void { $active = AgentApiKey::generate($this->workspace, 'Active'); $revoked = AgentApiKey::generate($this->workspace, 'Revoked'); $revoked->revoke(); $expired = AgentApiKey::generate($this->workspace, 'Expired', [], 100, Carbon::now()->subDay()); $this->assertEquals('Active', $active->getStatusLabel()); $this->assertEquals('Revoked', $revoked->getStatusLabel()); $this->assertEquals('Expired', $expired->getStatusLabel()); } public function test_get_status_colour_returns_correct_colour(): void { $active = AgentApiKey::generate($this->workspace, 'Active'); $revoked = AgentApiKey::generate($this->workspace, 'Revoked'); $revoked->revoke(); $expired = AgentApiKey::generate($this->workspace, 'Expired', [], 100, Carbon::now()->subDay()); $this->assertEquals('green', $active->getStatusColor()); $this->assertEquals('red', $revoked->getStatusColor()); $this->assertEquals('amber', $expired->getStatusColor()); } public function test_get_last_used_for_humans_returns_never_when_null(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $this->assertEquals('Never', $key->getLastUsedForHumans()); } public function test_get_last_used_for_humans_returns_diff_when_set(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $key->update(['last_used_at' => Carbon::now()->subHour()]); $this->assertStringContainsString('ago', $key->getLastUsedForHumans()); } public function test_get_expires_for_humans_returns_never_when_null(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $this->assertEquals('Never', $key->getExpiresForHumans()); } public function test_get_expires_for_humans_returns_expired_when_past(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 100, Carbon::now()->subDay() ); $this->assertStringContainsString('Expired', $key->getExpiresForHumans()); } public function test_get_expires_for_humans_returns_expires_when_future(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [], 100, Carbon::now()->addDay() ); $this->assertStringContainsString('Expires', $key->getExpiresForHumans()); } // ========================================================================= // Array Output Tests // ========================================================================= public function test_to_array_includes_expected_keys(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key', [AgentApiKey::PERM_PLANS_READ] ); $array = $key->toArray(); $this->assertArrayHasKey('id', $array); $this->assertArrayHasKey('workspace_id', $array); $this->assertArrayHasKey('name', $array); $this->assertArrayHasKey('permissions', $array); $this->assertArrayHasKey('rate_limit', $array); $this->assertArrayHasKey('call_count', $array); $this->assertArrayHasKey('status', $array); $this->assertArrayHasKey('ip_restriction_enabled', $array); $this->assertArrayHasKey('ip_whitelist_count', $array); // Should NOT include the key hash $this->assertArrayNotHasKey('key', $array); } // ========================================================================= // Available Permissions Tests // ========================================================================= public function test_available_permissions_returns_all_permissions(): void { $permissions = AgentApiKey::availablePermissions(); $this->assertIsArray($permissions); $this->assertArrayHasKey(AgentApiKey::PERM_PLANS_READ, $permissions); $this->assertArrayHasKey(AgentApiKey::PERM_PLANS_WRITE, $permissions); $this->assertArrayHasKey(AgentApiKey::PERM_SESSIONS_READ, $permissions); $this->assertArrayHasKey(AgentApiKey::PERM_SESSIONS_WRITE, $permissions); $this->assertArrayHasKey(AgentApiKey::PERM_NOTIFY_READ, $permissions); $this->assertArrayHasKey(AgentApiKey::PERM_NOTIFY_WRITE, $permissions); $this->assertArrayHasKey(AgentApiKey::PERM_NOTIFY_SEND, $permissions); } // ========================================================================= // Relationship Tests // ========================================================================= public function test_belongs_to_workspace(): void { $key = AgentApiKey::generate( $this->workspace, 'Test Key' ); $this->assertInstanceOf(Workspace::class, $key->workspace); $this->assertEquals($this->workspace->id, $key->workspace->id); } }