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), ); } }