forked from fa/breadcrumb-the-shire
295 lines
12 KiB
PHP
295 lines
12 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace MintyPHP\Tests\Service\User;
|
||
|
|
|
||
|
|
use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
|
||
|
|
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
||
|
|
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
|
||
|
|
use MintyPHP\Service\Tenant\TenantScopeService;
|
||
|
|
use MintyPHP\Service\User\UserDirectoryGateway;
|
||
|
|
use MintyPHP\Service\User\UserTenantContextService;
|
||
|
|
use PHPUnit\Framework\TestCase;
|
||
|
|
|
||
|
|
final class UserTenantContextServiceTest extends TestCase
|
||
|
|
{
|
||
|
|
public function testGetCurrentTenantIdReturnsNullWhenUserMissing(): void
|
||
|
|
{
|
||
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
||
|
|
$userRead->method('find')->willReturn(null);
|
||
|
|
|
||
|
|
$service = $this->makeService(userRead: $userRead);
|
||
|
|
|
||
|
|
$this->assertNull($service->getCurrentTenantId(42));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testGetCurrentTenantIdReturnsNullWhenUserHasNoAccessibleTenants(): void
|
||
|
|
{
|
||
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
||
|
|
$userRead->method('find')->willReturn(['current_tenant_id' => 5, 'primary_tenant_id' => 5]);
|
||
|
|
|
||
|
|
$scope = $this->createMock(TenantScopeService::class);
|
||
|
|
$scope->method('getUserTenantIds')->willReturn([]);
|
||
|
|
|
||
|
|
$service = $this->makeService(userRead: $userRead, scope: $scope);
|
||
|
|
|
||
|
|
$this->assertNull($service->getCurrentTenantId(42));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testGetCurrentTenantIdPrefersCurrentWhenInAccessibleSet(): void
|
||
|
|
{
|
||
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
||
|
|
$userRead->method('find')->willReturn([
|
||
|
|
'current_tenant_id' => 7,
|
||
|
|
'primary_tenant_id' => 3,
|
||
|
|
]);
|
||
|
|
|
||
|
|
$scope = $this->createMock(TenantScopeService::class);
|
||
|
|
$scope->method('getUserTenantIds')->willReturn([3, 7, 9]);
|
||
|
|
|
||
|
|
$service = $this->makeService(userRead: $userRead, scope: $scope);
|
||
|
|
|
||
|
|
$this->assertSame(7, $service->getCurrentTenantId(42));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testGetCurrentTenantIdFallsBackToPrimaryWhenCurrentNotAccessible(): void
|
||
|
|
{
|
||
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
||
|
|
$userRead->method('find')->willReturn([
|
||
|
|
'current_tenant_id' => 999,
|
||
|
|
'primary_tenant_id' => 3,
|
||
|
|
]);
|
||
|
|
|
||
|
|
$scope = $this->createMock(TenantScopeService::class);
|
||
|
|
$scope->method('getUserTenantIds')->willReturn([3, 9]);
|
||
|
|
|
||
|
|
$service = $this->makeService(userRead: $userRead, scope: $scope);
|
||
|
|
|
||
|
|
$this->assertSame(3, $service->getCurrentTenantId(42));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testGetCurrentTenantIdFallsBackToFirstAccessibleWhenNeitherCurrentNorPrimaryMatch(): void
|
||
|
|
{
|
||
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
||
|
|
$userRead->method('find')->willReturn([
|
||
|
|
'current_tenant_id' => 0,
|
||
|
|
'primary_tenant_id' => 999,
|
||
|
|
]);
|
||
|
|
|
||
|
|
$scope = $this->createMock(TenantScopeService::class);
|
||
|
|
$scope->method('getUserTenantIds')->willReturn([9, 11]);
|
||
|
|
|
||
|
|
$service = $this->makeService(userRead: $userRead, scope: $scope);
|
||
|
|
|
||
|
|
$this->assertSame(9, $service->getCurrentTenantId(42));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testGetCurrentTenantReturnsNullWhenNoCurrentTenantResolvable(): void
|
||
|
|
{
|
||
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
||
|
|
$userRead->method('find')->willReturn(null);
|
||
|
|
|
||
|
|
$directory = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directory->expects($this->never())->method('findTenant');
|
||
|
|
|
||
|
|
$service = $this->makeService(userRead: $userRead, directory: $directory);
|
||
|
|
|
||
|
|
$this->assertNull($service->getCurrentTenant(42));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testGetCurrentTenantLoadsTenantRecord(): void
|
||
|
|
{
|
||
|
|
$userRead = $this->createMock(UserReadRepositoryInterface::class);
|
||
|
|
$userRead->method('find')->willReturn(['current_tenant_id' => 7, 'primary_tenant_id' => 7]);
|
||
|
|
|
||
|
|
$scope = $this->createMock(TenantScopeService::class);
|
||
|
|
$scope->method('getUserTenantIds')->willReturn([7]);
|
||
|
|
|
||
|
|
$directory = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directory->expects($this->once())
|
||
|
|
->method('findTenant')
|
||
|
|
->with(7)
|
||
|
|
->willReturn(['id' => 7, 'description' => 'Acme']);
|
||
|
|
|
||
|
|
$service = $this->makeService(userRead: $userRead, scope: $scope, directory: $directory);
|
||
|
|
|
||
|
|
$this->assertSame(['id' => 7, 'description' => 'Acme'], $service->getCurrentTenant(42));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testGetAvailableTenantsReturnsEmptyWhenNoneAccessible(): void
|
||
|
|
{
|
||
|
|
$scope = $this->createMock(TenantScopeService::class);
|
||
|
|
$scope->method('getUserTenantIds')->willReturn([]);
|
||
|
|
|
||
|
|
$service = $this->makeService(scope: $scope);
|
||
|
|
|
||
|
|
$this->assertSame([], $service->getAvailableTenants(42));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testGetAvailableTenantsFiltersInactiveAndSortsByDescription(): void
|
||
|
|
{
|
||
|
|
$scope = $this->createMock(TenantScopeService::class);
|
||
|
|
$scope->method('getUserTenantIds')->willReturn([1, 2, 3]);
|
||
|
|
|
||
|
|
$directory = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directory->method('findTenant')->willReturnCallback(static function (int $id): ?array {
|
||
|
|
return match ($id) {
|
||
|
|
1 => ['id' => 1, 'description' => 'Charlie', 'status' => 'active'],
|
||
|
|
2 => ['id' => 2, 'description' => 'Alpha', 'status' => 'inactive'],
|
||
|
|
3 => ['id' => 3, 'description' => 'Bravo', 'status' => 'active'],
|
||
|
|
default => null,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
$service = $this->makeService(scope: $scope, directory: $directory);
|
||
|
|
$result = $service->getAvailableTenants(42);
|
||
|
|
|
||
|
|
$this->assertCount(2, $result);
|
||
|
|
$this->assertSame('Bravo', $result[0]['description']);
|
||
|
|
$this->assertSame('Charlie', $result[1]['description']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testGetAssignedActiveTenantsReturnsEmptyForInvalidUserId(): void
|
||
|
|
{
|
||
|
|
$service = $this->makeService();
|
||
|
|
|
||
|
|
$this->assertSame([], $service->getAssignedActiveTenants(0));
|
||
|
|
$this->assertSame([], $service->getAssignedActiveTenants(-1));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testGetAssignedActiveTenantsFiltersInactiveAndPreservesAssignedOrder(): void
|
||
|
|
{
|
||
|
|
$userTenant = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenant->method('listTenantIdsByUserId')->willReturn([3, 1, 2, 3, 0]);
|
||
|
|
|
||
|
|
$directory = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directory->expects($this->once())
|
||
|
|
->method('listActiveTenantIdsByIds')
|
||
|
|
->with([3, 1, 2])
|
||
|
|
->willReturn([1, 3]);
|
||
|
|
$directory->expects($this->once())
|
||
|
|
->method('listTenantsByIds')
|
||
|
|
->with([1, 3])
|
||
|
|
->willReturn([
|
||
|
|
['id' => 3, 'description' => 'Zulu'],
|
||
|
|
['id' => 1, 'description' => 'Alpha'],
|
||
|
|
]);
|
||
|
|
|
||
|
|
$service = $this->makeService(userTenant: $userTenant, directory: $directory);
|
||
|
|
$result = $service->getAssignedActiveTenants(42);
|
||
|
|
|
||
|
|
$this->assertCount(2, $result);
|
||
|
|
$this->assertSame(1, $result[0]['id']);
|
||
|
|
$this->assertSame(3, $result[1]['id']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testGetAssignedActiveTenantsReturnsEmptyWhenNoneActive(): void
|
||
|
|
{
|
||
|
|
$userTenant = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenant->method('listTenantIdsByUserId')->willReturn([1, 2]);
|
||
|
|
|
||
|
|
$directory = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directory->method('listActiveTenantIdsByIds')->willReturn([]);
|
||
|
|
$directory->expects($this->never())->method('listTenantsByIds');
|
||
|
|
|
||
|
|
$service = $this->makeService(userTenant: $userTenant, directory: $directory);
|
||
|
|
|
||
|
|
$this->assertSame([], $service->getAssignedActiveTenants(42));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSetCurrentTenantRejectsInvalidTenantId(): void
|
||
|
|
{
|
||
|
|
$service = $this->makeService();
|
||
|
|
|
||
|
|
$this->assertSame(['ok' => false, 'error' => 'invalid_tenant'], $service->setCurrentTenant(42, 0));
|
||
|
|
$this->assertSame(['ok' => false, 'error' => 'invalid_tenant'], $service->setCurrentTenant(42, -1));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSetCurrentTenantRejectsUnassignedTenant(): void
|
||
|
|
{
|
||
|
|
$userTenant = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenant->method('listTenantIdsByUserId')->willReturn([1, 2]);
|
||
|
|
|
||
|
|
$service = $this->makeService(userTenant: $userTenant);
|
||
|
|
|
||
|
|
$this->assertSame(['ok' => false, 'error' => 'tenant_not_assigned'], $service->setCurrentTenant(42, 999));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSetCurrentTenantRejectsMissingTenant(): void
|
||
|
|
{
|
||
|
|
$userTenant = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenant->method('listTenantIdsByUserId')->willReturn([7]);
|
||
|
|
|
||
|
|
$directory = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directory->method('findTenant')->willReturn(null);
|
||
|
|
|
||
|
|
$service = $this->makeService(userTenant: $userTenant, directory: $directory);
|
||
|
|
|
||
|
|
$this->assertSame(['ok' => false, 'error' => 'tenant_not_found'], $service->setCurrentTenant(42, 7));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSetCurrentTenantRejectsInactiveTenant(): void
|
||
|
|
{
|
||
|
|
$userTenant = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenant->method('listTenantIdsByUserId')->willReturn([7]);
|
||
|
|
|
||
|
|
$directory = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directory->method('findTenant')->willReturn(['id' => 7, 'status' => 'inactive']);
|
||
|
|
|
||
|
|
$service = $this->makeService(userTenant: $userTenant, directory: $directory);
|
||
|
|
|
||
|
|
$this->assertSame(['ok' => false, 'error' => 'tenant_inactive'], $service->setCurrentTenant(42, 7));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSetCurrentTenantReturnsUpdateFailedWhenPersistFails(): void
|
||
|
|
{
|
||
|
|
$userTenant = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenant->method('listTenantIdsByUserId')->willReturn([7]);
|
||
|
|
|
||
|
|
$directory = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directory->method('findTenant')->willReturn(['id' => 7, 'status' => 'active']);
|
||
|
|
|
||
|
|
$userWrite = $this->createMock(UserWriteRepositoryInterface::class);
|
||
|
|
$userWrite->method('setCurrentTenant')->willReturn(false);
|
||
|
|
|
||
|
|
$service = $this->makeService(userTenant: $userTenant, userWrite: $userWrite, directory: $directory);
|
||
|
|
|
||
|
|
$this->assertSame(['ok' => false, 'error' => 'update_failed'], $service->setCurrentTenant(42, 7));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSetCurrentTenantPersistsAndReturnsTenantOnSuccess(): void
|
||
|
|
{
|
||
|
|
$userTenant = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenant->method('listTenantIdsByUserId')->willReturn([7]);
|
||
|
|
|
||
|
|
$directory = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$tenant = ['id' => 7, 'status' => 'active', 'description' => 'Acme'];
|
||
|
|
$directory->method('findTenant')->willReturn($tenant);
|
||
|
|
|
||
|
|
$userWrite = $this->createMock(UserWriteRepositoryInterface::class);
|
||
|
|
$userWrite->expects($this->once())
|
||
|
|
->method('setCurrentTenant')
|
||
|
|
->with(42, 7)
|
||
|
|
->willReturn(true);
|
||
|
|
|
||
|
|
$service = $this->makeService(userTenant: $userTenant, userWrite: $userWrite, directory: $directory);
|
||
|
|
|
||
|
|
$this->assertSame(['ok' => true, 'tenant' => $tenant], $service->setCurrentTenant(42, 7));
|
||
|
|
}
|
||
|
|
|
||
|
|
private function makeService(
|
||
|
|
?UserReadRepositoryInterface $userRead = null,
|
||
|
|
?UserWriteRepositoryInterface $userWrite = null,
|
||
|
|
?UserTenantRepositoryInterface $userTenant = null,
|
||
|
|
?TenantScopeService $scope = null,
|
||
|
|
?UserDirectoryGateway $directory = null,
|
||
|
|
): UserTenantContextService {
|
||
|
|
return new UserTenantContextService(
|
||
|
|
$userRead ?? $this->createMock(UserReadRepositoryInterface::class),
|
||
|
|
$userWrite ?? $this->createMock(UserWriteRepositoryInterface::class),
|
||
|
|
$userTenant ?? $this->createMock(UserTenantRepositoryInterface::class),
|
||
|
|
$scope ?? $this->createMock(TenantScopeService::class),
|
||
|
|
$directory ?? $this->createMock(UserDirectoryGateway::class),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|