seed(RolesAndPermissionsSeeder::class); }); test('requesting access for an unknown email creates nothing and sends no mail', function () { /** @var TestCase $this */ Mail::fake(); $sent = app(ContactAccessService::class)->requestAccess('nobody@example.test'); expect($sent)->toBeFalse(); expect(User::query()->where('email', 'nobody@example.test')->exists())->toBeFalse(); Mail::assertNothingSent(); }); test('requesting access for a known contact lazily creates a scoped account and sends a link', function () { /** @var TestCase $this */ Mail::fake(); $company = Company::factory()->presseecho()->create(); Contact::factory()->for($company)->create([ 'email' => 'paula@example.test', 'first_name' => 'Paula', 'last_name' => 'Presse', 'portal' => $company->portal->value, ]); // Gross-/Kleinschreibung egal. $sent = app(ContactAccessService::class)->requestAccess('Paula@Example.test'); expect($sent)->toBeTrue(); $user = User::query()->where('email', 'paula@example.test')->firstOrFail(); expect($user->name)->toBe('Paula Presse'); expect($user->is_active)->toBeTrue(); expect($user->hasVerifiedEmail())->toBeTrue(); expect($user->hasRole('customer'))->toBeTrue(); expect($user->canAccessAdmin())->toBeFalse(); expect($user->canAccessCustomer())->toBeTrue(); expect($user->accessibleCompanyIds())->toContain($company->id); Mail::assertSent(MagicLoginLink::class, fn (MagicLoginLink $mail) => $mail->user->is($user)); }); test('requesting access reuses an existing user without creating a duplicate', function () { /** @var TestCase $this */ Mail::fake(); $company = Company::factory()->presseecho()->create(); Contact::factory()->for($company)->create([ 'email' => 'existing@example.test', 'portal' => $company->portal->value, ]); $existing = User::factory()->create(['email' => 'existing@example.test', 'is_active' => true]); $existing->assignRole('customer'); app(ContactAccessService::class)->requestAccess('existing@example.test'); expect(User::query()->where('email', 'existing@example.test')->count())->toBe(1); expect($existing->fresh()->accessibleCompanyIds())->toContain($company->id); Mail::assertSent(MagicLoginLink::class); }); test('the magic link form responds neutrally and triggers the service for a contact', function () { /** @var TestCase $this */ Mail::fake(); $company = Company::factory()->presseecho()->create(); Contact::factory()->for($company)->create([ 'email' => 'form@example.test', 'portal' => $company->portal->value, ]); Volt::test('auth.magic-link') ->set('email', 'form@example.test') ->call('requestLink') ->assertHasNoErrors() ->assertSet('email', ''); Mail::assertSent(MagicLoginLink::class); expect(User::query()->where('email', 'form@example.test')->exists())->toBeTrue(); }); test('the magic link form sends a link to an existing active user without a contact', function () { /** @var TestCase $this */ Mail::fake(); $user = User::factory()->create(['email' => 'plain@example.test', 'is_active' => true]); $user->assignRole('customer'); Volt::test('auth.magic-link') ->set('email', 'plain@example.test') ->call('requestLink') ->assertHasNoErrors() ->assertSet('email', ''); Mail::assertSent(MagicLoginLink::class, fn (MagicLoginLink $mail) => $mail->user->is($user)); }); test('requestMagicAccess does nothing for an unknown email', function () { /** @var TestCase $this */ Mail::fake(); $sent = app(ContactAccessService::class)->requestMagicAccess('nobody@example.test'); expect($sent)->toBeFalse(); expect(User::query()->where('email', 'nobody@example.test')->exists())->toBeFalse(); Mail::assertNothingSent(); }); test('requestMagicAccess does not send to an inactive existing user', function () { /** @var TestCase $this */ Mail::fake(); User::factory()->create(['email' => 'off@example.test', 'is_active' => false]); $sent = app(ContactAccessService::class)->requestMagicAccess('off@example.test'); expect($sent)->toBeFalse(); Mail::assertNothingSent(); }); test('a later-added company with the same email is linked on the next password login', function () { /** @var TestCase $this */ $user = User::factory()->create(['email' => 'sync@example.test', 'is_active' => true]); $user->assignRole('customer'); // Firma kommt NACH dem Account dazu (z. B. per API) und trägt dieselbe E-Mail. $company = Company::factory()->presseecho()->create(['email' => 'sync@example.test']); expect($user->accessibleCompanyIds())->not->toContain($company->id); Volt::test('auth.login') ->set('email', 'sync@example.test') ->set('password', 'password') ->call('login') ->assertHasNoErrors(); // Der Login-Hook (SyncCompanyMembershipsOnLogin) hat die Firma verknüpft. expect($user->fresh()->accessibleCompanyIds())->toContain($company->id); }); test('a magic-link account can manage (edit) the company it was linked to', function () { /** @var TestCase $this */ Mail::fake(); $company = Company::factory()->presseecho()->create(['email' => 'verantwortlich@example.test']); app(ContactAccessService::class)->requestMagicAccess('verantwortlich@example.test'); $user = User::query()->where('email', 'verantwortlich@example.test')->firstOrFail(); // Schreibzugriff (CompanyPolicy::update) – nicht nur Lesezugriff. expect($user->can('update', $company))->toBeTrue(); expect($user->can('view', $company))->toBeTrue(); }); test('an existing read-only member is upgraded to responsible on magic access', function () { /** @var TestCase $this */ Mail::fake(); $company = Company::factory()->presseecho()->create(['email' => 'upgrade@example.test']); $user = User::factory()->create(['email' => 'upgrade@example.test', 'is_active' => true]); $user->assignRole('customer'); // Vorher nur Lesezugriff. $user->companies()->attach($company->id, ['role' => 'member']); expect($user->can('update', $company))->toBeFalse(); app(ContactAccessService::class)->requestMagicAccess('upgrade@example.test'); expect($user->fresh()->can('update', $company))->toBeTrue(); // Genau ein Pivot-Eintrag, jetzt 'responsible'. expect($user->companies()->withoutGlobalScopes()->whereKey($company->id)->count())->toBe(1); }); test('a company email lazily creates an account scoped to that company', function () { /** @var TestCase $this */ Mail::fake(); $company = Company::factory()->presseecho()->create([ 'email' => 'firma@example.test', 'name' => 'Firma AG', ]); // Gross-/Kleinschreibung egal. $sent = app(ContactAccessService::class)->requestMagicAccess('Firma@Example.test'); expect($sent)->toBeTrue(); $user = User::query()->where('email', 'firma@example.test')->firstOrFail(); expect($user->is_active)->toBeTrue(); expect($user->hasVerifiedEmail())->toBeTrue(); expect($user->hasRole('customer'))->toBeTrue(); // Kein Kontaktname hinterlegt → Firmenname als Account-Name. expect($user->name)->toBe('Firma AG'); expect($user->accessibleCompanyIds())->toContain($company->id); Mail::assertSent(MagicLoginLink::class, fn (MagicLoginLink $mail) => $mail->user->is($user)); }); test('one account is created for an email stored across multiple companies and contacts', function () { /** @var TestCase $this */ Mail::fake(); // Dieselbe E-Mail an zwei Firmen (verschiedene Portale) direkt hinterlegt … $companyA = Company::factory()->presseecho()->create(['email' => 'multi@example.test']); $companyB = Company::factory()->businessportal24()->create(['email' => 'multi@example.test']); // … und zusätzlich als Pressekontakt einer dritten Firma. $companyC = Company::factory()->presseecho()->create(['email' => 'andere@example.test']); Contact::factory()->for($companyC)->create([ 'email' => 'multi@example.test', 'portal' => $companyC->portal->value, ]); $sent = app(ContactAccessService::class)->requestMagicAccess('multi@example.test'); expect($sent)->toBeTrue(); // Genau EIN Account für die E-Mail. expect(User::query()->where('email', 'multi@example.test')->count())->toBe(1); $user = User::query()->where('email', 'multi@example.test')->firstOrFail(); $ids = $user->accessibleCompanyIds(); // ALLE drei Firmen zugeordnet (zwei per Firmen-E-Mail, eine per Kontakt), // portalübergreifend. expect($ids)->toContain($companyA->id); expect($ids)->toContain($companyB->id); expect($ids)->toContain($companyC->id); Mail::assertSent(MagicLoginLink::class, 1); }); test('requesting access does not mutate an existing inactive account', function () { /** @var TestCase $this */ Mail::fake(); $company = Company::factory()->presseecho()->create(); Contact::factory()->for($company)->create([ 'email' => 'inactive@example.test', 'portal' => $company->portal->value, ]); $existing = User::factory()->create(['email' => 'inactive@example.test', 'is_active' => false]); $existing->assignRole('customer'); $sent = app(ContactAccessService::class)->requestAccess('inactive@example.test'); expect($sent)->toBeFalse(); // Keine Firmenzuordnung an einem deaktivierten Bestands-Account. expect($existing->fresh()->accessibleCompanyIds())->not->toContain($company->id); Mail::assertNothingSent(); }); test('the honeypot blocks bots without creating an account', function () { /** @var TestCase $this */ Mail::fake(); $company = Company::factory()->presseecho()->create(); Contact::factory()->for($company)->create([ 'email' => 'bot-target@example.test', 'portal' => $company->portal->value, ]); Volt::test('auth.magic-link') ->set('email', 'bot-target@example.test') ->set('website', 'http://spam.example') ->call('requestLink') ->assertHasNoErrors(); Mail::assertNothingSent(); expect(User::query()->where('email', 'bot-target@example.test')->exists())->toBeFalse(); });