diff --git a/.travis.yml b/.travis.yml index 57ab098..82c4c1d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,43 @@ php: sudo: false +env: + - TEST_SECURE=6001 TEST_PLAIN=6000 + +# install required system packages, see 'install' below for details +# Travis' containers require this, otherwise use this: +# sudo apt-get install openssl build-essential libev-dev libssl-dev +addons: + apt: + packages: + - openssl + - build-essential + - libev-dev + - libssl-dev + install: + # install this library plus its dependencies - composer install --prefer-source --no-interaction + # we need openssl and either stunnel or stud + # unfortunately these are not available in Travis' containers + # sudo apt-get install -y openssl stud + # sudo apt-get install -y openssl stunnel4 + + # instead, let's install stud from source + # build dependencies are already installed, see 'addons.apt.packages' above + # sudo apt-get install openssl build-essential libev-dev libssl-dev + - git clone https://github.com/bumptech/stud.git + - (cd stud && make) + + # create self-signed certificate + - openssl genrsa 1024 > stunnel.key + - openssl req -batch -subj '/CN=127.0.0.1' -new -x509 -nodes -sha1 -days 3650 -key stunnel.key > stunnel.cert + - cat stunnel.cert stunnel.key > stunnel.pem + + # start TLS/SSL terminating proxy + # stunnel -f -p stunnel.pem -d $TEST_SECURE -r $TEST_PLAIN 2>/dev/null & + - ./stud/stud --daemon -f 127.0.0.1,$TEST_SECURE -b 127.0.0.1,$TEST_PLAIN stunnel.pem + script: - phpunit --coverage-text diff --git a/README.md b/README.md index 699f6b9..2cc7398 100644 --- a/README.md +++ b/README.md @@ -137,3 +137,25 @@ $connector->create('/tmp/demo.sock')->then(function (React\Stream\Stream $stream $loop->run(); ``` + +## Tests + +To run the test suite, you need PHPUnit. Go to the project root and run: + +```bash +$ phpunit +``` + +The test suite also contains some optional integration tests which operate on a +TCP/IP socket server and an optional TLS/SSL terminating proxy in front of it. +The underlying TCP/IP socket server will be started automatically, whereas the +TLS/SSL terminating proxy has to be started and enabled like this: + +```bash +$ stunnel -f -p stunnel.pem -d 6001 -r 6000 & +$ TEST_SECURE=6001 TEST_PLAIN=6000 phpunit +``` + +See also the [Travis configuration](.travis.yml) for details on how to set up +the TLS/SSL terminating proxy and the required certificate file (`stunnel.pem`) +if you're unsure. diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index f39b59e..9e16281 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -21,22 +21,12 @@ public function gettingStuffFromGoogleShouldWork() $dns = $factory->create('8.8.8.8', $loop); $connector = new Connector($loop, $dns); - $connected = false; - $response = null; - - $connector->create('google.com', 80) - ->then(function ($conn) use (&$connected) { - $connected = true; - $conn->write("GET / HTTP/1.0\r\n\r\n"); - return BufferedSink::createPromise($conn); - }) - ->then(function ($data) use (&$response) { - $response = $data; - }); - - $loop->run(); - - $this->assertTrue($connected); + $conn = Block\await($connector->create('google.com', 80), $loop); + + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + $response = Block\await(BufferedSink::createPromise($conn), $loop); + $this->assertRegExp('#^HTTP/1\.0#', $response); } @@ -52,26 +42,17 @@ public function gettingEncryptedStuffFromGoogleShouldWork() $factory = new Factory(); $dns = $factory->create('8.8.8.8', $loop); - $connected = false; - $response = null; - $secureConnector = new SecureConnector( new Connector($loop, $dns), $loop ); - $secureConnector->create('google.com', 443) - ->then(function ($conn) use (&$connected) { - $connected = true; - $conn->write("GET / HTTP/1.0\r\n\r\n"); - return BufferedSink::createPromise($conn); - }) - ->then(function ($data) use (&$response) { - $response = $data; - }); - - $loop->run(); - - $this->assertTrue($connected); + + $conn = Block\await($secureConnector->create('google.com', 443), $loop); + + $conn->write("GET / HTTP/1.0\r\n\r\n"); + + $response = Block\await(BufferedSink::createPromise($conn), $loop); + $this->assertRegExp('#^HTTP/1\.0#', $response); } @@ -87,7 +68,6 @@ public function testSelfSignedRejectsIfVerificationIsEnabled() $factory = new Factory(); $dns = $factory->create('8.8.8.8', $loop); - $secureConnector = new SecureConnector( new Connector($loop, $dns), $loop, diff --git a/tests/SecureIntegrationTest.php b/tests/SecureIntegrationTest.php new file mode 100644 index 0000000..9a9e600 --- /dev/null +++ b/tests/SecureIntegrationTest.php @@ -0,0 +1,205 @@ +markTestSkipped('Not supported on HHVM'); + } + + $this->portSecure = getenv('TEST_SECURE'); + $this->portPlain = getenv('TEST_PLAIN'); + + if ($this->portSecure === false || $this->portPlain === false) { + $this->markTestSkipped('Needs TEST_SECURE=X and TEST_PLAIN=Y environment variables to run, see README.md'); + } + + $this->loop = LoopFactory::create(); + $this->server = new Server($this->loop); + $this->server->listen($this->portPlain); + $this->connector = new SecureConnector(new TcpConnector($this->loop), $this->loop, array('verify_peer' => false)); + } + + public function tearDown() + { + if ($this->server !== null) { + $this->server->shutdown(); + $this->server = null; + } + } + + public function testConnectToServer() + { + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + /* @var $client Stream */ + + $client->close(); + } + + public function testConnectToServerEmitsConnection() + { + $promiseServer = $this->createPromiseForEvent($this->server, 'connection', $this->expectCallableOnce()); + + $promiseClient = $this->connector->create('127.0.0.1', $this->portSecure); + + list($_, $client) = Block\awaitAll(array($promiseServer, $promiseClient), $this->loop); + /* @var $client Stream */ + + $client->close(); + } + + public function testSendSmallDataToServerReceivesOneChunk() + { + // server expects one connection which emits one data event + $received = new Deferred(); + $this->server->on('connection', function (Stream $peer) use ($received) { + $peer->on('data', function ($chunk) use ($received) { + $received->resolve($chunk); + }); + }); + + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + /* @var $client Stream */ + + $client->write('hello'); + + // await server to report one "data" event + $data = Block\await($received->promise(), $this->loop); + + $client->close(); + + $this->assertEquals('hello', $data); + } + + public function testSendDataWithEndToServerReceivesAllData() + { + $disconnected = new Deferred(); + $this->server->on('connection', function (Stream $peer) use ($disconnected) { + $received = ''; + $peer->on('data', function ($chunk) use (&$received) { + $received .= $chunk; + }); + $peer->on('close', function () use (&$received, $disconnected) { + $disconnected->resolve($received); + }); + }); + + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + /* @var $client Stream */ + + $data = str_repeat('a', 200000); + $client->end($data); + + // await server to report connection "close" event + $received = Block\await($disconnected->promise(), $this->loop); + + $this->assertEquals($data, $received); + } + + public function testSendDataWithoutEndingToServerReceivesAllData() + { + $received = ''; + $this->server->on('connection', function (Stream $peer) use (&$received) { + $peer->on('data', function ($chunk) use (&$received) { + $received .= $chunk; + }); + }); + + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + /* @var $client Stream */ + + $data = str_repeat('d', 200000); + $client->write($data); + + // buffer incoming data for 0.1s (should be plenty of time) + Block\sleep(0.1, $this->loop); + + $client->close(); + + $this->assertEquals($data, $received); + } + + public function testConnectToServerWhichSendsSmallDataReceivesOneChunk() + { + $this->server->on('connection', function (Stream $peer) { + $peer->write('hello'); + }); + + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + /* @var $client Stream */ + + // await client to report one "data" event + $receive = $this->createPromiseForEvent($client, 'data', $this->expectCallableOnceWith('hello')); + Block\await($receive, $this->loop); + + $client->close(); + } + + public function testConnectToServerWhichSendsDataWithEndReceivesAllData() + { + $data = str_repeat('b', 100000); + $this->server->on('connection', function (Stream $peer) use ($data) { + $peer->end($data); + }); + + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + /* @var $client Stream */ + + // await data from client until it closes + $received = Block\await(BufferedSink::createPromise($client), $this->loop); + + $this->assertEquals($data, $received); + } + + public function testConnectToServerWhichSendsDataWithoutEndingReceivesAllData() + { + $data = str_repeat('c', 100000); + $this->server->on('connection', function (Stream $peer) use ($data) { + $peer->write($data); + }); + + $client = Block\await($this->connector->create('127.0.0.1', $this->portSecure), $this->loop); + /* @var $client Stream */ + + // buffer incoming data for 0.1s (should be plenty of time) + $received = ''; + $client->on('data', function ($chunk) use (&$received) { + $received .= $chunk; + }); + Block\sleep(0.1, $this->loop); + + $client->close(); + + $this->assertEquals($data, $received); + } + + private function createPromiseForEvent(EventEmitterInterface $emitter, $event, $fn) + { + return new Promise(function ($resolve) use ($emitter, $event, $fn) { + $emitter->on($event, function () use ($resolve, $fn) { + $resolve(call_user_func_array($fn, func_get_args())); + }); + }); + } +} diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index 172102e..2fd700e 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -5,6 +5,7 @@ use React\EventLoop\StreamSelectLoop; use React\Socket\Server; use React\SocketClient\TcpConnector; +use Clue\React\Block; class TcpConnectorTest extends TestCase { @@ -23,8 +24,6 @@ public function connectionToEmptyPortShouldFail() /** @test */ public function connectionToTcpServerShouldSucceed() { - $capturedStream = null; - $loop = new StreamSelectLoop(); $server = new Server($loop); @@ -35,15 +34,12 @@ public function connectionToTcpServerShouldSucceed() $server->listen(9999); $connector = new TcpConnector($loop); - $connector->create('127.0.0.1', 9999) - ->then(function ($stream) use (&$capturedStream) { - $capturedStream = $stream; - $stream->end(); - }); - $loop->run(); + $stream = Block\await($connector->create('127.0.0.1', 9999), $loop); - $this->assertInstanceOf('React\Stream\Stream', $capturedStream); + $this->assertInstanceOf('React\Stream\Stream', $stream); + + $stream->close(); } /** @test */ @@ -62,8 +58,6 @@ public function connectionToEmptyIp6PortShouldFail() /** @test */ public function connectionToIp6TcpServerShouldSucceed() { - $capturedStream = null; - $loop = new StreamSelectLoop(); $server = new Server($loop); @@ -72,16 +66,12 @@ public function connectionToIp6TcpServerShouldSucceed() $server->listen(9999, '::1'); $connector = new TcpConnector($loop); - $connector - ->create('::1', 9999) - ->then(function ($stream) use (&$capturedStream) { - $capturedStream = $stream; - $stream->end(); - }); - $loop->run(); + $stream = Block\await($connector->create('::1', 9999), $loop); + + $this->assertInstanceOf('React\Stream\Stream', $stream); - $this->assertInstanceOf('React\Stream\Stream', $capturedStream); + $stream->close(); } /** @test */ diff --git a/tests/TestCase.php b/tests/TestCase.php index 6926ec8..bc3fc8b 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -24,6 +24,17 @@ protected function expectCallableOnce() return $mock; } + protected function expectCallableOnceWith($value) + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke') + ->with($this->equalTo($value)); + + return $mock; + } + protected function expectCallableNever() { $mock = $this->createCallableMock();