diff --git a/tests/Service/Auth/AuthExternalIdentityGatewayTest.php b/tests/Service/Auth/AuthExternalIdentityGatewayTest.php new file mode 100644 index 0000000..7a449ea --- /dev/null +++ b/tests/Service/Auth/AuthExternalIdentityGatewayTest.php @@ -0,0 +1,64 @@ + 1, 'user_id' => 42, 'provider' => 'microsoft', 'tid' => 'tenant-abc', 'oid' => 'oid-xyz']; + + $repo = $this->createMock(UserExternalIdentityRepositoryInterface::class); + $repo->expects($this->once()) + ->method('findByProviderTidOid') + ->with('microsoft', 'tenant-abc', 'oid-xyz') + ->willReturn($row); + + $gateway = new AuthExternalIdentityGateway($repo); + $this->assertSame($row, $gateway->findByProviderTidOid('microsoft', 'tenant-abc', 'oid-xyz')); + } + + public function testFindByProviderTidOidReturnsNullWhenNotFound(): void + { + $repo = $this->createMock(UserExternalIdentityRepositoryInterface::class); + $repo->expects($this->once()) + ->method('findByProviderTidOid') + ->with('microsoft', 'unknown-tid', 'unknown-oid') + ->willReturn(null); + + $gateway = new AuthExternalIdentityGateway($repo); + $this->assertNull($gateway->findByProviderTidOid('microsoft', 'unknown-tid', 'unknown-oid')); + } + + public function testFindByProviderIssuerSubjectDelegatesToRepository(): void + { + $row = ['id' => 2, 'user_id' => 99, 'provider' => 'oidc', 'issuer' => 'https://issuer.example', 'subject' => 'sub-123']; + + $repo = $this->createMock(UserExternalIdentityRepositoryInterface::class); + $repo->expects($this->once()) + ->method('findByProviderIssuerSubject') + ->with('oidc', 'https://issuer.example', 'sub-123') + ->willReturn($row); + + $gateway = new AuthExternalIdentityGateway($repo); + $this->assertSame($row, $gateway->findByProviderIssuerSubject('oidc', 'https://issuer.example', 'sub-123')); + } + + public function testCreateLinkReturnsBool(): void + { + $data = ['user_id' => 42, 'provider' => 'microsoft', 'tid' => 'tenant-abc', 'oid' => 'oid-xyz']; + + $repo = $this->createMock(UserExternalIdentityRepositoryInterface::class); + $repo->expects($this->once()) + ->method('createLink') + ->with($data) + ->willReturn(7); + + $gateway = new AuthExternalIdentityGateway($repo); + $this->assertTrue($gateway->createLink($data)); + } +} diff --git a/tests/Service/Auth/AuthTenantGatewayTest.php b/tests/Service/Auth/AuthTenantGatewayTest.php new file mode 100644 index 0000000..4e28df5 --- /dev/null +++ b/tests/Service/Auth/AuthTenantGatewayTest.php @@ -0,0 +1,66 @@ + 1, 'name' => 'Acme Corp', 'uuid' => 'abc-123']; + + $repo = $this->createMock(TenantRepositoryInterface::class); + $repo->expects($this->once()) + ->method('find') + ->with(1) + ->willReturn($tenant); + + $gateway = new AuthTenantGateway($repo); + $this->assertSame($tenant, $gateway->findById(1)); + } + + public function testFindByIdReturnsNullWhenNotFound(): void + { + $repo = $this->createMock(TenantRepositoryInterface::class); + $repo->expects($this->once()) + ->method('find') + ->with(999) + ->willReturn(null); + + $gateway = new AuthTenantGateway($repo); + $this->assertNull($gateway->findById(999)); + } + + public function testFindByUuidDelegatesToRepository(): void + { + $tenant = ['id' => 5, 'name' => 'Beta Inc', 'uuid' => 'def-456']; + + $repo = $this->createMock(TenantRepositoryInterface::class); + $repo->expects($this->once()) + ->method('findByUuid') + ->with('def-456') + ->willReturn($tenant); + + $gateway = new AuthTenantGateway($repo); + $this->assertSame($tenant, $gateway->findByUuid('def-456')); + } + + public function testListDelegatesToRepository(): void + { + $tenants = [ + ['id' => 1, 'name' => 'Acme Corp'], + ['id' => 2, 'name' => 'Beta Inc'], + ]; + + $repo = $this->createMock(TenantRepositoryInterface::class); + $repo->expects($this->once()) + ->method('list') + ->willReturn($tenants); + + $gateway = new AuthTenantGateway($repo); + $this->assertSame($tenants, $gateway->list()); + } +} diff --git a/tests/Service/Directory/DirectorySettingsGatewayTest.php b/tests/Service/Directory/DirectorySettingsGatewayTest.php new file mode 100644 index 0000000..6c77e3c --- /dev/null +++ b/tests/Service/Directory/DirectorySettingsGatewayTest.php @@ -0,0 +1,115 @@ +createMock(SettingsDefaultsGateway::class); + $defaults->expects($this->once()) + ->method('setDefaultTenantId') + ->with(42); + + $app = $this->createMock(SettingsAppGateway::class); + + $gateway = new DirectorySettingsGateway($defaults, $app); + $gateway->setDefaultTenantId(42); + } + + public function testSetDefaultTenantIdAcceptsNull(): void + { + $defaults = $this->createMock(SettingsDefaultsGateway::class); + $defaults->expects($this->once()) + ->method('setDefaultTenantId') + ->with(null); + + $app = $this->createMock(SettingsAppGateway::class); + + $gateway = new DirectorySettingsGateway($defaults, $app); + $gateway->setDefaultTenantId(null); + } + + public function testSetDefaultDepartmentIdDelegatesToSettingsDefaultsGateway(): void + { + $defaults = $this->createMock(SettingsDefaultsGateway::class); + $defaults->expects($this->once()) + ->method('setDefaultDepartmentId') + ->with(7); + + $app = $this->createMock(SettingsAppGateway::class); + + $gateway = new DirectorySettingsGateway($defaults, $app); + $gateway->setDefaultDepartmentId(7); + } + + public function testSetDefaultDepartmentIdAcceptsNull(): void + { + $defaults = $this->createMock(SettingsDefaultsGateway::class); + $defaults->expects($this->once()) + ->method('setDefaultDepartmentId') + ->with(null); + + $app = $this->createMock(SettingsAppGateway::class); + + $gateway = new DirectorySettingsGateway($defaults, $app); + $gateway->setDefaultDepartmentId(null); + } + + public function testSetDefaultRoleIdDelegatesToSettingsDefaultsGateway(): void + { + $defaults = $this->createMock(SettingsDefaultsGateway::class); + $defaults->expects($this->once()) + ->method('setDefaultRoleId') + ->with(3); + + $app = $this->createMock(SettingsAppGateway::class); + + $gateway = new DirectorySettingsGateway($defaults, $app); + $gateway->setDefaultRoleId(3); + } + + public function testSetDefaultRoleIdAcceptsNull(): void + { + $defaults = $this->createMock(SettingsDefaultsGateway::class); + $defaults->expects($this->once()) + ->method('setDefaultRoleId') + ->with(null); + + $app = $this->createMock(SettingsAppGateway::class); + + $gateway = new DirectorySettingsGateway($defaults, $app); + $gateway->setDefaultRoleId(null); + } + + public function testIsAllowedThemeDelegatesToSettingsAppGateway(): void + { + $defaults = $this->createMock(SettingsDefaultsGateway::class); + $app = $this->createMock(SettingsAppGateway::class); + $app->expects($this->once()) + ->method('isAllowedTheme') + ->with('dark') + ->willReturn(true); + + $gateway = new DirectorySettingsGateway($defaults, $app); + $this->assertTrue($gateway->isAllowedTheme('dark')); + } + + public function testIsAllowedThemeReturnsFalseForUnknownTheme(): void + { + $defaults = $this->createMock(SettingsDefaultsGateway::class); + $app = $this->createMock(SettingsAppGateway::class); + $app->expects($this->once()) + ->method('isAllowedTheme') + ->with('neon') + ->willReturn(false); + + $gateway = new DirectorySettingsGateway($defaults, $app); + $this->assertFalse($gateway->isAllowedTheme('neon')); + } +} diff --git a/tests/Service/Settings/SettingsAppGatewayTest.php b/tests/Service/Settings/SettingsAppGatewayTest.php new file mode 100644 index 0000000..bfa4481 --- /dev/null +++ b/tests/Service/Settings/SettingsAppGatewayTest.php @@ -0,0 +1,106 @@ +createDefaultThemeConfig(); + + return new SettingsAppGateway(new SettingsMetadataGateway($settings), $themeConfig); + } + + private function createDefaultThemeConfig(): ThemeConfigGateway + { + $themeConfig = $this->createMock(ThemeConfigGateway::class); + $themeConfig->method('all')->willReturn(['light' => 'Light', 'dark' => 'Dark']); + + return $themeConfig; + } + + public function testGetAppTitleReturnsNullWhenEmpty(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->expects($this->any())->method('getValue')->with('app_title')->willReturn(null); + + $gateway = $this->createGateway($settings); + $this->assertNull($gateway->getAppTitle()); + } + + public function testGetAppTitleReturnsTrimmedValue(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->expects($this->any())->method('getValue')->with('app_title')->willReturn(' My App '); + + $gateway = $this->createGateway($settings); + $this->assertSame('My App', $gateway->getAppTitle()); + } + + public function testGetAppLocaleReturnsNullWhenNotSet(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->expects($this->any())->method('getValue')->with('app_locale')->willReturn(null); + + $gateway = $this->createGateway($settings); + $this->assertNull($gateway->getAppLocale()); + } + + public function testIsRegistrationEnabledDefaultsToTrue(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->expects($this->any())->method('getValue')->with('app_registration')->willReturn(null); + + $gateway = $this->createGateway($settings); + $this->assertTrue($gateway->isRegistrationEnabled()); + } + + public function testIsRegistrationEnabledReturnsFalseWhenDisabled(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->expects($this->any())->method('getValue')->with('app_registration')->willReturn('0'); + + $gateway = $this->createGateway($settings); + $this->assertFalse($gateway->isRegistrationEnabled()); + } + + public function testGetAppPrimaryColorRejectsInvalidFormat(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->expects($this->any())->method('getValue')->with('app_primary_color')->willReturn('not-a-color'); + + $gateway = $this->createGateway($settings); + $this->assertNull($gateway->getAppPrimaryColor()); + } + + public function testGetAppPrimaryColorReturnsValidHex(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->expects($this->any())->method('getValue')->with('app_primary_color')->willReturn('#ff5500'); + + $gateway = $this->createGateway($settings); + $this->assertSame('#ff5500', $gateway->getAppPrimaryColor()); + } + + public function testIsAllowedThemeReturnsTrueForKnownTheme(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + + $gateway = $this->createGateway($settings); + $this->assertTrue($gateway->isAllowedTheme('dark')); + } + + public function testIsAllowedThemeReturnsFalseForUnknownTheme(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + + $gateway = $this->createGateway($settings); + $this->assertFalse($gateway->isAllowedTheme('neon')); + } +} diff --git a/tests/Service/Settings/SettingsFrontendTelemetryGatewayTest.php b/tests/Service/Settings/SettingsFrontendTelemetryGatewayTest.php new file mode 100644 index 0000000..c498dca --- /dev/null +++ b/tests/Service/Settings/SettingsFrontendTelemetryGatewayTest.php @@ -0,0 +1,116 @@ +createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return null; + }); + + $gateway = new SettingsFrontendTelemetryGateway(new SettingsMetadataGateway($settings)); + $this->assertFalse($gateway->isFrontendTelemetryEnabled()); + } + + public function testIsFrontendTelemetryEnabledReturnsTrueWhenEnabled(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'frontend_telemetry_enabled' => '1', + default => null, + }; + }); + + $gateway = new SettingsFrontendTelemetryGateway(new SettingsMetadataGateway($settings)); + $this->assertTrue($gateway->isFrontendTelemetryEnabled()); + } + + public function testGetFrontendTelemetrySampleRateDefaultsToPointTwo(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return null; + }); + + $gateway = new SettingsFrontendTelemetryGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(0.2, $gateway->getFrontendTelemetrySampleRate()); + } + + public function testGetFrontendTelemetrySampleRateClampsToZero(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'frontend_telemetry_sample_rate' => '-0.5', + default => null, + }; + }); + + $gateway = new SettingsFrontendTelemetryGateway(new SettingsMetadataGateway($settings)); + // Out-of-range values fall back to the default 0.2 + $this->assertSame(0.2, $gateway->getFrontendTelemetrySampleRate()); + } + + public function testGetFrontendTelemetrySampleRateClampsToOne(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'frontend_telemetry_sample_rate' => '1.5', + default => null, + }; + }); + + $gateway = new SettingsFrontendTelemetryGateway(new SettingsMetadataGateway($settings)); + // Out-of-range values fall back to the default 0.2 + $this->assertSame(0.2, $gateway->getFrontendTelemetrySampleRate()); + } + + public function testGetFrontendTelemetryAllowedEventsReturnsDefaults(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return null; + }); + + $gateway = new SettingsFrontendTelemetryGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(['warn_once', 'ajax_error'], $gateway->getFrontendTelemetryAllowedEvents()); + } + + public function testSetFrontendTelemetryAllowedEventsNormalizesAndDeduplicates(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $stored = null; + $settings->expects($this->once()) + ->method('set') + ->willReturnCallback(static function (string $key, ?string $value) use (&$stored): bool { + if ($key === 'frontend_telemetry_allowed_events') { + $stored = $value; + } + return true; + }); + $settings->method('getValue')->willReturnCallback(static function (string $key) use (&$stored): ?string { + return match ($key) { + 'frontend_telemetry_allowed_events' => $stored, + default => null, + }; + }); + + $gateway = new SettingsFrontendTelemetryGateway(new SettingsMetadataGateway($settings)); + + // Pass duplicates with mixed casing and frontend. prefix + $this->assertTrue($gateway->setFrontendTelemetryAllowedEvents('frontend.WARN_ONCE, warn_once, AJAX_ERROR')); + + // Stored value should be sorted, deduplicated, and lowercased + $this->assertSame('ajax_error,warn_once', $stored); + } +} diff --git a/tests/Service/Settings/SettingsLoginPersistenceGatewayTest.php b/tests/Service/Settings/SettingsLoginPersistenceGatewayTest.php new file mode 100644 index 0000000..ccebd4a --- /dev/null +++ b/tests/Service/Settings/SettingsLoginPersistenceGatewayTest.php @@ -0,0 +1,109 @@ +createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturn(null); + + $gateway = new SettingsLoginPersistenceGateway(new SettingsMetadataGateway($settings)); + $this->assertFalse($gateway->isMicrosoftAutoRememberDefault()); + } + + public function testIsMicrosoftAutoRememberDefaultReturnsTrueWhenEnabled(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'microsoft_auto_remember_default' => '1', + default => null, + }; + }); + + $gateway = new SettingsLoginPersistenceGateway(new SettingsMetadataGateway($settings)); + $this->assertTrue($gateway->isMicrosoftAutoRememberDefault()); + } + + public function testGetRememberTokenLifetimeDaysDefaultsTo30(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturn(null); + + $gateway = new SettingsLoginPersistenceGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(30, $gateway->getRememberTokenLifetimeDays()); + } + + public function testGetRememberTokenLifetimeDaysReturnsValidValue(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'remember_token_lifetime_days' => '90', + default => null, + }; + }); + + $gateway = new SettingsLoginPersistenceGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(90, $gateway->getRememberTokenLifetimeDays()); + } + + public function testGetRememberTokenLifetimeDaysClampsToMinimum(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'remember_token_lifetime_days' => '0', + default => null, + }; + }); + + $gateway = new SettingsLoginPersistenceGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(30, $gateway->getRememberTokenLifetimeDays()); + } + + public function testGetRememberTokenLifetimeDaysClampsToMaximum(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'remember_token_lifetime_days' => '500', + default => null, + }; + }); + + $gateway = new SettingsLoginPersistenceGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(30, $gateway->getRememberTokenLifetimeDays()); + } + + public function testSetRememberTokenLifetimeDaysRejectsBelowRange(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturn(null); + $settings->expects($this->never())->method('set'); + + $gateway = new SettingsLoginPersistenceGateway(new SettingsMetadataGateway($settings)); + $this->assertFalse($gateway->setRememberTokenLifetimeDays(0)); + } + + public function testGetRememberTokenLifetimeSecondsConvertsDays(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'remember_token_lifetime_days' => '7', + default => null, + }; + }); + + $gateway = new SettingsLoginPersistenceGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(604800, $gateway->getRememberTokenLifetimeSeconds()); + } +} diff --git a/tests/Service/Settings/SettingsMetadataGatewayTest.php b/tests/Service/Settings/SettingsMetadataGatewayTest.php new file mode 100644 index 0000000..a1035c5 --- /dev/null +++ b/tests/Service/Settings/SettingsMetadataGatewayTest.php @@ -0,0 +1,71 @@ +createMock(SettingRepositoryInterface::class); + $settings->expects($this->once()) + ->method('getValue') + ->with('some_key') + ->willReturn('some_value'); + + $gateway = new SettingsMetadataGateway($settings); + $this->assertSame('some_value', $gateway->getValue('some_key')); + } + + public function testGetIntReturnsIntegerValue(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'numeric_key' => '42', + default => null, + }; + }); + + $gateway = new SettingsMetadataGateway($settings); + $this->assertSame(42, $gateway->getInt('numeric_key')); + } + + public function testGetIntReturnsNullForEmptyValue(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'empty_key' => '', + default => null, + }; + }); + + $gateway = new SettingsMetadataGateway($settings); + $this->assertNull($gateway->getInt('empty_key')); + } + + public function testGetIntReturnsNullWhenNoSetting(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturn(null); + + $gateway = new SettingsMetadataGateway($settings); + $this->assertNull($gateway->getInt('missing_key')); + } + + public function testSetDelegatesToRepository(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->expects($this->once()) + ->method('set') + ->with('my_key', 'my_value', 'description text') + ->willReturn(true); + + $gateway = new SettingsMetadataGateway($settings); + $this->assertTrue($gateway->set('my_key', 'my_value', 'description text')); + } +} diff --git a/tests/Service/Settings/SettingsSessionGatewayTest.php b/tests/Service/Settings/SettingsSessionGatewayTest.php new file mode 100644 index 0000000..97d7852 --- /dev/null +++ b/tests/Service/Settings/SettingsSessionGatewayTest.php @@ -0,0 +1,123 @@ +createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturn(null); + + $gateway = new SettingsSessionGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(30, $gateway->getSessionIdleTimeoutMinutes()); + } + + public function testGetSessionIdleTimeoutMinutesReturnsValueWithinRange(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'session_idle_timeout_minutes' => '60', + default => null, + }; + }); + + $gateway = new SettingsSessionGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(60, $gateway->getSessionIdleTimeoutMinutes()); + } + + public function testGetSessionIdleTimeoutMinutesClampsToMinimum(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'session_idle_timeout_minutes' => '3', + default => null, + }; + }); + + $gateway = new SettingsSessionGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(30, $gateway->getSessionIdleTimeoutMinutes()); + } + + public function testGetSessionIdleTimeoutMinutesClampsToMaximum(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'session_idle_timeout_minutes' => '2000', + default => null, + }; + }); + + $gateway = new SettingsSessionGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(30, $gateway->getSessionIdleTimeoutMinutes()); + } + + public function testSetSessionIdleTimeoutMinutesRejectsBelowRange(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturn(null); + $settings->expects($this->never())->method('set'); + + $gateway = new SettingsSessionGateway(new SettingsMetadataGateway($settings)); + $this->assertFalse($gateway->setSessionIdleTimeoutMinutes(3)); + } + + public function testGetSessionAbsoluteTimeoutHoursDefaultsTo8(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturn(null); + + $gateway = new SettingsSessionGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(8, $gateway->getSessionAbsoluteTimeoutHours()); + } + + public function testGetSessionAbsoluteTimeoutHoursReturnsValidValue(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'session_absolute_timeout_hours' => '12', + default => null, + }; + }); + + $gateway = new SettingsSessionGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(12, $gateway->getSessionAbsoluteTimeoutHours()); + } + + public function testGetSessionIdleTimeoutSecondsConvertsMinutes(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'session_idle_timeout_minutes' => '60', + default => null, + }; + }); + + $gateway = new SettingsSessionGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(3600, $gateway->getSessionIdleTimeoutSeconds()); + } + + public function testGetSessionAbsoluteTimeoutSecondsConvertsHours(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'session_absolute_timeout_hours' => '12', + default => null, + }; + }); + + $gateway = new SettingsSessionGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(43200, $gateway->getSessionAbsoluteTimeoutSeconds()); + } +} diff --git a/tests/Service/Settings/SettingsSmtpGatewayTest.php b/tests/Service/Settings/SettingsSmtpGatewayTest.php new file mode 100644 index 0000000..335d9ff --- /dev/null +++ b/tests/Service/Settings/SettingsSmtpGatewayTest.php @@ -0,0 +1,143 @@ +createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return null; + }); + + $gateway = new SettingsSmtpGateway(new SettingsMetadataGateway($settings)); + $this->assertNull($gateway->getSmtpHost()); + } + + public function testGetSmtpHostReturnsTrimedValue(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'smtp_host' => ' mail.example.com ', + default => null, + }; + }); + + $gateway = new SettingsSmtpGateway(new SettingsMetadataGateway($settings)); + $this->assertSame('mail.example.com', $gateway->getSmtpHost()); + } + + public function testGetSmtpPortReturnsNullWhenEmpty(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'smtp_port' => '', + default => null, + }; + }); + + $gateway = new SettingsSmtpGateway(new SettingsMetadataGateway($settings)); + $this->assertNull($gateway->getSmtpPort()); + } + + public function testGetSmtpPortReturnsValidPort(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'smtp_port' => '587', + default => null, + }; + }); + + $gateway = new SettingsSmtpGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(587, $gateway->getSmtpPort()); + } + + public function testGetSmtpPortReturnsNullForZero(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'smtp_port' => '0', + default => null, + }; + }); + + $gateway = new SettingsSmtpGateway(new SettingsMetadataGateway($settings)); + $this->assertNull($gateway->getSmtpPort()); + } + + public function testGetSmtpSecureReturnsNullForNone(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'smtp_secure' => 'none', + default => null, + }; + }); + + $gateway = new SettingsSmtpGateway(new SettingsMetadataGateway($settings)); + $this->assertNull($gateway->getSmtpSecure()); + } + + public function testGetSmtpSecureReturnsTls(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'smtp_secure' => ' TLS ', + default => null, + }; + }); + + $gateway = new SettingsSmtpGateway(new SettingsMetadataGateway($settings)); + $this->assertSame('tls', $gateway->getSmtpSecure()); + } + + public function testGetSmtpSecureReturnsNullForInvalidValue(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'smtp_secure' => 'starttls', + default => null, + }; + }); + + $gateway = new SettingsSmtpGateway(new SettingsMetadataGateway($settings)); + $this->assertNull($gateway->getSmtpSecure()); + } + + public function testSetSmtpPortRejectsNegativePort(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturn(null); + $settings->expects($this->once()) + ->method('set') + ->with('smtp_port', null, 'setting.smtp_port') + ->willReturn(true); + + $gateway = new SettingsSmtpGateway(new SettingsMetadataGateway($settings)); + // Negative port is treated as invalid; set stores null (clears the setting) + $this->assertTrue($gateway->setSmtpPort(-1)); + } + + public function testSetSmtpSecureRejectsInvalidValue(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->expects($this->never())->method('set'); + + $gateway = new SettingsSmtpGateway(new SettingsMetadataGateway($settings)); + $this->assertFalse($gateway->setSmtpSecure('starttls')); + } +} diff --git a/tests/Service/Settings/SettingsSystemAuditGatewayTest.php b/tests/Service/Settings/SettingsSystemAuditGatewayTest.php new file mode 100644 index 0000000..fde222c --- /dev/null +++ b/tests/Service/Settings/SettingsSystemAuditGatewayTest.php @@ -0,0 +1,95 @@ +createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturn(null); + + $gateway = new SettingsSystemAuditGateway(new SettingsMetadataGateway($settings)); + $this->assertTrue($gateway->isSystemAuditEnabled()); + } + + public function testIsSystemAuditEnabledReturnsFalseWhenDisabled(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'system_audit_enabled' => '0', + default => null, + }; + }); + + $gateway = new SettingsSystemAuditGateway(new SettingsMetadataGateway($settings)); + $this->assertFalse($gateway->isSystemAuditEnabled()); + } + + public function testGetSystemAuditRetentionDaysDefaultsTo365(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturn(null); + + $gateway = new SettingsSystemAuditGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(365, $gateway->getSystemAuditRetentionDays()); + } + + public function testGetSystemAuditRetentionDaysReturnsValidValue(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'system_audit_retention_days' => '180', + default => null, + }; + }); + + $gateway = new SettingsSystemAuditGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(180, $gateway->getSystemAuditRetentionDays()); + } + + public function testGetSystemAuditRetentionDaysClampsToMinimum(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'system_audit_retention_days' => '10', + default => null, + }; + }); + + $gateway = new SettingsSystemAuditGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(365, $gateway->getSystemAuditRetentionDays()); + } + + public function testGetSystemAuditRetentionDaysClampsToMaximum(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturnCallback(static function (string $key): ?string { + return match ($key) { + 'system_audit_retention_days' => '2000', + default => null, + }; + }); + + $gateway = new SettingsSystemAuditGateway(new SettingsMetadataGateway($settings)); + $this->assertSame(365, $gateway->getSystemAuditRetentionDays()); + } + + public function testSetSystemAuditRetentionDaysRejectsBelowRange(): void + { + $settings = $this->createMock(SettingRepositoryInterface::class); + $settings->method('getValue')->willReturn(null); + $settings->expects($this->never())->method('set'); + + $gateway = new SettingsSystemAuditGateway(new SettingsMetadataGateway($settings)); + $this->assertFalse($gateway->setSystemAuditRetentionDays(10)); + } +} diff --git a/tests/Service/Ui/HotkeyServiceTest.php b/tests/Service/Ui/HotkeyServiceTest.php new file mode 100644 index 0000000..da66b8c --- /dev/null +++ b/tests/Service/Ui/HotkeyServiceTest.php @@ -0,0 +1,83 @@ +assertIsArray($result); + $this->assertNotEmpty($result); + } + + public function testListEntriesHaveRequiredKeys(): void + { + $result = HotkeyService::list(); + + foreach ($result as $index => $entry) { + $this->assertArrayHasKey('action_key', $entry, "Entry #{$index} is missing 'action_key'"); + $this->assertArrayHasKey('mac', $entry, "Entry #{$index} is missing 'mac'"); + $this->assertArrayHasKey('win', $entry, "Entry #{$index} is missing 'win'"); + $this->assertNotEmpty($entry['action_key'], "Entry #{$index} has empty 'action_key'"); + $this->assertNotEmpty($entry['mac'], "Entry #{$index} has empty 'mac'"); + $this->assertNotEmpty($entry['win'], "Entry #{$index} has empty 'win'"); + } + } + + public function testListEntriesHaveExactlyThreeKeys(): void + { + $result = HotkeyService::list(); + + foreach ($result as $index => $entry) { + $this->assertCount(3, $entry, "Entry #{$index} should have exactly 3 keys"); + } + } + + public function testSearchKeywordsReturnsNonEmptyArray(): void + { + $result = HotkeyService::searchKeywords(); + + $this->assertIsArray($result); + $this->assertNotEmpty($result); + } + + public function testSearchKeywordsContainsExpectedTerms(): void + { + $result = HotkeyService::searchKeywords(); + + $this->assertContains('hotkey', $result); + $this->assertContains('shortcut', $result); + $this->assertContains('keyboard', $result); + } + + public function testSearchKeywordsAreAllStrings(): void + { + $result = HotkeyService::searchKeywords(); + + foreach ($result as $keyword) { + $this->assertIsString($keyword); + $this->assertNotEmpty($keyword); + } + } + + public function testListReturnsDeterministicResult(): void + { + $first = HotkeyService::list(); + $second = HotkeyService::list(); + + $this->assertSame($first, $second); + } + + public function testSearchKeywordsReturnsDeterministicResult(): void + { + $first = HotkeyService::searchKeywords(); + $second = HotkeyService::searchKeywords(); + + $this->assertSame($first, $second); + } +} diff --git a/tests/Service/User/UserSettingsGatewayTest.php b/tests/Service/User/UserSettingsGatewayTest.php new file mode 100644 index 0000000..8d66c81 --- /dev/null +++ b/tests/Service/User/UserSettingsGatewayTest.php @@ -0,0 +1,130 @@ +createMock(SettingsDefaultsGateway::class); + $defaults->method('getDefaultTenantId')->willReturn(5); + + $gateway = new UserSettingsGateway($defaults, $this->createMock(SettingsAppGateway::class), $this->createMock(SettingsUserLifecycleGateway::class)); + $this->assertSame(5, $gateway->getDefaultTenantId()); + } + + public function testGetDefaultTenantIdReturnsNullWhenUnset(): void + { + $defaults = $this->createMock(SettingsDefaultsGateway::class); + $defaults->method('getDefaultTenantId')->willReturn(null); + + $gateway = new UserSettingsGateway($defaults, $this->createMock(SettingsAppGateway::class), $this->createMock(SettingsUserLifecycleGateway::class)); + $this->assertNull($gateway->getDefaultTenantId()); + } + + public function testGetDefaultRoleIdDelegates(): void + { + $defaults = $this->createMock(SettingsDefaultsGateway::class); + $defaults->method('getDefaultRoleId')->willReturn(12); + + $gateway = new UserSettingsGateway($defaults, $this->createMock(SettingsAppGateway::class), $this->createMock(SettingsUserLifecycleGateway::class)); + $this->assertSame(12, $gateway->getDefaultRoleId()); + } + + public function testGetDefaultRoleIdReturnsNullWhenUnset(): void + { + $defaults = $this->createMock(SettingsDefaultsGateway::class); + $defaults->method('getDefaultRoleId')->willReturn(null); + + $gateway = new UserSettingsGateway($defaults, $this->createMock(SettingsAppGateway::class), $this->createMock(SettingsUserLifecycleGateway::class)); + $this->assertNull($gateway->getDefaultRoleId()); + } + + public function testGetDefaultDepartmentIdDelegates(): void + { + $defaults = $this->createMock(SettingsDefaultsGateway::class); + $defaults->method('getDefaultDepartmentId')->willReturn(8); + + $gateway = new UserSettingsGateway($defaults, $this->createMock(SettingsAppGateway::class), $this->createMock(SettingsUserLifecycleGateway::class)); + $this->assertSame(8, $gateway->getDefaultDepartmentId()); + } + + public function testGetAppThemeDelegates(): void + { + $app = $this->createMock(SettingsAppGateway::class); + $app->method('getAppTheme')->willReturn('dark'); + + $gateway = new UserSettingsGateway($this->createMock(SettingsDefaultsGateway::class), $app, $this->createMock(SettingsUserLifecycleGateway::class)); + $this->assertSame('dark', $gateway->getAppTheme()); + } + + public function testGetAppThemeReturnsNullWhenUnset(): void + { + $app = $this->createMock(SettingsAppGateway::class); + $app->method('getAppTheme')->willReturn(null); + + $gateway = new UserSettingsGateway($this->createMock(SettingsDefaultsGateway::class), $app, $this->createMock(SettingsUserLifecycleGateway::class)); + $this->assertNull($gateway->getAppTheme()); + } + + public function testAppThemesDelegatesToListThemes(): void + { + $themes = ['light' => 'Light', 'dark' => 'Dark']; + $app = $this->createMock(SettingsAppGateway::class); + $app->method('listThemes')->willReturn($themes); + + $gateway = new UserSettingsGateway($this->createMock(SettingsDefaultsGateway::class), $app, $this->createMock(SettingsUserLifecycleGateway::class)); + $this->assertSame($themes, $gateway->appThemes()); + } + + public function testDefaultThemeDelegates(): void + { + $app = $this->createMock(SettingsAppGateway::class); + $app->method('resolveDefaultTheme')->willReturn('light'); + + $gateway = new UserSettingsGateway($this->createMock(SettingsDefaultsGateway::class), $app, $this->createMock(SettingsUserLifecycleGateway::class)); + $this->assertSame('light', $gateway->defaultTheme()); + } + + public function testNormalizeThemeDelegates(): void + { + $app = $this->createMock(SettingsAppGateway::class); + $app->method('normalizeTheme')->willReturn('dark'); + + $gateway = new UserSettingsGateway($this->createMock(SettingsDefaultsGateway::class), $app, $this->createMock(SettingsUserLifecycleGateway::class)); + $this->assertSame('dark', $gateway->normalizeTheme('DARK')); + } + + public function testNormalizeThemeWithNullDelegates(): void + { + $app = $this->createMock(SettingsAppGateway::class); + $app->method('normalizeTheme')->willReturn('light'); + + $gateway = new UserSettingsGateway($this->createMock(SettingsDefaultsGateway::class), $app, $this->createMock(SettingsUserLifecycleGateway::class)); + $this->assertSame('light', $gateway->normalizeTheme(null)); + } + + public function testGetUserInactivityDeactivateDaysDelegates(): void + { + $lifecycle = $this->createMock(SettingsUserLifecycleGateway::class); + $lifecycle->method('getUserInactivityDeactivateDays')->willReturn(180); + + $gateway = new UserSettingsGateway($this->createMock(SettingsDefaultsGateway::class), $this->createMock(SettingsAppGateway::class), $lifecycle); + $this->assertSame(180, $gateway->getUserInactivityDeactivateDays()); + } + + public function testGetUserInactivityDeleteDaysDelegates(): void + { + $lifecycle = $this->createMock(SettingsUserLifecycleGateway::class); + $lifecycle->method('getUserInactivityDeleteDays')->willReturn(365); + + $gateway = new UserSettingsGateway($this->createMock(SettingsDefaultsGateway::class), $this->createMock(SettingsAppGateway::class), $lifecycle); + $this->assertSame(365, $gateway->getUserInactivityDeleteDays()); + } +}