a26f5af2da
Code-quality review of Task 3: index() filtered by is_active only — an expired-but-active key would be listed as valid. Adds an expires_at > now() filter plus a test. Cannot occur today (regenerate is the only write path, always +1 year) but is the correct semantic contract for an «active key» listing. phpstan-baseline.neon: count bumps only for ApiKeyControllerTest.php ($tenant 5→7, $user 3→5, getJson 3→4). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
98 lines
3.5 KiB
PHP
98 lines
3.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\ApiKey;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Facades\Hash;
|
|
|
|
uses(DatabaseTransactions::class);
|
|
|
|
beforeEach(function () {
|
|
$this->tenant = Tenant::factory()->create();
|
|
$this->user = User::factory()->create(['tenant_id' => $this->tenant->id]);
|
|
$this->actingAs($this->user);
|
|
});
|
|
|
|
test('GET /api/api-keys возвращает активные ключи тенанта', function () {
|
|
ApiKey::factory()->create(['tenant_id' => $this->tenant->id, 'user_id' => $this->user->id]);
|
|
|
|
$response = $this->getJson('/api/api-keys');
|
|
|
|
$response->assertOk();
|
|
expect($response->json('data'))->toHaveCount(1);
|
|
expect($response->json('data.0'))->toHaveKeys(['id', 'name', 'key_prefix', 'last_used_at', 'created_at']);
|
|
expect($response->json('data.0'))->not->toHaveKey('key_hash');
|
|
});
|
|
|
|
test('GET /api/api-keys без auth: 401', function () {
|
|
auth()->logout();
|
|
$this->getJson('/api/api-keys')->assertStatus(401);
|
|
});
|
|
|
|
test('GET /api/api-keys изолирован по тенанту', function () {
|
|
$otherTenant = Tenant::factory()->create();
|
|
$otherUser = User::factory()->create(['tenant_id' => $otherTenant->id]);
|
|
ApiKey::factory()->create(['tenant_id' => $otherTenant->id, 'user_id' => $otherUser->id]);
|
|
|
|
$response = $this->getJson('/api/api-keys');
|
|
|
|
$response->assertOk();
|
|
expect($response->json('data'))->toHaveCount(0);
|
|
});
|
|
|
|
test('GET /api/api-keys не возвращает истёкшие ключи', function () {
|
|
ApiKey::factory()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'user_id' => $this->user->id,
|
|
'is_active' => true,
|
|
'expires_at' => now()->subDay(),
|
|
]);
|
|
ApiKey::factory()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'user_id' => $this->user->id,
|
|
'is_active' => true,
|
|
'expires_at' => now()->addYear(),
|
|
]);
|
|
|
|
$response = $this->getJson('/api/api-keys');
|
|
|
|
$response->assertOk();
|
|
expect($response->json('data'))->toHaveCount(1);
|
|
});
|
|
|
|
test('POST /api/api-keys/regenerate создаёт ключ и возвращает plaintext один раз', function () {
|
|
$response = $this->postJson('/api/api-keys/regenerate');
|
|
|
|
$response->assertStatus(201);
|
|
expect($response->json('key'))->toStartWith('lpkapi_');
|
|
expect($response->json('key_prefix'))->toBe(substr($response->json('key'), 0, 10));
|
|
expect($response->json())->toHaveKeys(['id', 'name', 'key', 'key_prefix']);
|
|
|
|
$row = ApiKey::query()->where('tenant_id', $this->tenant->id)->where('is_active', true)->first();
|
|
expect($row)->not->toBeNull();
|
|
expect($row->key_hash)->not->toBe($response->json('key'));
|
|
expect(Hash::check($response->json('key'), $row->key_hash))->toBeTrue();
|
|
});
|
|
|
|
test('POST /api/api-keys/regenerate деактивирует предыдущий активный ключ', function () {
|
|
$old = ApiKey::factory()->create([
|
|
'tenant_id' => $this->tenant->id,
|
|
'user_id' => $this->user->id,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$this->postJson('/api/api-keys/regenerate')->assertStatus(201);
|
|
|
|
$old->refresh();
|
|
expect($old->is_active)->toBeFalse();
|
|
expect(ApiKey::query()->where('tenant_id', $this->tenant->id)->where('is_active', true)->count())->toBe(1);
|
|
});
|
|
|
|
test('POST /api/api-keys/regenerate без auth: 401', function () {
|
|
auth()->logout();
|
|
$this->postJson('/api/api-keys/regenerate')->assertStatus(401);
|
|
});
|