PageRenderTime 57ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Codeception/Module/REST.php

https://github.com/pmcjury/Codeception
PHP | 653 lines | 316 code | 55 blank | 282 comment | 48 complexity | 5968af37d9dfc8040647cdbdb1707957 MD5 | raw file
  1. <?php
  2. namespace Codeception\Module;
  3. use Symfony\Component\BrowserKit\Cookie;
  4. use Codeception\Exception\ModuleConfig as ModuleConfigException;
  5. /**
  6. * Module for testing REST WebService.
  7. *
  8. * This module can be used either with frameworks or PHPBrowser.
  9. * It tries to guess the framework is is attached to.
  10. *
  11. * Whether framework is used it operates via standard framework modules.
  12. * Otherwise sends raw HTTP requests to url via PHPBrowser.
  13. *
  14. * ## Status
  15. *
  16. * * Maintainer: **tiger-seo**, **davert**
  17. * * Stability: **stable**
  18. * * Contact: codecept@davert.mail.ua
  19. * * Contact: tiger.seo@gmail.com
  20. *
  21. * ## Configuration
  22. *
  23. * * url *optional* - the url of api
  24. *
  25. * This module requires PHPBrowser or any of Framework modules enabled.
  26. *
  27. * ### Example
  28. *
  29. * modules:
  30. * enabled: [PhpBrowser, REST]
  31. * config:
  32. * PHPBrowser:
  33. url: http://serviceapp/
  34. * REST:
  35. * url: 'http://serviceapp/api/v1/'
  36. *
  37. * ## Public Properties
  38. *
  39. * * headers - array of headers going to be sent.
  40. * * params - array of sent data
  41. * * response - last response (string)
  42. *
  43. *
  44. */
  45. class REST extends \Codeception\Module
  46. {
  47. protected $config = array(
  48. 'url' => '',
  49. 'xdebug_remote' => false
  50. );
  51. /**
  52. * @var \Symfony\Component\BrowserKit\Client
  53. */
  54. public $client = null;
  55. public $isFunctional = false;
  56. public $headers = array();
  57. public $params = array();
  58. public $response = "";
  59. public function _before(\Codeception\TestCase $test)
  60. {
  61. if (!$this->client) {
  62. if (!strpos($this->config['url'], '://')) {
  63. // not valid url
  64. foreach ($this->getModules() as $module) {
  65. if ($module instanceof \Codeception\Lib\Framework) {
  66. $this->client = $module->client;
  67. $this->isFunctional = true;
  68. $this->client->setServerParameters(array());
  69. break;
  70. }
  71. }
  72. } else {
  73. if (!$this->hasModule('PhpBrowser')) {
  74. throw new ModuleConfigException(__CLASS__, "For REST testing via HTTP please enable PhpBrowser module");
  75. }
  76. $this->client = &$this->getModule('PhpBrowser')->client;
  77. }
  78. }
  79. $this->headers = array();
  80. $this->params = array();
  81. $this->response = "";
  82. if ($this->config['xdebug_remote']
  83. && function_exists('xdebug_is_enabled')
  84. && ini_get('xdebug.remote_enable')
  85. && !$this->isFunctional
  86. ) {
  87. $cookie = new Cookie('XDEBUG_SESSION', $this->config['xdebug_remote'], null, '/');
  88. $this->client->getCookieJar()->set($cookie);
  89. }
  90. }
  91. /**
  92. * Sets HTTP header
  93. *
  94. * @param $name
  95. * @param $value
  96. */
  97. public function haveHttpHeader($name, $value)
  98. {
  99. $this->headers[$name] = $value;
  100. }
  101. /**
  102. * Checks over the given HTTP header and (optionally)
  103. * its value, asserting that are there
  104. *
  105. * @param $name
  106. * @param $value
  107. */
  108. public function seeHttpHeader($name, $value = null)
  109. {
  110. if ($value) {
  111. \PHPUnit_Framework_Assert::assertEquals(
  112. $this->client->getInternalResponse()->getHeader($name),
  113. $value
  114. );
  115. }
  116. else {
  117. \PHPUnit_Framework_Assert::assertNotNull($this->client->getInternalResponse()->getHeader($name));
  118. }
  119. }
  120. /**
  121. * Checks over the given HTTP header and (optionally)
  122. * its value, asserting that are not there
  123. *
  124. * @param $name
  125. * @param $value
  126. */
  127. public function dontSeeHttpHeader($name, $value = null) {
  128. if ($value) {
  129. \PHPUnit_Framework_Assert::assertNotEquals(
  130. $this->client->getInternalResponse()->getHeader($name),
  131. $value
  132. );
  133. }
  134. else {
  135. \PHPUnit_Framework_Assert::assertNull($this->client->getInternalResponse()->getHeader($name));
  136. }
  137. }
  138. /**
  139. * Checks that http response header is received only once.
  140. * HTTP RFC2616 allows multiple response headers with the same name.
  141. * You can check that you didn't accidentally sent the same header twice.
  142. *
  143. * ``` php
  144. * <?php
  145. * $I->seeHttpHeaderOnce('Cache-Control');
  146. * ?>>
  147. * ```
  148. *
  149. * @param $name
  150. */
  151. public function seeHttpHeaderOnce($name)
  152. {
  153. $headers = $this->client->getInternalResponse()->getHeader($name, false);
  154. $this->assertEquals(1, count($headers));
  155. }
  156. /**
  157. * Returns the value of the specified header name
  158. *
  159. * @param $name
  160. * @param Boolean $first Whether to return the first value or all header values
  161. *
  162. * @return string|array The first header value if $first is true, an array of values otherwise
  163. */
  164. public function grabHttpHeader($name, $first = true) {
  165. return $this->client->getInternalResponse()->getHeader($name, $first);
  166. }
  167. /**
  168. * Adds HTTP authentication via username/password.
  169. *
  170. * @param $username
  171. * @param $password
  172. */
  173. public function amHttpAuthenticated($username, $password)
  174. {
  175. if ($this->isFunctional) {
  176. $this->client->setServerParameter('PHP_AUTH_USER', $username);
  177. $this->client->setServerParameter('PHP_AUTH_PW', $password);
  178. } else {
  179. $this->client->setAuth($username, $password);
  180. }
  181. }
  182. /**
  183. * Adds Digest authentication via username/password.
  184. *
  185. * @param $username
  186. * @param $password
  187. */
  188. public function amDigestAuthenticated($username, $password)
  189. {
  190. $this->client->setAuth($username, $password, CURLAUTH_DIGEST);
  191. }
  192. /**
  193. * Sends a POST request to given uri.
  194. *
  195. * Parameters and files (as array of filenames) can be provided.
  196. *
  197. * @param $url
  198. * @param array $params
  199. * @param array $files
  200. */
  201. public function sendPOST($url, $params = array(), $files = array())
  202. {
  203. $this->execute('POST', $url, $params, $files);
  204. }
  205. /**
  206. * Sends a HEAD request to given uri.
  207. *
  208. * @param $url
  209. * @param array $params
  210. */
  211. public function sendHEAD($url, $params = array())
  212. {
  213. $this->execute('HEAD', $url, $params);
  214. }
  215. /**
  216. * Sends an OPTIONS request to given uri.
  217. *
  218. * @param $url
  219. * @param array $params
  220. */
  221. public function sendOPTIONS($url, $params = array())
  222. {
  223. $this->execute('OPTIONS', $url, $params);
  224. }
  225. /**
  226. * Sends a GET request to given uri.
  227. *
  228. * @param $url
  229. * @param array $params
  230. */
  231. public function sendGET($url, $params = array())
  232. {
  233. $this->execute('GET', $url, $params);
  234. }
  235. /**
  236. * Sends PUT request to given uri.
  237. *
  238. * @param $url
  239. * @param array $params
  240. * @param array $files
  241. */
  242. public function sendPUT($url, $params = array(), $files = array())
  243. {
  244. $this->execute('PUT', $url, $params, $files);
  245. }
  246. /**
  247. * Sends PATCH request to given uri.
  248. *
  249. * @param $url
  250. * @param array $params
  251. * @param array $files
  252. */
  253. public function sendPATCH($url, $params = array(), $files = array())
  254. {
  255. $this->execute('PATCH', $url, $params, $files);
  256. }
  257. /**
  258. * Sends DELETE request to given uri.
  259. *
  260. * @param $url
  261. * @param array $params
  262. * @param array $files
  263. */
  264. public function sendDELETE($url, $params = array(), $files = array())
  265. {
  266. $this->execute('DELETE', $url, $params, $files);
  267. }
  268. /**
  269. * Sets Headers "Link" as one header "Link" based on linkEntries
  270. *
  271. * @param array $linkEntries (entry is array with keys "uri" and "link-param")
  272. *
  273. * @link http://tools.ietf.org/html/rfc2068#section-19.6.2.4
  274. *
  275. * @author samva.ua@gmail.com
  276. */
  277. private function setHeaderLink(array $linkEntries)
  278. {
  279. $values = array();
  280. foreach ($linkEntries as $linkEntry) {
  281. \PHPUnit_Framework_Assert::assertArrayHasKey(
  282. 'uri',
  283. $linkEntry,
  284. 'linkEntry should contain property "uri"'
  285. );
  286. \PHPUnit_Framework_Assert::assertArrayHasKey(
  287. 'link-param',
  288. $linkEntry,
  289. 'linkEntry should contain property "link-param"'
  290. );
  291. $values[] = $linkEntry['uri'] . '; ' . $linkEntry['link-param'];
  292. }
  293. $this->headers['Link'] = join(', ', $values);
  294. }
  295. /**
  296. * Sends LINK request to given uri.
  297. *
  298. * @param $url
  299. * @param array $linkEntries (entry is array with keys "uri" and "link-param")
  300. *
  301. * @link http://tools.ietf.org/html/rfc2068#section-19.6.2.4
  302. *
  303. * @author samva.ua@gmail.com
  304. */
  305. public function sendLINK($url, array $linkEntries)
  306. {
  307. $this->setHeaderLink($linkEntries);
  308. $this->execute('LINK', $url);
  309. }
  310. /**
  311. * Sends UNLINK request to given uri.
  312. *
  313. * @param $url
  314. * @param array $linkEntries (entry is array with keys "uri" and "link-param")
  315. * @link http://tools.ietf.org/html/rfc2068#section-19.6.2.4
  316. * @author samva.ua@gmail.com
  317. */
  318. public function sendUNLINK($url, array $linkEntries)
  319. {
  320. $this->setHeaderLink($linkEntries);
  321. $this->execute('UNLINK', $url);
  322. }
  323. protected function execute($method = 'GET', $url, $parameters = array(), $files = array())
  324. {
  325. foreach ($this->headers as $header => $val) {
  326. $header = str_replace('-','_',strtoupper($header));
  327. $this->client->setServerParameter("HTTP_$header", $val);
  328. # Issue #827 - symfony foundation requires 'CONTENT_TYPE' without HTTP_
  329. if ($this->isFunctional and $header == 'CONTENT_TYPE') {
  330. $this->client->setServerParameter($header, $val);
  331. }
  332. }
  333. // allow full url to be requested
  334. $url = (strpos($url, '://') === false ? $this->config['url'] : '') . $url;
  335. $parameters = $this->encodeApplicationJson($method, $parameters);
  336. if (is_array($parameters) || $method == 'GET') {
  337. if (!empty($parameters) && $method == 'GET') {
  338. $url .= '?' . http_build_query($parameters);
  339. }
  340. if($method == 'GET') {
  341. $this->debugSection("Request", "$method $url");
  342. } else {
  343. $this->debugSection("Request", "$method $url ".json_encode($parameters));
  344. }
  345. $this->client->request($method, $url, $parameters, $files);
  346. } else {
  347. $this->debugSection("Request", "$method $url " . $parameters);
  348. $this->client->request($method, $url, array(), $files, array(), $parameters);
  349. }
  350. $this->response = (string)$this->client->getInternalResponse()->getContent();
  351. $this->debugSection("Response", $this->response);
  352. if (count($this->client->getInternalRequest()->getCookies())) {
  353. $this->debugSection('Cookies', $this->client->getInternalRequest()->getCookies());
  354. }
  355. $this->debugSection("Headers", $this->client->getInternalResponse()->getHeaders());
  356. $this->debugSection("Status", $this->client->getInternalResponse()->getStatus());
  357. }
  358. protected function encodeApplicationJson($method, $parameters)
  359. {
  360. if (is_array($parameters) || $parameters instanceof \ArrayAccess) {
  361. $parameters = $this->scalarizeArray($parameters);
  362. if (array_key_exists('Content-Type', $this->headers)
  363. && $this->headers['Content-Type'] === 'application/json'
  364. && $method != 'GET'
  365. ) {
  366. return json_encode($parameters);
  367. }
  368. }
  369. return $parameters;
  370. }
  371. /**
  372. * Checks whether last response was valid JSON.
  373. * This is done with json_last_error function.
  374. *
  375. */
  376. public function seeResponseIsJson()
  377. {
  378. json_decode($this->response);
  379. \PHPUnit_Framework_Assert::assertEquals(
  380. 0,
  381. $num = json_last_error(),
  382. "json decoding error #$num, see http://php.net/manual/en/function.json-last-error.php"
  383. );
  384. }
  385. /**
  386. * Checks whether last response was valid XML.
  387. * This is done with libxml_get_last_error function.
  388. *
  389. */
  390. public function seeResponseIsXml()
  391. {
  392. libxml_use_internal_errors(true);
  393. $doc = simplexml_load_string($this->response);
  394. $num="";
  395. $title="";
  396. if ($doc===false) {
  397. $error = libxml_get_last_error();
  398. $num=$error->code;
  399. $title=trim($error->message);
  400. libxml_clear_errors();
  401. }
  402. libxml_use_internal_errors(false);
  403. \PHPUnit_Framework_Assert::assertNotSame(false,
  404. $doc ,
  405. "xml decoding error #$num with message \"$title\", see http://www.xmlsoft.org/html/libxml-xmlerror.html"
  406. );
  407. }
  408. /**
  409. * Checks whether the last response contains text.
  410. *
  411. * @param $text
  412. */
  413. public function seeResponseContains($text)
  414. {
  415. \PHPUnit_Framework_Assert::assertContains($text, $this->response, "REST response contains");
  416. }
  417. /**
  418. * Checks whether last response do not contain text.
  419. *
  420. * @param $text
  421. */
  422. public function dontSeeResponseContains($text)
  423. {
  424. \PHPUnit_Framework_Assert::assertNotContains($text, $this->response, "REST response contains");
  425. }
  426. /**
  427. * Checks whether the last JSON response contains provided array.
  428. * The response is converted to array with json_decode($response, true)
  429. * Thus, JSON is represented by associative array.
  430. * This method matches that response array contains provided array.
  431. *
  432. * Examples:
  433. *
  434. * ``` php
  435. * <?php
  436. * // response: {name: john, email: john@gmail.com}
  437. * $I->seeResponseContainsJson(array('name' => 'john'));
  438. *
  439. * // response {user: john, profile: { email: john@gmail.com }}
  440. * $I->seeResponseContainsJson(array('email' => 'john@gmail.com'));
  441. *
  442. * ?>
  443. * ```
  444. *
  445. * This method recursively checks if one array can be found inside of another.
  446. *
  447. * @param array $json
  448. */
  449. public function seeResponseContainsJson($json = array())
  450. {
  451. $resp_json = json_decode($this->response, true);
  452. \PHPUnit_Framework_Assert::assertTrue(
  453. $this->arrayHasArray($json, $resp_json),
  454. "Response JSON contains provided\n"
  455. ."- ".print_r($json, true)
  456. ."+ ".print_r($resp_json, true)
  457. );
  458. }
  459. /**
  460. * Returns current response so that it can be used in next scenario steps.
  461. *
  462. * Example:
  463. *
  464. * ``` php
  465. * <?php
  466. * $user_id = $I->grabResponse();
  467. * $I->sendPUT('/user', array('id' => $user_id, 'name' => 'davert'));
  468. * ?>
  469. * ```
  470. *
  471. * @version 1.1
  472. * @return string
  473. */
  474. public function grabResponse()
  475. {
  476. return $this->response;
  477. }
  478. /**
  479. * Returns data from the current JSON response using specified path
  480. * so that it can be used in next scenario steps
  481. *
  482. * Example:
  483. *
  484. * ``` php
  485. * <?php
  486. * $user_id = $I->grabDataFromJsonResponse('user.user_id');
  487. * $I->sendPUT('/user', array('id' => $user_id, 'name' => 'davert'));
  488. * ?>
  489. * ```
  490. *
  491. * @param string $path
  492. *
  493. * @since 1.1.2
  494. * @return string
  495. *
  496. * @author tiger.seo@gmail.com
  497. */
  498. public function grabDataFromJsonResponse($path)
  499. {
  500. $data = $response = json_decode($this->response, true);
  501. if (json_last_error() !== JSON_ERROR_NONE) {
  502. $this->fail('Response is not of JSON format or is malformed');
  503. $this->debugSection('Response', $this->response);
  504. }
  505. if ($path === '') {
  506. return $data;
  507. }
  508. foreach (explode('.', $path) as $key) {
  509. if (!is_array($data) || !array_key_exists($key, $data)) {
  510. $this->fail('Response does not have required data');
  511. $this->debugSection('Response', $response);
  512. }
  513. $data = $data[$key];
  514. }
  515. return $data;
  516. }
  517. /**
  518. * @author nleippe@integr8ted.com
  519. * @author tiger.seo@gmail.com
  520. * @link http://www.php.net/manual/en/function.array-intersect-assoc.php#39822
  521. *
  522. * @param mixed $arr1
  523. * @param mixed $arr2
  524. *
  525. * @return array|bool
  526. */
  527. private function arrayIntersectAssocRecursive($arr1, $arr2)
  528. {
  529. if (!is_array($arr1) || !is_array($arr2)) {
  530. return null;
  531. }
  532. $commonkeys = array_intersect(array_keys($arr1), array_keys($arr2));
  533. $ret = array();
  534. foreach ($commonkeys as $key) {
  535. $_return = $this->arrayIntersectAssocRecursive($arr1[$key], $arr2[$key]);
  536. if ($_return) {
  537. $ret[$key] = $_return;
  538. continue;
  539. }
  540. if ($arr1[$key] === $arr2[$key]) {
  541. $ret[$key] = $arr1[$key];
  542. }
  543. }
  544. if (empty($commonkeys)) {
  545. foreach ($arr2 as $arr) {
  546. $_return = $this->arrayIntersectAssocRecursive($arr1, $arr);
  547. if ($_return && $_return == $arr1) return $_return;
  548. }
  549. }
  550. return $ret;
  551. }
  552. protected function arrayHasArray(array $needle, array $haystack)
  553. {
  554. return $needle == $this->arrayIntersectAssocRecursive($needle, $haystack);
  555. }
  556. /**
  557. * Opposite to seeResponseContainsJson
  558. *
  559. * @param array $json
  560. */
  561. public function dontSeeResponseContainsJson($json = array())
  562. {
  563. $resp_json = json_decode($this->response, true);
  564. \PHPUnit_Framework_Assert::assertFalse(
  565. $this->arrayHasArray($json, $resp_json),
  566. "Response JSON does not contain JSON provided\n"
  567. ."- ".print_r($json, true)
  568. ."+ ".print_r($resp_json, true)
  569. );
  570. }
  571. /**
  572. * Checks if response is exactly the same as provided.
  573. *
  574. * @param $response
  575. */
  576. public function seeResponseEquals($response)
  577. {
  578. \PHPUnit_Framework_Assert::assertEquals($response, $this->response);
  579. }
  580. /**
  581. * Checks response code equals to provided value.
  582. *
  583. * @param $code
  584. */
  585. public function seeResponseCodeIs($code)
  586. {
  587. $this->assertEquals($code, $this->client->getInternalResponse()->getStatus());
  588. }
  589. /**
  590. * Checks that response code is not equal to provided value.
  591. *
  592. * @param $code
  593. */
  594. public function dontSeeResponseCodeIs($code)
  595. {
  596. $this->assertNotEquals($code, $this->client->getInternalResponse()->getStatus());
  597. }
  598. }