create(['is_active' => true])); $plainTextToken = $user->createToken('API Client', ['press-releases:read'])->plainTextToken; $tokenId = $user->tokens()->value('id'); $this->withHeader('Authorization', "Bearer {$plainTextToken}") ->getJson('/api/v1/categories') ->assertOk(); $log = ApiUsageLog::query()->firstOrFail(); expect($log->user_id)->toBe($user->id) ->and($log->personal_access_token_id)->toBe($tokenId) ->and($log->method)->toBe('GET') ->and($log->path)->toBe('/api/v1/categories') ->and($log->status_code)->toBe(200) ->and(json_encode($log->toArray()))->not->toContain($plainTextToken); }); test('legacy api keys in query parameters are rejected and redacted from logs', function () { /** @var TestCase $this */ // Query-Keys werden nicht für die Auth akzeptiert, erhalten aber den // 410-Migrationshinweis (statt eines opaken 401) und bleiben aus den Logs. $this->getJson('/api/v1/categories?api_key=legacy-secret') ->assertStatus(410); $log = ApiUsageLog::query()->firstOrFail(); expect($log->user_id)->toBeNull() ->and($log->personal_access_token_id)->toBeNull() ->and($log->path)->toBe('/api/v1/categories') ->and($log->status_code)->toBe(410) ->and(json_encode($log->toArray()))->not->toContain('legacy-secret'); }); test('inactive users cannot use existing api tokens', function () { /** @var TestCase $this */ $user = User::factory()->create(['is_active' => false]); $plainTextToken = $user->createToken('Old API Client', ['press-releases:read'])->plainTextToken; $this->withHeader('Authorization', "Bearer {$plainTextToken}") ->getJson('/api/v1/categories') ->assertForbidden() ->assertJsonPath('message', 'API access is disabled for inactive users.'); expect(ApiUsageLog::query()->value('status_code'))->toBe(403); }); test('api rate limit is scoped to each personal access token', function () { /** @var TestCase $this */ $user = grantApiAccess(User::factory()->create(['is_active' => true])); $firstToken = $user->createToken('Busy Client', ['press-releases:read'])->plainTextToken; $secondToken = $user->createToken('Other Client', ['press-releases:read'])->plainTextToken; expect(strtok($firstToken, '|'))->not->toBe(strtok($secondToken, '|')); for ($request = 1; $request <= 60; $request++) { $this->withHeader('Authorization', "Bearer {$firstToken}") ->getJson('/api/v1/categories') ->assertOk(); } $this->withHeader('Authorization', "Bearer {$firstToken}") ->getJson('/api/v1/categories') ->assertStatus(429) ->assertJsonPath('message', 'API rate limit exceeded.'); $this->flushHeaders(); $this->withHeader('Authorization', "Bearer {$secondToken}") ->getJson('/api/v1/categories') ->assertOk(); }); /* * 08.10.2026: API-Zugang nur mit laufender Buchung — bei jedem Aufruf, nicht * nur beim Erzeugen des Tokens. Tokens bleiben erhalten und funktionieren * wieder, sobald erneut gebucht ist. */ test('tokens stop working once the booking has ended and work again after rebooking', function (string $path) { /** @var TestCase $this */ $user = grantApiAccess(User::factory()->create(['is_active' => true])); $plainTextToken = $user->createToken('Integration', ['press-releases:read'])->plainTextToken; $this->withToken($plainTextToken)->getJson($path)->assertSuccessful(); UserPaymentOption::query()->where('user_id', $user->id)->update([ 'status' => UserPaymentOptionStatus::Cancelled->value, 'current_period_end' => now()->subDay(), ]); $this->withToken($plainTextToken)->getJson($path) ->assertForbidden() ->assertJsonPath('message', 'API access requires an active subscription or booking.'); grantApiAccess($user); $this->withToken($plainTextToken)->getJson($path)->assertSuccessful(); expect($user->tokens()->count())->toBe(1); })->with([ 'api v1' => '/api/v1/categories', 'legacy service alias' => '/service/pressreleases', ]); test('tokens of accounts without any booking are rejected', function () { /** @var TestCase $this */ $user = User::factory()->create(['is_active' => true]); $plainTextToken = $user->createToken('Ohne Buchung', ['press-releases:read'])->plainTextToken; $this->withToken($plainTextToken)->getJson('/api/v1/categories')->assertForbidden(); }); test('expired tokens are rejected', function () { /** @var TestCase $this */ $user = grantApiAccess(User::factory()->create(['is_active' => true])); $plainTextToken = $user->createToken('Befristet', ['press-releases:read'], now()->addDay())->plainTextToken; $this->withToken($plainTextToken)->getJson('/api/v1/categories')->assertSuccessful(); $this->travel(2)->days(); // Der Test-Container behält den einmal aufgelösten Nutzer über Anfragen hinweg. app('auth')->forgetGuards(); $this->withToken($plainTextToken)->getJson('/api/v1/categories')->assertUnauthorized(); });