forked from fa/breadcrumb-the-shire
558 lines
24 KiB
PHP
558 lines
24 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace MintyPHP\Tests\Service\User;
|
||
|
|
|
||
|
|
use MintyPHP\Repository\Access\UserRoleRepositoryInterface;
|
||
|
|
use MintyPHP\Repository\Org\UserDepartmentRepositoryInterface;
|
||
|
|
use MintyPHP\Repository\Support\DatabaseSessionRepository;
|
||
|
|
use MintyPHP\Repository\Tenant\UserTenantRepositoryInterface;
|
||
|
|
use MintyPHP\Repository\User\UserWriteRepositoryInterface;
|
||
|
|
use MintyPHP\Service\Access\AssignableRoleService;
|
||
|
|
use MintyPHP\Service\Access\PermissionService;
|
||
|
|
use MintyPHP\Service\User\UserAssignmentService;
|
||
|
|
use MintyPHP\Service\User\UserDirectoryGateway;
|
||
|
|
use PHPUnit\Framework\TestCase;
|
||
|
|
|
||
|
|
class UserAssignmentServiceTest extends TestCase
|
||
|
|
{
|
||
|
|
// ── buildAssignmentsForUser ──────────────────────────────────────────
|
||
|
|
|
||
|
|
public function testBuildAssignmentsForUserReturnsEmptyStructureWhenUserIdIsZero(): void
|
||
|
|
{
|
||
|
|
$service = $this->newService();
|
||
|
|
$result = $service->buildAssignmentsForUser(0);
|
||
|
|
|
||
|
|
$this->assertSame([], $result['tenants']);
|
||
|
|
$this->assertSame([], $result['departments']);
|
||
|
|
$this->assertSame([], $result['roles']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testBuildAssignmentsForUserReturnsEmptyStructureWhenUserIdIsNegative(): void
|
||
|
|
{
|
||
|
|
$service = $this->newService();
|
||
|
|
$result = $service->buildAssignmentsForUser(-5);
|
||
|
|
|
||
|
|
$this->assertSame([], $result['tenants']);
|
||
|
|
$this->assertSame([], $result['departments']);
|
||
|
|
$this->assertSame([], $result['roles']);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testBuildAssignmentsForUserReturnsTenantsDepartmentsAndRoles(): void
|
||
|
|
{
|
||
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenantRepo->expects($this->once())->method('listTenantIdsByUserId')->with(10)->willReturn([1, 2]);
|
||
|
|
|
||
|
|
$userDeptRepo = $this->createMock(UserDepartmentRepositoryInterface::class);
|
||
|
|
$userDeptRepo->expects($this->once())->method('listDepartmentIdsByUserId')->with(10)->willReturn([100]);
|
||
|
|
|
||
|
|
$userRoleRepo = $this->createMock(UserRoleRepositoryInterface::class);
|
||
|
|
$userRoleRepo->expects($this->once())->method('listRoleIdsByUserId')->with(10)->willReturn([50]);
|
||
|
|
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->expects($this->once())->method('listTenantsByIds')->with([1, 2])->willReturn([
|
||
|
|
['id' => 1, 'uuid' => 'tenant-1', 'description' => 'Tenant 1', 'status' => 'active'],
|
||
|
|
['id' => 2, 'uuid' => 'tenant-2', 'description' => 'Tenant 2', 'status' => 'active'],
|
||
|
|
]);
|
||
|
|
$directoryGateway->expects($this->once())->method('listDepartmentsByIds')->with([100], true)->willReturn([
|
||
|
|
['id' => 100, 'uuid' => 'dept-100', 'description' => 'Dept 100', 'active' => 1, 'tenant_id' => 1],
|
||
|
|
]);
|
||
|
|
$directoryGateway->expects($this->once())->method('listRolesByIds')->with([50])->willReturn([
|
||
|
|
['id' => 50, 'uuid' => 'role-50', 'description' => 'Role 50', 'active' => 1],
|
||
|
|
]);
|
||
|
|
|
||
|
|
$service = $this->newService(
|
||
|
|
userTenantRepository: $userTenantRepo,
|
||
|
|
userDepartmentRepository: $userDeptRepo,
|
||
|
|
userRoleRepository: $userRoleRepo,
|
||
|
|
directoryGateway: $directoryGateway
|
||
|
|
);
|
||
|
|
|
||
|
|
$result = $service->buildAssignmentsForUser(10);
|
||
|
|
|
||
|
|
$this->assertCount(2, $result['tenants']);
|
||
|
|
$this->assertSame(1, $result['tenants'][0]['id']);
|
||
|
|
$this->assertSame(2, $result['tenants'][1]['id']);
|
||
|
|
$this->assertCount(1, $result['departments']);
|
||
|
|
$this->assertSame(100, $result['departments'][0]['id']);
|
||
|
|
$this->assertSame('tenant-1', $result['departments'][0]['tenant_uuid']);
|
||
|
|
$this->assertCount(1, $result['roles']);
|
||
|
|
$this->assertSame(50, $result['roles'][0]['id']);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── syncTenants ─────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
public function testSyncTenantsNormalizesAndCallsRepository(): void
|
||
|
|
{
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->expects($this->once())->method('listTenantIds')->willReturn([1, 2, 3]);
|
||
|
|
|
||
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenantRepo->expects($this->once())
|
||
|
|
->method('replaceForUser')
|
||
|
|
->with(10, $this->callback(static fn (array $ids): bool => $ids === [1, 2]))
|
||
|
|
->willReturn(true);
|
||
|
|
|
||
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
||
|
|
$userWriteRepo->expects($this->once())->method('bumpAuthzVersion')->with(10);
|
||
|
|
|
||
|
|
$service = $this->newService(
|
||
|
|
userWriteRepository: $userWriteRepo,
|
||
|
|
userTenantRepository: $userTenantRepo,
|
||
|
|
directoryGateway: $directoryGateway
|
||
|
|
);
|
||
|
|
|
||
|
|
$result = $service->syncTenants(10, [1, 2]);
|
||
|
|
$this->assertTrue($result);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSyncTenantsDoesNotBumpAuthzWhenFlagIsFalse(): void
|
||
|
|
{
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->method('listTenantIds')->willReturn([1]);
|
||
|
|
|
||
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenantRepo->method('replaceForUser')->willReturn(true);
|
||
|
|
|
||
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
||
|
|
$userWriteRepo->expects($this->never())->method('bumpAuthzVersion');
|
||
|
|
|
||
|
|
$service = $this->newService(
|
||
|
|
userWriteRepository: $userWriteRepo,
|
||
|
|
userTenantRepository: $userTenantRepo,
|
||
|
|
directoryGateway: $directoryGateway
|
||
|
|
);
|
||
|
|
|
||
|
|
$result = $service->syncTenants(10, [1], false);
|
||
|
|
$this->assertTrue($result);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── syncRoles ───────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
public function testSyncRolesDeduplicatesAndFiltersAgainstActiveRoles(): void
|
||
|
|
{
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->expects($this->once())->method('listActiveRoleIds')->willReturn([1, 2, 3]);
|
||
|
|
|
||
|
|
$userRoleRepo = $this->createMock(UserRoleRepositoryInterface::class);
|
||
|
|
$userRoleRepo->expects($this->once())
|
||
|
|
->method('replaceForUser')
|
||
|
|
->with(10, $this->callback(static fn (array $ids): bool => $ids === [1, 3]))
|
||
|
|
->willReturn(true);
|
||
|
|
|
||
|
|
$permissionService = $this->createMock(PermissionService::class);
|
||
|
|
$permissionService->expects($this->once())->method('clearUserCache')->with(10);
|
||
|
|
|
||
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
||
|
|
$userWriteRepo->expects($this->once())->method('bumpAuthzVersion')->with(10);
|
||
|
|
|
||
|
|
$service = $this->newService(
|
||
|
|
userWriteRepository: $userWriteRepo,
|
||
|
|
userRoleRepository: $userRoleRepo,
|
||
|
|
directoryGateway: $directoryGateway,
|
||
|
|
permissionService: $permissionService
|
||
|
|
);
|
||
|
|
|
||
|
|
// Pass duplicates and an inactive role (99)
|
||
|
|
$result = $service->syncRoles(10, [1, 3, 3, 99]);
|
||
|
|
$this->assertTrue($result);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSyncRolesWithActorEnforcesAssignableRoleFreeze(): void
|
||
|
|
{
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->method('listActiveRoleIds')->willReturn([1, 2, 3, 4]);
|
||
|
|
|
||
|
|
$userRoleRepo = $this->createMock(UserRoleRepositoryInterface::class);
|
||
|
|
$userRoleRepo->expects($this->once())->method('listRoleIdsByUserId')->with(10)->willReturn([2, 4]);
|
||
|
|
$userRoleRepo->expects($this->once())
|
||
|
|
->method('replaceForUser')
|
||
|
|
->with(10, [4, 1]) // frozen(4) + touched(1)
|
||
|
|
->willReturn(true);
|
||
|
|
|
||
|
|
$assignableRoleService = $this->createMock(AssignableRoleService::class);
|
||
|
|
$assignableRoleService->expects($this->once())
|
||
|
|
->method('computeEffectiveRoleIds')
|
||
|
|
->with(99, [1, 3], [2, 4])
|
||
|
|
->willReturn([4, 1]); // role 4 frozen, role 1 submitted & assignable
|
||
|
|
|
||
|
|
$permissionService = $this->createMock(PermissionService::class);
|
||
|
|
$permissionService->expects($this->once())->method('clearUserCache')->with(10);
|
||
|
|
|
||
|
|
$service = $this->newService(
|
||
|
|
userRoleRepository: $userRoleRepo,
|
||
|
|
directoryGateway: $directoryGateway,
|
||
|
|
permissionService: $permissionService,
|
||
|
|
assignableRoleService: $assignableRoleService
|
||
|
|
);
|
||
|
|
|
||
|
|
$result = $service->syncRoles(10, [1, 3], true, 99);
|
||
|
|
$this->assertTrue($result);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSyncRolesClearsPermissionCacheEvenOnFailure(): void
|
||
|
|
{
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->method('listActiveRoleIds')->willReturn([1]);
|
||
|
|
|
||
|
|
$userRoleRepo = $this->createMock(UserRoleRepositoryInterface::class);
|
||
|
|
$userRoleRepo->expects($this->once())->method('replaceForUser')->willReturn(false);
|
||
|
|
|
||
|
|
$permissionService = $this->createMock(PermissionService::class);
|
||
|
|
$permissionService->expects($this->once())->method('clearUserCache')->with(10);
|
||
|
|
|
||
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
||
|
|
$userWriteRepo->expects($this->never())->method('bumpAuthzVersion');
|
||
|
|
|
||
|
|
$service = $this->newService(
|
||
|
|
userWriteRepository: $userWriteRepo,
|
||
|
|
userRoleRepository: $userRoleRepo,
|
||
|
|
directoryGateway: $directoryGateway,
|
||
|
|
permissionService: $permissionService
|
||
|
|
);
|
||
|
|
|
||
|
|
$result = $service->syncRoles(10, [1]);
|
||
|
|
$this->assertFalse($result);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── syncDepartments ─────────────────────────────────────────────────
|
||
|
|
|
||
|
|
public function testSyncDepartmentsValidatesDepartmentsBelongToUserTenants(): void
|
||
|
|
{
|
||
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenantRepo->expects($this->once())->method('listTenantIdsByUserId')->with(10)->willReturn([1]);
|
||
|
|
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->expects($this->once())
|
||
|
|
->method('listActiveDepartmentIdsByTenantIds')
|
||
|
|
->with([1])
|
||
|
|
->willReturn([100, 101]);
|
||
|
|
|
||
|
|
$userDeptRepo = $this->createMock(UserDepartmentRepositoryInterface::class);
|
||
|
|
$userDeptRepo->expects($this->once())
|
||
|
|
->method('replaceForUser')
|
||
|
|
->with(10, [100]) // 200 filtered out because not in allowed list
|
||
|
|
->willReturn(true);
|
||
|
|
|
||
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
||
|
|
$userWriteRepo->expects($this->once())->method('bumpAuthzVersion')->with(10);
|
||
|
|
|
||
|
|
$service = $this->newService(
|
||
|
|
userWriteRepository: $userWriteRepo,
|
||
|
|
userTenantRepository: $userTenantRepo,
|
||
|
|
userDepartmentRepository: $userDeptRepo,
|
||
|
|
directoryGateway: $directoryGateway
|
||
|
|
);
|
||
|
|
|
||
|
|
$result = $service->syncDepartments(10, [100, 200]);
|
||
|
|
$this->assertTrue($result);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSyncDepartmentsRejectsAllWhenUserHasNoTenants(): void
|
||
|
|
{
|
||
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenantRepo->expects($this->once())->method('listTenantIdsByUserId')->with(10)->willReturn([]);
|
||
|
|
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->expects($this->never())->method('listActiveDepartmentIdsByTenantIds');
|
||
|
|
|
||
|
|
$userDeptRepo = $this->createMock(UserDepartmentRepositoryInterface::class);
|
||
|
|
$userDeptRepo->expects($this->once())
|
||
|
|
->method('replaceForUser')
|
||
|
|
->with(10, [])
|
||
|
|
->willReturn(true);
|
||
|
|
|
||
|
|
$service = $this->newService(
|
||
|
|
userTenantRepository: $userTenantRepo,
|
||
|
|
userDepartmentRepository: $userDeptRepo,
|
||
|
|
directoryGateway: $directoryGateway
|
||
|
|
);
|
||
|
|
|
||
|
|
$result = $service->syncDepartments(10, [100]);
|
||
|
|
$this->assertTrue($result);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSyncDepartmentsRejectsAllWhenNoActiveDepartmentsInTenants(): void
|
||
|
|
{
|
||
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenantRepo->method('listTenantIdsByUserId')->willReturn([1]);
|
||
|
|
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->method('listActiveDepartmentIdsByTenantIds')->willReturn([]);
|
||
|
|
|
||
|
|
$userDeptRepo = $this->createMock(UserDepartmentRepositoryInterface::class);
|
||
|
|
$userDeptRepo->expects($this->once())
|
||
|
|
->method('replaceForUser')
|
||
|
|
->with(10, [])
|
||
|
|
->willReturn(true);
|
||
|
|
|
||
|
|
$service = $this->newService(
|
||
|
|
userTenantRepository: $userTenantRepo,
|
||
|
|
userDepartmentRepository: $userDeptRepo,
|
||
|
|
directoryGateway: $directoryGateway
|
||
|
|
);
|
||
|
|
|
||
|
|
$result = $service->syncDepartments(10, [100]);
|
||
|
|
$this->assertTrue($result);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── syncAllAssignments ──────────────────────────────────────────────
|
||
|
|
|
||
|
|
public function testSyncAllAssignmentsReturnsFalseWhenUserIdIsZero(): void
|
||
|
|
{
|
||
|
|
$service = $this->newService();
|
||
|
|
$this->assertFalse($service->syncAllAssignments(0, [], [], []));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSyncAllAssignmentsCommitsTransactionOnSuccess(): void
|
||
|
|
{
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->method('listTenantIds')->willReturn([1]);
|
||
|
|
$directoryGateway->method('listActiveRoleIds')->willReturn([2]);
|
||
|
|
$directoryGateway->method('listActiveDepartmentIdsByTenantIds')->willReturn([3]);
|
||
|
|
|
||
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenantRepo->method('replaceForUser')->willReturn(true);
|
||
|
|
$userTenantRepo->method('listTenantIdsByUserId')->willReturn([1]);
|
||
|
|
|
||
|
|
$userRoleRepo = $this->createMock(UserRoleRepositoryInterface::class);
|
||
|
|
$userRoleRepo->method('replaceForUser')->willReturn(true);
|
||
|
|
|
||
|
|
$userDeptRepo = $this->createMock(UserDepartmentRepositoryInterface::class);
|
||
|
|
$userDeptRepo->method('replaceForUser')->willReturn(true);
|
||
|
|
|
||
|
|
$permissionService = $this->createMock(PermissionService::class);
|
||
|
|
|
||
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
||
|
|
$userWriteRepo->expects($this->once())->method('bumpAuthzVersion')->with(10);
|
||
|
|
|
||
|
|
$dbSession = $this->createMock(DatabaseSessionRepository::class);
|
||
|
|
$dbSession->expects($this->once())->method('beginTransaction');
|
||
|
|
$dbSession->expects($this->once())->method('commitTransaction');
|
||
|
|
$dbSession->expects($this->never())->method('rollbackTransaction');
|
||
|
|
|
||
|
|
$service = $this->newService(
|
||
|
|
userWriteRepository: $userWriteRepo,
|
||
|
|
userTenantRepository: $userTenantRepo,
|
||
|
|
userRoleRepository: $userRoleRepo,
|
||
|
|
userDepartmentRepository: $userDeptRepo,
|
||
|
|
directoryGateway: $directoryGateway,
|
||
|
|
permissionService: $permissionService,
|
||
|
|
databaseSessionRepository: $dbSession
|
||
|
|
);
|
||
|
|
|
||
|
|
$result = $service->syncAllAssignments(10, [1], [2], [3]);
|
||
|
|
$this->assertTrue($result);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSyncAllAssignmentsRollsBackOnSyncFailure(): void
|
||
|
|
{
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->method('listTenantIds')->willReturn([1]);
|
||
|
|
|
||
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenantRepo->method('replaceForUser')->willReturn(false); // tenant sync fails
|
||
|
|
|
||
|
|
$dbSession = $this->createMock(DatabaseSessionRepository::class);
|
||
|
|
$dbSession->expects($this->once())->method('beginTransaction');
|
||
|
|
$dbSession->expects($this->never())->method('commitTransaction');
|
||
|
|
$dbSession->expects($this->once())->method('rollbackTransaction');
|
||
|
|
|
||
|
|
$service = $this->newService(
|
||
|
|
userTenantRepository: $userTenantRepo,
|
||
|
|
directoryGateway: $directoryGateway,
|
||
|
|
databaseSessionRepository: $dbSession
|
||
|
|
);
|
||
|
|
|
||
|
|
$result = $service->syncAllAssignments(10, [1], [2], [3]);
|
||
|
|
$this->assertFalse($result);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSyncAllAssignmentsRollsBackOnException(): void
|
||
|
|
{
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->method('listTenantIds')
|
||
|
|
->willThrowException(new \RuntimeException('db error'));
|
||
|
|
|
||
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
|
||
|
|
$dbSession = $this->createMock(DatabaseSessionRepository::class);
|
||
|
|
$dbSession->expects($this->once())->method('beginTransaction');
|
||
|
|
$dbSession->expects($this->never())->method('commitTransaction');
|
||
|
|
$dbSession->expects($this->once())->method('rollbackTransaction');
|
||
|
|
|
||
|
|
$service = $this->newService(
|
||
|
|
userTenantRepository: $userTenantRepo,
|
||
|
|
directoryGateway: $directoryGateway,
|
||
|
|
databaseSessionRepository: $dbSession
|
||
|
|
);
|
||
|
|
|
||
|
|
$result = $service->syncAllAssignments(10, [1], [], []);
|
||
|
|
$this->assertFalse($result);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testSyncAllAssignmentsBumpsAuthzOnlyOnce(): void
|
||
|
|
{
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->method('listTenantIds')->willReturn([1]);
|
||
|
|
$directoryGateway->method('listActiveRoleIds')->willReturn([2]);
|
||
|
|
$directoryGateway->method('listActiveDepartmentIdsByTenantIds')->willReturn([3]);
|
||
|
|
|
||
|
|
$userTenantRepo = $this->createMock(UserTenantRepositoryInterface::class);
|
||
|
|
$userTenantRepo->method('replaceForUser')->willReturn(true);
|
||
|
|
$userTenantRepo->method('listTenantIdsByUserId')->willReturn([1]);
|
||
|
|
|
||
|
|
$userRoleRepo = $this->createMock(UserRoleRepositoryInterface::class);
|
||
|
|
$userRoleRepo->method('replaceForUser')->willReturn(true);
|
||
|
|
|
||
|
|
$userDeptRepo = $this->createMock(UserDepartmentRepositoryInterface::class);
|
||
|
|
$userDeptRepo->method('replaceForUser')->willReturn(true);
|
||
|
|
|
||
|
|
$permissionService = $this->createMock(PermissionService::class);
|
||
|
|
|
||
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
||
|
|
// bumpAuthzVersion should be called exactly once (not three times)
|
||
|
|
$userWriteRepo->expects($this->once())->method('bumpAuthzVersion')->with(10);
|
||
|
|
|
||
|
|
$dbSession = $this->createMock(DatabaseSessionRepository::class);
|
||
|
|
|
||
|
|
$service = $this->newService(
|
||
|
|
userWriteRepository: $userWriteRepo,
|
||
|
|
userTenantRepository: $userTenantRepo,
|
||
|
|
userRoleRepository: $userRoleRepo,
|
||
|
|
userDepartmentRepository: $userDeptRepo,
|
||
|
|
directoryGateway: $directoryGateway,
|
||
|
|
permissionService: $permissionService,
|
||
|
|
databaseSessionRepository: $dbSession
|
||
|
|
);
|
||
|
|
|
||
|
|
$service->syncAllAssignments(10, [1], [2], [3]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── bumpAuthzVersion ────────────────────────────────────────────────
|
||
|
|
|
||
|
|
public function testBumpAuthzVersionReturnsEarlyWhenUserIdIsZero(): void
|
||
|
|
{
|
||
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
||
|
|
$userWriteRepo->expects($this->never())->method('bumpAuthzVersion');
|
||
|
|
|
||
|
|
$service = $this->newService(userWriteRepository: $userWriteRepo);
|
||
|
|
$service->bumpAuthzVersion(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testBumpAuthzVersionReturnsEarlyWhenUserIdIsNegative(): void
|
||
|
|
{
|
||
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
||
|
|
$userWriteRepo->expects($this->never())->method('bumpAuthzVersion');
|
||
|
|
|
||
|
|
$service = $this->newService(userWriteRepository: $userWriteRepo);
|
||
|
|
$service->bumpAuthzVersion(-1);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testBumpAuthzVersionCallsRepository(): void
|
||
|
|
{
|
||
|
|
$userWriteRepo = $this->createMock(UserWriteRepositoryInterface::class);
|
||
|
|
$userWriteRepo->expects($this->once())->method('bumpAuthzVersion')->with(42);
|
||
|
|
|
||
|
|
$service = $this->newService(userWriteRepository: $userWriteRepo);
|
||
|
|
$service->bumpAuthzVersion(42);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── normalizeIdInput ────────────────────────────────────────────────
|
||
|
|
|
||
|
|
public function testNormalizeIdInputWithScalarInt(): void
|
||
|
|
{
|
||
|
|
$service = $this->newService();
|
||
|
|
$this->assertSame([5], $service->normalizeIdInput(5));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testNormalizeIdInputWithArrayOfInts(): void
|
||
|
|
{
|
||
|
|
$service = $this->newService();
|
||
|
|
$this->assertSame([1, 2, 3], $service->normalizeIdInput([1, 2, 3]));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testNormalizeIdInputWithCommaSeparatedString(): void
|
||
|
|
{
|
||
|
|
$service = $this->newService();
|
||
|
|
$this->assertSame([1, 2, 3], $service->normalizeIdInput('1,2,3'));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testNormalizeIdInputWithNestedArrays(): void
|
||
|
|
{
|
||
|
|
$service = $this->newService();
|
||
|
|
$this->assertSame([1, 2, 3], $service->normalizeIdInput([[1, [2]], 3]));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testNormalizeIdInputFiltersNegativeValues(): void
|
||
|
|
{
|
||
|
|
$service = $this->newService();
|
||
|
|
$this->assertSame([1, 3], $service->normalizeIdInput([1, -2, 3, 0]));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testNormalizeIdInputRemovesDuplicates(): void
|
||
|
|
{
|
||
|
|
$service = $this->newService();
|
||
|
|
$this->assertSame([1, 2], $service->normalizeIdInput([1, 2, 1, 2]));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testNormalizeIdInputHandlesEmptyString(): void
|
||
|
|
{
|
||
|
|
$service = $this->newService();
|
||
|
|
$this->assertSame([], $service->normalizeIdInput(''));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testNormalizeIdInputHandlesEmptyArray(): void
|
||
|
|
{
|
||
|
|
$service = $this->newService();
|
||
|
|
$this->assertSame([], $service->normalizeIdInput([]));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── normalizeTenantIds ──────────────────────────────────────────────
|
||
|
|
|
||
|
|
public function testNormalizeTenantIdsFiltersAgainstValidTenantIds(): void
|
||
|
|
{
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->expects($this->once())->method('listTenantIds')->willReturn([1, 2, 3]);
|
||
|
|
|
||
|
|
$service = $this->newService(directoryGateway: $directoryGateway);
|
||
|
|
$result = $service->normalizeTenantIds([1, 99, 2]);
|
||
|
|
|
||
|
|
$this->assertSame([1, 2], $result);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testNormalizeTenantIdsFiltersNegativeAndZeroIds(): void
|
||
|
|
{
|
||
|
|
$directoryGateway = $this->createMock(UserDirectoryGateway::class);
|
||
|
|
$directoryGateway->method('listTenantIds')->willReturn([1, 2]);
|
||
|
|
|
||
|
|
$service = $this->newService(directoryGateway: $directoryGateway);
|
||
|
|
$result = $service->normalizeTenantIds([0, -1, 1]);
|
||
|
|
|
||
|
|
$this->assertSame([1], $result);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── Helper ──────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
private function newService(
|
||
|
|
?UserWriteRepositoryInterface $userWriteRepository = null,
|
||
|
|
?UserTenantRepositoryInterface $userTenantRepository = null,
|
||
|
|
?UserRoleRepositoryInterface $userRoleRepository = null,
|
||
|
|
?UserDepartmentRepositoryInterface $userDepartmentRepository = null,
|
||
|
|
?UserDirectoryGateway $directoryGateway = null,
|
||
|
|
?PermissionService $permissionService = null,
|
||
|
|
?DatabaseSessionRepository $databaseSessionRepository = null,
|
||
|
|
?AssignableRoleService $assignableRoleService = null,
|
||
|
|
): UserAssignmentService {
|
||
|
|
return new UserAssignmentService(
|
||
|
|
$userWriteRepository ?? $this->createMock(UserWriteRepositoryInterface::class),
|
||
|
|
$userTenantRepository ?? $this->createMock(UserTenantRepositoryInterface::class),
|
||
|
|
$userRoleRepository ?? $this->createMock(UserRoleRepositoryInterface::class),
|
||
|
|
$userDepartmentRepository ?? $this->createMock(UserDepartmentRepositoryInterface::class),
|
||
|
|
$directoryGateway ?? $this->createMock(UserDirectoryGateway::class),
|
||
|
|
$permissionService ?? $this->createMock(PermissionService::class),
|
||
|
|
$databaseSessionRepository ?? $this->createMock(DatabaseSessionRepository::class),
|
||
|
|
$assignableRoleService ?? $this->createMock(AssignableRoleService::class),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|