feat: add LDAP authentication as per-tenant login method

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>
This commit is contained in:
2026-03-25 07:26:04 +01:00
parent a0d816caaa
commit d1eeac6692
22 changed files with 2325 additions and 18 deletions

View File

@@ -0,0 +1,341 @@
<?php
namespace MintyPHP\Tests\Service\Auth;
use MintyPHP\Service\Auth\LdapAuthService;
use MintyPHP\Service\Auth\LdapConnectionGateway;
use PHPUnit\Framework\TestCase;
class LdapAuthServiceTest extends TestCase
{
private function newService(?LdapConnectionGateway $ldap = null): LdapAuthService
{
return new LdapAuthService(
$ldap ?? $this->createMock(LdapConnectionGateway::class)
);
}
private function baseConfig(): array
{
return [
'host' => 'ldap.example.com',
'port' => 389,
'encryption_mode' => 'none',
'verify_tls_certificate' => false,
'base_dn' => 'DC=example,DC=com',
'bind_dn' => 'CN=svc,DC=example,DC=com',
'bind_password' => 'secret',
'user_search_filter' => '(&(objectClass=user)(sAMAccountName=%s))',
'user_search_scope' => 'sub',
'bind_method' => 'search_then_bind',
'bind_username_format' => '',
'unique_id_attribute' => 'objectGUID',
'attribute_map' => '{"first_name":"givenName","last_name":"sn","email":"mail"}',
'network_timeout' => 5,
];
}
public function testAuthenticateReturnsCredentialsEmptyForEmptyUsername(): void
{
$service = $this->newService();
$result = $service->authenticate($this->baseConfig(), '', 'password');
$this->assertFalse($result['ok']);
$this->assertSame('credentials_empty', $result['error']);
}
public function testAuthenticateReturnsCredentialsEmptyForEmptyPassword(): void
{
$service = $this->newService();
$result = $service->authenticate($this->baseConfig(), 'user', '');
$this->assertFalse($result['ok']);
$this->assertSame('credentials_empty', $result['error']);
}
public function testAuthenticateReturnsConnectionFailedWhenConnectFails(): void
{
$ldap = $this->createMock(LdapConnectionGateway::class);
$ldap->method('connect')->willReturn(false);
$service = $this->newService($ldap);
$result = $service->authenticate($this->baseConfig(), 'user', 'pass');
$this->assertFalse($result['ok']);
$this->assertSame('connection_failed', $result['error']);
}
public function testAuthenticateReturnsConnectionFailedForEmptyHost(): void
{
$config = $this->baseConfig();
$config['host'] = '';
$service = $this->newService();
$result = $service->authenticate($config, 'user', 'pass');
$this->assertFalse($result['ok']);
$this->assertSame('connection_failed', $result['error']);
}
public function testAuthenticateSearchThenBindServiceBindFails(): void
{
$conn = $this->createMockConnection();
$ldap = $this->createMock(LdapConnectionGateway::class);
$ldap->method('connect')->willReturn($conn);
$ldap->method('setOption')->willReturn(true);
$ldap->method('bind')->willReturn(false);
$ldap->method('unbind')->willReturn(true);
$service = $this->newService($ldap);
$result = $service->authenticate($this->baseConfig(), 'user', 'pass');
$this->assertFalse($result['ok']);
$this->assertSame('service_bind_failed', $result['error']);
}
public function testAuthenticateSearchThenBindUserNotFound(): void
{
$conn = $this->createMockConnection();
$ldap = $this->createMock(LdapConnectionGateway::class);
$ldap->method('connect')->willReturn($conn);
$ldap->method('setOption')->willReturn(true);
$ldap->method('bind')->willReturn(true);
$ldap->method('search')->willReturn(false);
$ldap->method('unbind')->willReturn(true);
$service = $this->newService($ldap);
$result = $service->authenticate($this->baseConfig(), 'user', 'pass');
$this->assertFalse($result['ok']);
$this->assertSame('user_not_found', $result['error']);
}
public function testAuthenticateSearchThenBindInvalidPassword(): void
{
$conn = $this->createMockConnection();
$searchResult = $this->createMockSearchResult();
$ldap = $this->createMock(LdapConnectionGateway::class);
$ldap->method('connect')->willReturn($conn);
$ldap->method('setOption')->willReturn(true);
$ldap->method('escape')->willReturnCallback(fn ($v) => $v);
$ldap->method('search')->willReturn($searchResult);
$ldap->method('getEntries')->willReturn([
'count' => 1,
0 => [
'dn' => 'CN=Test User,DC=example,DC=com',
'givenname' => ['count' => 1, 0 => 'Test'],
'sn' => ['count' => 1, 0 => 'User'],
'mail' => ['count' => 1, 0 => 'test@example.com'],
],
]);
$ldap->method('unbind')->willReturn(true);
$bindCallCount = 0;
$ldap->method('bind')->willReturnCallback(function () use (&$bindCallCount) {
$bindCallCount++;
return $bindCallCount <= 1;
});
$service = $this->newService($ldap);
$result = $service->authenticate($this->baseConfig(), 'testuser', 'wrongpass');
$this->assertFalse($result['ok']);
$this->assertSame('invalid_credentials', $result['error']);
}
public function testAuthenticateSearchThenBindSuccess(): void
{
$conn = $this->createMockConnection();
$searchResult = $this->createMockSearchResult();
$ldap = $this->createMock(LdapConnectionGateway::class);
$ldap->method('connect')->willReturn($conn);
$ldap->method('setOption')->willReturn(true);
$ldap->method('bind')->willReturn(true);
$ldap->method('escape')->willReturnCallback(fn ($v) => $v);
$ldap->method('search')->willReturn($searchResult);
$ldap->method('getEntries')->willReturn([
'count' => 1,
0 => [
'dn' => 'CN=Test User,DC=example,DC=com',
'givenname' => ['count' => 1, 0 => 'Test'],
'sn' => ['count' => 1, 0 => 'User'],
'mail' => ['count' => 1, 0 => 'test@example.com'],
'objectguid' => ['count' => 1, 0 => 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'],
],
]);
$ldap->method('unbind')->willReturn(true);
$service = $this->newService($ldap);
$result = $service->authenticate($this->baseConfig(), 'testuser', 'correctpass');
$this->assertTrue($result['ok']);
$this->assertSame('CN=Test User,DC=example,DC=com', $result['dn']);
$this->assertSame('Test', $result['attributes']['first_name']);
$this->assertSame('User', $result['attributes']['last_name']);
$this->assertSame('test@example.com', $result['attributes']['email']);
}
public function testAuthenticateDirectBindSuccess(): void
{
$config = $this->baseConfig();
$config['bind_method'] = 'direct_bind';
$config['bind_username_format'] = '%s@example.com';
$conn = $this->createMockConnection();
$searchResult = $this->createMockSearchResult();
$ldap = $this->createMock(LdapConnectionGateway::class);
$ldap->method('connect')->willReturn($conn);
$ldap->method('setOption')->willReturn(true);
$ldap->method('bind')->willReturn(true);
$ldap->method('escape')->willReturnCallback(fn ($v) => $v);
$ldap->method('search')->willReturn($searchResult);
$ldap->method('getEntries')->willReturn([
'count' => 1,
0 => [
'dn' => 'CN=Test User,DC=example,DC=com',
'givenname' => ['count' => 1, 0 => 'Direct'],
'sn' => ['count' => 1, 0 => 'User'],
'mail' => ['count' => 1, 0 => 'direct@example.com'],
],
]);
$ldap->method('unbind')->willReturn(true);
$service = $this->newService($ldap);
$result = $service->authenticate($config, 'testuser', 'pass');
$this->assertTrue($result['ok']);
$this->assertSame('Direct', $result['attributes']['first_name']);
}
public function testLdapInjectionIsEscaped(): void
{
$ldap = $this->createMock(LdapConnectionGateway::class);
$escapedValue = '';
$ldap->method('escape')->willReturnCallback(function (string $v) use (&$escapedValue) {
$escapedValue = $v;
return 'escaped_' . $v;
});
$conn = $this->createMockConnection();
$ldap->method('connect')->willReturn($conn);
$ldap->method('setOption')->willReturn(true);
$ldap->method('bind')->willReturn(true);
$ldap->method('search')->willReturn(false);
$ldap->method('unbind')->willReturn(true);
$service = $this->newService($ldap);
$result = $service->authenticate($this->baseConfig(), 'user*)(|(uid=*)', 'pass');
$this->assertSame('user*)(|(uid=*)', $escapedValue);
$this->assertFalse($result['ok']);
$this->assertSame('user_not_found', $result['error']);
}
public function testBinaryGuidToUuid(): void
{
$binary = hex2bin('a1a2a3a4b1b2c1c2d1d2e1e2e3e4e5e6');
$uuid = LdapAuthService::binaryGuidToUuid($binary);
$this->assertMatchesRegularExpression('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $uuid);
}
public function testBinaryGuidToUuidNon16Bytes(): void
{
$result = LdapAuthService::binaryGuidToUuid('short');
$this->assertSame(bin2hex('short'), $result);
}
public function testTestConnectionSuccess(): void
{
$conn = $this->createMockConnection();
$ldap = $this->createMock(LdapConnectionGateway::class);
$ldap->method('connect')->willReturn($conn);
$ldap->method('setOption')->willReturn(true);
$ldap->method('bind')->willReturn(true);
$ldap->method('unbind')->willReturn(true);
$service = $this->newService($ldap);
$result = $service->testConnection($this->baseConfig());
$this->assertTrue($result['ok']);
}
public function testTestConnectionBindFailure(): void
{
$conn = $this->createMockConnection();
$ldap = $this->createMock(LdapConnectionGateway::class);
$ldap->method('connect')->willReturn($conn);
$ldap->method('setOption')->willReturn(true);
$ldap->method('bind')->willReturn(false);
$ldap->method('error')->willReturn('Invalid credentials');
$ldap->method('unbind')->willReturn(true);
$service = $this->newService($ldap);
$result = $service->testConnection($this->baseConfig());
$this->assertFalse($result['ok']);
$this->assertStringContainsString('Service account bind failed', $result['error']);
}
public function testStarttlsFailureReturnsConnectionFailed(): void
{
$config = $this->baseConfig();
$config['encryption_mode'] = 'starttls';
$conn = $this->createMockConnection();
$ldap = $this->createMock(LdapConnectionGateway::class);
$ldap->method('connect')->willReturn($conn);
$ldap->method('setOption')->willReturn(true);
$ldap->method('startTls')->willReturn(false);
$service = $this->newService($ldap);
$result = $service->authenticate($config, 'user', 'pass');
$this->assertFalse($result['ok']);
$this->assertSame('connection_failed', $result['error']);
}
private function createMockConnection(): \LDAP\Connection
{
// We need a real LDAP\Connection object for type hints.
// Since we can't create one without ldap_connect, use reflection.
// For tests where ldap extension may not be available, skip.
if (!extension_loaded('ldap')) {
$this->markTestSkipped('LDAP extension not available');
}
// ldap_connect with a URI doesn't actually connect in PHP 8.1+
$conn = ldap_connect('ldap://127.0.0.1:1');
if ($conn === false) {
$this->markTestSkipped('Could not create LDAP connection resource');
}
return $conn;
}
private function createMockSearchResult(): \LDAP\Result
{
// LDAP\Result can't be mocked directly, but since our gateway wraps
// all ldap_* calls, the actual result object is opaque to LdapAuthService.
// The gateway mock returns it, and we just need a type-compatible value.
// Use a real connection + search if ldap is available; otherwise skip.
if (!extension_loaded('ldap')) {
$this->markTestSkipped('LDAP extension not available');
}
// Create a real but unused Result object via reflection workaround.
// Since we mock getEntries(), the actual result content doesn't matter.
// But LDAP\Result has no public constructor, so we use a trick:
// We accept that the gateway returns LDAP\Result|false and our mocks
// already handle this. For this test helper, return a mock-compatible value.
// Since LdapConnectionGateway::search returns \LDAP\Result|false,
// and we mock the gateway, we can use createMock on a stdClass and
// rely on the mock returning it. But PHPUnit mock of LdapConnectionGateway
// allows any return value when configured via willReturn().
// So we just need any object - the gateway mock will return it to the service,
// and the service passes it back to gateway->getEntries().
$conn = ldap_connect('ldap://127.0.0.1:1');
if ($conn === false) {
$this->markTestSkipped('Could not create LDAP connection');
}
// We can't get a real LDAP\Result without a real server, but since
// the service only passes it to the mocked gateway, we can create
// a test double. PHPUnit allows returning any value from willReturn().
// Let's use a simple approach: the gateway mock already handles this.
// For the type system, we use an anonymous class that extends nothing
// but the mock system handles the actual passing.
// Actually, since we mock LdapConnectionGateway::search() to return
// this value, and LdapConnectionGateway::getEntries() is also mocked,
// the value just needs to be truthy (not false).
// We can use createStub to get a compatible value.
return $this->createStub(\LDAP\Result::class);
}
}

View File

@@ -804,6 +804,154 @@ class SsoUserLinkServiceTest extends TestCase
$this->assertTrue($result['ok']);
}
// ── LDAP user link tests ──────────────────────────────────────────
public function testLinkOrProvisionLdapUserReturnsIdentityInvalidForEmptyHost(): void
{
$service = $this->newService();
$result = $service->linkOrProvisionLdapUser(
['id' => 1],
['unique_id' => 'guid-123', 'dn' => 'CN=User,DC=example,DC=com', 'attributes' => []],
['host' => '']
);
$this->assertFalse($result['ok']);
$this->assertSame('identity_invalid', $result['error']);
}
public function testLinkOrProvisionLdapUserReturnsIdentityInvalidForEmptyOid(): void
{
$service = $this->newService();
$result = $service->linkOrProvisionLdapUser(
['id' => 1],
['unique_id' => '', 'dn' => '', 'attributes' => []],
['host' => 'ldap.example.com']
);
$this->assertFalse($result['ok']);
$this->assertSame('identity_invalid', $result['error']);
}
public function testLinkOrProvisionLdapUserLinksExistingIdentity(): void
{
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
$identityGateway->method('findByProviderTidOid')
->willReturn(['user_id' => 42]);
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
$userReadRepo->method('find')
->willReturn(['id' => 42, 'active' => 1]);
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
$userTenantRepo->method('listTenantIdsByUserId')
->willReturn([1]);
$tenantSsoService = $this->createMock(TenantSsoService::class);
$tenantSsoService->method('ldapProviderKey')->willReturn('ldap');
$tenantSsoService->method('normalizeLdapProfileSyncFields')->willReturn([]);
$tenantSsoService->method('defaultProfileSyncFields')->willReturn([]);
$service = $this->newService($userReadRepo, null, null, $userTenantRepo, null, $tenantSsoService, null, $identityGateway);
$result = $service->linkOrProvisionLdapUser(
['id' => 1],
['unique_id' => 'guid-123', 'dn' => 'CN=User,DC=example,DC=com', 'attributes' => ['email' => 'test@example.com']],
['host' => 'ldap.example.com', 'sync_profile_on_login' => false]
);
$this->assertTrue($result['ok']);
$this->assertSame(42, $result['user_id']);
$this->assertFalse($result['created']);
}
public function testLinkOrProvisionLdapUserReturnsInactiveForDisabledUser(): void
{
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
$identityGateway->method('findByProviderTidOid')
->willReturn(['user_id' => 42]);
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
$userReadRepo->method('find')
->willReturn(['id' => 42, 'active' => 0]);
$tenantSsoService = $this->createMock(TenantSsoService::class);
$tenantSsoService->method('ldapProviderKey')->willReturn('ldap');
$service = $this->newService($userReadRepo, null, null, null, null, $tenantSsoService, null, $identityGateway);
$result = $service->linkOrProvisionLdapUser(
['id' => 1],
['unique_id' => 'guid-123', 'dn' => 'CN=User,DC=example,DC=com', 'attributes' => []],
['host' => 'ldap.example.com']
);
$this->assertFalse($result['ok']);
$this->assertSame('user_inactive', $result['error']);
}
public function testLinkOrProvisionLdapUserProvisionsNewUser(): void
{
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
$identityGateway->method('findByProviderTidOid')->willReturn(null);
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
$userReadRepo->method('findByEmail')->willReturn(null);
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
$userWriteRepo->method('create')->willReturn(99);
$userWriteRepo->method('setEmailVerified')->willReturn(true);
$userWriteRepo->method('updateProfileFieldsFromSso')->willReturn(true);
$userAssignment = $this->createMock(UserAssignmentService::class);
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
$settingsGateway->method('getDefaultRoleId')->willReturn(0);
$settingsGateway->method('getDefaultDepartmentId')->willReturn(0);
$settingsGateway->method('getAppTheme')->willReturn('light');
$settingsGateway->method('normalizeTheme')->willReturn('light');
$tenantSsoService = $this->createMock(TenantSsoService::class);
$tenantSsoService->method('ldapProviderKey')->willReturn('ldap');
$tenantSsoService->method('normalizeLdapProfileSyncFields')->willReturn(['first_name', 'last_name']);
$tenantSsoService->method('defaultProfileSyncFields')->willReturn(['first_name', 'last_name']);
$service = $this->newService($userReadRepo, $userWriteRepo, $userAssignment, null, $settingsGateway, $tenantSsoService, null, $identityGateway);
$result = $service->linkOrProvisionLdapUser(
['id' => 1],
[
'unique_id' => 'guid-new',
'dn' => 'CN=New User,DC=example,DC=com',
'attributes' => [
'first_name' => 'New',
'last_name' => 'User',
'email' => 'new@example.com',
],
],
['host' => 'ldap.example.com', 'sync_profile_on_login' => true, 'sync_profile_fields' => ['first_name', 'last_name']]
);
$this->assertTrue($result['ok']);
$this->assertSame(99, $result['user_id']);
$this->assertTrue($result['created']);
}
public function testLinkOrProvisionLdapUserReturnsEmailMissingWhenNoEmail(): void
{
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
$identityGateway->method('findByProviderTidOid')->willReturn(null);
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
$tenantSsoService = $this->createMock(TenantSsoService::class);
$tenantSsoService->method('ldapProviderKey')->willReturn('ldap');
$service = $this->newService($userReadRepo, null, null, null, null, $tenantSsoService, null, $identityGateway);
$result = $service->linkOrProvisionLdapUser(
['id' => 1],
['unique_id' => 'guid-123', 'dn' => 'CN=User,DC=example,DC=com', 'attributes' => ['email' => '']],
['host' => 'ldap.example.com']
);
$this->assertFalse($result['ok']);
$this->assertSame('email_missing', $result['error']);
}
// ---------------------------------------------------------------
// Factory helper
// ---------------------------------------------------------------

View File

@@ -2,6 +2,7 @@
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;
@@ -458,13 +459,139 @@ class TenantSsoServiceTest extends TestCase
$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
?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);
@@ -482,6 +609,7 @@ class TenantSsoServiceTest extends TestCase
return new TenantSsoService(
$tenantGateway,
$repository,
$ldapRepository,
$settingsGateway,
$cryptoGateway
);