From 6e4656946c18b5aa2703623ae99f17227e9bcafa Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Wed, 28 Jan 2026 21:49:16 +0100 Subject: [PATCH] Expand support API integration tests and add load script --- scripts/load/support-api-k6.js | 47 ++++++++++++ tests/Feature/Support/SupportApiTest.php | 96 ++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 scripts/load/support-api-k6.js diff --git a/scripts/load/support-api-k6.js b/scripts/load/support-api-k6.js new file mode 100644 index 0000000..7bf21af --- /dev/null +++ b/scripts/load/support-api-k6.js @@ -0,0 +1,47 @@ +import http from 'k6/http' +import { check, sleep } from 'k6' + +const BASE_URL = __ENV.SUPPORT_API_BASE_URL || 'http://localhost' +const TOKEN = __ENV.SUPPORT_API_TOKEN || '' +const DEFAULT_SLEEP = __ENV.SUPPORT_API_SLEEP || '1' + +if (!TOKEN) { + throw new Error('SUPPORT_API_TOKEN is required') +} + +export const options = { + vus: Number(__ENV.SUPPORT_API_VUS || 10), + duration: __ENV.SUPPORT_API_DURATION || '1m', + thresholds: { + http_req_failed: ['rate<0.01'], + http_req_duration: ['p(95)<500'], + }, +} + +function authHeaders() { + return { + headers: { + Authorization: `Bearer ${TOKEN}`, + 'Content-Type': 'application/json', + }, + } +} + +export default function () { + const listTenants = http.get(`${BASE_URL}/api/v1/support/tenants?per_page=25`, authHeaders()) + check(listTenants, { + 'list tenants 200': (res) => res.status === 200, + }) + + const listEvents = http.get(`${BASE_URL}/api/v1/support/events?per_page=25`, authHeaders()) + check(listEvents, { + 'list events 200': (res) => res.status === 200, + }) + + const listPhotos = http.get(`${BASE_URL}/api/v1/support/photos?per_page=25`, authHeaders()) + check(listPhotos, { + 'list photos 200': (res) => res.status === 200, + }) + + sleep(Number(DEFAULT_SLEEP)) +} diff --git a/tests/Feature/Support/SupportApiTest.php b/tests/Feature/Support/SupportApiTest.php index 9159219..9d34b31 100644 --- a/tests/Feature/Support/SupportApiTest.php +++ b/tests/Feature/Support/SupportApiTest.php @@ -3,12 +3,14 @@ namespace Tests\Feature\Support; use App\Models\BlogCategory; +use App\Models\EventType; use App\Models\Photo; use App\Models\SuperAdminActionLog; use App\Models\Tenant; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Bus; +use Illuminate\Support\Facades\Hash; use Laravel\Sanctum\Sanctum; use Tests\TestCase; @@ -39,6 +41,100 @@ class SupportApiTest extends TestCase ->assertJsonStructure(['data', 'meta']); } + public function test_support_token_endpoint_issues_bearer_token_and_allows_api_access(): void + { + $user = User::factory()->create([ + 'role' => 'super_admin', + 'password' => Hash::make('secret-password'), + ]); + + Tenant::factory()->create(); + + $response = $this->postJson('/api/v1/support/auth/token', [ + 'login' => $user->email, + 'password' => 'secret-password', + ]); + + $response->assertOk() + ->assertJsonStructure(['token', 'token_type', 'abilities', 'user']); + + $token = $response->json('token'); + + $this->assertIsString($token); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->getJson('/api/v1/support/tenants') + ->assertOk() + ->assertJsonStructure(['data', 'meta']); + } + + public function test_support_requests_require_support_admin_ability(): void + { + $user = User::factory()->create([ + 'role' => 'super_admin', + ]); + + $token = $user->createToken('support-api', ['support:read'])->plainTextToken; + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->getJson('/api/v1/support/tenants'); + + $response->assertStatus(403) + ->assertJsonPath('error.code', 'support_forbidden'); + } + + public function test_support_write_requires_write_ability(): void + { + $user = User::factory()->create([ + 'role' => 'super_admin', + ]); + + $tenant = Tenant::factory()->create(); + + $token = $user->createToken('support-api', ['support-admin', 'support:read'])->plainTextToken; + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->patchJson('/api/v1/support/tenants/'.$tenant->id, [ + 'data' => [ + 'slug' => 'not-allowed', + ], + ]); + + $response->assertStatus(403) + ->assertJsonPath('error.code', 'forbidden'); + } + + public function test_support_read_only_resource_rejects_deletes(): void + { + $user = User::factory()->create([ + 'role' => 'super_admin', + ]); + + $eventType = EventType::factory()->create(); + + $token = $user->createToken('support-api', ['support-admin', 'support:write'])->plainTextToken; + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->deleteJson('/api/v1/support/event-types/'.$eventType->id); + + $response->assertStatus(403) + ->assertJsonPath('error.code', 'support_mutation_not_allowed'); + } + + public function test_expired_support_token_is_rejected(): void + { + $user = User::factory()->create([ + 'role' => 'super_admin', + ]); + + $token = $user->createToken('support-api', ['support-admin', 'support:read'], now()->subMinute())->plainTextToken; + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->getJson('/api/v1/support/tenants'); + + $response->assertStatus(401); + } + public function test_support_resource_update_rejects_invalid_fields(): void { $user = User::factory()->create([