- Convert AgentApiKeyTest from PHPUnit class-based syntax to Pest functional syntax - Add tests/Pest.php configuration with helper functions (createWorkspace, createApiKey) - Organise tests using describe() blocks for better structure - Add additional test coverage for key rotation and security edge cases - Update TODO.md to reflect Pest syntax usage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
919 lines
29 KiB
PHP
919 lines
29 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Tests for the AgentApiKey model.
|
|
*
|
|
* Covers generation, validation, permissions, rate limiting, and IP restrictions.
|
|
*/
|
|
|
|
use Carbon\Carbon;
|
|
use Core\Mod\Agentic\Models\AgentApiKey;
|
|
use Core\Tenant\Models\Workspace;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
// =========================================================================
|
|
// Key Generation Tests
|
|
// =========================================================================
|
|
|
|
describe('key generation', function () {
|
|
it('generates key with correct prefix', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
expect($key->plainTextKey)
|
|
->toStartWith('ak_')
|
|
->toHaveLength(35); // ak_ + 32 random chars
|
|
});
|
|
|
|
it('stores hashed key with Argon2id', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
// Argon2id hashes start with $argon2id$
|
|
expect($key->key)->toStartWith('$argon2id$');
|
|
});
|
|
|
|
it('makes plaintext key available only once after creation', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
expect($key->plainTextKey)->not->toBeNull();
|
|
|
|
// After fetching from database, plaintext should be null
|
|
$freshKey = AgentApiKey::find($key->id);
|
|
|
|
expect($freshKey->plainTextKey)->toBeNull();
|
|
});
|
|
|
|
it('generates key with workspace ID', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace->id, 'Test Key');
|
|
|
|
expect($key->workspace_id)->toBe($workspace->id);
|
|
});
|
|
|
|
it('generates key with workspace model', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
expect($key->workspace_id)->toBe($workspace->id);
|
|
});
|
|
|
|
it('generates key with permissions', function () {
|
|
$workspace = createWorkspace();
|
|
$permissions = [
|
|
AgentApiKey::PERM_PLANS_READ,
|
|
AgentApiKey::PERM_PLANS_WRITE,
|
|
];
|
|
|
|
$key = AgentApiKey::generate($workspace, 'Test Key', $permissions);
|
|
|
|
expect($key->permissions)->toBe($permissions);
|
|
});
|
|
|
|
it('generates key with custom rate limit', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key', [], 500);
|
|
|
|
expect($key->rate_limit)->toBe(500);
|
|
});
|
|
|
|
it('generates key with expiry date', function () {
|
|
$workspace = createWorkspace();
|
|
$expiresAt = Carbon::now()->addDays(30);
|
|
|
|
$key = AgentApiKey::generate($workspace, 'Test Key', [], 100, $expiresAt);
|
|
|
|
expect($key->expires_at->toDateTimeString())
|
|
->toBe($expiresAt->toDateTimeString());
|
|
});
|
|
|
|
it('initialises call count to zero', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
expect($key->call_count)->toBe(0);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Key Lookup Tests
|
|
// =========================================================================
|
|
|
|
describe('key lookup', function () {
|
|
it('finds key by plaintext value', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
$found = AgentApiKey::findByKey($plainKey);
|
|
|
|
expect($found)
|
|
->not->toBeNull()
|
|
->and($found->id)->toBe($key->id);
|
|
});
|
|
|
|
it('returns null for invalid key', function () {
|
|
$workspace = createWorkspace();
|
|
AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
$found = AgentApiKey::findByKey('ak_invalid_key_that_does_not_exist');
|
|
|
|
expect($found)->toBeNull();
|
|
});
|
|
|
|
it('returns null for malformed key', function () {
|
|
expect(AgentApiKey::findByKey(''))->toBeNull();
|
|
expect(AgentApiKey::findByKey('invalid'))->toBeNull();
|
|
expect(AgentApiKey::findByKey('ak_short'))->toBeNull();
|
|
});
|
|
|
|
it('does not find revoked keys', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
$key->revoke();
|
|
|
|
$found = AgentApiKey::findByKey($plainKey);
|
|
|
|
expect($found)->toBeNull();
|
|
});
|
|
|
|
it('does not find expired keys', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[],
|
|
100,
|
|
Carbon::now()->subDay()
|
|
);
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
$found = AgentApiKey::findByKey($plainKey);
|
|
|
|
expect($found)->toBeNull();
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Key Verification Tests
|
|
// =========================================================================
|
|
|
|
describe('key verification', function () {
|
|
it('verifyKey returns true for matching key', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
expect($key->verifyKey($plainKey))->toBeTrue();
|
|
});
|
|
|
|
it('verifyKey returns false for non-matching key', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
expect($key->verifyKey('ak_wrong_key_entirely'))->toBeFalse();
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Status Tests
|
|
// =========================================================================
|
|
|
|
describe('status helpers', function () {
|
|
it('isActive returns true for fresh key', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
expect($key->isActive())->toBeTrue();
|
|
});
|
|
|
|
it('isActive returns false for revoked key', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
$key->revoke();
|
|
|
|
expect($key->isActive())->toBeFalse();
|
|
});
|
|
|
|
it('isActive returns false for expired key', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[],
|
|
100,
|
|
Carbon::now()->subDay()
|
|
);
|
|
|
|
expect($key->isActive())->toBeFalse();
|
|
});
|
|
|
|
it('isActive returns true for key with future expiry', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[],
|
|
100,
|
|
Carbon::now()->addDay()
|
|
);
|
|
|
|
expect($key->isActive())->toBeTrue();
|
|
});
|
|
|
|
it('isRevoked returns correct value', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
expect($key->isRevoked())->toBeFalse();
|
|
|
|
$key->revoke();
|
|
|
|
expect($key->isRevoked())->toBeTrue();
|
|
});
|
|
|
|
it('isExpired returns correct value for various states', function () {
|
|
$workspace = createWorkspace();
|
|
|
|
$notExpired = AgentApiKey::generate(
|
|
$workspace,
|
|
'Not Expired',
|
|
[],
|
|
100,
|
|
Carbon::now()->addDay()
|
|
);
|
|
|
|
$expired = AgentApiKey::generate(
|
|
$workspace,
|
|
'Expired',
|
|
[],
|
|
100,
|
|
Carbon::now()->subDay()
|
|
);
|
|
|
|
$noExpiry = AgentApiKey::generate($workspace, 'No Expiry');
|
|
|
|
expect($notExpired->isExpired())->toBeFalse();
|
|
expect($expired->isExpired())->toBeTrue();
|
|
expect($noExpiry->isExpired())->toBeFalse();
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Permission Tests
|
|
// =========================================================================
|
|
|
|
describe('permissions', function () {
|
|
it('hasPermission returns true when granted', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[AgentApiKey::PERM_PLANS_READ, AgentApiKey::PERM_PLANS_WRITE]
|
|
);
|
|
|
|
expect($key->hasPermission(AgentApiKey::PERM_PLANS_READ))->toBeTrue();
|
|
expect($key->hasPermission(AgentApiKey::PERM_PLANS_WRITE))->toBeTrue();
|
|
});
|
|
|
|
it('hasPermission returns false when not granted', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
);
|
|
|
|
expect($key->hasPermission(AgentApiKey::PERM_PLANS_WRITE))->toBeFalse();
|
|
});
|
|
|
|
it('hasAnyPermission returns true when one matches', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
);
|
|
|
|
expect($key->hasAnyPermission([
|
|
AgentApiKey::PERM_PLANS_READ,
|
|
AgentApiKey::PERM_PLANS_WRITE,
|
|
]))->toBeTrue();
|
|
});
|
|
|
|
it('hasAnyPermission returns false when none match', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[AgentApiKey::PERM_TEMPLATES_READ]
|
|
);
|
|
|
|
expect($key->hasAnyPermission([
|
|
AgentApiKey::PERM_PLANS_READ,
|
|
AgentApiKey::PERM_PLANS_WRITE,
|
|
]))->toBeFalse();
|
|
});
|
|
|
|
it('hasAllPermissions returns true when all granted', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[AgentApiKey::PERM_PLANS_READ, AgentApiKey::PERM_PLANS_WRITE, AgentApiKey::PERM_SESSIONS_READ]
|
|
);
|
|
|
|
expect($key->hasAllPermissions([
|
|
AgentApiKey::PERM_PLANS_READ,
|
|
AgentApiKey::PERM_PLANS_WRITE,
|
|
]))->toBeTrue();
|
|
});
|
|
|
|
it('hasAllPermissions returns false when missing one', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
);
|
|
|
|
expect($key->hasAllPermissions([
|
|
AgentApiKey::PERM_PLANS_READ,
|
|
AgentApiKey::PERM_PLANS_WRITE,
|
|
]))->toBeFalse();
|
|
});
|
|
|
|
it('updatePermissions changes permissions', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
);
|
|
|
|
$key->updatePermissions([AgentApiKey::PERM_SESSIONS_WRITE]);
|
|
|
|
expect($key->hasPermission(AgentApiKey::PERM_PLANS_READ))->toBeFalse();
|
|
expect($key->hasPermission(AgentApiKey::PERM_SESSIONS_WRITE))->toBeTrue();
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Rate Limiting Tests
|
|
// =========================================================================
|
|
|
|
describe('rate limiting', function () {
|
|
it('isRateLimited returns false when under limit', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key', [], 100);
|
|
|
|
Cache::put("agent_api_key_rate:{$key->id}", 50, 60);
|
|
|
|
expect($key->isRateLimited())->toBeFalse();
|
|
});
|
|
|
|
it('isRateLimited returns true when at limit', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key', [], 100);
|
|
|
|
Cache::put("agent_api_key_rate:{$key->id}", 100, 60);
|
|
|
|
expect($key->isRateLimited())->toBeTrue();
|
|
});
|
|
|
|
it('isRateLimited returns true when over limit', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key', [], 100);
|
|
|
|
Cache::put("agent_api_key_rate:{$key->id}", 150, 60);
|
|
|
|
expect($key->isRateLimited())->toBeTrue();
|
|
});
|
|
|
|
it('getRecentCallCount returns cache value', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
Cache::put("agent_api_key_rate:{$key->id}", 42, 60);
|
|
|
|
expect($key->getRecentCallCount())->toBe(42);
|
|
});
|
|
|
|
it('getRecentCallCount returns zero when not cached', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
expect($key->getRecentCallCount())->toBe(0);
|
|
});
|
|
|
|
it('getRemainingCalls returns correct value', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key', [], 100);
|
|
|
|
Cache::put("agent_api_key_rate:{$key->id}", 30, 60);
|
|
|
|
expect($key->getRemainingCalls())->toBe(70);
|
|
});
|
|
|
|
it('getRemainingCalls returns zero when over limit', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key', [], 100);
|
|
|
|
Cache::put("agent_api_key_rate:{$key->id}", 150, 60);
|
|
|
|
expect($key->getRemainingCalls())->toBe(0);
|
|
});
|
|
|
|
it('updateRateLimit changes limit', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key', [], 100);
|
|
|
|
$key->updateRateLimit(200);
|
|
|
|
expect($key->fresh()->rate_limit)->toBe(200);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// IP Restriction Tests
|
|
// =========================================================================
|
|
|
|
describe('IP restrictions', function () {
|
|
it('has IP restrictions disabled by default', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
expect($key->ip_restriction_enabled)->toBeFalse();
|
|
});
|
|
|
|
it('enableIpRestriction sets flag', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
$key->enableIpRestriction();
|
|
|
|
expect($key->fresh()->ip_restriction_enabled)->toBeTrue();
|
|
});
|
|
|
|
it('disableIpRestriction clears flag', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
$key->enableIpRestriction();
|
|
|
|
$key->disableIpRestriction();
|
|
|
|
expect($key->fresh()->ip_restriction_enabled)->toBeFalse();
|
|
});
|
|
|
|
it('updateIpWhitelist sets list', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
$key->updateIpWhitelist(['192.168.1.1', '10.0.0.0/8']);
|
|
|
|
expect($key->fresh()->ip_whitelist)->toBe(['192.168.1.1', '10.0.0.0/8']);
|
|
});
|
|
|
|
it('addToIpWhitelist adds entry', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
$key->updateIpWhitelist(['192.168.1.1']);
|
|
|
|
$key->addToIpWhitelist('10.0.0.1');
|
|
|
|
$whitelist = $key->fresh()->ip_whitelist;
|
|
expect($whitelist)->toContain('192.168.1.1');
|
|
expect($whitelist)->toContain('10.0.0.1');
|
|
});
|
|
|
|
it('addToIpWhitelist does not duplicate', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
$key->updateIpWhitelist(['192.168.1.1']);
|
|
|
|
$key->addToIpWhitelist('192.168.1.1');
|
|
|
|
expect($key->fresh()->ip_whitelist)->toHaveCount(1);
|
|
});
|
|
|
|
it('removeFromIpWhitelist removes entry', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
$key->updateIpWhitelist(['192.168.1.1', '10.0.0.1']);
|
|
|
|
$key->removeFromIpWhitelist('192.168.1.1');
|
|
|
|
$whitelist = $key->fresh()->ip_whitelist;
|
|
expect($whitelist)->not->toContain('192.168.1.1');
|
|
expect($whitelist)->toContain('10.0.0.1');
|
|
});
|
|
|
|
it('hasIpRestrictions returns correct value', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
// No restrictions
|
|
expect($key->hasIpRestrictions())->toBeFalse();
|
|
|
|
// Enabled but no whitelist
|
|
$key->enableIpRestriction();
|
|
expect($key->fresh()->hasIpRestrictions())->toBeFalse();
|
|
|
|
// Enabled with whitelist
|
|
$key->updateIpWhitelist(['192.168.1.1']);
|
|
expect($key->fresh()->hasIpRestrictions())->toBeTrue();
|
|
});
|
|
|
|
it('getIpWhitelistCount returns correct value', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
expect($key->getIpWhitelistCount())->toBe(0);
|
|
|
|
$key->updateIpWhitelist(['192.168.1.1', '10.0.0.0/8', '172.16.0.0/12']);
|
|
|
|
expect($key->fresh()->getIpWhitelistCount())->toBe(3);
|
|
});
|
|
|
|
it('recordLastUsedIp stores IP', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
$key->recordLastUsedIp('192.168.1.100');
|
|
|
|
expect($key->fresh()->last_used_ip)->toBe('192.168.1.100');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Actions Tests
|
|
// =========================================================================
|
|
|
|
describe('actions', function () {
|
|
it('revoke sets revoked_at', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
$key->revoke();
|
|
|
|
expect($key->fresh()->revoked_at)->not->toBeNull();
|
|
});
|
|
|
|
it('recordUsage increments count', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
$key->recordUsage();
|
|
$key->recordUsage();
|
|
$key->recordUsage();
|
|
|
|
expect($key->fresh()->call_count)->toBe(3);
|
|
});
|
|
|
|
it('recordUsage updates last_used_at', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
expect($key->last_used_at)->toBeNull();
|
|
|
|
$key->recordUsage();
|
|
|
|
expect($key->fresh()->last_used_at)->not->toBeNull();
|
|
});
|
|
|
|
it('extendExpiry updates expiry', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[],
|
|
100,
|
|
Carbon::now()->addDay()
|
|
);
|
|
|
|
$newExpiry = Carbon::now()->addMonth();
|
|
$key->extendExpiry($newExpiry);
|
|
|
|
expect($key->fresh()->expires_at->toDateTimeString())
|
|
->toBe($newExpiry->toDateTimeString());
|
|
});
|
|
|
|
it('removeExpiry clears expiry', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[],
|
|
100,
|
|
Carbon::now()->addDay()
|
|
);
|
|
|
|
$key->removeExpiry();
|
|
|
|
expect($key->fresh()->expires_at)->toBeNull();
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Scope Tests
|
|
// =========================================================================
|
|
|
|
describe('scopes', function () {
|
|
it('active scope filters correctly', function () {
|
|
$workspace = createWorkspace();
|
|
|
|
AgentApiKey::generate($workspace, 'Active Key');
|
|
$revoked = AgentApiKey::generate($workspace, 'Revoked Key');
|
|
$revoked->revoke();
|
|
AgentApiKey::generate($workspace, 'Expired Key', [], 100, Carbon::now()->subDay());
|
|
|
|
$activeKeys = AgentApiKey::active()->get();
|
|
|
|
expect($activeKeys)->toHaveCount(1);
|
|
expect($activeKeys->first()->name)->toBe('Active Key');
|
|
});
|
|
|
|
it('forWorkspace scope filters correctly', function () {
|
|
$workspace = createWorkspace();
|
|
$otherWorkspace = createWorkspace();
|
|
|
|
AgentApiKey::generate($workspace, 'Our Key');
|
|
AgentApiKey::generate($otherWorkspace, 'Their Key');
|
|
|
|
$ourKeys = AgentApiKey::forWorkspace($workspace)->get();
|
|
|
|
expect($ourKeys)->toHaveCount(1);
|
|
expect($ourKeys->first()->name)->toBe('Our Key');
|
|
});
|
|
|
|
it('revoked scope filters correctly', function () {
|
|
$workspace = createWorkspace();
|
|
|
|
AgentApiKey::generate($workspace, 'Active Key');
|
|
$revoked = AgentApiKey::generate($workspace, 'Revoked Key');
|
|
$revoked->revoke();
|
|
|
|
$revokedKeys = AgentApiKey::revoked()->get();
|
|
|
|
expect($revokedKeys)->toHaveCount(1);
|
|
expect($revokedKeys->first()->name)->toBe('Revoked Key');
|
|
});
|
|
|
|
it('expired scope filters correctly', function () {
|
|
$workspace = createWorkspace();
|
|
|
|
AgentApiKey::generate($workspace, 'Active Key');
|
|
AgentApiKey::generate($workspace, 'Expired Key', [], 100, Carbon::now()->subDay());
|
|
|
|
$expiredKeys = AgentApiKey::expired()->get();
|
|
|
|
expect($expiredKeys)->toHaveCount(1);
|
|
expect($expiredKeys->first()->name)->toBe('Expired Key');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Display Helper Tests
|
|
// =========================================================================
|
|
|
|
describe('display helpers', function () {
|
|
it('getMaskedKey returns masked format', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
$masked = $key->getMaskedKey();
|
|
|
|
expect($masked)
|
|
->toStartWith('ak_')
|
|
->toEndWith('...');
|
|
});
|
|
|
|
it('getStatusLabel returns correct label', function () {
|
|
$workspace = createWorkspace();
|
|
|
|
$active = AgentApiKey::generate($workspace, 'Active');
|
|
$revoked = AgentApiKey::generate($workspace, 'Revoked');
|
|
$revoked->revoke();
|
|
$expired = AgentApiKey::generate($workspace, 'Expired', [], 100, Carbon::now()->subDay());
|
|
|
|
expect($active->getStatusLabel())->toBe('Active');
|
|
expect($revoked->getStatusLabel())->toBe('Revoked');
|
|
expect($expired->getStatusLabel())->toBe('Expired');
|
|
});
|
|
|
|
it('getStatusColor returns correct colour', function () {
|
|
$workspace = createWorkspace();
|
|
|
|
$active = AgentApiKey::generate($workspace, 'Active');
|
|
$revoked = AgentApiKey::generate($workspace, 'Revoked');
|
|
$revoked->revoke();
|
|
$expired = AgentApiKey::generate($workspace, 'Expired', [], 100, Carbon::now()->subDay());
|
|
|
|
expect($active->getStatusColor())->toBe('green');
|
|
expect($revoked->getStatusColor())->toBe('red');
|
|
expect($expired->getStatusColor())->toBe('amber');
|
|
});
|
|
|
|
it('getLastUsedForHumans returns Never when null', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
expect($key->getLastUsedForHumans())->toBe('Never');
|
|
});
|
|
|
|
it('getLastUsedForHumans returns diff when set', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
$key->update(['last_used_at' => Carbon::now()->subHour()]);
|
|
|
|
expect($key->getLastUsedForHumans())->toContain('ago');
|
|
});
|
|
|
|
it('getExpiresForHumans returns Never when null', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
expect($key->getExpiresForHumans())->toBe('Never');
|
|
});
|
|
|
|
it('getExpiresForHumans returns Expired when past', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[],
|
|
100,
|
|
Carbon::now()->subDay()
|
|
);
|
|
|
|
expect($key->getExpiresForHumans())->toContain('Expired');
|
|
});
|
|
|
|
it('getExpiresForHumans returns Expires when future', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[],
|
|
100,
|
|
Carbon::now()->addDay()
|
|
);
|
|
|
|
expect($key->getExpiresForHumans())->toContain('Expires');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Array Output Tests
|
|
// =========================================================================
|
|
|
|
describe('array output', function () {
|
|
it('toArray includes expected keys', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate(
|
|
$workspace,
|
|
'Test Key',
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
);
|
|
|
|
$array = $key->toArray();
|
|
|
|
expect($array)
|
|
->toHaveKey('id')
|
|
->toHaveKey('workspace_id')
|
|
->toHaveKey('name')
|
|
->toHaveKey('permissions')
|
|
->toHaveKey('rate_limit')
|
|
->toHaveKey('call_count')
|
|
->toHaveKey('status')
|
|
->toHaveKey('ip_restriction_enabled')
|
|
->toHaveKey('ip_whitelist_count');
|
|
|
|
// Should NOT include the key hash
|
|
expect($array)->not->toHaveKey('key');
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Available Permissions Tests
|
|
// =========================================================================
|
|
|
|
describe('available permissions', function () {
|
|
it('returns all permissions', function () {
|
|
$permissions = AgentApiKey::availablePermissions();
|
|
|
|
expect($permissions)
|
|
->toBeArray()
|
|
->toHaveKey(AgentApiKey::PERM_PLANS_READ)
|
|
->toHaveKey(AgentApiKey::PERM_PLANS_WRITE)
|
|
->toHaveKey(AgentApiKey::PERM_SESSIONS_READ)
|
|
->toHaveKey(AgentApiKey::PERM_SESSIONS_WRITE)
|
|
->toHaveKey(AgentApiKey::PERM_NOTIFY_READ)
|
|
->toHaveKey(AgentApiKey::PERM_NOTIFY_WRITE)
|
|
->toHaveKey(AgentApiKey::PERM_NOTIFY_SEND);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Relationship Tests
|
|
// =========================================================================
|
|
|
|
describe('relationships', function () {
|
|
it('belongs to workspace', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
expect($key->workspace)
|
|
->toBeInstanceOf(Workspace::class)
|
|
->and($key->workspace->id)->toBe($workspace->id);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Key Rotation Tests
|
|
// =========================================================================
|
|
|
|
describe('key rotation', function () {
|
|
it('can generate a new key for the same workspace and revoke old', function () {
|
|
$workspace = createWorkspace();
|
|
|
|
$oldKey = AgentApiKey::generate($workspace, 'Old Key');
|
|
$oldPlainKey = $oldKey->plainTextKey;
|
|
|
|
// Revoke old key
|
|
$oldKey->revoke();
|
|
|
|
// Create new key
|
|
$newKey = AgentApiKey::generate($workspace, 'New Key');
|
|
$newPlainKey = $newKey->plainTextKey;
|
|
|
|
// Old key should not be found
|
|
expect(AgentApiKey::findByKey($oldPlainKey))->toBeNull();
|
|
|
|
// New key should be found
|
|
expect(AgentApiKey::findByKey($newPlainKey))->not->toBeNull();
|
|
});
|
|
|
|
it('workspace can have multiple active keys', function () {
|
|
$workspace = createWorkspace();
|
|
|
|
AgentApiKey::generate($workspace, 'Key 1');
|
|
AgentApiKey::generate($workspace, 'Key 2');
|
|
AgentApiKey::generate($workspace, 'Key 3');
|
|
|
|
$activeKeys = AgentApiKey::forWorkspace($workspace)->active()->get();
|
|
|
|
expect($activeKeys)->toHaveCount(3);
|
|
});
|
|
});
|
|
|
|
// =========================================================================
|
|
// Security Edge Cases
|
|
// =========================================================================
|
|
|
|
describe('security edge cases', function () {
|
|
it('different keys for same workspace have unique hashes', function () {
|
|
$workspace = createWorkspace();
|
|
|
|
$key1 = AgentApiKey::generate($workspace, 'Key 1');
|
|
$key2 = AgentApiKey::generate($workspace, 'Key 2');
|
|
|
|
expect($key1->key)->not->toBe($key2->key);
|
|
});
|
|
|
|
it('same plaintext would produce different Argon2id hashes', function () {
|
|
// This tests that Argon2id includes a random salt
|
|
$workspace = createWorkspace();
|
|
$plainKey = 'ak_test_key_12345678901234567890';
|
|
|
|
$hash1 = password_hash($plainKey, PASSWORD_ARGON2ID);
|
|
$hash2 = password_hash($plainKey, PASSWORD_ARGON2ID);
|
|
|
|
expect($hash1)->not->toBe($hash2);
|
|
|
|
// But both verify correctly
|
|
expect(password_verify($plainKey, $hash1))->toBeTrue();
|
|
expect(password_verify($plainKey, $hash2))->toBeTrue();
|
|
});
|
|
|
|
it('cannot find key using partial plaintext', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
$partialKey = substr($key->plainTextKey, 0, 20);
|
|
|
|
expect(AgentApiKey::findByKey($partialKey))->toBeNull();
|
|
});
|
|
|
|
it('cannot find key using hash directly', function () {
|
|
$workspace = createWorkspace();
|
|
$key = AgentApiKey::generate($workspace, 'Test Key');
|
|
|
|
// Trying to use the hash as if it were the plaintext key
|
|
expect(AgentApiKey::findByKey($key->key))->toBeNull();
|
|
});
|
|
});
|