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)); });