presseportale/tests/Feature/Auth/MailAccessTest.php
Kevin Adametz 1efb30f9ce mail: Anmeldelink an die Adresse der Mail statt „in Ihrem Konto“
Roadmap 6.17. Veröffentlichungs- und Ablehnungsmails zu API-Meldungen gehen
an Pressekontakt oder Firmen-E-Mail — Empfänger ohne (bekanntes) Konto. Die
Mail sagte „in Ihrem Konto … mit Passwort oder Magic-Link“, gelesen wurde
„geht nicht“. Jetzt ein Kasten mit der Adresse und dem Knopf „Anmeldelink
anfordern“: signierter Link (30 Tage, Adresse verschlüsselt) auf /zugang,
ein Klick schickt den Anmeldelink, die Anmeldung landet auf der Meldung.
Ablehnung zu API-Meldungen: nur ansehen, neu eingereicht wird auf dem
ursprünglichen Weg. Kunden mit Konto: Knopf „Im pressekonto öffnen“.
Stufen 2 und 3 des Trichters messen die neue Seite mit.

Changelog: Verbessert
Public: Wer eine Veröffentlichungs- oder Ablehnungsmail bekommt, erhält mit einem Klick einen Anmeldelink an genau diese Adresse – ohne Konto und ohne Passwort.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:06:14 +00:00

157 lines
6.8 KiB
PHP

