Add LDAP as a third authentication option alongside local password and Microsoft Entra ID SSO. Per-tenant configuration supports Active Directory, OpenLDAP, and FreeIPA with configurable attribute mapping, search-then-bind and direct-bind methods, encrypted bind credentials (AES-256-GCM), profile sync on login, and user auto-provisioning via external identity linking. Includes admin UI for connection settings with test-connection button, login page LDAP form, 25 new PHPUnit tests, and de/en translations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
618 lines
27 KiB
PHP
618 lines
27 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Tests\Service\Auth;
|
|
|
|
use MintyPHP\Repository\Tenant\TenantLdapAuthRepositoryInterface;
|
|
use MintyPHP\Repository\Tenant\TenantMicrosoftAuthRepositoryInterface;
|
|
use MintyPHP\Service\Auth\AuthCryptoGateway;
|
|
use MintyPHP\Service\Auth\AuthSettingsGateway;
|
|
use MintyPHP\Service\Auth\AuthTenantGateway;
|
|
use MintyPHP\Service\Auth\TenantSsoService;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class TenantSsoServiceTest extends TestCase
|
|
{
|
|
public function testGetEffectiveMicrosoftProviderConfigReturnsSsoDisabledWhenTenantAuthDisabled(): void
|
|
{
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn(['enabled' => 0]);
|
|
|
|
$service = $this->newService($repository);
|
|
$result = $service->getEffectiveMicrosoftProviderConfig(['id' => 5]);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('sso_disabled', $result['error']);
|
|
}
|
|
|
|
public function testGetEffectiveMicrosoftProviderConfigRejectsInvalidTenantId(): void
|
|
{
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'enabled' => 1,
|
|
'entra_tenant_id' => 'invalid',
|
|
'use_shared_app' => 1,
|
|
]);
|
|
|
|
$service = $this->newService($repository);
|
|
$result = $service->getEffectiveMicrosoftProviderConfig(['id' => 5]);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('tenant_id_invalid', $result['error']);
|
|
}
|
|
|
|
public function testGetEffectiveMicrosoftProviderConfigRequiresSharedCredentials(): void
|
|
{
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'enabled' => 1,
|
|
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
|
'use_shared_app' => 1,
|
|
]);
|
|
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('getMicrosoftSharedClientId')->willReturn('');
|
|
$settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('');
|
|
|
|
$service = $this->newService($repository, null, $settingsGateway);
|
|
$result = $service->getEffectiveMicrosoftProviderConfig(['id' => 5]);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('shared_credentials_missing', $result['error']);
|
|
}
|
|
|
|
public function testGetEffectiveMicrosoftProviderConfigRequiresOverrideClientId(): void
|
|
{
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'enabled' => 1,
|
|
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
|
'use_shared_app' => 0,
|
|
'client_id_override' => '',
|
|
'client_secret_override_enc' => 'enc',
|
|
]);
|
|
|
|
$service = $this->newService($repository);
|
|
$result = $service->getEffectiveMicrosoftProviderConfig(['id' => 5]);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('client_id_override_required', $result['error']);
|
|
}
|
|
|
|
public function testGetEffectiveMicrosoftProviderConfigRejectsInvalidOverrideSecret(): void
|
|
{
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'enabled' => 1,
|
|
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
|
'use_shared_app' => 0,
|
|
'client_id_override' => 'client-id',
|
|
'client_secret_override_enc' => 'enc',
|
|
]);
|
|
|
|
$cryptoGateway = $this->createMock(AuthCryptoGateway::class);
|
|
$cryptoGateway->method('decryptString')->willThrowException(new \RuntimeException('invalid secret'));
|
|
|
|
$service = $this->newService($repository, null, null, $cryptoGateway);
|
|
$result = $service->getEffectiveMicrosoftProviderConfig(['id' => 5]);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('secret_invalid', $result['error']);
|
|
}
|
|
|
|
public function testGetEffectiveMicrosoftProviderConfigReturnsConfigForOverrideCredentials(): void
|
|
{
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'enabled' => 1,
|
|
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
|
'allowed_domains' => 'example.com',
|
|
'enforce_microsoft_login' => 1,
|
|
'sync_profile_on_login' => 1,
|
|
'sync_profile_fields' => 'first_name,avatar',
|
|
'use_shared_app' => 0,
|
|
'client_id_override' => 'client-id',
|
|
'client_secret_override_enc' => 'enc',
|
|
]);
|
|
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('getMicrosoftAuthority')->willReturn('https://login.microsoftonline.com/common/v2.0');
|
|
|
|
$cryptoGateway = $this->createMock(AuthCryptoGateway::class);
|
|
$cryptoGateway->method('decryptString')->willReturn('secret-value');
|
|
|
|
$service = $this->newService($repository, null, $settingsGateway, $cryptoGateway);
|
|
$result = $service->getEffectiveMicrosoftProviderConfig(['id' => 5]);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertSame('client-id', $result['config']['client_id']);
|
|
$this->assertSame('secret-value', $result['config']['client_secret']);
|
|
$this->assertTrue((bool) $result['config']['sync_profile_needs_graph']);
|
|
$this->assertSame('https://login.microsoftonline.com/common/v2.0', $result['config']['authority']);
|
|
$this->assertFalse((bool) $result['config']['use_shared_app']);
|
|
}
|
|
|
|
public function testResolveTenantLoginMethodsReturnsTenantNotFoundForInactiveTenant(): void
|
|
{
|
|
$tenantGateway = $this->createMock(AuthTenantGateway::class);
|
|
$tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'inactive']);
|
|
|
|
$service = $this->newService(null, $tenantGateway);
|
|
$result = $service->resolveTenantLoginMethods(5);
|
|
|
|
$this->assertFalse($result['local']);
|
|
$this->assertFalse($result['microsoft']);
|
|
$this->assertSame('tenant_not_found', $result['microsoft_reason']);
|
|
}
|
|
|
|
public function testResolveTenantLoginMethodsReturnsLocalOnlyWhenSsoDisabled(): void
|
|
{
|
|
$tenantGateway = $this->createMock(AuthTenantGateway::class);
|
|
$tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);
|
|
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn(['enabled' => 0]);
|
|
|
|
$service = $this->newService($repository, $tenantGateway);
|
|
$result = $service->resolveTenantLoginMethods(5);
|
|
|
|
$this->assertTrue($result['local']);
|
|
$this->assertFalse($result['microsoft']);
|
|
$this->assertSame('sso_disabled', $result['microsoft_reason']);
|
|
}
|
|
|
|
public function testBuildMicrosoftUiStateReturnsLocalOnlyPasswordModeWhenSsoDisabled(): void
|
|
{
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'enabled' => 0,
|
|
'enforce_microsoft_login' => 1,
|
|
]);
|
|
|
|
$service = $this->newService($repository);
|
|
$result = $service->buildMicrosoftUiState(5);
|
|
|
|
$this->assertSame('local_only', $result['password_mode']);
|
|
}
|
|
|
|
public function testBuildMicrosoftUiStateReturnsLocalAndMicrosoftPasswordModeWhenSsoEnabledWithoutEnforcement(): void
|
|
{
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'enabled' => 1,
|
|
'enforce_microsoft_login' => 0,
|
|
]);
|
|
|
|
$service = $this->newService($repository);
|
|
$result = $service->buildMicrosoftUiState(5);
|
|
|
|
$this->assertSame('local_and_microsoft', $result['password_mode']);
|
|
}
|
|
|
|
public function testBuildMicrosoftUiStateReturnsMicrosoftOnlyPasswordModeWhenEnforced(): void
|
|
{
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'enabled' => 1,
|
|
'enforce_microsoft_login' => 1,
|
|
]);
|
|
|
|
$service = $this->newService($repository);
|
|
$result = $service->buildMicrosoftUiState(5);
|
|
|
|
$this->assertSame('microsoft_only', $result['password_mode']);
|
|
}
|
|
|
|
public function testResolveTenantLoginMethodsReturnsMicrosoftOnlyWhenEnforcedAndConfigured(): void
|
|
{
|
|
$tenantGateway = $this->createMock(AuthTenantGateway::class);
|
|
$tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);
|
|
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'enabled' => 1,
|
|
'enforce_microsoft_login' => 1,
|
|
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
|
'use_shared_app' => 1,
|
|
]);
|
|
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('getMicrosoftSharedClientId')->willReturn('shared-client');
|
|
$settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('shared-secret');
|
|
$settingsGateway->method('getMicrosoftAuthority')->willReturn('https://login.microsoftonline.com/common/v2.0');
|
|
|
|
$service = $this->newService($repository, $tenantGateway, $settingsGateway);
|
|
$result = $service->resolveTenantLoginMethods(5);
|
|
|
|
$this->assertFalse($result['local']);
|
|
$this->assertTrue($result['microsoft']);
|
|
$this->assertSame('', $result['microsoft_reason']);
|
|
}
|
|
|
|
public function testSaveTenantMicrosoftAuthReturnsErrorWhenTenantDoesNotExist(): void
|
|
{
|
|
$tenantGateway = $this->createMock(AuthTenantGateway::class);
|
|
$tenantGateway->method('findById')->willReturn(null);
|
|
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->expects($this->never())->method('upsertByTenantId');
|
|
|
|
$service = $this->newService($repository, $tenantGateway);
|
|
$result = $service->saveTenantMicrosoftAuth(5, ['microsoft_enabled' => '1']);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertNotEmpty($result['errors']);
|
|
}
|
|
|
|
public function testSaveTenantMicrosoftAuthValidatesCryptoWhenOverrideSecretIsProvided(): void
|
|
{
|
|
$tenantGateway = $this->createMock(AuthTenantGateway::class);
|
|
$tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);
|
|
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'enabled' => 0,
|
|
'use_shared_app' => 0,
|
|
'client_secret_override_enc' => '',
|
|
]);
|
|
$repository->expects($this->never())->method('upsertByTenantId');
|
|
|
|
$cryptoGateway = $this->createMock(AuthCryptoGateway::class);
|
|
$cryptoGateway->method('isConfigured')->willReturn(false);
|
|
|
|
$service = $this->newService($repository, $tenantGateway, null, $cryptoGateway);
|
|
$result = $service->saveTenantMicrosoftAuth(5, [
|
|
'microsoft_enabled' => '1',
|
|
'use_shared_app' => '',
|
|
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
|
'client_id_override' => 'client-id',
|
|
'client_secret_override' => 'new-secret',
|
|
]);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertNotEmpty($result['errors']);
|
|
}
|
|
|
|
public function testSaveTenantMicrosoftAuthPersistsNormalizedSharedAppPayload(): void
|
|
{
|
|
$tenantGateway = $this->createMock(AuthTenantGateway::class);
|
|
$tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);
|
|
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'enabled' => 0,
|
|
'use_shared_app' => 1,
|
|
'client_secret_override_enc' => '',
|
|
]);
|
|
$repository->expects($this->once())
|
|
->method('upsertByTenantId')
|
|
->with(
|
|
5,
|
|
$this->callback(static function (array $data): bool {
|
|
return $data['enabled'] === true
|
|
&& $data['enforce_microsoft_login'] === true
|
|
&& $data['sync_profile_on_login'] === true
|
|
&& $data['sync_profile_fields'] === 'first_name,last_name'
|
|
&& $data['entra_tenant_id'] === '11111111-1111-1111-1111-111111111111'
|
|
&& $data['allowed_domains'] === 'example.com,test.org'
|
|
&& $data['use_shared_app'] === true
|
|
&& $data['client_id_override'] === null
|
|
&& $data['client_secret_override_enc'] === null;
|
|
})
|
|
)
|
|
->willReturn(true);
|
|
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('getMicrosoftSharedClientId')->willReturn('shared-client');
|
|
$settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('shared-secret');
|
|
|
|
$service = $this->newService($repository, $tenantGateway, $settingsGateway);
|
|
$result = $service->saveTenantMicrosoftAuth(5, [
|
|
'microsoft_enabled' => '1',
|
|
'enforce_microsoft_login' => '1',
|
|
'sync_profile_on_login' => '1',
|
|
'sync_profile_fields' => [],
|
|
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
|
'allowed_domains' => "example.com, invalid, test.org\nexample.com",
|
|
'use_shared_app' => '1',
|
|
'client_id_override' => 'ignored',
|
|
'client_secret_override' => '',
|
|
]);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// shouldAutoRememberOnMicrosoftLogin — policy resolution
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testShouldAutoRememberReturnsTrueWhenForceOn(): void
|
|
{
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'auto_remember_mode' => 1,
|
|
]);
|
|
|
|
$service = $this->newService($repository);
|
|
$this->assertTrue($service->shouldAutoRememberOnMicrosoftLogin(5));
|
|
}
|
|
|
|
public function testShouldAutoRememberReturnsFalseWhenForceOff(): void
|
|
{
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'auto_remember_mode' => 0,
|
|
]);
|
|
|
|
$service = $this->newService($repository);
|
|
$this->assertFalse($service->shouldAutoRememberOnMicrosoftLogin(5));
|
|
}
|
|
|
|
public function testShouldAutoRememberInheritUsesGlobalDefaultTrue(): void
|
|
{
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'auto_remember_mode' => null,
|
|
]);
|
|
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('getMicrosoftSharedClientId')->willReturn('');
|
|
$settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('');
|
|
$settingsGateway->method('getMicrosoftAuthority')->willReturn('');
|
|
$settingsGateway->method('isMicrosoftAutoRememberDefault')->willReturn(true);
|
|
|
|
$service = $this->newService($repository, null, $settingsGateway);
|
|
$this->assertTrue($service->shouldAutoRememberOnMicrosoftLogin(5));
|
|
}
|
|
|
|
public function testShouldAutoRememberInheritUsesGlobalDefaultFalse(): void
|
|
{
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([
|
|
'auto_remember_mode' => null,
|
|
]);
|
|
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('getMicrosoftSharedClientId')->willReturn('');
|
|
$settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('');
|
|
$settingsGateway->method('getMicrosoftAuthority')->willReturn('');
|
|
$settingsGateway->method('isMicrosoftAutoRememberDefault')->willReturn(false);
|
|
|
|
$service = $this->newService($repository, null, $settingsGateway);
|
|
$this->assertFalse($service->shouldAutoRememberOnMicrosoftLogin(5));
|
|
}
|
|
|
|
public function testSaveTenantMicrosoftAuthPersistsAutoRememberMode(): void
|
|
{
|
|
$tenantGateway = $this->createMock(AuthTenantGateway::class);
|
|
$tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);
|
|
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([]);
|
|
$repository->expects($this->once())
|
|
->method('upsertByTenantId')
|
|
->with(
|
|
5,
|
|
$this->callback(static function (array $data): bool {
|
|
return $data['auto_remember_mode'] === 1;
|
|
})
|
|
)
|
|
->willReturn(true);
|
|
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('getMicrosoftSharedClientId')->willReturn('client-id');
|
|
$settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('client-secret');
|
|
$settingsGateway->method('getMicrosoftAuthority')->willReturn('');
|
|
|
|
$service = $this->newService($repository, $tenantGateway, $settingsGateway);
|
|
$result = $service->saveTenantMicrosoftAuth(5, [
|
|
'microsoft_enabled' => '1',
|
|
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
|
'use_shared_app' => '1',
|
|
'sync_profile_on_login' => '',
|
|
'sync_profile_fields' => [],
|
|
'allowed_domains' => '',
|
|
'enforce_microsoft_login' => '',
|
|
'client_id_override' => '',
|
|
'client_secret_override' => '',
|
|
'microsoft_auto_remember_mode' => '1',
|
|
]);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
}
|
|
|
|
public function testSaveTenantMicrosoftAuthPersistsInheritAsNull(): void
|
|
{
|
|
$tenantGateway = $this->createMock(AuthTenantGateway::class);
|
|
$tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);
|
|
|
|
$repository = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$repository->method('findByTenantId')->willReturn([]);
|
|
$repository->expects($this->once())
|
|
->method('upsertByTenantId')
|
|
->with(
|
|
5,
|
|
$this->callback(static function (array $data): bool {
|
|
return $data['auto_remember_mode'] === null;
|
|
})
|
|
)
|
|
->willReturn(true);
|
|
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('getMicrosoftSharedClientId')->willReturn('client-id');
|
|
$settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('client-secret');
|
|
$settingsGateway->method('getMicrosoftAuthority')->willReturn('');
|
|
|
|
$service = $this->newService($repository, $tenantGateway, $settingsGateway);
|
|
$result = $service->saveTenantMicrosoftAuth(5, [
|
|
'microsoft_enabled' => '1',
|
|
'entra_tenant_id' => '11111111-1111-1111-1111-111111111111',
|
|
'use_shared_app' => '1',
|
|
'sync_profile_on_login' => '',
|
|
'sync_profile_fields' => [],
|
|
'allowed_domains' => '',
|
|
'enforce_microsoft_login' => '',
|
|
'client_id_override' => '',
|
|
'client_secret_override' => '',
|
|
'microsoft_auto_remember_mode' => 'inherit',
|
|
]);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
}
|
|
|
|
// ── LDAP tests ────────────────────────────────────────────────────
|
|
|
|
public function testGetTenantLdapAuthReturnsDefaults(): void
|
|
{
|
|
$ldapRepository = $this->createMock(TenantLdapAuthRepositoryInterface::class);
|
|
$ldapRepository->method('findByTenantId')->willReturn(null);
|
|
|
|
$service = $this->newService(null, null, null, null, $ldapRepository);
|
|
$result = $service->getTenantLdapAuth(1);
|
|
|
|
$this->assertFalse($result['enabled']);
|
|
$this->assertFalse($result['enforce_ldap_login']);
|
|
$this->assertSame('', $result['host']);
|
|
$this->assertSame(389, $result['port']);
|
|
$this->assertSame('starttls', $result['encryption_mode']);
|
|
}
|
|
|
|
public function testGetEffectiveLdapProviderConfigReturnsDisabledWhenNotEnabled(): void
|
|
{
|
|
$ldapRepository = $this->createMock(TenantLdapAuthRepositoryInterface::class);
|
|
$ldapRepository->method('findByTenantId')->willReturn(['enabled' => 0]);
|
|
|
|
$service = $this->newService(null, null, null, null, $ldapRepository);
|
|
$result = $service->getEffectiveLdapProviderConfig(['id' => 5]);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('ldap_disabled', $result['error']);
|
|
}
|
|
|
|
public function testGetEffectiveLdapProviderConfigReturnsHostMissing(): void
|
|
{
|
|
$ldapRepository = $this->createMock(TenantLdapAuthRepositoryInterface::class);
|
|
$ldapRepository->method('findByTenantId')->willReturn([
|
|
'enabled' => 1,
|
|
'host' => '',
|
|
'base_dn' => 'DC=example,DC=com',
|
|
]);
|
|
|
|
$service = $this->newService(null, null, null, null, $ldapRepository);
|
|
$result = $service->getEffectiveLdapProviderConfig(['id' => 5]);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('ldap_host_missing', $result['error']);
|
|
}
|
|
|
|
public function testGetEffectiveLdapProviderConfigReturnsBaseDnMissing(): void
|
|
{
|
|
$ldapRepository = $this->createMock(TenantLdapAuthRepositoryInterface::class);
|
|
$ldapRepository->method('findByTenantId')->willReturn([
|
|
'enabled' => 1,
|
|
'host' => 'ldap.example.com',
|
|
'base_dn' => '',
|
|
]);
|
|
|
|
$service = $this->newService(null, null, null, null, $ldapRepository);
|
|
$result = $service->getEffectiveLdapProviderConfig(['id' => 5]);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('ldap_base_dn_missing', $result['error']);
|
|
}
|
|
|
|
public function testGetEffectiveLdapProviderConfigSuccessNoBindCredentials(): void
|
|
{
|
|
$ldapRepository = $this->createMock(TenantLdapAuthRepositoryInterface::class);
|
|
$ldapRepository->method('findByTenantId')->willReturn([
|
|
'enabled' => 1,
|
|
'host' => 'ldap.example.com',
|
|
'port' => 389,
|
|
'encryption_mode' => 'starttls',
|
|
'base_dn' => 'DC=example,DC=com',
|
|
'bind_dn_enc' => '',
|
|
'bind_password_enc' => '',
|
|
'user_search_filter' => '(&(objectClass=user)(sAMAccountName=%s))',
|
|
]);
|
|
|
|
$service = $this->newService(null, null, null, null, $ldapRepository);
|
|
$result = $service->getEffectiveLdapProviderConfig(['id' => 5]);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertSame('ldap', $result['config']['provider']);
|
|
$this->assertSame('ldap.example.com', $result['config']['host']);
|
|
}
|
|
|
|
public function testResolveTenantLoginMethodsIncludesLdap(): void
|
|
{
|
|
$tenantGateway = $this->createMock(AuthTenantGateway::class);
|
|
$tenantGateway->method('findById')->willReturn(['id' => 5, 'status' => 'active']);
|
|
|
|
$microsoftRepo = $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$microsoftRepo->method('findByTenantId')->willReturn(['enabled' => 0]);
|
|
|
|
$ldapRepo = $this->createMock(TenantLdapAuthRepositoryInterface::class);
|
|
$ldapRepo->method('findByTenantId')->willReturn([
|
|
'enabled' => 1,
|
|
'host' => 'ldap.example.com',
|
|
'base_dn' => 'DC=example,DC=com',
|
|
'bind_dn_enc' => '',
|
|
'bind_password_enc' => '',
|
|
]);
|
|
|
|
$service = $this->newService($microsoftRepo, $tenantGateway, null, null, $ldapRepo);
|
|
$result = $service->resolveTenantLoginMethods(5);
|
|
|
|
$this->assertTrue($result['local']);
|
|
$this->assertFalse($result['microsoft']);
|
|
$this->assertTrue($result['ldap']);
|
|
}
|
|
|
|
public function testNormalizeLdapAttributeMapParsesJson(): void
|
|
{
|
|
$service = $this->newService();
|
|
$map = $service->normalizeLdapAttributeMap('{"first_name":"givenName","email":"mail"}');
|
|
$this->assertSame('givenName', $map['first_name']);
|
|
$this->assertSame('mail', $map['email']);
|
|
}
|
|
|
|
public function testNormalizeLdapAttributeMapReturnsDefaultForInvalidJson(): void
|
|
{
|
|
$service = $this->newService();
|
|
$map = $service->normalizeLdapAttributeMap('not json');
|
|
$this->assertArrayHasKey('first_name', $map);
|
|
$this->assertSame('givenName', $map['first_name']);
|
|
}
|
|
|
|
private function newService(
|
|
?TenantMicrosoftAuthRepositoryInterface $repository = null,
|
|
?AuthTenantGateway $tenantGateway = null,
|
|
?AuthSettingsGateway $settingsGateway = null,
|
|
?AuthCryptoGateway $cryptoGateway = null,
|
|
?TenantLdapAuthRepositoryInterface $ldapRepository = null
|
|
): TenantSsoService {
|
|
$repository = $repository ?? $this->createMock(TenantMicrosoftAuthRepositoryInterface::class);
|
|
$ldapRepository = $ldapRepository ?? $this->createMock(TenantLdapAuthRepositoryInterface::class);
|
|
$tenantGateway = $tenantGateway ?? $this->createMock(AuthTenantGateway::class);
|
|
if ($settingsGateway === null) {
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('getMicrosoftSharedClientId')->willReturn('');
|
|
$settingsGateway->method('getMicrosoftSharedClientSecret')->willReturn('');
|
|
$settingsGateway->method('getMicrosoftAuthority')->willReturn('');
|
|
}
|
|
if ($cryptoGateway === null) {
|
|
$cryptoGateway = $this->createMock(AuthCryptoGateway::class);
|
|
$cryptoGateway->method('isConfigured')->willReturn(true);
|
|
$cryptoGateway->method('decryptString')->willReturn('decrypted-secret');
|
|
$cryptoGateway->method('encryptString')->willReturn('encrypted-secret');
|
|
}
|
|
|
|
return new TenantSsoService(
|
|
$tenantGateway,
|
|
$repository,
|
|
$ldapRepository,
|
|
$settingsGateway,
|
|
$cryptoGateway
|
|
);
|
|
}
|
|
}
|