PageRenderTime 54ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/tests/PHPUnit/IntegrationTestCase.php

https://github.com/CodeYellowBV/piwik
PHP | 1031 lines | 725 code | 116 blank | 190 comment | 104 complexity | 8f6f41a2cbf55b79cea793697f8eaaf6 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. */
  8. use Piwik\API\DocumentationGenerator;
  9. use Piwik\API\Proxy;
  10. use Piwik\API\Request;
  11. use Piwik\ArchiveProcessor\Rules;
  12. use Piwik\Common;
  13. use Piwik\Config;
  14. use Piwik\DataAccess\ArchiveTableCreator;
  15. use Piwik\Db;
  16. use Piwik\DbHelper;
  17. use Piwik\ReportRenderer;
  18. use Piwik\Translate;
  19. use Piwik\UrlHelper;
  20. require_once PIWIK_INCLUDE_PATH . '/libs/PiwikTracker/PiwikTracker.php';
  21. /**
  22. * Base class for Integration tests.
  23. *
  24. * Provides helpers to track data and then call API get* methods to check outputs automatically.
  25. *
  26. */
  27. abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase
  28. {
  29. public $defaultApiNotToCall = array(
  30. 'LanguagesManager',
  31. 'DBStats',
  32. 'Dashboard',
  33. 'UsersManager',
  34. 'SitesManager',
  35. 'ExampleUI',
  36. 'Overlay',
  37. 'Live',
  38. 'SEO',
  39. 'ExampleAPI',
  40. 'ScheduledReports',
  41. 'MobileMessaging',
  42. 'Transitions',
  43. 'API',
  44. 'ImageGraph',
  45. 'Annotations',
  46. 'SegmentEditor',
  47. 'UserCountry.getLocationFromIP',
  48. 'Dashboard',
  49. 'ExamplePluginTemplate',
  50. 'CustomAlerts',
  51. 'Insights'
  52. );
  53. /**
  54. * List of Modules, or Module.Method that should not be called as part of the XML output compare
  55. * Usually these modules either return random changing data, or are already tested in specific unit tests.
  56. */
  57. public $apiNotToCall = array();
  58. public $apiToCall = array();
  59. /**
  60. * Identifies the last language used in an API/Controller call.
  61. *
  62. * @var string
  63. */
  64. protected $lastLanguage;
  65. protected $missingExpectedFiles = array();
  66. protected $comparisonFailures = array();
  67. public static function setUpBeforeClass()
  68. {
  69. if (!isset(static::$fixture)) {
  70. $fixture = new Fixture();
  71. } else {
  72. $fixture = static::$fixture;
  73. }
  74. try {
  75. $fixture->performSetUp();
  76. } catch (Exception $e) {
  77. static::fail("Failed to setup fixture: " . $e->getMessage() . "\n" . $e->getTraceAsString());
  78. }
  79. }
  80. public static function tearDownAfterClass()
  81. {
  82. if (!isset(static::$fixture)) {
  83. $fixture = new Fixture();
  84. } else {
  85. $fixture = static::$fixture;
  86. }
  87. $fixture->performTearDown();
  88. }
  89. public function setUp()
  90. {
  91. parent::setUp();
  92. // Make sure the browser running the test does not influence the Country detection code
  93. $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'en';
  94. $this->changeLanguage('en');
  95. }
  96. /**
  97. * Returns true if continuous integration running this request
  98. * Useful to exclude tests which may fail only on this setup
  99. */
  100. static public function isTravisCI()
  101. {
  102. $travis = getenv('TRAVIS');
  103. return !empty($travis);
  104. }
  105. static public function isPhpVersion53()
  106. {
  107. return strpos(PHP_VERSION, '5.3') === 0;
  108. }
  109. static public function isMysqli()
  110. {
  111. return getenv('MYSQL_ADAPTER') == 'MYSQLI';
  112. }
  113. protected function alertWhenImagesExcludedFromTests()
  114. {
  115. if (!Fixture::canImagesBeIncludedInScheduledReports()) {
  116. $this->markTestSkipped(
  117. 'Scheduled reports generated during integration tests will not contain the image graphs. ' .
  118. 'For tests to generate images, use a machine with the following specifications : ' .
  119. 'OS = '.Fixture::IMAGES_GENERATED_ONLY_FOR_OS.', PHP = '.Fixture::IMAGES_GENERATED_FOR_PHP .
  120. ' and GD = ' . Fixture::IMAGES_GENERATED_FOR_GD
  121. );
  122. }
  123. }
  124. /**
  125. * Return 4 Api Urls for testing scheduled reports :
  126. * - one in HTML format with all available reports
  127. * - one in PDF format with all available reports
  128. * - two in SMS (one for each available report: MultiSites.getOne & MultiSites.getAll)
  129. *
  130. * @param string $dateTime eg '2010-01-01 12:34:56'
  131. * @param string $period eg 'day', 'week', 'month', 'year'
  132. * @return array
  133. */
  134. protected static function getApiForTestingScheduledReports($dateTime, $period)
  135. {
  136. $apiCalls = array();
  137. // HTML Scheduled Report
  138. array_push(
  139. $apiCalls,
  140. array(
  141. 'ScheduledReports.generateReport',
  142. array(
  143. 'testSuffix' => '_scheduled_report_in_html_tables_only',
  144. 'date' => $dateTime,
  145. 'periods' => array($period),
  146. 'format' => 'original',
  147. 'fileExtension' => 'html',
  148. 'otherRequestParameters' => array(
  149. 'idReport' => 1,
  150. 'reportFormat' => ReportRenderer::HTML_FORMAT,
  151. 'outputType' => \Piwik\Plugins\ScheduledReports\API::OUTPUT_RETURN
  152. )
  153. )
  154. )
  155. );
  156. // CSV Scheduled Report
  157. array_push(
  158. $apiCalls,
  159. array(
  160. 'ScheduledReports.generateReport',
  161. array(
  162. 'testSuffix' => '_scheduled_report_in_csv',
  163. 'date' => $dateTime,
  164. 'periods' => array($period),
  165. 'format' => 'original',
  166. 'fileExtension' => 'csv',
  167. 'otherRequestParameters' => array(
  168. 'idReport' => 1,
  169. 'reportFormat' => ReportRenderer::CSV_FORMAT,
  170. 'outputType' => \Piwik\Plugins\ScheduledReports\API::OUTPUT_RETURN
  171. )
  172. )
  173. )
  174. );
  175. if(Fixture::canImagesBeIncludedInScheduledReports()) {
  176. // PDF Scheduled Report
  177. // tests/PHPUnit/Integration/processed/test_ecommerceOrderWithItems_scheduled_report_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdf
  178. array_push(
  179. $apiCalls,
  180. array(
  181. 'ScheduledReports.generateReport',
  182. array(
  183. 'testSuffix' => '_scheduled_report_in_pdf_tables_only',
  184. 'date' => $dateTime,
  185. 'periods' => array($period),
  186. 'format' => 'original',
  187. 'fileExtension' => 'pdf',
  188. 'otherRequestParameters' => array(
  189. 'idReport' => 1,
  190. 'reportFormat' => ReportRenderer::PDF_FORMAT,
  191. 'outputType' => \Piwik\Plugins\ScheduledReports\API::OUTPUT_RETURN
  192. )
  193. )
  194. )
  195. );
  196. }
  197. // SMS Scheduled Report, one site
  198. array_push(
  199. $apiCalls,
  200. array(
  201. 'ScheduledReports.generateReport',
  202. array(
  203. 'testSuffix' => '_scheduled_report_via_sms_one_site',
  204. 'date' => $dateTime,
  205. 'periods' => array($period),
  206. 'format' => 'original',
  207. 'fileExtension' => 'sms.txt',
  208. 'otherRequestParameters' => array(
  209. 'idReport' => 2,
  210. 'outputType' => \Piwik\Plugins\ScheduledReports\API::OUTPUT_RETURN
  211. )
  212. )
  213. )
  214. );
  215. // SMS Scheduled Report, all sites
  216. array_push(
  217. $apiCalls,
  218. array(
  219. 'ScheduledReports.generateReport',
  220. array(
  221. 'testSuffix' => '_scheduled_report_via_sms_all_sites',
  222. 'date' => $dateTime,
  223. 'periods' => array($period),
  224. 'format' => 'original',
  225. 'fileExtension' => 'sms.txt',
  226. 'otherRequestParameters' => array(
  227. 'idReport' => 3,
  228. 'outputType' => \Piwik\Plugins\ScheduledReports\API::OUTPUT_RETURN
  229. )
  230. )
  231. )
  232. );
  233. if (Fixture::canImagesBeIncludedInScheduledReports()) {
  234. // HTML Scheduled Report with images
  235. array_push(
  236. $apiCalls,
  237. array(
  238. 'ScheduledReports.generateReport',
  239. array(
  240. 'testSuffix' => '_scheduled_report_in_html_tables_and_graph',
  241. 'date' => $dateTime,
  242. 'periods' => array($period),
  243. 'format' => 'original',
  244. 'fileExtension' => 'html',
  245. 'otherRequestParameters' => array(
  246. 'idReport' => 4,
  247. 'reportFormat' => ReportRenderer::HTML_FORMAT,
  248. 'outputType' => \Piwik\Plugins\ScheduledReports\API::OUTPUT_RETURN
  249. )
  250. )
  251. )
  252. );
  253. // mail report with one row evolution based png graph
  254. array_push(
  255. $apiCalls,
  256. array(
  257. 'ScheduledReports.generateReport',
  258. array(
  259. 'testSuffix' => '_scheduled_report_in_html_row_evolution_graph',
  260. 'date' => $dateTime,
  261. 'periods' => array($period),
  262. 'format' => 'original',
  263. 'fileExtension' => 'html',
  264. 'otherRequestParameters' => array(
  265. 'idReport' => 5,
  266. 'outputType' => \Piwik\Plugins\ScheduledReports\API::OUTPUT_RETURN
  267. )
  268. )
  269. )
  270. );
  271. }
  272. return $apiCalls;
  273. }
  274. /**
  275. * Given a list of default parameters to set, returns the URLs of APIs to call
  276. * If any API was specified in $this->apiNotToCall we ensure only these are tested.
  277. * If any API is set as excluded (see list below) then it will be ignored.
  278. *
  279. * @param array $parametersToSet Parameters to set in api call
  280. * @param array $formats Array of 'format' to fetch from API
  281. * @param array $periods Array of 'period' to query API
  282. * @param bool $supertableApi
  283. * @param bool $setDateLastN If set to true, the 'date' parameter will be rewritten to query instead a range of dates, rather than one period only.
  284. * @param bool|string $language 2 letter language code, defaults to default piwik language
  285. * @param bool|string $fileExtension
  286. *
  287. * @throws Exception
  288. *
  289. * @return array of API URLs query strings
  290. */
  291. protected function generateUrlsApi($parametersToSet, $formats, $periods, $supertableApi = false, $setDateLastN = false, $language = false, $fileExtension = false)
  292. {
  293. // Get the URLs to query against the API for all functions starting with get*
  294. $skipped = $requestUrls = array();
  295. $apiMetadata = new DocumentationGenerator;
  296. foreach (Proxy::getInstance()->getMetadata() as $class => $info) {
  297. $moduleName = Proxy::getInstance()->getModuleNameFromClassName($class);
  298. foreach ($info as $methodName => $infoMethod) {
  299. $apiId = $moduleName . '.' . $methodName;
  300. // If Api to test were set, we only test these
  301. if (!empty($this->apiToCall)
  302. && in_array($moduleName, $this->apiToCall) === false
  303. && in_array($apiId, $this->apiToCall) === false
  304. ) {
  305. $skipped[] = $apiId;
  306. continue;
  307. } elseif (
  308. ((strpos($methodName, 'get') !== 0 && $methodName != 'generateReport')
  309. || in_array($moduleName, $this->apiNotToCall) === true
  310. || in_array($apiId, $this->apiNotToCall) === true
  311. || $methodName == 'getLogoUrl'
  312. || $methodName == 'getSVGLogoUrl'
  313. || $methodName == 'hasSVGLogo'
  314. || $methodName == 'getHeaderLogoUrl'
  315. )
  316. ) { // Excluded modules from test
  317. $skipped[] = $apiId;
  318. continue;
  319. }
  320. foreach ($periods as $period) {
  321. $parametersToSet['period'] = $period;
  322. // If date must be a date range, we process this date range by adding 6 periods to it
  323. if ($setDateLastN) {
  324. if (!isset($parametersToSet['dateRewriteBackup'])) {
  325. $parametersToSet['dateRewriteBackup'] = $parametersToSet['date'];
  326. }
  327. $lastCount = (int)$setDateLastN;
  328. if ($setDateLastN === true) {
  329. $lastCount = 6;
  330. }
  331. $firstDate = $parametersToSet['dateRewriteBackup'];
  332. $secondDate = date('Y-m-d', strtotime("+$lastCount " . $period . "s", strtotime($firstDate)));
  333. $parametersToSet['date'] = $firstDate . ',' . $secondDate;
  334. }
  335. // Set response language
  336. if ($language !== false) {
  337. $parametersToSet['language'] = $language;
  338. }
  339. // set idSubtable if subtable API is set
  340. if ($supertableApi !== false) {
  341. $request = new Request(array(
  342. 'module' => 'API',
  343. 'method' => $supertableApi,
  344. 'idSite' => $parametersToSet['idSite'],
  345. 'period' => $parametersToSet['period'],
  346. 'date' => $parametersToSet['date'],
  347. 'format' => 'php',
  348. 'serialize' => 0,
  349. ));
  350. // find first row w/ subtable
  351. $content = $request->process();
  352. $this->checkRequestResponse($content);
  353. foreach ($content as $row) {
  354. if (isset($row['idsubdatatable'])) {
  355. $parametersToSet['idSubtable'] = $row['idsubdatatable'];
  356. break;
  357. }
  358. }
  359. // if no subtable found, throw
  360. if (!isset($parametersToSet['idSubtable'])) {
  361. throw new Exception(
  362. "Cannot find subtable to load for $apiId in $supertableApi.");
  363. }
  364. }
  365. // Generate for each specified format
  366. foreach ($formats as $format) {
  367. $parametersToSet['format'] = $format;
  368. $parametersToSet['hideIdSubDatable'] = 1;
  369. $parametersToSet['serialize'] = 1;
  370. $exampleUrl = $apiMetadata->getExampleUrl($class, $methodName, $parametersToSet);
  371. if ($exampleUrl === false) {
  372. $skipped[] = $apiId;
  373. continue;
  374. }
  375. // Remove the first ? in the query string
  376. $exampleUrl = substr($exampleUrl, 1);
  377. $apiRequestId = $apiId;
  378. if (strpos($exampleUrl, 'period=') !== false) {
  379. $apiRequestId .= '_' . $period;
  380. }
  381. $apiRequestId .= '.' . $format;
  382. if ($fileExtension) {
  383. $apiRequestId .= '.' . $fileExtension;
  384. }
  385. $requestUrls[$apiRequestId] = $exampleUrl;
  386. }
  387. }
  388. }
  389. }
  390. return $requestUrls;
  391. }
  392. /**
  393. * Will return all api urls for the given data
  394. *
  395. * @param string|array $formats String or array of formats to fetch from API
  396. * @param int|bool $idSite Id site
  397. * @param string|bool $dateTime Date time string of reports to request
  398. * @param array|bool|string $periods String or array of strings of periods (day, week, month, year)
  399. * @param bool $setDateLastN When set to true, 'date' parameter passed to API request will be rewritten to query a range of dates rather than 1 date only
  400. * @param string|bool $language 2 letter language code to request data in
  401. * @param string|bool $segment Custom Segment to query the data for
  402. * @param string|bool $visitorId Only used for Live! API testing
  403. * @param bool $abandonedCarts Only used in Goals API testing
  404. * @param bool $idGoal
  405. * @param bool $apiModule
  406. * @param bool $apiAction
  407. * @param array $otherRequestParameters
  408. * @param array|bool $supertableApi
  409. * @param array|bool $fileExtension
  410. *
  411. * @return array
  412. */
  413. protected function _generateApiUrls($formats = 'xml', $idSite = false, $dateTime = false, $periods = false,
  414. $setDateLastN = false, $language = false, $segment = false, $visitorId = false,
  415. $abandonedCarts = false, $idGoal = false, $apiModule = false, $apiAction = false,
  416. $otherRequestParameters = array(), $supertableApi = false, $fileExtension = false)
  417. {
  418. list($pathProcessed, $pathExpected) = static::getProcessedAndExpectedDirs();
  419. if ($periods === false) {
  420. $periods = 'day';
  421. }
  422. if (!is_array($periods)) {
  423. $periods = array($periods);
  424. }
  425. if (!is_array($formats)) {
  426. $formats = array($formats);
  427. }
  428. if (!is_writable($pathProcessed)) {
  429. $this->fail('To run the tests, you need to give write permissions to the following directory (create it if it doesn\'t exist).<code><br/>mkdir ' . $pathProcessed . '<br/>chmod 777 ' . $pathProcessed . '</code><br/>');
  430. }
  431. $parametersToSet = array(
  432. 'idSite' => $idSite,
  433. 'date' => ($periods == array('range') || strpos($dateTime, ',') !== false) ?
  434. $dateTime : date('Y-m-d', strtotime($dateTime)),
  435. 'expanded' => '1',
  436. 'piwikUrl' => 'http://example.org/piwik/',
  437. // Used in getKeywordsForPageUrl
  438. 'url' => 'http://example.org/store/purchase.htm',
  439. // Used in Actions.getPageUrl, .getDownload, etc.
  440. // tied to Main.test.php doTest_oneVisitorTwoVisits
  441. // will need refactoring when these same API functions are tested in a new function
  442. 'downloadUrl' => 'http://piwik.org/path/again/latest.zip?phpsessid=this is ignored when searching',
  443. 'outlinkUrl' => 'http://dev.piwik.org/svn',
  444. 'pageUrl' => 'http://example.org/index.htm?sessionid=this is also ignored by default',
  445. 'pageName' => ' Checkout / Purchasing... ',
  446. // do not show the millisec timer in response or tests would always fail as value is changing
  447. 'showTimer' => 0,
  448. 'language' => $language ? $language : 'en',
  449. 'abandonedCarts' => $abandonedCarts ? 1 : 0,
  450. 'idSites' => $idSite,
  451. );
  452. $parametersToSet = array_merge($parametersToSet, $otherRequestParameters);
  453. if (!empty($visitorId)) {
  454. $parametersToSet['visitorId'] = $visitorId;
  455. }
  456. if (!empty($apiModule)) {
  457. $parametersToSet['apiModule'] = $apiModule;
  458. }
  459. if (!empty($apiAction)) {
  460. $parametersToSet['apiAction'] = $apiAction;
  461. }
  462. if (!empty($segment)) {
  463. $parametersToSet['segment'] = urlencode($segment);
  464. }
  465. if ($idGoal !== false) {
  466. $parametersToSet['idGoal'] = $idGoal;
  467. }
  468. $requestUrls = $this->generateUrlsApi($parametersToSet, $formats, $periods, $supertableApi, $setDateLastN, $language, $fileExtension);
  469. $this->checkEnoughUrlsAreTested($requestUrls);
  470. return $requestUrls;
  471. }
  472. protected function checkEnoughUrlsAreTested($requestUrls)
  473. {
  474. $countUrls = count($requestUrls);
  475. $approximateCountApiToCall = count($this->apiToCall);
  476. if (empty($requestUrls)
  477. || $approximateCountApiToCall > $countUrls
  478. ) {
  479. throw new Exception("Only generated $countUrls API calls to test but was expecting more for this test.\n" .
  480. "Want to test APIs: " . implode(", ", $this->apiToCall) . ")\n" .
  481. "But only generated these URLs: \n" . implode("\n", $requestUrls) . ")\n"
  482. );
  483. }
  484. }
  485. protected function _testApiUrl($testName, $apiId, $requestUrl, $compareAgainst)
  486. {
  487. $isTestLogImportReverseChronological = strpos($testName, 'ImportedInRandomOrderTest') === false;
  488. $isLiveMustDeleteDates = (strpos($requestUrl, 'Live.getLastVisits') !== false
  489. || strpos($requestUrl, 'Live.getVisitorProfile') !== false)
  490. // except for that particular test that we care about dates!
  491. && $isTestLogImportReverseChronological;
  492. $request = new Request($requestUrl);
  493. $dateTime = Common::getRequestVar('date', '', 'string', UrlHelper::getArrayFromQueryString($requestUrl));
  494. list($processedFilePath, $expectedFilePath) =
  495. $this->getProcessedAndExpectedPaths($testName, $apiId, $format = null, $compareAgainst);
  496. // Cast as string is important. For example when calling
  497. // with format=original, objects or php arrays can be returned.
  498. // we also hide errors to prevent the 'headers already sent' in the ResponseBuilder (which sends Excel headers multiple times eg.)
  499. $response = (string)$request->process();
  500. if ($isLiveMustDeleteDates) {
  501. $response = $this->removeAllLiveDatesFromXml($response);
  502. }
  503. $response = $this->normalizePdfContent($response);
  504. $expected = $this->loadExpectedFile($expectedFilePath);
  505. $expectedContent = $expected;
  506. $expected = $this->normalizePdfContent($expected);
  507. if (empty($expected)) {
  508. if (empty($compareAgainst)) {
  509. file_put_contents($processedFilePath, $response);
  510. }
  511. print("The expected file is not found at '$expectedFilePath'. The Processed response was:");
  512. print("\n----------------------------\n\n");
  513. var_dump($response);
  514. print("\n----------------------------\n");
  515. return;
  516. }
  517. $expected = $this->removeXmlElement($expected, 'idsubdatatable', $testNotSmallAfter = false);
  518. $response = $this->removeXmlElement($response, 'idsubdatatable', $testNotSmallAfter = false);
  519. if ($isLiveMustDeleteDates) {
  520. $expected = $this->removeAllLiveDatesFromXml($expected);
  521. } // If date=lastN the <prettyDate> element will change each day, we remove XML element before comparison
  522. elseif (strpos($dateTime, 'last') !== false
  523. || strpos($dateTime, 'today') !== false
  524. || strpos($dateTime, 'now') !== false
  525. ) {
  526. if (strpos($requestUrl, 'API.getProcessedReport') !== false) {
  527. $expected = $this->removePrettyDateFromXml($expected);
  528. $response = $this->removePrettyDateFromXml($response);
  529. }
  530. $expected = $this->removeXmlElement($expected, 'visitServerHour');
  531. $response = $this->removeXmlElement($response, 'visitServerHour');
  532. if (strpos($requestUrl, 'date=') !== false) {
  533. $regex = "/date=[-0-9,%Ca-z]+/"; // need to remove %2C which is encoded ,
  534. $expected = preg_replace($regex, 'date=', $expected);
  535. $response = preg_replace($regex, 'date=', $response);
  536. }
  537. }
  538. // if idSubtable is in request URL, make sure idSubtable values are not in any urls
  539. if (strpos($requestUrl, 'idSubtable=') !== false) {
  540. $regex = "/idSubtable=[0-9]+/";
  541. $expected = preg_replace($regex, 'idSubtable=', $expected);
  542. $response = preg_replace($regex, 'idSubtable=', $response);
  543. }
  544. // Do not test for TRUNCATE(SUM()) returning .00 on mysqli since this is not working
  545. // http://bugs.php.net/bug.php?id=54508
  546. $expected = str_replace('.000000</l', '</l', $expected); //lat/long
  547. $response = str_replace('.000000</l', '</l', $response); //lat/long
  548. $expected = str_replace('.00</revenue>', '</revenue>', $expected);
  549. $response = str_replace('.00</revenue>', '</revenue>', $response);
  550. $response = str_replace('.1</revenue>', '</revenue>', $response);
  551. $expected = str_replace('.1</revenue>', '</revenue>', $expected);
  552. $expected = str_replace('.11</revenue>', '</revenue>', $expected);
  553. $response = str_replace('.11</revenue>', '</revenue>', $response);
  554. if (empty($compareAgainst)) {
  555. file_put_contents($processedFilePath, $response);
  556. }
  557. try {
  558. if (strpos($requestUrl, 'format=xml') !== false) {
  559. $this->assertXmlStringEqualsXmlString($expected, $response, "Differences with expected in: $processedFilePath");
  560. } else {
  561. $this->assertEquals(strlen($expected), strlen($response), "Differences with expected in: $processedFilePath");
  562. $this->assertEquals($expected, $response, "Differences with expected in: $processedFilePath");
  563. }
  564. if (trim($response) == trim($expected)
  565. && empty($compareAgainst)
  566. ) {
  567. if(trim($expectedContent) != trim($expected)) {
  568. file_put_contents($expectedFilePath, $expected);
  569. }
  570. }
  571. } catch (Exception $ex) {
  572. $this->comparisonFailures[] = $ex;
  573. }
  574. }
  575. protected function checkRequestResponse($response)
  576. {
  577. if(!is_string($response)) {
  578. $response = json_encode($response);
  579. }
  580. $this->assertTrue(stripos($response, 'error') === false, "error in $response");
  581. $this->assertTrue(stripos($response, 'exception') === false, "exception in $response");
  582. }
  583. protected function removeAllLiveDatesFromXml($input)
  584. {
  585. $toRemove = array(
  586. 'serverDate',
  587. 'firstActionTimestamp',
  588. 'lastActionTimestamp',
  589. 'lastActionDateTime',
  590. 'serverTimestamp',
  591. 'serverTimePretty',
  592. 'serverDatePretty',
  593. 'serverDatePrettyFirstAction',
  594. 'serverTimePrettyFirstAction',
  595. 'goalTimePretty',
  596. 'serverTimePretty',
  597. 'visitorId',
  598. 'nextVisitorId',
  599. 'previousVisitorId',
  600. 'visitServerHour',
  601. 'date',
  602. 'prettyDate',
  603. 'serverDateTimePrettyFirstAction'
  604. );
  605. foreach ($toRemove as $xml) {
  606. $input = $this->removeXmlElement($input, $xml);
  607. }
  608. return $input;
  609. }
  610. protected function removePrettyDateFromXml($input)
  611. {
  612. return $this->removeXmlElement($input, 'prettyDate');
  613. }
  614. protected function removeXmlElement($input, $xmlElement, $testNotSmallAfter = true)
  615. {
  616. // Only raise error if there was some data before
  617. $testNotSmallAfter = strlen($input > 100) && $testNotSmallAfter;
  618. $oldInput = $input;
  619. $input = preg_replace('/(<' . $xmlElement . '>.+?<\/' . $xmlElement . '>)/', '', $input);
  620. //check we didn't delete the whole string
  621. if ($testNotSmallAfter && $input != $oldInput) {
  622. $this->assertTrue(strlen($input) > 100);
  623. }
  624. return $input;
  625. }
  626. protected static function getProcessedAndExpectedDirs()
  627. {
  628. $path = static::getPathToTestDirectory();
  629. return array($path . '/processed/', $path . '/expected/');
  630. }
  631. private function getProcessedAndExpectedPaths($testName, $testId, $format = null, $compareAgainst = false)
  632. {
  633. $filenameSuffix = '__' . $testId;
  634. if ($format) {
  635. $filenameSuffix .= ".$format";
  636. }
  637. $processedFilename = $testName . $filenameSuffix;
  638. $expectedFilename = ($compareAgainst ?: $testName) . $filenameSuffix;
  639. list($processedDir, $expectedDir) = static::getProcessedAndExpectedDirs();
  640. return array($processedDir . $processedFilename, $expectedDir . $expectedFilename);
  641. }
  642. private function loadExpectedFile($filePath)
  643. {
  644. $result = @file_get_contents($filePath);
  645. if (empty($result)) {
  646. $expectedDir = dirname($filePath);
  647. $this->missingExpectedFiles[] = $filePath;
  648. return null;
  649. }
  650. return $result;
  651. }
  652. /**
  653. * Returns an array describing the API methods to call & compare with
  654. * expected output.
  655. *
  656. * The returned array must be of the following format:
  657. * <code>
  658. * array(
  659. * array('SomeAPI.method', array('testOption1' => 'value1', 'testOption2' => 'value2'),
  660. * array(array('SomeAPI.method', 'SomeOtherAPI.method'), array(...)),
  661. * .
  662. * .
  663. * .
  664. * )
  665. * </code>
  666. *
  667. * Valid test options:
  668. * <ul>
  669. * <li><b>testSuffix</b> The suffix added to the test name. Helps determine
  670. * the filename of the expected output.</li>
  671. * <li><b>format</b> The desired format of the output. Defaults to 'xml'.</li>
  672. * <li><b>idSite</b> The id of the website to get data for.</li>
  673. * <li><b>date</b> The date to get data for.</li>
  674. * <li><b>periods</b> The period or periods to get data for. Can be an array.</li>
  675. * <li><b>setDateLastN</b> Flag describing whether to query for a set of
  676. * dates or not.</li>
  677. * <li><b>language</b> The language to use.</li>
  678. * <li><b>segment</b> The segment to use.</li>
  679. * <li><b>visitorId</b> The visitor ID to use.</li>
  680. * <li><b>abandonedCarts</b> Whether to look for abandoned carts or not.</li>
  681. * <li><b>idGoal</b> The goal ID to use.</li>
  682. * <li><b>apiModule</b> The value to use in the apiModule request parameter.</li>
  683. * <li><b>apiAction</b> The value to use in the apiAction request parameter.</li>
  684. * <li><b>otherRequestParameters</b> An array of extra request parameters to use.</li>
  685. * <li><b>disableArchiving</b> Disable archiving before running tests.</li>
  686. * </ul>
  687. *
  688. * All test options are optional, except 'idSite' & 'date'.
  689. */
  690. public function getApiForTesting()
  691. {
  692. return array();
  693. }
  694. /**
  695. * Gets the string prefix used in the name of the expected/processed output files.
  696. */
  697. public static function getOutputPrefix()
  698. {
  699. $parts = explode("\\", get_called_class());
  700. $result = end($parts);
  701. $result = str_replace('Test_Piwik_Integration_', '', $result);
  702. return $result;
  703. }
  704. protected function _setCallableApi($api)
  705. {
  706. if ($api == 'all') {
  707. $this->apiToCall = array();
  708. $this->apiNotToCall = $this->defaultApiNotToCall;
  709. } else {
  710. if (!is_array($api)) {
  711. $api = array($api);
  712. }
  713. $this->apiToCall = $api;
  714. if (!in_array('UserCountry.getLocationFromIP', $api)) {
  715. $this->apiNotToCall = array('API.getPiwikVersion',
  716. 'UserCountry.getLocationFromIP');
  717. } else {
  718. $this->apiNotToCall = array();
  719. }
  720. }
  721. }
  722. /**
  723. * Runs API tests.
  724. */
  725. protected function runApiTests($api, $params)
  726. {
  727. // make sure that the reports we process here are not directly deleted in ArchiveProcessor/PluginsArchiver
  728. // (because we process reports in the past, they would sometimes be invalid, and would have been deleted)
  729. Rules::$purgeDisabledByTests = true;
  730. $testName = 'test_' . static::getOutputPrefix();
  731. $this->missingExpectedFiles = array();
  732. $this->comparisonFailures = array();
  733. $this->_setCallableApi($api);
  734. if (isset($params['disableArchiving']) && $params['disableArchiving'] === true) {
  735. Rules::$archivingDisabledByTests = true;
  736. Config::getInstance()->General['browser_archiving_disabled_enforce'] = 1;
  737. } else {
  738. Rules::$archivingDisabledByTests = false;
  739. Config::getInstance()->General['browser_archiving_disabled_enforce'] = 0;
  740. }
  741. if(!empty($params['hackDeleteRangeArchivesBefore'])) {
  742. Db::query('delete from '. Common::prefixTable('archive_numeric_2009_12') . ' where period = 5');
  743. Db::query('delete from '. Common::prefixTable('archive_blob_2009_12') . ' where period = 5');
  744. }
  745. if (isset($params['language'])) {
  746. $this->changeLanguage($params['language']);
  747. }
  748. $testSuffix = isset($params['testSuffix']) ? $params['testSuffix'] : '';
  749. $requestUrls = $this->_generateApiUrls(
  750. isset($params['format']) ? $params['format'] : 'xml',
  751. isset($params['idSite']) ? $params['idSite'] : false,
  752. isset($params['date']) ? $params['date'] : false,
  753. isset($params['periods']) ? $params['periods'] : (isset($params['period']) ? $params['period'] : false),
  754. isset($params['setDateLastN']) ? $params['setDateLastN'] : false,
  755. isset($params['language']) ? $params['language'] : false,
  756. isset($params['segment']) ? $params['segment'] : false,
  757. isset($params['visitorId']) ? $params['visitorId'] : false,
  758. isset($params['abandonedCarts']) ? $params['abandonedCarts'] : false,
  759. isset($params['idGoal']) ? $params['idGoal'] : false,
  760. isset($params['apiModule']) ? $params['apiModule'] : false,
  761. isset($params['apiAction']) ? $params['apiAction'] : false,
  762. isset($params['otherRequestParameters']) ? $params['otherRequestParameters'] : array(),
  763. isset($params['supertableApi']) ? $params['supertableApi'] : false,
  764. isset($params['fileExtension']) ? $params['fileExtension'] : false);
  765. $compareAgainst = isset($params['compareAgainst']) ? ('test_' . $params['compareAgainst']) : false;
  766. foreach ($requestUrls as $apiId => $requestUrl) {
  767. // this is a hack
  768. if(isset($params['skipGetPageTitles'])) {
  769. if($apiId == 'Actions.getPageTitles_day.xml') {
  770. continue;
  771. }
  772. }
  773. $this->_testApiUrl($testName . $testSuffix, $apiId, $requestUrl, $compareAgainst);
  774. }
  775. // Restore normal purge behavior
  776. Rules::$purgeDisabledByTests = false;
  777. // change the language back to en
  778. if ($this->lastLanguage != 'en') {
  779. $this->changeLanguage('en');
  780. }
  781. if (!empty($this->missingExpectedFiles)) {
  782. $expectedDir = dirname(reset($this->missingExpectedFiles));
  783. $this->fail(" ERROR: Could not find expected API output '"
  784. . implode("', '", $this->missingExpectedFiles)
  785. . "'. For new tests, to pass the test, you can copy files from the processed/ directory into"
  786. . " $expectedDir after checking that the output is valid. %s ");
  787. }
  788. // Display as one error all sub-failures
  789. if (!empty($this->comparisonFailures)) {
  790. $messages = '';
  791. $i = 1;
  792. foreach ($this->comparisonFailures as $failure) {
  793. $msg = $failure->getMessage();
  794. $msg = strtok($msg, "\n");
  795. $messages .= "\n#" . $i++ . ": " . $msg;
  796. }
  797. $messages .= " \n ";
  798. print($messages);
  799. $first = reset($this->comparisonFailures);
  800. throw $first;
  801. }
  802. return count($this->comparisonFailures) == 0;
  803. }
  804. /**
  805. * changing the language within one request is a bit fancy
  806. * in order to keep the core clean, we need a little hack here
  807. *
  808. * @param string $langId
  809. */
  810. protected function changeLanguage($langId)
  811. {
  812. if ($this->lastLanguage != $langId) {
  813. $_GET['language'] = $langId;
  814. Translate::reset();
  815. Translate::reloadLanguage($langId);
  816. }
  817. $this->lastLanguage = $langId;
  818. }
  819. /**
  820. * Path where expected/processed output files are stored.
  821. */
  822. public static function getPathToTestDirectory()
  823. {
  824. return dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Integration';
  825. }
  826. /**
  827. * Returns an array associating table names w/ lists of row data.
  828. *
  829. * @return array
  830. */
  831. protected static function getDbTablesWithData()
  832. {
  833. $result = array();
  834. foreach (DbHelper::getTablesInstalled() as $tableName) {
  835. $result[$tableName] = Db::fetchAll("SELECT * FROM $tableName");
  836. }
  837. return $result;
  838. }
  839. /**
  840. * Truncates all tables then inserts the data in $tables into each
  841. * mapped table.
  842. *
  843. * @param array $tables Array mapping table names with arrays of row data.
  844. */
  845. protected static function restoreDbTables($tables)
  846. {
  847. // truncate existing tables
  848. DbHelper::truncateAllTables();
  849. // insert data
  850. $existingTables = DbHelper::getTablesInstalled();
  851. foreach ($tables as $table => $rows) {
  852. // create table if it's an archive table
  853. if (strpos($table, 'archive_') !== false && !in_array($table, $existingTables)) {
  854. $tableType = strpos($table, 'archive_numeric') !== false ? 'archive_numeric' : 'archive_blob';
  855. $createSql = DbHelper::getTableCreateSql($tableType);
  856. $createSql = str_replace(Common::prefixTable($tableType), $table, $createSql);
  857. Db::query($createSql);
  858. }
  859. if (empty($rows)) {
  860. continue;
  861. }
  862. $rowsSql = array();
  863. $bind = array();
  864. foreach ($rows as $row) {
  865. $values = array();
  866. foreach ($row as $name => $value) {
  867. if (is_null($value)) {
  868. $values[] = 'NULL';
  869. } else if (is_numeric($value)) {
  870. $values[] = $value;
  871. } else if (!ctype_print($value)) {
  872. $values[] = "x'" . bin2hex(substr($value, 1)) . "'";
  873. } else {
  874. $values[] = "?";
  875. $bind[] = $value;
  876. }
  877. }
  878. $rowsSql[] = "(" . implode(',', $values) . ")";
  879. }
  880. $sql = "INSERT INTO $table VALUES " . implode(',', $rowsSql);
  881. Db::query($sql, $bind);
  882. }
  883. }
  884. /**
  885. * Drops all archive tables.
  886. */
  887. public static function deleteArchiveTables()
  888. {
  889. foreach (ArchiveTableCreator::getTablesArchivesInstalled() as $table) {
  890. Db::query("DROP TABLE IF EXISTS $table");
  891. }
  892. ArchiveTableCreator::refreshTableList($forceReload = true);
  893. }
  894. /**
  895. * Removes content from PDF binary the content that changes with the datetime or other random Ids
  896. */
  897. protected function normalizePdfContent($response)
  898. {
  899. // normalize date markups and document ID in pdf files :
  900. // - /LastModified (D:20120820204023+00'00')
  901. // - /CreationDate (D:20120820202226+00'00')
  902. // - /ModDate (D:20120820202226+00'00')
  903. // - /M (D:20120820202226+00'00')
  904. // - /ID [ <0f5cc387dc28c0e13e682197f485fe65> <0f5cc387dc28c0e13e682197f485fe65> ]
  905. $response = preg_replace('/\(D:[0-9]{14}/', '(D:19700101000000', $response);
  906. $response = preg_replace('/\/ID \[ <.*> ]/', '', $response);
  907. $response = preg_replace('/\/id:\[ <.*> ]/', '', $response);
  908. $response = $this->removeXmlElement($response, "xmp:CreateDate");
  909. $response = $this->removeXmlElement($response, "xmp:ModifyDate");
  910. $response = $this->removeXmlElement($response, "xmp:MetadataDate");
  911. $response = $this->removeXmlElement($response, "xmpMM:DocumentID");
  912. $response = $this->removeXmlElement($response, "xmpMM:InstanceID");
  913. return $response;
  914. }
  915. }