9]; $userTenantContextService = $this->createMock(UserTenantContextService::class); $userTenantContextService->method('getAvailableTenants')->willReturn([]); $userTenantContextService->expects($this->never())->method('getCurrentTenant'); $service = $this->createService($userTenantContextService); $service->hydrateForUser(5); $this->assertSame([], $_SESSION['available_tenants']); $this->assertTrue((bool) $_SESSION['no_active_tenant']); $this->assertArrayNotHasKey('current_tenant', $_SESSION); $this->assertArrayNotHasKey('module.addressbook.departments_by_tenant', $_SESSION); } public function testHydrateUsesFirstAvailableTenantAsFallback(): void { $availableTenants = [ ['id' => 7, 'uuid' => 'tenant-7'], ['id' => 8, 'uuid' => 'tenant-8'], ]; $userTenantContextService = $this->createMock(UserTenantContextService::class); $userTenantContextService->method('getAvailableTenants')->willReturn($availableTenants); $userTenantContextService->method('getCurrentTenant')->willReturn(null); $service = $this->createService($userTenantContextService); $service->hydrateForUser(5); $this->assertSame($availableTenants, $_SESSION['available_tenants']); $this->assertFalse((bool) $_SESSION['no_active_tenant']); $this->assertSame(7, (int) ($_SESSION['current_tenant']['id'] ?? 0)); $this->assertArrayNotHasKey('module.addressbook.departments_by_tenant', $_SESSION); } public function testHydratePreservesThemeAndPolicyFieldsInSession(): void { $availableTenants = [ [ 'id' => 3, 'uuid' => 'tenant-3', 'default_theme' => 'dark', 'allow_user_theme' => '0', ], ]; $userTenantContextService = $this->createMock(UserTenantContextService::class); $userTenantContextService->method('getAvailableTenants')->willReturn($availableTenants); $userTenantContextService->method('getCurrentTenant')->willReturn($availableTenants[0]); $service = $this->createService($userTenantContextService); $service->hydrateForUser(10); $tenant = $_SESSION['current_tenant'] ?? []; $this->assertSame('dark', $tenant['default_theme'] ?? null); $this->assertSame('0', $tenant['allow_user_theme'] ?? null); } public function testHydrateAndClearInvokeConfiguredModuleSessionProviders(): void { $fixturesDir = sys_get_temp_dir() . '/auth-session-provider-test-' . uniqid(); mkdir($fixturesDir . '/mod-session', 0777, true); file_put_contents( $fixturesDir . '/mod-session/module.php', ' 'mod-session', 'session_providers' => [DummyAuthSessionProvider::class], ], true) . ';' ); try { $registry = ModuleRegistry::boot($fixturesDir, ['mod-session']); $userTenantContextService = $this->createMock(UserTenantContextService::class); $userTenantContextService->method('getAvailableTenants')->willReturn([['id' => 7, 'uuid' => 'tenant-7']]); $userTenantContextService->method('getCurrentTenant')->willReturn(['id' => 7, 'uuid' => 'tenant-7']); $service = $this->createService($userTenantContextService, $registry); $_SESSION['user'] = ['id' => 42]; $service->hydrateForUser(42); $this->assertSame(1, DummyAuthSessionProvider::$populateCalls); $this->assertSame(42, (int) ($_SESSION['dummy_provider_user_id'] ?? 0)); $service->clearModuleSessionData(); $this->assertSame(1, DummyAuthSessionProvider::$clearCalls); $this->assertArrayNotHasKey('dummy_provider_user_id', $_SESSION); } finally { @unlink($fixturesDir . '/mod-session/module.php'); @rmdir($fixturesDir . '/mod-session'); @rmdir($fixturesDir); } } private function createService( UserTenantContextService $userTenantContextService, ?ModuleRegistry $registry = null ): AuthSessionTenantContextService { $registry ??= ModuleRegistry::boot(sys_get_temp_dir() . '/module-registry-empty-' . uniqid(), []); $container = new AppContainer(); $container->set(ModuleRegistry::class, static fn (): ModuleRegistry => $registry); return new AuthSessionTenantContextService( $userTenantContextService, new SessionStore(), $container ); } } final class DummyAuthSessionProvider implements SessionProvider { public static int $populateCalls = 0; public static int $clearCalls = 0; public function populate(array $user, AppContainer $container): void { self::$populateCalls++; $_SESSION['dummy_provider_user_id'] = (int) ($user['id'] ?? 0); } public function clear(): void { self::$clearCalls++; unset($_SESSION['dummy_provider_user_id']); } }