create(); /** @var TenantMembershipManager $manager */ $manager = app(TenantMembershipManager::class); $membership = $manager->addMember( tenant: $tenant, actor: $owner, member: $member, role: 'readonly', source: 'manual', ); $manager->changeRole( tenant: $tenant, actor: $owner, membership: $membership, newRole: 'operator', ); $manager->removeMember( tenant: $tenant, actor: $owner, membership: $membership, ); $logs = AuditLog::query() ->where('tenant_id', $tenant->id) ->whereIn('action', [ AuditActionId::TenantMembershipAdd->value, AuditActionId::TenantMembershipRoleChange->value, AuditActionId::TenantMembershipRemove->value, ]) ->get() ->keyBy('action'); expect($logs)->toHaveCount(3); $addLog = $logs->get(AuditActionId::TenantMembershipAdd->value); $roleChangeLog = $logs->get(AuditActionId::TenantMembershipRoleChange->value); $removeLog = $logs->get(AuditActionId::TenantMembershipRemove->value); expect($addLog)->not->toBeNull(); expect($roleChangeLog)->not->toBeNull(); expect($removeLog)->not->toBeNull(); expect($addLog->status)->toBe('success'); expect($roleChangeLog->status)->toBe('success'); expect($removeLog->status)->toBe('success'); expect($addLog->metadata) ->toHaveKey('member_user_id', $member->id) ->toHaveKey('role', 'readonly') ->toHaveKey('source', 'manual') ->not->toHaveKey('member_email') ->not->toHaveKey('member_name'); expect($roleChangeLog->metadata) ->toHaveKey('member_user_id', $member->id) ->toHaveKey('from_role', 'readonly') ->toHaveKey('to_role', 'operator') ->not->toHaveKey('member_email') ->not->toHaveKey('member_name'); expect($removeLog->metadata) ->toHaveKey('member_user_id', $member->id) ->toHaveKey('role', 'operator') ->not->toHaveKey('member_email') ->not->toHaveKey('member_name'); }); it('writes a last-owner-blocked audit log when demoting or removing the last owner', function () { [$owner, $tenant] = createUserWithTenant(role: 'owner'); $membership = TenantMembership::query() ->where('tenant_id', $tenant->id) ->where('user_id', $owner->id) ->firstOrFail(); /** @var TenantMembershipManager $manager */ $manager = app(TenantMembershipManager::class); expect(fn () => $manager->changeRole( tenant: $tenant, actor: $owner, membership: $membership, newRole: 'manager', ))->toThrow(DomainException::class); expect(fn () => $manager->removeMember( tenant: $tenant, actor: $owner, membership: $membership, ))->toThrow(DomainException::class); $blockedLogs = AuditLog::query() ->where('tenant_id', $tenant->id) ->where('action', AuditActionId::TenantMembershipLastOwnerBlocked->value) ->where('status', 'blocked') ->get(); expect($blockedLogs->count())->toBeGreaterThanOrEqual(2); expect($blockedLogs->contains(fn (AuditLog $log): bool => ( ($log->metadata['member_user_id'] ?? null) === $owner->id && ($log->metadata['attempted_to_role'] ?? null) === 'manager' )))->toBeTrue(); expect($blockedLogs->contains(fn (AuditLog $log): bool => ( ($log->metadata['member_user_id'] ?? null) === $owner->id && ($log->metadata['attempted_action'] ?? null) === 'remove' )))->toBeTrue(); foreach ($blockedLogs as $log) { expect($log->metadata) ->not->toHaveKey('member_email') ->not->toHaveKey('member_name'); } });