newsletter: Bestätigung/Ownership beim Re-Abo korrigieren (M7)

updateOrCreate ersetzt durch eine Ownership-Prüfung: ein fremder,
gesetzter Eigentümer wird abgelehnt (409), bestätigte Abos werden nicht
still ent-bestätigt. Verwaiste Datensätze (user_id NULL nach Konto-
Löschung/Legacy-Import) dürfen neu gebunden werden, statt dauerhaft
gesperrt zu bleiben.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-14 13:49:31 +00:00
parent ea8d017445
commit 1afcc364ac
2 changed files with 127 additions and 13 deletions

View file

@ -7,10 +7,10 @@ use App\Models\User;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;
test('unknown legacy api key requests receive gone response', function () {
test('legacy api keys in query parameters are not accepted but get the migration hint', function () {
/** @var TestCase $this */
// Übernahme 06.07.: importierte Legacy-Keys sind gültige Tokens
// (LegacyApiCompatibilityTest); nur unbekannte Keys bekommen 410.
// 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']);
@ -72,3 +72,82 @@ test('api user can subscribe an email to newsletter', function () {
->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();
});