Die Mail „Ihre Pressemitteilung wird persönlich geprüft“ verlinkte auch bei API-Meldungen in den Kundenbereich. Empfänger ohne Konto landeten auf der Passwort-Anmeldung. Wie in der Veröffentlichungs- und Ablehnungsmail schickt jetzt ein Klick einen Anmeldelink an die Adresse der Mail; die Zugangsseite übernimmt die Herkunft mail_review für den Funnel. Changelog: Verbessert Public: Auch die Mail zur persönlichen Prüfung führt Pressekontakte ohne Passwort zu ihrer Meldung. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
231 lines
10 KiB
PHP
231 lines
10 KiB
PHP
<?php
|
|
|
|
use App\Enums\ConversionEventType;
|
|
use App\Enums\ConversionSource;
|
|
use App\Enums\Portal;
|
|
use App\Enums\PressReleaseStatus;
|
|
use App\Mail\MagicLoginLink;
|
|
use App\Mail\PressReleaseHeldForReview;
|
|
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\PressReleaseStatusLog;
|
|
use App\Models\User;
|
|
use App\Services\Auth\MailAccessLink;
|
|
use App\Services\PressRelease\PressReleaseService;
|
|
use Database\Seeders\RolesAndPermissionsSeeder;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Illuminate\Support\Facades\Queue;
|
|
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 tells recipients without an account that they can revise and resubmit free of charge', 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('selbst überarbeiten und kostenlos erneut einreichen')
|
|
->and($html)->toContain('weder ein Konto noch ein Passwort')
|
|
->and($html)->toContain('presse@firma.de')
|
|
->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');
|
|
});
|
|
|
|
test('the held-for-review mail lets recipients without an account see the status via a sign-in link', function () {
|
|
/** @var TestCase $this */
|
|
$release = apiRelease(['status' => PressReleaseStatus::Review->value]);
|
|
$mail = new PressReleaseHeldForReview($release->user, $release, false, 'Frau Muster', 'presse@firma.de');
|
|
$html = $mail->render();
|
|
|
|
expect($mail->accessUrl)->toContain('/zugang?')
|
|
->and(accessQuery($mail->accessUrl)['src'])->toBe(ConversionSource::MailReview->value)
|
|
->and($html)->toContain('Sie müssen nichts tun')
|
|
->and($html)->toContain('Den Stand der Prüfung sehen?')
|
|
->and($html)->toContain('weder ein Konto noch ein Passwort')
|
|
->and($html)->toContain('presse@firma.de')
|
|
->and($html)->toContain('Anmeldelink anfordern')
|
|
->and($html)->not->toContain('Stand der Prüfung ansehen');
|
|
|
|
// Die Zugangsseite übernimmt die Herkunft aus der Prüfmail.
|
|
$this->get($mail->accessUrl)->assertOk();
|
|
expect(ConversionEvent::query()->where('type', ConversionEventType::ManagePageOpened->value)->sole()->source)
|
|
->toBe(ConversionSource::MailReview);
|
|
|
|
// Wer selbst eingereicht hat, öffnet die Meldung wie bisher im Konto.
|
|
$own = (new PressReleaseHeldForReview($release->user, $release, false))->render();
|
|
expect($own)->toContain('Stand der Prüfung ansehen')
|
|
->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]));
|
|
});
|
|
|
|
// ------------------------------------------- Wiedereinreichung (6.18)
|
|
|
|
test('a press contact revises a rejected api release, resubmits it free of charge and gets the result', function () {
|
|
/** @var TestCase $this */
|
|
Queue::fake();
|
|
$release = apiRelease(['status' => PressReleaseStatus::Rejected->value]);
|
|
PressReleaseStatusLog::query()->create([
|
|
'press_release_id' => $release->id,
|
|
'from_status' => PressReleaseStatus::Draft->value,
|
|
'to_status' => PressReleaseStatus::Review->value,
|
|
'source' => 'api',
|
|
]);
|
|
|
|
// So steht der Kontakt nach dem Anmeldelink da (ContactAccessService).
|
|
$contact = User::factory()->create(['email' => 'presse@firma.de', 'is_active' => true]);
|
|
$contact->assignRole('customer');
|
|
$contact->companies()->attach($release->company_id, ['role' => 'responsible']);
|
|
$quotaBefore = $release->user->pressReleaseQuotaRemaining();
|
|
|
|
$this->actingAs($contact);
|
|
|
|
expect(Gate::allows('update', $release))->toBeTrue()
|
|
->and(Gate::allows('submitForReview', $release))->toBeTrue();
|
|
|
|
$release->update(['title' => 'Firma GmbH eröffnet Standort in Bielefeld']);
|
|
app(PressReleaseService::class)->submitForReview($release->fresh());
|
|
|
|
$resubmitted = $release->fresh();
|
|
expect($resubmitted->status)->toBe(PressReleaseStatus::Review)
|
|
// Kostenlos: Ein Slot wird erst bei der Veröffentlichung verbraucht,
|
|
// die Wiedereinreichung selbst bucht nichts.
|
|
->and($release->user->fresh()->pressReleaseQuotaRemaining())->toBe($quotaBefore);
|
|
|
|
// Bleibt eine API-Meldung: Das Ergebnis geht an den Kontakt, nicht an das
|
|
// Konto des Einlieferers.
|
|
app(PressReleaseService::class)->publish($resubmitted);
|
|
|
|
Mail::assertQueued(PressReleasePublished::class, fn (PressReleasePublished $mail): bool => $mail->hasTo('presse@firma.de') && $mail->accessEmail === 'presse@firma.de');
|
|
Mail::assertNotQueued(PressReleasePublished::class, fn (PressReleasePublished $mail): bool => $mail->hasTo($release->user->email));
|
|
});
|