'site-key', 'services.turnstile.secret_key' => 'secret-key', ]); Mail::fake(); $this->turnstileResponse = fn () => Http::response(['success' => true]); Http::fake(['challenges.cloudflare.com/*' => fn () => ($this->turnstileResponse)()]); }); /** * @param array $overrides * @return array */ function registerPayload(array $overrides = []): array { return array_merge([ 'salutation' => 'ms', 'first_name' => 'Anna', 'last_name' => 'Müller', 'email' => 'anna@example.com', 'email-confirm' => 'anna@example.com', 'password' => 'geheim123', 'password_confirmation' => 'geheim123', 'accepted_data_protection' => '1', 'cf-turnstile-response' => 'token', FormSpamGuard::TIMESTAMP_FIELD => encrypt(now()->timestamp - 10), FormSpamGuard::HONEYPOT_FIELD => '', ], $overrides); } function makeSponsor(array $attributes = []): User { return User::forceCreate(array_merge([ 'email' => 'sponsor-'.uniqid().'@example.com', 'password' => bcrypt('secret'), 'lang' => 'de', 'active' => 1, 'blocked' => 0, 'payment_account' => now()->addYear(), ], $attributes)); } function memberNumber(User $user): string { return 'm'.($user->id + (int) config('mivita.add_number_id')); } it('zeigt das Registrierungsformular mit Turnstile statt reCAPTCHA', function () { $this->get('/registrierung') ->assertOk() ->assertSee('cf-turnstile', false) ->assertSee('data-sitekey="site-key"', false) ->assertSee(FormSpamGuard::HONEYPOT_FIELD, false) ->assertDontSee('g-recaptcha', false); }); it('legt bei gültiger Registrierung den Benutzer an und verschickt die Bestätigungsmail', function () { $this->post('/registrierung', registerPayload()) ->assertRedirect('/registrierung/finish'); $user = User::where('email', 'anna@example.com')->first(); expect($user)->not->toBeNull() ->and($user->account->first_name)->toBe('Anna') ->and($user->m_sponsor)->toBe(1); Mail::assertSent(MailVerifyAccount::class, fn ($mail) => $mail->hasTo('anna@example.com')); }); it('übernimmt einen aktiven Sponsor aus der Partner-Nummer', function () { $sponsor = makeSponsor(); $this->post('/registrierung', registerPayload(['from_member_id' => memberNumber($sponsor)])) ->assertRedirect('/registrierung/finish'); expect(User::where('email', 'anna@example.com')->first()->m_sponsor)->toBe($sponsor->id); }); it('ignoriert manipulierte Partner-Nummern von inaktiven oder unbekannten Beratern', function (Closure $memberId) { $this->post('/registrierung', registerPayload(['from_member_id' => $memberId()])) ->assertRedirect('/registrierung/finish'); expect(User::where('email', 'anna@example.com')->first()->m_sponsor)->toBe(1); })->with([ 'gesperrt' => [fn () => memberNumber(makeSponsor(['blocked' => 1]))], 'Account abgelaufen' => [fn () => memberNumber(makeSponsor(['payment_account' => now()->subDay()]))], 'unbekannt' => [fn () => 'm999999'], ]); it('lehnt Werbetexte, Links und fremde Schrift in Namensfeldern ab', function (string $name) { $this->post('/registrierung', registerPayload(['first_name' => $name])) ->assertSessionHasErrors('first_name'); expect(User::where('email', 'anna@example.com')->exists())->toBeFalse(); Mail::assertNothingSent(); })->with([ 'kyrillisch' => 'Приветствуем! Бонус 926USD', 'link' => 'Bonus https://share.google/abc', 'zu lang' => str_repeat('a', 51), ]); it('lehnt die Registrierung ab, wenn Turnstile das Token nicht bestätigt', function () { $this->turnstileResponse = fn () => Http::response(['success' => false]); $this->post('/registrierung', registerPayload()) ->assertSessionHasErrors('cf-turnstile-response'); expect(User::where('email', 'anna@example.com')->exists())->toBeFalse(); }); it('verlangt ein Passwort mit mindestens 8 Zeichen', function () { $this->post('/registrierung', registerPayload(['password' => 'kurz123', 'password_confirmation' => 'kurz123'])) ->assertSessionHasErrors('password'); expect(User::where('email', 'anna@example.com')->exists())->toBeFalse(); $this->post('/registrierung', registerPayload(['password' => 'genau8zk', 'password_confirmation' => 'genau8zk'])) ->assertSessionHasNoErrors(); expect(User::where('email', 'anna@example.com')->exists())->toBeTrue(); }); it('gibt das Passwort nach Fehlern nicht ins Formular zurück', function () { $this->post('/registrierung', registerPayload(['first_name' => 'Игорь'])) ->assertSessionHasErrors('first_name') ->assertSessionMissing('_old_input.password'); }); it('verwirft Bots still und legt keinen Benutzer an', function (Closure $overrides, string $reason) { Log::spy(); Log::shouldReceive('channel')->andReturnSelf(); $this->post('/registrierung', registerPayload($overrides())) ->assertRedirect('/registrierung/finish'); expect(User::where('email', 'anna@example.com')->exists())->toBeFalse(); Mail::assertNothingSent(); Log::shouldHaveReceived('channel')->with('form_spam'); Log::shouldHaveReceived('info')->withArgs( fn ($message, $context = []) => ($context['form'] ?? null) === 'register' && ($context['reason'] ?? null) === $reason ); })->with([ 'Honeypot ausgefüllt' => [fn () => [FormSpamGuard::HONEYPOT_FIELD => 'x'], 'honeypot'], 'zu schnell abgeschickt' => [fn () => [FormSpamGuard::TIMESTAMP_FIELD => encrypt(now()->timestamp)], 'too_fast'], 'Zeitstempel fehlt' => [fn () => [FormSpamGuard::TIMESTAMP_FIELD => ''], 'invalid_form_token'], ]); it('begrenzt die Anzahl der Registrierungen pro IP', function () { foreach (range(1, 5) as $i) { $this->post('/registrierung', registerPayload([ 'email' => "anna{$i}@example.com", 'email-confirm' => "anna{$i}@example.com", ]))->assertRedirect('/registrierung/finish'); } $this->from('/registrierung') ->post('/registrierung', registerPayload([ 'email' => 'anna6@example.com', 'email-confirm' => 'anna6@example.com', ])) ->assertRedirect('/registrierung') ->assertSessionHasErrors(['email' => __('form_protection.too_many_requests')]) ->assertSessionMissing('_old_input.password'); expect(User::where('email', 'anna6@example.com')->exists())->toBeFalse(); });