From 98afac24b14528c251da6b1e4d69c9c7dc816ec7 Mon Sep 17 00:00:00 2001 From: fs Date: Mon, 13 Apr 2026 20:46:20 +0200 Subject: [PATCH] test: add 92 tests for 12 untested gateway and service classes Cover Settings gateways (Session, LoginPersistence, SystemAudit, Smtp, FrontendTelemetry, Metadata, App), Auth gateways (ExternalIdentity, Tenant), DirectorySettings, UserSettings, and HotkeyService. Gateway test coverage rises from 25% to 52%, overall service/gateway coverage from 58.6% to 70.7%. Co-Authored-By: Claude Opus 4.6 --- .../Auth/AuthExternalIdentityGatewayTest.php | 64 ++++++++ tests/Service/Auth/AuthTenantGatewayTest.php | 66 ++++++++ .../DirectorySettingsGatewayTest.php | 115 ++++++++++++++ .../Settings/SettingsAppGatewayTest.php | 106 +++++++++++++ .../SettingsFrontendTelemetryGatewayTest.php | 116 ++++++++++++++ .../SettingsLoginPersistenceGatewayTest.php | 109 +++++++++++++ .../Settings/SettingsMetadataGatewayTest.php | 71 +++++++++ .../Settings/SettingsSessionGatewayTest.php | 123 +++++++++++++++ .../Settings/SettingsSmtpGatewayTest.php | 143 ++++++++++++++++++ .../SettingsSystemAuditGatewayTest.php | 95 ++++++++++++ tests/Service/Ui/HotkeyServiceTest.php | 83 ++++++++++ .../Service/User/UserSettingsGatewayTest.php | 130 ++++++++++++++++ 12 files changed, 1221 insertions(+) create mode 100644 tests/Service/Auth/AuthExternalIdentityGatewayTest.php create mode 100644 tests/Service/Auth/AuthTenantGatewayTest.php create mode 100644 tests/Service/Directory/DirectorySettingsGatewayTest.php create mode 100644 tests/Service/Settings/SettingsAppGatewayTest.php create mode 100644 tests/Service/Settings/SettingsFrontendTelemetryGatewayTest.php create mode 100644 tests/Service/Settings/SettingsLoginPersistenceGatewayTest.php create mode 100644 tests/Service/Settings/SettingsMetadataGatewayTest.php create mode 100644 tests/Service/Settings/SettingsSessionGatewayTest.php create mode 100644 tests/Service/Settings/SettingsSmtpGatewayTest.php create mode 100644 tests/Service/Settings/SettingsSystemAuditGatewayTest.php create mode 100644 tests/Service/Ui/HotkeyServiceTest.php create mode 100644 tests/Service/User/UserSettingsGatewayTest.php 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()); + } +}