diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..314fa3f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,40 @@ +sudo: required + +language: php +php: 7.0 + +env: + global: + - TEST_COMMAND=$(echo $TRAVIS_REPO_SLUG | cut -d/ -f 2) # Get command name to be tested + +before_script: + - sudo curl -L https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose + - | + # Remove Xdebug for a huge performance increase: + if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then + phpenv config-rm xdebug.ini + else + echo "xdebug.ini does not exist" + fi + - ./ci/prepare.sh + +script: + - cd "$TRAVIS_BUILD_DIR/../easyengine" + - sudo ./vendor/bin/behat + +after_script: + - cat /opt/easyengine/logs/ee.log + +cache: + directories: + - $HOME/.composer/cache + +notifications: + email: + on_success: never + on_failure: change + +addons: + apt: + packages: + - docker-ce diff --git a/ci/prepare.sh b/ci/prepare.sh new file mode 100755 index 0000000..9c4387e --- /dev/null +++ b/ci/prepare.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# called by Travis CI + +# install dependencies +wget -qO ee https://rt.cx/ee4beta && sudo bash ee +rm ee + +# Setup EE develop repo +cd .. +git clone https://github.com/EasyEngine/easyengine.git easyengine --depth=1 +cd easyengine + +# Copy tests to EE repo +rm -r features +cp -R ../$TEST_COMMAND/features . + +# Update repo branches +if [[ "$TRAVIS_BRANCH" != "master" ]]; then + sed -i 's/\(easyengine\/.*\):\ \".*\"/\1:\ \"dev-develop\"/' composer.json +fi + +# Install composer dependencies and update them for tests +composer update + +# Place the command inside EE repo +sudo rm -rf vendor/easyengine/$TEST_COMMAND +cp -R ../$TEST_COMMAND vendor/easyengine/ + +# Create phar and test it +php -dphar.readonly=0 ./utils/make-phar.php easyengine.phar --quite > /dev/null +sudo php easyengine.phar cli info \ No newline at end of file diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php new file mode 100644 index 0000000..be1c100 --- /dev/null +++ b/features/bootstrap/FeatureContext.php @@ -0,0 +1,288 @@ +init_logger(); +/* End. Loading required files to enable EE::launch() in tests. */ + +use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\AfterFeatureScope; +use Behat\Behat\Hook\Scope\AfterScenarioScope; + +use Behat\Gherkin\Node\PyStringNode, + Behat\Gherkin\Node\TableNode; + +define( 'EE_SITE_ROOT', EE_ROOT_DIR . '/sites' ); + +class FeatureContext implements Context +{ + public $command; + public $webroot_path; + public $ee_path; + + /** + * Initializes context. + */ + public function __construct() + { + $this->commands = []; + $this->ee_path = getcwd(); + } + + /** + * @Given ee phar is generated + */ + public function eePharIsPresent() + { + // Checks if phar already exists, replaces it + if( file_exists( 'ee-old.phar' ) ) { + // Below exec call is required as currenly `ee cli update` is ran with root + // which updates ee.phar with root privileges. + exec( "sudo rm ee.phar" ); + copy( 'ee-old.phar','ee.phar' ); + return 0; + } + exec( "php -dphar.readonly=0 utils/make-phar.php ee.phar", $output, $return_status ); + if ( 0 !== $return_status ) { + throw new Exception( "Unable to generate phar" . $return_status ); + } + + // Cache generaed phar as it is expensive to generate one + copy( 'ee.phar','ee-old.phar' ); + } + + /** + * @Given :command is installed + */ + public function isInstalled( $command ) + { + exec( "type " . $command, $output, $return_status ); + if ( 0 !== $return_status ) { + throw new Exception( $command . " is not installed! Exit code is:" . $return_status ); + } + } + + /** + * @When /I run '(.*)'|"(.*)"/ + */ + public function iRun( $command ) + { + $this->commands[] = EE::launch($command, false, true); + } + + /** + * @When I try :command + */ + public function iTry( $command ) + { + $this->commands[] = EE::launch($command, false, true); + } + + /** + * @Then After delay of :time seconds + */ + public function afterDelayOfSeconds( $time ) + { + sleep( $time ); + } + + /** + * @Then /(STDOUT|STDERR) should return exactly/ + */ + public function stdoutShouldReturnExactly( $output_stream, PyStringNode $expected_output ) + { + $command_output = $output_stream === "STDOUT" ? $this->commands[0]->stdout : $this->commands[0]->stderr; + + $command_output = str_replace(["\033[1;31m","\033[0m"],'',$command_output); + + if ( $expected_output->getRaw() !== trim($command_output)) { + throw new Exception("Actual output is:\n" . $command_output); + } + } + + /** + * @Then /(STDOUT|STDERR) should return something like/ + */ + public function stdoutShouldReturnSomethingLike( $output_stream, PyStringNode $expected_output ) + { + $command_output = $output_stream === "STDOUT" ? $this->commands[0]->stdout : $this->commands[0]->stderr; + + $expected_out = isset( $expected_output->getStrings()[0] ) ? $expected_output->getStrings()[0] : ''; + if ( strpos( $command_output, $expected_out ) === false ) { + throw new Exception( "Actual output is:\n" . $command_output ); + } + } + + /** + * @Then The ee should have admin-tools directory in root + */ + public function theEEShouldHaveToolsDir() + { + if ( ! file_exists( EE_ROOT_DIR . '/admin-tools' ) ) { + throw new Exception( "The admin-tools directory has not been created!" ); + } + } + + /** + * @Then The admin-tools should have index file + */ + public function theAdminToolsShouldHaveIndexFile() + { + if ( ! file_exists( EE_ROOT_DIR . '/admin-tools/index.php' ) ) { + throw new Exception( "Admin Tools data not found!" ); + } + } + + /** + * @Then The site :site should have index file + */ + public function theSiteShouldHaveIndexFile( $site ) + { + if ( ! file_exists( EE_SITE_ROOT . '/' . $site . "/app/htdocs/index.php" ) ) { + throw new Exception( "PHP site data not found!" ); + } + } + + /** + * @Then The site :site should have WordPress + */ + public function theSiteShouldHaveWordpress($site) + { + if ( ! file_exists( EE_SITE_ROOT . '/' . $site . "/app/wp-config.php" ) ) { + throw new Exception("WordPress data not found!"); + } + } + + /** + * @Then Request on :site should contain following headers: + */ + public function requestOnShouldContainFollowingHeaders( $site, TableNode $table ) + { + $url = 'http://' . $site; + + $ch = curl_init(); + + curl_setopt( $ch, CURLOPT_URL, $url ); + curl_setopt( $ch, CURLOPT_HEADER, true ); + curl_setopt( $ch, CURLOPT_NOBODY, true ); + curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); + curl_setopt( $ch, CURLOPT_VERBOSE, true ); + + $headers = curl_exec( $ch ); + + curl_close($ch); + + $rows = $table->getHash(); + + foreach ( $rows as $row ) { + if ( strpos( $headers, $row['header'] ) === false ) { + throw new Exception( "Unable to find " . $row['header'] . "\nActual output is : " . $headers ); + } + } + } + + /** + * @Then Auth request on :site with user :user and password :pass should contain following headers: + */ + public function authRequestOnShouldContainFollowingHeaders( $site, $user, $pass, TableNode $table ) + { + $url = 'http://' . $site; + + $ch = curl_init(); + + curl_setopt( $ch, CURLOPT_URL, $url ); + curl_setopt( $ch, CURLOPT_HEADER, true ); + curl_setopt( $ch, CURLOPT_NOBODY, true ); + curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); + curl_setopt( $ch, CURLOPT_VERBOSE, true ); + + curl_setopt( $ch, CURLOPT_USERPWD, $user . ':' . $pass ); + + $headers = curl_exec( $ch ); + + curl_close($ch); + + $rows = $table->getHash(); + + foreach ( $rows as $row ) { + if ( strpos( $headers, $row['header'] ) === false ) { + throw new Exception( "Unable to find " . $row['header'] . "\nActual output is : " . $headers ); + } + } + } + + /** + * @AfterScenario + */ + public function cleanupScenario( AfterScenarioScope $scope ) + { + $this->commands = []; + chdir( $this->ee_path ); + } + + /** + * @AfterFeature + */ + public static function cleanup( AfterFeatureScope $scope ) + { + $test_sites = [ + 'php.test', + 'wp.test' + ]; + + $result = EE::launch( 'sudo bin/ee site list --format=text', false, true ); + $running_sites = explode( "\n", $result->stdout ); + $sites_to_delete = array_intersect( $test_sites, $running_sites ); + + foreach ( $sites_to_delete as $site ) { + exec( "sudo bin/ee site delete $site --yes" ); + } + + if( file_exists( 'ee.phar' ) ) { + unlink( 'ee.phar' ); + } + + if( file_exists( 'ee-old.phar' ) ) { + unlink( 'ee-old.phar' ); + } + } + +} diff --git a/features/bootstrap/support.php b/features/bootstrap/support.php new file mode 100644 index 0000000..7f1d528 --- /dev/null +++ b/features/bootstrap/support.php @@ -0,0 +1,206 @@ + $value ) { + if ( ! compareContents( $value, $actual->$name ) ) { + return false; + } + } + } else if ( is_array( $expected ) ) { + foreach ( $expected as $key => $value ) { + if ( ! compareContents( $value, $actual[$key] ) ) { + return false; + } + } + } else { + return $expected === $actual; + } + + return true; +} + +/** + * Compare two strings containing JSON to ensure that @a $actualJson contains at + * least what the JSON string @a $expectedJson contains. + * + * @return whether or not @a $actualJson contains @a $expectedJson + * @retval true @a $actualJson contains @a $expectedJson + * @retval false @a $actualJson does not contain @a $expectedJson + * + * @param [in] $actualJson the JSON string to be tested + * @param [in] $expectedJson the expected JSON string + * + * Examples: + * expected: {'a':1,'array':[1,3,5]} + * + * 1 ) + * actual: {'a':1,'b':2,'c':3,'array':[1,2,3,4,5]} + * return: true + * + * 2 ) + * actual: {'b':2,'c':3,'array':[1,2,3,4,5]} + * return: false + * element 'a' is missing from the root object + * + * 3 ) + * actual: {'a':0,'b':2,'c':3,'array':[1,2,3,4,5]} + * return: false + * the value of element 'a' is not 1 + * + * 4 ) + * actual: {'a':1,'b':2,'c':3,'array':[1,2,4,5]} + * return: false + * the contents of 'array' does not include 3 + */ +function checkThatJsonStringContainsJsonString( $actualJson, $expectedJson ) { + $actualValue = json_decode( $actualJson ); + $expectedValue = json_decode( $expectedJson ); + + if ( ! $actualValue ) { + return false; + } + + return compareContents( $expectedValue, $actualValue ); +} + +/** + * Compare two strings to confirm $actualCSV contains $expectedCSV + * Both strings are expected to have headers for their CSVs. + * $actualCSV must match all data rows in $expectedCSV + * + * @param string A CSV string + * @param array A nested array of values + * + * @return bool Whether $actualCSV contains $expectedCSV + */ +function checkThatCsvStringContainsValues( $actualCSV, $expectedCSV ) { + $actualCSV = array_map( 'str_getcsv', explode( PHP_EOL, $actualCSV ) ); + + if ( empty( $actualCSV ) ) { + return false; + } + + // Each sample must have headers + $actualHeaders = array_values( array_shift( $actualCSV ) ); + $expectedHeaders = array_values( array_shift( $expectedCSV ) ); + + // Each expectedCSV must exist somewhere in actualCSV in the proper column + $expectedResult = 0; + foreach ( $expectedCSV as $expected_row ) { + $expected_row = array_combine( $expectedHeaders, $expected_row ); + foreach ( $actualCSV as $actual_row ) { + + if ( count( $actualHeaders ) != count( $actual_row ) ) { + continue; + } + + $actual_row = array_intersect_key( array_combine( $actualHeaders, $actual_row ), $expected_row ); + if ( $actual_row == $expected_row ) { + $expectedResult ++; + } + } + } + + return $expectedResult >= count( $expectedCSV ); +} + +/** + * Compare two strings containing YAML to ensure that @a $actualYaml contains at + * least what the YAML string @a $expectedYaml contains. + * + * @return whether or not @a $actualYaml contains @a $expectedJson + * @retval true @a $actualYaml contains @a $expectedJson + * @retval false @a $actualYaml does not contain @a $expectedJson + * + * @param [in] $actualYaml the YAML string to be tested + * @param [in] $expectedYaml the expected YAML string + */ +function checkThatYamlStringContainsYamlString( $actualYaml, $expectedYaml ) { + $actualValue = Mustangostang\Spyc::YAMLLoad( $actualYaml ); + $expectedValue = Mustangostang\Spyc::YAMLLoad( $expectedYaml ); + + if ( ! $actualValue ) { + return false; + } + + return compareContents( $expectedValue, $actualValue ); +} diff --git a/features/site.feature b/features/site.feature new file mode 100644 index 0000000..6acc99d --- /dev/null +++ b/features/site.feature @@ -0,0 +1,315 @@ +Feature: Auth Command + + Scenario: ee executable is command working correctly + Given 'bin/ee' is installed + When I run 'bin/ee' + Then STDOUT should return something like + """ + NAME + + ee + """ + + Scenario: Check auth command is present + When I run 'bin/ee auth' + Then STDOUT should return exactly + """ + usage: ee auth create [] [--user=] [--pass=] [--ip=] + or: ee auth delete [] [--user=] [--ip] + or: ee auth list [] [--ip] [--format=] + or: ee auth update [] [--user=] [--pass=] [--ip=] + + See 'ee help auth ' for more information on a specific command. + """ + + Scenario: Create php site + When I run 'bin/ee site create php.test --type=php' + Then After delay of 2 seconds + And The site 'php.test' should have index file + And Request on 'php.test' should contain following headers: + | header | + | HTTP/1.1 200 OK | + + Scenario: Check auth list sub command is present + When I run 'bin/ee auth list' + Then STDERR should return something like + """ + Error: Could not find the site you wish to run auth list command on. + Either pass it as an argument: `ee auth list ` + or run `ee auth list` from inside the site folder. + """ + + Scenario: Check auth list and should be return error + When I run 'bin/ee auth list php.test' + Then STDERR should return something like + """ + Error: Auth does not exists on php.test + """ + + Scenario: Check auth create sub command is present + When I run 'bin/ee auth create' + Then STDERR should return something like + """ + Error: Could not find the site you wish to run auth create command on. + Either pass it as an argument: `ee auth create ` + or run `ee auth create` from inside the site folder. + """ + + Scenario: Create auth for PHP site + When I run 'bin/ee auth create php.test --user=rtcamp --pass=easyengine' + Then STDOUT should return exactly + """ + Reloading global reverse proxy. + Success: Auth successfully updated for `php.test` scope. New values added: + User: rtcamp + Pass: easyengine + """ + And Auth request on 'php.test' with user 'rtcamp' and password 'easyengine' should contain following headers: + | header | + | HTTP/1.1 200 OK | + + Scenario: Check created auth credentials + When I run 'bin/ee auth list php.test --format=csv' + Then STDOUT should return exactly + """ + username,password + rtcamp,easyengine + """ + + Scenario: Check auth update sub command is present + When I run 'bin/ee auth update' + Then STDERR should return something like + """ + Error: Could not find the site you wish to run auth update command on. + Either pass it as an argument: `ee auth update ` + or run `ee auth update` from inside the site folder. + """ + + Scenario: Update auth for PHP site + When I run 'bin/ee auth update php.test --user=rtcamp --pass=rtcamp' + Then STDOUT should return exactly + """ + Reloading global reverse proxy. + Success: Auth successfully updated for `php.test` scope. New values added: + User: rtcamp + Pass: rtcamp + """ + And Auth request on 'php.test' with user 'rtcamp' and password 'rtcamp' should contain following headers: + | header | + | HTTP/1.1 200 OK | + And Request on 'php.test' should contain following headers: + | header | + | HTTP/1.1 200 OK | + + Scenario: Check updated auth credentials + When I run 'bin/ee auth list php.test --format=csv' + Then STDOUT should return exactly + """ + username,password + rtcamp,rtcamp + """ + + Scenario: White list ips + When I run 'bin/ee auth create php.test --ip="$(docker inspect -f '{{range .IPAM.Config}}{{.Gateway}}{{end}}' ee-global-frontend-network)"' + And Request on 'php.test' should contain following headers: + | header | + | HTTP/1.1 200 OK | + + Scenario: Check whitelisted ips list + When I run 'bin/ee auth list php.test --format=csv --ip' + Then STDOUT should return something like + """ + ip + """ + + Scenario: Update auth with unregistered user for PHP site to get exception + When I run 'bin/ee auth update php.test --user=rtcamp1 --pass=rtcamp' + Then STDERR should return something like + """ + Error: Auth with username: rtcamp1 does not exists on php.test + """ + + Scenario: Create new auth for PHP site + When I run 'bin/ee auth create php.test --user=rtcamp-test --pass=easyengine-test' + Then STDOUT should return exactly + """ + Reloading global reverse proxy. + Success: Auth successfully updated for `php.test` scope. New values added: + User: rtcamp-test + Pass: easyengine-test + """ + And Auth request on 'php.test' with user 'rtcamp-test' and password 'easyengine-test' should contain following headers: + | header | + | HTTP/1.1 200 OK | + + Scenario: Check auth list + When I run 'bin/ee auth list php.test --format=csv' + Then STDOUT should return exactly + """ + username,password + rtcamp,rtcamp + rtcamp-test,easyengine-test + """ + + Scenario: Delete specific auth user + When I run 'bin/ee auth delete php.test --user=rtcamp-test' + Then STDOUT should return exactly + """ + Success: http auth successfully removed on php.test. + Reloading global reverse proxy. + """ + + Scenario: Check auth list + When I run 'bin/ee auth list php.test --format=csv' + Then STDOUT should return exactly + """ + username,password + rtcamp,rtcamp + """ + + Scenario: Create new auth for PHP site + When I run 'bin/ee auth create php.test --user=test --pass=test' + Then STDOUT should return exactly + """ + Reloading global reverse proxy. + Success: Auth successfully updated for `php.test` scope. New values added: + User: test + Pass: test + """ + And Auth request on 'php.test' with user 'test' and password 'test' should contain following headers: + | header | + | HTTP/1.1 200 OK | + + Scenario: Check auth list + When I run 'bin/ee auth list php.test --format=csv' + Then STDOUT should return exactly + """ + username,password + rtcamp,rtcamp + test,test + """ + + Scenario: Create WordPress site + When I run 'bin/ee site create wp.test --type=wp' + Then After delay of 2 seconds + And The site 'wp.test' should have WordPress + And Request on 'wp.test' should contain following headers: + | header | + | HTTP/1.1 200 OK | + + Scenario: Create global auth + When I run 'bin/ee auth create global --user=superman --pass=hanuman' + And I run 'bin/ee auth delete global --user=easyengine' + And I run 'bin/ee auth delete global --ip' + Then STDOUT should return exactly + """ + Reloading global reverse proxy. + Success: Auth successfully updated for `global` scope. New values added: + User: superman + Pass: hanuman + """ + And Auth request on 'php.test' with user 'superman' and password 'hanuman' should contain following headers: + | header | + | HTTP/1.1 200 OK | + And Auth request on 'php.test' with user 'test' and password 'test' should contain following headers: + | header | + | HTTP/1.1 200 OK | + And Auth request on 'php.test' with user 'rtcamp' and password 'rtcamp' should contain following headers: + | header | + | HTTP/1.1 200 OK | + And Request on 'wp.test' should contain following headers: + | header | + | HTTP/1.1 401 Unauthorized | + And Auth request on 'wp.test' with user 'superman' and password 'hanuman' should contain following headers: + | header | + | HTTP/1.1 200 OK | + + Scenario: Check global auth list + When I run 'bin/ee auth list global --format=csv' + Then STDOUT should return exactly + """ + username,password + superman,hanuman + """ + + Scenario: Update global auth + When I run 'bin/ee auth update global --user=superman --pass=shaktiman' + Then STDOUT should return exactly + """ + Reloading global reverse proxy. + Success: Auth successfully updated for `global` scope. New values added: + User: superman + Pass: shaktiman + """ + And Auth request on 'php.test' with user 'superman' and password 'shaktiman' should contain following headers: + | header | + | HTTP/1.1 200 OK | + And Auth request on 'php.test' with user 'test' and password 'test' should contain following headers: + | header | + | HTTP/1.1 200 OK | + And Auth request on 'php.test' with user 'rtcamp' and password 'rtcamp' should contain following headers: + | header | + | HTTP/1.1 200 OK | + And Request on 'wp.test' should contain following headers: + | header | + | HTTP/1.1 401 Unauthorized | + And Auth request on 'wp.test' with user 'superman' and password 'shaktiman' should contain following headers: + | header | + | HTTP/1.1 200 OK | + + Scenario: Check updated global auth list + When I run 'bin/ee auth list global --format=csv' + Then STDOUT should return exactly + """ + username,password + superman,shaktiman + """ + + Scenario: Check global auth ips list and white list ips globally + When I run 'bin/ee auth list global --format=csv --ip' + And I run 'bin/ee auth update global --ip=127.0.0.2' + And I run 'bin/ee auth update global --ip=192.0.0.1' + Then STDOUT should return exactly + """ + ip + """ + + Scenario: Check global auth ips list + When I run 'bin/ee auth list global --ip --format=csv' + Then STDOUT should return exactly + """ + ip + 127.0.0.2 + 192.0.0.1 + """ + + Scenario: Delete specific auth user + When I run 'bin/ee auth delete php.test' + Then STDOUT should return exactly + """ + Success: http auth successfully removed on php.test on user + Reloading global reverse proxy. + """ + Then After delay of 2 seconds + And Request on 'php.test' should contain following headers: + | header | + | HTTP/1.1 401 Unauthorized | + And Auth request on 'php.test' with user 'superman' and password 'shaktiman' should contain following headers: + | header | + | HTTP/1.1 200 OK | + + Scenario: Delete global auth + When I run 'bin/ee auth delete global' + And I run 'bin/ee auth delete wp.test' + Then STDOUT should return exactly + """ + Success: http auth successfully removed on default on user + Reloading global reverse proxy. + """ + Then After delay of 2 seconds + And Request on 'php.test' should contain following headers: + | header | + | HTTP/1.1 200 OK | + And Request on 'wp.test' should contain following headers: + | header | + | HTTP/1.1 200 OK |