seed(RolesAndPermissionsSeeder::class); }); /** * @return array{0: User, 1: Company, 2: Contact} */ function memberViaContact(string $email = 'ex-mitarbeiterin@firma.test'): array { $user = User::factory()->create(['email' => $email, 'is_active' => true]); $user->assignRole('customer'); $company = Company::factory()->presseecho()->create(['email' => 'zentrale@firma.test']); $contact = Contact::factory()->for($company)->create(['email' => $email, 'portal' => $company->portal->value]); app(ContactAccessService::class)->syncMembershipsForEmail($user); return [$user, $company, $contact]; } function membershipOf(User $user, Company $company): ?object { return DB::table('company_user')->where('user_id', $user->id)->where('company_id', $company->id)->first(); } test('deleting the contact ends the automatic access within the running session', function () { /** @var TestCase $this */ [$user, $company, $contact] = memberViaContact(); expect(membershipOf($user, $company)?->granted_via)->toBe('auto'); $this->actingAs($user)->get(route('me.press-kits.show', $company->id))->assertSuccessful(); $contact->delete(); expect(membershipOf($user, $company))->toBeNull(); $this->actingAs($user->fresh())->get(route('me.press-kits.show', $company->id))->assertNotFound(); }); test('changing or moving the contact ends the automatic access', function (Closure $change) { [$user, $company, $contact] = memberViaContact(); $change($contact); expect(membershipOf($user, $company))->toBeNull() ->and($user->fresh()->canAccessCompany($company->id))->toBeFalse(); })->with([ 'email changed' => fn (Contact $contact) => $contact->update(['email' => 'nachfolger@firma.test']), 'moved to another company' => fn (Contact $contact) => $contact->update([ 'company_id' => Company::factory()->presseecho()->create()->id, ]), ]); test('a contact moved into a company grants its automatic access right away', function () { $user = User::factory()->create(['email' => 'neu@firma.test', 'is_active' => true]); $user->assignRole('customer'); $source = Company::factory()->presseecho()->create(); $target = Company::factory()->presseecho()->create(); $contact = Contact::factory()->for($source)->create(['email' => 'neu@firma.test', 'portal' => $source->portal->value]); app(ContactAccessService::class)->syncMembershipsForEmail($user); $contact->update(['company_id' => $target->id]); expect(membershipOf($user, $source))->toBeNull() ->and(membershipOf($user, $target)?->granted_via)->toBe('auto'); }); test('changing the company address ends access granted through it', function () { $user = User::factory()->create(['email' => 'zentrale@firma.test', 'is_active' => true]); $user->assignRole('customer'); $company = Company::factory()->presseecho()->create(['email' => 'zentrale@firma.test']); app(ContactAccessService::class)->syncMembershipsForEmail($user); expect(membershipOf($user, $company)?->granted_via)->toBe('auto'); $company->update(['email' => 'info@firma.test']); expect(membershipOf($user, $company))->toBeNull(); }); test('access stays while a second contact with the same address remains', function () { [$user, $company, $contact] = memberViaContact(); Contact::factory()->for($company)->create(['email' => $user->email, 'portal' => $company->portal->value]); $contact->delete(); expect(membershipOf($user, $company)?->granted_via)->toBe('auto'); }); test('manual memberships survive and upgraded ones fall back to member', function () { $manual = User::factory()->create(['email' => 'manuell@firma.test', 'is_active' => true]); $upgraded = User::factory()->create(['email' => 'team@firma.test', 'is_active' => true]); $company = Company::factory()->presseecho()->create(); $manualContact = Contact::factory()->for($company)->create(['email' => 'manuell@firma.test', 'portal' => $company->portal->value]); $teamContact = Contact::factory()->for($company)->create(['email' => 'team@firma.test', 'portal' => $company->portal->value]); $manual->companies()->attach($company->id, ['role' => 'responsible', 'granted_via' => 'manual']); $upgraded->companies()->attach($company->id, ['role' => 'member', 'granted_via' => 'manual']); app(ContactAccessService::class)->syncMembershipsForEmail($upgraded); expect(membershipOf($upgraded, $company)->granted_via)->toBe('auto-upgrade'); $manualContact->delete(); $teamContact->delete(); expect(membershipOf($manual, $company)->role)->toBe('responsible') ->and(membershipOf($manual, $company)->granted_via)->toBe('manual') ->and(membershipOf($upgraded, $company)->role)->toBe('member') ->and(membershipOf($upgraded, $company)->granted_via)->toBe('manual'); }); test('the resync command removes stale automatic grants left by bulk updates', function () { /** @var TestCase $this */ [$user, $company, $contact] = memberViaContact(); [$keeper, $keptCompany] = memberViaContact('bleibt@firma.test'); // Massen-Update am Model vorbei: kein Observer. DB::table('contacts')->where('id', $contact->id)->update(['email' => 'anders@firma.test']); expect(membershipOf($user, $company))->not->toBeNull(); $this->artisan('memberships:resync-auto') ->expectsOutputToContain('Automatische Rechte vorher 2, nachher 1') ->assertSuccessful(); expect(membershipOf($user, $company))->toBeNull() ->and(membershipOf($keeper, $keptCompany)?->granted_via)->toBe('auto'); }); /* * Regression v1.4.1: Bestandskonten aus dem Altsystem haben kein * `email_verified_at`. Der Abgleich am 07.10.2026 entzog ihnen pauschal alle * automatischen Rechte (19.598 von 19.607), obwohl die Grundlage bestand. */ test('legacy accounts without verified email keep automatic access whose basis still exists', function () { /** @var TestCase $this */ $legacy = User::factory()->unverified()->create(['email' => 'altkunde@firma.test', 'is_active' => true]); $company = Company::factory()->presseecho()->create(); $contact = Contact::factory()->for($company)->create(['email' => 'altkunde@firma.test', 'portal' => $company->portal->value]); $legacy->companies()->attach($company->id, ['role' => 'responsible', 'granted_via' => 'auto']); $this->artisan('memberships:resync-auto')->assertSuccessful(); app(ContactAccessService::class)->syncMembershipsForEmail($legacy); expect(membershipOf($legacy, $company)?->granted_via)->toBe('auto'); // Fällt die Grundlage weg, endet das Recht auch für sie (S-03). $contact->delete(); expect(membershipOf($legacy, $company))->toBeNull(); }); test('inactive or unverified accounts do not receive new automatic access', function (array $attributes) { $user = User::factory()->create(['email' => 'neu-ohne-nachweis@firma.test', ...$attributes]); $company = Company::factory()->presseecho()->create(); Contact::factory()->for($company)->create(['email' => 'neu-ohne-nachweis@firma.test', 'portal' => $company->portal->value]); app(ContactAccessService::class)->syncMembershipsForEmail($user); expect(membershipOf($user, $company))->toBeNull(); })->with([ 'unverified' => [['email_verified_at' => null]], 'inactive' => [['is_active' => false]], ]); test('the resync command rolls back a suspicious mass revocation unless forced', function () { /** @var TestCase $this */ $company = Company::factory()->presseecho()->create(); $users = User::factory()->count(60)->create(); foreach ($users as $user) { $user->companies()->attach($company->id, ['role' => 'responsible', 'granted_via' => 'auto']); } $count = fn (): int => DB::table('company_user')->where('granted_via', 'auto')->count(); $this->artisan('memberships:resync-auto', ['--dry-run' => true]) ->expectsOutputToContain('vorher 60, nachher 0. Probelauf') ->assertSuccessful(); expect($count())->toBe(60); $this->artisan('memberships:resync-auto') ->expectsOutputToContain('zurückgerollt') ->assertFailed(); expect($count())->toBe(60); $this->artisan('memberships:resync-auto', ['--force' => true])->assertSuccessful(); expect($count())->toBe(0); });