create([ 'is_active' => true, 'name' => 'Original Name', 'language' => 'de', ]); $this->actingAs($customer); Livewire::test('customer.profile') ->assertSee('Rechnungsadresse') ->assertSee('Agentur-Angaben') ->set('name', 'Neuer Anzeigename') ->set('agencyName', 'Muster Presseverteiler') ->set('firstName', 'Max') ->set('lastName', 'Mustermann') ->set('language', 'en') ->set('address', 'Musterfirma GmbH, Musterstrasse 1, 10115 Berlin') ->set('countryCode', 'AT') ->set('billingCompany', 'Musterfirma GmbH') ->set('billingFirstName', 'Max') ->set('billingLastName', 'Mustermann') ->set('billingAddress1', 'Musterstrasse 1') ->set('billingPostalCode', '10115') ->set('billingCity', 'Berlin') ->set('billingCountryCode', 'DE') ->set('taxIdNumber', 'DE123456789') ->set('showStats', true) ->call('saveProfile') ->assertHasNoErrors(); $customer->refresh(); expect($customer->name)->toBe('Neuer Anzeigename'); expect($customer->agency_name)->toBe('Muster Presseverteiler'); expect($customer->language)->toBe('en'); expect($customer->profile?->first_name)->toBe('Max'); expect($customer->profile?->last_name)->toBe('Mustermann'); expect($customer->profile?->address)->toBe('Musterfirma GmbH, Musterstrasse 1, 10115 Berlin'); expect($customer->profile?->country_code)->toBe('AT'); expect($customer->profile?->tax_id_number)->toBe('DE123456789'); expect($customer->profile?->show_stats)->toBeTrue(); expect($customer->billingAddress?->company)->toBe('Musterfirma GmbH'); expect($customer->billingAddress?->first_name)->toBe('Max'); expect($customer->billingAddress?->last_name)->toBe('Mustermann'); expect($customer->billingAddress?->name)->toBe('Max Mustermann'); expect($customer->billingAddress?->address1)->toBe('Musterstrasse 1'); expect($customer->billingAddress?->postal_code)->toBe('10115'); expect($customer->billingAddress?->city)->toBe('Berlin'); expect($customer->billingAddress?->country_code)->toBe('DE'); expect($customer->billingAddress?->vat_id)->toBe('DE123456789'); }); test('an incomplete billing address reports each missing field exactly once', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $this->actingAs($customer); Livewire::test('customer.profile') ->set('billingCompany', 'Nur Firma GmbH') ->call('saveProfile') ->assertHasErrors(['billingLastName', 'billingAddress1', 'billingPostalCode', 'billingCity']); }); test('every profile panel offers a save button', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $this->actingAs($customer); // 6 Panels (Persönliche Daten, Konto & Sicherheit, Rechnungsadresse, // Agentur-Angaben, E-Mail-Benachrichtigungen, Einstellungen) — jedes mit // eigenem Submit-Button für dasselbe Formular. $html = Livewire::test('customer.profile')->html(); expect(substr_count($html, 'type="submit"'))->toBe(6); }); test('a malformed vat id blocks saving with a field error', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $this->actingAs($customer); Livewire::test('customer.profile') ->set('taxIdNumber', '123-nicht-gueltig') ->call('saveProfile') ->assertHasErrors(['taxIdNumber']); }); test('the profile data can be copied into the billing address', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $this->actingAs($customer); Livewire::test('customer.profile') ->set('salutationKey', 'mr') ->set('firstName', 'Kopier') ->set('lastName', 'Kunde') ->set('countryCode', 'AT') ->call('copyProfileToBilling') ->assertSet('billingSalutationKey', 'mr') ->assertSet('billingFirstName', 'Kopier') ->assertSet('billingLastName', 'Kunde') ->assertSet('billingCountryCode', 'AT'); }); test('customer profile keeps company management out of the profile page', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $company = Company::factory()->presseecho()->create([ 'owner_user_id' => $customer->id, 'name' => 'Nicht im Profil geladene Firma', ]); $customer->companies()->attach($company->id, ['role' => 'owner']); $this->actingAs($customer); Livewire::test('customer.profile') ->assertSee('Rechnungsadresse') ->assertSee('Persönliche Daten') ->assertSee('Firmen verwalten') ->assertDontSee('Zugeordnete Firmen') ->assertDontSee('Nicht im Profil geladene Firma'); }); test('customer can save profile settings without a billing address', function () { /** @var TestCase $this */ $customer = User::factory()->create([ 'is_active' => true, 'name' => 'Ohne Rechnung', ]); $this->actingAs($customer); Livewire::test('customer.profile') ->set('name', 'Nur Profil') ->set('firstName', 'Nur') ->set('lastName', 'Profil') ->call('saveProfile') ->assertHasNoErrors(); expect($customer->refresh()->name)->toBe('Nur Profil'); expect($customer->billingAddress)->toBeNull(); }); test('customer security page renders password and email forms', function () { /** @var TestCase $this */ $customer = User::factory()->create([ 'is_active' => true, 'password' => bcrypt('current-password'), ]); $this->actingAs($customer); Livewire::test('customer.security') ->assertSee('Konto-Sicherheit') ->assertSee('Letzter Login') ->assertSee('Geräte und Anmeldungen') ->assertSee('Passwort ändern') ->assertSee('E-Mail-Adresse ändern') ->assertSee('Zwei-Faktor-Authentifizierung'); }); test('the security page lists the own recent logins with their method, not those of others', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $other = User::factory()->create(['is_active' => true]); foreach ([['magic-link', 2], ['google', 3], ['passkey', 40]] as [$method, $daysAgo]) { ActivityEvent::create([ 'type' => ActivityType::UserLoggedIn, 'occurred_at' => now()->subDays($daysAgo), 'user_id' => $customer->id, 'description' => 'Anmeldung', 'meta' => ['method' => $method], ]); } ActivityEvent::create([ 'type' => ActivityType::UserLoggedIn, 'occurred_at' => now()->subDay(), 'user_id' => $other->id, 'description' => 'Anmeldung', 'meta' => ['method' => 'impersonation'], ]); $this->actingAs($customer); Livewire::test('customer.security') ->assertSee('Dieses Gerät') ->assertSee('Anmeldung per Magic-Link') ->assertSee('Anmeldung mit Google') ->assertSee('Anmeldung mit Passkey') ->assertDontSee('Zugriff durch den Support') ->assertViewHas('loginsLast30Days', 2) ->assertViewHas('recentLogins', fn ($logins) => $logins->count() === 3); }); test('customer can change own password through security page', function () { /** @var TestCase $this */ $customer = User::factory()->create([ 'is_active' => true, 'password' => bcrypt('current-password'), ]); $this->actingAs($customer); Livewire::test('customer.security') ->set('current_password', 'current-password') ->set('password', 'new-strong-password') ->set('password_confirmation', 'new-strong-password') ->call('updatePassword') ->assertHasNoErrors(); $customer->refresh(); expect(Hash::check('new-strong-password', $customer->password))->toBeTrue(); }); test('two factor setup only becomes active after confirming an app code', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $this->actingAs($customer); $component = Livewire::test('customer.security') ->set('securityPassword', 'password') ->call('enableTwoFactorAuthentication'); $customer->refresh(); expect($customer->two_factor_secret)->not->toBeNull(); expect($customer->two_factor_confirmed_at)->toBeNull(); expect($customer->hasEnabledTwoFactorAuthentication())->toBeFalse(); $otp = app(Google2FA::class)->getCurrentOtp(decrypt($customer->two_factor_secret)); $component ->set('twoFactorCode', $otp) ->call('confirmTwoFactorAuthentication') ->assertHasNoErrors() ->assertSee('Wiederherstellungs-Codes'); $customer->refresh(); expect($customer->two_factor_confirmed_at)->not->toBeNull(); expect($customer->hasEnabledTwoFactorAuthentication())->toBeTrue(); }); test('an invalid two factor confirmation code is rejected', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $this->actingAs($customer); Livewire::test('customer.security') ->set('securityPassword', 'password') ->call('enableTwoFactorAuthentication') ->set('twoFactorCode', '000000') ->call('confirmTwoFactorAuthentication') ->assertHasErrors(['twoFactorCode']); expect($customer->refresh()->two_factor_confirmed_at)->toBeNull(); }); test('recovery codes stay hidden on a later visit to the security page', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $customer->forceFill([ 'two_factor_secret' => encrypt(app(Google2FA::class)->generateSecretKey()), 'two_factor_recovery_codes' => encrypt(json_encode(['geheim-code-eins', 'geheim-code-zwei'])), 'two_factor_confirmed_at' => now(), ])->save(); $this->actingAs($customer); Livewire::test('customer.security') ->assertDontSee('geheim-code-eins') ->call('regenerateRecoveryCodes') ->assertSee('Wiederherstellungs-Codes'); }); test('changing the email keeps the old address active and mails a confirm link to the new one', function () { /** @var TestCase $this */ Mail::fake(); $customer = User::factory()->create([ 'is_active' => true, 'email' => 'alt@example.com', 'email_verified_at' => now(), ]); $this->actingAs($customer); Livewire::test('customer.security') ->set('email', 'neu@example.com') ->set('securityPassword', 'password') ->call('updateEmail') ->assertHasNoErrors(); $customer->refresh(); expect($customer->email)->toBe('alt@example.com'); expect($customer->pending_email)->toBe('neu@example.com'); expect($customer->email_verified_at)->not->toBeNull(); Mail::assertSent(EmailChangeConfirmation::class, function ($mail) { return $mail->hasTo('neu@example.com') && str_contains($mail->confirmUrl, 'signature='); }); }); test('the signed link applies the pending email change', function () { /** @var TestCase $this */ $customer = User::factory()->create([ 'is_active' => true, 'email' => 'alt@example.com', 'pending_email' => 'neu@example.com', ]); $url = URL::temporarySignedRoute('email-change.confirm', now()->addHour(), [ 'user' => $customer->id, 'hash' => sha1('neu@example.com'), ]); $this->actingAs($customer) ->get($url) ->assertRedirect(route('me.security')); $customer->refresh(); expect($customer->email)->toBe('neu@example.com'); expect($customer->pending_email)->toBeNull(); expect($customer->email_verified_at)->not->toBeNull(); }); test('the email change link of another user is rejected', function () { /** @var TestCase $this */ $owner = User::factory()->create(['is_active' => true, 'pending_email' => 'neu@example.com']); $intruder = User::factory()->create(['is_active' => true]); $url = URL::temporarySignedRoute('email-change.confirm', now()->addHour(), [ 'user' => $owner->id, 'hash' => sha1('neu@example.com'), ]); $this->actingAs($intruder)->get($url)->assertForbidden(); expect($owner->refresh()->email)->not->toBe('neu@example.com'); }); test('an unsigned email change link is rejected', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true, 'pending_email' => 'neu@example.com']); $this->actingAs($customer) ->get(route('email-change.confirm', ['user' => $customer->id, 'hash' => sha1('neu@example.com')])) ->assertForbidden(); }); test('a pending email change can be discarded', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true, 'pending_email' => 'neu@example.com']); $this->actingAs($customer); Livewire::test('customer.security') ->call('cancelPendingEmailChange'); expect($customer->refresh()->pending_email)->toBeNull(); }); test('a pending email that got taken by another account meanwhile is discarded on confirm', function () { /** @var TestCase $this */ $customer = User::factory()->create([ 'is_active' => true, 'email' => 'alt@example.com', 'pending_email' => 'neu@example.com', ]); User::factory()->create(['email' => 'neu@example.com']); $url = URL::temporarySignedRoute('email-change.confirm', now()->addHour(), [ 'user' => $customer->id, 'hash' => sha1('neu@example.com'), ]); $this->actingAs($customer) ->get($url) ->assertRedirect(route('me.security')) ->assertSessionHas('security-error'); $customer->refresh(); expect($customer->email)->toBe('alt@example.com'); expect($customer->pending_email)->toBeNull(); }); test('press release policy denies access to other users releases', function () { /** @var TestCase $this */ $owner = User::factory()->create(['is_active' => true]); $intruder = User::factory()->create(['is_active' => true]); $pr = PressRelease::factory()->create([ 'user_id' => $owner->id, 'status' => 'draft', ]); expect($intruder->can('view', $pr))->toBeFalse(); expect($intruder->can('update', $pr))->toBeFalse(); expect($owner->can('view', $pr))->toBeTrue(); expect($owner->can('update', $pr))->toBeTrue(); }); test('me press release routes only resolve own press releases even for admins', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $otherUser = User::factory()->create(['is_active' => true]); $otherPressRelease = PressRelease::factory()->create([ 'user_id' => $otherUser->id, 'status' => 'draft', ]); $this->actingAs($admin); $this->get(route('me.press-releases.show', $otherPressRelease->id)) ->assertNotFound(); $this->get(route('me.press-releases.edit', $otherPressRelease->id)) ->assertNotFound(); }); test('customer press releases derive portal from selected company', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $company = Company::factory()->presseecho()->create(); $customer->companies()->attach($company->id, ['role' => 'owner']); $contact = Contact::factory()->for($company)->create(['portal' => $company->portal->value]); $this->actingAs($customer); Livewire::test('customer.press-releases.create') ->set('companyId', $company->id) ->set('portal', Portal::Businessportal24->value) ->set('categoryId', Category::factory()->create()->id) ->set('contactId', $contact->id) ->set('title', 'Neue Meldung fuer Presseecho') ->set('text', str_repeat('Dies ist ein ausreichend langer Testtext. ', 3)) ->call('save', 'draft') ->assertHasNoErrors(); $pressRelease = PressRelease::query()->where('user_id', $customer->id)->firstOrFail(); expect($pressRelease->portal)->toBe(Portal::Presseecho); }); test('customer press release detail shows assigned contacts and status history', function () { /** @var TestCase $this */ $customer = User::factory()->create([ 'is_active' => true, 'name' => 'Kunden Nutzer', ]); $company = Company::factory()->presseecho()->create([ 'name' => 'Alpha GmbH', ]); $customer->companies()->attach($company->id, ['role' => 'owner']); $pressRelease = PressRelease::factory()->forPortal(Portal::Presseecho)->create([ 'user_id' => $customer->id, 'company_id' => $company->id, 'title' => 'Alpha Detailmeldung', 'status' => PressReleaseStatus::Review->value, 'hits' => 1234, 'keywords' => 'Energie, Wasserstoff', ]); $contact = Contact::factory()->create([ 'company_id' => $company->id, 'portal' => Portal::Presseecho->value, 'first_name' => 'Paula', 'last_name' => 'Presse', 'responsibility' => 'PR Managerin', 'email' => 'paula@example.test', ]); $pressRelease->contacts()->attach($contact->id); PressReleaseStatusLog::query()->create([ 'press_release_id' => $pressRelease->id, 'changed_by_user_id' => $customer->id, 'from_status' => PressReleaseStatus::Draft->value, 'to_status' => PressReleaseStatus::Review->value, 'reason' => 'Zur Prüfung eingereicht', 'source' => 'customer', 'created_at' => now(), ]); $this->actingAs($customer); Livewire::test('customer.press-releases.show', ['id' => $pressRelease->id]) ->assertSee('Alpha Detailmeldung') ->assertSee('Status-Workflow') ->assertSee('Firma & Pressekontakt') ->assertSee('Alpha GmbH') ->assertSee('Paula Presse') ->assertSee('paula@example.test') ->assertSee('Status & Verlauf') ->assertSee('In Pruefung') ->assertSee('1.234') ->assertSee('Portal') ->assertSee('Kategorie') ->assertSee('Themen') ->assertSee('Energie') ->assertSee('Wasserstoff') ->assertSee('Zur Prüfung eingereicht') ->assertSee('durch Kunden Nutzer'); }); test('legacy invoice policy denies access to invoices of other users', function () { /** @var TestCase $this */ $owner = User::factory()->create(['is_active' => true]); $intruder = User::factory()->create(['is_active' => true]); $invoice = LegacyInvoice::query()->create([ 'legacy_portal' => 'presseecho', 'legacy_id' => 9001, 'user_id' => $owner->id, 'legacy_user_id' => 9001, 'number' => 'RE-OWN-9001', 'amount_cents' => 9900, 'total_cents' => 9900, 'status' => 'paid', 'invoice_date' => now()->subMonth(), 'imported_at' => now(), ]); expect($owner->can('view', $invoice))->toBeTrue(); expect($intruder->can('view', $invoice))->toBeFalse(); expect($intruder->can('downloadPdf', $invoice))->toBeFalse(); }); test('the fortify profile-information endpoint is switched off (Audit F01)', function () { /** @var TestCase $this */ $customer = User::factory()->create([ 'is_active' => true, 'email' => 'eigene@example.com', 'email_verified_at' => now(), ]); $this->actingAs($customer) ->put('/user/profile-information', ['name' => $customer->name, 'email' => 'fremd@example.com']) ->assertNotFound(); expect($customer->fresh()->email)->toBe('eigene@example.com'); expect($customer->fresh()->email_verified_at)->not->toBeNull(); }); test('recovery codes cannot be revealed by mutating the component property (Audit F03)', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $customer->forceFill([ 'two_factor_secret' => encrypt(app(Google2FA::class)->generateSecretKey()), 'two_factor_recovery_codes' => encrypt(json_encode(['geheim-code-eins', 'geheim-code-zwei'])), 'two_factor_confirmed_at' => now(), ])->save(); $this->actingAs($customer); Livewire::test('customer.security') ->set('showRecoveryCodes', true) ->assertDontSee('geheim-code-eins'); })->throws(CannotUpdateLockedPropertyException::class); test('disabling two-factor requires a current app code (Audit F03)', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $secret = app(Google2FA::class)->generateSecretKey(); $customer->forceFill([ 'two_factor_secret' => encrypt($secret), 'two_factor_recovery_codes' => encrypt(json_encode(['geheim-code-eins', 'geheim-code-zwei'])), 'two_factor_confirmed_at' => now(), ])->save(); $this->actingAs($customer); Livewire::test('customer.security') ->call('disableTwoFactorAuthentication') ->assertHasErrors(['securityCode']); Livewire::test('customer.security') ->set('securityCode', '000000') ->call('disableTwoFactorAuthentication') ->assertHasErrors(['securityCode']); expect($customer->fresh()->hasEnabledTwoFactorAuthentication())->toBeTrue(); Livewire::test('customer.security') ->set('securityCode', app(Google2FA::class)->getCurrentOtp($secret)) ->call('disableTwoFactorAuthentication') ->assertHasNoErrors(); expect($customer->fresh()->hasEnabledTwoFactorAuthentication())->toBeFalse(); }); test('regenerating recovery codes accepts a recovery code as proof and consumes it (Audit F03)', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $customer->forceFill([ 'two_factor_secret' => encrypt(app(Google2FA::class)->generateSecretKey()), 'two_factor_recovery_codes' => encrypt(json_encode(['geheim-code-eins', 'geheim-code-zwei'])), 'two_factor_confirmed_at' => now(), ])->save(); $this->actingAs($customer); Livewire::test('customer.security') ->call('regenerateRecoveryCodes') ->assertHasErrors(['securityCode']); expect($customer->fresh()->recoveryCodes())->toContain('geheim-code-eins'); Livewire::test('customer.security') ->set('securityCode', 'geheim-code-eins') ->call('regenerateRecoveryCodes') ->assertHasNoErrors() ->assertSee('Wiederherstellungs-Codes'); expect($customer->fresh()->recoveryCodes())->not->toContain('geheim-code-eins') ->and($customer->fresh()->recoveryCodes())->not->toContain('geheim-code-zwei'); }); test('cancelling an unconfirmed two-factor setup needs no proof', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $this->actingAs($customer); Livewire::test('customer.security') ->set('securityPassword', 'password') ->call('enableTwoFactorAuthentication') ->call('disableTwoFactorAuthentication') ->assertHasNoErrors(); expect($customer->fresh()->two_factor_secret)->toBeNull(); }); test('changing the email asks for the password, or for the app code once two-factor is active (Audit F03)', function () { /** @var TestCase $this */ Mail::fake(); $customer = User::factory()->create(['is_active' => true, 'email' => 'alt2@example.com']); $this->actingAs($customer); Livewire::test('customer.security') ->set('email', 'neu2@example.com') ->call('updateEmail') ->assertHasErrors(['securityPassword']); Livewire::test('customer.security') ->set('email', 'neu2@example.com') ->set('securityPassword', 'falsch') ->call('updateEmail') ->assertHasErrors(['securityPassword']); expect($customer->fresh()->pending_email)->toBeNull(); $secret = app(Google2FA::class)->generateSecretKey(); $customer->forceFill([ 'two_factor_secret' => encrypt($secret), 'two_factor_recovery_codes' => encrypt(json_encode(['r-eins'])), 'two_factor_confirmed_at' => now(), ])->save(); Livewire::test('customer.security') ->set('email', 'neu2@example.com') ->set('securityPassword', 'password') ->call('updateEmail') ->assertHasErrors(['securityCode']); Livewire::test('customer.security') ->set('email', 'neu2@example.com') ->set('securityCode', app(Google2FA::class)->getCurrentOtp($secret)) ->call('updateEmail') ->assertHasNoErrors(); expect($customer->fresh()->pending_email)->toBe('neu2@example.com'); }); /* * Sicherheitsaudit 07.10.2026, S-06: Ein passwortloses Konto ohne 2FA kann die * Adresse weiterhin ändern, aber nur mit einem Code an die BISHERIGE Adresse — * eine fremde Session allein reicht nicht mehr. */ test('a passwordless magic-link account confirms an email change with a code sent to its current address', function () { /** @var TestCase $this */ Mail::fake(); $customer = User::factory()->create(['is_active' => true, 'email' => 'magic@example.com']); // Wie im Altbestand (66.000 Konten): leeres Passwortfeld, kein Hash. Der // `hashed`-Cast würde auch '' hashen, daher direkt in die Tabelle. DB::table('users')->where('id', $customer->id)->update(['password' => '']); $this->actingAs($customer->fresh()); Livewire::test('customer.security') ->set('email', 'magic-neu@example.com') ->call('updateEmail') ->assertHasErrors(['securityEmailCode']); expect($customer->fresh()->pending_email)->toBeNull(); $component = Livewire::test('customer.security') ->call('requestSecurityEmailCode') ->assertHasNoErrors(); $code = null; Mail::assertQueued(SecurityConfirmationCode::class, function (SecurityConfirmationCode $mail) use (&$code) { $code = $mail->code; return $mail->hasTo('magic@example.com'); }); $component ->set('email', 'magic-neu@example.com') ->set('securityEmailCode', $code === '000000' ? '111111' : '000000') ->call('updateEmail') ->assertHasErrors(['securityEmailCode']); $component ->set('securityEmailCode', $code) ->call('updateEmail') ->assertHasNoErrors(); expect($customer->fresh()->pending_email)->toBe('magic-neu@example.com'); }); test('the email code is single use and burns after five wrong attempts', function () { /** @var TestCase $this */ Mail::fake(); $customer = User::factory()->create(['is_active' => true, 'email' => 'magic-burn@example.com']); DB::table('users')->where('id', $customer->id)->update(['password' => '']); $customer = $customer->fresh(); $confirmation = app(SecurityConfirmation::class); $confirmation->sendEmailCode($customer); $code = null; Mail::assertQueued(SecurityConfirmationCode::class, function (SecurityConfirmationCode $mail) use (&$code) { $code = $mail->code; return true; }); $wrong = $code === '000000' ? '111111' : '000000'; foreach (range(1, SecurityConfirmation::MAX_CODE_ATTEMPTS) as $attempt) { expect($confirmation->verifyEmailCode($customer, $wrong))->toBeFalse(); } expect($confirmation->verifyEmailCode($customer, $code))->toBeFalse(); $confirmation->sendEmailCode($customer); Mail::assertQueued(SecurityConfirmationCode::class, function (SecurityConfirmationCode $mail) use (&$code) { $code = $mail->code; return true; }); expect($confirmation->verifyEmailCode($customer, $code))->toBeTrue() ->and($confirmation->verifyEmailCode($customer, $code))->toBeFalse(); }); test('first-time two-factor setup needs a fresh identity proof', function () { /** @var TestCase $this */ $customer = User::factory()->create(['is_active' => true]); $this->actingAs($customer); Livewire::test('customer.security') ->call('enableTwoFactorAuthentication') ->assertHasErrors(['securityPassword']); Livewire::test('customer.security') ->set('securityPassword', 'falsch') ->call('enableTwoFactorAuthentication') ->assertHasErrors(['securityPassword']); expect($customer->fresh()->two_factor_secret)->toBeNull(); // Nach erfolgreichem Nachweis gilt die Session eine Weile als bestätigt. Livewire::test('customer.security') ->set('securityPassword', 'password') ->call('enableTwoFactorAuthentication') ->assertHasNoErrors() ->call('disableTwoFactorAuthentication') ->call('enableTwoFactorAuthentication') ->assertHasNoErrors(); expect($customer->fresh()->two_factor_secret)->not->toBeNull(); $this->travel(SecurityConfirmation::FRESH_FOR_SECONDS + 60)->seconds(); Livewire::test('customer.security') ->call('disableTwoFactorAuthentication') ->call('enableTwoFactorAuthentication') ->assertHasErrors(['securityPassword']); }); test('security changes are reported to the account address', function () { /** @var TestCase $this */ Mail::fake(); $customer = User::factory()->create(['is_active' => true, 'email' => 'hinweis@example.com']); $this->actingAs($customer); $component = Livewire::test('customer.security') ->set('securityPassword', 'password') ->call('enableTwoFactorAuthentication'); $otp = app(Google2FA::class)->getCurrentOtp(decrypt($customer->fresh()->two_factor_secret)); $component->set('twoFactorCode', $otp)->call('confirmTwoFactorAuthentication')->assertHasNoErrors(); Mail::assertQueued(SecurityChangeNotice::class, fn (SecurityChangeNotice $mail) => $mail->change === SecurityChange::TwoFactorEnabled && $mail->hasTo('hinweis@example.com')); Livewire::test('customer.security') ->set('current_password', 'password') ->set('password', 'new-strong-password') ->set('password_confirmation', 'new-strong-password') ->call('updatePassword') ->assertHasNoErrors(); Mail::assertQueued(SecurityChangeNotice::class, fn (SecurityChangeNotice $mail) => $mail->change === SecurityChange::PasswordChanged); }); test('a confirmed email change is reported to the previous address', function () { /** @var TestCase $this */ Mail::fake(); $customer = User::factory()->create([ 'is_active' => true, 'email' => 'vorher@example.com', 'pending_email' => 'nachher@example.com', ]); $url = URL::temporarySignedRoute('email-change.confirm', now()->addHour(), [ 'user' => $customer->id, 'hash' => sha1('nachher@example.com'), ]); $this->actingAs($customer)->get($url)->assertRedirect(route('me.security')); expect($customer->fresh()->email)->toBe('nachher@example.com'); Mail::assertQueued(SecurityChangeNotice::class, fn (SecurityChangeNotice $mail) => $mail->change === SecurityChange::EmailChanged && $mail->hasTo('vorher@example.com') && $mail->newEmail === 'nachher@example.com'); });