2026-01-29 13:36:53 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
/**
|
|
|
|
|
* Tests for the AgentApiKeyService.
|
|
|
|
|
*
|
|
|
|
|
* Covers authentication, IP validation, rate limit tracking, and key management.
|
|
|
|
|
*/
|
2026-01-29 13:36:53 +00:00
|
|
|
|
|
|
|
|
use Carbon\Carbon;
|
|
|
|
|
use Core\Mod\Agentic\Models\AgentApiKey;
|
|
|
|
|
use Core\Mod\Agentic\Services\AgentApiKeyService;
|
|
|
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
// =========================================================================
|
|
|
|
|
// Key Creation Tests
|
|
|
|
|
// =========================================================================
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
describe('key creation', function () {
|
|
|
|
|
it('creates and returns an AgentApiKey instance', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$key = $service->create($workspace, 'Test Key');
|
|
|
|
|
|
|
|
|
|
expect($key)
|
|
|
|
|
->toBeInstanceOf(AgentApiKey::class)
|
|
|
|
|
->and($key->plainTextKey)->not->toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('creates key using workspace ID', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
|
|
|
|
|
$key = $service->create($workspace->id, 'Test Key');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($key->workspace_id)->toBe($workspace->id);
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('creates key with specified permissions', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
2026-01-29 13:36:53 +00:00
|
|
|
$permissions = [
|
|
|
|
|
AgentApiKey::PERM_PLANS_READ,
|
|
|
|
|
AgentApiKey::PERM_PLANS_WRITE,
|
|
|
|
|
];
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$key = $service->create($workspace, 'Test Key', $permissions);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($key->permissions)->toBe($permissions);
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('creates key with custom rate limit', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$key = $service->create($workspace, 'Test Key', [], 500);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($key->rate_limit)->toBe(500);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('creates key with expiry date', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
2026-01-29 13:36:53 +00:00
|
|
|
$expiresAt = Carbon::now()->addMonth();
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$key = $service->create($workspace, 'Test Key', [], 100, $expiresAt);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($key->expires_at->toDateTimeString())
|
|
|
|
|
->toBe($expiresAt->toDateTimeString());
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
// =========================================================================
|
|
|
|
|
// Key Validation Tests
|
|
|
|
|
// =========================================================================
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
describe('key validation', function () {
|
|
|
|
|
it('returns key for valid plaintext key', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
2026-01-29 13:36:53 +00:00
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->validate($plainKey);
|
|
|
|
|
|
|
|
|
|
expect($result)
|
|
|
|
|
->not->toBeNull()
|
|
|
|
|
->and($result->id)->toBe($key->id);
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('returns null for invalid key', function () {
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->validate('ak_invalid_key_here');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeNull();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('returns null for revoked key', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
2026-01-29 13:36:53 +00:00
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
$key->revoke();
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->validate($plainKey);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeNull();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('returns null for expired key', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[],
|
|
|
|
|
100,
|
|
|
|
|
Carbon::now()->subDay()
|
|
|
|
|
);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->validate($plainKey);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
// =========================================================================
|
|
|
|
|
// Permission Check Tests
|
|
|
|
|
// =========================================================================
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
describe('permission checks', function () {
|
|
|
|
|
it('checkPermission returns true when permission granted', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->checkPermission($key, AgentApiKey::PERM_PLANS_READ);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeTrue();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('checkPermission returns false when permission not granted', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->checkPermission($key, AgentApiKey::PERM_PLANS_WRITE);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeFalse();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('checkPermission returns false for inactive key', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
|
|
|
);
|
|
|
|
|
$key->revoke();
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->checkPermission($key, AgentApiKey::PERM_PLANS_READ);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeFalse();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('checkPermissions returns true when all permissions granted', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ, AgentApiKey::PERM_PLANS_WRITE]
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->checkPermissions($key, [
|
2026-01-29 13:36:53 +00:00
|
|
|
AgentApiKey::PERM_PLANS_READ,
|
|
|
|
|
AgentApiKey::PERM_PLANS_WRITE,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeTrue();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('checkPermissions returns false when missing one permission', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->checkPermissions($key, [
|
2026-01-29 13:36:53 +00:00
|
|
|
AgentApiKey::PERM_PLANS_READ,
|
|
|
|
|
AgentApiKey::PERM_PLANS_WRITE,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeFalse();
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
// =========================================================================
|
|
|
|
|
// Rate Limiting Tests
|
|
|
|
|
// =========================================================================
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
describe('rate limiting', function () {
|
|
|
|
|
it('recordUsage increments cache counter', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
2026-01-29 13:36:53 +00:00
|
|
|
Cache::forget("agent_api_key_rate:{$key->id}");
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->recordUsage($key);
|
|
|
|
|
$service->recordUsage($key);
|
|
|
|
|
$service->recordUsage($key);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect(Cache::get("agent_api_key_rate:{$key->id}"))->toBe(3);
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('recordUsage records client IP', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->recordUsage($key, '192.168.1.100');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($key->fresh()->last_used_ip)->toBe('192.168.1.100');
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('recordUsage updates last_used_at', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->recordUsage($key);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($key->fresh()->last_used_at)->not->toBeNull();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('isRateLimited returns false when under limit', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key', [], 100);
|
2026-01-29 13:36:53 +00:00
|
|
|
Cache::put("agent_api_key_rate:{$key->id}", 50, 60);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->isRateLimited($key);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeFalse();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('isRateLimited returns true at limit', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key', [], 100);
|
2026-01-29 13:36:53 +00:00
|
|
|
Cache::put("agent_api_key_rate:{$key->id}", 100, 60);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->isRateLimited($key);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeTrue();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('isRateLimited returns true over limit', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key', [], 100);
|
2026-01-29 13:36:53 +00:00
|
|
|
Cache::put("agent_api_key_rate:{$key->id}", 150, 60);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->isRateLimited($key);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeTrue();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('getRateLimitStatus returns correct values', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key', [], 100);
|
2026-01-29 13:36:53 +00:00
|
|
|
Cache::put("agent_api_key_rate:{$key->id}", 30, 60);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$status = $service->getRateLimitStatus($key);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($status['limit'])->toBe(100)
|
|
|
|
|
->and($status['remaining'])->toBe(70)
|
|
|
|
|
->and($status['used'])->toBe(30)
|
|
|
|
|
->and($status)->toHaveKey('reset_in_seconds');
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
// =========================================================================
|
|
|
|
|
// Key Management Tests
|
|
|
|
|
// =========================================================================
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
describe('key management', function () {
|
|
|
|
|
it('revoke sets revoked_at', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->revoke($key);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($key->fresh()->revoked_at)->not->toBeNull();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('revoke clears rate limit cache', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
2026-01-29 13:36:53 +00:00
|
|
|
Cache::put("agent_api_key_rate:{$key->id}", 50, 60);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->revoke($key);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect(Cache::get("agent_api_key_rate:{$key->id}"))->toBeNull();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('updatePermissions changes permissions', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->updatePermissions($key, [AgentApiKey::PERM_SESSIONS_WRITE]);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
|
|
|
|
$fresh = $key->fresh();
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($fresh->hasPermission(AgentApiKey::PERM_PLANS_READ))->toBeFalse()
|
|
|
|
|
->and($fresh->hasPermission(AgentApiKey::PERM_SESSIONS_WRITE))->toBeTrue();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('updateRateLimit changes limit', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key', [], 100);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->updateRateLimit($key, 500);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($key->fresh()->rate_limit)->toBe(500);
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('extendExpiry updates expiry date', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[],
|
|
|
|
|
100,
|
|
|
|
|
Carbon::now()->addDay()
|
|
|
|
|
);
|
|
|
|
|
$newExpiry = Carbon::now()->addMonth();
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->extendExpiry($key, $newExpiry);
|
|
|
|
|
|
|
|
|
|
expect($key->fresh()->expires_at->toDateTimeString())
|
|
|
|
|
->toBe($newExpiry->toDateTimeString());
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('removeExpiry clears expiry date', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[],
|
|
|
|
|
100,
|
|
|
|
|
Carbon::now()->addDay()
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->removeExpiry($key);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($key->fresh()->expires_at)->toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
// =========================================================================
|
|
|
|
|
// IP Restriction Tests
|
|
|
|
|
// =========================================================================
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
describe('IP restrictions', function () {
|
|
|
|
|
it('updateIpRestrictions sets values', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->updateIpRestrictions($key, true, ['192.168.1.1', '10.0.0.0/8']);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
|
|
|
|
$fresh = $key->fresh();
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($fresh->ip_restriction_enabled)->toBeTrue()
|
|
|
|
|
->and($fresh->ip_whitelist)->toBe(['192.168.1.1', '10.0.0.0/8']);
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('enableIpRestrictions enables with whitelist', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->enableIpRestrictions($key, ['192.168.1.1']);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
|
|
|
|
$fresh = $key->fresh();
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($fresh->ip_restriction_enabled)->toBeTrue()
|
|
|
|
|
->and($fresh->ip_whitelist)->toBe(['192.168.1.1']);
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('disableIpRestrictions disables restrictions', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
|
|
|
|
$service->enableIpRestrictions($key, ['192.168.1.1']);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->disableIpRestrictions($key);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($key->fresh()->ip_restriction_enabled)->toBeFalse();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('isIpAllowed returns true when restrictions disabled', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->isIpAllowed($key, '192.168.1.100');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeTrue();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('isIpAllowed returns true when IP in whitelist', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
|
|
|
|
$service->enableIpRestrictions($key, ['192.168.1.100']);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->isIpAllowed($key->fresh(), '192.168.1.100');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeTrue();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('isIpAllowed returns false when IP not in whitelist', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
|
|
|
|
$service->enableIpRestrictions($key, ['192.168.1.100']);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->isIpAllowed($key->fresh(), '10.0.0.1');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeFalse();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('isIpAllowed supports CIDR ranges', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
|
|
|
|
$service->enableIpRestrictions($key, ['192.168.1.0/24']);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
|
|
|
|
$fresh = $key->fresh();
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($service->isIpAllowed($fresh, '192.168.1.50'))->toBeTrue()
|
|
|
|
|
->and($service->isIpAllowed($fresh, '192.168.1.254'))->toBeTrue()
|
|
|
|
|
->and($service->isIpAllowed($fresh, '192.168.2.1'))->toBeFalse();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('parseIpWhitelistInput parses valid input', function () {
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
2026-01-29 13:36:53 +00:00
|
|
|
$input = "192.168.1.1\n192.168.1.2\n10.0.0.0/8";
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->parseIpWhitelistInput($input);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result['errors'])->toBeEmpty()
|
|
|
|
|
->and($result['entries'])->toHaveCount(3)
|
|
|
|
|
->and($result['entries'])->toContain('192.168.1.1')
|
|
|
|
|
->and($result['entries'])->toContain('192.168.1.2')
|
|
|
|
|
->and($result['entries'])->toContain('10.0.0.0/8');
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('parseIpWhitelistInput returns errors for invalid entries', function () {
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
2026-01-29 13:36:53 +00:00
|
|
|
$input = "192.168.1.1\ninvalid_ip\n10.0.0.0/8";
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->parseIpWhitelistInput($input);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result['errors'])->toHaveCount(1)
|
|
|
|
|
->and($result['entries'])->toHaveCount(2);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
// =========================================================================
|
|
|
|
|
// Workspace Query Tests
|
|
|
|
|
// =========================================================================
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
describe('workspace queries', function () {
|
|
|
|
|
it('getActiveKeysForWorkspace returns active keys only', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
|
|
|
|
|
$active = $service->create($workspace, 'Active Key');
|
|
|
|
|
$revoked = $service->create($workspace, 'Revoked Key');
|
2026-01-29 13:36:53 +00:00
|
|
|
$revoked->revoke();
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->create($workspace, 'Expired Key', [], 100, Carbon::now()->subDay());
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$keys = $service->getActiveKeysForWorkspace($workspace);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($keys)->toHaveCount(1)
|
|
|
|
|
->and($keys->first()->name)->toBe('Active Key');
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('getActiveKeysForWorkspace filters by workspace', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$otherWorkspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->create($workspace, 'Our Key');
|
|
|
|
|
$service->create($otherWorkspace, 'Their Key');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$keys = $service->getActiveKeysForWorkspace($workspace);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($keys)->toHaveCount(1)
|
|
|
|
|
->and($keys->first()->name)->toBe('Our Key');
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('getAllKeysForWorkspace returns all keys', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
|
|
|
|
|
$service->create($workspace, 'Active Key');
|
|
|
|
|
$revoked = $service->create($workspace, 'Revoked Key');
|
2026-01-29 13:36:53 +00:00
|
|
|
$revoked->revoke();
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->create($workspace, 'Expired Key', [], 100, Carbon::now()->subDay());
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$keys = $service->getAllKeysForWorkspace($workspace);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($keys)->toHaveCount(3);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
// =========================================================================
|
|
|
|
|
// Validate With Permission Tests
|
|
|
|
|
// =========================================================================
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
describe('validateWithPermission', function () {
|
|
|
|
|
it('returns key when valid with permission', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
|
|
|
);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->validateWithPermission($plainKey, AgentApiKey::PERM_PLANS_READ);
|
|
|
|
|
|
|
|
|
|
expect($result)
|
|
|
|
|
->not->toBeNull()
|
|
|
|
|
->and($result->id)->toBe($key->id);
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('returns null for invalid key', function () {
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->validateWithPermission(
|
2026-01-29 13:36:53 +00:00
|
|
|
'ak_invalid_key',
|
|
|
|
|
AgentApiKey::PERM_PLANS_READ
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeNull();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('returns null without required permission', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_SESSIONS_READ]
|
|
|
|
|
);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->validateWithPermission($plainKey, AgentApiKey::PERM_PLANS_READ);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeNull();
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('returns null when rate limited', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ],
|
|
|
|
|
100
|
|
|
|
|
);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
Cache::put("agent_api_key_rate:{$key->id}", 150, 60);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->validateWithPermission($plainKey, AgentApiKey::PERM_PLANS_READ);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result)->toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
// =========================================================================
|
|
|
|
|
// Full Authentication Flow Tests
|
|
|
|
|
// =========================================================================
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
describe('authenticate', function () {
|
|
|
|
|
it('returns success for valid key with permission', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
|
|
|
);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result['success'])->toBeTrue()
|
|
|
|
|
->and($result['key'])->toBeInstanceOf(AgentApiKey::class)
|
|
|
|
|
->and($result['workspace_id'])->toBe($workspace->id)
|
|
|
|
|
->and($result)->toHaveKey('rate_limit');
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('returns error for invalid key', function () {
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->authenticate('ak_invalid_key', AgentApiKey::PERM_PLANS_READ);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result['success'])->toBeFalse()
|
|
|
|
|
->and($result['error'])->toBe('invalid_key');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns error for revoked key', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
|
|
|
);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
$key->revoke();
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result['success'])->toBeFalse()
|
|
|
|
|
->and($result['error'])->toBe('key_revoked');
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('returns error for expired key', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ],
|
|
|
|
|
100,
|
|
|
|
|
Carbon::now()->subDay()
|
|
|
|
|
);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result['success'])->toBeFalse()
|
|
|
|
|
->and($result['error'])->toBe('key_expired');
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('returns error for missing permission', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_SESSIONS_READ]
|
|
|
|
|
);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result['success'])->toBeFalse()
|
|
|
|
|
->and($result['error'])->toBe('permission_denied');
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('returns error when rate limited', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ],
|
|
|
|
|
100
|
|
|
|
|
);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
Cache::put("agent_api_key_rate:{$key->id}", 150, 60);
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result['success'])->toBeFalse()
|
|
|
|
|
->and($result['error'])->toBe('rate_limited')
|
|
|
|
|
->and($result)->toHaveKey('rate_limit');
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('checks IP restrictions', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
|
|
|
);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->enableIpRestrictions($key, ['192.168.1.100']);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ, '10.0.0.1');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result['success'])->toBeFalse()
|
|
|
|
|
->and($result['error'])->toBe('ip_not_allowed');
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('allows whitelisted IP', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
|
|
|
);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->enableIpRestrictions($key, ['192.168.1.100']);
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ, '192.168.1.100');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result['success'])->toBeTrue()
|
|
|
|
|
->and($result['client_ip'])->toBe('192.168.1.100');
|
|
|
|
|
});
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
it('records usage on success', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
|
|
|
);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
Cache::forget("agent_api_key_rate:{$key->id}");
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ, '192.168.1.50');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
|
|
|
|
$fresh = $key->fresh();
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($fresh->call_count)->toBe(1)
|
|
|
|
|
->and($fresh->last_used_at)->not->toBeNull()
|
|
|
|
|
->and($fresh->last_used_ip)->toBe('192.168.1.50');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not record usage on failure', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key', []);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
|
|
|
|
|
$service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ);
|
|
|
|
|
|
|
|
|
|
expect($key->fresh()->call_count)->toBe(0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// Edge Cases and Security Tests
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
describe('edge cases', function () {
|
|
|
|
|
it('handles empty permissions array', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
|
|
|
|
|
$key = $service->create($workspace, 'Test Key', []);
|
|
|
|
|
|
|
|
|
|
expect($key->permissions)->toBe([])
|
|
|
|
|
->and($service->checkPermission($key, AgentApiKey::PERM_PLANS_READ))->toBeFalse();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles multiple workspaces correctly', function () {
|
|
|
|
|
$workspace1 = createWorkspace();
|
|
|
|
|
$workspace2 = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
|
|
|
|
|
$key1 = $service->create($workspace1, 'Workspace 1 Key');
|
|
|
|
|
$key2 = $service->create($workspace2, 'Workspace 2 Key');
|
|
|
|
|
|
|
|
|
|
expect($key1->workspace_id)->toBe($workspace1->id)
|
|
|
|
|
->and($key2->workspace_id)->toBe($workspace2->id);
|
|
|
|
|
|
|
|
|
|
$workspace1Keys = $service->getAllKeysForWorkspace($workspace1);
|
|
|
|
|
$workspace2Keys = $service->getAllKeysForWorkspace($workspace2);
|
|
|
|
|
|
|
|
|
|
expect($workspace1Keys)->toHaveCount(1)
|
|
|
|
|
->and($workspace2Keys)->toHaveCount(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles concurrent rate limit updates atomically', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create($workspace, 'Test Key');
|
|
|
|
|
Cache::forget("agent_api_key_rate:{$key->id}");
|
|
|
|
|
|
|
|
|
|
// Simulate rapid concurrent requests
|
|
|
|
|
for ($i = 0; $i < 10; $i++) {
|
|
|
|
|
$service->recordUsage($key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect(Cache::get("agent_api_key_rate:{$key->id}"))->toBe(10);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles null client IP gracefully', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
2026-01-29 13:36:53 +00:00
|
|
|
'Test Key',
|
2026-01-29 18:50:41 +00:00
|
|
|
[AgentApiKey::PERM_PLANS_READ]
|
2026-01-29 13:36:53 +00:00
|
|
|
);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
$result = $service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ, null);
|
|
|
|
|
|
|
|
|
|
expect($result['success'])->toBeTrue()
|
|
|
|
|
->and($result['client_ip'])->toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('validates key before checking IP restrictions', function () {
|
|
|
|
|
$workspace = createWorkspace();
|
|
|
|
|
$service = app(AgentApiKeyService::class);
|
|
|
|
|
$key = $service->create(
|
|
|
|
|
$workspace,
|
|
|
|
|
'Test Key',
|
|
|
|
|
[AgentApiKey::PERM_PLANS_READ]
|
|
|
|
|
);
|
|
|
|
|
$plainKey = $key->plainTextKey;
|
|
|
|
|
$service->enableIpRestrictions($key, ['192.168.1.100']);
|
|
|
|
|
$key->revoke();
|
|
|
|
|
|
|
|
|
|
// Should fail on revoked check before IP check
|
|
|
|
|
$result = $service->authenticate($plainKey, AgentApiKey::PERM_PLANS_READ, '10.0.0.1');
|
2026-01-29 13:36:53 +00:00
|
|
|
|
2026-01-29 18:50:41 +00:00
|
|
|
expect($result['error'])->toBe('key_revoked');
|
|
|
|
|
});
|
|
|
|
|
});
|