Files
breadcrumb-the-shire/tests/Service/Auth/TenantSsoServiceTest.php

448 lines
20 KiB
PHP

<?php
namespace MintyPHP\Tests\Service\Auth;
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 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']);
}
private function newService(
?TenantMicrosoftAuthRepositoryInterface $repository = null,
?AuthTenantGateway $tenantGateway = null,
?AuthSettingsGateway $settingsGateway = null,
?AuthCryptoGateway $cryptoGateway = null
): TenantSsoService {
$repository = $repository ?? $this->createMock(TenantMicrosoftAuthRepositoryInterface::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,
$settingsGateway,
$cryptoGateway
);
}
}