createConfiguredMock(SettingsSessionGateway::class, [ 'getSessionIdleTimeoutMinutes' => 45, 'getSessionAbsoluteTimeoutHours' => 12, 'setSessionIdleTimeoutMinutes' => true, 'setSessionAbsoluteTimeoutHours' => true, ]); $settingsMetadataGateway = $this->createMock(SettingsMetadataGateway::class); $settingsMetadataGateway->method('getValue')->willReturn(null); $settingsMetadataGateway->method('get')->willReturnCallback( static fn (string $key): array => ['key' => $key, 'description' => 'desc-' . $key] ); $service = $this->newService($settingsSessionGateway, $settingsMetadataGateway); $data = $service->buildPageData(); $values = is_array($data['values'] ?? null) ? $data['values'] : []; $settings = is_array($data['settings'] ?? null) ? $data['settings'] : []; $this->assertSame(45, (int) ($values['session_idle_timeout_minutes'] ?? 0)); $this->assertSame(12, (int) ($values['session_absolute_timeout_hours'] ?? 0)); $this->assertSame( 'desc-session_idle_timeout_minutes', (string) (($settings['session_idle_timeout_minutes'] ?? [])['description'] ?? '') ); $this->assertSame( 'desc-session_absolute_timeout_hours', (string) (($settings['session_absolute_timeout_hours'] ?? [])['description'] ?? '') ); } public function testUpdateFromAdminPersistsValidSessionPolicyValues(): void { $settingsSessionGateway = $this->createMock(SettingsSessionGateway::class); $settingsSessionGateway->method('getSessionIdleTimeoutMinutes')->willReturn(30); $settingsSessionGateway->method('getSessionAbsoluteTimeoutHours')->willReturn(8); $settingsSessionGateway->expects($this->once()) ->method('setSessionIdleTimeoutMinutes') ->with(15) ->willReturn(true); $settingsSessionGateway->expects($this->once()) ->method('setSessionAbsoluteTimeoutHours') ->with(12) ->willReturn(true); $service = $this->newService($settingsSessionGateway); $result = $service->updateFromAdmin($this->validPostData([ 'session_idle_timeout_minutes' => '15', 'session_absolute_timeout_hours' => '12', ])); $this->assertSame([], $result['errors'] ?? []); } public function testUpdateFromAdminReturnsValidationErrorsForInvalidSessionPolicy(): void { $settingsSessionGateway = $this->createMock(SettingsSessionGateway::class); $settingsSessionGateway->method('getSessionIdleTimeoutMinutes')->willReturn(30); $settingsSessionGateway->method('getSessionAbsoluteTimeoutHours')->willReturn(8); $settingsSessionGateway->expects($this->once()) ->method('setSessionIdleTimeoutMinutes') ->with(2) ->willReturn(false); $settingsSessionGateway->expects($this->once()) ->method('setSessionAbsoluteTimeoutHours') ->with(100) ->willReturn(false); $service = $this->newService($settingsSessionGateway); $result = $service->updateFromAdmin($this->validPostData([ 'session_idle_timeout_minutes' => '2', 'session_absolute_timeout_hours' => '100', ])); $errors = is_array($result['errors'] ?? null) ? $result['errors'] : []; $errorKeys = []; foreach ($errors as $error) { if (is_array($error) && isset($error['key'])) { $errorKeys[] = (string) $error['key']; } } $this->assertContains('session_idle_timeout_invalid', $errorKeys); $this->assertContains('session_absolute_timeout_invalid', $errorKeys); } // --------------------------------------------------------------- // Login persistence settings // --------------------------------------------------------------- public function testBuildPageDataIncludesLoginPersistenceValues(): void { $loginPersistenceGateway = $this->createConfiguredMock(SettingsLoginPersistenceGateway::class, [ 'isMicrosoftAutoRememberDefault' => true, 'getRememberTokenLifetimeDays' => 14, ]); $service = $this->newService(loginPersistenceGateway: $loginPersistenceGateway); $data = $service->buildPageData(); $values = is_array($data['values'] ?? null) ? $data['values'] : []; $this->assertTrue((bool) ($values['microsoft_auto_remember_default'] ?? false)); $this->assertSame(14, (int) ($values['remember_token_lifetime_days'] ?? 0)); } public function testUpdateFromAdminPersistsLoginPersistenceValues(): void { $loginPersistenceGateway = $this->createMock(SettingsLoginPersistenceGateway::class); $loginPersistenceGateway->method('isMicrosoftAutoRememberDefault')->willReturn(false); $loginPersistenceGateway->method('getRememberTokenLifetimeDays')->willReturn(30); $loginPersistenceGateway->expects($this->once()) ->method('setMicrosoftAutoRememberDefault') ->with(true); $loginPersistenceGateway->expects($this->once()) ->method('setRememberTokenLifetimeDays') ->with(14) ->willReturn(true); $service = $this->newService(loginPersistenceGateway: $loginPersistenceGateway); $result = $service->updateFromAdmin($this->validPostData([ 'microsoft_auto_remember_default' => '1', 'remember_token_lifetime_days' => '14', ])); $this->assertSame([], $result['errors'] ?? []); } public function testUpdateFromAdminReturnsErrorForInvalidRememberTtl(): void { $loginPersistenceGateway = $this->createMock(SettingsLoginPersistenceGateway::class); $loginPersistenceGateway->method('isMicrosoftAutoRememberDefault')->willReturn(false); $loginPersistenceGateway->method('getRememberTokenLifetimeDays')->willReturn(30); $loginPersistenceGateway->method('setMicrosoftAutoRememberDefault'); $loginPersistenceGateway->expects($this->once()) ->method('setRememberTokenLifetimeDays') ->with(500) ->willReturn(false); $service = $this->newService(loginPersistenceGateway: $loginPersistenceGateway); $result = $service->updateFromAdmin($this->validPostData([ 'remember_token_lifetime_days' => '500', ])); $errors = is_array($result['errors'] ?? null) ? $result['errors'] : []; $errorKeys = []; foreach ($errors as $error) { if (is_array($error) && isset($error['key'])) { $errorKeys[] = (string) $error['key']; } } $this->assertContains('remember_token_lifetime_days_invalid', $errorKeys); } /** * @param array $overrides * @return array */ private function validPostData(array $overrides = []): array { $base = [ 'default_tenant_id' => '0', 'default_role_id' => '0', 'default_department_id' => '0', 'app_title' => 'Demo', 'app_locale' => 'de', 'app_registration' => '1', 'api_token_default_ttl_days' => '90', 'api_token_max_ttl_days' => '365', 'user_inactivity_deactivate_days' => '180', 'user_inactivity_delete_days' => '365', 'session_idle_timeout_minutes' => '30', 'session_absolute_timeout_hours' => '8', 'api_cors_allowed_origins' => '', 'system_audit_enabled' => '1', 'system_audit_retention_days' => '365', 'frontend_telemetry_enabled' => '1', 'frontend_telemetry_sample_rate' => '0.2', 'frontend_telemetry_allowed_events' => ['warn_once'], 'smtp_host' => '', 'smtp_port' => '', 'smtp_user' => '', 'smtp_password' => '', 'smtp_secure' => '', 'smtp_from' => '', 'smtp_from_name' => '', 'microsoft_shared_client_id' => '', 'microsoft_shared_client_secret' => '', 'microsoft_authority' => 'https://login.microsoftonline.com/common/v2.0', ]; return array_replace($base, $overrides); } private function newService( ?SettingsSessionGateway $settingsSessionGateway = null, ?SettingsMetadataGateway $settingsMetadataGateway = null, ?SettingsLoginPersistenceGateway $loginPersistenceGateway = null ): AdminSettingsService { $settingsDefaultsGateway = $this->createConfiguredMock(SettingsDefaultsGateway::class, [ 'getDefaultTenantId' => null, 'getDefaultRoleId' => null, 'getDefaultDepartmentId' => null, 'setDefaultTenantId' => true, 'setDefaultRoleId' => true, 'setDefaultDepartmentId' => true, ]); $settingsAppGateway = $this->createConfiguredMock(SettingsAppGateway::class, [ 'getAppLocale' => 'de', 'isRegistrationEnabled' => true, 'setAppTitle' => true, 'setAppLocale' => true, 'setRegistrationEnabled' => true, ]); $settingsApiPolicyGateway = $this->createConfiguredMock(SettingsApiPolicyGateway::class, [ 'getApiTokenDefaultTtlDays' => 90, 'getApiTokenMaxTtlDays' => 365, 'getApiCorsAllowedOriginsText' => '', 'setApiTokenDefaultTtlDays' => true, 'setApiTokenMaxTtlDays' => true, 'setApiCorsAllowedOrigins' => true, ]); $settingsUserLifecycleGateway = $this->createConfiguredMock(SettingsUserLifecycleGateway::class, [ 'getUserInactivityDeactivateDays' => 180, 'getUserInactivityDeleteDays' => 365, 'setUserInactivityDeactivateDays' => true, 'setUserInactivityDeleteDays' => true, ]); $settingsSessionGateway = $settingsSessionGateway ?? $this->createConfiguredMock(SettingsSessionGateway::class, [ 'getSessionIdleTimeoutMinutes' => 30, 'getSessionAbsoluteTimeoutHours' => 8, 'setSessionIdleTimeoutMinutes' => true, 'setSessionAbsoluteTimeoutHours' => true, ]); $settingsSystemAuditGateway = $this->createConfiguredMock(SettingsSystemAuditGateway::class, [ 'isSystemAuditEnabled' => true, 'getSystemAuditRetentionDays' => 365, 'setSystemAuditEnabled' => true, 'setSystemAuditRetentionDays' => true, ]); $settingsFrontendTelemetryGateway = $this->createConfiguredMock(SettingsFrontendTelemetryGateway::class, [ 'isFrontendTelemetryEnabled' => true, 'getFrontendTelemetrySampleRate' => 0.2, 'getFrontendTelemetryAllowedEvents' => ['warn_once'], 'setFrontendTelemetryEnabled' => true, 'setFrontendTelemetrySampleRate' => true, 'setFrontendTelemetryAllowedEvents' => true, ]); $settingsMicrosoftGateway = $this->createConfiguredMock(SettingsMicrosoftGateway::class, [ 'getMicrosoftAuthority' => 'https://login.microsoftonline.com/common/v2.0', 'setMicrosoftSharedClientId' => true, 'setMicrosoftSharedClientSecret' => true, 'setMicrosoftAuthority' => true, ]); $settingsSmtpGateway = $this->createConfiguredMock(SettingsSmtpGateway::class, [ 'getSmtpSecure' => '', 'setSmtpHost' => true, 'setSmtpPort' => true, 'setSmtpUser' => true, 'setSmtpPassword' => true, 'setSmtpSecure' => true, 'setSmtpFrom' => true, 'setSmtpFromName' => true, ]); $loginPersistenceGateway = $loginPersistenceGateway ?? $this->createConfiguredMock(SettingsLoginPersistenceGateway::class, [ 'isMicrosoftAutoRememberDefault' => false, 'getRememberTokenLifetimeDays' => 30, 'setMicrosoftAutoRememberDefault' => true, 'setRememberTokenLifetimeDays' => true, ]); if ($settingsMetadataGateway === null) { $settingsMetadataGateway = $this->createMock(SettingsMetadataGateway::class); $settingsMetadataGateway->method('getValue')->willReturn(null); $settingsMetadataGateway->method('get')->willReturnCallback( static fn (string $key): array => ['key' => $key, 'description' => 'setting.default'] ); } $settingCacheService = $this->createConfiguredMock(SettingCacheService::class, [ 'update' => true, ]); $tenantService = $this->createConfiguredMock(TenantService::class, [ 'list' => [], ]); $roleService = $this->createConfiguredMock(RoleService::class, [ 'listActive' => [], ]); $departmentService = $this->createConfiguredMock(DepartmentService::class, [ 'list' => [], ]); $rememberTokenRepository = $this->createConfiguredMock(RememberTokenRepository::class, [ 'countActive' => 0, ]); $apiTokenRepository = $this->createConfiguredMock(ApiTokenRepository::class, [ 'countActive' => 0, ]); $systemAuditService = $this->createConfiguredMock(AuditRecorderInterface::class, [ 'record' => null, ]); return new AdminSettingsService( $settingsDefaultsGateway, $settingsAppGateway, $settingsApiPolicyGateway, $settingsUserLifecycleGateway, $settingsSessionGateway, $settingsSystemAuditGateway, $settingsFrontendTelemetryGateway, $settingsMicrosoftGateway, $settingsSmtpGateway, $loginPersistenceGateway, $settingsMetadataGateway, $settingCacheService, $tenantService, $roleService, $departmentService, $rememberTokenRepository, $apiTokenRepository, $systemAuditService ); } }