createMock(SettingRepository::class); $settingRepository->expects($this->never())->method('set'); $tenantRepository = $this->createMock(TenantRepository::class); $tenantRepository->expects($this->once())->method('find')->with(99)->willReturn(null); $service = $this->newService($settingRepository, $tenantRepository); $this->assertFalse($service->setDefaultTenantId(99)); } public function testSetApiTokenDefaultTtlDaysRejectsAboveCurrentMax(): void { $settingRepository = $this->createMock(SettingRepository::class); $settingRepository->method('getValue')->willReturnCallback( static function (string $key): ?string { if ($key === SettingService::API_TOKEN_MAX_TTL_DAYS_KEY) { return '30'; } return null; } ); $settingRepository->expects($this->never())->method('set'); $service = $this->newService($settingRepository); $this->assertFalse($service->setApiTokenDefaultTtlDays(31)); } public function testSetApiTokenMaxTtlDaysUpdatesDefaultWhenNeeded(): void { $settingRepository = $this->createMock(SettingRepository::class); $settingRepository->method('getValue')->willReturnCallback( static function (string $key): ?string { if ($key === SettingService::API_TOKEN_DEFAULT_TTL_DAYS_KEY) { return '365'; } return null; } ); $calls = []; $settingRepository->expects($this->exactly(2)) ->method('set') ->willReturnCallback( static function (string $key, ?string $value, ?string $description) use (&$calls): bool { $calls[] = [$key, $value, $description]; return true; } ); $service = $this->newService($settingRepository); $this->assertTrue($service->setApiTokenMaxTtlDays(120)); $this->assertSame( [ [SettingService::API_TOKEN_DEFAULT_TTL_DAYS_KEY, '120', 'setting.api_token_default_ttl_days'], [SettingService::API_TOKEN_MAX_TTL_DAYS_KEY, '120', 'setting.api_token_max_ttl_days'], ], $calls ); } public function testSetUserInactivityDeleteDaysRequiresDeactivatePolicy(): void { $settingRepository = $this->createMock(SettingRepository::class); $settingRepository->method('getValue')->willReturnCallback( static function (string $key): ?string { if ($key === SettingService::USER_INACTIVITY_DEACTIVATE_DAYS_KEY) { return '0'; } return null; } ); $settingRepository->expects($this->never())->method('set'); $service = $this->newService($settingRepository); $this->assertFalse($service->setUserInactivityDeleteDays(30)); } public function testSetApiCorsAllowedOriginsRejectsInvalidOrigins(): void { $settingRepository = $this->createMock(SettingRepository::class); $settingRepository->expects($this->never())->method('set'); $service = $this->newService($settingRepository); $this->assertFalse($service->setApiCorsAllowedOrigins("https://ok.example\njavascript:alert(1)")); } public function testSetMicrosoftAuthorityRequiresHttps(): void { $settingRepository = $this->createMock(SettingRepository::class); $settingRepository->expects($this->never())->method('set'); $service = $this->newService($settingRepository); $this->assertFalse($service->setMicrosoftAuthority('http://login.microsoftonline.com/common/v2.0')); } public function testSetAppPrimaryColorNormalizesHexWithoutHash(): void { $settingRepository = $this->createMock(SettingRepository::class); $settingRepository->expects($this->once()) ->method('set') ->with(SettingService::APP_PRIMARY_COLOR_KEY, '#abc', 'setting.app_primary_color') ->willReturn(true); $service = $this->newService($settingRepository); $this->assertTrue($service->setAppPrimaryColor('abc')); } private function newService( SettingRepository $settingRepository, ?TenantRepository $tenantRepository = null, ?RoleRepository $roleRepository = null, ?DepartmentRepository $departmentRepository = null ): SettingService { $tenantRepository = $tenantRepository ?? $this->createMock(TenantRepository::class); $roleRepository = $roleRepository ?? $this->createMock(RoleRepository::class); $departmentRepository = $departmentRepository ?? $this->createMock(DepartmentRepository::class); $themeConfigService = $this->createMock(ThemeConfigService::class); $themeConfigService->method('all')->willReturn(['light' => 'Light', 'dark' => 'Dark']); return new SettingService( $settingRepository, $tenantRepository, $roleRepository, $departmentRepository, $themeConfigService ); } }