mockConnection = $this->getMockBuilder(ClusterInterface::class)->getMock(); $this->mockStrategy = $this->getMockBuilder(\Predis\Cluster\StrategyInterface::class)->getMock(); $this->mockCommand = $this->getMockBuilder(CommandInterface::class)->getMock(); $this->mockConnection ->method('getClusterStrategy') ->willReturn($this->mockStrategy); } /** * @group disconnected * @return void */ public function testExecuteCommandThrowsExceptionOnNonInitializedTransactionContext(): void { $strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState()); $this->expectException(TransactionException::class); $this->expectExceptionMessage('Transaction context should be initialized first'); $strategy->executeCommand($this->mockCommand); } /** * @group disconnected * @return void * @throws TransactionException */ public function testExecuteCommandAssignsHashSlotToSlotlessCommand(): void { $anotherCommand = new SET(); $this->mockStrategy ->expects($this->exactly(2)) ->method('getSlot') ->withConsecutive([$this->mockCommand], [$anotherCommand]) ->willReturnOnConsecutiveCalls(10, null); $strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState()); $strategy->initializeTransaction(); $this->assertEquals('QUEUED', $strategy->executeCommand($this->mockCommand)); $this->assertEquals('QUEUED', $strategy->executeCommand($anotherCommand)); $this->assertEquals(10, $anotherCommand->getSlot()); } /** * @group disconnected * @return void * @throws TransactionException */ public function testExecuteCommandReturnsErrorOnMissMatchingSlots(): void { $this->mockStrategy ->expects($this->exactly(2)) ->method('getSlot') ->with($this->mockCommand) ->willReturnOnConsecutiveCalls(123, 124); $strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState()); $strategy->initializeTransaction(); $strategy->executeCommand($this->mockCommand); $response = $strategy->executeCommand($this->mockCommand); $this->assertInstanceOf(ErrorInterface::class, $response); $this->assertSame( 'To be able to execute a transaction against cluster, all commands should operate on the same hash slot', $response->getMessage() ); } /** * @group disconnected * @return void * @throws TransactionException */ public function testExecuteCommandQueueCommandsMappedToTheSameSlot(): void { $anotherMockCommand = $this->getMockBuilder(CommandInterface::class)->getMock(); $this->mockStrategy ->expects($this->exactly(2)) ->method('getSlot') ->withConsecutive([$this->mockCommand], [$anotherMockCommand]) ->willReturnOnConsecutiveCalls([123], [123]); $strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState()); $strategy->initializeTransaction(); $resp1 = $strategy->executeCommand($this->mockCommand); $resp2 = $strategy->executeCommand($this->mockCommand); $this->assertEquals('QUEUED', $resp1); $this->assertEquals('QUEUED', $resp2); } /** * @group disconnected * @return void */ public function testInitializeTransactionContext(): void { $strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState()); $this->assertTrue($strategy->initializeTransaction()); $this->assertTrue($strategy->initializeTransaction()); } /** * @group disconnected * @return void * @throws TransactionException */ public function testExecuteTransactionReturnsNullOnInitializeError(): void { $this->mockConnection ->expects($this->once()) ->method('executeCommand') ->with(new MULTI()) ->willReturn('ERR'); $strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState()); $strategy->initializeTransaction(); $strategy->executeCommand(new SET()); $this->assertNull($strategy->executeTransaction()); } /** * @group disconnected * @return void * @throws TransactionException */ public function testExecuteTransactionReturnsNullOnQueueingError(): void { $this->mockConnection ->expects($this->exactly(3)) ->method('executeCommand') ->withConsecutive([new MULTI()], [new SET()], [new SET()]) ->willReturnOnConsecutiveCalls('OK', 'QUEUED', 'ERR'); $strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState()); $strategy->initializeTransaction(); $strategy->executeCommand(new SET()); $strategy->executeCommand(new SET()); $strategy->executeCommand(new SET()); $this->assertNull($strategy->executeTransaction()); } /** * @group disconnected * @return void * @throws TransactionException */ public function testExecuteTransactionReturnsResultOnSuccessfulTransaction(): void { $command1 = new SET(); $command2 = new SET(); $command3 = new SET(); $command1->setArguments(['{foo}bar', 'value']); $command2->setArguments(['{foo}baz', 'value']); $command3->setArguments(['{foo}foo', 'value']); $this->mockConnection ->expects($this->exactly(5)) ->method('executeCommand') ->withConsecutive( [new MULTI()], [$command1], [$command2], [$command3], [new EXEC()] ) ->willReturnOnConsecutiveCalls( 'OK', 'QUEUED', 'QUEUED', 'QUEUED', [new Status('OK'), new Status('OK'), new Status('OK')] ); $strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState()); $strategy->initializeTransaction(); $strategy->executeCommand($command1); $strategy->executeCommand($command2); $strategy->executeCommand($command3); $this->assertEquals( [new Status('OK'), new Status('OK'), new Status('OK')], $strategy->executeTransaction() ); } /** * @group disconnected * @return void * @throws TransactionException */ public function testMulti(): void { $this->mockConnection ->expects($this->once()) ->method('executeCommand') ->with(new MULTI()) ->willReturnOnConsecutiveCalls('ERR', 'OK'); $strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState()); $this->assertEquals('ERR', $strategy->multi()); $this->expectException(TransactionException::class); $this->expectExceptionMessage('Transaction context should be initialized first'); $strategy->executeCommand(new SET()); $this->assertEquals('OK', $strategy->multi()); $this->assertEquals('QUEUED', $strategy->executeCommand(new SET())); } /** * @group disconnected * @return void * @throws TransactionException */ public function testWatchThrowsExceptionOnKeysPointingToDifferentSlots(): void { $this->mockStrategy ->expects($this->once()) ->method('checkSameSlotForKeys') ->with(['key1', 'key2', 'key3']) ->willReturn(false); $strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState()); $this->expectException(TransactionException::class); $this->expectExceptionMessage('WATCHed keys should point to the same hash slot'); $strategy->watch(['key1', 'key2', 'key3']); } /** * @group disconnected * @return void * @throws TransactionException */ public function testWatchReturnCorrectResponse(): void { $this->mockStrategy ->expects($this->once()) ->method('checkSameSlotForKeys') ->with(['key1', 'key2', 'key3']) ->willReturn(true); $this->mockStrategy ->expects($this->once()) ->method('getSlotByKey') ->with('key1') ->willReturn(10); $this->mockConnection ->expects($this->once()) ->method('executeCommand') ->withAnyParameters() ->willReturn(new Status('OK')); $strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState()); $this->assertTrue($strategy->watch(['key1', 'key2', 'key3'])); } /** * @group disconnected * @return void * @throws TransactionException */ public function testWatchAdditionallyInitializeTransactionContextOnCASTransaction(): void { $this->mockStrategy ->expects($this->once()) ->method('checkSameSlotForKeys') ->with(['key1', 'key2', 'key3']) ->willReturn(true); $this->mockStrategy ->expects($this->once()) ->method('getSlotByKey') ->with('key1') ->willReturn(10); $this->mockConnection ->expects($this->once()) ->method('executeCommand') ->withAnyParameters() ->willReturn(new Status('OK')); $state = new MultiExecState(); $state->set(MultiExecState::CAS); $strategy = new ClusterConnectionStrategy($this->mockConnection, $state); $this->assertTrue($strategy->watch(['key1', 'key2', 'key3'])); $this->assertEquals('QUEUED', $strategy->executeCommand(new SET())); } /** * @group disconnected * @return void */ public function testDiscard(): void { $this->mockConnection ->expects($this->once()) ->method('executeCommand') ->withAnyParameters() ->willReturn(new Status('OK')); $strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState()); $this->assertEquals('OK', $strategy->discard()); } /** * @group disconnected * @return void */ public function testUnwatch(): void { $this->mockConnection ->expects($this->once()) ->method('executeCommand') ->withAnyParameters() ->willReturn(new Status('OK')); $strategy = new ClusterConnectionStrategy($this->mockConnection, new MultiExecState()); $this->assertEquals('OK', $strategy->unwatch()); } }