<?php
use App\Enums\ConversionEventType;
use App\Enums\ConversionSource;
use App\Enums\Portal;
use App\Mail\MagicLoginLink;
use App\Mail\PressReleasePublished;
use App\Mail\PressReleaseRejected;
use App\Models\Company;
use App\Models\ConversionEvent;
use App\Models\MagicLink;
use App\Models\PressRelease;
use App\Models\User;
use App\Services\Auth\MailAccessLink;
use Database\Seeders\RolesAndPermissionsSeeder;
use Illuminate\Support\Facades\Mail;
use Livewire\Livewire;
use Tests\TestCase;
/**
* Anmeldelink an die Adresse einer Kundenmail (Roadmap 6.17): Empfänger von
* API-Meldungen haben aus ihrer Sicht kein Konto. Ein Klick in der Mail
* schickt den Anmeldelink an genau ihre Adresse, die Anmeldung landet auf
* der Meldung.
*/
beforeEach(function (): void {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
Mail::fake();
});
function apiRelease(array $attributes = []): PressRelease
{
$company = Company::factory()->businessportal24()->create([
'name' => 'Firma GmbH', 'slug' => 'firma-gmbh', 'email' => 'presse@firma.de', 'is_active' => true,
]);
return PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create(array_merge([
'company_id' => $company->id, 'language' => 'de', 'title' => 'Firma GmbH eröffnet Standort',
], $attributes));
}
/**
* @return array{t: string, src: string}
*/
function accessQuery(string $url): array
{
parse_str((string) parse_url($url, PHP_URL_QUERY), $query);
return ['t' => (string) $query['t'], 'src' => (string) $query['src']];
}
// ------------------------------------------------------------------ Mails
test('the publication mail offers a sign-in link to the address it went to when the recipient has no account', function () {
/** @var TestCase $this */
$release = apiRelease();
$mail = new PressReleasePublished($release->user, $release, 'Frau Muster', 'presse@firma.de', true);
$html = $mail->render();
expect($mail->accessEmail)->toBe('presse@firma.de')
->and($mail->accessUrl)->toContain('/zugang?')
->and($mail->accessUrl)->not->toContain('presse@firma.de')
->and($mail->accessUrl)->not->toContain('presse%40firma.de')
->and($html)->toContain('Etwas ändern oder die Aufrufe sehen?')
->and($html)->toContain('weder ein Konto noch ein Passwort')
->and($html)->toContain('presse@firma.de')
->and($html)->toContain('den Newsroom von Firma GmbH pflegen')
->and($html)->toContain('Anmeldelink anfordern')
->and($html)->not->toContain('Im pressekonto öffnen')
->and($html)->not->toContain('mit Ihrem Passwort oder per Magic-Link');
});
test('the rejection mail lets recipients without an account view the release, resubmission stays with the original channel', function () {
/** @var TestCase $this */
$release = apiRelease(['status' => 'rejected']);
$html = (new PressReleaseRejected($release->user, $release, 'Kein Anlass erkennbar.', 'Frau Muster', 'presse@firma.de'))->render();
expect($html)->toContain('ohne Konto und ohne Passwort ansehen')
->and($html)->toContain('auf demselben Weg ein wie diese Meldung')
->and($html)->toContain('Anmeldelink anfordern')
->and($html)->not->toContain('Pressemitteilung bearbeiten');
// Wer selbst eingereicht hat, überarbeitet wie bisher im Konto.
$own = (new PressReleaseRejected($release->user, $release, 'Kein Anlass erkennbar.'))->render();
expect($own)->toContain('Pressemitteilung bearbeiten')
->and($own)->not->toContain('Anmeldelink anfordern');
});
// --------------------------------------------------------- Zugangsseite
test('the access page only opens with a valid signature and shows the address masked', function () {
/** @var TestCase $this */
$release = apiRelease();
$url = app(MailAccessLink::class)->url('presse@firma.de', $release, ConversionSource::MailPublish);
$this->get($url)
->assertOk()
->assertSeeText('p***@firma.de')
->assertSeeText('Firma GmbH eröffnet Standort')
->assertDontSeeText('presse@firma.de');
$this->get(str_replace('src=mail_publish', 'src=mail_reject', $url))->assertForbidden();
$opened = ConversionEvent::query()->where('type', ConversionEventType::ManagePageOpened->value)->sole();
expect($opened->source)->toBe(ConversionSource::MailPublish)
->and($opened->press_release_id)->toBe($release->id);
});
test('one click sends the sign-in link to that address and the login lands on the release', function () {
/** @var TestCase $this */
$release = apiRelease();
$url = app(MailAccessLink::class)->url('presse@firma.de', $release, ConversionSource::MailPublish);
Livewire::withQueryParams(accessQuery($url))
->test('auth.mail-access')
->call('requestLink')
->assertSet('sent', true)
->assertSee('p***@firma.de');
$user = User::query()->where('email', 'presse@firma.de')->firstOrFail();
Mail::assertQueued(MagicLoginLink::class, fn (MagicLoginLink $mail): bool => $mail->user->is($user));
$link = MagicLink::query()->where('user_id', $user->id)->latest('id')->firstOrFail();
expect($link->source)->toBe(ConversionSource::MailPublish->value)
->and($link->payload)->toBe(['company_id' => $release->company_id, 'press_release_id' => $release->id])
->and(ConversionEvent::query()->where('type', ConversionEventType::AccessRequested->value)->sole()->company_id)->toBe($release->company_id);
$plain = str_replace('/magic-login/', '', parse_url(Mail::queued(MagicLoginLink::class)->first()->loginUrl, PHP_URL_PATH));
$this->get('https://pressekonto.test/magic-login/'.ltrim($plain, '/'))
->assertRedirect(route('me.press-releases.show', ['id' => $release->id], absolute: false));
});
test('requests per address are limited', function () {
/** @var TestCase $this */
$release = apiRelease();
$query = accessQuery(app(MailAccessLink::class)->url('presse@firma.de', $release, ConversionSource::MailReject));
foreach (range(1, 3) as $i) {
Livewire::withQueryParams($query)->test('auth.mail-access')->call('requestLink')->assertHasNoErrors();
}
Livewire::withQueryParams($query)->test('auth.mail-access')->call('requestLink')->assertHasErrors('link');
Mail::assertQueuedCount(3);
});
test('a signed-in recipient goes straight to the release', function () {
/** @var TestCase $this */
$release = apiRelease();
$user = User::factory()->create(['email' => 'presse@firma.de', 'is_active' => true]);
$user->assignRole('customer');
$user->companies()->attach($release->company_id, ['role' => 'responsible']);
$this->actingAs($user)
->get(app(MailAccessLink::class)->url('presse@firma.de', $release, ConversionSource::MailPublish))
->assertRedirect(route('me.press-releases.show', ['id' => $release->id]));
});