Bisher blieb ein einmal erstellter Token gültig, auch wenn Abo bzw. Buchung längst beendet waren. EnsureApiAccessIsBooked prüft jetzt bei jeder Anfrage (v1 und /service/…-Altpfade), ob das Konto API-Zugang hat (ApiAccessEligibilityService::hasApiAccess), sonst 403. Die Tokens bleiben erhalten und funktionieren nach erneuter Buchung wieder. Kunden können beim Erstellen optional ein Ablaufdatum setzen (bis 5 Jahre); die Token-Liste zeigt „Gültig bis“ und einen Hinweis, wenn der Zugang pausiert ist. sanctum:prune-expired räumt abgelaufene Tokens nach 30 Tagen täglich ab. API-Doku (public/api/v1.yml) ergänzt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
153 lines
5.4 KiB
PHP
153 lines
5.4 KiB
PHP
<?php
|
|
|
|
use App\Enums\Portal;
|
|
use App\Models\Category;
|
|
use App\Models\Company;
|
|
use App\Models\NewsletterSubscription;
|
|
use App\Models\User;
|
|
use Tests\TestCase;
|
|
|
|
test('legacy api keys in query parameters are not accepted but get the migration hint', function () {
|
|
/** @var TestCase $this */
|
|
// Legacy-Keys sind nur noch in Headern zulässig; der Query-Parameter wird
|
|
// nicht für die Auth genutzt, liefert aber den 410-Migrationshinweis.
|
|
$this->getJson('/api/v1/categories?api_key=legacy-key')
|
|
->assertStatus(410)
|
|
->assertJsonStructure(['message', 'migration_url', 'docs_url']);
|
|
});
|
|
|
|
test('api user can read only own companies', function () {
|
|
/** @var TestCase $this */
|
|
$user = User::factory()->create();
|
|
$ownCompany = Company::factory()->presseecho()->create(['name' => 'Eigene Firma']);
|
|
$otherCompany = Company::factory()->businessportal24()->create(['name' => 'Fremde Firma']);
|
|
$user->companies()->attach($ownCompany->id, ['role' => 'owner']);
|
|
|
|
apiActingAs($user, ['companies:read']);
|
|
|
|
$this->getJson('/api/v1/companies')
|
|
->assertOk()
|
|
->assertSee('Eigene Firma')
|
|
->assertDontSee('Fremde Firma');
|
|
|
|
$this->getJson("/api/v1/companies/{$otherCompany->id}")
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('api user can list categories with press release read ability', function () {
|
|
/** @var TestCase $this */
|
|
$user = User::factory()->create();
|
|
Category::factory()->withTranslations()->create(['portal' => Portal::Both]);
|
|
|
|
apiActingAs($user, ['press-releases:read']);
|
|
|
|
$this->getJson('/api/v1/categories')
|
|
->assertOk()
|
|
->assertJsonCount(1, 'data')
|
|
->assertJsonStructure([
|
|
'data' => [
|
|
'*' => ['id', 'portal', 'translations'],
|
|
],
|
|
]);
|
|
});
|
|
|
|
test('api user can subscribe an email to newsletter', function () {
|
|
/** @var TestCase $this */
|
|
$user = User::factory()->create();
|
|
|
|
apiActingAs($user, ['newsletter:subscribe']);
|
|
|
|
$this->postJson('/api/v1/newsletter/subscribe', [
|
|
'portal' => 'presseecho',
|
|
'email' => 'newsletter@example.com',
|
|
'first_name' => 'Max',
|
|
'last_name' => 'Muster',
|
|
])
|
|
->assertCreated()
|
|
->assertJsonPath('data.email', 'newsletter@example.com')
|
|
->assertJsonPath('data.is_confirmed', false);
|
|
|
|
expect(NewsletterSubscription::withoutGlobalScopes()
|
|
->where('email', 'newsletter@example.com')
|
|
->where('portal', 'presseecho')
|
|
->exists())->toBeTrue();
|
|
});
|
|
|
|
test('resubscribing keeps an existing confirmation intact', function () {
|
|
/** @var TestCase $this */
|
|
$user = User::factory()->create();
|
|
$subscription = NewsletterSubscription::withoutGlobalScopes()->create([
|
|
'portal' => 'presseecho',
|
|
'user_id' => $user->id,
|
|
'email' => 'confirmed@example.com',
|
|
'is_confirmed' => true,
|
|
'confirmation_token' => null,
|
|
'subscribed_at' => now()->subMonth(),
|
|
'unsubscribed_at' => now()->subDay(),
|
|
]);
|
|
apiActingAs($user, ['newsletter:subscribe']);
|
|
|
|
$this->postJson('/api/v1/newsletter/subscribe', [
|
|
'portal' => 'presseecho',
|
|
'email' => 'confirmed@example.com',
|
|
'first_name' => 'Neu',
|
|
])->assertCreated()
|
|
->assertJsonPath('data.is_confirmed', true);
|
|
|
|
$subscription->refresh();
|
|
|
|
expect($subscription->is_confirmed)->toBeTrue()
|
|
->and($subscription->confirmation_token)->toBeNull()
|
|
->and($subscription->unsubscribed_at)->toBeNull()
|
|
->and($subscription->first_name)->toBe('Neu');
|
|
});
|
|
|
|
test('an orphaned subscription without owner can be rebound instead of being permanently blocked', function () {
|
|
/** @var TestCase $this */
|
|
// user_id NULL entsteht z. B. nach Konto-Löschung (FK nullOnDelete) oder
|
|
// durch Legacy-Import. Ohne Rebind wäre portal+email dauerhaft gesperrt.
|
|
$subscription = NewsletterSubscription::withoutGlobalScopes()->create([
|
|
'portal' => 'presseecho',
|
|
'user_id' => null,
|
|
'email' => 'orphan@example.com',
|
|
'is_confirmed' => false,
|
|
'subscribed_at' => now()->subMonth(),
|
|
]);
|
|
|
|
$user = User::factory()->create();
|
|
apiActingAs($user, ['newsletter:subscribe']);
|
|
|
|
$this->postJson('/api/v1/newsletter/subscribe', [
|
|
'portal' => 'presseecho',
|
|
'email' => 'orphan@example.com',
|
|
'first_name' => 'Wieder da',
|
|
])->assertCreated();
|
|
|
|
expect($subscription->fresh()->user_id)->toBe($user->id)
|
|
->and($subscription->fresh()->first_name)->toBe('Wieder da');
|
|
});
|
|
|
|
test('an api user cannot take over another newsletter subscription', function () {
|
|
/** @var TestCase $this */
|
|
$owner = User::factory()->create();
|
|
$attacker = User::factory()->create();
|
|
$subscription = NewsletterSubscription::withoutGlobalScopes()->create([
|
|
'portal' => 'presseecho',
|
|
'user_id' => $owner->id,
|
|
'email' => 'owned@example.com',
|
|
'first_name' => 'Original',
|
|
'is_confirmed' => true,
|
|
'subscribed_at' => now(),
|
|
]);
|
|
apiActingAs($attacker, ['newsletter:subscribe']);
|
|
|
|
$this->postJson('/api/v1/newsletter/subscribe', [
|
|
'portal' => 'presseecho',
|
|
'email' => 'owned@example.com',
|
|
'first_name' => 'Übernommen',
|
|
])->assertConflict();
|
|
|
|
expect($subscription->fresh()->user_id)->toBe($owner->id)
|
|
->and($subscription->fresh()->first_name)->toBe('Original')
|
|
->and($subscription->fresh()->is_confirmed)->toBeTrue();
|
|
});
|