forked from fa/breadcrumb-the-shire
Removes the global app_theme, app_theme_user and app_primary_color settings from the admin/settings area and the underlying service/cache/API/i18n/docs layers. The tenant columns (tenants.primary_color, default_theme, allow_user_theme) become the single source of truth for appearance. Branding helpers are tenant-only and fall back to a hardcoded 'light' theme with no brand color when no tenant context is available (login, error, pre-session pages). The APP_THEME env var is removed. Scope: - UI: Appearance tab gone from /admin/settings; tenant edit form drops the global-fallback color preview. - Service: SettingsAppGateway no longer exposes setting-backed accessors for theme/user-theme/primary-color. Theme catalog utilities (listThemes, isAllowedTheme, normalizeTheme, resolveDefaultTheme) stay and are used across Tenant / Auth / User flows; resolveDefaultTheme now hardcodes 'light' with a catalog fallback (no global/env lookup). - AdminSettingsService: buildPageData, sanitization, audit snapshot, apply step and cache payload are all stripped of the three keys. Dead helper applyAppPrimaryColor + normalizePrimaryColor removed. - Cache: SettingCacheService HOT_PATH_KEYS drops the three keys. - API: /settings (GET/PUT) and /settings/public (GET) no longer expose appearance fields; OpenAPI schemas pruned accordingly. - Audit redaction list shortened. - DB: db/init/init.sql seed rows removed. No migration for existing installs — legacy rows stay inert. - i18n + docs: setting.app_theme, setting.app_theme_user, setting.app_primary_color removed from de+en locales; reference and explanation docs updated. - Tests: covering tests for the removed methods pruned; ThemeResolutionTest rewritten for tenant-only semantics. One unrelated PHP-CS-Fixer issue in UserRegistrar.php cleaned up to keep QG-006 green. Workflow: .agents/runs/SETTINGS-APPEARANCE-TENANT-ONLY/ (gitignored). Gates: QG-001..003, QG-006, QG-008 all pass (1985 tests, 28764 assertions). Reviews: code, security, acceptance all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
974 lines
43 KiB
PHP
974 lines
43 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Tests\Service\Auth;
|
|
|
|
use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
|
|
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
|
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
|
|
use MintyPHP\Service\Auth\AuthExternalIdentityGateway;
|
|
use MintyPHP\Service\Auth\AuthSettingsGateway;
|
|
use MintyPHP\Service\Auth\SsoUserLinkService;
|
|
use MintyPHP\Service\Auth\TenantSsoService;
|
|
use MintyPHP\Service\User\UserAssignmentService;
|
|
use MintyPHP\Service\User\UserAvatarService;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class SsoUserLinkServiceTest extends TestCase
|
|
{
|
|
private const BASE_CLAIMS = [
|
|
'tid' => 'ms-tenant-id',
|
|
'oid' => 'ms-object-id',
|
|
'issuer' => 'https://login.microsoftonline.com/tid/v2.0',
|
|
'subject' => 'subject-123',
|
|
'email' => 'user@example.com',
|
|
'given_name' => 'Ada',
|
|
'family_name' => 'Lovelace',
|
|
'name' => 'Ada Lovelace',
|
|
];
|
|
|
|
private const BASE_TENANT = ['id' => 1];
|
|
|
|
// ---------------------------------------------------------------
|
|
// Identity validation errors
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testIdentityInvalidWhenTidIsMissing(): void
|
|
{
|
|
$service = $this->newService();
|
|
$claims = self::BASE_CLAIMS;
|
|
$claims['tid'] = '';
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, $claims);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('identity_invalid', $result['error']);
|
|
}
|
|
|
|
public function testIdentityInvalidWhenOidIsMissing(): void
|
|
{
|
|
$service = $this->newService();
|
|
$claims = self::BASE_CLAIMS;
|
|
$claims['oid'] = '';
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, $claims);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('identity_invalid', $result['error']);
|
|
}
|
|
|
|
public function testIdentityInvalidWhenIssuerIsMissing(): void
|
|
{
|
|
$service = $this->newService();
|
|
$claims = self::BASE_CLAIMS;
|
|
$claims['issuer'] = '';
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, $claims);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('identity_invalid', $result['error']);
|
|
}
|
|
|
|
public function testIdentityInvalidWhenSubjectIsMissing(): void
|
|
{
|
|
$service = $this->newService();
|
|
$claims = self::BASE_CLAIMS;
|
|
$claims['subject'] = '';
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, $claims);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('identity_invalid', $result['error']);
|
|
}
|
|
|
|
public function testIdentityInvalidWhenTenantIdIsZero(): void
|
|
{
|
|
$service = $this->newService();
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(['id' => 0], self::BASE_CLAIMS);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('identity_invalid', $result['error']);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Phase 1: OID identity match
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testPhase1OidMatchReturnsExistingUserAndSyncsProfile(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(['user_id' => 42]);
|
|
$identityGateway->expects($this->never())->method('findByProviderIssuerSubject');
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->expects($this->any())->method('find')->with(42)->willReturn(['id' => 42, 'active' => 1]);
|
|
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
|
$userTenantRepo->expects($this->any())->method('listTenantIdsByUserId')->with(42)->willReturn([1, 2]);
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
userTenantRepository: $userTenantRepo,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, self::BASE_CLAIMS);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertSame(42, $result['user_id']);
|
|
$this->assertFalse($result['created']);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Phase 2: Issuer/Subject match
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testPhase2IssuerSubjectMatchReturnsExistingUser(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(null);
|
|
$identityGateway->method('findByProviderIssuerSubject')->willReturn(['user_id' => 43]);
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->expects($this->any())->method('find')->with(43)->willReturn(['id' => 43, 'active' => 1]);
|
|
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
|
$userTenantRepo->expects($this->any())->method('listTenantIdsByUserId')->with(43)->willReturn([1]);
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
userTenantRepository: $userTenantRepo,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, self::BASE_CLAIMS);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertSame(43, $result['user_id']);
|
|
$this->assertFalse($result['created']);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Identity found but user inactive / tenant not assigned
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testIdentityFoundButUserInactive(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(['user_id' => 44]);
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->expects($this->any())->method('find')->with(44)->willReturn(['id' => 44, 'active' => 0]);
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, self::BASE_CLAIMS);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('user_inactive', $result['error']);
|
|
}
|
|
|
|
public function testIdentityFoundButUserNotFound(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(['user_id' => 45]);
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->expects($this->any())->method('find')->with(45)->willReturn(null);
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, self::BASE_CLAIMS);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('user_inactive', $result['error']);
|
|
}
|
|
|
|
public function testIdentityFoundButTenantNotAssigned(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(['user_id' => 46]);
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->expects($this->any())->method('find')->with(46)->willReturn(['id' => 46, 'active' => 1]);
|
|
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
|
$userTenantRepo->expects($this->any())->method('listTenantIdsByUserId')->with(46)->willReturn([99, 100]);
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
userTenantRepository: $userTenantRepo,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, self::BASE_CLAIMS);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('tenant_not_assigned', $result['error']);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Email linking: email exists, user active
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testEmailLinkingCreatesIdentityLink(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(null);
|
|
$identityGateway->method('findByProviderIssuerSubject')->willReturn(null);
|
|
$identityGateway->expects($this->once())
|
|
->method('createLink')
|
|
->with($this->callback(function (array $data): bool {
|
|
return ($data['user_id'] ?? 0) === 50
|
|
&& ($data['provider'] ?? '') === 'microsoft'
|
|
&& ($data['oid'] ?? '') === 'ms-object-id';
|
|
}));
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->expects($this->any())->method('findByEmail')->with('user@example.com')->willReturn([
|
|
'id' => 50,
|
|
'active' => 1,
|
|
'email' => 'user@example.com',
|
|
]);
|
|
// find() is called by syncProfileFromMicrosoft
|
|
$userReadRepo->method('find')->willReturn(['id' => 50, 'active' => 1, 'uuid' => 'u-uuid']);
|
|
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
|
$userTenantRepo->method('listTenantIdsByUserId')->willReturn([1]);
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
userTenantRepository: $userTenantRepo,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, self::BASE_CLAIMS);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertSame(50, $result['user_id']);
|
|
$this->assertFalse($result['created']);
|
|
}
|
|
|
|
public function testEmailLinkingReturnsInactiveWhenUserIsInactive(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(null);
|
|
$identityGateway->method('findByProviderIssuerSubject')->willReturn(null);
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->method('findByEmail')->willReturn([
|
|
'id' => 51,
|
|
'active' => 0,
|
|
]);
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, self::BASE_CLAIMS);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('user_inactive', $result['error']);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Email linking ensures tenant assignment
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testEmailLinkingEnsuresTenantAssignment(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(null);
|
|
$identityGateway->method('findByProviderIssuerSubject')->willReturn(null);
|
|
$identityGateway->method('createLink')->willReturn(true);
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->method('findByEmail')->willReturn(['id' => 52, 'active' => 1]);
|
|
$userReadRepo->method('find')->willReturn(['id' => 52, 'active' => 1, 'uuid' => 'u-uuid']);
|
|
|
|
// User is NOT assigned to tenant 1 yet
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
|
$userTenantRepo->method('listTenantIdsByUserId')->willReturn([99]);
|
|
|
|
$userAssignmentService = $this->createMock(UserAssignmentService::class);
|
|
$userAssignmentService->expects($this->once())
|
|
->method('syncTenants')
|
|
->with(52, $this->callback(function (array $ids): bool {
|
|
return in_array(1, $ids, true) && in_array(99, $ids, true);
|
|
}));
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
userAssignmentService: $userAssignmentService,
|
|
userTenantRepository: $userTenantRepo,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, self::BASE_CLAIMS);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Email missing: no identity, no email
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testEmailMissingWhenNoIdentityAndNoEmail(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(null);
|
|
$identityGateway->method('findByProviderIssuerSubject')->willReturn(null);
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$claims = self::BASE_CLAIMS;
|
|
$claims['email'] = '';
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, $claims);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('email_missing', $result['error']);
|
|
}
|
|
|
|
public function testEmailMissingWhenInvalidEmailFormat(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(null);
|
|
$identityGateway->method('findByProviderIssuerSubject')->willReturn(null);
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$claims = self::BASE_CLAIMS;
|
|
$claims['email'] = 'not-an-email';
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, $claims);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('email_missing', $result['error']);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Full provisioning: creates new user
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testFullProvisioningCreatesNewUser(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(null);
|
|
$identityGateway->method('findByProviderIssuerSubject')->willReturn(null);
|
|
$identityGateway->expects($this->once())->method('createLink');
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->method('findByEmail')->willReturn(null);
|
|
$userReadRepo->method('find')->willReturn(['id' => 100, 'uuid' => 'new-uuid', 'active' => 1]);
|
|
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$userWriteRepo->expects($this->once())
|
|
->method('create')
|
|
->with($this->callback(function (array $data): bool {
|
|
return ($data['first_name'] ?? '') === 'Ada'
|
|
&& ($data['last_name'] ?? '') === 'Lovelace'
|
|
&& ($data['email'] ?? '') === 'user@example.com'
|
|
&& str_starts_with($data['password'] ?? '', 'sso-')
|
|
&& str_ends_with($data['password'] ?? '', '-A1!')
|
|
&& ($data['primary_tenant_id'] ?? 0) === 1
|
|
&& ($data['current_tenant_id'] ?? 0) === 1
|
|
&& ($data['active'] ?? 0) === 1;
|
|
}))
|
|
->willReturn(100);
|
|
$userWriteRepo->expects($this->once())->method('setEmailVerified')->with(100);
|
|
|
|
$userAssignmentService = $this->createMock(UserAssignmentService::class);
|
|
$userAssignmentService->expects($this->once())->method('syncTenants')->with(100, [1]);
|
|
$userAssignmentService->expects($this->once())->method('syncRoles')->with(100, [5]);
|
|
$userAssignmentService->expects($this->once())->method('syncDepartments')->with(100, [3]);
|
|
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('getDefaultRoleId')->willReturn(5);
|
|
$settingsGateway->method('getDefaultDepartmentId')->willReturn(3);
|
|
$settingsGateway->expects($this->any())->method('normalizeTheme')->willReturn('light');
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
userWriteRepository: $userWriteRepo,
|
|
userAssignmentService: $userAssignmentService,
|
|
settingsGateway: $settingsGateway,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, self::BASE_CLAIMS);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertSame(100, $result['user_id']);
|
|
$this->assertTrue($result['created']);
|
|
}
|
|
|
|
public function testFullProvisioningParsesFullNameWhenGivenNameMissing(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(null);
|
|
$identityGateway->method('findByProviderIssuerSubject')->willReturn(null);
|
|
$identityGateway->method('createLink')->willReturn(true);
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->method('findByEmail')->willReturn(null);
|
|
$userReadRepo->method('find')->willReturn(['id' => 101, 'uuid' => 'u-uuid', 'active' => 1]);
|
|
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$userWriteRepo->expects($this->once())
|
|
->method('create')
|
|
->with($this->callback(function (array $data): bool {
|
|
return ($data['first_name'] ?? '') === 'Marie'
|
|
&& ($data['last_name'] ?? '') === 'Von Curie';
|
|
}))
|
|
->willReturn(101);
|
|
$userWriteRepo->method('setEmailVerified')->willReturn(true);
|
|
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('getDefaultRoleId')->willReturn(0);
|
|
$settingsGateway->method('getDefaultDepartmentId')->willReturn(0);
|
|
$settingsGateway->method('normalizeTheme')->willReturn('light');
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
userWriteRepository: $userWriteRepo,
|
|
settingsGateway: $settingsGateway,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$claims = self::BASE_CLAIMS;
|
|
$claims['given_name'] = '';
|
|
$claims['family_name'] = '';
|
|
$claims['name'] = 'Marie Von Curie';
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, $claims);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
$this->assertTrue($result['created']);
|
|
}
|
|
|
|
public function testFullProvisioningUsesEmailPrefixWhenNoNameAvailable(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(null);
|
|
$identityGateway->method('findByProviderIssuerSubject')->willReturn(null);
|
|
$identityGateway->method('createLink')->willReturn(true);
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->method('findByEmail')->willReturn(null);
|
|
$userReadRepo->method('find')->willReturn(['id' => 102, 'uuid' => 'u-uuid', 'active' => 1]);
|
|
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$userWriteRepo->expects($this->once())
|
|
->method('create')
|
|
->with($this->callback(function (array $data): bool {
|
|
// first_name should be "User" (ucfirst of email prefix)
|
|
return ($data['first_name'] ?? '') === 'User'
|
|
&& ($data['last_name'] ?? '') === '';
|
|
}))
|
|
->willReturn(102);
|
|
$userWriteRepo->method('setEmailVerified')->willReturn(true);
|
|
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('getDefaultRoleId')->willReturn(0);
|
|
$settingsGateway->method('getDefaultDepartmentId')->willReturn(0);
|
|
$settingsGateway->method('normalizeTheme')->willReturn('auto');
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
userWriteRepository: $userWriteRepo,
|
|
settingsGateway: $settingsGateway,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$claims = self::BASE_CLAIMS;
|
|
$claims['given_name'] = '';
|
|
$claims['family_name'] = '';
|
|
$claims['name'] = '';
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, $claims);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
}
|
|
|
|
public function testFullProvisioningSkipsRoleAndDeptWhenDefaultsAreZero(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(null);
|
|
$identityGateway->method('findByProviderIssuerSubject')->willReturn(null);
|
|
$identityGateway->method('createLink')->willReturn(true);
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->method('findByEmail')->willReturn(null);
|
|
$userReadRepo->method('find')->willReturn(['id' => 103, 'uuid' => 'u-uuid', 'active' => 1]);
|
|
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$userWriteRepo->method('create')->willReturn(103);
|
|
$userWriteRepo->method('setEmailVerified')->willReturn(true);
|
|
|
|
$userAssignmentService = $this->createMock(UserAssignmentService::class);
|
|
$userAssignmentService->expects($this->once())->method('syncTenants');
|
|
$userAssignmentService->expects($this->never())->method('syncRoles');
|
|
$userAssignmentService->expects($this->never())->method('syncDepartments');
|
|
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('getDefaultRoleId')->willReturn(0);
|
|
$settingsGateway->method('getDefaultDepartmentId')->willReturn(0);
|
|
$settingsGateway->method('normalizeTheme')->willReturn('auto');
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
userWriteRepository: $userWriteRepo,
|
|
userAssignmentService: $userAssignmentService,
|
|
settingsGateway: $settingsGateway,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, self::BASE_CLAIMS);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// user_create_failed
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testUserCreateFailedWhenRepositoryReturnsFalse(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(null);
|
|
$identityGateway->method('findByProviderIssuerSubject')->willReturn(null);
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->method('findByEmail')->willReturn(null);
|
|
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$userWriteRepo->method('create')->willReturn(false);
|
|
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('normalizeTheme')->willReturn('auto');
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
userWriteRepository: $userWriteRepo,
|
|
settingsGateway: $settingsGateway,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, self::BASE_CLAIMS);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('user_create_failed', $result['error']);
|
|
}
|
|
|
|
public function testUserCreateFailedWhenRepositoryReturnsZero(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(null);
|
|
$identityGateway->method('findByProviderIssuerSubject')->willReturn(null);
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->method('findByEmail')->willReturn(null);
|
|
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$userWriteRepo->method('create')->willReturn(0);
|
|
|
|
$settingsGateway = $this->createMock(AuthSettingsGateway::class);
|
|
$settingsGateway->method('normalizeTheme')->willReturn('auto');
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
userWriteRepository: $userWriteRepo,
|
|
settingsGateway: $settingsGateway,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, self::BASE_CLAIMS);
|
|
|
|
$this->assertFalse($result['ok']);
|
|
$this->assertSame('user_create_failed', $result['error']);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Avatar sync
|
|
// ---------------------------------------------------------------
|
|
|
|
public function testAvatarSyncProcessesValidJpegBase64(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(['user_id' => 60]);
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->expects($this->any())->method('find')->with(60)->willReturn(['id' => 60, 'active' => 1, 'uuid' => 'user-uuid-60']);
|
|
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
|
$userTenantRepo->method('listTenantIdsByUserId')->willReturn([1]);
|
|
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$userWriteRepo->expects($this->once())
|
|
->method('updateProfileFieldsFromSso')
|
|
->with(60, $this->isArray())
|
|
->willReturn(true);
|
|
|
|
$avatarService = $this->createMock(UserAvatarService::class);
|
|
$avatarService->expects($this->any())->method('isValidUuid')->with('user-uuid-60')->willReturn(true);
|
|
$avatarService->expects($this->once())
|
|
->method('saveBinary')
|
|
->with('user-uuid-60', $this->isString(), 'image/jpeg')
|
|
->willReturn(['ok' => true]);
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
$tenantSsoService->method('normalizeProfileSyncFields')
|
|
->willReturn(['first_name', 'last_name', 'avatar']);
|
|
$tenantSsoService->method('defaultProfileSyncFields')
|
|
->willReturn(['first_name', 'last_name']);
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
userWriteRepository: $userWriteRepo,
|
|
userTenantRepository: $userTenantRepo,
|
|
avatarService: $avatarService,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$claims = self::BASE_CLAIMS;
|
|
$claims['sync_profile_on_login'] = true;
|
|
$claims['sync_profile_fields'] = ['first_name', 'last_name', 'avatar'];
|
|
$claims['graph_avatar_data_base64'] = base64_encode('fake-jpeg-binary-data');
|
|
$claims['graph_avatar_mime'] = 'image/jpeg';
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, $claims);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
}
|
|
|
|
public function testAvatarSyncSkipsInvalidMime(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(['user_id' => 61]);
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->expects($this->any())->method('find')->with(61)->willReturn(['id' => 61, 'active' => 1, 'uuid' => 'user-uuid-61']);
|
|
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
|
$userTenantRepo->method('listTenantIdsByUserId')->willReturn([1]);
|
|
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$userWriteRepo->expects($this->once())->method('updateProfileFieldsFromSso');
|
|
|
|
$avatarService = $this->createMock(UserAvatarService::class);
|
|
$avatarService->expects($this->never())->method('saveBinary');
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
$tenantSsoService->method('normalizeProfileSyncFields')
|
|
->willReturn(['first_name', 'avatar']);
|
|
$tenantSsoService->method('defaultProfileSyncFields')
|
|
->willReturn(['first_name', 'last_name']);
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
userWriteRepository: $userWriteRepo,
|
|
userTenantRepository: $userTenantRepo,
|
|
avatarService: $avatarService,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
$claims = self::BASE_CLAIMS;
|
|
$claims['sync_profile_on_login'] = true;
|
|
$claims['sync_profile_fields'] = ['first_name', 'avatar'];
|
|
$claims['graph_avatar_data_base64'] = base64_encode('some-data');
|
|
$claims['graph_avatar_mime'] = 'application/pdf';
|
|
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, $claims);
|
|
|
|
$this->assertTrue($result['ok']);
|
|
}
|
|
|
|
public function testProfileSyncSkippedWhenFlagNotSet(): void
|
|
{
|
|
$identityGateway = $this->createMock(AuthExternalIdentityGateway::class);
|
|
$identityGateway->method('findByProviderTidOid')->willReturn(['user_id' => 62]);
|
|
|
|
$userReadRepo = $this->createMock(UserReadRepositoryInterface::class);
|
|
$userReadRepo->expects($this->any())->method('find')->with(62)->willReturn(['id' => 62, 'active' => 1]);
|
|
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
|
$userTenantRepo->method('listTenantIdsByUserId')->willReturn([1]);
|
|
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
|
$userWriteRepo->expects($this->never())->method('updateProfileFieldsFromSso');
|
|
|
|
$avatarService = $this->createMock(UserAvatarService::class);
|
|
$avatarService->expects($this->never())->method('saveBinary');
|
|
|
|
$tenantSsoService = $this->createMock(TenantSsoService::class);
|
|
$tenantSsoService->method('microsoftProviderKey')->willReturn('microsoft');
|
|
|
|
$service = $this->newService(
|
|
userReadRepository: $userReadRepo,
|
|
userWriteRepository: $userWriteRepo,
|
|
userTenantRepository: $userTenantRepo,
|
|
avatarService: $avatarService,
|
|
tenantSsoService: $tenantSsoService,
|
|
externalIdentityGateway: $identityGateway
|
|
);
|
|
|
|
// No sync_profile_on_login key
|
|
$result = $service->linkOrProvisionMicrosoftUser(self::BASE_TENANT, self::BASE_CLAIMS);
|
|
|
|
$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('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
|
|
// ---------------------------------------------------------------
|
|
|
|
private function newService(
|
|
?UserReadRepositoryInterface $userReadRepository = null,
|
|
?UserWriteRepositoryInterface $userWriteRepository = null,
|
|
?UserAssignmentService $userAssignmentService = null,
|
|
?UserTenantRepositoryInterface $userTenantRepository = null,
|
|
?AuthSettingsGateway $settingsGateway = null,
|
|
?TenantSsoService $tenantSsoService = null,
|
|
?UserAvatarService $avatarService = null,
|
|
?AuthExternalIdentityGateway $externalIdentityGateway = null
|
|
): SsoUserLinkService {
|
|
return new SsoUserLinkService(
|
|
$userReadRepository ?? $this->createMock(UserReadRepositoryInterface::class),
|
|
$userWriteRepository ?? $this->createMock(UserWriteRepositoryInterface::class),
|
|
$userAssignmentService ?? $this->createMock(UserAssignmentService::class),
|
|
$userTenantRepository ?? $this->createMock(UserTenantRepositoryInterface::class),
|
|
$settingsGateway ?? $this->createMock(AuthSettingsGateway::class),
|
|
$tenantSsoService ?? $this->createMock(TenantSsoService::class),
|
|
$avatarService ?? $this->createMock(UserAvatarService::class),
|
|
$externalIdentityGateway ?? $this->createMock(AuthExternalIdentityGateway::class)
|
|
);
|
|
}
|
|
}
|