luyadev/luya-testsuite

View on GitHub
src/cases/ServerTestCase.php

Summary

Maintainability
A
2 hrs
Test Coverage
F
10%
<?php

namespace luya\testsuite\cases;

use Curl\Curl;
use Exception;
use Yii;
use luya\base\Boot;
use yii\helpers\Json;

/**
 * Webserver Test Case.
 *
 * Generates a local Server in order to Test URLs.
 *
 * An example usage:
 *
 * ```php
 * class MyWebsite extends ServerTestCase
 * {
 *    public function getConfigArray()
 *    {
 *       return [
 *           'id' => 'mytestapp',
 *           'basePath' => dirname(__DIR__),
 *       ];
 *   }
 *
 *   public function testSites()
 *   {
 *       $this->assertUrlHomepageIsOk();
 *       $this->assertUrlIsOk('about');
 *       $this->assertUrlGetResponseContains('about/me', 'Hello World');
 *       $this->assertUrlIsError('errorpage');
 *   }
 * }
 * ```
 *
 * @author Basil Suter <basil@nadar.io>
 * @since 1.0.2
 */
abstract class ServerTestCase extends BaseTestSuite
{
    /**
     * @var string
     */
    public $host = 'localhost';
   
    /**
     * @var integer
     */
    public $port = 1549;
    
    /**
     * @var string
     */
    public $documentRoot = '@app/public_html';
    
    /**
     * @var boolean
     */
    public $debug = false;
    
    /**
     * {@inheritDoc}
     * @see \luya\testsuite\cases\BaseTestSuite::bootApplication()
     */
    public function bootApplication(Boot $boot)
    {
        $boot->applicationConsole();
    }
    
    private $_pid = 0;
    
    /**
     *
     * {@inheritDoc}
     * @see \luya\testsuite\cases\BaseTestSuite::afterSetup()
     */
    public function afterSetup()
    {
        $this->_pid = $this->bootstrapServer($this->host, $this->port, $this->documentRoot);
    }
    
    /**
     * {@inheritDoc}
     * @see \luya\testsuite\cases\BaseTestSuite::beforeTearDown()
     */
    public function beforeTearDown()
    {
        $this->killServer($this->_pid);
        $this->waitForServerShutdown($this->host, $this->port);
    }
    
    /**
     * Check whether homage is online and OK response.
     *
     * @since 1.0.3
     */
    public function assertUrlHomepageIsOk()
    {
        $this->assertUrlIsOk(null);
    }
    
    /**
     * Test an URL whether a page has response code 200
     *
     * @param string|array $url The base path on the current server. If array provided the first key is used as path and other values
     * are merged with $params attribte.
     * @param array $params Optional parameters to bind url with.
     * @since 1.0.3
     */
    public function assertUrlIsOk($url, array $params = [])
    {
        $curl = $this->createGetCurl($url, $params);
        $printUrl = $this->buildCallUrl($url, $params);
        $this->assertTrue($curl->isSuccess(), "GET URL '{$printUrl}' return {$curl->http_status_code} instead of 200 (OK).");
    }
    
    /**
     * Test an URL whether a page has response code 400
     *
     * @param string|array $url The base path on the current server. If array provided the first key is used as path and other values
     * are merged with $params attribte.
     * @param array $params Optional parameters to bind url with.
     * @since 1.0.3
     */
    public function assertUrlIsError($url, array $params = [])
    {
        $curl = $this->createGetCurl($url, $params);
        $printUrl = $this->buildCallUrl($url, $params);
        $this->assertTrue($curl->isError(), "GET URL '{$printUrl}' return {$curl->http_status_code} instead of 400 (Error).");
    }

    /**
     * Test whether url is redirect.
     *
     * @param string|array $url The base path on the current server. If array provided the first key is used as path and other values
     * are merged with $params attribte.
     * @param array $params Optional parameters to bind url with.
     * @since 1.0.3
     */
    public function assertUrlIsRedirect($url, array $params = [])
    {
        $curl = $this->createGetCurl($url, $params);
        $printUrl = $this->buildCallUrl($url, $params);
        $this->assertTrue($curl->isRedirect(), "GET URL '{$printUrl}' return {$curl->http_status_code} instead of 300 (Error).");
    }
    
