1. sfTesterResponse.class.php
  2. /** * sfTesterResponse implements tests for the symfony response object. * * @package symfony * @subpackage test * @author Fabien Potencier * @version SVN: $Id: sfTesterResponse.class.php 27061 2010-01-22 17:08:04Z FabianLange $ */
  3. class sfTesterResponse extends sfTester
  4. {
  5. protected
  6. $response = null,
  7. $dom = null,
  8. $domCssSelector = null;
  9. /**
  10. * Prepares the tester.
  11. */
  12. public function prepare()
  13. {
  14. }
  15. /**
  16. * Initializes the tester.
  17. */
  18. public function initialize()
  19. {
  20. $this->response = $this->browser->getResponse();
  21. $this->dom = null;
  22. $this->domCssSelector = null;
  23. if (preg_match('/(x|ht)ml/i', $this->response->getContentType(), $matches))
  24. {
  25. $this->dom = new DOMDocument('1.0', $this->response->getCharset());
  26. $this->dom->validateOnParse = true;
  27. if ('x' == $matches[1])
  28. {
  29. @$this->dom->loadXML($this->response->getContent());
  30. }
  31. else
  32. {
  33. @$this->dom->loadHTML($this->response->getContent());
  34. }
  35. $this->domCssSelector = new sfDomCssSelector($this->dom);
  36. }
  37. }
  38. /**
  39. * Tests that the response matches a given CSS selector.
  40. *
  41. * @param string $selector The response selector or a sfDomCssSelector object
  42. * @param mixed $value Flag for the selector
  43. * @param array $options Options for the current test
  44. *
  45. * @return sfTestFunctionalBase|sfTester
  46. */
  47. public function checkElement($selector, $value = true, $options = array())
  48. {
  49. if (null === $this->dom)
  50. {
  51. throw new LogicException('The DOM is not accessible because the browser response content type is not HTML.');
  52. }
  53. if (is_object($selector))
  54. {
  55. $values = $selector->getValues();
  56. }
  57. else
  58. {
  59. $values = $this->domCssSelector->matchAll($selector)->getValues();
  60. }
  61. if (false === $value)
  62. {
  63. $this->tester->is(count($values), 0, sprintf('response selector "%s" does not exist', $selector));
  64. }
  65. else if (true === $value)
  66. {
  67. $this->tester->cmp_ok(count($values), '>', 0, sprintf('response selector "%s" exists', $selector));
  68. }
  69. else if (is_int($value))
  70. {
  71. $this->tester->is(count($values), $value, sprintf('response selector "%s" matches "%s" times', $selector, $value));
  72. }
  73. else if (preg_match('/^(!)?([^a-zA-Z0-9\\\\]).+?\\2[ims]?$/', $value, $match))
  74. {
  75. $position = isset($options['position']) ? $options['position'] : 0;
  76. if ($match[1] == '!')
  77. {
  78. $this->tester->unlike(@$values[$position], substr($value, 1), sprintf('response selector "%s" does not match regex "%s"', $selector, substr($value, 1)));
  79. }
  80. else
  81. {
  82. $this->tester->like(@$values[$position], $value, sprintf('response selector "%s" matches regex "%s"', $selector, $value));
  83. }
  84. }
  85. else
  86. {
  87. $position = isset($options['position']) ? $options['position'] : 0;
  88. $this->tester->is(@$values[$position], $value, sprintf('response selector "%s" matches "%s"', $selector, $value));
  89. }
  90. if (isset($options['count']))
  91. {
  92. $this->tester->is(count($values), $options['count'], sprintf('response selector "%s" matches "%s" times', $selector, $options['count']));
  93. }
  94. return $this->getObjectToReturn();
  95. }
  96. /**
  97. * Checks that a form is rendered correctly.
  98. *
  99. * @param sfForm|string $form A form object or the name of a form class
  100. * @param string $selector CSS selector for the root form element for this form
  101. *
  102. * @return sfTestFunctionalBase|sfTester
  103. */
  104. public function checkForm($form, $selector = 'form')
  105. {
  106. if (!$form instanceof sfForm)
  107. {
  108. $form = new $form();
  109. }
  110. $rendered = array();
  111. foreach ($this->domCssSelector->matchAll(sprintf('%1$s input, %1$s textarea, %1$s select', $selector))->getNodes() as $element)
  112. {
  113. $rendered[] = $element->getAttribute('name');
  114. }
  115. foreach ($form as $field => $widget)
  116. {
  117. $dom = new DOMDocument('1.0', sfConfig::get('sf_charset'));
  118. $dom->loadHTML((string) $widget);
  119. foreach ($dom->getElementsByTagName('*') as $element)
  120. {
  121. if (in_array($element->tagName, array('input', 'select', 'textarea')))
  122. {
  123. if (false !== $pos = array_search($element->getAttribute('name'), $rendered))
  124. {
  125. unset($rendered[$pos]);
  126. }
  127. $this->tester->ok(false !== $pos, sprintf('response includes "%s" form "%s" field - "%s %s[name=%s]"', get_class($form), $field, $selector, $element->tagName, $element->getAttribute('name')));
  128. }
  129. }
  130. }
  131. return $this->getObjectToReturn();
  132. }
  133. /**
  134. * Validates the response.
  135. *
  136. * @param mixed $checkDTD Either true to validate against the response DTD or
  137. * provide the path to a *.xsd, *.rng or *.rnc schema
  138. *
  139. * @return sfTestFunctionalBase|sfTester
  140. *
  141. * @throws LogicException If the response is neither XML nor (X)HTML
  142. */
  143. public function isValid($checkDTD = false)
  144. {
  145. if (preg_match('/(x|ht)ml/i', $this->response->getContentType()))
  146. {
  147. $revert = libxml_use_internal_errors(true);
  148. $dom = new DOMDocument('1.0', $this->response->getCharset());
  149. $content = $this->response->getContent();
  150. if (true === $checkDTD)
  151. {
  152. $cache = sfConfig::get('sf_cache_dir').'/sf_tester_response/w3';
  153. if ($cache[1] == ':')
  154. {
  155. // On Windows systems the path will be like c:\symfony\cache\xml.dtd
  156. // I did not manage to get DOMDocument loading a file protocol url including the drive letter
  157. // file://c:\symfony\cache\xml.dtd or file://c:/symfony/cache/xml.dtd
  158. // The first one simply doesnt work, the second one is treated as remote call.
  159. // However the following works. Unfortunatly this means we can only access the current disk
  160. // file:///symfony/cache/xml.dtd
  161. // Note that all work for file_get_contents so the bug is most likely in DOMDocument.
  162. $local = 'file://'.substr(str_replace(DIRECTORY_SEPARATOR, '/', $cache), 2);
  163. }
  164. else
  165. {
  166. $local = 'file://'.$cache;
  167. }
  168. if (!file_exists($cache.'/TR/xhtml11/DTD/xhtml11.dtd'))
  169. {
  170. $filesystem = new sfFilesystem();
  171. $finder = sfFinder::type('any')->discard('.sf');
  172. $filesystem->mirror(dirname(__FILE__).'/w3', $cache, $finder);
  173. $finder = sfFinder::type('file');
  174. $filesystem->replaceTokens($finder->in($cache), '##', '##', array('LOCAL_W3' => $local));
  175. }
  176. $content = preg_replace('#(<!DOCTYPE[^>]+")http://www.w3.org(.*")#i', '\\1'.$local.'\\2', $content);
  177. $dom->validateOnParse = $checkDTD;
  178. }
  179. $dom->loadXML($content);
  180. switch (pathinfo($checkDTD, PATHINFO_EXTENSION))
  181. {
  182. case 'xsd':
  183. $dom->schemaValidate($checkDTD);
  184. $message = sprintf('response validates per XSD schema "%s"', basename($checkDTD));
  185. break;
  186. case 'rng':
  187. case 'rnc':
  188. $dom->relaxNGValidate($checkDTD);
  189. $message = sprintf('response validates per relaxNG schema "%s"', basename($checkDTD));
  190. break;
  191. default:
  192. $message = $dom->validateOnParse ? sprintf('response validates as "%s"', $dom->doctype->name) : 'response is well-formed "xml"';
  193. }
  194. if (count($errors = libxml_get_errors()))
  195. {
  196. $lines = explode(PHP_EOL, $this->response->getContent());
  197. $this->tester->fail($message);
  198. foreach ($errors as $error)
  199. {
  200. $this->tester->diag(' '.trim($error->message));
  201. if (preg_match('/line (\d+)/', $error->message, $match) && $error->line != $match[1])
  202. {
  203. $this->tester->diag(' '.str_pad($match[1].':', 6).trim($lines[$match[1] - 1]));
  204. }
  205. $this->tester->diag(' '.str_pad($error->line.':', 6).trim($lines[$error->line - 1]));
  206. }
  207. }
  208. else
  209. {
  210. $this->tester->pass($message);
  211. }
  212. libxml_use_internal_errors($revert);
  213. }
  214. else
  215. {
  216. throw new LogicException(sprintf('Unable to validate responses of content type "%s"', $this->response->getContentType()));
  217. }
  218. return $this->getObjectToReturn();
  219. }
  220. /**
  221. * Tests for a response header.
  222. *
  223. * @param string $key
  224. * @param string $value
  225. *
  226. * @return sfTestFunctionalBase|sfTester
  227. */
  228. public function isHeader($key, $value)
  229. {
  230. $headers = explode(', ', $this->response->getHttpHeader($key));
  231. $ok = false;
  232. $regex = false;
  233. $mustMatch = true;
  234. if (preg_match('/^(!)?([^a-zA-Z0-9\\\\]).+?\\2[ims]?$/', $value, $match))
  235. {
  236. $regex = $value;
  237. if ($match[1] == '!')
  238. {
  239. $mustMatch = false;
  240. $regex = substr($value, 1);
  241. }
  242. }
  243. foreach ($headers as $header)
  244. {
  245. if (false !== $regex)
  246. {
  247. if ($mustMatch)
  248. {
  249. if (preg_match($regex, $header))
  250. {
  251. $ok = true;
  252. $this->tester->pass(sprintf('response header "%s" matches "%s" (%s)', $key, $value, $this->response->getHttpHeader($key)));
  253. break;
  254. }
  255. }
  256. else
  257. {
  258. if (preg_match($regex, $header))
  259. {
  260. $ok = true;
  261. $this->tester->fail(sprintf('response header "%s" does not match "%s" (%s)', $key, $value, $this->response->getHttpHeader($key)));
  262. break;
  263. }
  264. }
  265. }
  266. else if ($header == $value)
  267. {
  268. $ok = true;
  269. $this->tester->pass(sprintf('response header "%s" is "%s" (%s)', $key, $value, $this->response->getHttpHeader($key)));
  270. break;
  271. }
  272. }
  273. if (!$ok)
  274. {
  275. if (!$mustMatch)
  276. {
  277. $this->tester->pass(sprintf('response header "%s" matches "%s" (%s)', $key, $value, $this->response->getHttpHeader($key)));
  278. }
  279. else
  280. {
  281. $this->tester->fail(sprintf('response header "%s" matches "%s" (%s)', $key, $value, $this->response->getHttpHeader($key)));
  282. }
  283. }
  284. return $this->getObjectToReturn();
  285. }
  286. /**
  287. * Tests if a cookie was set.
  288. *
  289. * @param string $name
  290. * @param string $value
  291. * @param array $attributes Other cookie attributes to check (expires, path, domain, etc)
  292. *
  293. * @return sfTestFunctionalBase|sfTester
  294. */
  295. public function setsCookie($name, $value = null, $attributes = array())
  296. {
  297. foreach ($this->response->getCookies() as $cookie)
  298. {
  299. if ($name == $cookie['name'])
  300. {
  301. if (null === $value)
  302. {
  303. $this->tester->pass(sprintf('response sets cookie "%s"', $name));
  304. }
  305. else
  306. {
  307. $this->tester->ok($value == $cookie['value'], sprintf('response sets cookie "%s" to "%s"', $name, $value));
  308. }
  309. foreach ($attributes as $attributeName => $attributeValue)
  310. {
  311. if (!array_key_exists($attributeName, $cookie))
  312. {
  313. throw new LogicException(sprintf('The cookie attribute "%s" is not valid.', $attributeName));
  314. }
  315. $this->tester->is($cookie[$attributeName], $attributeValue, sprintf('"%s" cookie "%s" attribute is "%s"', $name, $attributeName, $attributeValue));
  316. }
  317. return $this->getObjectToReturn();
  318. }
  319. }
  320. $this->tester->fail(sprintf('response sets cookie "%s"', $name));
  321. return $this->getObjectToReturn();
  322. }
  323. /**
  324. * Tests the response content against a regex.
  325. *
  326. * @param string Regex
  327. *
  328. * @return sfTestFunctionalBase|sfTester
  329. */
  330. public function matches($regex)
  331. {
  332. if (!preg_match('/^(!)?([^a-zA-Z0-9\\\\]).+?\\2[ims]?$/', $regex, $match))
  333. {
  334. throw new InvalidArgumentException(sprintf('"%s" is not a valid regular expression.', $regex));
  335. }
  336. if ($match[1] == '!')
  337. {
  338. $this->tester->unlike($this->response->getContent(), substr($regex, 1), sprintf('response content does not match regex "%s"', substr($regex, 1)));
  339. }
  340. else
  341. {
  342. $this->tester->like($this->response->getContent(), $regex, sprintf('response content matches regex "%s"', $regex));
  343. }
  344. return $this->getObjectToReturn();
  345. }
  346. /**
  347. * Tests the status code.
  348. *
  349. * @param string $statusCode Status code to check, default 200
  350. *
  351. * @return sfTestFunctionalBase|sfTester
  352. */
  353. public function isStatusCode($statusCode = 200)
  354. {
  355. $this->tester->is($this->response->getStatusCode(), $statusCode, sprintf('status code is "%s"', $statusCode));
  356. return $this->getObjectToReturn();
  357. }
  358. /**
  359. * Tests if the current request has been redirected.
  360. *
  361. * @param bool $boolean Flag for redirection mode
  362. *
  363. * @return sfTestFunctionalBase|sfTester
  364. */
  365. public function isRedirected($boolean = true)
  366. {
  367. if ($location = $this->response->getHttpHeader('location'))
  368. {
  369. $boolean ? $this->tester->pass(sprintf('page redirected to "%s"', $location)) : $this->tester->fail(sprintf('page redirected to "%s"', $location));
  370. }
  371. else
  372. {
  373. $boolean ? $this->tester->fail('page redirected') : $this->tester->pass('page not redirected');
  374. }
  375. return $this->getObjectToReturn();
  376. }
  377. /**
  378. * Outputs some debug information about the current response.
  379. *
  380. * @param string $realOutput Whether to display the actual content of the response when an error occurred
  381. * or the exception message and the stack trace to ease debugging
  382. */
  383. public function debug($realOutput = false)
  384. {
  385. print $this->tester->error('Response debug');
  386. if (!$realOutput && null !== sfException::getLastException())
  387. {
  388. // print the exception and the stack trace instead of the "normal" output
  389. $this->tester->comment('WARNING');
  390. $this->tester->comment('An error occurred when processing this request.');
  391. $this->tester->comment('The real response content has been replaced with the exception message to ease debugging.');
  392. }
  393. printf("HTTP/1.X %s\n", $this->response->getStatusCode());
  394. foreach ($this->response->getHttpHeaders() as $name => $value)
  395. {
  396. printf("%s: %s\n", $name, $value);
  397. }
  398. foreach ($this->response->getCookies() as $cookie)
  399. {
  400. vprintf("Set-Cookie: %s=%s; %spath=%s%s%s%s\n", array(
  401. $cookie['name'],
  402. $cookie['value'],
  403. null === $cookie['expire'] ? '' : sprintf('expires=%s; ', date('D d-M-Y H:i:s T', $cookie['expire'])),
  404. $cookie['path'],
  405. $cookie['domain'] ? sprintf('; domain=%s', $cookie['domain']) : '',
  406. $cookie['secure'] ? '; secure' : '',
  407. $cookie['httpOnly'] ? '; HttpOnly' : '',
  408. ));
  409. }
  410. echo "\n";
  411. if (!$realOutput && null !== $exception = sfException::getLastException())
  412. {
  413. echo $exception;
  414. }
  415. else
  416. {
  417. echo $this->response->getContent();
  418. }
  419. echo "\n";
  420. exit(1);
  421. }
  422. }

Debug toolbar