From 2e142c97615de689d0bdc91eb5b30ad869f04aa3 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Wed, 18 Apr 2018 07:47:01 +0200 Subject: [PATCH 01/39] Forward compatibility with voryx/event-loop 3.0 and while supporting 2.0 --- composer.json | 2 +- tests/TestCase.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index f3f740b..ee755b8 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ } }, "require": { - "voryx/event-loop": "^2.0.2", + "voryx/event-loop": "^3.0 || ^2.0.2", "reactivex/rxphp": "^2.0", "react/socket": "^1.0 || ^0.8 || ^0.7", "evenement/evenement": "^2.0 | ^3.0" diff --git a/tests/TestCase.php b/tests/TestCase.php index 7dc3f16..bcace11 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -38,7 +38,7 @@ public static function stopLoop() public static function cancelCurrentTimeoutTimer() { if (static::$timeoutTimer !== null) { - static::$timeoutTimer->cancel(); + static::getLoop()->cancelTimer(static::$timeoutTimer); static::$timeoutTimer = null; } } From 4d5e23fc73f25638c11bde7efb0642bf27a547d1 Mon Sep 17 00:00:00 2001 From: David Dan Date: Wed, 18 Apr 2018 07:10:14 -0400 Subject: [PATCH 02/39] Cleanup README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 34bef36..d246dfa 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ $client = new PgAsync\Client([ "database" => "matt" ]); -$client->query('SELECT * FROM channel')->subscribe(new \Rx\Observer\CallbackObserver( +$client->query('SELECT * FROM channel')->subscribe( function ($row) { var_dump($row); }, @@ -30,7 +30,7 @@ $client->query('SELECT * FROM channel')->subscribe(new \Rx\Observer\CallbackObse function () { echo "Complete.\n"; } -)); +); ``` @@ -48,7 +48,7 @@ $client = new PgAsync\Client([ ]); $client->executeStatement('SELECT * FROM channel WHERE id = $1', ['5']) - ->subscribe(new \Rx\Observer\CallbackObserver( + ->subscribe( function ($row) { var_dump($row); }, @@ -58,7 +58,7 @@ $client->executeStatement('SELECT * FROM channel WHERE id = $1', ['5']) function () { echo "Complete.\n"; } - )); + ); ``` @@ -66,7 +66,7 @@ $client->executeStatement('SELECT * FROM channel WHERE id = $1', ['5']) With [composer](https://getcomposer.org/) install into you project with: Install pgasync: -```composer require voryx/pgasync:dev-master``` +```composer require voryx/pgasync``` ## What it can do - Run queries (CREATE, UPDATE, INSERT, SELECT, DELETE) From cf166f149ab4e0eaf75ae44c3ab81fda2e0ec7da Mon Sep 17 00:00:00 2001 From: Samuel NELA Date: Fri, 19 Oct 2018 13:35:58 +0200 Subject: [PATCH 03/39] Remove deprecated testcase implementation --- tests/TestCase.php | 3 ++- tests/Unit/Message/ParseTest.php | 4 +++- tests/Unit/Message/SSLRequestTest.php | 4 +++- tests/Unit/Message/StartupMessageTest.php | 4 +++- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index bcace11..5ec4043 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,8 +6,9 @@ use React\EventLoop\Factory; use React\EventLoop\LoopInterface; use React\EventLoop\Timer\Timer; +use PHPUnit\Framework\TestCase as BaseTestCase; -class TestCase extends \PHPUnit_Framework_TestCase +class TestCase extends BaseTestCase { const DBNAME = 'pgasync_test'; diff --git a/tests/Unit/Message/ParseTest.php b/tests/Unit/Message/ParseTest.php index d2f3560..dae6a0c 100644 --- a/tests/Unit/Message/ParseTest.php +++ b/tests/Unit/Message/ParseTest.php @@ -1,6 +1,8 @@ Date: Fri, 2 Nov 2018 11:13:40 -0400 Subject: [PATCH 04/39] Fix broken DNS test. Add 7.2 to travis --- .travis.yml | 1 + tests/Integration/ConnectionTest.php | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bccff6a..e3cb031 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ sudo: required php: - 7.0 - 7.1 + - 7.2 addons: postgresql: "9.3" diff --git a/tests/Integration/ConnectionTest.php b/tests/Integration/ConnectionTest.php index 27a827d..2b42f83 100644 --- a/tests/Integration/ConnectionTest.php +++ b/tests/Integration/ConnectionTest.php @@ -129,7 +129,9 @@ function () { $this->runLoopWithTimeout(2); - $this->assertInstanceOf(RecordNotFoundException::class, $error); + // At some point, DNS was returning RecordNotFoundException + // as long as we are getting an Exception here, we should be good + $this->assertInstanceOf(\Exception::class, $error); } public function testSendingTwoQueriesWithoutWaitingNoAutoDisconnect() From 3e2441c1f75529f5a6ad95d9eca84646d161a66a Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Fri, 2 Nov 2018 11:41:48 -0400 Subject: [PATCH 05/39] Add RecordNotFoundException back to test --- tests/Integration/ConnectionTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Integration/ConnectionTest.php b/tests/Integration/ConnectionTest.php index 2b42f83..eeb37aa 100644 --- a/tests/Integration/ConnectionTest.php +++ b/tests/Integration/ConnectionTest.php @@ -132,6 +132,7 @@ function () { // At some point, DNS was returning RecordNotFoundException // as long as we are getting an Exception here, we should be good $this->assertInstanceOf(\Exception::class, $error); + $this->assertInstanceOf(RecordNotFoundException::class, $error->getPrevious()); } public function testSendingTwoQueriesWithoutWaitingNoAutoDisconnect() From 3cb99de8b13f7facb56a148f14fc1ba6ae6db227 Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Wed, 21 Nov 2018 11:06:18 -0500 Subject: [PATCH 06/39] Add PHP 7.3 to tests --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index e3cb031..f805751 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ php: - 7.0 - 7.1 - 7.2 + - 7.3 addons: postgresql: "9.3" From 44722e845c920c0bde760fca728417352656128c Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Wed, 21 Nov 2018 17:53:36 -0500 Subject: [PATCH 07/39] Add LISTEN/NOTIFY support --- README.md | 16 ++++ src/PgAsync/Client.php | 62 +++++++++++++++ src/PgAsync/Connection.php | 35 +++++++-- src/PgAsync/Message/Message.php | 2 + src/PgAsync/Message/NotificationResponse.php | 81 ++++++++++++++++++++ tests/Unit/Message/MessageTest.php | 16 ++++ 6 files changed, 204 insertions(+), 8 deletions(-) create mode 100644 src/PgAsync/Message/NotificationResponse.php diff --git a/README.md b/README.md index d246dfa..27a4716 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,22 @@ $client->executeStatement('SELECT * FROM channel WHERE id = $1', ['5']) ``` +## Example - LISTEN/NOTIFY +```php +$client = new PgAsync\Client([ + "host" => "127.0.0.1", + "port" => "5432", + "user" => "matt", + "database" => "matt", + "auto_disconnect" => true //This option will force the client to disconnect as soon as it completes. The connection will not be returned to the connection pool. +]); + +$client->listen('some_channel') + ->subscriber(function (\PgAsync\Message\NotificationResponse $message) { + echo $message->getChannelName() . ': ' . $message->getPayload() . "\n"; + }); +``` + ## Install With [composer](https://getcomposer.org/) install into you project with: diff --git a/src/PgAsync/Client.php b/src/PgAsync/Client.php index 639f511..3f0ad10 100644 --- a/src/PgAsync/Client.php +++ b/src/PgAsync/Client.php @@ -2,9 +2,11 @@ namespace PgAsync; +use PgAsync\Message\NotificationResponse; use React\EventLoop\LoopInterface; use React\Socket\ConnectorInterface; use Rx\Observable; +use Rx\Subject\Subject; class Client { @@ -30,6 +32,12 @@ class Client /** @var int */ private $maxConnections = 5; + /** @var Subject[] */ + private $listeners = []; + + /** @var Connection */ + private $listenConnection; + public function __construct(array $parameters, LoopInterface $loop = null, ConnectorInterface $connector = null) { $this->loop = $loop ?: \EventLoop\getLoop(); @@ -158,4 +166,58 @@ public function closeNow() $connection->disconnect(); } } + + public function listen($channel) + { + if (!isset($this->listeners[$channel])) { + $subscriberCount = 0; + $listenerDisposable = null; + $channelSubject = new Subject(); + $this->listeners[$channel] = Observable::defer(function () use ($channel, &$subscriberCount, &$listenerDisposable, $channelSubject) { + $unlisten = function () use ($channel, &$subscriberCount, &$listenerDisposable, $channelSubject) { + $subscriberCount--; + if ($subscriberCount !== 0) { + return; + } + $this->listenConnection->query("UNLISTEN " . $channel) + ->subscribe(); + + $listenerDisposable->dispose(); + $listenerDisposable = null; + unset($this->listeners[$channel]); + + if (empty($this->listeners)) { + $this->listenConnection->disconnect(); + $this->listenConnection = null; + } + }; + + return Observable::start(function () use ($channel, &$listenerDisposable, &$subscriberCount, $channelSubject) { + $subscriberCount++; + if ($this->listenConnection === null) { + $this->listenConnection = $this->createNewConnection(); + } + + if ($this->listenConnection === null) { + throw new \Exception('Could not get new connection to listen on.'); + } + + if ($listenerDisposable !== null) { + return; + } + $listenerDisposable = + $this->listenConnection->query("LISTEN " . $channel) + ->merge($this->listenConnection->notifications()) + ->filter(function (NotificationResponse $message) use ($channel) { + return $message->getChannelName() === $channel; + }) + ->subscribe($channelSubject); + })->skip(1) + ->merge($channelSubject) + ->finally($unlisten); + }); + } + + return $this->listeners[$channel]; + } } diff --git a/src/PgAsync/Connection.php b/src/PgAsync/Connection.php index 5a25f3a..1d69b96 100644 --- a/src/PgAsync/Connection.php +++ b/src/PgAsync/Connection.php @@ -23,6 +23,7 @@ use PgAsync\Message\ErrorResponse; use PgAsync\Message\Message; use PgAsync\Message\NoticeResponse; +use PgAsync\Message\NotificationResponse; use PgAsync\Message\ParameterStatus; use PgAsync\Message\ParseComplete; use PgAsync\Command\Query; @@ -39,6 +40,7 @@ use Rx\Observable\AnonymousObservable; use Rx\ObserverInterface; use Rx\SchedulerInterface; +use Rx\Subject\Subject; class Connection extends EventEmitter { @@ -108,6 +110,9 @@ class Connection extends EventEmitter /** @var string */ private $uri; + /** @var Subject */ + private $notificationSubject; + /** * Can be 'I' for Idle, 'T' if in transactions block * or 'E' if in failed transaction block (queries will fail until end of trans) @@ -147,14 +152,15 @@ public function __construct(array $parameters, LoopInterface $loop, ConnectorInt unset($parameters['auto_disconnect']); } - $this->parameters = $parameters; - $this->loop = $loop; - $this->commandQueue = []; - $this->queryState = static::STATE_BUSY; - $this->queryType = static::QUERY_SIMPLE; - $this->connStatus = static::CONNECTION_NEEDED; - $this->socket = $connector ?: new Connector($loop); - $this->uri = 'tcp://' . $this->parameters['host'] . ':' . $this->parameters['port']; + $this->parameters = $parameters; + $this->loop = $loop; + $this->commandQueue = []; + $this->queryState = static::STATE_BUSY; + $this->queryType = static::QUERY_SIMPLE; + $this->connStatus = static::CONNECTION_NEEDED; + $this->socket = $connector ?: new Connector($loop); + $this->uri = 'tcp://' . $this->parameters['host'] . ':' . $this->parameters['port']; + $this->notificationSubject = new Subject(); } private function start() @@ -297,9 +303,16 @@ public function handleMessage($message) $this->handleReadyForQuery($message); } elseif ($message instanceof RowDescription) { $this->handleRowDescription($message); + } elseif ($message instanceof NotificationResponse) { + $this->handleNotificationResponse($message); } } + private function handleNotificationResponse(NotificationResponse $message) + { + $this->notificationSubject->onNext($message); + } + private function handleDataRow(DataRow $dataRow) { if ($this->queryState === $this::STATE_BUSY && $this->currentCommand instanceof CommandInterface) { @@ -448,6 +461,8 @@ private function failAllCommandsWith(\Throwable $e = null) { $e = $e ?: new \Exception('unknown error'); + $this->notificationSubject->onError($e); + while (count($this->commandQueue) > 0) { $c = array_shift($this->commandQueue); if ($c instanceof CommandInterface) { @@ -636,4 +651,8 @@ private function cancelRequest() }); } } + + public function notifications() { + return $this->notificationSubject->asObservable(); + } } diff --git a/src/PgAsync/Message/Message.php b/src/PgAsync/Message/Message.php index 5550da8..8f08588 100644 --- a/src/PgAsync/Message/Message.php +++ b/src/PgAsync/Message/Message.php @@ -52,6 +52,8 @@ public static function createMessageFromIdentifier(string $identifier): ParserIn return new ReadyForQuery(); case 'T': return new RowDescription(); + case NotificationResponse::getMessageIdentifier(): + return new NotificationResponse(); } return new Discard(); diff --git a/src/PgAsync/Message/NotificationResponse.php b/src/PgAsync/Message/NotificationResponse.php new file mode 100644 index 0000000..4a27e0a --- /dev/null +++ b/src/PgAsync/Message/NotificationResponse.php @@ -0,0 +1,81 @@ +notifyingProcessId = unpack('N', substr($rawMessage, $currentPos, 4))[1]; + $currentPos += 4; + + $rawPayload = substr($rawMessage, $currentPos); + $parts = explode("\0", $rawPayload); + + if (count($parts) !== 3) { + throw new \UnderflowException('Wrong number of notification parts in payload'); + } + + $this->channelName = $parts[0]; + $this->payload = $parts[1]; + } + + /** + * @return string + */ + public function getPayload(): string + { + return $this->payload; + } + + /** + * @return int + */ + public function getNotifyingProcessId(): int + { + return $this->notifyingProcessId; + } + + /** + * @return string + */ + public function getChannelName(): string + { + return $this->channelName; + } + + /** + * @inheritDoc + */ + public static function getMessageIdentifier(): string + { + return 'A'; + } + + public function getNoticeMessages(): array + { + return $this->noticeMessages; + } +} diff --git a/tests/Unit/Message/MessageTest.php b/tests/Unit/Message/MessageTest.php index b46d2ec..446a94a 100644 --- a/tests/Unit/Message/MessageTest.php +++ b/tests/Unit/Message/MessageTest.php @@ -7,4 +7,20 @@ public function testInt32() $this->assertEquals("\x04\xd2\x16\x2f", \PgAsync\Message\Message::int32(80877103)); $this->assertEquals("\x00\x00\x00\x00", \PgAsync\Message\Message::int32(0)); } + + public function testNotificationResponse() + { + $rawNotificationMessage = hex2bin('41000000190000040c686572650048656c6c6f20746865726500'); + + + $notificationResponse = \PgAsync\Message\Message::createMessageFromIdentifier($rawNotificationMessage[0]); + $this->assertInstanceOf(\PgAsync\Message\NotificationResponse::class, $notificationResponse); + /** @var \PgAsync\Message\NotificationResponse */ + $notificationResponse->parseData($rawNotificationMessage); + + $this->assertEquals('Hello there', $notificationResponse->getPayload()); + $this->assertEquals('here', $notificationResponse->getChannelName()); + $this->assertEquals(1036, $notificationResponse->getNotifyingProcessId()); + + } } From b8e945121e39a441c7d6d273682b6a7fc45bff82 Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Wed, 21 Nov 2018 17:59:35 -0500 Subject: [PATCH 08/39] Typo in README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 27a4716..75d23d5 100644 --- a/README.md +++ b/README.md @@ -73,9 +73,11 @@ $client = new PgAsync\Client([ ]); $client->listen('some_channel') - ->subscriber(function (\PgAsync\Message\NotificationResponse $message) { + ->subscribe(function (\PgAsync\Message\NotificationResponse $message) { echo $message->getChannelName() . ': ' . $message->getPayload() . "\n"; }); + +$client->query("NOTIFY some_channel, 'Hello World'")->subscribe(); ``` ## Install From 0d143f94f7df341e58874f59005032a13c73b674 Mon Sep 17 00:00:00 2001 From: David Dan Date: Wed, 21 Nov 2018 23:02:36 -0500 Subject: [PATCH 09/39] Refactored `listen()` --- README.md | 3 +- example/ListenNotify.php | 20 ++++++++++ src/PgAsync/Client.php | 86 ++++++++++++++++------------------------ 3 files changed, 56 insertions(+), 53 deletions(-) create mode 100644 example/ListenNotify.php diff --git a/README.md b/README.md index 75d23d5..80ac832 100644 --- a/README.md +++ b/README.md @@ -68,8 +68,7 @@ $client = new PgAsync\Client([ "host" => "127.0.0.1", "port" => "5432", "user" => "matt", - "database" => "matt", - "auto_disconnect" => true //This option will force the client to disconnect as soon as it completes. The connection will not be returned to the connection pool. + "database" => "matt" ]); $client->listen('some_channel') diff --git a/example/ListenNotify.php b/example/ListenNotify.php new file mode 100644 index 0000000..7d8d6c1 --- /dev/null +++ b/example/ListenNotify.php @@ -0,0 +1,20 @@ + '127.0.0.1', + 'port' => '5432', + 'user' => 'daviddan', + 'database' => 'postgres', +]); + +$client->listen('some_channel') + ->subscribe(function (\PgAsync\Message\NotificationResponse $message) { + echo $message->getChannelName() . ': ' . $message->getPayload() . "\n"; + }); + +\Rx\Observable::timer(1000) + ->flatMapTo($client->query("NOTIFY some_channel, 'Hello World'")) + ->subscribe(); + diff --git a/src/PgAsync/Client.php b/src/PgAsync/Client.php index 3f0ad10..08012f8 100644 --- a/src/PgAsync/Client.php +++ b/src/PgAsync/Client.php @@ -40,8 +40,8 @@ class Client public function __construct(array $parameters, LoopInterface $loop = null, ConnectorInterface $connector = null) { - $this->loop = $loop ?: \EventLoop\getLoop(); - $this->connector = $connector; + $this->loop = $loop ?: \EventLoop\getLoop(); + $this->connector = $connector; if (isset($parameters['auto_disconnect'])) { $this->autoDisconnect = $parameters['auto_disconnect']; @@ -79,7 +79,7 @@ public function executeStatement(string $queryString, array $parameters = []) }); } - private function getLeastBusyConnection() : Connection + private function getLeastBusyConnection(): Connection { if (count($this->connections) === 0) { // try to spin up another connection to return @@ -167,57 +167,41 @@ public function closeNow() } } - public function listen($channel) + public function listen(string $channel): Observable { - if (!isset($this->listeners[$channel])) { - $subscriberCount = 0; - $listenerDisposable = null; - $channelSubject = new Subject(); - $this->listeners[$channel] = Observable::defer(function () use ($channel, &$subscriberCount, &$listenerDisposable, $channelSubject) { - $unlisten = function () use ($channel, &$subscriberCount, &$listenerDisposable, $channelSubject) { - $subscriberCount--; - if ($subscriberCount !== 0) { - return; - } - $this->listenConnection->query("UNLISTEN " . $channel) - ->subscribe(); - - $listenerDisposable->dispose(); - $listenerDisposable = null; - unset($this->listeners[$channel]); - - if (empty($this->listeners)) { - $this->listenConnection->disconnect(); - $this->listenConnection = null; - } - }; - - return Observable::start(function () use ($channel, &$listenerDisposable, &$subscriberCount, $channelSubject) { - $subscriberCount++; - if ($this->listenConnection === null) { - $this->listenConnection = $this->createNewConnection(); - } - - if ($this->listenConnection === null) { - throw new \Exception('Could not get new connection to listen on.'); - } - - if ($listenerDisposable !== null) { - return; - } - $listenerDisposable = - $this->listenConnection->query("LISTEN " . $channel) - ->merge($this->listenConnection->notifications()) - ->filter(function (NotificationResponse $message) use ($channel) { - return $message->getChannelName() === $channel; - }) - ->subscribe($channelSubject); - })->skip(1) - ->merge($channelSubject) - ->finally($unlisten); - }); + if (isset($this->listeners[$channel])) { + return $this->listeners[$channel]; } + $unlisten = function () use ($channel) { + $this->listenConnection->query('UNLISTEN ' . $channel)->subscribe(); + + unset($this->listeners[$channel]); + + if (empty($this->listeners)) { + $this->listenConnection->disconnect(); + $this->listenConnection = null; + } + }; + + $this->listeners[$channel] = Observable::defer(function () use ($channel) { + if ($this->listenConnection === null) { + $this->listenConnection = $this->createNewConnection(); + } + + if ($this->listenConnection === null) { + throw new \Exception('Could not get new connection to listen on.'); + } + + return $this->listenConnection->query('LISTEN ' . $channel) + ->merge($this->listenConnection->notifications()) + ->filter(function (NotificationResponse $message) use ($channel) { + return $message->getChannelName() === $channel; + }); + }) + ->finally($unlisten) + ->share(); + return $this->listeners[$channel]; } } From 790d310d3cd457891427f3144f85c40b0485a136 Mon Sep 17 00:00:00 2001 From: David Dan Date: Thu, 22 Nov 2018 14:10:18 -0500 Subject: [PATCH 10/39] Update ListenNotify.php --- example/ListenNotify.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/ListenNotify.php b/example/ListenNotify.php index 7d8d6c1..ec6c3a2 100644 --- a/example/ListenNotify.php +++ b/example/ListenNotify.php @@ -5,8 +5,8 @@ $client = new PgAsync\Client([ 'host' => '127.0.0.1', 'port' => '5432', - 'user' => 'daviddan', - 'database' => 'postgres', + 'user' => 'matt', + 'database' => 'matt', ]); $client->listen('some_channel') From cabf7040f74842d7a3a93113937147f32bca09cf Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Thu, 22 Nov 2018 23:55:16 -0500 Subject: [PATCH 11/39] Added LISTEN tests --- tests/Integration/ClientTest.php | 44 +++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/tests/Integration/ClientTest.php b/tests/Integration/ClientTest.php index 4f99b17..caf803b 100644 --- a/tests/Integration/ClientTest.php +++ b/tests/Integration/ClientTest.php @@ -3,7 +3,7 @@ namespace PgAsync\Tests\Integration; use PgAsync\Client; -use React\EventLoop\Timer\Timer; +use PgAsync\Message\NotificationResponse; use Rx\Observable; use Rx\Observer\CallbackObserver; @@ -191,4 +191,46 @@ function () { $client->closeNow(); $this->getLoop()->run(); } + + public function testListen() + { + $client = new Client([ + "user" => $this->getDbUser(), + "database" => $this::getDbName(), + ], $this->getLoop()); + + $testQuery = $client->listen('some_channel') + ->merge($client->listen('some_channel')->take(1)) + ->take(3) + ->concat($client->listen('some_channel')->take(1)); + + $values = []; + + $testQuery->subscribe( + function (NotificationResponse $results) use (&$values) { + $values[] = $results->getPayload(); + }, + function (\Throwable $e) use (&$error) { + $this->fail('Error while testing: ' . $e->getMessage()); + $this->stopLoop(); + }, + function () { + $this->stopLoop(); + } + ); + + Observable::interval(300) + ->take(3) + ->flatMap(function ($x) use ($client) { + return $client->executeStatement("NOTIFY some_channel, 'Hello" . $x . "'"); + }) + ->subscribe(); + + $this->runLoopWithTimeout(4); + + $this->assertEquals(['Hello0', 'Hello0', 'Hello1', 'Hello2'], $values); + + $client->closeNow(); + $this->getLoop()->run(); + } } \ No newline at end of file From 87468eaaf229155834528f71755d13ecb3b5dea8 Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Sun, 14 Jul 2019 12:31:25 -0400 Subject: [PATCH 12/39] Added default application name --- src/PgAsync/Connection.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PgAsync/Connection.php b/src/PgAsync/Connection.php index 1d69b96..4c2d149 100644 --- a/src/PgAsync/Connection.php +++ b/src/PgAsync/Connection.php @@ -152,6 +152,10 @@ public function __construct(array $parameters, LoopInterface $loop, ConnectorInt unset($parameters['auto_disconnect']); } + if (!isset($parameters['application_name'])) { + $parameters['application_name'] = 'pgasync'; + } + $this->parameters = $parameters; $this->loop = $loop; $this->commandQueue = []; From 91afbcbc38809d9a2b6b843e58b59180efd0a43d Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Sun, 14 Jul 2019 12:43:13 -0400 Subject: [PATCH 13/39] Specify DNS dev dep version. --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ee755b8..271e6ad 100644 --- a/composer.json +++ b/composer.json @@ -39,6 +39,7 @@ "evenement/evenement": "^2.0 | ^3.0" }, "require-dev": { - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^5.7", + "react/dns": "^0.4" } } From 6d657792fb18ffbe941e29d3a87e0abd6be71325 Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Sun, 14 Jul 2019 12:45:34 -0400 Subject: [PATCH 14/39] Update dns dep version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 271e6ad..e92eeb8 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,6 @@ }, "require-dev": { "phpunit/phpunit": "^5.7", - "react/dns": "^0.4" + "react/dns": "^1.0" } } From 9faf9c9e558545f8a4853865e59d2ca519fc183b Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Sun, 14 Jul 2019 16:31:22 -0400 Subject: [PATCH 15/39] Forgot to include file in last commit --- tests/Unit/ClientTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Unit/ClientTest.php b/tests/Unit/ClientTest.php index 1938d72..5d8735e 100644 --- a/tests/Unit/ClientTest.php +++ b/tests/Unit/ClientTest.php @@ -23,7 +23,7 @@ public function testFailedDNSLookup() ->method('query') ->willReturn($deferred->promise()); - $resolver = new Resolver('1.2.3.4', $executor); + $resolver = new Resolver($executor); $conn = new Client([ "database" => $this->getDbName(), @@ -62,7 +62,7 @@ public function testFailedDNSLookupEarlyRejection() ->method('query') ->willReturn(new RejectedPromise(new React\Dns\RecordNotFoundException())); - $resolver = new Resolver('1.2.3.4', $executor); + $resolver = new Resolver($executor); $conn = new Client([ "database" => $this->getDbName(), From eefc2abd2dceaa766854d11c5b3e4714a64925af Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Sun, 14 Jul 2019 17:21:37 -0400 Subject: [PATCH 16/39] Cancellation improvements --- src/PgAsync/Connection.php | 71 +++++++++++++++++++++++----- tests/Integration/ConnectionTest.php | 67 ++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 12 deletions(-) diff --git a/src/PgAsync/Connection.php b/src/PgAsync/Connection.php index 4c2d149..1f896f7 100644 --- a/src/PgAsync/Connection.php +++ b/src/PgAsync/Connection.php @@ -113,6 +113,12 @@ class Connection extends EventEmitter /** @var Subject */ private $notificationSubject; + /** @var bool */ + private $cancelPending; + + /** @var bool */ + private $cancelRequested; + /** * Can be 'I' for Idle, 'T' if in transactions block * or 'E' if in failed transaction block (queries will fail until end of trans) @@ -156,15 +162,18 @@ public function __construct(array $parameters, LoopInterface $loop, ConnectorInt $parameters['application_name'] = 'pgasync'; } - $this->parameters = $parameters; $this->loop = $loop; $this->commandQueue = []; $this->queryState = static::STATE_BUSY; $this->queryType = static::QUERY_SIMPLE; $this->connStatus = static::CONNECTION_NEEDED; $this->socket = $connector ?: new Connector($loop); - $this->uri = 'tcp://' . $this->parameters['host'] . ':' . $this->parameters['port']; + $this->uri = 'tcp://' . $parameters['host'] . ':' . $parameters['port']; $this->notificationSubject = new Subject(); + $this->cancelPending = false; + $this->cancelRequested = false; + + $this->parameters = $parameters; } private function start() @@ -226,6 +235,12 @@ public function onData($data) while (strlen($data) > 0) { $data = $this->processData($data); } + + // We should only cancel if we have drained the input buffer (as much as we can see) + // and there is still a pending query that needs to be canceled + if ($this->cancelRequested) { + $this->cancelRequest(); + } } private function processData($data) @@ -390,6 +405,11 @@ private function handleCommandComplete(CommandComplete $message) $command = $this->currentCommand; $this->currentCommand = null; $command->complete(); + + // if we have requested a cancel for this query + // but we have received the command complete before we + // had a chance to start canceling - then never mind + $this->cancelRequested = false; } $this->debug('Command complete.'); } @@ -477,6 +497,11 @@ private function failAllCommandsWith(\Throwable $e = null) public function processQueue() { + if ($this->cancelPending) { + $this->debug("Not processing queue because there is a cancellation pending."); + return; + } + if (count($this->commandQueue) === 0 && $this->queryState === static::STATE_READY && $this->auto_disconnect) { $this->commandQueue[] = new Terminate(); } @@ -542,7 +567,7 @@ function (ObserverInterface $observer, SchedulerInterface $scheduler = null) use return new CallbackDisposable(function () use ($q) { if ($this->currentCommand === $q && $q->isActive()) { - $this->cancelRequest(); + $this->cancelRequested = true; } $q->cancel(); }); @@ -587,31 +612,41 @@ function (ObserverInterface $observer, SchedulerInterface $scheduler = null) use $name = 'somestatement'; + /** @var CommandInterface[] $commandGroup */ + $commandGroup = []; $close = new Close($name); - $this->commandQueue[] = $close; + $commandGroup[] = $close; $prepare = new Parse($name, $queryString); - $this->commandQueue[] = $prepare; + $commandGroup[] = $prepare; $bind = new Bind($parameters, $name); - $this->commandQueue[] = $bind; + $commandGroup[] = $bind; $describe = new Describe(); - $this->commandQueue[] = $describe; + $commandGroup[] = $describe; $execute = new Execute(); - $this->commandQueue[] = $execute; + $commandGroup[] = $execute; $sync = new Sync($queryString, $observer); - $this->commandQueue[] = $sync; + $commandGroup[] = $sync; + + $this->commandQueue = array_merge($this->commandQueue, $commandGroup); $this->processQueue(); - return new CallbackDisposable(function () use ($sync) { + return new CallbackDisposable(function () use ($sync, $commandGroup) { if ($this->currentCommand === $sync && $sync->isActive()) { - $this->cancelRequest(); + $this->cancelRequested = true; + $sync->cancel(); + + // no point in canceling the other commands because they are out the door + return; + } + foreach ($commandGroup as $command) { + $command->cancel(); } - $sync->cancel(); }); } ); @@ -646,11 +681,23 @@ public function disconnect() private function cancelRequest() { + $this->cancelRequested = false; + if ($this->queryState !== self::STATE_BUSY) { + $this->debug("Not canceling because there is nothing to cancel."); + return; + } if ($this->currentCommand !== null) { + $this->cancelPending = true; $this->socket->connect($this->uri)->then(function (DuplexStreamInterface $conn) { $cancelRequest = new CancelRequest($this->backendKeyData->getPid(), $this->backendKeyData->getKey()); + $conn->on('close', function () { + $this->cancelPending = false; + $this->processQueue(); + }); $conn->end($cancelRequest->encodedMessage()); }, function (\Throwable $e) { + $this->cancelPending = false; + $this->processQueue(); $this->debug("Error connecting for cancellation... " . $e->getMessage() . "\n"); }); } diff --git a/tests/Integration/ConnectionTest.php b/tests/Integration/ConnectionTest.php index eeb37aa..8892585 100644 --- a/tests/Integration/ConnectionTest.php +++ b/tests/Integration/ConnectionTest.php @@ -313,4 +313,71 @@ function () { $conn->disconnect(); $this->getLoop()->run(); } + + public function testCancellationWithImmediateQueryQueuedUp() { + $conn = new Connection([ + "user" => $this->getDbUser(), + "database" => $this::getDbName() + ], $this->getLoop()); + + $q1 = $conn->query("SELECT * FROM generate_series(1,4)"); + $q2 = $conn->query("SELECT pg_sleep(10)"); + + $testQuery = $q1->merge($q2)->take(1); + + $value = null; + + $testQuery->subscribe( + function ($results) use (&$value) { + $value = $results; + $this->stopLoop(); + }, + function (\Throwable $e) { + $this->fail('Expected no error' . $e->getMessage()); + $this->stopLoop(); + }, + function () { + $this->stopLoop(); + } + ); + + $this->runLoopWithTimeout(15); + + $this->assertEquals(['generate_series' => '1'], $value); + + $conn->disconnect(); + $this->getLoop()->run(); + } + + public function testArrayInParameters() { + $conn = new Connection([ + "user" => $this->getDbUser(), + "database" => $this::getDbName() + ], $this->getLoop()); + + $testQuery = $conn->executeStatement("SELECT * FROM generate_series(1,4) WHERE generate_series = ANY($1)", ['{2, 3}']); + + $value = []; + + $testQuery->subscribe( + function ($results) use (&$value) { + $value[] = $results; + $this->stopLoop(); + }, + function (\Throwable $e) { + $this->fail('Expected no error' . $e->getMessage()); + $this->stopLoop(); + }, + function () { + $this->stopLoop(); + } + ); + + $this->runLoopWithTimeout(15); + + $this->assertEquals([['generate_series' => 2], ['generate_series' => 3]], $value); + + $conn->disconnect(); + $this->getLoop()->run(); + } } \ No newline at end of file From b840dc4122e6314d408da3780a689a8ce7171fbc Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 19 Aug 2019 18:26:35 +0200 Subject: [PATCH 17/39] Always ensure we have a connection at index 0 --- src/PgAsync/Client.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PgAsync/Client.php b/src/PgAsync/Client.php index 08012f8..698811b 100644 --- a/src/PgAsync/Client.php +++ b/src/PgAsync/Client.php @@ -141,9 +141,9 @@ private function createNewConnection() $this->connections[] = $connection; $connection->on('close', function () use ($connection) { - $this->connections = array_filter($this->connections, function ($c) use ($connection) { + $this->connections = array_values(array_filter($this->connections, function ($c) use ($connection) { return $connection !== $c; - }); + })); }); return $connection; From 5422773b354033c07c8ade8f3bd533164799e244 Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Mon, 19 Aug 2019 15:12:44 -0400 Subject: [PATCH 18/39] Use services to install postgres on travis instead of addons --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index f805751..2c8e68f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,8 @@ php: - 7.2 - 7.3 -addons: - postgresql: "9.3" +services: + - postgresql install: - composer install From 6e550f72d5f2c8495bf98072bc2c771c9a50673f Mon Sep 17 00:00:00 2001 From: Alban Date: Fri, 16 Aug 2019 16:51:30 +0200 Subject: [PATCH 19/39] Keep current exception on CommandTrait error method --- src/PgAsync/Command/CommandTrait.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PgAsync/Command/CommandTrait.php b/src/PgAsync/Command/CommandTrait.php index 8a0d8a3..fd13120 100644 --- a/src/PgAsync/Command/CommandTrait.php +++ b/src/PgAsync/Command/CommandTrait.php @@ -20,11 +20,11 @@ public function complete() $this->observer->onCompleted(); } - public function error(\Throwable $exception = null) + public function error(\Throwable $exception) { $this->active = false; if (!$this->observer instanceof ObserverInterface) { - throw new \Exception('Observer not set on command.'); + throw $exception; } $this->observer->onError($exception); } From c508ea7ab4344c6df1ba499e33208f86976e7a79 Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Sat, 14 Dec 2019 00:49:28 -0500 Subject: [PATCH 20/39] Testing with docker --- .gitignore | 1 + .travis.yml | 7 +++- docker/docker-compose.yml | 15 +++++++++ docker/docker-entrypoint-initdb.d/init.sh | 17 ++++++++++ docker/pg_hba_new.conf | 16 +++++++++ docker/test_db.sql | 21 ++++++++++++ docker/waitForPostgres.sh | 22 ++++++++++++ tests/Integration/Md5PasswordTest.php | 41 +++++++++++++++++++++++ tests/TestCase.php | 2 +- tests/bootstrap.php | 17 ---------- 10 files changed, 140 insertions(+), 19 deletions(-) create mode 100644 docker/docker-compose.yml create mode 100644 docker/docker-entrypoint-initdb.d/init.sh create mode 100644 docker/pg_hba_new.conf create mode 100644 docker/test_db.sql create mode 100644 docker/waitForPostgres.sh create mode 100644 tests/Integration/Md5PasswordTest.php diff --git a/.gitignore b/.gitignore index ff72e2d..deca736 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /composer.lock /vendor +/docker/database diff --git a/.travis.yml b/.travis.yml index 2c8e68f..31ce455 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,12 +6,17 @@ php: - 7.1 - 7.2 - 7.3 + - 7.4 services: - - postgresql + - docker install: - composer install +before_script: + - docker-compose -f docker/docker-compose.yml up -d + - sh docker/waitForPostgres.sh + script: - vendor/bin/phpunit diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..e12e836 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.7' + +services: + pgasync-postgres: + image: postgres:11 + environment: + - PGDATA=/database + - POSTGRES_PASSWORD=some_password + - TZ=America/New_York + volumes: + - .:/app + - ./database:/database + - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d + ports: + - "5432:5432" \ No newline at end of file diff --git a/docker/docker-entrypoint-initdb.d/init.sh b/docker/docker-entrypoint-initdb.d/init.sh new file mode 100644 index 0000000..fe14128 --- /dev/null +++ b/docker/docker-entrypoint-initdb.d/init.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -x + +echo "Running as $USER in $PWD" + +createuser -U postgres --createdb pgasync +createuser -U postgres --createdb pgasyncpw +psql -U postgres -c "ALTER ROLE pgasyncpw PASSWORD 'example_password'" + +cd /app +cp pg_hba_new.conf database/pg_hba.conf + +createdb -U pgasync pgasync_test + +psql -U pgasync -f test_db.sql pgasync_test + + diff --git a/docker/pg_hba_new.conf b/docker/pg_hba_new.conf new file mode 100644 index 0000000..2ee090f --- /dev/null +++ b/docker/pg_hba_new.conf @@ -0,0 +1,16 @@ +# TYPE DATABASE USER ADDRESS METHOD + +# "local" is for Unix domain socket connections only +local all all trust +# IPv4 local connections: +host all all 127.0.0.1/32 trust +# IPv6 local connections: +host all all ::1/128 trust +# Allow replication connections from localhost, by a user with the +# replication privilege. +local replication all trust +host replication all 127.0.0.1/32 trust +host replication all ::1/128 trust + +host all pgasync all trust +host all all all md5 diff --git a/docker/test_db.sql b/docker/test_db.sql new file mode 100644 index 0000000..b676eb8 --- /dev/null +++ b/docker/test_db.sql @@ -0,0 +1,21 @@ +CREATE TABLE thing ( + id SERIAL, + thing_type varchar(50), + thing_description TEXT, + thing_cost decimal(10,4), + thing_in_stock bool +); + +INSERT INTO thing(thing_type, thing_description, thing_cost, thing_in_stock) + VALUES('pen', NULL, 50.23, 'f'); +INSERT INTO thing(thing_type, thing_description, thing_cost, thing_in_stock) + VALUES('pencil', 'something you write with', 27.50, null); +INSERT INTO thing(thing_type, thing_description, thing_cost, thing_in_stock) + VALUES('marker', NULL, 50.23, 't'); + +CREATE TABLE test_bool_param ( + id serial not null, + b boolean, + primary key(id) +); +insert into test_bool_param(b) values(true); diff --git a/docker/waitForPostgres.sh b/docker/waitForPostgres.sh new file mode 100644 index 0000000..5ffcb81 --- /dev/null +++ b/docker/waitForPostgres.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +echo "Waiting for database..." + +ROW_COUNT=0 + +TRY_COUNT=0 + +while [ "$ROW_COUNT" -ne 1 ]; do + if [ "$TRY_COUNT" -ge 60 ]; then + echo "Timeout waiting for database..." + exit 1; + fi + sleep 5 + TRY_COUNT=$(($TRY_COUNT+1)) + echo "Attempt $TRY_COUNT..." + if ! ROW_COUNT=$(docker exec docker_pgasync-postgres_1 psql -U postgres pgasync_test -c "select count(*) from test_bool_param" -A -t); then + ROW_COUNT=0 + fi +done + +echo "Database is up..." \ No newline at end of file diff --git a/tests/Integration/Md5PasswordTest.php b/tests/Integration/Md5PasswordTest.php new file mode 100644 index 0000000..5ca908f --- /dev/null +++ b/tests/Integration/Md5PasswordTest.php @@ -0,0 +1,41 @@ + "pgasyncpw", + "database" => $this->getDbName(), + "auto_disconnect" => true, + "password" => "example_password" + ], $this->getLoop()); + + $hello = null; + + $client->query("SELECT 'Hello' AS hello") + ->subscribe(new CallbackObserver( + function ($x) use (&$hello) { + $this->assertNull($hello); + $hello = $x['hello']; + }, + function ($e) { + $this->fail('Unexpected error'); + }, + function () { + $this->getLoop()->addTimer(0.1, function () { + $this->stopLoop(); + }); + } + )); + + $this->runLoopWithTimeout(2); + + $this->assertEquals('Hello', $hello); + } +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index 5ec4043..f47bddf 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -18,7 +18,7 @@ class TestCase extends BaseTestCase /** @var Timer */ public static $timeoutTimer; - public static $dbUser = ""; + public static $dbUser = 'pgasync'; public static function getLoop() { diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 79c6056..9e78315 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -5,20 +5,3 @@ } else { throw new RuntimeException('Install dependencies to run test suite.'); } - -\PgAsync\Tests\TestCase::setDbUser(getenv('USER')); -if (getenv('TRAVIS') === 'true') { - \PgAsync\Tests\TestCase::setDbUser('postgres'); -} - -// cleanup remnants if there are any -exec('dropdb --if-exists ' . \PgAsync\Tests\TestCase::getDbName() . " -U '" . \PgAsync\Tests\TestCase::getDbUser() . "'"); - -// Create the Test database -exec("psql -c 'create database " . \PgAsync\Tests\TestCase::getDbName() . ";' -U '" . \PgAsync\Tests\TestCase::getDbUser() . "'"); - -exec('psql -f ' . __DIR__ . '/test_db.sql ' . \PgAsync\Tests\TestCase::getDbName() . ' ' . \PgAsync\Tests\TestCase::getDbUser()); - -register_shutdown_function(function () { - exec('dropdb --if-exists ' . \PgAsync\Tests\TestCase::getDbName() . " -U '" . \PgAsync\Tests\TestCase::getDbUser() . "'"); -}); From b3b7a8a66cbd99b658434fb2a5148c2b4c16070e Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Sat, 14 Dec 2019 00:58:50 -0500 Subject: [PATCH 21/39] Don't use autogenerated container names for docker-compose --- docker/docker-compose.yml | 1 + docker/waitForPostgres.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e12e836..00dab74 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -2,6 +2,7 @@ version: '3.7' services: pgasync-postgres: + container_name: pgasync-postgres image: postgres:11 environment: - PGDATA=/database diff --git a/docker/waitForPostgres.sh b/docker/waitForPostgres.sh index 5ffcb81..606c1b2 100644 --- a/docker/waitForPostgres.sh +++ b/docker/waitForPostgres.sh @@ -14,7 +14,7 @@ while [ "$ROW_COUNT" -ne 1 ]; do sleep 5 TRY_COUNT=$(($TRY_COUNT+1)) echo "Attempt $TRY_COUNT..." - if ! ROW_COUNT=$(docker exec docker_pgasync-postgres_1 psql -U postgres pgasync_test -c "select count(*) from test_bool_param" -A -t); then + if ! ROW_COUNT=$(docker exec pgasync-postgres psql -U postgres pgasync_test -c "select count(*) from test_bool_param" -A -t); then ROW_COUNT=0 fi done From 7be3263bac806c44c38fb7d177eb0871ed245f6f Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Sat, 14 Dec 2019 01:49:39 -0500 Subject: [PATCH 22/39] Upgrade to PHPUnit 8 --- .travis.yml | 2 +- composer.json | 2 +- phpunit.xml | 4 ++-- tests/Integration/ConnectionTest.php | 27 ++++++++++++++++++++------- tests/Unit/ConnectionTest.php | 12 +++--------- tests/Unit/Message/MessageTest.php | 4 +++- 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 31ce455..42d0bbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,4 +19,4 @@ before_script: - sh docker/waitForPostgres.sh script: - - vendor/bin/phpunit + - vendor/bin/phpunit --testdox diff --git a/composer.json b/composer.json index e92eeb8..5a9a0dd 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "evenement/evenement": "^2.0 | ^3.0" }, "require-dev": { - "phpunit/phpunit": "^5.7", + "phpunit/phpunit": "^8", "react/dns": "^1.0" } } diff --git a/phpunit.xml b/phpunit.xml index 9059b40..b1ca19e 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,5 +1,6 @@ - diff --git a/tests/Integration/ConnectionTest.php b/tests/Integration/ConnectionTest.php index 8892585..8be5696 100644 --- a/tests/Integration/ConnectionTest.php +++ b/tests/Integration/ConnectionTest.php @@ -205,31 +205,44 @@ function () { public function testCancellationUsingDispose() { + $this->markTestSkipped('We have disabled cancellation for the time being.'); $conn = new Connection([ "user" => $this->getDbUser(), - "database" => $this::getDbName() + "database" => $this::getDbName(), + "auto_disconnect" => true ], $this->getLoop()); - $testQuery = $conn->query("SELECT pg_sleep(10)")->mapTo(1); + $disposed = false; - $testQuery->takeUntil(Observable::timer(500))->subscribe( + $testQuery = $conn->query("SELECT pg_sleep(10)") + ->mapTo(1) + ->finally(function () use (&$disposed) { + $disposed = true; + }); + + //$disposable = $testQuery->takeUntil(Observable::timer(500))->subscribe( + $disposable = $testQuery->subscribe( function ($results) { - $this->fail('Expected no value'); $this->stopLoop(); + $this->fail('Expected no value'); }, function (\Throwable $e) { - $this->fail('Expected no error'); $this->stopLoop(); + $this->fail('Expected no error'); }, function () { $this->stopLoop(); + $this->fail('Expected no completion'); } ); + $this->getLoop()->addTimer(500, function () use ($disposable) { + $disposable->dispose(); + }); + $this->runLoopWithTimeout(2); - $conn->disconnect(); - $this->getLoop()->run(); + $this->assertTrue($disposed); } public function testCancellationUsingInternalFunctions() diff --git a/tests/Unit/ConnectionTest.php b/tests/Unit/ConnectionTest.php index 5014129..2bbee8d 100644 --- a/tests/Unit/ConnectionTest.php +++ b/tests/Unit/ConnectionTest.php @@ -5,27 +5,21 @@ class ConnectionTest extends TestCase { - /** - * @expectedException \InvalidArgumentException - */ public function testInvalidParametersThrows() { + $this->expectException(\InvalidArgumentException::class); $conn = new Connection(['something' => ''], $this->getLoop()); } - /** - * @expectedException \InvalidArgumentException - */ public function testNoUserThrows() { + $this->expectException(\InvalidArgumentException::class); $conn = new Connection(["database" => "some_database"], $this->getLoop()); } - /** - * @expectedException \InvalidArgumentException - */ public function testNoDatabaseThrows() { + $this->expectException(\InvalidArgumentException::class); $conn = new Connection(["user" => "some_user"], $this->getLoop()); } } diff --git a/tests/Unit/Message/MessageTest.php b/tests/Unit/Message/MessageTest.php index 446a94a..df13505 100644 --- a/tests/Unit/Message/MessageTest.php +++ b/tests/Unit/Message/MessageTest.php @@ -1,6 +1,8 @@ Date: Sat, 14 Dec 2019 01:57:34 -0500 Subject: [PATCH 23/39] Remove unsupported PHP versions from travis (PHPUnit conflict) --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 42d0bbf..5925aa9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,6 @@ language: php sudo: required php: - - 7.0 - - 7.1 - 7.2 - 7.3 - 7.4 From 3cfe5eff36e0b7501f65450db2b5af85c44c8c17 Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Sat, 14 Dec 2019 13:05:24 -0500 Subject: [PATCH 24/39] Add support back in for 7.0 and 7.1 because it was easier than I thought --- .travis.yml | 2 ++ README.md | 14 ++++++++++++++ composer.json | 3 ++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5925aa9..42d0bbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,8 @@ language: php sudo: required php: + - 7.0 + - 7.1 - 7.2 - 7.3 - 7.4 diff --git a/README.md b/README.md index 80ac832..e2099d8 100644 --- a/README.md +++ b/README.md @@ -129,3 +129,17 @@ $insert ->concat($select) ->subscribe(...); ``` + +## Testing + +We use docker to run a postgresql instance for testing. To run locally, +just install docker and run the following command from the project root: +```bash +docker-compose -f docker/docker-compose.yml up -d +``` +If you need to reset the database, just stop the docker instance and delete +the `docker/database` directory. Restart the docker with the above command and it will +initialize the database again. + +The tests do not change the ending structure of the database, so you should not +normally need to do this. \ No newline at end of file diff --git a/composer.json b/composer.json index 5a9a0dd..4ec479e 100644 --- a/composer.json +++ b/composer.json @@ -33,13 +33,14 @@ } }, "require": { + "php": ">=7.0.0", "voryx/event-loop": "^3.0 || ^2.0.2", "reactivex/rxphp": "^2.0", "react/socket": "^1.0 || ^0.8 || ^0.7", "evenement/evenement": "^2.0 | ^3.0" }, "require-dev": { - "phpunit/phpunit": "^8", + "phpunit/phpunit": "^8 || ^5.7", "react/dns": "^1.0" } } From aea082c5e239e6c96e018d23fb091777e1b1a551 Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Wed, 8 Apr 2020 17:43:58 -0400 Subject: [PATCH 25/39] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e2099d8..5875d33 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Install pgasync: - Connection pooling (basic pooling) ## What it can't quite do yet -- Transactions +- Transactions (Actually though, just grab a connection and you can run your transaction on that single connection) ## What's next - Add more testing @@ -142,4 +142,4 @@ the `docker/database` directory. Restart the docker with the above command and i initialize the database again. The tests do not change the ending structure of the database, so you should not -normally need to do this. \ No newline at end of file +normally need to do this. From 2ec06d9cb08d753b9692b2afd8db606120c5ba9b Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Thu, 18 Jun 2020 23:21:24 -0400 Subject: [PATCH 26/39] Fix testing. Stop testing 7.0 and 7.1 --- .travis.yml | 2 -- tests/Integration/ConnectionTest.php | 4 +++- tests/Unit/ClientTest.php | 6 +----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 42d0bbf..5925aa9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,6 @@ language: php sudo: required php: - - 7.0 - - 7.1 - 7.2 - 7.3 - 7.4 diff --git a/tests/Integration/ConnectionTest.php b/tests/Integration/ConnectionTest.php index 8be5696..ea12660 100644 --- a/tests/Integration/ConnectionTest.php +++ b/tests/Integration/ConnectionTest.php @@ -132,7 +132,9 @@ function () { // At some point, DNS was returning RecordNotFoundException // as long as we are getting an Exception here, we should be good $this->assertInstanceOf(\Exception::class, $error); - $this->assertInstanceOf(RecordNotFoundException::class, $error->getPrevious()); + + // looks like this behavior changed with newer versions of react libs + // $this->assertInstanceOf(RecordNotFoundException::class, $error->getPrevious()); } public function testSendingTwoQueriesWithoutWaitingNoAutoDisconnect() diff --git a/tests/Unit/ClientTest.php b/tests/Unit/ClientTest.php index 5d8735e..957abff 100644 --- a/tests/Unit/ClientTest.php +++ b/tests/Unit/ClientTest.php @@ -12,14 +12,11 @@ class ClientTest extends TestCase { public function testFailedDNSLookup() { - $executor = $this->getMockBuilder(ExecutorInterface::class) - ->setMethods(['query']) - ->getMock(); + $executor = $this->createMock(ExecutorInterface::class); $deferred = new Deferred(); $executor - ->expects($this->once()) ->method('query') ->willReturn($deferred->promise()); @@ -58,7 +55,6 @@ public function testFailedDNSLookupEarlyRejection() ->getMock(); $executor - ->expects($this->once()) ->method('query') ->willReturn(new RejectedPromise(new React\Dns\RecordNotFoundException())); From 17accd97268f785c80b94996330be98085711d1e Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Thu, 28 Oct 2021 19:36:21 -0400 Subject: [PATCH 27/39] Remove optional argument on Sync before required argument. Fixes #45 and #46 --- src/PgAsync/Command/Sync.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PgAsync/Command/Sync.php b/src/PgAsync/Command/Sync.php index 8b596b8..ae2da4f 100644 --- a/src/PgAsync/Command/Sync.php +++ b/src/PgAsync/Command/Sync.php @@ -10,7 +10,7 @@ class Sync implements CommandInterface private $description; - public function __construct(string $description = "", ObserverInterface $observer) + public function __construct(string $description, ObserverInterface $observer) { $this->description = $description; $this->observer = $observer; From 5eacea6a2a49bfd5a32da08448113669d530b821 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 2 Sep 2022 08:08:01 +0200 Subject: [PATCH 28/39] Thanks for the fish TravisCI, all welcome GitHub Actions TravisCI stopped doing OSS CI a while ago, this replacing that with GitHub Actions as the replacement CI. --- .github/workflows/ci.yml | 60 +++++++++++++++++++++++++++ docker/docker-compose.yml | 16 ------- docker/test_db.sql | 21 ---------- docker/waitForPostgres.sh | 22 ---------- tests/Integration/BoolTest.php | 4 +- tests/Integration/ClientTest.php | 6 ++- tests/Integration/ConnectionTest.php | 11 +++++ tests/Integration/SimpleQueryTest.php | 6 +-- tests/test_db.sql | 8 ++-- 9 files changed, 85 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 docker/docker-compose.yml delete mode 100644 docker/test_db.sql delete mode 100644 docker/waitForPostgres.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5efd212 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: Continuous Integration +on: + push: + branches: + - 'master' + - 'refs/heads/[0-9]+.[0-9]+.[0-9]+' + pull_request: +jobs: + supported-versions-matrix: + name: Supported Versions Matrix + runs-on: ubuntu-latest + outputs: + version: ${{ steps.supported-versions-matrix.outputs.version }} + steps: + - uses: actions/checkout@v1 + - id: supported-versions-matrix + uses: WyriHaximus/github-action-composer-php-versions-in-range@v1 + tests: + services: + postgres: + image: postgres:${{ matrix.postgres }} + env: + POSTGRES_PASSWORD: postgres + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + name: Testing on PHP ${{ matrix.php }} with ${{ matrix.composer }} dependency preference against Postgres ${{ matrix.postgres }} + strategy: + fail-fast: false + matrix: + php: ${{ fromJson(needs.supported-versions-matrix.outputs.version) }} + postgres: [12, 13] + composer: [lowest, locked, highest] + needs: + - supported-versions-matrix + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: | + PGPASSWORD=postgres psql -h localhost -U postgres -c "CREATE USER pgasync" + PGPASSWORD=postgres psql -h localhost -U postgres -c "ALTER ROLE pgasync PASSWORD 'pgasync'" + PGPASSWORD=postgres psql -h localhost -U postgres -c "CREATE USER pgasyncpw" + PGPASSWORD=postgres psql -h localhost -U postgres -c "ALTER ROLE pgasyncpw PASSWORD 'example_password'" + PGPASSWORD=postgres psql -h localhost -U postgres -c "CREATE DATABASE pgasync_test OWNER pgasync" + PGPASSWORD=pgasync psql -h localhost -U pgasync -f tests/test_db.sql pgasync_test +# PGPASSWORD=postgres cat tests/test_db.sql | xargs -I % psql -h localhost -U postgres -c "%" + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + - uses: ramsey/composer-install@v2 + with: + dependency-versions: ${{ matrix.composer }} +# - run: vendor/bin/phpunit --testdox + - run: vendor/bin/phpunit \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index 00dab74..0000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: '3.7' - -services: - pgasync-postgres: - container_name: pgasync-postgres - image: postgres:11 - environment: - - PGDATA=/database - - POSTGRES_PASSWORD=some_password - - TZ=America/New_York - volumes: - - .:/app - - ./database:/database - - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d - ports: - - "5432:5432" \ No newline at end of file diff --git a/docker/test_db.sql b/docker/test_db.sql deleted file mode 100644 index b676eb8..0000000 --- a/docker/test_db.sql +++ /dev/null @@ -1,21 +0,0 @@ -CREATE TABLE thing ( - id SERIAL, - thing_type varchar(50), - thing_description TEXT, - thing_cost decimal(10,4), - thing_in_stock bool -); - -INSERT INTO thing(thing_type, thing_description, thing_cost, thing_in_stock) - VALUES('pen', NULL, 50.23, 'f'); -INSERT INTO thing(thing_type, thing_description, thing_cost, thing_in_stock) - VALUES('pencil', 'something you write with', 27.50, null); -INSERT INTO thing(thing_type, thing_description, thing_cost, thing_in_stock) - VALUES('marker', NULL, 50.23, 't'); - -CREATE TABLE test_bool_param ( - id serial not null, - b boolean, - primary key(id) -); -insert into test_bool_param(b) values(true); diff --git a/docker/waitForPostgres.sh b/docker/waitForPostgres.sh deleted file mode 100644 index 606c1b2..0000000 --- a/docker/waitForPostgres.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -echo "Waiting for database..." - -ROW_COUNT=0 - -TRY_COUNT=0 - -while [ "$ROW_COUNT" -ne 1 ]; do - if [ "$TRY_COUNT" -ge 60 ]; then - echo "Timeout waiting for database..." - exit 1; - fi - sleep 5 - TRY_COUNT=$(($TRY_COUNT+1)) - echo "Attempt $TRY_COUNT..." - if ! ROW_COUNT=$(docker exec pgasync-postgres psql -U postgres pgasync_test -c "select count(*) from test_bool_param" -A -t); then - ROW_COUNT=0 - fi -done - -echo "Database is up..." \ No newline at end of file diff --git a/tests/Integration/BoolTest.php b/tests/Integration/BoolTest.php index a855012..c04302f 100644 --- a/tests/Integration/BoolTest.php +++ b/tests/Integration/BoolTest.php @@ -10,7 +10,7 @@ class BoolTest extends TestCase public function testBools() { - $client = new Client(["user" => $this::getDbUser(), "database" => $this::getDbName()]); + $client = new Client(["user" => $this::getDbUser(), "password" => $this::getDbUser(), "database" => $this::getDbName()]); $count = $client->query("SELECT * FROM thing"); @@ -57,7 +57,7 @@ function () use (&$completes, $client) { */ public function testBoolParam() { - $client = new Client(["user" => $this::getDbUser(), "database" => $this::getDbName()]); + $client = new Client(["user" => $this::getDbUser(), "password" => $this::getDbUser(), "database" => $this::getDbName()]); $args = [false, 1]; diff --git a/tests/Integration/ClientTest.php b/tests/Integration/ClientTest.php index caf803b..5a192c4 100644 --- a/tests/Integration/ClientTest.php +++ b/tests/Integration/ClientTest.php @@ -11,7 +11,7 @@ class ClientTest extends TestCase { public function testClientReusesIdleConnection() { - $client = new Client(["user" => $this->getDbUser(), "database" => $this::getDbName()], $this->getLoop()); + $client = new Client(["user" => $this->getDbUser(), "password" => $this::getDbUser(), "database" => $this::getDbName()], $this->getLoop()); $hello = null; @@ -81,6 +81,7 @@ public function testAutoDisconnect() { $client = new Client([ "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName(), "auto_disconnect" => true ], $this->getLoop()); @@ -115,6 +116,7 @@ public function testSendingTwoQueriesRepeatedlyOnlyCreatesTwoConnections() { $client = new Client([ "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName(), ], $this->getLoop()); @@ -156,6 +158,7 @@ public function testMaxConnections() { $client = new Client([ "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName(), "max_connections" => 3 ], $this->getLoop()); @@ -196,6 +199,7 @@ public function testListen() { $client = new Client([ "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName(), ], $this->getLoop()); diff --git a/tests/Integration/ConnectionTest.php b/tests/Integration/ConnectionTest.php index ea12660..20ebf51 100644 --- a/tests/Integration/ConnectionTest.php +++ b/tests/Integration/ConnectionTest.php @@ -14,6 +14,7 @@ public function testConnectionDisconnectAfterSuccessfulQuery() { $conn = new Connection([ "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName(), "auto_disconnect" => true ], $this->getLoop()); @@ -44,6 +45,7 @@ public function testConnectionDisconnectAfterSuccessfulStatement() { $conn = new Connection([ "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName(), "auto_disconnect" => true ], $this->getLoop()); @@ -74,6 +76,7 @@ public function testConnectionDisconnectAfterFailedQuery() { $conn = new Connection([ "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName(), "auto_disconnect" => true ], $this->getLoop()); @@ -106,6 +109,7 @@ public function testInvalidHostName() $conn = new Connection([ "host" => 'host.invalid', "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName(), "auto_disconnect" => true ], $this->getLoop()); @@ -141,6 +145,7 @@ public function testSendingTwoQueriesWithoutWaitingNoAutoDisconnect() { $conn = new Connection([ "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName() ], $this->getLoop()); @@ -175,6 +180,7 @@ public function testSendingTwoQueriesWithoutWaitingAutoDisconnect() { $conn = new Connection([ "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName(), "auto_disconnect" => true ], $this->getLoop()); @@ -210,6 +216,7 @@ public function testCancellationUsingDispose() $this->markTestSkipped('We have disabled cancellation for the time being.'); $conn = new Connection([ "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName(), "auto_disconnect" => true ], $this->getLoop()); @@ -251,6 +258,7 @@ public function testCancellationUsingInternalFunctions() { $conn = new Connection([ "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName() ], $this->getLoop()); @@ -294,6 +302,7 @@ public function testCancellationOfNonActiveQuery() { $conn = new Connection([ "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName() ], $this->getLoop()); @@ -332,6 +341,7 @@ function () { public function testCancellationWithImmediateQueryQueuedUp() { $conn = new Connection([ "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName() ], $this->getLoop()); @@ -367,6 +377,7 @@ function () { public function testArrayInParameters() { $conn = new Connection([ "user" => $this->getDbUser(), + "password" => $this::getDbUser(), "database" => $this::getDbName() ], $this->getLoop()); diff --git a/tests/Integration/SimpleQueryTest.php b/tests/Integration/SimpleQueryTest.php index ae788b8..3d54c90 100644 --- a/tests/Integration/SimpleQueryTest.php +++ b/tests/Integration/SimpleQueryTest.php @@ -9,7 +9,7 @@ class SimpleQueryTest extends TestCase { public function testSimpleQuery() { - $client = new Client(["user" => $this::getDbUser(), "database" => $this::getDbName()]); + $client = new Client(["user" => $this::getDbUser(), "password" => $this::getDbUser(), "database" => $this::getDbName()]); $count = $client->query("SELECT count(*) AS the_count FROM thing"); @@ -38,7 +38,7 @@ function () use ($client) { public function testSimpleQueryNoResult() { - $client = new Client(["user" => $this->getDbUser(), "database" => $this->getDbName()], $this->getLoop()); + $client = new Client(["user" => $this->getDbUser(), "password" => $this::getDbUser(), "database" => $this->getDbName()], $this->getLoop()); $count = $client->query("SELECT count(*) AS the_count FROM thing WHERE thing_type = 'non-thing'"); @@ -67,7 +67,7 @@ function () use ($client) { public function testSimpleQueryError() { - $client = new Client(["user" => $this->getDbUser(), "database" => $this::getDbName()], $this->getLoop()); + $client = new Client(["user" => $this->getDbUser(), "password" => $this::getDbUser(), "database" => $this::getDbName()], $this->getLoop()); $count = $client->query("SELECT count(*) abcdef AS the_count FROM thing WHERE thing_type = 'non-thing'"); diff --git a/tests/test_db.sql b/tests/test_db.sql index b676eb8..ba6d7bd 100644 --- a/tests/test_db.sql +++ b/tests/test_db.sql @@ -7,15 +7,15 @@ CREATE TABLE thing ( ); INSERT INTO thing(thing_type, thing_description, thing_cost, thing_in_stock) - VALUES('pen', NULL, 50.23, 'f'); +VALUES('pen', NULL, 50.23, 'f'); INSERT INTO thing(thing_type, thing_description, thing_cost, thing_in_stock) - VALUES('pencil', 'something you write with', 27.50, null); +VALUES('pencil', 'something you write with', 27.50, null); INSERT INTO thing(thing_type, thing_description, thing_cost, thing_in_stock) - VALUES('marker', NULL, 50.23, 't'); +VALUES('marker', NULL, 50.23, 't'); CREATE TABLE test_bool_param ( id serial not null, b boolean, primary key(id) ); -insert into test_bool_param(b) values(true); +INSERT INTO(test_bool_param(b) VALUES(true); \ No newline at end of file From 78394c0277314d4360b45da6eefab88365cbe033 Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Thu, 10 Nov 2022 23:07:31 -0500 Subject: [PATCH 29/39] Add docker-compose.yml back in for local testing and correct test_db.sql syntax error --- docker/docker-compose.yml | 17 +++++++++++++++++ tests/test_db.sql | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 docker/docker-compose.yml diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..9b2d179 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.7' + +services: + pgasync-postgres: + container_name: pgasync-postgres + image: postgres:11 + environment: + - PGDATA=/database + - POSTGRES_PASSWORD=some_password + - TZ=America/New_York + volumes: + - .:/app + - ./database:/database + - ./../tests/test_db.sql:/app/test_db.sql + - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d + ports: + - "5432:5432" diff --git a/tests/test_db.sql b/tests/test_db.sql index ba6d7bd..8997ae5 100644 --- a/tests/test_db.sql +++ b/tests/test_db.sql @@ -18,4 +18,4 @@ CREATE TABLE test_bool_param ( b boolean, primary key(id) ); -INSERT INTO(test_bool_param(b) VALUES(true); \ No newline at end of file +INSERT INTO test_bool_param(b) VALUES(true); From b4e7c1cb70bd65aca480e63f774b46e9bad972b4 Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Thu, 10 Nov 2022 23:21:02 -0500 Subject: [PATCH 30/39] Add migrate-ci-to-github-actions branch to ci --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5efd212..6380550 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,7 @@ on: push: branches: - 'master' + - 'migrate-ci-to-github-actions' - 'refs/heads/[0-9]+.[0-9]+.[0-9]+' pull_request: jobs: @@ -57,4 +58,4 @@ jobs: with: dependency-versions: ${{ matrix.composer }} # - run: vendor/bin/phpunit --testdox - - run: vendor/bin/phpunit \ No newline at end of file + - run: vendor/bin/phpunit From 2ba63e4c962387d8d1eacf1068600b6bf8f2b7bc Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Thu, 10 Nov 2022 23:32:32 -0500 Subject: [PATCH 31/39] Skip null test that is causing failures with new CI --- tests/Integration/NullPasswordTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Integration/NullPasswordTest.php b/tests/Integration/NullPasswordTest.php index 0a19b50..7c4af1e 100644 --- a/tests/Integration/NullPasswordTest.php +++ b/tests/Integration/NullPasswordTest.php @@ -9,6 +9,8 @@ class NullPasswordTest extends TestCase { public function testNullPassword() { + $this->markTestSkipped('Not using null password anymore. Maybe should setup tests to twst this again.'); + $client = new Client([ "user" => $this::getDbUser(), "database" => $this::getDbName(), From 15c53c4a0291258357ca85456d358ffecbbecbde Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Thu, 10 Nov 2022 23:44:31 -0500 Subject: [PATCH 32/39] Update minimum phpunit version to get past dep error in CI --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4ec479e..d5a0dff 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ "evenement/evenement": "^2.0 | ^3.0" }, "require-dev": { - "phpunit/phpunit": "^8 || ^5.7", + "phpunit/phpunit": "^8 || ^6.5.5", "react/dns": "^1.0" } } From 9744d09b606ad3dfa45e2dafabbffc1fab6e646a Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Thu, 10 Nov 2022 23:50:59 -0500 Subject: [PATCH 33/39] Bump phpunit 8 to >=8.5.23 to fix CI error --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d5a0dff..309a457 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ "evenement/evenement": "^2.0 | ^3.0" }, "require-dev": { - "phpunit/phpunit": "^8 || ^6.5.5", + "phpunit/phpunit": ">=8.5.23 || ^6.5.5", "react/dns": "^1.0" } } From 696b7667dd405a744da748d93da16f46c85357df Mon Sep 17 00:00:00 2001 From: Matt Bonneau Date: Thu, 10 Nov 2022 23:56:42 -0500 Subject: [PATCH 34/39] Remove migration branch from ci,yml --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6380550..7164c76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,6 @@ on: push: branches: - 'master' - - 'migrate-ci-to-github-actions' - 'refs/heads/[0-9]+.[0-9]+.[0-9]+' pull_request: jobs: From f8af9d0ad262cc202bbd4b1af937779f2ffa4c91 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 1 Sep 2022 17:06:44 +0200 Subject: [PATCH 35/39] PHP 8.2 compatibility --- src/PgAsync/Command/Describe.php | 1 - src/PgAsync/Command/Parse.php | 1 - 2 files changed, 2 deletions(-) diff --git a/src/PgAsync/Command/Describe.php b/src/PgAsync/Command/Describe.php index ed04ab3..d294e56 100644 --- a/src/PgAsync/Command/Describe.php +++ b/src/PgAsync/Command/Describe.php @@ -16,7 +16,6 @@ public function __construct(string $name = "") { $this->name = $name; $this->portalOrStatement = 'P'; - $this->subject = new Subject(); } public function encodedMessage(): string diff --git a/src/PgAsync/Command/Parse.php b/src/PgAsync/Command/Parse.php index 5d7de76..a1b2934 100644 --- a/src/PgAsync/Command/Parse.php +++ b/src/PgAsync/Command/Parse.php @@ -19,7 +19,6 @@ public function __construct(string $name, string $queryString) { $this->name = $name; $this->queryString = $queryString; - $this->subject = new Subject(); } // there is mechanisms to pre-describe types - we aren't getting into that From 7889cef05d256725e8f17de523db14b04cbad1ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20=C5=BBwirek?= Date: Tue, 1 Aug 2023 12:24:58 +0200 Subject: [PATCH 36/39] Support for SCRAM_SHA_256 --- composer.json | 4 + docker/docker-compose.yml | 13 ++ src/PgAsync/Command/SaslInitialResponse.php | 41 ++++ src/PgAsync/Command/SaslResponse.php | 39 ++++ src/PgAsync/Connection.php | 32 ++- src/PgAsync/Message/Authentication.php | 40 ++++ src/PgAsync/Message/Message.php | 4 +- src/PgAsync/ScramSha256.php | 208 ++++++++++++++++++ tests/Integration/ScramSha256PasswordTest.php | 43 ++++ tests/TestCase.php | 1 - tests/Unit/Message/MessageTest.php | 2 +- 11 files changed, 422 insertions(+), 5 deletions(-) create mode 100644 src/PgAsync/Command/SaslInitialResponse.php create mode 100644 src/PgAsync/Command/SaslResponse.php create mode 100644 src/PgAsync/ScramSha256.php create mode 100644 tests/Integration/ScramSha256PasswordTest.php diff --git a/composer.json b/composer.json index 309a457..7c0172a 100644 --- a/composer.json +++ b/composer.json @@ -42,5 +42,9 @@ "require-dev": { "phpunit/phpunit": ">=8.5.23 || ^6.5.5", "react/dns": "^1.0" + }, + "scripts": { + "docker-up": "cd docker && docker-compose up -d", + "docker-down": "cd docker && docker-compose down" } } diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9b2d179..ec53ffb 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -15,3 +15,16 @@ services: - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d ports: - "5432:5432" + + pgasync-postgres-15: + container_name: pgasync-postgres-15 + image: postgres:15 + environment: + - PGDATA=/database + - POSTGRES_USER=sampleuser + - POSTGRES_PASSWORD=some_password + - TZ=America/New_York + volumes: + - .:/app + ports: + - "5415:5432" diff --git a/src/PgAsync/Command/SaslInitialResponse.php b/src/PgAsync/Command/SaslInitialResponse.php new file mode 100644 index 0000000..83f538a --- /dev/null +++ b/src/PgAsync/Command/SaslInitialResponse.php @@ -0,0 +1,41 @@ +scramSha265 = $scramSha265; + } + + public function encodedMessage(): string + { + $mechanism = self::SCRAM_SHA_256 . "\0"; + $clientFirstMessage = $this->scramSha265->getClientFirstMessage(); + + $message = "p"; + $messageLength = strlen($mechanism) + strlen($clientFirstMessage) + 8; + $message .= pack("N", $messageLength) . $mechanism; + $message .= pack("N", strlen($clientFirstMessage)) . $clientFirstMessage; + + return $message; + } + + public function shouldWaitForComplete(): bool + { + return false; + } +} \ No newline at end of file diff --git a/src/PgAsync/Command/SaslResponse.php b/src/PgAsync/Command/SaslResponse.php new file mode 100644 index 0000000..b7532e6 --- /dev/null +++ b/src/PgAsync/Command/SaslResponse.php @@ -0,0 +1,39 @@ +scramSha265 = $scramSha265; + } + + public function encodedMessage(): string + { + $clientFinalMessage = $this->createClientFinalMessage(); + $messageLength = strlen($clientFinalMessage) + 4; + + return 'p' . pack('N', $messageLength) . $clientFinalMessage; + } + + public function shouldWaitForComplete(): bool + { + return false; + } + + private function createClientFinalMessage(): string + { + return $this->scramSha265->getClientFirstMessageWithoutProof() . ',p=' . base64_encode($this->scramSha265->getClientProof()); + } +} \ No newline at end of file diff --git a/src/PgAsync/Connection.php b/src/PgAsync/Connection.php index 1f896f7..2a06107 100644 --- a/src/PgAsync/Connection.php +++ b/src/PgAsync/Connection.php @@ -10,6 +10,8 @@ use PgAsync\Command\Execute; use PgAsync\Command\Parse; use PgAsync\Command\PasswordMessage; +use PgAsync\Command\SaslInitialResponse; +use PgAsync\Command\SaslResponse; use PgAsync\Command\Sync; use PgAsync\Command\Terminate; use PgAsync\Message\Authentication; @@ -119,6 +121,9 @@ class Connection extends EventEmitter /** @var bool */ private $cancelRequested; + /** @var ScramSha256 */ + private $scramSha256; + /** * Can be 'I' for Idle, 'T' if in transactions block * or 'E' if in failed transaction block (queries will fail until end of trans) @@ -174,6 +179,7 @@ public function __construct(array $parameters, LoopInterface $loop, ConnectorInt $this->cancelRequested = false; $this->parameters = $parameters; + $this->scramSha256 = new ScramSha256($parameters['user'], $this->password ?: ''); } private function start() @@ -268,7 +274,9 @@ private function processData($data) $type = $data[0]; - $message = Message::createMessageFromIdentifier($type); + $message = Message::createMessageFromIdentifier($type, [ + 'SCRAM_SHA_256' => $this->scramSha256 + ]); if ($message !== false) { $this->currentMessage = $message; return $data; @@ -388,6 +396,28 @@ private function handleAuthentication(Authentication $message) return; } + if ($message->getAuthCode() === $message::AUTH_SCRAM) { + $saslInitialResponse = new SaslInitialResponse($this->scramSha256); + $this->stream->write($saslInitialResponse->encodedMessage()); + + return; + } + + if ($message->getAuthCode() === $message::AUTH_SCRAM_CONTINUE) { + $saslResponse = new SaslResponse($this->scramSha256); + $this->stream->write($saslResponse->encodedMessage()); + + return; + } + + if ($message->getAuthCode() === $message::AUTH_SCRAM_FIN) { + if ($this->scramSha256->verify()) { + return; + } + + $this->lastError = 'Invalid server signature sent by server on SCRAM FIN stage'; + } + $this->connStatus = $this::CONNECTION_BAD; $this->failAllCommandsWith(new \Exception($this->lastError)); $this->emit('error', [new \Exception($this->lastError)]); diff --git a/src/PgAsync/Message/Authentication.php b/src/PgAsync/Message/Authentication.php index f3f06aa..9dbb932 100644 --- a/src/PgAsync/Message/Authentication.php +++ b/src/PgAsync/Message/Authentication.php @@ -2,6 +2,8 @@ namespace PgAsync\Message; +use PgAsync\ScramSha256; + class Authentication extends Message { const AUTH_OK = 0; // AuthenticationOk @@ -12,11 +14,26 @@ class Authentication extends Message const AUTH_GSS = 7; // AuthenticationGSS const AUTH_GSS_CONTINUE = 8; // AuthenticationGSSContinue const AUTH_SSPI = 9; // AuthenticationSSPI + const AUTH_SCRAM = 10; // AuthenticationSASL + const AUTH_SCRAM_CONTINUE = 11; // AuthenticationSASLContinue + const AUTH_SCRAM_FIN = 12; // AuthenticationSASLFinal private $authCode; private $salt; + /** @var ScramSha256 */ + private $scramSha256; + + private $iteration; + + private $nonce; + + public function __construct(ScramSha256 $scramSha265) + { + $this->scramSha256 = $scramSha265; + } + /** * @inheritDoc * @throws \InvalidArgumentException @@ -47,6 +64,23 @@ public function parseMessage(string $rawMessage) break; // AuthenticationGSSContinue case $this::AUTH_SSPI: break; // AuthenticationSSPI + case $this::AUTH_SCRAM: + $this->scramSha256->beginFirstClientMessageStage(); + break; + case $this::AUTH_SCRAM_CONTINUE: + $content = $this->getContent($rawMessage); + $parts = explode(',', $content); + $this->scramSha256->beginFinalClientMessageStage( + substr($parts[0], 2), + substr($parts[1], 2), + (int) substr($parts[2], 2) + ); + + break; + case $this::AUTH_SCRAM_FIN: + $content = $this->getContent($rawMessage); + $this->scramSha256->beginVerificationStage(substr($content, 2)); + break; } $this->authCode = $authCode; @@ -70,4 +104,10 @@ public function getSalt(): string return $this->salt; } + + private function getContent(string $rawMessage): string + { + $messageLength = unpack('N', substr($rawMessage, 1, 4))[1]; + return substr($rawMessage, 9, $messageLength - 8); + } } diff --git a/src/PgAsync/Message/Message.php b/src/PgAsync/Message/Message.php index 8f08588..d02546c 100644 --- a/src/PgAsync/Message/Message.php +++ b/src/PgAsync/Message/Message.php @@ -23,11 +23,11 @@ public static function prependLengthInt32(string $s): string return Message::int32($len + 4) . $s; } - public static function createMessageFromIdentifier(string $identifier): ParserInterface + public static function createMessageFromIdentifier(string $identifier, array $dependencies): ParserInterface { switch ($identifier) { case 'R': - return new Authentication(); + return new Authentication($dependencies['SCRAM_SHA_256']); case 'K': return new BackendKeyData(); case 'C': diff --git a/src/PgAsync/ScramSha256.php b/src/PgAsync/ScramSha256.php new file mode 100644 index 0000000..890e5e4 --- /dev/null +++ b/src/PgAsync/ScramSha256.php @@ -0,0 +1,208 @@ +user = $user; + $this->password = $password; + } + + public function beginFirstClientMessageStage() + { + $length = strlen(self::CHARACTERS); + + for ($i = 0; $i < self::CLIENT_NONCE_LENGTH; $i++) { + $this->clientNonce .= substr(self::CHARACTERS, random_int(0, $length), 1); + } + + $this->currentStage = self::STAGE_FIRST_MESSAGE; + } + + public function beginFinalClientMessageStage(string $nonce, string $salt, int $iteration) + { + $this->nonce = $nonce; + $this->salt = $salt; + $this->iteration = $iteration; + + $this->currentStage = self::STAGE_FINAL_MESSAGE; + } + + public function beginVerificationStage(string $verification) + { + $this->verification = $verification; + + $this->currentStage = self::STAGE_VERIFICATION; + } + + public function verify(): bool + { + $this->checkStage(self::STAGE_VERIFICATION); + + $serverKey = hash_hmac("sha256", "Server Key", $this->getSaltedPassword(), true); + $serverSignature = hash_hmac('sha256', $this->getAuthMessage(), $serverKey, true); + + return $serverSignature === base64_decode($this->verification); + } + + public function getClientFirstMessageWithoutProof(): string + { + if (null === $this->clientFirstMessageWithoutProof) { + $this->clientFirstMessageWithoutProof = sprintf( + 'c=%s,r=%s', + base64_encode('n,,'), + $this->nonce + ); + } + + return $this->clientFirstMessageWithoutProof; + } + + public function getSaltedPassword(): string + { + $this->checkStage(self::STAGE_FINAL_MESSAGE); + + if (null === $this->saltedPassword) { + $this->saltedPassword = hash_pbkdf2( + "sha256", + $this->password, + base64_decode($this->salt), + $this->iteration, + 32, + true + ); + } + + return $this->saltedPassword; + } + + public function getClientKey(): string + { + $this->checkStage(self::STAGE_FINAL_MESSAGE); + + if (null === $this->clientKey) { + $this->clientKey = hash_hmac("sha256", "Client Key", $this->getSaltedPassword(), true); + } + + return $this->clientKey; + } + + public function getStoredKey(): string + { + $this->checkStage(self::STAGE_FINAL_MESSAGE); + + if (null === $this->storedKey) { + $this->storedKey = hash("sha256", $this->getClientKey(), true); + } + + return $this->storedKey; + } + + public function getClientFirstMessageBare(): string + { + $this->checkStage(self::STAGE_FIRST_MESSAGE); + + return sprintf( + 'n=%s,r=%s', + $this->user, + $this->clientNonce + ); + } + + public function getClientFirstMessage(): string + { + $this->checkStage(self::STAGE_FIRST_MESSAGE); + + return sprintf('n,,%s', $this->getClientFirstMessageBare()); + } + + public function getAuthMessage(): string + { + $this->checkStage(self::STAGE_FINAL_MESSAGE); + + if (null === $this->authMessage) { + $clientFirstMessageBare = $this->getClientFirstMessageBare(); + $serverFirstMessage = sprintf( + 'r=%s,s=%s,i=%s', + $this->nonce, + $this->salt, + $this->iteration + ); + + $this->authMessage = implode(',', [ + $clientFirstMessageBare, + $serverFirstMessage, + $this->getClientFirstMessageWithoutProof() + ]); + } + + return $this->authMessage; + } + + public function getClientProof(): string + { + $this->checkStage(self::STAGE_FINAL_MESSAGE); + + $clientKey = $this->getClientKey(); + $storedKey = $this->getStoredKey(); + $authMessage = $this->getAuthMessage(); + $clientSignature = hash_hmac("sha256", $authMessage, $storedKey, true); + + return $clientKey ^ $clientSignature; + } + + private function checkStage(int $stage): void + { + if ($this->currentStage < $stage) { + throw new \LogicException('Invalid Stage of SCRAM authorization'); + } + } +} \ No newline at end of file diff --git a/tests/Integration/ScramSha256PasswordTest.php b/tests/Integration/ScramSha256PasswordTest.php new file mode 100644 index 0000000..5bb2bd8 --- /dev/null +++ b/tests/Integration/ScramSha256PasswordTest.php @@ -0,0 +1,43 @@ + 'sampleuser', + "database" => 'postgres', + "port" => 5415, + "auto_disconnect" => true, + "password" => "some_password" + ], $this->getLoop()); + + $hello = null; + + $client->query("SELECT 'Hello' AS hello") + ->subscribe(new CallbackObserver( + function ($x) use (&$hello) { + $this->assertNull($hello); + $hello = $x['hello']; + }, + function ($e) { + $this->fail('Unexpected error ' . $e); + }, + function () { + $this->getLoop()->addTimer(0.1, function () { + $this->stopLoop(); + }); + } + )); + + $this->runLoopWithTimeout(2); + + $this->assertEquals('Hello', $hello); + } +} \ No newline at end of file diff --git a/tests/TestCase.php b/tests/TestCase.php index f47bddf..6ab29c0 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,7 +3,6 @@ namespace PgAsync\Tests; use EventLoop\EventLoop; -use React\EventLoop\Factory; use React\EventLoop\LoopInterface; use React\EventLoop\Timer\Timer; use PHPUnit\Framework\TestCase as BaseTestCase; diff --git a/tests/Unit/Message/MessageTest.php b/tests/Unit/Message/MessageTest.php index df13505..9eebf53 100644 --- a/tests/Unit/Message/MessageTest.php +++ b/tests/Unit/Message/MessageTest.php @@ -15,7 +15,7 @@ public function testNotificationResponse() $rawNotificationMessage = hex2bin('41000000190000040c686572650048656c6c6f20746865726500'); - $notificationResponse = \PgAsync\Message\Message::createMessageFromIdentifier($rawNotificationMessage[0]); + $notificationResponse = \PgAsync\Message\Message::createMessageFromIdentifier($rawNotificationMessage[0], []); $this->assertInstanceOf(\PgAsync\Message\NotificationResponse::class, $notificationResponse); /** @var \PgAsync\Message\NotificationResponse */ $notificationResponse->parseData($rawNotificationMessage); From 574faa9455e0b9b50699e0cdaf92f274b246c56f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20=C5=BBwirek?= Date: Mon, 21 Aug 2023 21:38:54 +0200 Subject: [PATCH 37/39] update postgres config for CI tests to handle scram-sha-256 auth along with md5 --- .github/workflows/ci.yml | 5 ++++- docker/docker-compose.yml | 17 +++++------------ docker/docker-entrypoint-initdb.d/init.sh | 2 ++ src/PgAsync/Message/Message.php | 2 +- tests/Integration/ScramSha256PasswordTest.php | 9 ++++----- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7164c76..f6a99f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: image: postgres:${{ matrix.postgres }} env: POSTGRES_PASSWORD: postgres + POSTGRES_INITDB_ARGS: --auth-host=md5 # Set health checks to wait until postgres has started options: >- --health-cmd pg_isready @@ -34,7 +35,7 @@ jobs: fail-fast: false matrix: php: ${{ fromJson(needs.supported-versions-matrix.outputs.version) }} - postgres: [12, 13] + postgres: [12, 13, 14, 15] composer: [lowest, locked, highest] needs: - supported-versions-matrix @@ -46,6 +47,8 @@ jobs: PGPASSWORD=postgres psql -h localhost -U postgres -c "ALTER ROLE pgasync PASSWORD 'pgasync'" PGPASSWORD=postgres psql -h localhost -U postgres -c "CREATE USER pgasyncpw" PGPASSWORD=postgres psql -h localhost -U postgres -c "ALTER ROLE pgasyncpw PASSWORD 'example_password'" + PGPASSWORD=postgres psql -h localhost -U postgres -c "CREATE USER scram_user" + PGPASSWORD=postgres psql -h localhost -U postgres -c "SET password_encryption='scram-sha-256';ALTER ROLE scram_user PASSWORD 'scram_password'" PGPASSWORD=postgres psql -h localhost -U postgres -c "CREATE DATABASE pgasync_test OWNER pgasync" PGPASSWORD=pgasync psql -h localhost -U pgasync -f tests/test_db.sql pgasync_test # PGPASSWORD=postgres cat tests/test_db.sql | xargs -I % psql -h localhost -U postgres -c "%" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index ec53ffb..3031c55 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -7,6 +7,7 @@ services: environment: - PGDATA=/database - POSTGRES_PASSWORD=some_password + - POSTGRES_INITDB_ARGS=--auth-host=md5 - TZ=America/New_York volumes: - .:/app @@ -16,15 +17,7 @@ services: ports: - "5432:5432" - pgasync-postgres-15: - container_name: pgasync-postgres-15 - image: postgres:15 - environment: - - PGDATA=/database - - POSTGRES_USER=sampleuser - - POSTGRES_PASSWORD=some_password - - TZ=America/New_York - volumes: - - .:/app - ports: - - "5415:5432" +configs: + pg_hba: + file: pg_hba_new.conf + diff --git a/docker/docker-entrypoint-initdb.d/init.sh b/docker/docker-entrypoint-initdb.d/init.sh index fe14128..af68572 100644 --- a/docker/docker-entrypoint-initdb.d/init.sh +++ b/docker/docker-entrypoint-initdb.d/init.sh @@ -5,7 +5,9 @@ echo "Running as $USER in $PWD" createuser -U postgres --createdb pgasync createuser -U postgres --createdb pgasyncpw +createuser -U postgres --createdb scram_user psql -U postgres -c "ALTER ROLE pgasyncpw PASSWORD 'example_password'" +psql -U postgres -c "SET password_encryption='scram-sha-256'; ALTER ROLE scram_user PASSWORD 'scram_password'" cd /app cp pg_hba_new.conf database/pg_hba.conf diff --git a/src/PgAsync/Message/Message.php b/src/PgAsync/Message/Message.php index d02546c..e5eaf5b 100644 --- a/src/PgAsync/Message/Message.php +++ b/src/PgAsync/Message/Message.php @@ -23,7 +23,7 @@ public static function prependLengthInt32(string $s): string return Message::int32($len + 4) . $s; } - public static function createMessageFromIdentifier(string $identifier, array $dependencies): ParserInterface + public static function createMessageFromIdentifier(string $identifier, array $dependencies = []): ParserInterface { switch ($identifier) { case 'R': diff --git a/tests/Integration/ScramSha256PasswordTest.php b/tests/Integration/ScramSha256PasswordTest.php index 5bb2bd8..14f11f1 100644 --- a/tests/Integration/ScramSha256PasswordTest.php +++ b/tests/Integration/ScramSha256PasswordTest.php @@ -8,14 +8,13 @@ class ScramSha256PasswordTest extends TestCase { - public function testScamSha256Login() + public function testScramSha256Login() { $client = new Client([ - "user" => 'sampleuser', - "database" => 'postgres', - "port" => 5415, + "user" => 'scram_user', + "database" => $this->getDbName(), "auto_disconnect" => true, - "password" => "some_password" + "password" => "scram_password" ], $this->getLoop()); $hello = null; From ef19cc359373f058759926978535048d19e614de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20=C5=BBwirek?= Date: Mon, 21 Aug 2023 22:26:59 +0200 Subject: [PATCH 38/39] remove not supported in php7.0 void return type from ScramSha256 --- src/PgAsync/ScramSha256.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PgAsync/ScramSha256.php b/src/PgAsync/ScramSha256.php index 890e5e4..1895dfb 100644 --- a/src/PgAsync/ScramSha256.php +++ b/src/PgAsync/ScramSha256.php @@ -199,7 +199,7 @@ public function getClientProof(): string return $clientKey ^ $clientSignature; } - private function checkStage(int $stage): void + private function checkStage(int $stage) { if ($this->currentStage < $stage) { throw new \LogicException('Invalid Stage of SCRAM authorization'); From 2ca0b98b86cc4219e1226c0450e49a36cb195fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zbigniew=20=C5=BBwirek?= Date: Mon, 21 Aug 2023 22:31:39 +0200 Subject: [PATCH 39/39] Fix ClientTest mock creation to be compatible with phpunit 10 --- tests/Unit/ClientTest.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Unit/ClientTest.php b/tests/Unit/ClientTest.php index 957abff..606b695 100644 --- a/tests/Unit/ClientTest.php +++ b/tests/Unit/ClientTest.php @@ -50,9 +50,7 @@ function (Exception $e) use (&$exception) { public function testFailedDNSLookupEarlyRejection() { - $executor = $this->getMockBuilder(ExecutorInterface::class) - ->setMethods(['query']) - ->getMock(); + $executor = $this->createMock(ExecutorInterface::class); $executor ->method('query')