    /**
     * Test an url and see if the response contains.
     *
     * @param string|array $url The base path on the current server. If array provided the first key is used as path and other values
     * are merged with $params attribte.
     * @param string|array $contains If its an array it will be json encoded by default and the first and last char (wrapping)
     * brackets are cute off, so you can easy search for a key value parining inside the json response.
     * @param array $params Optional parameters to bind url with
     * @since 1.0.3
     */
    public function assertUrlGetResponseContains($url, $contains, array $params = [])
    {
        $curl = $this->createGetCurl($url, $params);
        $this->assertContains($this->buildPartialJson($contains, true), $curl->response);
    }
    
    /**
     * Make a GET request and see if the response is the same as.
     *
     * @param string|array $url The base path on the current server. If array provided the first key is used as path and other values
     * are merged with $params attribte.
     * @param string|array $contains If its an array it will be json encoded by default and the first and last char (wrapping).a
     * brackets are cute off, so you can easy search for a key value parining inside the json response.
     * @param array $data The data to post on the $url ($_POST data).
     * @param array $params Optional parameters to bind url with.
     * @since 1.0.3
     */
    public function assertUrlGetResponseSame($url, $same, array $params = [])
    {
        $curl = $this->createGetCurl($url, $params);
        $this->assertSame($this->buildPartialJson($same), $curl->response);
    }
    
    /**
     * Make a POST request and see if the response contains in.
     *
     * @param string|array $url The base path on the current server. If array provided the first key is used as path and other values
     * are merged with $params attribte.
     * @param string|array $contains If its an array it will be json encoded by default and the first and last char (wrapping)
     * brackets are cute off, so you can easy search for a key value parining inside the json response.
     * @param array $data The data to post on the $url ($_POST data)
     * @param array $params Optional parameters to bind url with
     * @since 1.0.3
     */
    public function assertUrlPostResponseContains($url, $contains, array $data = [], array $params = [])
    {
        $curl = $this->createPostCurl($url, $data, $params);
        $this->assertContains($this->buildPartialJson($contains, true), $curl->response);
    }

    /**
     *
     * @param string|array $url
     * @param string|array $same
     * @param array $data
     * @since 1.0.3
     */
    public function assertUrlPostResponseSame($url, $same, array $data = [], array $params = [])
    {
        $curl = $this->createPostCurl($url, $data, $params);
        $this->assertSame($this->buildPartialJson($same), $curl->response);
    }
    
    /**
     *
     * @param string|array $contains
     * @param string $removeBrackets
     * @return string
     * @since 1.0.3
     */
    protected function buildPartialJson($contains, $removeBrackets = false)
    {
        if (is_array($contains)) {
            $contains = Json::encode($contains);
            if ($removeBrackets) {
                $contains = substr(substr($contains, 1), 0, -1);
            }
        }
        
        return $contains;
    }
   
    /**
     * Build the url to call with current local host and port.
     *
     * If the url is an array the key is the path and the later key value paired are used for params.
     *
     * ```php
     * buildCallUrl(['path/to/api', 'access-token' => 123]);
     * ```
     *
     * is equals to:
     *
     * ```php
     * buildCallUrl('path/to/api', ['access-tokne' => 123]);
     * ```
     *
     * @param string|array $url The local base path to build the url from. If an array the first key is used for the path defintion.
     * @param array $params Optional key value paired arguments to build the url from.
     * @return string
     * @since 1.0.3
     */
    protected function buildCallUrl($url, array $params = [])
    {
        if (is_array($url)) {
            $path = $url[0];
            unset($url[0]);
            $params = array_merge($url, $params);
        } else {
            $path = $url;
        }
        
        $url = "{$this->host}:{$this->port}/" . ltrim($path, '/');
        
        if (!empty($params)) {
            $url .= '?' . http_build_query($params);
        }
        
        return $url;
    }
    
