Files
predis/tests/Predis/Transaction/Strategy/ClusterConnectionStrategyTest.php
T
2025-03-31 12:14:37 +03:00

382 lines
12 KiB
PHP

<?php
/*
* This file is part of the Predis package.
*
* (c) 2009-2020 Daniele Alessandri
* (c) 2021-2025 Till Krüss
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Predis\Transaction\Strategy;
use PHPUnit\Framework\TestCase;
use Predis\Command\CommandInterface;
use Predis\Command\Redis\EXEC;
use Predis\Command\Redis\MULTI;
use Predis\Command\Redis\SET;
use Predis\Connection\Cluster\ClusterInterface;
use Predis\Response\ErrorInterface;
use Predis\Response\Status;
use Predis\Transaction\Exception\TransactionException;
use Predis\Transaction\MultiExecState;
class ClusterConnectionStrategyTest extends TestCase
{
/**
* @var ClusterInterface
*/
private $mockConnection;
/**
* @var \Predis\Cluster\StrategyInterface
*/
private $mockStrategy;
/**
* @var CommandInterface
*/
private $mockCommand;
/**
* @return void
*/
protected function setUp(): void
{
$this->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());
}
}