forked from fa/breadcrumb-the-shire
833 lines
37 KiB
PHP
833 lines
37 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->method('getAppTheme')->willReturn('auto');
|
||
|
|
$settingsGateway->expects($this->any())->method('normalizeTheme')->with('auto')->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']);
|
||
|
|
$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('getAppTheme')->willReturn('light');
|
||
|
|
$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('getAppTheme')->willReturn('auto');
|
||
|
|
$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('getAppTheme')->willReturn('auto');
|
||
|
|
$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('getAppTheme')->willReturn('auto');
|
||
|
|
$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('getAppTheme')->willReturn('auto');
|
||
|
|
$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->isType('array'))
|
||
|
|
->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->isType('string'), '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']);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------
|
||
|
|
// 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)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|