    /**
     * Print a echoing debug message for a curl request.
     * 
     * @param string $url
     * @param Curl $curl 
     * @since 1.0.24
     */
    protected function debugMessage($url, Curl $curl)
    {
        echo PHP_EOL;
        echo "======================================================" . PHP_EOL;
        echo "REQUEST URL: " . $url . PHP_EOL;
        echo "------------------------------------------------------" . PHP_EOL;
        echo "REQUEST HEADERS:" . PHP_EOL;
        print_r($curl->request_headers) . PHP_EOL;
        echo "------------------------------------------------------" . PHP_EOL;
        echo "RESPONSE:" . PHP_EOL;
        echo $curl->response;
        echo "------------------------------------------------------" . PHP_EOL;
        echo "RESPONSE HEADERS:" . PHP_EOL;
        print_r($curl->response_headers) . PHP_EOL;
        echo "======================================================" . PHP_EOL;
        echo PHP_EOL;
    }

    /**
     * @param string $url
     * @return \Curl\Curl
     * @since 1.0.3
     */
    public function createGetCurl($url, array $params = [])
    {
        $callUrl = $this->buildCallUrl($url, $params);
        $curl = (new Curl())->get($callUrl);
        
        if ($this->debug) {
            $this->debugMessage($callUrl, $curl);
        }
        
        return $curl;
    }
    
    /**
     *
     * @param string $url
     * @param array $data
     * @return \Curl\Curl
     * @since 1.0.3
     */
    public function createPostCurl($url, array $data = [], array $params = [])
    {
        $callUrl = $this->buildCallUrl($url, $params);
        $curl = (new Curl())->post($callUrl, $data);
        
        if ($this->debug) {
            $this->debugMessage($callUrl, $curl);
        }
        
        return $curl;
    }
    
    /**
     *
     * @param string $host
     * @param string $port
     * @param string $documentRoot
     * @throws Exception
     * @return number
     * @since 1.0.2
     */
    protected function bootstrapServer($host, $port, $documentRoot)
    {
        $documentRoot = Yii::getAlias($documentRoot);
        if ($this->connectToServer($host, $port)) {
            throw new Exception("The $host:$port is already taken, choose another host and/or port.");
        }
        
        $pid = $this->createServer($host, $port, $documentRoot);
        
        $this->waitForServer($host, $port);
        
        return $pid;
    }
    
    /**
     *
     * @param string $host
     * @param string $port
     * @param string $documentRoot
     * @param integer PID (process id)
     * @since 1.0.2
     */
    protected function createServer($host, $port, $documentRoot)
    {
        $command = sprintf(PHP_BINARY . ' -S %s:%d -t %s >/dev/null 2>&1 & echo $!', $host, $port, $documentRoot);
        
        // Execute the command and store the process ID
        $output = [];
        exec($command, $output);
        
        return (int) $output[0];
    }
    
    /**
     *
     * @param string $host
     * @param string $port
     * @return boolean
     * @since 1.0.2
     */
    protected function waitForServer($host, $port)
    {
        $start = microtime(true);
        while (microtime(true) - $start <= (int) 200) {
            if ($this->connectToServer($host, $port)) {
                break;
            }
        }
        
        return true;
    }
    
    /**
     *
     * @param string $host
     * @param string $port
     * @return boolean
     * @since 1.0.3
     */
    protected function waitForServerShutdown($host, $port)
    {
        $start = microtime(true);
        while (microtime(true) - $start <= (int) 200) {
            if (!$this->connectToServer($host, $port)) {
                break;
            }
        }
        
        return true;
    }
    
    /**
     *
     * @param string $host
     * @param string $port
     * @return boolean
     * @since 1.0.2
     */
    protected function connectToServer($host, $port)
    {
        $fp = @fsockopen($host, $port, $errno, $errstr, 3);
        if ($fp === false) {
            return false;
        }
        fclose($fp);
        return true;
    }
    
    /**
     *
     * @param string $pid
     * @since 1.0.2
     */
    protected function killServer($pid)
    {
        exec('kill -9 ' . (int) $pid);
    }
}