diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8631b5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.idea +/vendor +composer.phar +composer.lock +.php_cs.cache +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..15b0e2e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +language: php + +sudo: required + +php: + - 5.6 + - 7.0 + - 7.1 + +install: + - composer self-update + - travis_retry composer install --no-interaction --prefer-source --dev + +before_script: + - composer dump-autoload + +script: + - php -S 127.0.0.1:8000 tests/server.php > /dev/null 2>&1 & + - mkdir -p build/logs + - vendor/bin/phpunit --bootstrap ./tests/bootstrap.php --configuration ./phpunit.xml --coverage-clover build/logs/clover.xml + +after_script: + - php vendor/bin/coveralls -v + +after_success: + - coveralls \ No newline at end of file diff --git a/README.md b/README.md index b875602..26e1f0d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,245 @@ -# http -Neutrino Http Client +Neutrino : Http Client component +============================================== +[![Build Status](https://travis-ci.org/pn-neutrino/http.svg?branch=master)](https://travis-ci.org/pn-neutrino/http) [![Coverage Status](https://coveralls.io/repos/github/pn-neutrino/http/badge.svg?branch=master)](https://coveralls.io/github/pn-neutrino/http) + +Http Client library using Curl or HttpStream. + +# Provider + +## Curl +require curl extension. + +#### How use : +```php +use \Neutrino\Http\Provider\Curl as HttpCurl; +use \Neutrino\Http\Method; + +$curl = new HttpCurl; + +$response = $curl + ->get('http://www.google.com', ['foo' => 'bar'], ['Accept' => 'text/plain']) + ->send(); + +$response->code; // HTTP Status Code +``` + +### Curl\Streaming +Curl\Stream allows you to work with large queries, by recovers content part by part. + +#### How use : +```php +use \Neutrino\Http\Provider\Curl\Streaming as HttpCurlStream; +use \Neutrino\Http\Method; + +$curl = new HttpCurlStream; + +$response = $curl + ->get('http://www.google.com') + ->on(HttpCurlStream::EVENT_START, function (HttpCurlStream $curl) { + // Start to download response body + // Header are fully loaded when the event are raised + }) + ->on(HttpCurlStream::EVENT_PROGRESS, function (HttpCurlStream $curl, $content) { + // Download progress + // $content contain the response part + }) + ->send(); +``` + +Transfer huge data, without overloading the php memory : +```php +$curl + ->get('http://www.google.com') + ->on(HttpCurlStream::EVENT_START, function (HttpCurlStream $curl) { + if ($curl->response->header->has('Content-Length')) { + header('Content-Length: ' . $curl->response->header->get('Content-Length')); + } + }) + ->on(HttpCurlStream::EVENT_PROGRESS, function (HttpCurlStream $curl, $content) { + echo $content; + ob_flush(); + flush(); + // => Direct echo contents & flush the output (free memory) + }) + ->send(); +``` + +Download huge file, without overloading the php memory : +```php +$resource = fopen($path, 'w'); + +$curl + ->get('http://www.google.com') + ->on(HttpCurlStream::EVENT_PROGRESS, function (HttpCurlStream $curl, $content) use ($resource) { + fwrite($resource, $content, strlen($content)); + }) + ->send(); + +fclose($resource); +``` + +## StreamContext +StreamContext make HTTP call via the php wrapper. + +This require you have "allow_url_fopen" configuration value set to '1'. + +#### How use : +```php +use \Neutrino\Http\Provider\StreamContext as HttpStreamCtx; +use \Neutrino\Http\Method; + +$streamCtx = new HttpStreamCtx; + +$response = $streamCtx + ->get('http://www.google.com', ['foo' => 'bar'], ['Accept' => 'text/plain']) + ->send(); + +$response->code; // HTTP Status Code +``` + +### StreamContext\Streaming +Such as Curl\Streaming, StreamContext\Streaming allows you to work with large queries, by recovers content part by part. + +#### How use : +```php +use \Neutrino\Http\Provider\StreamContext\Streaming as HttpStreamCtxStreaming; +use \Neutrino\Http\Method; + +$streamCtx = new HttpStreamCtxStreaming; + +$response = $streamCtx + ->get('http://www.google.com') + ->on(HttpStreamCtxStreaming::EVENT_START, function (HttpStreamCtxStreaming $streamCtx) { + // Start to download response body + // Header are fully loaded when the event are raised + }) + ->on(HttpStreamCtxStreaming::EVENT_PROGRESS, function (HttpStreamCtxStreaming $streamCtx, $content) { + // Download progress + // $content contain the response part + }) + ->send(); +``` + +# Auth +Authentication is a request component. + +## Auth\Basic +Auth\Basic provides the elements to configure a call with an Basic Authorization. + +#### How use : +```php +use \Neutrino\Http\Auth\Basic as AuthBasic; +use \Neutrino\Http\Provider\StreamContext as HttpStreamCtx; +use \Neutrino\Http\Method; + +$streamCtx = new HttpStreamCtx; + +$response = $streamCtx + ->get('http://www.google.com') + ->setAuth(new AuthBasic('user', 'pass')) + ->send(); +``` + +## Auth\Curl +Specific for Curl provider. + +Auth\Curl provides the elements to build a call with Curl Auth. + +#### How use : +```php +use \Neutrino\Http\Auth\Curl as AuthCurl; +use \Neutrino\Http\Provider\Curl as HttpCurl; +use \Neutrino\Http\Method; + +$curl = new HttpCurl; + +$response = $curl + ->get('http://www.google.com') + ->setAuth(new AuthCurl(CURLAUTH_BASIC | CURLAUTH_DIGEST, 'user', 'pass')) + ->send(); +``` + +## Custom Auth Component +You can easily make your own Auth Component : + +```php +namespace MyLib\Http\Auth; + +use Neutrino\Http\Request; +use Neutrino\Http\Contract\Request\Component; + +class Hmac implements Component +{ + private $id; + private $value; + + public function __construct($id, $value) + { + $this->id = $id; + $this->value = $value; + } + + public function build(Request $request) + { + $date = date('D, d M Y H:i:s', time()); + $signature = urlencode(base64_encode(hash_hmac('sha1', "date: $date", $this->value, true))); + + $request + ->setHeader('Date', $date) + ->setHeader('Authorization', 'Signature keyId="' . $this->id . '",algorithm="hmac-sha1",signature="' . $signature . '"'); + } +} +``` +```php +use \MyLib\Http\Auth\Hmac as AuthHmac; +use \Neutrino\Http\Provider\Curl as HttpCurl; +use \Neutrino\Http\Method; + +$curl = new HttpCurl; + +$response = $curl + ->get('http://www.google.com') + ->setAuth(new AuthHmac('key_id', 'key_value')) + ->send(); +``` + +# Response +## Basic +```php +$response->code; // HTTP Status Code +$response->status; // HTTP Status Message +$response->header; // Response Headers +$response->body; // Response Body +``` +## Provider Info +```php +$response->errorCode; // Provider Error Code +$response->error; // Provider Error Message +$response->providerDatas; // All Provider Information (if available) +``` + +## Parse + +```php +use \Neutrino\Http\Parser; + +// Json Body => Object +$jsonObject = $response->parse(Parser\Json::class)->data; + +// Xml Body => SimpleXMLElement +$xmlElement = $response->parse(Parser\Xml::class)->data; + +// Xml Body => array +$xmlArray = $response->parse(Parser\XmlArray::class)->data; + +// Other exemple : (PHP7) +$response->parse(new class implements Parser\Parserize +{ + public function parse($body) + { + return unserialize($body); + } +}); + +$response->data; // Unserialized body +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5251a0f --- /dev/null +++ b/composer.json @@ -0,0 +1,51 @@ +{ + "name": "neutrino/http", + "description": "The Neutrino Http package.", + "keywords": [ + "neutrino", + "nucleon", + "http", + "http client", + "api", + "json", + "requests", + "rest", + "restful", + "web service", + "xml", + "curl", + "stream", + "stream context", + "streaming" + ], + "license": "MIT", + "homepage": "https://phalcon-nucleon.github.io", + "support": { + "issues": "https://github.com/pn-neutrino/http/issues", + "source": "https://github.com/pn-neutrino/http" + }, + "authors": [ + { + "name": "Ark4ne (Guillaume Allegret)", + "email": "gui.allegret@gmail.com" + } + ], + "require": { + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "~5.7", + "satooshi/php-coveralls": "~1.0" + }, + "autoload": { + "psr-4": { + "Neutrino\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Test\\": "tests/Test" + } + }, + "minimum-stability": "dev" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..d063f2f --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,21 @@ + + + + ./tests/ + + + + ./src + + + \ No newline at end of file diff --git a/src/Http/Auth/Basic.php b/src/Http/Auth/Basic.php new file mode 100644 index 0000000..9504315 --- /dev/null +++ b/src/Http/Auth/Basic.php @@ -0,0 +1,37 @@ +user = $user; + $this->pass = $pass; + } + + public function build(Request $request) + { + $request->setHeader('Authorization', 'Basic ' . base64_encode($this->user . ':' . $this->pass)); + } +} diff --git a/src/Http/Auth/Curl.php b/src/Http/Auth/Curl.php new file mode 100644 index 0000000..a684451 --- /dev/null +++ b/src/Http/Auth/Curl.php @@ -0,0 +1,60 @@ + PHP 7.0.7 Only + || $type & CURLAUTH_GSSNEGOTIATE + || $type & CURLAUTH_NTLM + //|| $type & CURLAUTH_NTLM_WB + || ($type & 32 && PHP_VERSION_ID >= 70707 ) + ) { + $this->type = $type; + $this->user = $user; + $this->pass = $pass; + + return; + } + + throw new Exception(self::class . ' : Doesn\'t support Auth type : ' . $type); + } + + public function build(Request $request) + { + $request + ->setOption(CURLOPT_HTTPAUTH, $this->type) + ->setOption(CURLOPT_USERPWD, $this->user . ':' . $this->pass); + } +} diff --git a/src/Http/Auth/Exception.php b/src/Http/Auth/Exception.php new file mode 100644 index 0000000..3adecd7 --- /dev/null +++ b/src/Http/Auth/Exception.php @@ -0,0 +1,8 @@ +emitter)) { + $this->emitter = new Emitter(); + } + + return $this->emitter; + } + + public function on($event, $callback) + { + $this->checkEvent($event); + + $this->getEmitter()->attach($event, $callback); + + return $this; + } + + public function off($event, $callback) + { + $this->checkEvent($event); + + $this->getEmitter()->detach($event, $callback); + + return $this; + } + + public function setBufferSize($size) + { + $this->bufferSize = $size; + + return $this; + } + + private function checkEvent($event) + { + if ($event == Streamable::EVENT_START + || $event == Streamable::EVENT_PROGRESS + || $event == Streamable::EVENT_FINISH + + ) { + return; + } + + throw new \RuntimeException(static::class . ' only support ' . implode(', ', + [ + Streamable::EVENT_START, + Streamable::EVENT_PROGRESS, + Streamable::EVENT_FINISH, + ])); + } +} diff --git a/src/Http/Event/Emitter.php b/src/Http/Event/Emitter.php new file mode 100644 index 0000000..17a71bc --- /dev/null +++ b/src/Http/Event/Emitter.php @@ -0,0 +1,72 @@ +listeners[$event][] = $closure; + + return true; + } + + /** + * @param string $event + * @param callable $closure + * + * @return bool + */ + public function detach($event, $closure) + { + if (isset($this->listeners[$event])) { + if (!is_null($closure)) { + $index = array_search($closure, $this->listeners[$event], true); + + if (false !== $index) { + unset($this->listeners[$event][$index]); + + return true; + } + } + } + + return false; + } + + public function clear($event) + { + if (isset($this->listeners[$event])) { + unset($this->listeners[$event]); + + return true; + } + return false; + } + + /** + * @param string $event + * @param array $arguments + * + * @return $this + */ + public function fire($event, array $arguments = []) + { + if (isset($this->listeners[$event])) { + foreach ($this->listeners[$event] as $event) { + call_user_func_array($event, $arguments); + } + } + + return $this; + } +} diff --git a/src/Http/Exception.php b/src/Http/Exception.php new file mode 100644 index 0000000..4f99e65 --- /dev/null +++ b/src/Http/Exception.php @@ -0,0 +1,8 @@ +headers[$name] = $value; + + return $this; + } + + /** + * @param string $name + * @param mixed $default + * + * @return mixed + */ + public function get($name, $default = null) + { + if ($this->has($name)) { + return $this->headers[$name]; + } + + return $default; + } + + /** + * Determine if a header exists with a specific key. + * + * @param string $name + * + * @return boolean + */ + public function has($name) + { + return isset($this->headers[$name]) || array_key_exists($name, $this->headers); + } + + /** + * @param string $name + * + * @return $this + */ + public function remove($name) + { + if ($this->has($name)) { + unset($this->headers[$name]); + } + + return $this; + } + + /** + * Set multiple headers. + * + * + * $headers = [ + * 'X-Foo' => 'bar', + * 'Content-Type' => 'application/json', + * ]; + * + * $curl->addMultiple($headers); + * + * + * @param array $fields + * @param bool $merge + * + * @return $this + */ + public function setHeaders(array $fields, $merge = false) + { + if ($merge) { + $this->headers = array_merge($this->headers, $fields); + } else { + $this->headers = $fields; + } + + return $this; + } + + /** + * @return array + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Return the header builded for the HTTP Request + * + * @return array + */ + public function build() + { + $headers = []; + + foreach ($this->headers as $name => $value) { + $headers[] = $name . ': ' . $value; + } + + return $headers; + } + + /** + * Count elements of an object + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + *

