diff --git a/.travis.yml b/.travis.yml index aca9235..742e5db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,16 +9,32 @@ php: - 7.1 before_install: + - sudo ln -s /home/travis/.phpenv/versions/$(phpenv version-name)/bin/phpize /usr/bin/ + - sudo ln -s /home/travis/.phpenv/versions/$(phpenv version-name)/bin/php-config /usr/bin/ - export PHP_MAJOR="$(echo $TRAVIS_PHP_VERSION | cut -d '.' -f 1,2)" install: - travis_retry composer self-update - travis_retry composer install --prefer-source --no-interaction --ignore-platform-reqs + - sudo apt-get -qq update + - bash build-ci/install_prereqs_$PHP_MAJOR.sh - php -m +before_script: + - ulimit -c unlimited -S || true + - echo '/tmp/core_%e.%p' | sudo tee /proc/sys/kernel/core_pattern &> /dev/null + script: - vendor/bin/phpunit -d memory_limit=1024M --coverage-text --coverage-clover=coverage.clover after_script: - if [ "$TRAVIS_PHP_VERSION" = "7.1" ]; then wget https://scrutinizer-ci.com/ocular.phar; fi - if [ "$TRAVIS_PHP_VERSION" = "7.1" ]; then php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi + +after_failure: + - bash build-ci/install_failure.sh + +addons: + apt: + packages: + - gdb diff --git a/build-ci/ini/ssh2.ini b/build-ci/ini/ssh2.ini new file mode 100644 index 0000000..98796b1 --- /dev/null +++ b/build-ci/ini/ssh2.ini @@ -0,0 +1 @@ +extension=ssh2.so \ No newline at end of file diff --git a/build-ci/install_failure.sh b/build-ci/install_failure.sh new file mode 100644 index 0000000..94bebb2 --- /dev/null +++ b/build-ci/install_failure.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# +# This source file is subject to the MIT License that is bundled +# with this package in the MIT license. + +shopt -s nullglob +export LC_ALL=C + +for i in /tmp/core_*.*; do + if [ -f "$i" -a "$(file "$i" | grep -o 'core file')" ]; then + gdb -q $(phpenv which php) "$i" < /dev/null + ./configure &> /dev/null + + make --silent -j4 &> /dev/null + make --silent install + + if [ -z $(php -m | grep ssh2) ]; then + phpenv config-add "${TRAVIS_BUILD_DIR}/build-ci/ini/ssh2.ini" + fi +} \ No newline at end of file diff --git a/build-ci/install_php_7.sh b/build-ci/install_php_7.sh new file mode 100644 index 0000000..8e959fd --- /dev/null +++ b/build-ci/install_php_7.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# +# This source file is subject to the MIT License that is bundled +# with this package in the MIT license. + +CURRENT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +TRAVIS_BUILD_DIR="${TRAVIS_BUILD_DIR:-$(dirname $(dirname $CURRENT_DIR))}" + +install_ssh2() { + sudo apt-get install -y -qq libssh2-1-dev libssh2-1 + + git clone -q https://github.com/php/pecl-networking-ssh2 -b master /tmp/ssh2 + cd /tmp/ssh2 + + phpize &> /dev/null + ./configure &> /dev/null + + make --silent -j4 &> /dev/null + make --silent install + + if [ -z $(php -m | grep ssh2) ]; then + phpenv config-add "${TRAVIS_BUILD_DIR}/build-ci/ini/ssh2.ini" + fi +} \ No newline at end of file diff --git a/build-ci/install_php_common.sh b/build-ci/install_php_common.sh new file mode 100644 index 0000000..bc0e96a --- /dev/null +++ b/build-ci/install_php_common.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# +# This source file is subject to the MIT License that is bundled +# with this package in the MIT license. + +CURRENT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +TRAVIS_BUILD_DIR="${TRAVIS_BUILD_DIR:-$(dirname $(dirname $CURRENT_DIR))}" + +pecl channel-update pecl.php.net || true +echo `whoami`":1234" | sudo chpasswd + +enable_extension() { + if [ -z $(php -m | grep "${1}") ] && [ -f "${TRAVIS_BUILD_DIR}/build-ci/ini/${1}.ini" ]; then + phpenv config-add "${TRAVIS_BUILD_DIR}/build-ci/ini/${1}.ini" + fi +} + +install_extension() { + INSTALLED=$(pecl list "${1}" | grep 'not installed') + + if [ -z "${INSTALLED}" ]; then + printf "\n" | pecl upgrade "${1}" &> /dev/null + else + printf "\n" | pecl install "${1}" &> /dev/null + fi + + enable_extension "${1}" +} \ No newline at end of file diff --git a/build-ci/install_prereqs_5.6.sh b/build-ci/install_prereqs_5.6.sh new file mode 100644 index 0000000..90f765d --- /dev/null +++ b/build-ci/install_prereqs_5.6.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# +# This source file is subject to the MIT License that is bundled +# with this package in the MIT license. + +CURRENT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +TRAVIS_BUILD_DIR="${TRAVIS_BUILD_DIR:-$(dirname $(dirname $CURRENT_DIR))}" + +source ${TRAVIS_BUILD_DIR}/build-ci/install_php_common.sh +source ${TRAVIS_BUILD_DIR}/build-ci/install_php_5.sh + +install_ssh2 \ No newline at end of file diff --git a/build-ci/install_prereqs_7.0.sh b/build-ci/install_prereqs_7.0.sh new file mode 100644 index 0000000..2f5fb0d --- /dev/null +++ b/build-ci/install_prereqs_7.0.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# +# This source file is subject to the MIT License that is bundled +# with this package in the MIT license. + +CURRENT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +TRAVIS_BUILD_DIR="${TRAVIS_BUILD_DIR:-$(dirname $(dirname $CURRENT_DIR))}" + +source ${TRAVIS_BUILD_DIR}/build-ci/install_php_common.sh +source ${TRAVIS_BUILD_DIR}/build-ci/install_php_7.sh + +install_ssh2 \ No newline at end of file diff --git a/build-ci/install_prereqs_7.1.sh b/build-ci/install_prereqs_7.1.sh new file mode 100644 index 0000000..2f5fb0d --- /dev/null +++ b/build-ci/install_prereqs_7.1.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# +# This source file is subject to the MIT License that is bundled +# with this package in the MIT license. + +CURRENT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +TRAVIS_BUILD_DIR="${TRAVIS_BUILD_DIR:-$(dirname $(dirname $CURRENT_DIR))}" + +source ${TRAVIS_BUILD_DIR}/build-ci/install_php_common.sh +source ${TRAVIS_BUILD_DIR}/build-ci/install_php_7.sh + +install_ssh2 \ No newline at end of file diff --git a/composer.json b/composer.json index 3cf922a..808cb26 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ }, "autoload": { "psr-4": { - "Dazzle\\SSH\\": "src/Event", + "Dazzle\\SSH\\": "src/SSH", "Dazzle\\SSH\\Test\\": "test" } }, diff --git a/test/Callback.php b/test/Callback.php new file mode 100644 index 0000000..451a262 --- /dev/null +++ b/test/Callback.php @@ -0,0 +1,9 @@ +loop = null; + $this->sim = null; + } + + /** + * + */ + public function tearDown() + { + unset($this->sim); + unset($this->loop); + } + + /** + * @return LoopInterface|null + */ + public function getLoop() + { + return $this->loop; + } + + /** + * Run test scenario as simulation. + * + * @param callable(Simulation) $scenario + * @return TModule + */ + public function simulate(callable $scenario) + { + try + { + $this->loop = new Loop(new SelectLoop); + $this->loop->erase(true); + + $this->sim = new Simulation($this->loop); + $this->sim->setScenario($scenario); + $this->sim->begin(); + } + catch (Exception $ex) + { + $this->fail($ex->getMessage()); + } + + return $this; + } + + /** + * @param $events + * @param int $flags + * @return TModule + */ + public function expect($events, $flags = Simulation::EVENTS_COMPARE_IN_ORDER) + { + $expectedEvents = []; + + foreach ($events as $event) + { + $data = isset($event[1]) ? $event[1] : []; + $expectedEvents[] = new Event($event[0], $data); + } + + $this->assertEvents( + $this->sim->getExpectations(), + $expectedEvents, + $flags + ); + + return $this; + } + + /** + * @param Event[] $actualEvents + * @param Event[] $expectedEvents + * @param int $flags + */ + public function assertEvents($actualEvents = [], $expectedEvents = [], $flags = Simulation::EVENTS_COMPARE_IN_ORDER) + { + $count = max(count($actualEvents), count($expectedEvents)); + + if ($flags === Simulation::EVENTS_COMPARE_RANDOMLY) + { + sort($actualEvents); + sort($expectedEvents); + } + + for ($i=0; $i<$count; $i++) + { + if (!isset($actualEvents[$i])) + { + $this->fail( + sprintf(self::MSG_EVENT_GET_ASSERTION_FAILED, $i, $expectedEvents[$i]->name(), 'null') + ); + } + else if (!isset($expectedEvents[$i])) + { + $this->fail( + sprintf(self::MSG_EVENT_GET_ASSERTION_FAILED, $i, 'null', $actualEvents[$i]->name()) + ); + } + + $actualEvent = $actualEvents[$i]; + $expectedEvent = $expectedEvents[$i]; + + $this->assertSame( + $expectedEvent->name(), + $actualEvent->name(), + sprintf(self::MSG_EVENT_NAME_ASSERTION_FAILED, $i) + ); + $this->assertSame( + $expectedEvent->data(), + $actualEvent->data(), + sprintf(self::MSG_EVENT_DATA_ASSERTION_FAILED, $i) + ); + } + } +} diff --git a/test/TModule/SSH2Test.php b/test/TModule/SSH2Test.php new file mode 100644 index 0000000..4eeb106 --- /dev/null +++ b/test/TModule/SSH2Test.php @@ -0,0 +1,335 @@ +markTestSkipped('Test has been skipped because of lacking SSH2 extension!'); + } + + $sim = $this; + $sim = $sim->simulate(function(SimulationInterface $sim) { + $loop = $sim->getLoop(); + $auth = new SSH2Password(TEST_USER, TEST_PASSWORD); + $config = new SSH2Config(); + $ssh2 = new SSH2($auth, $config, $loop); + + $ssh2->on('connect', function(SSH2Interface $ssh) use($sim) { + $sim->expect('connect'); + $ssh->disconnect(); + }); + + $ssh2->on('disconnect', function(SSH2Interface $ssh) use($sim) { + $sim->expect('disconnect'); + $sim->done(); + }); + + $ssh2->on('error', function(SSH2Interface $ssh, $ex) use($sim) { + $sim->fail($ex->getMessage()); + }); + + $sim->onStart(function() use($ssh2) { + $ssh2->connect(); + }); + $sim->onStop(function() use($ssh2) { + $ssh2->disconnect(); + }); + + }); + $sim = $sim->expect([ + [ 'connect', [] ], + [ 'disconnect', [] ] + ]); + } + + /** + * + */ + public function testSSH2_ShellDriver_IsAbleToExecute_SingleLineCommand() + { + if (!extension_loaded('ssh2')) + { + $this->markTestSkipped('Test has been skipped because of lacking SSH2 extension!'); + } + + $sim = $this; + $sim = $sim->simulate(function(SimulationInterface $sim) { + $loop = $sim->getLoop(); + $auth = new SSH2Password(TEST_USER, TEST_PASSWORD); + $config = new SSH2Config(); + $ssh2 = new SSH2($auth, $config, $loop); + + $ssh2->on('connect:shell', function(SSH2DriverInterface $shell) use($sim) { + $sim->expect('connect:shell'); + $buffer = ''; + + $command = $shell->open(); + $command->write('echo "test"'); + $command->on('data', function($command, $data) use(&$buffer) { + $buffer .= $data; + }); + $command->on('end', function() use(&$buffer, $shell, $sim) { + $sim->expect('buffer', [ $buffer ]); + $shell->disconnect(); + }); + }); + + $ssh2->on('disconnect:shell', function(SSH2DriverInterface $shell) use($sim, $ssh2) { + $sim->expect('disconnect:shell'); + $ssh2->disconnect(); + }); + + $ssh2->on('connect', function(SSH2Interface $ssh) use($sim) { + $sim->expect('connect'); + $ssh->createDriver(SSH2::DRIVER_SHELL) + ->connect(); + }); + + $ssh2->on('disconnect', function(SSH2Interface $ssh) use($sim) { + $sim->expect('disconnect'); + $sim->done(); + }); + + $ssh2->on('error', function(SSH2Interface $ssh, $ex) use($sim) { + $sim->fail($ex->getMessage()); + }); + + $sim->onStart(function() use($ssh2) { + $ssh2->connect(); + }); + $sim->onStop(function() use($ssh2) { + $ssh2->disconnect(); + }); + }); + $sim = $sim->expect([ + [ 'connect', [] ], + [ 'connect:shell', [] ], + [ 'buffer', [ "test\r\n" ] ], + [ 'disconnect:shell', [] ], + [ 'disconnect', [] ] + ]); + } + + /** + * + */ + public function testSSH2_ShellDriver_IsAbleToExecute_MultiLineCommand() + { + if (!extension_loaded('ssh2')) + { + $this->markTestSkipped('Test has been skipped because of lacking SSH2 extension!'); + } + + $sim = $this; + $sim = $sim->simulate(function(SimulationInterface $sim) { + $loop = $sim->getLoop(); + $auth = new SSH2Password(TEST_USER, TEST_PASSWORD); + $config = new SSH2Config(); + $ssh2 = new SSH2($auth, $config, $loop); + + $ssh2->on('connect:shell', function(SSH2DriverInterface $shell) use($sim) { + $sim->expect('connect:shell'); + + $buffer = ''; + $command = $shell->open(); + $command->write('printf "A\nB\nC\n"'); + $command->on('data', function($command, $data) use(&$buffer) { + $buffer .= $data; + }); + $command->on('end', function() use(&$buffer, $shell, $sim) { + $sim->expect('buffer', [ $buffer ]); + $shell->disconnect(); + }); + }); + + $ssh2->on('disconnect:shell', function(SSH2DriverInterface $shell) use($sim, $ssh2) { + $sim->expect('disconnect:shell'); + $ssh2->disconnect(); + }); + + $ssh2->on('connect', function(SSH2Interface $ssh) use($sim) { + $sim->expect('connect'); + $ssh->createDriver(SSH2::DRIVER_SHELL) + ->connect(); + }); + + $ssh2->on('disconnect', function(SSH2Interface $ssh) use($sim) { + $sim->expect('disconnect'); + $sim->done(); + }); + + $ssh2->on('error', function(SSH2Interface $ssh, $ex) use($sim) { + $sim->fail($ex->getMessage()); + }); + + $sim->onStart(function() use($ssh2) { + $ssh2->connect(); + }); + $sim->onStop(function() use($ssh2) { + $ssh2->disconnect(); + }); + }); + $sim = $sim->expect([ + [ 'connect', [] ], + [ 'connect:shell', [] ], + [ 'buffer', [ "A\r\nB\r\nC\r\n" ] ], + [ 'disconnect:shell', [] ], + [ 'disconnect', [] ] + ]); + } + + /** + * + */ + public function testSSH2_SftpDriver_IsAbleToWriteFiles() + { + $this->markTestSkipped('It seems there are problems with setting this automation on Travis.'); + + $sim = $this; + $sim = $sim->simulate(function(SimulationInterface $sim) { + $loop = $sim->getLoop(); + $auth = new SSH2Password(TEST_USER, TEST_PASSWORD); + $config = new SSH2Config(); + $ssh2 = new SSH2($auth, $config, $loop); + + $ssh2->on('connect:sftp', function(SSH2DriverInterface $sftp) use($sim) { + $sim->expect('connect:sftp'); + + $lines = [ "DAZZLE\n", "IS\n", "AWESOME!\n" ]; + $linesPointer = 0; + + $file = $sftp->open(__DIR__ . '/_Data/file_write.txt', 'w+'); + $file->write(); + $file->on('drain', function(SSH2ResourceInterface $file) use(&$lines, &$linesPointer) { + if ($linesPointer < count($lines)) { + $file->write($lines[$linesPointer++]); + } + }); + $file->on('finish', function(SSH2ResourceInterface $file) { + $file->close(); + }); + $file->on('close', function(SSH2ResourceInterface $file) use($sftp) { + $sftp->disconnect(); + }); + }); + + $ssh2->on('disconnect:sftp', function(SSH2DriverInterface $sftp) use($sim, $ssh2) { + $sim->expect('disconnect:sftp'); + $ssh2->disconnect(); + }); + + $ssh2->on('connect', function(SSH2Interface $ssh) use($sim) { + $sim->expect('connect'); + $ssh->createDriver(SSH2::DRIVER_SFTP) + ->connect(); + }); + + $ssh2->on('disconnect', function(SSH2Interface $ssh) use($sim) { + $sim->expect('disconnect'); + $sim->done(); + }); + + $ssh2->on('error', function(SSH2Interface $ssh, $ex) use($sim) { + $sim->fail($ex->getMessage()); + }); + + $sim->onStart(function() use($ssh2) { + $ssh2->connect(); + }); + $sim->onStop(function() use($ssh2) { + $ssh2->disconnect(); + }); + }); + $sim = $sim->expect([ + [ 'connect', [] ], + [ 'connect:sftp', [] ], + [ 'disconnect:sftp', [] ], + [ 'disconnect', [] ] + ]); + } + + /** + * + */ + public function testSSH2_SftpDriver_IsAbleToReadFiles() + { + $this->markTestSkipped('It seems there are problems with setting this automation on Travis.'); + + $sim = $this; + $sim = $sim->simulate(function(SimulationInterface $sim) { + $loop = $sim->getLoop(); + $auth = new SSH2Password(TEST_USER, TEST_PASSWORD); + $config = new SSH2Config(); + $ssh2 = new SSH2($auth, $config, $loop); + + $ssh2->on('connect:sftp', function(SSH2DriverInterface $sftp) use($sim) { + $sim->expect('connect:sftp'); + + $buffer = ''; + $file = $sftp->open(__DIR__ . '/_Data/file_read.txt', 'r+'); + $file->read(); + $file->on('data', function(SSH2ResourceInterface $file, $data) use(&$buffer) { + $buffer .= $data; + }); + $file->on('end', function(SSH2ResourceInterface $file) use(&$buffer, $sim) { + $sim->expect('buffer', [ $buffer ]); + $file->close(); + }); + $file->on('close', function(SSH2ResourceInterface $file) use($sftp) { + $sftp->disconnect(); + }); + }); + + $ssh2->on('disconnect:sftp', function(SSH2DriverInterface $sftp) use($sim, $ssh2) { + $sim->expect('disconnect:sftp'); + $ssh2->disconnect(); + }); + + $ssh2->on('connect', function(SSH2Interface $ssh) use($sim) { + $sim->expect('connect'); + $ssh->createDriver(SSH2::DRIVER_SFTP) + ->connect(); + }); + + $ssh2->on('disconnect', function(SSH2Interface $ssh) use($sim) { + $sim->expect('disconnect'); + $sim->done(); + }); + + $ssh2->on('error', function(SSH2Interface $ssh, $ex) use($sim) { + $sim->fail($ex->getMessage()); + }); + + $sim->onStart(function() use($ssh2) { + $ssh2->connect(); + }); + $sim->onStop(function() use($ssh2) { + $ssh2->disconnect(); + }); + }); + $sim = $sim->expect([ + [ 'connect', [] ], + [ 'connect:sftp', [] ], + [ 'buffer', [ "DAZZLE\r\nIS\r\nAWESOME\r\n" ] ], + [ 'disconnect:sftp', [] ], + [ 'disconnect', [] ] + ]); + } +} diff --git a/test/TModule/_Data/file_read.txt b/test/TModule/_Data/file_read.txt new file mode 100644 index 0000000..cb71521 --- /dev/null +++ b/test/TModule/_Data/file_read.txt @@ -0,0 +1,3 @@ +DAZZLE +IS +AWESOME diff --git a/test/TModule/_Data/file_write.txt b/test/TModule/_Data/file_write.txt new file mode 100644 index 0000000..e69de29 diff --git a/test/TUnit.php b/test/TUnit.php new file mode 100644 index 0000000..4be1045 --- /dev/null +++ b/test/TUnit.php @@ -0,0 +1,211 @@ +exactly(2); + } + + /** + * Creates a callback that must be called $amount times or the test will fail. + * + * @param $amount + * @return callable|\PHPUnit_Framework_MockObject_MockObject + */ + public function expectCallableExactly($amount) + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->exactly($amount)) + ->method('__invoke'); + + return $mock; + } + + /** + * Creates a callback that must be called once. + * + * @return callable|\PHPUnit_Framework_MockObject_MockObject + */ + public function expectCallableOnce() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->once()) + ->method('__invoke'); + + return $mock; + } + + /** + * Creates a callback that must be called twice. + * + * @return callable|\PHPUnit_Framework_MockObject_MockObject + */ + public function expectCallableTwice() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->exactly(2)) + ->method('__invoke'); + + return $mock; + } + + /** + * Creates a callable that must not be called once. + * + * @return callable|\PHPUnit_Framework_MockObject_MockObject + */ + public function expectCallableNever() + { + $mock = $this->createCallableMock(); + $mock + ->expects($this->never()) + ->method('__invoke'); + + return $mock; + } + + /** + * Creates a callable mock. + * + * @return callable|\PHPUnit_Framework_MockObject_MockObject + */ + public function createCallableMock() + { + return $this->getMock(Callback::class); + } + + /** + * @return LoopInterface|\PHPUnit_Framework_MockObject_MockObject + */ + public function createLoopMock() + { + return $this->getMock('Dazzle\Loop\LoopInterface'); + } + + /** + * @return LoopInterface|\PHPUnit_Framework_MockObject_MockObject + */ + public function createWritableLoopMock() + { + $loop = $this->createLoopMock(); + $loop + ->expects($this->once()) + ->method('addWriteStream') + ->will($this->returnCallback(function($stream, $listener) { + call_user_func($listener, $stream); + })); + + return $loop; + } + + /** + * @return LoopInterface|\PHPUnit_Framework_MockObject_MockObject + */ + public function createReadableLoopMock() + { + $loop = $this->createLoopMock(); + $loop + ->expects($this->once()) + ->method('addReadStream') + ->will($this->returnCallback(function($stream, $listener) { + call_user_func($listener, $stream); + })); + + return $loop; + } + + /** + * Check if protected property exists. + * + * @param object $object + * @param string $property + * @return bool + */ + public function existsProtectedProperty($object, $property) + { + $reflection = new ReflectionClass($object); + return $reflection->hasProperty($property); + } + + /** + * Get protected property from given object via reflection. + * + * @param object $object + * @param string $property + * @return mixed + */ + public function getProtectedProperty($object, $property) + { + $reflection = new ReflectionClass($object); + $reflection_property = $reflection->getProperty($property); + $reflection_property->setAccessible(true); + + return $reflection_property->getValue($object); + } + + /** + * Set protected property on a given object via reflection. + * + * @param object $object + * @param string $property + * @param mixed $value + * @return object + */ + public function setProtectedProperty($object, $property, $value) + { + $reflection = new ReflectionClass($object); + $reflection_property = $reflection->getProperty($property); + $reflection_property->setAccessible(true); + $reflection_property->setValue($object, $value); + + return $object; + } + + /** + * Call protected method on a given object via reflection. + * + * @param object|string $objectOrClass + * @param string $method + * @param mixed[] $args + * @return mixed + */ + public function callProtectedMethod($objectOrClass, $method, $args = []) + { + $reflection = new ReflectionClass($objectOrClass); + $reflectionMethod = $reflection->getMethod($method); + $reflectionMethod->setAccessible(true); + $reflectionTarget = is_object($objectOrClass) ? $objectOrClass : null; + + return $reflectionMethod->invokeArgs($reflectionTarget, $args); + } +} diff --git a/test/TUnit/Auth/SSH2AgentTest.php b/test/TUnit/Auth/SSH2AgentTest.php new file mode 100644 index 0000000..42c4025 --- /dev/null +++ b/test/TUnit/Auth/SSH2AgentTest.php @@ -0,0 +1,32 @@ +assertInstanceOf(SSH2AuthInterface::class, $auth); + $this->assertAttributeEquals($user, 'username', $auth); + } + + /** + * + */ + public function testDestructor_DoesNotThrowException() + { + $user = 'user'; + $auth = new SSH2Agent($user); + unset($auth); + } +} diff --git a/test/TUnit/Auth/SSH2HostBasedFileTest.php b/test/TUnit/Auth/SSH2HostBasedFileTest.php new file mode 100644 index 0000000..ac2b7fd --- /dev/null +++ b/test/TUnit/Auth/SSH2HostBasedFileTest.php @@ -0,0 +1,49 @@ +assertInstanceOf(SSH2HostBasedFile::class, $auth); + $this->assertAttributeEquals($user, 'username', $auth); + $this->assertAttributeEquals($host, 'hostname', $auth); + $this->assertAttributeEquals($publicKey, 'publicKeyFile', $auth); + $this->assertAttributeEquals($privateKey, 'privateKeyFile', $auth); + $this->assertAttributeEquals($passPhrase, 'passPhrase', $auth); + $this->assertAttributeEquals($localUser, 'localUsername', $auth); + } + + /** + * + */ + public function testDestructor_DoesNotThrowThrowable() + { + $user = 'user'; + $host = 'example.com'; + $publicKey = 'path/public.key'; + $privateKey = 'path/private.key'; + $passPhrase = 'passPhrase'; + $localUser = 'localUser'; + + $auth = new SSH2HostBasedFile($user, $host, $publicKey, $privateKey, $passPhrase, $localUser); + unset($auth); + } +} + \ No newline at end of file diff --git a/test/TUnit/Auth/SSH2NoneTest.php b/test/TUnit/Auth/SSH2NoneTest.php new file mode 100644 index 0000000..953b45e --- /dev/null +++ b/test/TUnit/Auth/SSH2NoneTest.php @@ -0,0 +1,33 @@ +assertInstanceOf(SSH2AuthInterface::class, $auth); + $this->assertAttributeEquals($user, 'username', $auth); + } + + /** + * + */ + public function testDestructor_DoesNotThrowException() + { + $user = 'user'; + $auth = new SSH2None($user); + unset($auth); + } +} + \ No newline at end of file diff --git a/test/TUnit/Auth/SSH2PasswordTest.php b/test/TUnit/Auth/SSH2PasswordTest.php new file mode 100644 index 0000000..004ce6b --- /dev/null +++ b/test/TUnit/Auth/SSH2PasswordTest.php @@ -0,0 +1,36 @@ +assertInstanceOf(SSH2AuthInterface::class, $auth); + $this->assertAttributeEquals($user, 'username', $auth); + $this->assertAttributeEquals($pass, 'password', $auth); + } + + /** + * + */ + public function testDestructor_DoesNotThrowException() + { + $user = 'user'; + $pass = 'pass'; + $auth = new SSH2Password($user, $pass); + unset($auth); + } +} + \ No newline at end of file diff --git a/test/TUnit/Auth/SSH2PublicKeyFileTest.php b/test/TUnit/Auth/SSH2PublicKeyFileTest.php new file mode 100644 index 0000000..bf7f388 --- /dev/null +++ b/test/TUnit/Auth/SSH2PublicKeyFileTest.php @@ -0,0 +1,44 @@ +assertInstanceOf(SSH2AuthInterface::class, $auth); + $this->assertAttributeEquals($user, 'username', $auth); + $this->assertAttributeEquals($publicKey, 'publicKeyFile', $auth); + $this->assertAttributeEquals($privateKey, 'privateKeyFile', $auth); + $this->assertAttributeEquals($passPhrase, 'passPhrase', $auth); + } + + /** + * + */ + public function testDestructor_DoesNotThrowException() + { + $user = 'user'; + $publicKey = 'path/public.key'; + $privateKey = 'path/private.key'; + $passPhrase = 'passPhrase'; + + $auth = new SSH2PublicKeyFile($user, $publicKey, $privateKey, $passPhrase); + unset($auth); + } +} + \ No newline at end of file diff --git a/test/TUnit/Driver/SftpTest.php b/test/TUnit/Driver/SftpTest.php new file mode 100644 index 0000000..33c17eb --- /dev/null +++ b/test/TUnit/Driver/SftpTest.php @@ -0,0 +1,355 @@ +createDriver(); + $this->assertInstanceOf(SSH2DriverInterface::class, $driver); + } + + /** + * + */ + public function testDestructor_DoesNotThrowThrowable() + { + $driver = $this->createDriver(); + unset($driver); + } + + /** + * + */ + public function testApiGetName_ReturnsSftpName() + { + $driver = $this->createDriver(); + + $this->assertSame(SSH2::DRIVER_SFTP, $driver->getName()); + } + + /** + * + */ + public function testApiConnect_DoesNothing_WhenConnectionIsEstablished() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r')); + + $driver->on('error', $this->expectCallableNever()); + $driver->on('connect', $this->expectCallableNever()); + + $driver->connect(); + } + + /** + * + */ + public function testApiConnect_EmitsErrorEvent_WhenConnectionCouldNotBeEstablished_AndReturnedFalse() + { + $driver = $this->createDriver([ 'createConnection' ]); + $driver + ->expects($this->once()) + ->method('createConnection') + ->will($this->returnValue(false)); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($driver, $this->isInstanceOf(ExecutionException::class)); + + $driver->on('error', $callback); + $driver->on('connect', $this->expectCallableNever()); + + $driver->connect(); + } + + /** + * + */ + public function testApiConnect_EmitsErrorEvent_WhenConnectionCouldNotBeEstablished_AndReturnedNotResource() + { + $driver = $this->createDriver([ 'createConnection' ]); + $driver + ->expects($this->once()) + ->method('createConnection') + ->will($this->returnValue(true)); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($driver, $this->isInstanceOf(ExecutionException::class)); + + $driver->on('error', $callback); + $driver->on('connect', $this->expectCallableNever()); + + $driver->connect(); + } + + /** + * + */ + public function testApiConnect_EmitsConnectEvent() + { + $stream = fopen('php://memory', 'r'); + + $driver = $this->createDriver([ 'createConnection' ]); + $driver + ->expects($this->once()) + ->method('createConnection') + ->will($this->returnValue($stream)); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($driver); + + $driver->on('error', $this->expectCallableNever()); + $driver->on('connect', $callback); + + $driver->connect(); + } + + /** + * + */ + public function testApiDisconnect_DoesNothing_WhenConnectionIsNull() + { + $driver = $this->createDriver(); + + $driver->on('disconnect', $this->expectCallableNever()); + + $driver->disconnect(); + } + + /** + * + */ + public function testApiDisconnect_DoesNothing_WhenConnectionIsNotResource() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'resource', true); + + $driver->on('disconnect', $this->expectCallableNever()); + + $driver->disconnect(); + } + + /** + * + */ + public function testApiDisconnect_EmitsDisconnectEvent() + { + $driver = $this->createDriver([ 'pause', 'handleDisconnect' ]); + $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r+')); + + $driver + ->expects($this->once()) + ->method('pause'); + $driver + ->expects($this->once()) + ->method('handleDisconnect'); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($driver); + + $driver->on('disconnect', $callback); + + $driver->disconnect(); + } + + /** + * + */ + public function testApiDisconnect_ClosesEachResource() + { + $driver = $this->createDriver([ 'pause', 'handleDisconnect' ]); + + $resource1 = $this->getMock(SftpResource::class, [], [], '', false); + $resource1 + ->expects($this->once()) + ->method('close'); + + $resource2 = $this->getMock(SftpResource::class, [], [], '', false); + $resource1 + ->expects($this->once()) + ->method('close'); + + $resources = [ $resource1, $resource2 ]; + + $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r+')); + $this->setProtectedProperty($driver, 'resources', $resources); + + $driver + ->expects($this->once()) + ->method('pause'); + $driver + ->expects($this->once()) + ->method('handleDisconnect'); + + $driver->disconnect(); + } + + /** + * + */ + public function testApiIsConnected_ReturnsFalse_WhenConnectionIsNull() + { + $driver = $this->createDriver(); + + $this->assertFalse($driver->isConnected()); + } + + /** + * + */ + public function testApiIsConnected_ReturnsFalse_WhenConnectionIsNotResource() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'resource', true); + + $this->assertFalse($driver->isConnected()); + } + + /** + * + */ + public function testApiIsConnected_ReturnsTrue_WhenConnectionIsResource() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r')); + + $this->assertTrue($driver->isConnected()); + } + + /** + * + */ + public function testApiResume_DoesNothing_WhenDriverIsNotPaused() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'paused', false); + + $driver->resume(); + + $this->assertFalse($driver->isPaused()); + } + + /** + * + */ + public function testApiResume_ResumesDriver_WhenDriverIsPaused() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'paused', true); + + $driver->resume(); + + $this->assertFalse($driver->isPaused()); + } + + /** + * + */ + public function testApiPause_PausesDriver_WhenDriverIsNotPaused() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'paused', false); + + $driver->pause(); + + $this->assertTrue($driver->isPaused()); + } + + /** + * + */ + public function testApiPause_DoesNothing_WhenDriverIsPaused() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'paused', true); + + $driver->pause(); + + $this->assertTrue($driver->isPaused()); + } + + /** + * + */ + public function testApiIsPaused_ReturnsFalse_WhenDriverIsNotPaused() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'paused', false); + + $this->assertFalse($driver->isPaused()); + } + + /** + * + */ + public function testApiIsPaused_ReturnsTrue_WhenDriverIsPaused() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'paused', true); + + $this->assertTrue($driver->isPaused()); + } + + /** + * Create Sftp driver. + * + * @param string[] $methods + * @param mixed + * @return Sftp|\PHPUnit_Framework_MockObject_MockObject + */ + public function createDriver($methods = null, $constructorParams = []) + { + if (isset($constructorParams['ssh2'])) + { + $ssh2 = $constructorParams['ssh2']; + } + else + { + $timer = $this->getMock(TimerInterface::class, [], [], '', false); + $loop = $this->getMock(Loop::class, [ 'addPeriodicTimer' ], [], '', false); + $loop + ->expects($this->any()) + ->method('addPeriodicTimer') + ->will($this->returnValue($timer)); + + $ssh2 = $this->getMock(SSH2Interface::class, [], [], '', false); + $ssh2 + ->expects($this->any()) + ->method('getLoop') + ->will($this->returnValue($loop)); + } + + $conn = isset($constructorParams['conn']) ? $constructorParams['conn'] : fopen('php://memory', 'r+'); + + $mock = $this->getMock(Sftp::class, $methods, [ $ssh2, $conn ]); + + return $mock; + } +} diff --git a/test/TUnit/Driver/ShellTest.php b/test/TUnit/Driver/ShellTest.php new file mode 100644 index 0000000..7c5e251 --- /dev/null +++ b/test/TUnit/Driver/ShellTest.php @@ -0,0 +1,355 @@ +createDriver(); + $this->assertInstanceOf(SSH2DriverInterface::class, $driver); + } + + /** + * + */ + public function testDestructor_DoesNotThrowThrowable() + { + $driver = $this->createDriver(); + unset($driver); + } + + /** + * + */ + public function testApiGetName_ReturnsShellName() + { + $driver = $this->createDriver(); + + $this->assertSame(SSH2::DRIVER_SHELL, $driver->getName()); + } + + /** + * + */ + public function testApiConnect_DoesNothing_WhenConnectionIsEstablished() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r')); + + $driver->on('error', $this->expectCallableNever()); + $driver->on('connect', $this->expectCallableNever()); + + $driver->connect(); + } + + /** + * + */ + public function testApiConnect_EmitsErrorEvent_WhenConnectionCouldNotBeEstablished_AndReturnedFalse() + { + $driver = $this->createDriver([ 'createConnection' ]); + $driver + ->expects($this->once()) + ->method('createConnection') + ->will($this->returnValue(false)); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($driver, $this->isInstanceOf(ExecutionException::class)); + + $driver->on('error', $callback); + $driver->on('connect', $this->expectCallableNever()); + + $driver->connect(); + } + + /** + * + */ + public function testApiConnect_EmitsErrorEvent_WhenConnectionCouldNotBeEstablished_AndReturnedNotResource() + { + $driver = $this->createDriver([ 'createConnection' ]); + $driver + ->expects($this->once()) + ->method('createConnection') + ->will($this->returnValue(true)); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($driver, $this->isInstanceOf(ExecutionException::class)); + + $driver->on('error', $callback); + $driver->on('connect', $this->expectCallableNever()); + + $driver->connect(); + } + + /** + * + */ + public function testApiConnect_EmitsConnectEvent() + { + $stream = fopen('php://memory', 'r'); + + $driver = $this->createDriver([ 'createConnection' ]); + $driver + ->expects($this->once()) + ->method('createConnection') + ->will($this->returnValue($stream)); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($driver); + + $driver->on('error', $this->expectCallableNever()); + $driver->on('connect', $callback); + + $driver->connect(); + } + + /** + * + */ + public function testApiDisconnect_DoesNothing_WhenConnectionIsNull() + { + $driver = $this->createDriver(); + + $driver->on('disconnect', $this->expectCallableNever()); + + $driver->disconnect(); + } + + /** + * + */ + public function testApiDisconnect_DoesNothing_WhenConnectionIsNotResource() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'resource', true); + + $driver->on('disconnect', $this->expectCallableNever()); + + $driver->disconnect(); + } + + /** + * + */ + public function testApiDisconnect_EmitsDisconnectEvent() + { + $driver = $this->createDriver([ 'pause', 'handleDisconnect' ]); + $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r+')); + + $driver + ->expects($this->once()) + ->method('pause'); + $driver + ->expects($this->once()) + ->method('handleDisconnect'); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($driver); + + $driver->on('disconnect', $callback); + + $driver->disconnect(); + } + + /** + * + */ + public function testApiDisconnect_ClosesEachResource() + { + $driver = $this->createDriver([ 'pause', 'handleDisconnect' ]); + + $resource1 = $this->getMock(ShellResource::class, [], [], '', false); + $resource1 + ->expects($this->once()) + ->method('close'); + + $resource2 = $this->getMock(ShellResource::class, [], [], '', false); + $resource1 + ->expects($this->once()) + ->method('close'); + + $resources = [ $resource1, $resource2 ]; + + $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r+')); + $this->setProtectedProperty($driver, 'resources', $resources); + + $driver + ->expects($this->once()) + ->method('pause'); + $driver + ->expects($this->once()) + ->method('handleDisconnect'); + + $driver->disconnect(); + } + + /** + * + */ + public function testApiIsConnected_ReturnsFalse_WhenConnectionIsNull() + { + $driver = $this->createDriver(); + + $this->assertFalse($driver->isConnected()); + } + + /** + * + */ + public function testApiIsConnected_ReturnsFalse_WhenConnectionIsNotResource() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'resource', true); + + $this->assertFalse($driver->isConnected()); + } + + /** + * + */ + public function testApiIsConnected_ReturnsTrue_WhenConnectionIsResource() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'resource', $stream = fopen('php://memory', 'r')); + + $this->assertTrue($driver->isConnected()); + } + + /** + * + */ + public function testApiResume_DoesNothing_WhenDriverIsNotPaused() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'paused', false); + + $driver->resume(); + + $this->assertFalse($driver->isPaused()); + } + + /** + * + */ + public function testApiResume_ResumesDriver_WhenDriverIsPaused() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'paused', true); + + $driver->resume(); + + $this->assertFalse($driver->isPaused()); + } + + /** + * + */ + public function testApiPause_PausesDriver_WhenDriverIsNotPaused() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'paused', false); + + $driver->pause(); + + $this->assertTrue($driver->isPaused()); + } + + /** + * + */ + public function testApiPause_DoesNothing_WhenDriverIsPaused() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'paused', true); + + $driver->pause(); + + $this->assertTrue($driver->isPaused()); + } + + /** + * + */ + public function testApiIsPaused_ReturnsFalse_WhenDriverIsNotPaused() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'paused', false); + + $this->assertFalse($driver->isPaused()); + } + + /** + * + */ + public function testApiIsPaused_ReturnsTrue_WhenDriverIsPaused() + { + $driver = $this->createDriver(); + $this->setProtectedProperty($driver, 'paused', true); + + $this->assertTrue($driver->isPaused()); + } + + /** + * Create Shell driver. + * + * @param string[] $methods + * @param mixed + * @return Shell|\PHPUnit_Framework_MockObject_MockObject + */ + public function createDriver($methods = null, $constructorParams = []) + { + if (isset($constructorParams['ssh2'])) + { + $ssh2 = $constructorParams['ssh2']; + } + else + { + $timer = $this->getMock(TimerInterface::class, [], [], '', false); + $loop = $this->getMock(Loop::class, [ 'addPeriodicTimer' ], [], '', false); + $loop + ->expects($this->any()) + ->method('addPeriodicTimer') + ->will($this->returnValue($timer)); + + $ssh2 = $this->getMock(SSH2Interface::class, [], [], '', false); + $ssh2 + ->expects($this->any()) + ->method('getLoop') + ->will($this->returnValue($loop)); + } + + $conn = isset($constructorParams['conn']) ? $constructorParams['conn'] : fopen('php://memory', 'r+'); + + $mock = $this->getMock(Shell::class, $methods, [ $ssh2, $conn ]); + + return $mock; + } +} diff --git a/test/TUnit/SSH2ConfigTest.php b/test/TUnit/SSH2ConfigTest.php new file mode 100644 index 0000000..a56cd57 --- /dev/null +++ b/test/TUnit/SSH2ConfigTest.php @@ -0,0 +1,69 @@ +assertAttributeEquals('localhost', 'host', $config); + $this->assertAttributeEquals(22, 'port', $config); + $this->assertAttributeEquals([], 'methods', $config); + } + + /** + * + */ + public function testConstructor_UsesPassedArguments() + { + $config = new SSH2Config($host = 'A', $port = 50, $methods = [ 'method' => 'option' ]); + + $this->assertAttributeEquals($host, 'host', $config); + $this->assertAttributeEquals($port, 'port', $config); + $this->assertAttributeEquals($methods, 'methods', $config); + } + + /** + * + */ + public function testDestructor_DoesNotThrowThrowable() + { + $config = new SSH2Config(); + unset($config); + } + + /** + * + */ + public function testApiGetHost_ReturnsHost() + { + $config = new SSH2Config($host = 'A'); + $this->assertSame($host, $config->getHost()); + } + + /** + * + */ + public function testApiGetPort_ReturnsPort() + { + $config = new SSH2Config('A', $port = 50); + $this->assertSame($port, $config->getPort()); + } + + /** + * + */ + public function testApiGetMethods_ReturnsMethods() + { + $config = new SSH2Config('A', 50, $methods = [ 'something' => 'value' ]); + $this->assertSame($methods, $config->getMethods()); + } +} diff --git a/test/TUnit/SSH2Test.php b/test/TUnit/SSH2Test.php new file mode 100644 index 0000000..baad57d --- /dev/null +++ b/test/TUnit/SSH2Test.php @@ -0,0 +1,374 @@ +createSSH2(); + $this->assertInstanceOf(SSH2Interface::class, $ssh2); + } + + /** + * + */ + public function testDestructor_DoesNotThrowThrowable() + { + $ssh2 = $this->createSSH2(); + unset($ssh2); + } + + /** + * + */ + public function testApiConnect_DoesNothing_WhenConnectionIsNotNull() + { + $ssh2 = $this->createSSH2(); + $this->setProtectedProperty($ssh2, 'conn', $stream = fopen('php://memory', 'r')); + + $ssh2->on('error', $this->expectCallableNever()); + $ssh2->on('connect', $this->expectCallableNever()); + + $ssh2->connect(); + } + + /** + * + */ + public function testApiConnect_EmitsErrorEvent_WhenConnectionCouldNotBeEstablished_AndReturnedFalse() + { + $ssh2 = $this->createSSH2([ 'createConnection' ]); + $ssh2 + ->expects($this->once()) + ->method('createConnection') + ->will($this->returnValue(false)); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($ssh2, $this->isInstanceOf(ExecutionException::class)); + + $ssh2->on('error', $callback); + $ssh2->on('connect', $this->expectCallableNever()); + + $ssh2->connect(); + } + + /** + * + */ + public function testApiConnect_EmitsErrorEvent_WhenConnectionCouldNotBeEstablished_AndReturnedNonResource() + { + $ssh2 = $this->createSSH2([ 'createConnection' ]); + $ssh2 + ->expects($this->once()) + ->method('createConnection') + ->will($this->returnValue(true)); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($ssh2, $this->isInstanceOf(ExecutionException::class)); + + $ssh2->on('error', $callback); + $ssh2->on('connect', $this->expectCallableNever()); + + $ssh2->connect(); + } + + /** + * + */ + public function testApiConnect_EmitsErrorEvent_WhenAuthenticationIsInvalid() + { + $stream = fopen('php://memory', 'r'); + + $auth = $this->getMock(SSH2AuthInterface::class, [ 'authenticate' ], [], '', false); + $auth + ->expects($this->once()) + ->method('authenticate') + ->will($this->returnValue(false)); + + $ssh2 = $this->createSSH2([ 'createConnection' ], [ 'auth' => $auth ]); + $ssh2 + ->expects($this->once()) + ->method('createConnection') + ->will($this->returnValue($stream)); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($ssh2, $this->isInstanceOf(ExecutionException::class)); + + $ssh2->on('error', $callback); + $ssh2->on('connect', $this->expectCallableNever()); + + $ssh2->connect(); + } + + /** + * + */ + public function testApiConnect_EmitsConnectEvent() + { + $stream = fopen('php://memory', 'r'); + + $auth = $this->getMock(SSH2AuthInterface::class, [ 'authenticate' ], [], '', false); + $auth + ->expects($this->once()) + ->method('authenticate') + ->will($this->returnValue(true)); + + $ssh2 = $this->createSSH2([ 'createConnection' ], [ 'auth' => $auth ]); + $ssh2 + ->expects($this->once()) + ->method('createConnection') + ->will($this->returnValue($stream)); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($ssh2); + + $ssh2->on('error', $this->expectCallableNever()); + $ssh2->on('connect', $callback); + + $ssh2->connect(); + } + + /** + * + */ + public function testApiDisconnect_DoesNothing_WhenConnectionIsNull() + { + $ssh2 = $this->createSSH2(); + + $ssh2->on('disconnect', $this->expectCallableNever()); + + $ssh2->disconnect(); + } + + /** + * + */ + public function testApiDisconnect_DoesNothing_WhenConnectionIsNotResource() + { + $ssh2 = $this->createSSH2(); + $this->setProtectedProperty($ssh2, 'conn', true); + + $ssh2->on('disconnect', $this->expectCallableNever()); + + $ssh2->disconnect(); + } + + /** + * + */ + public function testApiDisconnect_EmitsDisconnectEvent() + { + $ssh2 = $this->createSSH2(); + $this->setProtectedProperty($ssh2, 'conn', $stream = fopen('php://memory', 'r')); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($ssh2); + + $ssh2->on('disconnect', $callback); + + $ssh2->disconnect(); + } + + /** + * + */ + public function testApiDisconnect_CallsDisconnectMethodAndRemovesListeners_OnEachDriver() + { + $ssh2 = $this->createSSH2(); + + $stream = fopen('php://memory', 'r'); + + $driver1 = $this->getMock(SSH2DriverInterface::class, [], [], '', false); + $driver1 + ->expects($this->once()) + ->method('disconnect'); + $driver1 + ->expects($this->exactly(3)) + ->method('removeListener') + ->with($this->isType('string'), $this->isType('callable')); + + $driver2 = $this->getMock(SSH2DriverInterface::class, [], [], '', false); + $driver2 + ->expects($this->once()) + ->method('disconnect'); + $driver2 + ->expects($this->exactly(3)) + ->method('removeListener') + ->with($this->isType('string'), $this->isType('callable')); + + $drivers = [ 'd1' => $driver1, 'd2' => $driver2 ]; + + $this->setProtectedProperty($ssh2, 'conn', $stream); + $this->setProtectedProperty($ssh2, 'drivers', $drivers); + + $callback = $this->createCallableMock(); + $callback + ->expects($this->once()) + ->method('__invoke') + ->with($ssh2); + + $ssh2->on('disconnect', $callback); + + $ssh2->disconnect(); + } + + /** + * + */ + public function testApiIsConnected_ReturnsFalse_WhenConnectionIsNull() + { + $ssh2 = $this->createSSH2(); + + $this->assertFalse($ssh2->isConnected()); + } + + /** + * + */ + public function testApiIsConnected_ReturnsFalse_WhenConnectionIsNotResource() + { + $ssh2 = $this->createSSH2(); + $this->setProtectedProperty($ssh2, 'conn', true); + + $this->assertFalse($ssh2->isConnected()); + } + + /** + * + */ + public function testApiIsConnected_ReturnsTrue_WhenConnectionIsResource() + { + $ssh2 = $this->createSSH2(); + $this->setProtectedProperty($ssh2, 'conn', $stream = fopen('php://memory', 'r')); + + $this->assertTrue($ssh2->isConnected()); + } + + /** + * + */ + public function testApiCreateDriver_ReturnsDriver_WhenDriverDoesExist() + { + $ssh2 = $this->createSSH2(); + + $driver = $this->getMock(SSH2DriverInterface::class, [], [], '', false); + $drivers = [ 'name' => $driver ]; + + $this->setProtectedProperty($ssh2, 'drivers', $drivers); + + $this->assertSame($driver, $ssh2->createDriver('name')); + } + + /** + * + */ + public function testApiCreateDriver_ThrowsException_WhenConnectionIsNotEstablished() + { + $ssh2 = $this->createSSH2(); + + $this->setExpectedException(ExecutionException::class); + $ssh2->createDriver('name'); + } + + /** + * + */ + public function testApiCreateDriver_ThrowsException_WhenInvalidDriverIsRequested() + { + $ssh2 = $this->createSSH2([ 'isConnected' ]); + $ssh2 + ->expects($this->once()) + ->method('isConnected') + ->will($this->returnValue(true)); + + $this->setExpectedException(InvalidArgumentException::class); + $ssh2->createDriver('invalidDriver'); + } + + /** + * + */ + public function testApiCreateDriver_CreatesDriver_WhenShellIsRequested() + { + $ssh2 = $this->createSSH2([ 'isConnected' ]); + $ssh2 + ->expects($this->once()) + ->method('isConnected') + ->will($this->returnValue(true)); + + $driver = $ssh2->createDriver(SSH2::DRIVER_SHELL); + + $this->assertInstanceOf(SSH2DriverInterface::class, $driver); + $this->assertInstanceOf(Shell::class, $driver); + } + + /** + * + */ + public function testApiCreateDriver_CreatesDriver_WhenSftpIsRequested() + { + $ssh2 = $this->createSSH2([ 'isConnected' ]); + $ssh2 + ->expects($this->once()) + ->method('isConnected') + ->will($this->returnValue(true)); + + $driver = $ssh2->createDriver(SSH2::DRIVER_SFTP); + + $this->assertInstanceOf(SSH2DriverInterface::class, $driver); + $this->assertInstanceOf(Sftp::class, $driver); + } + + /** + * Create SSH2 driver. + * + * @param string[]|null $methods + * @param mixed + * @return SSH2|\PHPUnit_Framework_MockObject_MockObject + */ + public function createSSH2($methods = null, $constructorParams = []) + { + $loop = isset($constructorParams['loop']) + ? $constructorParams['loop'] + : $this->getMock(LoopInterface::class, [], [], '', false); + + $auth = isset($constructorParams['auth']) + ? $constructorParams['auth'] + : $this->getMock(SSH2AuthInterface::class, [], [], '', false); + + $config = new SSH2Config(); + + return $this->getMock(SSH2::class, $methods, [ $auth, $config, $loop ]); + } +} diff --git a/test/_Simulation/Event.php b/test/_Simulation/Event.php new file mode 100644 index 0000000..3e61ae4 --- /dev/null +++ b/test/_Simulation/Event.php @@ -0,0 +1,42 @@ +name = $name; + $this->data = $data; + } + + /** + * @return mixed + */ + public function name() + { + return $this->name; + } + + /** + * @return mixed[] + */ + public function data() + { + return $this->data; + } +} diff --git a/test/_Simulation/EventCollection.php b/test/_Simulation/EventCollection.php new file mode 100644 index 0000000..ceb25c7 --- /dev/null +++ b/test/_Simulation/EventCollection.php @@ -0,0 +1,8 @@ +loop = $loop; + $this->scenario = function() {}; + $this->events = []; + $this->failureMessage = null; + $this->startCallback = function() {}; + $this->stopCallback = function() {}; + $this->stopFlags = false; + } + + /** + * + */ + public function __destruct() + { + $this->stop(); + + unset($loop); + unset($this->scenario); + unset($this->events); + unset($this->failureMessage); + unset($this->startCallback); + unset($this->stopCallback); + unset($this->stopFlags); + } + + /** + * @param callable(SimulationInterface) $scenario + */ + public function setScenario(callable $scenario) + { + $this->scenario = $scenario; + } + + /** + * @return callable(SimulationInterface)|null $scenario + */ + public function getScenario() + { + return $this->scenario; + } + + /** + * @return LoopInterface + */ + public function getLoop() + { + return $this->loop; + } + + /** + * + */ + public function begin() + { + $this->start(); + } + + /** + * + */ + public function done() + { + $this->stop(); + } + + /** + * + */ + public function fail($message) + { + $this->failureMessage = $message; + $this->stop(); + } + + /** + * @param mixed $expected + * @param mixed $actual + * @param string $message + */ + public function assertSame($expected, $actual, $message = "Assertion failed, expected \"%s\" got \"%s\"") + { + if ($expected !== $actual) + { + $stringExpected = (is_object($expected) || is_array($expected)) ? json_encode($expected) : (string) $expected; + $stringActual = (is_object($actual) || is_array($actual)) ? json_encode($actual) : (string) $actual; + + $this->fail(sprintf($message, $stringExpected, $stringActual)); + } + } + + /** + * @param string $name + * @param mixed $data + */ + public function expect($name, $data = []) + { + $this->events[] = new Event($name, $data); + } + + /** + * @return Event[] + */ + public function getExpectations() + { + return $this->events; + } + + /** + * @param callable $callable + */ + public function onStart(callable $callable) + { + $this->startCallback = $callable; + } + + /** + * @param callable $callable + */ + public function onStop(callable $callable) + { + $this->stopCallback = $callable; + } + + /** + * @param string $model + * @param mixed[] $config + * @return object + */ + public function reflect($model, $config = []) + { + foreach ($config as $key=>$value) + { + if ($value === 'Dazzle\Loop\Loop' || $value === 'Dazzle\Loop\LoopInterface') + { + $config[$key] = $this->getLoop(); + } + } + + return (new ReflectionClass($model))->newInstanceArgs($config); + } + + /** + * @throws Exception + */ + private function start() + { + $sim = $this; + + $scenario = $this->scenario; + $scenario($sim); + + if ($this->stopFlags === true) + { + return; + } + + $onStart = $this->startCallback; + $loop = $this->loop; + + $loop->onStart(function() use($sim, $onStart) { + $onStart($sim); + }); + $loop->addTimer(5, function() use($sim) { + $sim->fail('Timeout for test has been reached.'); + }); + + $loop->start(); + + if ($sim->failureMessage !== null) + { + throw new Exception($sim->failureMessage); + } + } + + /** + * + */ + private function stop() + { + if ($this->loop !== null && $this->loop->isRunning()) + { + $this->loop->stop(); + } + + if ($this->stopFlags === false) + { + $callable = $this->stopCallback; + $callable($this); + } + + $this->stopFlags = true; + } +} diff --git a/test/_Simulation/SimulationInterface.php b/test/_Simulation/SimulationInterface.php new file mode 100644 index 0000000..c22d5e5 --- /dev/null +++ b/test/_Simulation/SimulationInterface.php @@ -0,0 +1,55 @@ +