agent/php/tests/Feature/Mod/Api/ApiKeyFoundationTest.php
Snider 5385385314 feat(agent/api): RFC foundation — API keys, webhooks, rate limiting, docs split
Foundation slice for Mantis #844 php/Mod/Api RFC implementation:

* New php/Mod/Api/ package: Boot, Controllers, Documentation, Jobs,
  Middleware, Models, RateLimit, Routes, Services
* Models: ApiKey, WebhookEndpoint, WebhookDelivery
* WebhookService::dispatch() with DB::transaction + afterCommit
* DeliverWebhookJob with retry/backoff
* WebhookSignature with timing-safe verification + 5-minute tolerance +
  dual-secret rotation support
* Sliding-window rate limiter in RateLimit/RateLimitService.php
* AuthenticateApiKey middleware: hk_ prefix + Sanctum fallback
* DocsController / DocumentationController split
* 3 root migrations: api_keys, webhook_endpoints, webhook_deliveries
* Foundation tests under php/tests/Feature/Mod/Api/
* FOLLOWUP.md tracks remaining RFC scope

php -l clean across 21 PHP files. Pest unrunnable in sandbox (no vendor/).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=844
2026-04-25 21:01:54 +01:00

47 lines
1.8 KiB
PHP

<?php
declare(strict_types=1);
use Core\Mod\Agentic\Mod\Api\Boot as ApiBoot;
use Core\Mod\Agentic\Mod\Api\Models\ApiKey;
use Illuminate\Support\Facades\DB;
beforeEach(function (): void {
$this->app->register(ApiBoot::class);
});
describe('ApiKey foundation', function () {
it('generates bcrypt-backed hk keys with the required format', function (): void {
$workspace = createWorkspace();
$result = ApiKey::generate($workspace->id, null, 'Gateway Key');
expect($result['plain_key'])->toMatch('/^hk_[A-Za-z0-9]{8}_[A-Za-z0-9]{48}$/')
->and($result['api_key']->prefix)->toBe(substr($result['plain_key'], 0, 11))
->and(password_get_info($result['api_key']->key)['algoName'])->toBe('bcrypt')
->and(password_verify(explode('_', $result['plain_key'], 3)[2], $result['api_key']->key))->toBeTrue();
});
it('finds a key by prefix and candidate verification rather than hashing in the query', function (): void {
$workspace = createWorkspace();
$result = ApiKey::generate($workspace->id, null, 'Lookup Key');
DB::flushQueryLog();
DB::enableQueryLog();
$found = ApiKey::findByPlainKey($result['plain_key']);
$queries = collect(DB::getQueryLog())->pluck('query')->implode("\n");
expect($found?->is($result['api_key']))->toBeTrue()
->and($queries)->toContain('prefix');
});
it('rejects malformed and expired keys', function (): void {
$workspace = createWorkspace();
$expired = ApiKey::generate($workspace->id, null, 'Expired Key', expiresAt: now()->subMinute());
expect(ApiKey::findByPlainKey(''))->toBeNull()
->and(ApiKey::findByPlainKey('hk_short'))->toBeNull()
->and(ApiKey::findByPlainKey($expired['plain_key']))->toBeNull();
});
});