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']); Sanctum::actingAs($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]); Sanctum::actingAs($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(); Sanctum::actingAs($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(), ]); Sanctum::actingAs($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(); Sanctum::actingAs($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(), ]); Sanctum::actingAs($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(); });