+ *

+ * The return value is cast to an integer. + * @since 5.1.0 + */ + public function count() + { + return count($this->headers); + } + + public function parse($raw) + { + if(is_string($raw)){ + $raw = array_filter(explode("\r\n", $raw)); + } + + foreach ($raw as $header) { + if (preg_match('%^HTTP/(\d(?:\.\d)?)\s+(\d{3})\s?+(.+)?$%i', $header, $status)) { + $this->version = $status[1]; + $this->code = intval($status[2]); + $this->status = isset($status[3]) ? $status[3] : ''; + } else { + $field = explode(':', $header, 2); + + $this->set(trim($field[0]), isset($field[1]) ? trim($field[1]) : null); + } + } + } +} diff --git a/src/Http/Parser/Json.php b/src/Http/Parser/Json.php new file mode 100644 index 0000000..02f0467 --- /dev/null +++ b/src/Http/Parser/Json.php @@ -0,0 +1,13 @@ +setOption(CURLOPT_TIMEOUT, $timeout); + } + + /** + * Definie le timeout de connexion de la requete + * Applique l'option CURL 'CURLOPT_CONNECTTIMEOUT' + * + * @param int $timeout + * + * @return $this + */ + public function setConnectTimeout($timeout) + { + return $this->setOption(CURLOPT_CONNECTTIMEOUT, $timeout); + } + + /** + * @return \Neutrino\Http\Response + * @throws \Exception + */ + protected function makeCall() + { + try { + $ch = curl_init(); + + $this->curlOptions($ch); + + $this->curlExec($ch); + + $this->curlInfos($ch); + + if ($this->response->errorCode) { + throw new HttpException($this->response->error, $this->response->errorCode); + } + + return $this->response; + } finally { + if (isset($ch) && is_resource($ch)) { + curl_close($ch); + } + } + } + + protected function curlOptions($ch) + { + $method = $this->method; + + if ($method === Method::HEAD) { + curl_setopt($ch, CURLOPT_NOBODY, true); + } + + // Default Options + curl_setopt_array($ch, + [ + CURLOPT_URL => $this->uri->build(), + CURLOPT_CUSTOMREQUEST => $method, + CURLOPT_AUTOREFERER => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 20, + CURLOPT_HEADER => false, + CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS, + CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS, + CURLOPT_CONNECTTIMEOUT => 30, + CURLOPT_TIMEOUT => 30, + CURLOPT_HEADERFUNCTION => [$this, 'curlHeaderFunction'], + ]); + + curl_setopt_array($ch, $this->options); + } + + /** + * @param resource $ch + * + * @return void + */ + protected function curlExec($ch) + { + $result = curl_exec($ch); + + $this->response->body = $result; + } + + protected function curlHeaderFunction($ch, $raw) + { + if ($this->response->code === null) { + $this->response->code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + } + + $this->response->header->parse($raw); + + return strlen($raw); + } + + public function curlInfos($ch) + { + if ($this->response->code === null) { + $this->response->code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + } + + if (($errno = curl_errno($ch)) !== 0) { + $this->response->errorCode = curl_errno($ch); + $this->response->error = curl_error($ch); + } + + $this->response->providerDatas = curl_getinfo($ch); + } + + /** + * Construit les parametres de la requete. + * HTTP [POST, PUT, PATCH] : Applique l'option CURL "CURLOPT_POSTFIELDS" + * HTTP [...] : Contruit l'url de la requete + * + * @return $this + */ + protected function buildParams() + { + if ($this->isPostMethod()) { + if ($this->isJsonRequest()) { + return $this + ->setOption(CURLOPT_POSTFIELDS, json_encode($this->params)) + ->setHeader('Content-Type', 'application/json'); + } + + return $this + ->setOption(CURLOPT_POSTFIELDS, http_build_query($this->params)) + ->setHeader('Content-Type', 'application/x-www-form-urlencoded'); + } + + return $this->buildUrl(); + } + + /** + * Construit les cookies de la requete + * Applique l'option CURL 'CURLOPT_COOKIE' + * + * @return $this + */ + protected function buildCookies() + { + if (!empty($this->cookies)) { + return $this->setOption(CURLOPT_COOKIE, $this->getCookies(true)); + } + + return $this; + } + + /** + * Construit les headers de la requete. + * Applique l'option CURL "CURLOPT_HTTPHEADER" + * + * @return $this + */ + protected function buildHeaders() + { + if (!empty($this->header->getHeaders())) { + return $this->setOption(CURLOPT_HTTPHEADER, $this->header->build()); + } + + return $this; + } + + /** + * Definie le proxy de la requete + * Applique les options CURL : CURLOPT_PROXY, CURLOPT_PROXYPORT, CURLOPT_PROXYUSERPWD + * + * @return $this + */ + protected function buildProxy() + { + if (isset($this->proxy['host'])) { + $this + ->setOption(CURLOPT_PROXY, $this->proxy['host']) + ->setOption(CURLOPT_PROXYPORT, $this->proxy['port']); + + if (isset($this->proxy['access'])) { + $this->setOption(CURLOPT_PROXYUSERPWD, $this->proxy['access']); + } + } + + return $this; + } +} diff --git a/src/Http/Provider/Curl/Streaming.php b/src/Http/Provider/Curl/Streaming.php new file mode 100644 index 0000000..0024144 --- /dev/null +++ b/src/Http/Provider/Curl/Streaming.php @@ -0,0 +1,60 @@ +hasStarted) { + $this->hasStarted = true; + + $this->emitter->fire(self::EVENT_START, [$this]); + } + + $length = strlen($content); + + $this->emitter->fire(self::EVENT_PROGRESS, [$this, $content]); + + return $length; + } + + public function send() + { + parent::send(); + + $this->emitter->fire(self::EVENT_FINISH, [$this]); + } + + protected function curlOptions($ch) + { + parent::curlOptions($ch); + + curl_setopt_array($ch, + [ + CURLOPT_RETURNTRANSFER => false, + CURLOPT_WRITEFUNCTION => [$this, 'curlWriteFunction'], + ]); + + if (isset($this->bufferSize)) { + curl_setopt($ch, CURLOPT_BUFFERSIZE, $this->bufferSize); + } + } +} diff --git a/src/Http/Provider/Exception.php b/src/Http/Provider/Exception.php new file mode 100644 index 0000000..b84796c --- /dev/null +++ b/src/Http/Provider/Exception.php @@ -0,0 +1,8 @@ +setOption('timeout', $timeout); + } + + /** + * @param $errno + * @param $errstr + * + * @throws HttpException + */ + protected function errorHandler($errno, $errstr) + { + $this->response->error = $errstr; + $this->response->errorCode = $errno; + + throw new HttpException($errstr, $errno); + } + + protected function makeCall() + { + try { + $context = stream_context_create(); + + $this->streamContextOptions($context); + + set_error_handler([$this, 'errorHandler']); + + $content = $this->streamContextExec($context); + + restore_error_handler(); + + $this->response->body = $content; + + return $this->response; + } finally { + $context = null; + } + } + + protected function streamContextOptions($context) + { + stream_context_set_option($context, ['http' => array_merge([ + 'follow_location' => 1, + 'max_redirects' => 20, + 'timeout' => 30, + 'ignore_errors' => true, + ], $this->options, ['method' => $this->method])]); + } + + protected function streamContextExec($context) + { + if ($this->method !== Method::HEAD) { + $content = file_get_contents($this->uri->build(), false, $context); + + $this->streamContextParseHeader($http_response_header); + + return $content; + } + + try { + $handler = fopen($this->uri->build(), 'r', null, $context); + + $this->streamContextParseHeader($http_response_header); + + return ''; + } finally { + if (isset($handler) && is_resource($handler)) { + fclose($handler); + } + } + } + + protected function streamContextParseHeader($headers) + { + $this->response->header->parse($headers); + + $this->response->code = $this->response->header->code; + } + + /** + * Construit les parametres de la requete. + * + * @return $this + */ + protected function buildParams() + { + if ($this->isPostMethod()) { + if ($this->isJsonRequest()) { + return $this + ->setHeader('Content-Type', 'application/json') + ->setOption('content', json_encode($this->params)); + } + + return $this + ->setHeader('Content-Type', 'application/x-www-form-urlencoded') + ->setOption('content', http_build_query($this->params)); + } + + return $this->buildUrl(); + } + + /** + * Construit les headers de la requete. + * + * @return $this + */ + protected function buildHeaders() + { + $headers = $this->header->build(); + + return $this->setOption('header', implode(PHP_EOL, $headers)); + } + + /** + * Construit le proxy de la requete + * + * @return $this + */ + protected function buildProxy() + { + if (isset($this->proxy['host'])) { + $uri = new Uri([ + 'scheme' => 'tcp', + 'host' => $this->proxy['host'], + 'port' => isset($this->proxy['port']) ? $this->proxy['port'] : 80 + ]); + + if (isset($this->proxy['access'])) { + $uri->user = $this->proxy['access']; + } + + $this->setOption('proxy', $uri->build()); + } + + return $this; + } + + /** + * Construit les cookies de la requete + * + * @return $this + */ + protected function buildCookies() + { + if (!empty($this->cookies)) { + return $this->setHeader('Cookie', $this->getCookies(true)); + } + + return $this; + } +} diff --git a/src/Http/Provider/StreamContext/Streaming.php b/src/Http/Provider/StreamContext/Streaming.php new file mode 100644 index 0000000..c7a37b5 --- /dev/null +++ b/src/Http/Provider/StreamContext/Streaming.php @@ -0,0 +1,44 @@ +getEmitter(); + + try { + $handler = fopen($this->uri->build(), 'r', null, $context); + + $this->streamContextParseHeader($http_response_header); + + $this->response->providerDatas = stream_get_meta_data($handler); + + $emitter->fire(self::EVENT_START, [$this]); + + $buffer = $this->bufferSize ? $this->bufferSize : 4096; + + while (!feof($handler)) { + $emitter->fire(self::EVENT_PROGRESS, [$this, stream_get_contents($handler, $buffer)]); + } + + $this->response->providerDatas = stream_get_meta_data($handler); + + $emitter->fire(self::EVENT_FINISH, [$this]); + + return true; + } finally { + if (isset($handler) && is_resource($handler)) { + fclose($handler); + } + } + } + +} diff --git a/src/Http/Request.php b/src/Http/Request.php new file mode 100644 index 0000000..4b135f1 --- /dev/null +++ b/src/Http/Request.php @@ -0,0 +1,606 @@ +header = new Header(); + } + + /** + * Retour l'url de la requete, construite avec les parametres si la method HTTP n'est pas POST, PUT, PATCH + * + * @return Uri + */ + public function getUri() + { + if ($this->isPostMethod()) { + return $this->uri; + } + + return $this->buildUrl()->uri; + } + + /** + * Definie l'url de la requete + * + * @param string|Uri $uri + * + * @return $this + */ + public function setUri($uri) + { + $this->uri = new Uri($uri); + + return $this; + } + + /** + * Retourne la method HTTP de la requete + * + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * Definie la method HTTP de la requete + * + * @param string $method + * + * @return $this + */ + public function setMethod($method) + { + $this->method = $method; + + return $this; + } + + /** + * Renvoie si l'on fais un appel HTTP basée sur le POST + * + * @return bool + */ + protected function isPostMethod() + { + $method = $this->getMethod(); + + return $method == 'POST' || $method == 'PUT' || $method == 'PATCH'; + } + + /** + * Est-ce que l'on doit envoyer un body "json" contenant les parametres de la requete + * + * @return bool + */ + public function isJsonRequest() + { + return $this->jsonRequest; + } + + /** + * Definie si l'on doit envoyer un body "json" contenant les parametres de la requete + * + * @param bool $jsonRequest + * + * @return $this + */ + public function setJsonRequest($jsonRequest) + { + $this->jsonRequest = $jsonRequest; + + return $this; + } + + /** + * Retourne les parametres de la requete + * + * @return array + */ + public function getParams() + { + return $this->params; + } + + /** + * Definie, ou ajoute, des parametres de la requete + * + * @param array $parameters + * @param bool $merge Est-ce que l'on ajout les parametres aux parametres existant, ou les ecrases + * + * @return $this + */ + public function setParams($parameters, $merge = false) + { + if ($merge) { + $this->params = array_merge($this->params, $parameters); + } else { + $this->params = $parameters; + } + + return $this; + } + + /** + * Ajout un parametre à la requete + * + * @param string $name + * @param string|array $value + * + * @return $this + */ + public function setParam($name, $value) + { + $this->params[$name] = $value; + + return $this; + } + + /** + * Construit l'url de la requete, Si la method HTTP n'est pas [POST, PUT, PATCH] + * + * @return $this + */ + protected function buildUrl() + { + if ($this->isPostMethod()) { + return $this; + } + + return $this->extendUrl($this->params); + } + + /** + * Ajout des parametres en GET à l'url + * + * @param array $parameters + * + * @return $this + */ + public function extendUrl(array $parameters = []) + { + if (!empty($parameters)) { + $this->uri->extendQuery($parameters); + } + + return $this; + } + + /** + * Definie, ou ajoute, des headers à la requete + * + * @param array $headers + * @param bool $merge Est-ce que l'on ajout les parametres aux parametres existant, ou les ecrases + * + * @return $this + */ + public function setHeaders($headers, $merge = false) + { + $this->header->setHeaders($headers, $merge); + + return $this; + } + + /** + * Ajout un header à la requete + * + * @param string $name + * @param string $value + * + * @return $this + */ + public function setHeader($name, $value) + { + $this->header->set($name, $value); + + return $this; + } + + /** + * Retourne les informations de proxy + * + * @return array + */ + public function getProxy() + { + return $this->proxy; + } + + /** + * Definie les informations de proxy + * + * @param string $host + * @param int $port + * @param string $access + * + * @return $this + */ + public function setProxy($host, $port = 8080, $access = null) + { + $this->proxy = [ + 'host' => $host, + 'port' => $port, + 'access' => $access, + ]; + + return $this; + } + + /** + * Retourne les informations d'authentification + * + * @return Component|null + */ + public function getAuth() + { + return $this->auth; + } + + /** + * Definie les informations d'authentification + * + * @param \Neutrino\Http\Contract\Request\Component $authComponent + * + * @return $this + */ + public function setAuth(Component $authComponent) + { + $this->auth = $authComponent; + + return $this; + } + + /** + * Construit les informations d'authentification de la requete + * + * @return $this + */ + protected function buildAuth() + { + if (isset($this->auth)) { + $this->auth->build($this); + } + + return $this; + } + + /** + * Retourne les cookies + * + * @param bool $format Retourne les cookies formatés + * + * @return array|string + */ + public function getCookies($format = false) + { + if ($format) { + return implode(';', $this->cookies); + } + + return $this->cookies; + } + + /** + * @param array $cookies + * @param bool $merge Est-ce que l'on ajout les $cookies aux $cookies existant, ou les ecrases + * + * @return $this + */ + public function setCookies($cookies, $merge = false) + { + if ($merge) { + $this->cookies = array_merge($this->cookies, $cookies); + } else { + $this->cookies = $cookies; + } + + return $this; + } + + /** + * Ajoute un cookie a la requete + * + * @param null|string $key + * @param string $value + * + * @return $this + * @throws \InvalidArgumentException + */ + public function setCookie($key, $value) + { + if (is_null($key)) { + $this->cookies[] = $value; + } else { + $this->cookies[$key] = $value; + } + + return $this; + } + + /** + * Retourne les options + * + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * Definie, ou ajoute, des options + * + * @param array $options + * @param bool $merge Est-ce que l'on ajoute les options aux options existantes, ou les ecrases + * + * @return $this + */ + public function setOptions($options, $merge = false) + { + if ($merge) { + $this->options = array_merge($this->options, $options); + } else { + $this->options = $options; + } + + return $this; + } + + /** + * Ajout une option CURL + * + * @param string $name + * @param string $value + * + * @return $this + */ + public function setOption($name, $value) + { + $this->options[$name] = $value; + + return $this; + } + + /** + * @return \Neutrino\Http\Response + */ + public function send() + { + $this + ->buildParams() + ->buildAuth() + ->buildProxy() + ->buildCookies() + ->buildHeaders(); + + $this->response = new Response(); + + return $this->makeCall(); + } + + /** + * @param string $method + * @param string $uri + * @param array $params + * @param array $headers + * + * @return $this + */ + public function request($method, $uri, array $params = [], array $headers = []) + { + $this + ->setMethod($method) + ->setUri($uri); + + if (!empty($params)) { + $this->setParams($params, true); + } + if (!empty($headers)) { + $this->setHeaders($headers, true); + } + + return $this; + } + + /** + * @param string $uri + * @param array $params + * @param array $headers + * + * @return $this + */ + public function get($uri, array $params = [], array $headers = []) + { + return $this->request(Method::GET, $uri, $params, $headers); + } + + /** + * @param string $uri + * @param array $params + * @param array $headers + * + * @return $this + */ + public function head($uri, array $params = [], array $headers = []) + { + return $this->request(Method::HEAD, $uri, $params, $headers); + } + + /** + * @param string $uri + * @param array $params + * @param array $headers + * + * @return $this + */ + public function delete($uri, array $params = [], array $headers = []) + { + return $this->request(Method::DELETE, $uri, $params, $headers); + } + + /** + * @param string $uri + * @param array $params + * @param array $headers + * @param bool $json + * + * @return $this + */ + public function post($uri, array $params = [], array $headers = [], $json = false) + { + return $this->request(Method::POST, $uri, $params, $headers)->setJsonRequest($json); + } + + /** + * @param string $uri + * @param array $params + * @param array $headers + * @param bool $json + * + * @return $this + */ + public function put($uri, array $params = [], array $headers = [], $json = false) + { + return $this->request(Method::PUT, $uri, $params, $headers)->setJsonRequest($json); + } + + /** + * @param string $uri + * @param array $params + * @param array $headers + * @param bool $json + * + * @return $this + */ + public function patch($uri, array $params = [], array $headers = [], $json = false) + { + return $this->request(Method::PATCH, $uri, $params, $headers)->setJsonRequest($json); + } + + /** + * Definie le timeout de la requete + * + * @param int $timeout + * + * @return $this + */ + abstract public function setTimeout($timeout); + + /** + * @return \Neutrino\Http\Response + */ + abstract protected function makeCall(); + + /** + * Construit les parametres de la requete. + * + * @return $this + */ + abstract protected function buildParams(); + + /** + * Construit les headers de la requete. + * + * @return $this + */ + abstract protected function buildHeaders(); + + /** + * Construit le proxy de la requete + * + * @return $this + */ + abstract protected function buildProxy(); + + /** + * Construit les cookies de la requete + * + * @return $this + */ + abstract protected function buildCookies(); +} diff --git a/src/Http/Response.php b/src/Http/Response.php new file mode 100644 index 0000000..3a2359f --- /dev/null +++ b/src/Http/Response.php @@ -0,0 +1,93 @@ +header = $header === null ? new Header() : $header; + } + + /** + * Check if the HTTP Response is valid (2xx) + * + * @return bool + */ + public function isOk() + { + return $this->code >= 200 && $this->code < 300; + } + + /** + * Check if an HTTP error append + * + * @return bool + */ + public function isFail() + { + return $this->code < 200 || $this->code >= 300; + } + + /** + * Check if an CURL error append + * + * @return bool + */ + public function isError() + { + return isset($this->errorCode) && ($this->errorCode !== null || $this->errorCode !== 0); + } + + /** + * @param Parserize|string $parser + * + * @return $this + * @throws \RuntimeException + */ + public function parse($parser) + { + if (is_string($parser)) { + $parser = new $parser; + } + + if ($parser instanceof Parserize) { + $this->data = $parser->parse($this->body); + + return $this; + } + + throw new \RuntimeException(__METHOD__ . ': $parserize must implement ' . Parserize::class); + } +} diff --git a/src/Http/Standard/Method.php b/src/Http/Standard/Method.php new file mode 100644 index 0000000..e04c7a3 --- /dev/null +++ b/src/Http/Standard/Method.php @@ -0,0 +1,16 @@ + 'Continue', + self::SWITCHING_PROTOCOLS => 'Switching Protocols', + + // Success 2xx + self::OK => 'OK', + self::CREATED => 'Created', + self::ACCEPTED => 'Accepted', + self::NON_AUTHORITATIVE_INFORMATION => 'Non-Authoritative Information', + self::NO_CONTENT => 'No Content', + self::RESET_CONTENT => 'Reset Content', + self::PARTIAL_CONTENT => 'Partial Content', + self::MULTI_STATUS => 'Multi-Status', + self::ALREADY_REPORTED => 'Already Reported', + self::IM_USED => 'IM Used', + + // Redirection 3xx + self::MULTIPLE_CHOICES => 'Multiple Choices', + self::MOVED_PERMANENTLY => 'Moved Permanently', + self::FOUND => 'Found', // 1.1 + self::SEE_OTHER => 'See Other', + self::NOT_MODIFIED => 'Not Modified', + self::USE_PROXY => 'Use Proxy', + // 306 is deprecated but reserved + self::TEMPORARY_REDIRECT => 'Temporary Redirect', + self::PERMANENT_REDIRECT => 'Permanent Redirect', + + // Client Error 4xx + self::BAD_REQUEST => 'Bad Request', + self::BAD_UNAUTHORIZED => 'Unauthorized', + self::PAYMENT_REQUIRED => 'Payment Required', + self::FORBIDDEN => 'Forbidden', + self::NOT_FOUND => 'Not Found', + self::METHOD_NOT_ALLOWED => 'Method Not Allowed', + self::NOT_ACCEPTABLE => 'Not Acceptable', + self::PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required', + self::REQUEST_TIMEOUT => 'Request Timeout', + self::CONFLICT => 'Conflict', + self::GONE => 'Gone', + self::LENGTH_REQUIRED => 'Length Required', + self::PRECONDITION_FAILED => 'Precondition Failed', + self::REQUEST_ENTITY_TOO_LARGE => 'Request Entity Too Large', + self::REQUEST_URI_TOO_LONG => 'Request-URI Too Long', + self::UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type', + self::REQUEST_RANGE_NOT_SATISFIABLE => 'Requested Range Not Satisfiable', + self::EXPECTATION_FAILED => 'Expectation Failed', + self::I_AM_A_TEAPOT => "I'm a teapot", + self::UNPROCESSABLE_ENTITY => 'Unprocessable Entity', + self::LOCKED => 'Locked', + self::FAILED_DEPENDENCY => 'Failed Dependency', + self::UPDATE_REQUIRED => 'Upgrade Required', + self::PRECONDITION_REQUIRED => 'Precondition Required', + self::TOO_MANY_REQUESTS => 'Too Many Requests', + self::REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large', + + // Server Error 5xx + self::INTERNAL_SERVER_ERROR => 'Internal Server Error', + self::NOT_IMPLEMENTED => 'Not Implemented', + self::BAD_GATEWAY => 'Bad Gateway', + self::SERVICE_UNAVAILABLE => 'Service Unavailable', + self::GATEWAY_TIMEOUT => 'Gateway Timeout', + self::HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version Not Supported', + self::VARIANT_ALSO_NEGOTIATES => 'Variant Also Negotiates', + self::INSUFFICIENT_STORAGE => 'Insufficient Storage', + self::LOOP_DETECTED => 'Loop Detected', + self::BANDWIDTH_LIMIT_EXCEED => 'Bandwidth Limit Exceeded', + self::NOT_EXTENDED => 'Not Extended', + self::NETWORK_AUTHENTICATION_REQUIRED => 'Network Authentication Required', + ]; + + /** + * Get response message + * + * @param int $code + * + * @return string + */ + public static function message($code) + { + return (isset(self::$messages[$code])) ? self::$messages[$code] : ''; + } +} diff --git a/src/Http/Uri.php b/src/Http/Uri.php new file mode 100644 index 0000000..cfcfd7e --- /dev/null +++ b/src/Http/Uri.php @@ -0,0 +1,160 @@ +parts = parse_url($uri); + if (!empty($this->parts['query'])) { + $query = []; + parse_str($this->parts['query'], $query); + $this->parts['query'] = $query; + } + return; + } + if (is_array($uri)) { + $this->parts = $uri; + return; + } + if ($uri instanceof self) { + $this->parts = $uri->parts; + } + } + + public function __toString() + { + return $this->build(); + } + + public function __unset($name) + { + unset($this->parts[$name]); + } + + public function __set($name, $value) + { + $this->parts[$name] = $value; + } + + public function __get($name) + { + return $this->parts[$name]; + } + + public function __isset($name) + { + return isset($this->parts[$name]); + } + + public function build() + { + $uri = ''; + $parts = $this->parts; + if (!empty($parts['scheme'])) { + $uri .= $parts['scheme'] . ':'; + if (!empty($parts['host'])) { + $uri .= '//'; + if (!empty($parts['user'])) { + $uri .= $parts['user']; + if (!empty($parts['pass'])) { + $uri .= ':' . $parts['pass']; + } + $uri .= '@'; + } + $uri .= $parts['host']; + + if (!empty($parts['port'])) { + $uri .= ':' . $parts['port']; + } + } + } + if (!empty($parts['path'])) { + $uri .= $parts['path']; + } + if (!empty($parts['query'])) { + $uri .= '?' . $this->buildQuery($parts['query']); + } + if (!empty($parts['fragment'])) { + $uri .= '#' . $parts['fragment']; + } + return $uri; + } + + public function buildQuery($query) + { + return (is_array($query) ? http_build_query($query) : $query); + } + + public function resolve($uri) + { + $newUri = new self($this); + $newUri->extend($uri); + return $newUri; + } + + public function extend($uri) + { + if (!$uri instanceof self) { + $uri = new self($uri); + } + $this->parts = array_merge( + $this->parts, + array_diff_key($uri->parts, array_flip(['query', 'path'])) + ); + if (!empty($uri->parts['query'])) { + $this->extendQuery($uri->parts['query']); + } + if (!empty($uri->parts['path'])) { + $this->extendPath($uri->parts['path']); + } + return $this; + } + + public function extendQuery(array $params = null) + { + $query = empty($this->parts['query']) ? [] : $this->parts['query']; + $params = empty($params) ? [] : $params; + $this->parts['query'] = array_merge($query, $params); + return $this; + } + + public function extendPath($path) + { + if (empty($path)) { + return $this; + } + if (!strncmp($path, '/', 1)) { + $this->parts['path'] = $path; + return $this; + } + if (empty($this->parts['path'])) { + $this->parts['path'] = '/' . $path; + return $this; + } + $this->parts['path'] = substr($this->parts['path'], 0, strrpos($this->parts['path'], '/') + 1) . $path; + return $this; + } +} diff --git a/tests/Test/Auth/CurlTest.php b/tests/Test/Auth/CurlTest.php new file mode 100644 index 0000000..e3ce8c3 --- /dev/null +++ b/tests/Test/Auth/CurlTest.php @@ -0,0 +1,68 @@ + [CURLAUTH_ANY, 'user', 'pass'], + 'ANYSAFE' => [CURLAUTH_ANYSAFE, 'user', 'pass'], + 'BASIC' => [CURLAUTH_BASIC, 'user', 'pass'], + 'DIGEST' => [CURLAUTH_DIGEST, 'user', 'pass'], + 'NTLM' => [CURLAUTH_NTLM, 'user', 'pass'], + //'NTLM_WB' => [CURLAUTH_NTLM_WB, 'user', 'pass'], + //'NEGOTIATE' => [CURLAUTH_NEGOTIATE, 'user', 'pass'], + 'GSSNEGOTIATE' => [CURLAUTH_GSSNEGOTIATE, 'user', 'pass'], + + 'BASIC|DIGEST' => [CURLAUTH_BASIC | CURLAUTH_DIGEST, 'user', 'pass'], + 'BASIC|DIGEST|GSSNEGOTIATE' => [CURLAUTH_BASIC | CURLAUTH_DIGEST | CURLAUTH_GSSNEGOTIATE, 'user', 'pass'], + ]; + } + + /** + * @dataProvider dataProvider + * + * @param $type + */ + public function testType($type) + { + $this->assertInstanceOf(Auth\Curl::class, new Auth\Curl($type, '', '')); + } + + /** + * @dataProvider dataProvider + * + * @param $type + */ + public function testBuild($type, $user, $pass) + { + $request = new Curl(); + + $auth = new Auth\Curl($type, $user, $pass); + + $auth->build($request); + + $options = $request->getOptions(); + + $this->assertArrayHasKey(CURLOPT_HTTPAUTH, $options); + $this->assertArrayHasKey(CURLOPT_USERPWD, $options); + + $this->assertEquals($type, $options[CURLOPT_HTTPAUTH]); + $this->assertEquals($user . ':' . $pass, $options[CURLOPT_USERPWD]); + } + + /** + * @expectedException \Neutrino\Http\Auth\Exception + */ + public function testWrongType() + { + new Auth\Curl(100000, '', ''); + } +} diff --git a/tests/Test/Event/EmitterTest.php b/tests/Test/Event/EmitterTest.php new file mode 100644 index 0000000..465b160 --- /dev/null +++ b/tests/Test/Event/EmitterTest.php @@ -0,0 +1,45 @@ +assertTrue($emitter->attach('test', $closure)); + + $emitter->fire('test'); + $this->assertEquals(1, $watcher); + + $this->assertTrue($emitter->attach('test', $closure)); + + $emitter->fire('test'); + $this->assertEquals(3, $watcher); + + $this->assertTrue($emitter->detach('test', $closure)); + + $emitter->fire('test'); + $this->assertEquals(4, $watcher); + + $this->assertTrue($emitter->detach('test', $closure)); + $this->assertFalse($emitter->detach('test', $closure)); + + $emitter->fire('test'); + $this->assertEquals(4, $watcher); + + $this->assertTrue($emitter->clear('test')); + $this->assertFalse($emitter->clear('test')); + $this->assertFalse($emitter->detach('test', $closure)); + } +} \ No newline at end of file diff --git a/tests/Test/HeaderTest.php b/tests/Test/HeaderTest.php new file mode 100644 index 0000000..892aee7 --- /dev/null +++ b/tests/Test/HeaderTest.php @@ -0,0 +1,136 @@ +assertEquals($header, $header->set('test', 'test')); + + $header->set('test', 'value'); + $header->set('test_1', 'value_1'); + + $this->assertEquals(['test' => 'value', 'test_1' => 'value_1'], $header->getHeaders()); + + $this->assertTrue($header->has('test')); + $this->assertTrue($header->has('test_1')); + $this->assertFalse($header->has('test_2')); + + $this->assertEquals('value', $header->get('test')); + $this->assertEquals('value_1', $header->get('test_1')); + $this->assertEquals(null, $header->get('test_2')); + $this->assertEquals('value', $header->get('test', 'default')); + $this->assertEquals('default', $header->get('test_2', 'default')); + + $this->assertEquals(2, $header->count()); + $this->assertCount(2, $header); + + $this->assertEquals($header, $header->remove('test')); + + $this->assertEquals(null, $header->get('test')); + + $this->assertEquals(1, $header->count()); + $this->assertCount(1, $header); + + $this->assertEquals($header, $header->setHeaders([ + 'test' => 'value', + 'test_2' => 'value_2' + ], true)); + + $this->assertEquals('value_2', $header->get('test_2', 'default')); + + $this->assertEquals(3, $header->count()); + $this->assertCount(3, $header); + + $this->assertEquals($header, $header->setHeaders([ + 'test' => 'value', + 'test_2' => 'value_2' + ])); + + $this->assertEquals(2, $header->count()); + $this->assertCount(2, $header); + } + + public function testBuild() + { + $header = new Header(); + + $header->set('test', 'test'); + $header->set('test', 'value'); + $header->set('test_1', 'value_1'); + + $this->assertEquals([ + 'test: value', + 'test_1: value_1', + ], $header->build()); + } + + public function dataParse() + { + return [ + [null, null, null, [], ""], + [null, null, null, ['Date' => 'Thu, 27 Apr 2017 13:42:19 GMT'], PHP_EOL . "Date: Thu, 27 Apr 2017 13:42:19 GMT" . PHP_EOL], + ['1.1', 200, 'OK', [], "HTTP/1.1 200 OK"], + ['1.1', 200, 'Success', [], "HTTP/1.1 200 Success"], + ['1.1', 302, 'Redirect', [], "HTTP/1.1 302 Redirect"], + ['1.1', 418, 'I\'m a teapot', [], "HTTP/1.1 418 I'm a teapot"], + ['1.1', 526, 'Whoops', [], "HTTP/1.1 526 Whoops"], + [null, null, null, [ + 'Date' => 'Thu, 27 Apr 2017 13:42:19 GMT', + 'X-Powered-By' => 'PHP/7.0.10', + 'Content-Length' => '5524', + 'Server' => 'Apache/2.4.23 (Win64) PHP/7.0.10', + ], [ + "Date: Thu, 27 Apr 2017 13:42:19 GMT", + "X-Powered-By: PHP/7.0.10\r\n Content-Length: 5524", + "Server: Apache/2.4.23 (Win64) PHP/7.0.10", + ]], + ['1.1', 200, 'OK', [ + 'Date' => 'Thu, 27 Apr 2017 13:42:19 GMT', + 'Server' => 'Apache/2.4.23 (Win64) PHP/7.0.10', + 'X-Powered-By' => 'PHP/7.0.10', + 'Content-Length' => '5524', + 'Content-Type' => 'text/html; charset=UTF-8', + ], implode("\r\n", [ + "HTTP/1.1 200 OK", + 'Date: Thu, 27 Apr 2017 13:42:19 GMT', + 'Server: Apache/2.4.23 (Win64) PHP/7.0.10', + 'X-Powered-By: PHP/7.0.10', + 'Content-Length: 5524', + 'Content-Type: text/html; charset=UTF-8' + ]), + ] + ]; + } + + /** + * @dataProvider dataParse + * + * @param $expectedCode + * @param $expectedStatus + * @param $expectedHeaders + * @param $raw + */ + public function testParse($expectedVersion, $expectedCode, $expectedStatus, $expectedHeaders, $raw) + { + $header = new Header(); + + if (is_array($raw)) { + foreach ($raw as $item) { + $header->parse($item); + } + } else { + $header->parse($raw); + } + + $this->assertEquals($expectedVersion, $header->version); + $this->assertEquals($expectedCode, $header->code); + $this->assertEquals($expectedStatus, $header->status); + $this->assertEquals($expectedHeaders, $header->getHeaders()); + } +} diff --git a/tests/Test/Parser/JsonTest.php b/tests/Test/Parser/JsonTest.php new file mode 100644 index 0000000..fc5fffc --- /dev/null +++ b/tests/Test/Parser/JsonTest.php @@ -0,0 +1,21 @@ +assertEquals((object)['data' => 'test'], $parser->parse(json_encode(['data' => 'test']))); + } +} diff --git a/tests/Test/Parser/XmlArrayTest.php b/tests/Test/Parser/XmlArrayTest.php new file mode 100644 index 0000000..4104ee3 --- /dev/null +++ b/tests/Test/Parser/XmlArrayTest.php @@ -0,0 +1,29 @@ + + +value +value + +XML; + + $this->assertEquals(['test' => ['value', 'value']], $parser->parse($xmlString)); + } +} diff --git a/tests/Test/Parser/XmlTest.php b/tests/Test/Parser/XmlTest.php new file mode 100644 index 0000000..0c144d9 --- /dev/null +++ b/tests/Test/Parser/XmlTest.php @@ -0,0 +1,31 @@ + + +value +value + +XML; + $xml = simplexml_load_string($xmlString); + + $this->assertEquals($xml, $parser->parse($xmlString)); + $this->assertEquals(var_export($xml, true), var_export($parser->parse($xmlString), true)); + } +} diff --git a/tests/Test/Provider/Curl/CurlStreamingTest.php b/tests/Test/Provider/Curl/CurlStreamingTest.php new file mode 100644 index 0000000..8ec4ee0 --- /dev/null +++ b/tests/Test/Provider/Curl/CurlStreamingTest.php @@ -0,0 +1,117 @@ +get('http://127.0.0.1:8000/', ['stream' => true]) + ->setProxy('', null, null)// Force Remove proxy + ->setBufferSize(2048) + ->on(Curl\Streaming::EVENT_START, function (Curl\Streaming $curlStream) use (&$whatcher) { + if (isset($whatcher[Curl\Streaming::EVENT_START])) { + throw new \Exception('EVENT_START already raised'); + } + + $whatcher[Curl\Streaming::EVENT_START] = [ + 'code' => $curlStream->response->code, + 'status' => $curlStream->response->header->status, + 'headers' => $curlStream->response->header->getHeaders(), + ]; + + $whatcher['memory_start'] = memory_get_usage(); + }) + ->on(Curl\Streaming::EVENT_PROGRESS, function (Curl\Streaming $curlStream, $content) use (&$whatcher) { + if (!isset($whatcher[Curl\Streaming::EVENT_PROGRESS])) { + $whatcher[Curl\Streaming::EVENT_PROGRESS] = [ + 'count' => 1, + 'length' => strlen($content) + ]; + } else { + $whatcher[Curl\Streaming::EVENT_PROGRESS]['count']++; + $whatcher[Curl\Streaming::EVENT_PROGRESS]['length'] += strlen($content); + } + + $whatcher['memory_progress'] = memory_get_usage(); + + if ($whatcher['memory_progress'] > $whatcher['memory_start']) { + $delta = $whatcher['memory_progress'] - $whatcher['memory_start']; + if ($delta / $whatcher['memory_start'] > 0.05) { + throw new \Exception("Memory Leak in progress"); + } + } + }) + ->on(Curl\Streaming::EVENT_FINISH, function (Curl\Streaming $curlStream) use (&$whatcher) { + if (isset($whatcher[Curl\Streaming::EVENT_FINISH])) { + throw new \Exception('EVENT_FINISH already raised'); + } + + $whatcher[Curl\Streaming::EVENT_FINISH] = true; + $whatcher['memory_finish'] = memory_get_usage(); + }) + ->send(); + + $response = $curlStream->response; + + $this->assertArrayHasKey(Curl\Streaming::EVENT_START, $whatcher); + $this->assertArrayHasKey(Curl\Streaming::EVENT_PROGRESS, $whatcher); + $this->assertArrayHasKey(Curl\Streaming::EVENT_FINISH, $whatcher); + + $this->assertEquals($whatcher[Curl\Streaming::EVENT_START]['code'], $response->code); + $this->assertEquals($whatcher[Curl\Streaming::EVENT_START]['status'], $response->header->status); + $this->assertEquals($whatcher[Curl\Streaming::EVENT_START]['headers'], $response->header->getHeaders()); + + $this->assertGreaterThanOrEqual(1, $whatcher[Curl\Streaming::EVENT_PROGRESS]['count']); + $this->assertGreaterThanOrEqual(1, $whatcher[Curl\Streaming::EVENT_PROGRESS]['length']); + + $this->assertGreaterThanOrEqual($response->header->get('Content-Length'), $whatcher[Curl\Streaming::EVENT_PROGRESS]['length']); + + if ($whatcher['memory_finish'] > $whatcher['memory_start']) { + $delta = $whatcher['memory_finish'] - $whatcher['memory_start']; + if ($delta / $whatcher['memory_start'] > 0.05) { + throw new \Exception("Memory Leak in progress"); + } + } + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Neutrino\Http\Provider\Curl\Streaming only support start, progress, finish + */ + public function testTryRegisterWrongEvent() + { + $curlStream = new Curl\Streaming(); + + $curlStream->on('test', function (){}); + } + + public function testSetBufferSize() + { + $curlReflectionClass = new \ReflectionClass(Curl\Streaming::class); + $bufferSizeProperty = $curlReflectionClass->getProperty('bufferSize'); + $bufferSizeProperty->setAccessible(true); + + $curlStream = new Curl\Streaming(); + + $curlStream->setBufferSize(2048); + + $bufferSize = $bufferSizeProperty->getValue($curlStream); + + $this->assertEquals(2048, $bufferSize); + } +} diff --git a/tests/Test/Provider/Curl/CurlTest.php b/tests/Test/Provider/Curl/CurlTest.php new file mode 100644 index 0000000..e54713b --- /dev/null +++ b/tests/Test/Provider/Curl/CurlTest.php @@ -0,0 +1,261 @@ + self::makeDataCall(Method::GET, 200), + "HEAD 200" => self::makeDataCall(Method::HEAD, 200), + "DELETE 200" => self::makeDataCall(Method::DELETE, 200), + "POST 200" => self::makeDataCall(Method::POST, 200), + "PUT 200" => self::makeDataCall(Method::PUT, 200), + "PATCH 200" => self::makeDataCall(Method::PATCH, 200), + + "GET 300" => self::makeDataCall(Method::GET, 300), + "GET 400" => self::makeDataCall(Method::GET, 400), + "GET 500" => self::makeDataCall(Method::GET, 500), + "GET 600" => self::makeDataCall(Method::GET, 600), + + "GET 200'Success'" => self::makeDataCall(Method::GET, 200, 'Success'), + + "GET 200 query" => self::makeDataCall(Method::GET, 200, null, ['query' => 'test']), + "HEAD 200 query" => self::makeDataCall(Method::HEAD, 200, null, ['query' => 'test']), + "DELETE 200 query" => self::makeDataCall(Method::DELETE, 200, null, ['query' => 'test']), + "POST 200 query" => self::makeDataCall(Method::POST, 200, null, ['query' => 'test']), + "PUT 200 query" => self::makeDataCall(Method::PUT, 200, null, ['query' => 'test']), + "PATCH 200 query" => self::makeDataCall(Method::PATCH, 200, null, ['query' => 'test']), + + "GET 200 json" => self::makeDataCall(Method::POST, 200, null, ['query' => 'test'], true), + "POST 200 json" => self::makeDataCall(Method::POST, 200, null, ['query' => 'test'], true), + ]; + } + + /** + * @dataProvider dataCall + * + * @param $expected + * @param $method + * @param $url + * @param $params + */ + public function testCall($expected, $method, $url, $params = [], $json = false) + { + if ($method !== Method::HEAD) { + $jsonBody = json_decode($expected['body'], true); + + $jsonBody['header_send']['Accept'] = '*/*'; + ksort($jsonBody['header_send']); + $expected['body'] = json_encode($jsonBody); + } + + $curl = new Curl(); + + $curl + ->request($method, 'http://127.0.0.1:8000' . $url, $params) + ->setJsonRequest($json) + ->setProxy('', null, null)// Force Remove proxy + ->send(); + + $response = $curl->response; + + $this->assertEquals($response->code, $response->header->code); + $this->assertEquals($expected['code'], $response->code); + $this->assertEquals($expected['body'], $response->body); + $this->assertEquals($expected['status'], $response->header->status); + + $header = $response->header; + foreach ($expected['headers'] as $name => $value) { + $this->assertTrue($header->has($name)); + $this->assertEquals($value, $header->get($name)); + } + } + + /** + * @expectedException \Neutrino\Http\Exception + */ + public function testCallFailed() + { + try { + $curl = new Curl(); + + $curl + ->request(Method::GET, 'http://invalid domain') + ->setProxy('', null, null)// Force Remove proxy + ->send(); + + } catch (\Neutrino\Http\Provider\Exception $e) { + $this->assertFalse($e); + } catch (\Neutrino\Http\Exception $e) { + $this->assertEquals(null, $curl->response->code); + $this->assertEquals(null, $curl->response->body); + $this->assertEquals(null, $curl->response->data); + $this->assertEquals($e->getMessage(), $curl->response->error); + $this->assertEquals($e->getCode(), $curl->response->errorCode); + + throw $e; + } + } + + public function testBuildProxy() + { + $reflectionClass = new \ReflectionClass(Curl::class); + $buildProxyMethod = $reflectionClass->getMethod('buildProxy'); + $buildProxyMethod->setAccessible(true); + + $streamCtx = new Curl; + + $streamCtx->setProxy('domain.com'); + + $buildProxyMethod->invoke($streamCtx); + + $options = $streamCtx->getOptions(); + $this->assertArrayHasKey(CURLOPT_PROXY, $options); + $this->assertArrayHasKey(CURLOPT_PROXYPORT, $options); + $this->assertArrayNotHasKey(CURLOPT_PROXYUSERPWD, $options); + $this->assertEquals('domain.com', $options[CURLOPT_PROXY]); + $this->assertEquals(8080, $options[CURLOPT_PROXYPORT]); + + $streamCtx->setProxy('domain.com', 8888); + + $buildProxyMethod->invoke($streamCtx); + + $options = $streamCtx->getOptions(); + $this->assertArrayHasKey(CURLOPT_PROXY, $options); + $this->assertArrayHasKey(CURLOPT_PROXYPORT, $options); + $this->assertArrayNotHasKey(CURLOPT_PROXYUSERPWD, $options); + $this->assertEquals('domain.com', $options[CURLOPT_PROXY]); + $this->assertEquals(8888, $options[CURLOPT_PROXYPORT]); + + $streamCtx->setProxy('domain.com', 8888, 'user:pass'); + + $buildProxyMethod->invoke($streamCtx); + + $options = $streamCtx->getOptions(); + $this->assertArrayHasKey(CURLOPT_PROXY, $options); + $this->assertArrayHasKey(CURLOPT_PROXYPORT, $options); + $this->assertArrayHasKey(CURLOPT_PROXYUSERPWD, $options); + $this->assertEquals('domain.com', $options[CURLOPT_PROXY]); + $this->assertEquals(8888, $options[CURLOPT_PROXYPORT]); + $this->assertEquals('user:pass', $options[CURLOPT_PROXYUSERPWD]); + } + + public function testBuildCookies() + { + $reflectionClass = new \ReflectionClass(Curl::class); + $buildCookiesMethod = $reflectionClass->getMethod('buildCookies'); + $buildCookiesMethod->setAccessible(true); + + $curl = new Curl; + + $curl->setCookie(null, 'biscuit'); + $curl->setCookie(null, 'muffin'); + + $buildCookiesMethod->invoke($curl); + + $options = $curl->getOptions(); + $this->assertArrayHasKey(CURLOPT_COOKIE, $options); + $this->assertEquals(implode(';', ['biscuit', 'muffin']), $options[CURLOPT_COOKIE]); + } + + public function testSetTimeout() + { + $curl = new Curl(); + + $curl->setTimeout(10); + + $options = $curl->getOptions(); + + $this->assertArrayHasKey(CURLOPT_TIMEOUT, $options); + $this->assertEquals(10, $options[CURLOPT_TIMEOUT]); + } + public function testSetConnectTimeout() + { + $curl = new Curl(); + + $curl->setConnectTimeout(10); + + $options = $curl->getOptions(); + + $this->assertArrayHasKey(CURLOPT_CONNECTTIMEOUT, $options); + $this->assertEquals(10, $options[CURLOPT_CONNECTTIMEOUT]); + } + + + public function testOnOff() + { + $streamingReflectionClass = new \ReflectionClass(Curl\Streaming::class); + $emitterProperty = $streamingReflectionClass->getProperty('emitter'); + $emitterProperty->setAccessible(true); + + $curl = new Curl\Streaming(); + + $watcher = []; + + $closureStart = function () use (&$watcher) { + $watcher[] = 'start'; + }; + $closureProgress = function () use (&$watcher) { + $watcher[] = 'progress'; + }; + $closureFinish = function () use (&$watcher) { + $watcher[] = 'finish'; + }; + + $curl->on($curl::EVENT_START, $closureStart); + $curl->on($curl::EVENT_PROGRESS, $closureProgress); + $curl->on($curl::EVENT_FINISH, $closureFinish); + + $emitter = $emitterProperty->getValue($curl); + + + $emitterReflectionClass = new \ReflectionClass(Emitter::class); + $listenerProperty = $emitterReflectionClass->getProperty('listeners'); + $listenerProperty->setAccessible(true); + $listener = $listenerProperty->getValue($emitter); + + $this->assertArrayHasKey($curl::EVENT_START, $listener); + $this->assertArrayHasKey($curl::EVENT_PROGRESS, $listener); + $this->assertArrayHasKey($curl::EVENT_FINISH, $listener); + + $this->assertEquals([$closureStart], $listener[$curl::EVENT_START]); + $this->assertEquals([$closureProgress], $listener[$curl::EVENT_PROGRESS]); + $this->assertEquals([$closureFinish], $listener[$curl::EVENT_FINISH]); + + $curl->off($curl::EVENT_START, $closureStart); + $listener = $listenerProperty->getValue($emitter); + + $this->assertEquals([], $listener[$curl::EVENT_START]); + + $curl->off($curl::EVENT_PROGRESS, $closureProgress); + $listener = $listenerProperty->getValue($emitter); + + $this->assertEquals([], $listener[$curl::EVENT_PROGRESS]); + + $curl->off($curl::EVENT_FINISH, $closureFinish); + $listener = $listenerProperty->getValue($emitter); + + $this->assertEquals([], $listener[$curl::EVENT_FINISH]); + } + + /** + * @expectedException \Neutrino\Http\Provider\Exception + * @expectedExceptionMessage Neutrino\Http\Provider\Curl require curl extension. + */ + public function testAvailabilityFail() + { + $reflection = new \ReflectionClass(Curl::class); + $isAvailableProperty = $reflection->getProperty('isAvailable'); + $isAvailableProperty->setAccessible(true); + $isAvailableProperty->setValue(null, false); + + new Curl; + } +} \ No newline at end of file diff --git a/tests/Test/Provider/Stream/StreamContextStreamingTest.php b/tests/Test/Provider/Stream/StreamContextStreamingTest.php new file mode 100644 index 0000000..ce49111 --- /dev/null +++ b/tests/Test/Provider/Stream/StreamContextStreamingTest.php @@ -0,0 +1,108 @@ +get('http://127.0.0.1:8000/', ['stream' => true]) + ->setBufferSize(2048) + ->on(Curl\Streaming::EVENT_START, function (StreamContext\Streaming $streamCtxStreaming) use (&$whatcher) { + if (isset($whatcher[StreamContext\Streaming::EVENT_START])) { + throw new \Exception('EVENT_START already raised'); + } + + $whatcher[StreamContext\Streaming::EVENT_START] = [ + 'code' => $streamCtxStreaming->response->code, + 'status' => $streamCtxStreaming->response->header->status, + 'headers' => $streamCtxStreaming->response->header->getHeaders(), + ]; + + $whatcher['memory_start'] = memory_get_peak_usage(); + }) + ->on(StreamContext\Streaming::EVENT_PROGRESS, + function (StreamContext\Streaming $streamCtxStreaming, $content) use (&$whatcher) { + if (!isset($whatcher[StreamContext\Streaming::EVENT_PROGRESS])) { + $whatcher[StreamContext\Streaming::EVENT_PROGRESS] = [ + 'count' => 1, + 'length' => strlen($content) + ]; + } else { + $whatcher[StreamContext\Streaming::EVENT_PROGRESS]['count']++; + $whatcher[StreamContext\Streaming::EVENT_PROGRESS]['length'] += strlen($content); + } + + $whatcher['memory_progress'] = memory_get_peak_usage(); + + if ($whatcher['memory_progress'] > $whatcher['memory_start']) { + $delta = $whatcher['memory_progress'] - $whatcher['memory_start']; + if ($delta / $whatcher['memory_start'] > 0.05) { + throw new \Exception("Memory Leak in progress"); + } + } + }) + ->on(StreamContext\Streaming::EVENT_FINISH, function (StreamContext\Streaming $curlStream) use (&$whatcher) { + if (isset($whatcher[StreamContext\Streaming::EVENT_FINISH])) { + throw new \Exception('EVENT_FINISH already raised'); + } + + $whatcher[StreamContext\Streaming::EVENT_FINISH] = true; + $whatcher['memory_finish'] = memory_get_usage(); + }) + ->send(); + + $response = $streamCtxStreaming->response; + + $this->assertArrayHasKey(StreamContext\Streaming::EVENT_START, $whatcher); + $this->assertArrayHasKey(StreamContext\Streaming::EVENT_PROGRESS, $whatcher); + $this->assertArrayHasKey(StreamContext\Streaming::EVENT_FINISH, $whatcher); + + $this->assertEquals($whatcher[StreamContext\Streaming::EVENT_START]['code'], $response->code); + $this->assertEquals($whatcher[StreamContext\Streaming::EVENT_START]['status'], $response->header->status); + $this->assertEquals($whatcher[StreamContext\Streaming::EVENT_START]['headers'], $response->header->getHeaders()); + + $this->assertGreaterThanOrEqual(1, $whatcher[StreamContext\Streaming::EVENT_PROGRESS]['count']); + $this->assertGreaterThanOrEqual(1, $whatcher[StreamContext\Streaming::EVENT_PROGRESS]['length']); + + $this->assertGreaterThanOrEqual($response->header->get('Content-Length'), + $whatcher[StreamContext\Streaming::EVENT_PROGRESS]['length']); + + if ($whatcher['memory_finish'] > $whatcher['memory_start']) { + $delta = $whatcher['memory_finish'] - $whatcher['memory_start']; + if ($delta / $whatcher['memory_start'] > 0.05) { + throw new \Exception("Memory Leak in progress"); + } + } + } + + public function testSetBufferSize() + { + $streamingReflectionClass = new \ReflectionClass(StreamContext\Streaming::class); + $bufferSizeProperty = $streamingReflectionClass->getProperty('bufferSize'); + $bufferSizeProperty->setAccessible(true); + + $streamCtxStreaming = new StreamContext\Streaming(); + + $streamCtxStreaming->setBufferSize(2048); + + $bufferSize = $bufferSizeProperty->getValue($streamCtxStreaming); + + $this->assertEquals(2048, $bufferSize); + } +} diff --git a/tests/Test/Provider/Stream/StreamContextTest.php b/tests/Test/Provider/Stream/StreamContextTest.php new file mode 100644 index 0000000..e8bec6b --- /dev/null +++ b/tests/Test/Provider/Stream/StreamContextTest.php @@ -0,0 +1,259 @@ + self::makeDataCall(Method::GET, 200), + "HEAD 200" => self::makeDataCall(Method::HEAD, 200), + "DELETE 200" => self::makeDataCall(Method::DELETE, 200), + "POST 200" => self::makeDataCall(Method::POST, 200), + "PUT 200" => self::makeDataCall(Method::PUT, 200), + "PATCH 200" => self::makeDataCall(Method::PATCH, 200), + + "GET 300" => self::makeDataCall(Method::GET, 300), + "GET 400" => self::makeDataCall(Method::GET, 400), + "GET 500" => self::makeDataCall(Method::GET, 500), + "GET 600" => self::makeDataCall(Method::GET, 600), + + "GET 200'Success'" => self::makeDataCall(Method::GET, 200, 'Success'), + + "GET 200 query" => self::makeDataCall(Method::GET, 200, null, ['query' => 'test']), + "HEAD 200 query" => self::makeDataCall(Method::HEAD, 200, null, ['query' => 'test']), + "DELETE 200 query" => self::makeDataCall(Method::DELETE, 200, null, ['query' => 'test']), + "POST 200 query" => self::makeDataCall(Method::POST, 200, null, ['query' => 'test']), + "PUT 200 query" => self::makeDataCall(Method::PUT, 200, null, ['query' => 'test']), + "PATCH 200 query" => self::makeDataCall(Method::PATCH, 200, null, ['query' => 'test']), + + "GET 200 json" => self::makeDataCall(Method::POST, 200, null, ['query' => 'test'], true), + "POST 200 json" => self::makeDataCall(Method::POST, 200, null, ['query' => 'test'], true), + ]; + } + + /** + * @dataProvider dataCall + * + * @param $expected + * @param $method + * @param $url + * @param $params + */ + public function testCall($expected, $method, $url, $params = [], $json = false) + { + if ($method !== Method::HEAD) { + $jsonBody = json_decode($expected['body'], true); + + $jsonBody['header_send']['Connection'] = 'close'; + + if (isset($jsonBody['header_send']['Content-Length']) && $jsonBody['header_send']['Content-Length'] == '0') { + unset($jsonBody['header_send']['Content-Length']); + } + + ksort($jsonBody['header_send']); + $expected['body'] = json_encode($jsonBody); + } + + $streamCtx = new StreamContext(); + + $streamCtx + ->request($method, 'http://127.0.0.1:8000' . $url, $params) + ->setJsonRequest($json) + ->send(); + + $response = $streamCtx->response; + + $this->assertEquals($response->code, $response->header->code); + $this->assertEquals($expected['code'], $response->code); + $this->assertEquals($expected['body'], $response->body); + $this->assertEquals($expected['status'], $response->header->status); + + $header = $response->header; + foreach ($expected['headers'] as $name => $value) { + $this->assertTrue($header->has($name)); + $this->assertEquals($value, $header->get($name)); + } + } + + /** + * @expectedException \Neutrino\Http\Exception + */ + public function testCallFailed() + { + try { + $curl = new StreamContext(); + + $curl + ->setMethod('GET') + ->setUri('http://invalid domain') + ->setProxy('', null, null)// Force Remove proxy + ->send(); + + } catch (\Neutrino\Http\Provider\Exception $e) { + $this->assertFalse($e); + } catch (\Neutrino\Http\Exception $e) { + $this->assertEquals(null, $curl->response->code); + $this->assertEquals(null, $curl->response->body); + $this->assertEquals(null, $curl->response->data); + $this->assertEquals($e->getMessage(), $curl->response->error); + $this->assertEquals($e->getCode(), $curl->response->errorCode); + + throw $e; + } + } + + public function testBuildProxy() + { + $reflectionClass = new \ReflectionClass(StreamContext::class); + $buildProxyMethod = $reflectionClass->getMethod('buildProxy'); + $buildProxyMethod->setAccessible(true); + + $streamCtx = new StreamContext; + + $streamCtx->setProxy('domain.com'); + + $buildProxyMethod->invoke($streamCtx); + + $this->assertEquals('tcp://domain.com:8080', $streamCtx->getOptions()['proxy']); + + $streamCtx->setProxy('domain.com', 8888); + + $buildProxyMethod->invoke($streamCtx); + + $this->assertEquals('tcp://domain.com:8888', $streamCtx->getOptions()['proxy']); + + $streamCtx->setProxy('domain.com', 8888, 'user:pass'); + + $buildProxyMethod->invoke($streamCtx); + + $this->assertEquals('tcp://user:pass@domain.com:8888', $streamCtx->getOptions()['proxy']); + } + + public function testBuildCookies() + { + $reflectionClass = new \ReflectionClass(StreamContext::class); + $buildCookiesMethod = $reflectionClass->getMethod('buildCookies'); + $buildCookiesMethod->setAccessible(true); + + $streamCtx = new StreamContext; + + $streamCtx->setCookie(null, 'biscuit'); + $streamCtx->setCookie(null, 'muffin'); + + $buildCookiesMethod->invoke($streamCtx); + + $headerProperty = $reflectionClass->getProperty('header'); + $headerProperty->setAccessible(true); + $header = $headerProperty->getValue($streamCtx); + + $this->assertTrue($header->has('Cookie')); + $this->assertEquals(implode(';', ['biscuit', 'muffin']), $header->get('Cookie')); + } + + public function testSetTimeout() + { + $streamCtx = new StreamContext(); + + $streamCtx->setTimeout(10); + + $options = $streamCtx->getOptions(); + + $this->assertArrayHasKey('timeout', $options); + $this->assertEquals(10, $options['timeout']); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Neutrino\Http\Provider\StreamContext\Streaming only support start, progress, finish + */ + public function testTryRegisterWrongEvent() + { + $streamCtx = new StreamContext\Streaming(); + + $streamCtx->on('test', function () { + }); + } + + public function testOnOff() + { + $streamingReflectionClass = new \ReflectionClass(StreamContext\Streaming::class); + $emitterProperty = $streamingReflectionClass->getProperty('emitter'); + $emitterProperty->setAccessible(true); + + $streamCtx = new StreamContext\Streaming(); + + $watcher = []; + + $closureStart = function () use (&$watcher) { + $watcher[] = 'start'; + }; + $closureProgress = function () use (&$watcher) { + $watcher[] = 'progress'; + }; + $closureFinish = function () use (&$watcher) { + $watcher[] = 'finish'; + }; + + $streamCtx->on($streamCtx::EVENT_START, $closureStart); + $streamCtx->on($streamCtx::EVENT_PROGRESS, $closureProgress); + $streamCtx->on($streamCtx::EVENT_FINISH, $closureFinish); + + $emitter = $emitterProperty->getValue($streamCtx); + + + $emitterReflectionClass = new \ReflectionClass(Emitter::class); + $listenerProperty = $emitterReflectionClass->getProperty('listeners'); + $listenerProperty->setAccessible(true); + $listener = $listenerProperty->getValue($emitter); + + $this->assertArrayHasKey($streamCtx::EVENT_START, $listener); + $this->assertArrayHasKey($streamCtx::EVENT_PROGRESS, $listener); + $this->assertArrayHasKey($streamCtx::EVENT_FINISH, $listener); + + $this->assertEquals([$closureStart], $listener[$streamCtx::EVENT_START]); + $this->assertEquals([$closureProgress], $listener[$streamCtx::EVENT_PROGRESS]); + $this->assertEquals([$closureFinish], $listener[$streamCtx::EVENT_FINISH]); + + $streamCtx->off($streamCtx::EVENT_START, $closureStart); + $listener = $listenerProperty->getValue($emitter); + + $this->assertEquals([], $listener[$streamCtx::EVENT_START]); + + $streamCtx->off($streamCtx::EVENT_PROGRESS, $closureProgress); + $listener = $listenerProperty->getValue($emitter); + + $this->assertEquals([], $listener[$streamCtx::EVENT_PROGRESS]); + + $streamCtx->off($streamCtx::EVENT_FINISH, $closureFinish); + $listener = $listenerProperty->getValue($emitter); + + $this->assertEquals([], $listener[$streamCtx::EVENT_FINISH]); + } + + /** + * @expectedException \Neutrino\Http\Provider\Exception + * @expectedExceptionMessage Neutrino\Http\Provider\StreamContext HTTP or HTTPS stream wrappers not registered. + */ + public function testAvailabilityFail() + { + $reflection = new \ReflectionClass(StreamContext::class); + $isAvailableProperty = $reflection->getProperty('isAvailable'); + $isAvailableProperty->setAccessible(true); + $isAvailableProperty->setValue(null, false); + + new StreamContext; + } +} diff --git a/tests/Test/Provider/TestCase.php b/tests/Test/Provider/TestCase.php new file mode 100644 index 0000000..cd7ec52 --- /dev/null +++ b/tests/Test/Provider/TestCase.php @@ -0,0 +1,48 @@ + $code, + 'status' => $statusMessage, + 'body' => '', + 'headers' => [ + 'Status-Code' => $statusCode, + 'Request-Method' => $method + ] + ]; + + $header_send = [ + 'Host' => '127.0.0.1:8000', + ]; + + if ($method === Method::POST || $method === Method::PATCH || $method === Method::PUT) { + $header_send['Content-Type'] = $json ? 'application/json' : 'application/x-www-form-urlencoded'; + $header_send['Content-Length'] = '' . strlen($json ? json_encode($params) : http_build_query($params)); + } + + if ($method !== Method::HEAD) { + $expected['body'] = json_encode([ + 'header_send' => $header_send, + 'query' => $params + ]); + } + + return [$expected, $method, "/$code" . (!empty($status) ? "/" . trim($status) : ''), $params, $json]; + } +} diff --git a/tests/Test/RequestTest.php b/tests/Test/RequestTest.php new file mode 100644 index 0000000..a558827 --- /dev/null +++ b/tests/Test/RequestTest.php @@ -0,0 +1,386 @@ +setUri('http://www.google.com/'); + + $this->assertEquals(new Uri('http://www.google.com/'), $request->getUri()); + + $request + ->setUri('http://www.google.com/') + ->setParams(['test' => 'test']); + + $this->assertEquals(new Uri('http://www.google.com/?test=test'), $request->getUri()); + + $request + ->setMethod(Method::POST) + ->setUri('http://www.google.com/') + ->setParams(['test' => 'test']); + + $this->assertEquals(new Uri('http://www.google.com/'), $request->getUri()); + } + + public function testParams() + { + $request = new _Fake\FakeRequest(); + + $request->setParams([ + 'test' => 'value', + 'test1' => 'value1', + ]); + + $this->assertEquals([ + 'test' => 'value', + 'test1' => 'value1', + ], $request->getParams()); + + $request->setParam('test', 'test'); + + $this->assertEquals([ + 'test' => 'test', + 'test1' => 'value1', + ], $request->getParams()); + + $request->setParams([ + 'test' => 'value', + 'test2' => 'value2', + 'test3' => 'value3', + ], true); + + $this->assertEquals([ + 'test' => 'value', + 'test1' => 'value1', + 'test2' => 'value2', + 'test3' => 'value3' + ], $request->getParams()); + + + $request->setParams([ + 'test1' => 'value1', + 'test2' => 'value2', + 'test3' => 'value3', + ]); + + $this->assertEquals([ + 'test1' => 'value1', + 'test2' => 'value2', + 'test3' => 'value3' + ], $request->getParams()); + } + + public function testAuth() + { + $request = new _Fake\FakeRequest(); + + $request->setAuth(new Auth\Basic('user', 'pass')); + + $this->assertEquals(new Auth\Basic('user', 'pass'), $request->getAuth()); + + $reflectionClass = new \ReflectionClass(_Fake\FakeRequest::class); + + $buildAuthMethod = $reflectionClass->getMethod('buildAuth'); + $buildAuthMethod->setAccessible(true); + $buildAuthMethod->invoke($request); + + $headerProperty = $reflectionClass->getProperty('header'); + $headerProperty->setAccessible(true); + $header = $headerProperty->getValue($request); + + $this->assertTrue($header->has('Authorization')); + $this->assertEquals($header->get('Authorization'), 'Basic ' . base64_encode('user:pass')); + } + + public function testProxy() + { + $request = new _Fake\FakeRequest(); + + $request->setProxy('domain.com'); + + $this->assertEquals([ + 'host' => 'domain.com', + 'port' => 8080, + 'access' => null + ], $request->getProxy()); + + $request->setProxy('domain.com', 8888); + + $this->assertEquals([ + 'host' => 'domain.com', + 'port' => 8888, + 'access' => null + ], $request->getProxy()); + + $request->setProxy('domain.com', 8888, 'user:pass'); + + $this->assertEquals([ + 'host' => 'domain.com', + 'port' => 8888, + 'access' => 'user:pass' + ], $request->getProxy()); + } + + public function testOptions() + { + $request = new _Fake\FakeRequest(); + + $request->setOptions([ + 'test' => 'value', + 'test1' => 'value1', + ]); + + $this->assertEquals([ + 'test' => 'value', + 'test1' => 'value1', + ], $request->getOptions()); + + $request->setOption('test', 'test'); + + $this->assertEquals([ + 'test' => 'test', + 'test1' => 'value1', + ], $request->getOptions()); + + $request->setOptions([ + 'test' => 'value', + 'test2' => 'value2', + 'test3' => 'value3', + ], true); + + $this->assertEquals([ + 'test' => 'value', + 'test1' => 'value1', + 'test2' => 'value2', + 'test3' => 'value3' + ], $request->getOptions()); + + + $request->setOptions([ + 'test1' => 'value1', + 'test2' => 'value2', + 'test3' => 'value3', + ]); + + $this->assertEquals([ + 'test1' => 'value1', + 'test2' => 'value2', + 'test3' => 'value3' + ], $request->getOptions()); + } + + public function testHeader() + { + $request = new _Fake\FakeRequest(); + + $reflectionClass = new \ReflectionClass(_Fake\FakeRequest::class); + + $headerProperty = $reflectionClass->getProperty('header'); + $headerProperty->setAccessible(true); + $header = $headerProperty->getValue($request); + + $request->setHeaders([ + 'test' => 'value', + 'test1' => 'value1', + ]); + + $this->assertEquals([ + 'test' => 'value', + 'test1' => 'value1', + ], $header->getHeaders()); + + $request->setHeader('test', 'test'); + + $this->assertEquals([ + 'test' => 'test', + 'test1' => 'value1', + ], $header->getHeaders()); + + $request->setHeaders([ + 'test' => 'value', + 'test2' => 'value2', + 'test3' => 'value3', + ], true); + + $this->assertEquals([ + 'test' => 'value', + 'test1' => 'value1', + 'test2' => 'value2', + 'test3' => 'value3' + ], $header->getHeaders()); + + + $request->setHeaders([ + 'test1' => 'value1', + 'test2' => 'value2', + 'test3' => 'value3', + ]); + + $this->assertEquals([ + 'test1' => 'value1', + 'test2' => 'value2', + 'test3' => 'value3' + ], $header->getHeaders()); + } + + public function testCookies() + { + $request = new _Fake\FakeRequest(); + + $request->setCookies([ + 'test' => 'value', + 'test1' => 'value1', + ]); + + $this->assertEquals([ + 'test' => 'value', + 'test1' => 'value1', + ], $request->getCookies()); + + $request->setCookie('test', 'test'); + $request->setCookie(null, 'test'); + + $this->assertEquals([ + 0 => 'test', + 'test' => 'test', + 'test1' => 'value1', + ], $request->getCookies()); + + $request->setCookies([ + 'test' => 'value', + 'test2' => 'value2', + 'test3' => 'value3', + ], true); + + $this->assertEquals([ + 0 => 'test', + 'test' => 'value', + 'test1' => 'value1', + 'test2' => 'value2', + 'test3' => 'value3' + ], $request->getCookies()); + + $request->setCookies([ + 'test1' => 'value1', + 'test2' => 'value2', + 'test3' => 'value3', + ]); + + $this->assertEquals([ + 'test1' => 'value1', + 'test2' => 'value2', + 'test3' => 'value3' + ], $request->getCookies()); + + $this->assertEquals(implode(';', [ + 'test1' => 'value1', + 'test2' => 'value2', + 'test3' => 'value3' + ]), $request->getCookies(true)); + } + + public function testJson() + { + $request = new _Fake\FakeRequest(); + + $request->setJsonRequest(true); + + $this->assertTrue($request->isJsonRequest()); + + $request->setJsonRequest(false); + + $this->assertFalse($request->isJsonRequest()); + } + + public function testBuildUrl() + { + $reflectionClass = new \ReflectionClass(_Fake\FakeRequest::class); + $buildUrlMethod = $reflectionClass->getMethod('buildUrl'); + $buildUrlMethod->setAccessible(true); + + $request = new _Fake\FakeRequest(); + + $request->setUri('http://www.google.com/'); + + $request->setMethod(Method::POST); + + $buildUrlMethod->invoke($request); + + $this->assertEquals([], $request->called); + + $request->setMethod(Method::GET); + + $buildUrlMethod->invoke($request); + + $this->assertEquals(['extendUrl'], $request->called); + } + + public function testSend() + { + $request = new _Fake\FakeRequest(); + + $request->send(); + + $this->assertEquals([ + 'buildParams', + 'buildAuth', + 'buildProxy', + 'buildCookies', + 'buildHeaders', + 'makeCall' + ], $request->called); + } + + public function dataRequest() + { + return [ + [Method::GET, '/', ['q' => 'q'], ['Accept' => '*/*'], '/?q=q'], + [Method::HEAD, '/', ['q' => 'q'], ['Accept' => '*/*'], '/?q=q'], + [Method::DELETE, '/', ['q' => 'q'], ['Accept' => '*/*'], '/?q=q'], + [Method::POST, '/', ['q' => 'q'], ['Accept' => '*/*'], '/'], + [Method::PUT, '/', ['q' => 'q'], ['Accept' => '*/*'], '/'], + [Method::PATCH, '/', ['q' => 'q'], ['Accept' => '*/*'], '/'], + ]; + } + + /** + * @dataProvider dataRequest + */ + public function testRequest($method, $url, $params, $headers, $expectedUri) + { + $reflectionClass = new \ReflectionClass(_Fake\FakeRequest::class); + + $headerProperty = $reflectionClass->getProperty('header'); + $headerProperty->setAccessible(true); + + $request = new _Fake\FakeRequest(); + + $this->assertEquals($request, $request->{strtolower($method)}($url, $params, $headers)); + + $this->assertEquals($method, $request->getMethod()); + $this->assertEquals($expectedUri, $request->getUri()->build()); + $this->assertEquals($params, $request->getParams()); + $this->assertEquals($headers, $headerProperty->getValue($request)->getHeaders()); + + $this->assertEquals($request, $request->request($method, $url, $params, $headers)); + + $this->assertEquals($method, $request->getMethod()); + $this->assertEquals($expectedUri, $request->getUri()->build()); + $this->assertEquals($params, $request->getParams()); + $this->assertEquals($headers, $headerProperty->getValue($request)->getHeaders()); + } +} diff --git a/tests/Test/ResponseTest.php b/tests/Test/ResponseTest.php new file mode 100644 index 0000000..f841a6c --- /dev/null +++ b/tests/Test/ResponseTest.php @@ -0,0 +1,90 @@ +code = 200; + $this->assertTrue($response->isOk()); + $this->assertFalse($response->isFail()); + $this->assertFalse($response->isError()); + + $response->code = 300; + $this->assertFalse($response->isOk()); + $this->assertTrue($response->isFail()); + $this->assertFalse($response->isError()); + + $response->code = 400; + $this->assertFalse($response->isOk()); + $this->assertTrue($response->isFail()); + $this->assertFalse($response->isError()); + + $response->code = 500; + $this->assertFalse($response->isOk()); + $this->assertTrue($response->isFail()); + $this->assertFalse($response->isError()); + + $response->code = 600; + $this->assertFalse($response->isOk()); + $this->assertTrue($response->isFail()); + $this->assertFalse($response->isError()); + + $response->errorCode = 1; + $this->assertFalse($response->isOk()); + $this->assertTrue($response->isFail()); + $this->assertTrue($response->isError()); + } + + public function testParse() + { + $data = ['int' => 123, 'str' => 'abc', 'null' => null]; + + $response = new Response(); + + $response->body = json_encode($data); + + $this->assertEquals(null, $response->data); + + $response->parse(Json::class); + + $this->assertEquals((object)$data, $response->data); + + $response->parse(JsonArray::class); + + $this->assertEquals($data, $response->data); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Neutrino\Http\Response::parse: $parserize must implement Neutrino\Http\Contract\Parser\Parserize + */ + public function testParseException() + { + $response = new Response(); + + $response->parse([]); + } +} + +class JsonArray implements Parserize +{ + + public function parse($raw) + { + return json_decode($raw, true); + } +} \ No newline at end of file diff --git a/tests/Test/UriTest.php b/tests/Test/UriTest.php new file mode 100644 index 0000000..7c3bc05 --- /dev/null +++ b/tests/Test/UriTest.php @@ -0,0 +1,260 @@ +getProperty('parts'); + $partsProperty->setAccessible(true); + + return $partsProperty->getValue($uri); + } + + public function dataConstruct() + { + return [ + [[], null], + [[], ''], + [[ + 'path' => '/' + ], '/'], + [[ + 'path' => '/', + 'query' => ['q' => 's'], + ], '/?q=s'], + [[ + 'path' => '/', + 'query' => ['q' => 'a'], + ], '/?q=s&q=a'], + [[ + 'path' => '/', + 'query' => ['q' => ['s', 'a']], + ], '/?q[]=s&q[]=a'], + [[ + 'scheme' => 'http', + 'host' => 'www.domain.com', + 'path' => '/', + 'query' => ['q' => ['s', 'a']], + ], 'http://www.domain.com/?q[]=s&q[]=a'], + [[ + 'scheme' => 'http', + 'host' => 'www.domain.com', + 'port' => '8080', + 'path' => '/path', + 'query' => ['q' => ['s', 'a']], + ], 'http://www.domain.com:8080/path?q[]=s&q[]=a'], + [[ + 'scheme' => 'http', + 'host' => 'www.domain.com', + 'user' => 'user', + 'pass' => 'pass', + 'path' => '/path', + 'query' => ['q' => ['s', 'a']], + ], 'http://user:pass@www.domain.com/path?q[]=s&q[]=a'], + [[ + 'scheme' => 'http', + 'user' => 'user', + 'pass' => 'pass', + 'host' => 'www.domain.com', + 'port' => '8080', + 'path' => '/path', + 'fragment' => 'frag', + 'query' => ['q' => ['s', 'a']], + ], 'http://user:pass@www.domain.com:8080/path?q[]=s&q[]=a#frag'], + + [[ + 'path' => '/', + 'query' => ['q' => 's'], + ], new Uri('/?q=s')], + + [[ + 'path' => '/', + 'query' => ['q' => 's'], + ], [ + 'path' => '/', + 'query' => ['q' => 's'], + ]], + ]; + } + + /** + * @dataProvider dataConstruct + * + * @param $expectedParts + * @param $url + */ + public function testBasic($expectedParts, $url) + { + $uri = new Uri($url); + + $parts = $this->getPartsProperty($uri); + + $this->assertEquals($expectedParts, $parts); + + foreach ($expectedParts as $key => $part) { + $this->assertTrue(isset($uri->$key)); + $this->assertEquals($part, $uri->$key); + + $uri->$key = 'test'; + + $parts = $this->getPartsProperty($uri); + $this->assertEquals('test', $parts[$key]); + $this->assertEquals('test', $uri->$key); + + unset($uri->$key); + + $parts = $this->getPartsProperty($uri); + $this->assertArrayNotHasKey($key, $parts); + $this->assertFalse(isset($uri->$key)); + } + + $uri->test = 'test'; + + $parts = $this->getPartsProperty($uri); + $this->assertArrayHasKey('test', $parts); + $this->assertEquals('test', $parts['test']); + $this->assertEquals('test', $uri->test); + + unset($uri->test); + + $parts = $this->getPartsProperty($uri); + $this->assertArrayNotHasKey('test', $parts); + $this->assertFalse(isset($uri->test)); + } + + public function dataBuild() + { + return [ + ['', null], + ['', ''], + ['/', '/'], + ['/?q=s', '/?q=s'], + ['/?q=a', '/?q=s&q=a'], + ['/?q%5B0%5D=s&q%5B1%5D=a', '/?q[]=s&q[]=a'], + ['http://www.domain.com/?q%5B0%5D=s&q%5B1%5D=a', 'http://www.domain.com/?q[]=s&q[]=a'], + ['http://www.domain.com/path?q%5B0%5D=s&q%5B1%5D=a', 'http://www.domain.com/path?q[]=s&q[]=a'], + ['http://user:pass@www.domain.com/path?q%5B0%5D=s&q%5B1%5D=a', 'http://user:pass@www.domain.com/path?q[]=s&q[]=a'], + ['http://user:pass@www.domain.com:8080/path?q%5B0%5D=s&q%5B1%5D=a#frag', 'http://user:pass@www.domain.com:8080/path?q[]=s&q[]=a#frag'], + + ['/?q=s', new Uri('/?q=s')], + + ['/?q=s', [ + 'path' => '/', + 'query' => ['q' => 's'], + ]], + ['/?q%5B0%5D=s&q%5B1%5D=a', [ + 'path' => '/', + 'query' => ['q' => ['s', 'a']], + ]], + ['http://domain.com:8080/?q%5B0%5D=s&q%5B1%5D=a', [ + 'scheme' => 'http', + 'host' => 'domain.com', + 'port' => '8080', + 'path' => '/', + 'query' => ['q' => ['s', 'a']], + ]], + ]; + } + + /** + * @dataProvider dataBuild + * @param $expectedUri + * @param $uri + */ + public function testBuild($expectedUri, $uri) + { + $uri = new Uri($uri); + $this->assertEquals($expectedUri, $uri->build()); + $this->assertEquals($expectedUri, (string)$uri); + } + + public function dataExtendQuery() + { + return [ + [[], '', null], + [['q' => 's'], '?q=s', null], + [['q' => 'q'], '?q[]=s&q[]=a', ['q' => 'q']], + [['q' => ['q', 's']], '?q=s', ['q' => ['q', 's']]], + [['q' => ['q', 's']], 'http://www.domain.com/?q=a', ['q' => ['q', 's']]], + ]; + } + + /** + * @dataProvider dataExtendQuery + * + * @param $expectedQuery + * @param $uri + * @param $query + */ + public function testExtendQuery($expectedQuery, $uri, $query) + { + $uri = new Uri($uri); + $uri->extendQuery($query); + $this->assertEquals($expectedQuery, $uri->query); + } + + public function dataExtendPath() + { + return [ + ['/', '/', null], + ['/path', '', 'path'], + ['/path', '/path', null], + ['/query', '/path', '/query'], + ['/path/query', '/path/path', 'query'], + ['/foo/bar/last', 'http://phalconphp.com/foo/bar/baz?var1=a&var2=1', 'last'], + ]; + } + + /** + * @dataProvider dataExtendPath + * + * @param $expectedPath + * @param $uri + * @param $path + */ + public function testExtendPath($expectedPath, $uri, $path) + { + $uri = new Uri($uri); + $uri->extendPath($path); + $this->assertEquals($expectedPath, $uri->path); + } + + public function dataResolve() + { + return [ + ['/', '/', null], + ['/path', '/path', null], + ['/query', '/path', '/query'], + ['/path/query', '/path/path', 'query'], + ['http://domain.com/path', '/path', 'http://domain.com'], + ['http://domain.com/path', 'http://domain.com', '/path'], + ['http://domain.com/foo/bar/last?var1=a&var2=1', 'http://domain.com/foo/bar/baz?var1=a&var2=1', 'last'], + ['http://sub.domain.com/foo/bar/baz?var1=a&var2=1', 'http://domain.com/foo/bar/baz?var1=a&var2=1', 'http://sub.domain.com'], + ['http://domain.com/foo/bar/baz?var1=a&var2=1&q=1', 'http://domain.com/foo/bar/baz?var1=a&var2=1', '?q=1'], + ]; + } + + /** + * @dataProvider dataResolve + * + * @param $expectedUri + * @param $uri + * @param $resolve + */ + public function testResolve($expectedUri, $uri, $resolve) + { + $uri = new Uri($uri); + $newUri = $uri->resolve($resolve); + $this->assertEquals($expectedUri, (string)$newUri); + } +} diff --git a/tests/Test/_Fake/FakeRequest.php b/tests/Test/_Fake/FakeRequest.php new file mode 100644 index 0000000..f8099d1 --- /dev/null +++ b/tests/Test/_Fake/FakeRequest.php @@ -0,0 +1,110 @@ +setOption('timeout', $timeout); + } + + /** + * @return \Neutrino\Http\Response + */ + protected function makeCall() + { + $this->called[] = __FUNCTION__; + + $this->response->body = json_encode($this->options); + + return $this->response; + } + + /** + * Construit les parametres de la requete. + * + * @return $this + */ + protected function buildParams() + { + $this->called[] = __FUNCTION__; + + if (!empty($this->params)) { + + if ($this->isPostMethod()) { + if ($this->isJsonRequest()) { + return $this->setOption('params', json_encode($this->params)); + } + + return $this->setOption('params', $this->params); + } + + $this->uri->extendQuery($this->params); + } + + return $this; + } + + /** + * Construit les headers de la requete. + * + * @return $this + */ + protected function buildHeaders() + { + $this->called[] = __FUNCTION__; + + return $this->setOption('headers', $this->header->build()); + } + + /** + * Construit le proxy de la requete + * + * @return $this + */ + protected function buildProxy() + { + $this->called[] = __FUNCTION__; + + return $this->setOption('proxy', $this->proxy); + } + + /** + * Construit les cookies de la requete + * + * @return $this + */ + protected function buildCookies() + { + $this->called[] = __FUNCTION__; + + return $this->setOption('cookies', $this->getCookies(true)); + } + + protected function buildAuth() + { + $this->called[] = __FUNCTION__; + + return parent::buildAuth(); + } + + public function extendUrl(array $parameters = []) + { + $this->called[] = __FUNCTION__; + + return parent::extendUrl($parameters); + } + +} \ No newline at end of file diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..dacc064 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,4 @@ +path, '/')); + +$httpCode = !empty($parts[0]) ? $parts[0] : 200; +$httpMessage = !empty($parts[1]) ? $parts[1] : StatusCode::message($httpCode); +$method = $_SERVER['REQUEST_METHOD']; +header("HTTP/1.1 $httpCode $httpMessage"); +header("Status-Code: $httpCode $httpMessage"); +header("Request-Method: {$method}"); + +$headers = []; +foreach ($_SERVER as $key => $value) { + if (substr($key, 0, 5) <> 'HTTP_') { + continue; + } + $header = str_replace(' ', '-', ucwords(str_replace('_', ' ', strtolower(substr($key, 5))))); + $headers[$header] = $value; +} + +$jsonRequest = isset($headers['Content-Type']) && $headers['Content-Type'] === 'application/json'; + +switch ($method) { + case Method::GET: + case Method::DELETE: + case Method::HEAD: + $query = $_GET; + break; + case Method::POST: + if ($jsonRequest) { + $query = json_decode(file_get_contents("php://input"), true); + } else { + $query = $_POST; + } + break; + default: + if ($jsonRequest) { + $query = json_decode(file_get_contents("php://input"), true); + } else { + parse_str(urldecode(file_get_contents("php://input")), $query); + } +} + +if (!empty($query)) { + if (isset($query['stream'])) { + $output = implode('', range('1', '9')) . PHP_EOL; + $loop = 10000; + + header('Content-Length: ' . (strlen($output) * $loop)); + + for ($i = 0; $i < $loop; $i++) { + echo $output; + ob_flush(); + flush(); + } + } +} + +ksort($headers); + +echo json_encode(['header_send' => $headers, 'query' => $query]);