diff --git a/tests/PHPUnit/Impl/ApiTestConfig.php b/tests/PHPUnit/Impl/ApiTestConfig.php new file mode 100644 index 0000000000000000000000000000000000000000..ecb778f536396f72b0691b767a88e0958f15b9e8 --- /dev/null +++ b/tests/PHPUnit/Impl/ApiTestConfig.php @@ -0,0 +1,214 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Tests\Impl; + +use \Exception; + +/** + * Holds the specification for a set of API tests. + * + * An API test consists of calling Piwik's API and comparing the result with an expected + * result. The expected result is stored in a file, usually in an **expected** folder. + * + * The test specification describes how the API is called and how the API response is + * processed before comparison. + * + * Instances of this class are not created directly. Instead, an array mapping config + * property names w/ values is passed to IntegrationTestCase::runApiTests. For example, + * + * $this->runApiTests("UserCountry", array( + * 'idSite' => 1, + * 'date' => '2012-01-01', + * // ... + * )); + */ +class ApiTestConfig +{ + /** + * The value of the idSite query parameter to send to Piwik's API. Can be a comma separated + * list of integers or `'all'`. + * + * This option is required. + * + * @var int|string + */ + public $idSite; + + /** + * The value of the date query parameter to send to Piwik's API. + * + * @var string + */ + public $date; + + /** + * One or more periods to test for. Multiple periods will result in multiple API calls and + * multiple comparisons. + * + * @var string[] + */ + public $periods = array('day'); + + /** + * The desired output format of the API response. Used to test DataTable renderers. + * + * @var string + */ + public $format = 'xml'; + + /** + * Controls whether to query for multiple periods or not. If set to true, the last 6 dates will be + * queried for. If set to an integer, then that number of periods will be queried for. + * + * @var bool|int + */ + public $setDateLastN = false; + + /** + * The language to retrieve results in. Defaults to 'en'. + * + * @var string|false + */ + public $language = false; + + /** + * An optional value to use for the segment query parameter. + * + * @var string|false + */ + public $segment = false; + + /** + * The value to use for the idGoal query parameter. + * + * @var int|bool + */ + public $idGoal = false; + + /** + * The value to use for the apiModule query parameter. + * + * @var string|false + */ + public $apiModule = false; + + /** + * The value to use for the apiAction query parameter. + * + * @var string|false + */ + public $apiAction = false; + + /** + * Associative array of query parameters to set in API requests. For example, + * + * array('param1' => 'value1', 'param2' => 'value2') + * + * @var string[] + */ + public $otherRequestParameters = array(); + + /** + * This property is used to test API methods that return subtables and should be set to the API method that + * returns the super table of the API method being tested. If set, TestRequestCollection will look for the + * first valid idSubtable value to use in the test request. Since these values are assigned dynamically, + * there's no other way to set idSubtable. + * + * @var string|bool eg, `"Referrers.getWebsites"` + */ + public $supertableApi = false; + + /** + * If supplied, this value will be used as the expected and processed file's extension **without** + * setting the 'format' query parameter. + * + * Used when testing scheduled reports. + * + * @var string|bool eg, `"html"` + */ + public $fileExtension = false; + + /** + * An array of API methods that shouldn't be called. If `'all'` is specified in IntegrationTestCase::runApiTests, + * the methods in this property will be ignored when calling all API methods. + * + * @var string[]|false eg, `array("Actions", "Referrers.getWebsites", ...)` + */ + public $apiNotToCall = false; + + /** + * If true, archiving will be disabled when the API is called. + * + * @var bool + */ + public $disableArchiving = false; + + /** + * An extra suffix to apply to the expected and processed output file names. + * + * @param string + */ + public $testSuffix = ''; + + /** + * If supplied, tests will compare API responses with files using a different file prefix. + * Normally, the test name is used as the test prefix, so this will usually be set to the + * name of the integration test. Either that or the value in the test's getOutputPrefix + * method. + * + * @param string|bool eg, `'OneVisitorTwoVisitsTest'` + */ + public $compareAgainst = false; + + /** + * An array of XML fields that should be removed from processed API response before + * comparing. These fields should be fields that change on every test execution and have + * to be removed in order to make tests pass. + * + * @param string[]|false + */ + public $xmlFieldsToRemove = false; + + /** + * If true, XML fields that change on each request for Live API methods are retained. + * Normally, they are removed before comparing the API response w/ expected. + * + * @param bool + */ + public $keepLiveDates = false; + + /** + * Constructor. Sets class properties using an associative array mapping property names w/ values. + * + * @param array $params eg, `array('idSite' => 1, 'date' => '2012-01-01', ...)` + * @throws Exception if a property name in `$params` is invalid + */ + public function __construct($params) + { + foreach ($params as $key => $value) { + if ($key == 'period') { + $key = 'periods'; + } + + if (!property_exists($this, $key)) { + throw new Exception("Invalid API test property '$key'! Check your Integration tests."); + } + + $this->$key = $value; + } + + if (!is_array($this->periods)) { + $this->periods = array($this->periods); + } + + if ($this->setDateLastN === true) { + $this->setDateLastN = 6; + } + } +} \ No newline at end of file diff --git a/tests/PHPUnit/Impl/TestRequestCollection.php b/tests/PHPUnit/Impl/TestRequestCollection.php new file mode 100644 index 0000000000000000000000000000000000000000..17ff280eb9cc6f5c5f3fa05667fa7688c6a44aef --- /dev/null +++ b/tests/PHPUnit/Impl/TestRequestCollection.php @@ -0,0 +1,341 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Tests\Impl; + +use Piwik\API\DocumentationGenerator; +use Piwik\API\Proxy; +use Piwik\API\Request; +use Piwik\UrlHelper; +use Piwik\Tests\IntegrationTestCase; +use \Exception; +use \PHPUnit_Framework_Assert; + +/** + * Utility class used to generate a set of API requests given API methods to call, API + * methods to exclude, and an ApiTestConfig instance. + */ +class TestRequestCollection +{ + public $defaultApiNotToCall = array( + 'LanguagesManager', + 'DBStats', + 'Dashboard', + 'UsersManager', + 'SitesManager', + 'ExampleUI', + 'Overlay', + 'Live', + 'SEO', + 'ExampleAPI', + 'ScheduledReports', + 'MobileMessaging', + 'Transitions', + 'API', + 'ImageGraph', + 'Annotations', + 'SegmentEditor', + 'UserCountry.getLocationFromIP', + 'Dashboard', + 'ExamplePluginTemplate', + 'CustomAlerts', + 'Insights' + ); + + /** + * The list of generated API requests. + * + * @var array[] + */ + private $requestUrls; + + /** + * The config for this set of API requests. + * + * @var ApiTestConfig + */ + private $testConfig; + + /** + * The set of API methods to test. Each API method will have at least one request URL in + * $requestUrls. + * + * @var string[]|string Can be set to 'all' to test all available API methods. + */ + private $apiToCall; + + /** + * The set of API methods/modules that should not be called. These methods will be excluded + * from the generated request URLs. + * + * @var string[]|string + */ + private $apiNotToCall; + + /** + * Constructor. + */ + public function __construct($api, ApiTestConfig $testConfig, $apiToCall) + { + $this->testConfig = $testConfig; + $this->setExplicitApiToCallAndNotCall($apiToCall); + + $this->requestUrls = $this->_generateApiUrls(); + } + + public function getRequestUrls() + { + return $this->requestUrls; + } + + /** + * Will return all api urls for the given data + * + * @return array + */ + protected function _generateApiUrls() + { + $parametersToSet = array( + 'idSite' => $this->testConfig->idSite, + 'date' => ($this->testConfig->periods == array('range') || strpos($this->testConfig->date, ',') !== false) ? + $this->testConfig->date : date('Y-m-d', strtotime($this->testConfig->date)), + 'expanded' => '1', + 'piwikUrl' => 'http://example.org/piwik/', + // Used in getKeywordsForPageUrl + 'url' => 'http://example.org/store/purchase.htm', + + // Used in Actions.getPageUrl, .getDownload, etc. + // tied to Main.test.php doTest_oneVisitorTwoVisits + // will need refactoring when these same API functions are tested in a new function + 'downloadUrl' => 'http://piwik.org/path/again/latest.zip?phpsessid=this is ignored when searching', + 'outlinkUrl' => 'http://dev.piwik.org/svn', + 'pageUrl' => 'http://example.org/index.htm?sessionid=this is also ignored by default', + 'pageName' => ' Checkout / Purchasing... ', + + // do not show the millisec timer in response or tests would always fail as value is changing + 'showTimer' => 0, + + 'language' => $this->testConfig->language ?: 'en', + 'idSites' => $this->testConfig->idSite, + ); + $parametersToSet = array_merge($parametersToSet, $this->testConfig->otherRequestParameters); + if (!empty($this->testConfig->apiModule)) { + $parametersToSet['apiModule'] = $this->testConfig->apiModule; + } + if (!empty($this->testConfig->apiAction)) { + $parametersToSet['apiAction'] = $this->testConfig->apiAction; + } + if (!empty($this->testConfig->segment)) { + $parametersToSet['segment'] = urlencode($this->testConfig->segment); + } + if ($this->testConfig->idGoal !== false) { + $parametersToSet['idGoal'] = $this->testConfig->idGoal; + } + + $requestUrls = $this->generateApiUrlPermutations($parametersToSet); + + $this->checkEnoughUrlsAreTested($requestUrls); + + return $requestUrls; + } + + protected function checkEnoughUrlsAreTested($requestUrls) + { + $countUrls = count($requestUrls); + $approximateCountApiToCall = count($this->apiToCall); + if (empty($requestUrls) + || $approximateCountApiToCall > $countUrls + ) { + throw new Exception("Only generated $countUrls API calls to test but was expecting more for this test.\n" . + "Want to test APIs: " . implode(", ", $this->apiToCall) . ")\n" . + "But only generated these URLs: \n" . implode("\n", $requestUrls) . ")\n" + ); + } + } + + /** + * Given a list of default parameters to set, returns the URLs of APIs to call + * If any API was specified in $this->apiNotToCall we ensure only these are tested. + * If any API is set as excluded (see list below) then it will be ignored. + * + * @param array $parametersToSet Parameters to set in api call + * @param array $formats Array of 'format' to fetch from API + * @param array $periods Array of 'period' to query API + * @param bool $supertableApi + * @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. + * @param bool|string $language 2 letter language code, defaults to default piwik language + * @param bool|string $fileExtension + * + * @throws Exception + * + * @return array of API URLs query strings + */ + protected function generateApiUrlPermutations($parametersToSet) + { + $formats = array($this->testConfig->format); + $originalDate = $parametersToSet['date']; + + $requestUrls = array(); + $apiMetadata = new DocumentationGenerator; + + // Get the URLs to query against the API for all functions starting with get* + foreach ($this->getAllApiMethods() as $apiMethodInfo) { + list($class, $moduleName, $methodName) = $apiMethodInfo; + + $apiId = $moduleName . '.' . $methodName; + + foreach ($this->testConfig->periods as $period) { + $parametersToSet['period'] = $period; + + // If date must be a date range, we process this date range by adding 6 periods to it + if ($this->testConfig->setDateLastN) { + if (!isset($parametersToSet['dateRewriteBackup'])) { + $parametersToSet['dateRewriteBackup'] = $parametersToSet['date']; + } + + $lastCount = $this->testConfig->setDateLastN; + + $secondDate = date('Y-m-d', strtotime("+$lastCount " . $period . "s", strtotime($originalDate))); + $parametersToSet['date'] = $originalDate . ',' . $secondDate; + } + + // Set response language + if ($this->testConfig->language !== false) { + $parametersToSet['language'] = $this->testConfig->language; + } + + // set idSubtable if subtable API is set + if ($this->testConfig->supertableApi !== false) { + $request = new Request(array( + 'module' => 'API', + 'method' => $this->testConfig->supertableApi, + 'idSite' => $parametersToSet['idSite'], + 'period' => $parametersToSet['period'], + 'date' => $parametersToSet['date'], + 'format' => 'php', + 'serialize' => 0, + )); + + $content = $request->process(); + IntegrationTestCase::assertApiResponseHasNoError($content); + + // find first row w/ subtable + foreach ($content as $row) { + if (isset($row['idsubdatatable'])) { + $parametersToSet['idSubtable'] = $row['idsubdatatable']; + break; + } + } + + // if no subtable found, throw + if (!isset($parametersToSet['idSubtable'])) { + throw new Exception( + "Cannot find subtable to load for $apiId in {$this->testConfig->supertableApi}."); + } + } + + // Generate for each specified format + foreach ($formats as $format) { + $parametersToSet['format'] = $format; + $parametersToSet['hideIdSubDatable'] = 1; + $parametersToSet['serialize'] = 1; + + $exampleUrl = $apiMetadata->getExampleUrl($class, $methodName, $parametersToSet); + + if ($exampleUrl === false) { + continue; + } + + // Remove the first ? in the query string + $exampleUrl = substr($exampleUrl, 1); + $apiRequestId = $apiId; + if (strpos($exampleUrl, 'period=') !== false) { + $apiRequestId .= '_' . $period; + } + + $apiRequestId .= '.' . $format; + + if ($this->testConfig->fileExtension) { + $apiRequestId .= '.' . $this->testConfig->fileExtension; + } + + $requestUrls[$apiRequestId] = UrlHelper::getArrayFromQueryString($exampleUrl); + } + } + } + return $requestUrls; + } + + private function getAllApiMethods() + { + $result = array(); + + foreach (Proxy::getInstance()->getMetadata() as $class => $info) { + $moduleName = Proxy::getInstance()->getModuleNameFromClassName($class); + foreach ($info as $methodName => $infoMethod) { + if ($this->shouldSkipApiMethod($moduleName, $methodName)) { + continue; + } + + $result[] = array($class, $moduleName, $methodName); + } + } + + return $result; + } + + private function shouldSkipApiMethod($moduleName, $methodName) { + $apiId = $moduleName . '.' . $methodName; + + // If Api to test were set, we only test these + if (!empty($this->apiToCall) + && in_array($moduleName, $this->apiToCall) === false + && in_array($apiId, $this->apiToCall) === false + ) { + return true; + } elseif ( + ((strpos($methodName, 'get') !== 0 && $methodName != 'generateReport') + || in_array($moduleName, $this->apiNotToCall) === true + || in_array($apiId, $this->apiNotToCall) === true + || $methodName == 'getLogoUrl' + || $methodName == 'getSVGLogoUrl' + || $methodName == 'hasSVGLogo' + || $methodName == 'getHeaderLogoUrl' + ) + ) { // Excluded modules from test + return true; + } + + return false; + } + + private function setExplicitApiToCallAndNotCall($apiToCall) + { + if ($apiToCall == 'all') { + $this->apiToCall = array(); + $this->apiNotToCall = $this->defaultApiNotToCall; + } else { + if (!is_array($apiToCall)) { + $apiToCall = array($apiToCall); + } + + $this->apiToCall = $apiToCall; + + if (!in_array('UserCountry.getLocationFromIP', $apiToCall)) { + $this->apiNotToCall = array('API.getPiwikVersion', + 'UserCountry.getLocationFromIP'); + } else { + $this->apiNotToCall = array(); + } + } + + if (!empty($this->testConfig->apiNotToCall)) { + $this->apiNotToCall = array_merge($this->apiNotToCall, $this->testConfig->apiNotToCall); + } + } +} \ No newline at end of file diff --git a/tests/PHPUnit/Impl/TestRequestResponse.php b/tests/PHPUnit/Impl/TestRequestResponse.php new file mode 100644 index 0000000000000000000000000000000000000000..6e18e8460e416139a3fe444b0d585eef4c8c4bea --- /dev/null +++ b/tests/PHPUnit/Impl/TestRequestResponse.php @@ -0,0 +1,222 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Tests\Impl; + +use Piwik\API\Request; +use Piwik\Tests\IntegrationTestCase; +use PHPUnit_Framework_Assert as Asserts; +use Exception; + +/** + * Utility class used to obtain and process API responses for API tests. + */ +class TestRequestResponse +{ + private $processedResponseText; + + private $params; + + private $requestUrl; + + public function __construct($apiResponse, $params, $requestUrl) + { + $this->params = $params; + $this->requestUrl = $requestUrl; + + $apiResponse = (string) $apiResponse; + $this->processedResponseText = $this->normalizeApiResponse($apiResponse); + } + + public function getResponseText() + { + return $this->processedResponseText; + } + + public function save($path) + { + file_put_contents($path, $this->processedResponseText); + } + + public static function loadFromFile($path, $params, $requestUrl) + { + $contents = @file_get_contents($path); + + if (empty($contents)) { + throw new Exception("$path does not exist"); + } + + return new TestRequestResponse($contents, $params, $requestUrl); + } + + public static function loadFromApi($params, $requestUrl) + { + $testRequest = new Request($requestUrl); + + // Cast as string is important. For example when calling + // with format=original, objects or php arrays can be returned. + // we also hide errors to prevent the 'headers already sent' in the ResponseBuilder (which sends Excel headers multiple times eg.) + $response = (string) $testRequest->process(); + + return new TestRequestResponse($response, $params, $requestUrl); + } + + public static function assertEquals(TestRequestResponse $expected, TestRequestResponse $actual, $message = false) + { + $expectedText = $expected->getResponseText(); + $actualText = $actual->getResponseText(); + + if ($expected->requestUrl['format'] == 'xml') { + Asserts::assertXmlStringEqualsXmlString($expectedText, $actualText, $message); + } else { + Asserts::assertEquals(strlen($expectedText), strlen($actualText), $message); + Asserts::assertEquals($expectedText, $actualText, $message); + } + } + + private function normalizeApiResponse($apiResponse) + { + if ($this->shouldDeleteLiveDates()) { + $apiResponse = $this->removeAllLiveDatesFromXml($apiResponse); + } else if ($this->requestHasNonDeterministicDate()) { + // If date=lastN the <prettyDate> element will change each day, we remove XML element before comparison + + if ($this->requestUrl['method'] == 'API.getProcessedReport') { + $apiResponse = $this->removeXmlElement($apiResponse, 'prettyDate'); + } + + $apiResponse = $this->removeXmlElement($apiResponse, 'visitServerHour'); + + $regex = "/date=[-0-9,%Ca-z]+/"; // need to remove %2C which is encoded , + $apiResponse = preg_replace($regex, 'date=', $apiResponse); + } + + // if idSubtable is in request URL, make sure idSubtable values are not in any urls + if (!empty($this->requestUrl['idSubtable'])) { + $apiResponse = $this->removeIdSubtableParamFromUrlsInResponse($apiResponse); + } + + $apiResponse = $this->normalizePdfContent($apiResponse); + $apiResponse = $this->removeXmlFields($apiResponse); + $apiResponse = $this->normalizeDecimalFields($apiResponse); + + return $apiResponse; + } + + private function removeIdSubtableParamFromUrlsInResponse($apiResponse) + { + return preg_replace("/idSubtable=[0-9]+/", 'idSubtable=', $apiResponse); + } + + private function removeAllLiveDatesFromXml($apiResponse) + { + $toRemove = array( + 'serverDate', + 'firstActionTimestamp', + 'lastActionTimestamp', + 'lastActionDateTime', + 'serverTimestamp', + 'serverTimePretty', + 'serverDatePretty', + 'serverDatePrettyFirstAction', + 'serverTimePrettyFirstAction', + 'goalTimePretty', + 'serverTimePretty', + 'visitorId', + 'nextVisitorId', + 'previousVisitorId', + 'visitServerHour', + 'date', + 'prettyDate', + 'serverDateTimePrettyFirstAction' + ); + return $this->removeXmlFields($apiResponse, $toRemove); + } + + /** + * Removes content from PDF binary the content that changes with the datetime or other random Ids + */ + private function normalizePdfContent($response) + { + // normalize date markups and document ID in pdf files : + // - /LastModified (D:20120820204023+00'00') + // - /CreationDate (D:20120820202226+00'00') + // - /ModDate (D:20120820202226+00'00') + // - /M (D:20120820202226+00'00') + // - /ID [ <0f5cc387dc28c0e13e682197f485fe65> <0f5cc387dc28c0e13e682197f485fe65> ] + $response = preg_replace('/\(D:[0-9]{14}/', '(D:19700101000000', $response); + $response = preg_replace('/\/ID \[ <.*> ]/', '', $response); + $response = preg_replace('/\/id:\[ <.*> ]/', '', $response); + $response = $this->removeXmlElement($response, "xmp:CreateDate"); + $response = $this->removeXmlElement($response, "xmp:ModifyDate"); + $response = $this->removeXmlElement($response, "xmp:MetadataDate"); + $response = $this->removeXmlElement($response, "xmpMM:DocumentID"); + $response = $this->removeXmlElement($response, "xmpMM:InstanceID"); + return $response; + } + + private function removeXmlFields($input, $fieldsToRemove = false) + { + if ($fieldsToRemove === false) { + $fieldsToRemove = @$this->params['xmlFieldsToRemove']; + } + + $fieldsToRemove[] = 'idsubdatatable'; // TODO: had testNotSmallAfter, should still? + + foreach ($fieldsToRemove as $xml) { + $input = $this->removeXmlElement($input, $xml); + } + return $input; + } + + private function removeXmlElement($input, $xmlElement, $testNotSmallAfter = true) + { + // Only raise error if there was some data before + $testNotSmallAfter = strlen($input > 100) && $testNotSmallAfter; + + $oldInput = $input; + $input = preg_replace('/(<' . $xmlElement . '>.+?<\/' . $xmlElement . '>)/', '', $input); + + // check we didn't delete the whole string + if ($testNotSmallAfter && $input != $oldInput) { + $this->assertTrue(strlen($input) > 100); + } + return $input; + } + + private function requestHasNonDeterministicDate() + { + if (empty($this->requestUrl['date'])) { + return false; + } + + $dateTime = $this->requestUrl['date']; + return strpos($dateTime, 'last') !== false + || strpos($dateTime, 'today') !== false + || strpos($dateTime, 'now') !== false; + } + + private function shouldDeleteLiveDates() + { + return empty($this->params['keepLiveDates']) + && ($this->requestUrl['method'] == 'Live.getLastVisits' + || $this->requestUrl['method'] == 'Live.getLastVisitsDetails' + || $this->requestUrl['method'] == 'Live.getVisitorProfile'); + } + + private function normalizeDecimalFields($response) + { + // Do not test for TRUNCATE(SUM()) returning .00 on mysqli since this is not working + // http://bugs.php.net/bug.php?id=54508 + $response = str_replace('.000000</l', '</l', $response); //lat/long + $response = str_replace('.00</revenue>', '</revenue>', $response); + $response = str_replace('.1</revenue>', '</revenue>', $response); + $response = str_replace('.11</revenue>', '</revenue>', $response); + return $response; + } +} \ No newline at end of file diff --git a/tests/PHPUnit/Integration/AutoSuggestAPITest.php b/tests/PHPUnit/Integration/AutoSuggestAPITest.php index 644425c292db85993f7421700e0cf0ad5e0c78b2..f5b47d254d8cc12dbd814239c29a5ee3dd86a710 100644 --- a/tests/PHPUnit/Integration/AutoSuggestAPITest.php +++ b/tests/PHPUnit/Integration/AutoSuggestAPITest.php @@ -93,7 +93,7 @@ class AutoSuggestAPITest extends IntegrationTestCase . '&format=php&serialize=0' ); $response = $request->process(); - $this->checkRequestResponse($response); + $this->assertApiResponseHasNoError($response); $topSegmentValue = @$response[0]; if ($topSegmentValue !== false && !is_null($topSegmentValue)) { diff --git a/tests/PHPUnit/Integration/BackwardsCompatibility1XTest.php b/tests/PHPUnit/Integration/BackwardsCompatibility1XTest.php index 1331786c69f12ac2ba09df2bd3c7e5f6f1feb4dc..a24c50787710857b076c8b4aa1aa296a1af291ea 100644 --- a/tests/PHPUnit/Integration/BackwardsCompatibility1XTest.php +++ b/tests/PHPUnit/Integration/BackwardsCompatibility1XTest.php @@ -55,19 +55,6 @@ class BackwardsCompatibility1XTest extends IntegrationTestCase VisitFrequencyApi::getInstance()->get(1, 'year', '2012-12-29'); } - public function setUp() - { - parent::setUp(); - - $this->defaultApiNotToCall[] = 'Referrers'; - - // changes made to SQL dump to test VisitFrequency change the day of week - $this->defaultApiNotToCall[] = 'VisitTime.getByDayOfWeek'; - - // we test VisitFrequency explicitly - $this->defaultApiNotToCall[] = 'VisitFrequency.get'; - } - /** * @dataProvider getApiForTesting */ @@ -81,14 +68,26 @@ class BackwardsCompatibility1XTest extends IntegrationTestCase $idSite = 1; $dateTime = '2012-03-06 11:22:33'; + $apiNotToCall = array( + // in the SQL dump, a referrer is named referer.com, but now in OneVisitorTwoVisits it is referrer.com + 'Referrers', + + // changes made to SQL dump to test VisitFrequency change the day of week + 'VisitTime.getByDayOfWeek', + + // we test VisitFrequency explicitly + 'VisitFrequency.get', + + // the Action.getPageTitles test fails for unknown reason, so skipping it + // eg. https://travis-ci.org/piwik/piwik/jobs/24449365 + 'Action.getPageTitles' + ); + return array( array('all', array('idSite' => $idSite, 'date' => $dateTime, 'compareAgainst' => 'OneVisitorTwoVisits', 'disableArchiving' => true, - - // the Action.getPageTitles test fails for unknown reason, so skipping it - // eg. https://travis-ci.org/piwik/piwik/jobs/24449365 - 'skipGetPageTitles' => true )), + 'apiNotToCall' => $apiNotToCall)), array('VisitFrequency.get', array('idSite' => $idSite, 'date' => '2012-03-03', 'setDateLastN' => true, 'disableArchiving' => true, 'testSuffix' => '_multipleDates')), @@ -108,4 +107,4 @@ class BackwardsCompatibility1XTest extends IntegrationTestCase BackwardsCompatibility1XTest::$fixture = new SqlDump(); BackwardsCompatibility1XTest::$fixture->dumpUrl = PIWIK_INCLUDE_PATH . BackwardsCompatibility1XTest::FIXTURE_LOCATION; -BackwardsCompatibility1XTest::$fixture->tablesPrefix = ''; \ No newline at end of file +BackwardsCompatibility1XTest::$fixture->tablesPrefix = ''; diff --git a/tests/PHPUnit/Integration/CustomEventsTest.php b/tests/PHPUnit/Integration/CustomEventsTest.php index c2a3bd9c6267f1532c3057bb9d6254b29505ee24..542e0b852b14eca0bba898abcecaf738c889e471 100644 --- a/tests/PHPUnit/Integration/CustomEventsTest.php +++ b/tests/PHPUnit/Integration/CustomEventsTest.php @@ -138,4 +138,4 @@ class CustomEventsTest extends IntegrationTestCase } } -CustomEventsTest::$fixture = new TwoVisitsWithCustomEvents(); \ No newline at end of file +CustomEventsTest::$fixture = new TwoVisitsWithCustomEvents(); diff --git a/tests/PHPUnit/Integration/EcommerceOrderWithItemsTest.php b/tests/PHPUnit/Integration/EcommerceOrderWithItemsTest.php index 6a3c5b796b40c4b260e499baa491c4def412cc43..cd0fe79f73e027dbf256cdcbd3c285ccd378b780 100755 --- a/tests/PHPUnit/Integration/EcommerceOrderWithItemsTest.php +++ b/tests/PHPUnit/Integration/EcommerceOrderWithItemsTest.php @@ -94,8 +94,11 @@ class EcommerceOrderWithItemsTest extends IntegrationTestCase // abandoned carts tests array($goalItemApi, array('idSite' => $idSite, 'date' => $dateTime, - 'periods' => array('day', 'week'), 'abandonedCarts' => 1, - 'testSuffix' => '_AbandonedCarts')), + 'periods' => array('day', 'week'), + 'testSuffix' => '_AbandonedCarts', + 'otherRequestParameters' => array( + 'abandonedCarts' => 1 + ))), // multiple periods tests array($goalItemApi, array('idSite' => $idSite, 'date' => $dateTime, 'periods' => array('day'), diff --git a/tests/PHPUnit/Integration/OneVisitorSeveralDaysImportedInRandomOrderTest.php b/tests/PHPUnit/Integration/OneVisitorSeveralDaysImportedInRandomOrderTest.php index 5c4f427e7937f235d91db85a6130bf327eafe3ec..675f4cef06da5b68cfb2f4d9987e162fc94c8792 100644 --- a/tests/PHPUnit/Integration/OneVisitorSeveralDaysImportedInRandomOrderTest.php +++ b/tests/PHPUnit/Integration/OneVisitorSeveralDaysImportedInRandomOrderTest.php @@ -36,15 +36,16 @@ class OneVisitorSeveralDaysImportedInRandomOrderTest extends IntegrationTestCase return array( // This should show 1 visit on 3 different days array('Live.getLastVisitsDetails', array( - 'idSite' => '1', - 'date' => self::$fixture->dateTime, - 'periods' => 'month', + 'idSite' => '1', + 'date' => self::$fixture->dateTime, + 'periods' => 'month', 'testSuffix' => '_shouldShowOneVisit_InEachOfThreeDays', - 'otherRequestParameters' => array('hideColumns' => 'visitorId') + 'otherRequestParameters' => array('hideColumns' => 'visitorId'), + 'keepLiveDates' => true )), ); } } -OneVisitorSeveralDaysImportedInRandomOrderTest::$fixture = new VisitOverSeveralDaysImportedLogs(); \ No newline at end of file +OneVisitorSeveralDaysImportedInRandomOrderTest::$fixture = new VisitOverSeveralDaysImportedLogs(); diff --git a/tests/PHPUnit/Integration/OneVisitorTwoVisitsTest.php b/tests/PHPUnit/Integration/OneVisitorTwoVisitsTest.php index 0de2982ad3267115613a732348bd4fb8bc8a2f88..af0fd8de446284b0c0c226fcade7847a736b02f6 100755 --- a/tests/PHPUnit/Integration/OneVisitorTwoVisitsTest.php +++ b/tests/PHPUnit/Integration/OneVisitorTwoVisitsTest.php @@ -230,4 +230,4 @@ class OneVisitorTwoVisitsTest extends IntegrationTestCase } OneVisitorTwoVisitsTest::$fixture = new OneVisitorTwoVisits(); -OneVisitorTwoVisitsTest::$fixture->excludeMozilla = true; \ No newline at end of file +OneVisitorTwoVisitsTest::$fixture->excludeMozilla = true; diff --git a/tests/PHPUnit/Integration/PeriodIsRangeDateIsLastNMetadataAndNormalAPITest.php b/tests/PHPUnit/Integration/PeriodIsRangeDateIsLastNMetadataAndNormalAPITest.php index cbf5b2e5f7de1eac94d04b09b293a724cf7ae427..c5847cdbfc65baa786d3be2ea8477ff683dd54fb 100755 --- a/tests/PHPUnit/Integration/PeriodIsRangeDateIsLastNMetadataAndNormalAPITest.php +++ b/tests/PHPUnit/Integration/PeriodIsRangeDateIsLastNMetadataAndNormalAPITest.php @@ -69,10 +69,9 @@ class PeriodIsRangeDateIsLastNMetadataAndNormalAPITest extends IntegrationTestCa foreach ($dates as $date) { $result[] = array($apiToCall, array('idSite' => $idSite, 'date' => $date, 'periods' => array('range'), 'segment' => $segment, - // testing getLastVisitsForVisitor requires a visitor ID - 'visitorId' => $visitorId, 'otherRequestParameters' => array( 'lastMinutes' => 60 * 24, + 'visitorId' => $visitorId // testing getLastVisitsForVisitor requires a visitor ID ))); } } diff --git a/tests/PHPUnit/Integration/RowEvolutionTest.php b/tests/PHPUnit/Integration/RowEvolutionTest.php index 268692df7e43297f75b8a7a8e1c22c72075dfdb6..2af1f5fccde37ea020e4b45d90825cebfe0c925b 100755 --- a/tests/PHPUnit/Integration/RowEvolutionTest.php +++ b/tests/PHPUnit/Integration/RowEvolutionTest.php @@ -88,7 +88,7 @@ class RowEvolutionTest extends IntegrationTestCase $config['periods'] = array('day', 'week'); $config['otherRequestParameters']['apiModule'] = 'Actions'; $config['otherRequestParameters']['apiAction'] = 'getPageTitles'; - $config['otherRequestParameters']['label'] = urlencode('incredible title 0'); + $config['otherRequestParameters']['label'] = ('incredible title 0'); $config['otherRequestParameters']['filter_limit'] = 1; // should have no effect $return[] = array('API.getRowEvolution', $config); diff --git a/tests/PHPUnit/Integration/TwoVisitorsTwoWebsitesDifferentDaysArchivingDisabledTest.php b/tests/PHPUnit/Integration/TwoVisitorsTwoWebsitesDifferentDaysArchivingDisabledTest.php index d9dce3ad9c3b4156101e3224d066fc100a2d4990..a5f1aa5f5ab0b692692e38ea4f9c139e197646f9 100755 --- a/tests/PHPUnit/Integration/TwoVisitorsTwoWebsitesDifferentDaysArchivingDisabledTest.php +++ b/tests/PHPUnit/Integration/TwoVisitorsTwoWebsitesDifferentDaysArchivingDisabledTest.php @@ -74,7 +74,6 @@ class TwoVisitorsTwoWebsitesDifferentDaysArchivingDisabledTest extends Integrati 'date' => $dateRange, 'periods' => array('range'), 'disableArchiving' => true, - 'hackDeleteRangeArchivesBefore' => true, 'testSuffix' => '_disabledBefore_isDateRange')), ); diff --git a/tests/PHPUnit/Integration/VisitsInPastInvalidateOldReportsTest.php b/tests/PHPUnit/Integration/VisitsInPastInvalidateOldReportsTest.php index 5ee8fa169972f31213a7917983231c6a6fec8a19..51c327e54d8dc1355dd279ede87a9b7e64426670 100644 --- a/tests/PHPUnit/Integration/VisitsInPastInvalidateOldReportsTest.php +++ b/tests/PHPUnit/Integration/VisitsInPastInvalidateOldReportsTest.php @@ -75,20 +75,20 @@ class VisitsInPastInvalidateOldReportsTest extends IntegrationTestCase // 1) Invalidate old reports for the 2 websites // Test invalidate 1 date only $r = new Request("module=API&method=CoreAdminHome.invalidateArchivedReports&idSites=4,5,6,55,-1,s',1&dates=2010-01-03"); - $this->checkRequestResponse($r->process()); + $this->assertApiResponseHasNoError($r->process()); // Test invalidate comma separated dates $r = new Request("module=API&method=CoreAdminHome.invalidateArchivedReports&idSites=" . $idSite . "," . $idSite2 . "&dates=2010-01-06,2009-10-30"); - $this->checkRequestResponse($r->process()); + $this->assertApiResponseHasNoError($r->process()); // test invalidate date in the past // Format=original will re-throw exception $r = new Request("module=API&method=CoreAdminHome.invalidateArchivedReports&idSites=" . $idSite2 . "&dates=2009-06-29&format=original"); - $this->checkRequestResponse( json_encode( $r->process() ) ); + $this->assertApiResponseHasNoError($r->process()); // invalidate a date more recent to check the date is only updated when it's earlier than current $r = new Request("module=API&method=CoreAdminHome.invalidateArchivedReports&idSites=" . $idSite2 . "&dates=2010-03-03"); - $this->checkRequestResponse($r->process()); + $this->assertApiResponseHasNoError($r->process()); // Make an invalid call $idSiteNoAccess = 777; diff --git a/tests/PHPUnit/IntegrationTestCase.php b/tests/PHPUnit/IntegrationTestCase.php index 107c575a3212b1de82b052a9c82da381e86cf60b..db4104c3cae03ef71f381c902e8133b63fe90dad 100755 --- a/tests/PHPUnit/IntegrationTestCase.php +++ b/tests/PHPUnit/IntegrationTestCase.php @@ -20,6 +20,9 @@ use Piwik\DbHelper; use Piwik\ReportRenderer; use Piwik\Translate; use Piwik\UrlHelper; +use Piwik\Tests\Impl\TestRequestCollection; +use Piwik\Tests\Impl\TestRequestResponse; +use Piwik\Tests\Impl\ApiTestConfig; use Piwik\Log; use PHPUnit_Framework_TestCase; @@ -33,38 +36,6 @@ require_once PIWIK_INCLUDE_PATH . '/libs/PiwikTracker/PiwikTracker.php'; */ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase { - public $defaultApiNotToCall = array( - 'LanguagesManager', - 'DBStats', - 'Dashboard', - 'UsersManager', - 'SitesManager', - 'ExampleUI', - 'Overlay', - 'Live', - 'SEO', - 'ExampleAPI', - 'ScheduledReports', - 'MobileMessaging', - 'Transitions', - 'API', - 'ImageGraph', - 'Annotations', - 'SegmentEditor', - 'UserCountry.getLocationFromIP', - 'Dashboard', - 'ExamplePluginTemplate', - 'CustomAlerts', - 'Insights' - ); - - /** - * List of Modules, or Module.Method that should not be called as part of the XML output compare - * Usually these modules either return random changing data, or are already tested in specific unit tests. - */ - public $apiNotToCall = array(); - public $apiToCall = array(); - /** * Identifies the last language used in an API/Controller call. * @@ -107,16 +78,6 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase $fixture->performTearDown(); } - public function setUp() - { - parent::setUp(); - - // Make sure the browser running the test does not influence the Country detection code - $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'en'; - - $this->changeLanguage('en'); - } - /** * Returns true if continuous integration running this request * Useful to exclude tests which may fail only on this setup @@ -308,417 +269,61 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase return $apiCalls; } - /** - * Given a list of default parameters to set, returns the URLs of APIs to call - * If any API was specified in $this->apiNotToCall we ensure only these are tested. - * If any API is set as excluded (see list below) then it will be ignored. - * - * @param array $parametersToSet Parameters to set in api call - * @param array $formats Array of 'format' to fetch from API - * @param array $periods Array of 'period' to query API - * @param bool $supertableApi - * @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. - * @param bool|string $language 2 letter language code, defaults to default piwik language - * @param bool|string $fileExtension - * - * @throws Exception - * - * @return array of API URLs query strings - */ - protected function generateUrlsApi($parametersToSet, $formats, $periods, $supertableApi = false, $setDateLastN = false, $language = false, $fileExtension = false) - { - // Get the URLs to query against the API for all functions starting with get* - $requestUrls = array(); - $apiMetadata = new DocumentationGenerator; - foreach (Proxy::getInstance()->getMetadata() as $class => $info) { - $moduleName = Proxy::getInstance()->getModuleNameFromClassName($class); - foreach ($info as $methodName => $infoMethod) { - $apiId = $moduleName . '.' . $methodName; - - // If Api to test were set, we only test these - if (!empty($this->apiToCall) - && in_array($moduleName, $this->apiToCall) === false - && in_array($apiId, $this->apiToCall) === false - ) { - continue; - } elseif ( - ((strpos($methodName, 'get') !== 0 && $methodName != 'generateReport') - || in_array($moduleName, $this->apiNotToCall) === true - || in_array($apiId, $this->apiNotToCall) === true - || $methodName == 'getLogoUrl' - || $methodName == 'getSVGLogoUrl' - || $methodName == 'hasSVGLogo' - || $methodName == 'getHeaderLogoUrl' - ) - ) { // Excluded modules from test - continue; - } - - foreach ($periods as $period) { - $parametersToSet['period'] = $period; - - // If date must be a date range, we process this date range by adding 6 periods to it - if ($setDateLastN) { - if (!isset($parametersToSet['dateRewriteBackup'])) { - $parametersToSet['dateRewriteBackup'] = $parametersToSet['date']; - } - - $lastCount = (int)$setDateLastN; - if ($setDateLastN === true) { - $lastCount = 6; - } - $firstDate = $parametersToSet['dateRewriteBackup']; - $secondDate = date('Y-m-d', strtotime("+$lastCount " . $period . "s", strtotime($firstDate))); - $parametersToSet['date'] = $firstDate . ',' . $secondDate; - } - - // Set response language - if ($language !== false) { - $parametersToSet['language'] = $language; - } - - // set idSubtable if subtable API is set - if ($supertableApi !== false) { - $request = new Request(array( - 'module' => 'API', - 'method' => $supertableApi, - 'idSite' => $parametersToSet['idSite'], - 'period' => $parametersToSet['period'], - 'date' => $parametersToSet['date'], - 'format' => 'php', - 'serialize' => 0, - )); - - // find first row w/ subtable - $content = $request->process(); - - $this->checkRequestResponse($content); - foreach ($content as $row) { - if (isset($row['idsubdatatable'])) { - $parametersToSet['idSubtable'] = $row['idsubdatatable']; - break; - } - } - - // if no subtable found, throw - if (!isset($parametersToSet['idSubtable'])) { - throw new Exception( - "Cannot find subtable to load for $apiId in $supertableApi."); - } - } - - // Generate for each specified format - foreach ($formats as $format) { - $parametersToSet['format'] = $format; - $parametersToSet['hideIdSubDatable'] = 1; - $parametersToSet['serialize'] = 1; - - $exampleUrl = $apiMetadata->getExampleUrl($class, $methodName, $parametersToSet); - - if ($exampleUrl === false) { - continue; - } - - // Remove the first ? in the query string - $exampleUrl = substr($exampleUrl, 1); - $apiRequestId = $apiId; - if (strpos($exampleUrl, 'period=') !== false) { - $apiRequestId .= '_' . $period; - } - - $apiRequestId .= '.' . $format; - - if ($fileExtension) { - $apiRequestId .= '.' . $fileExtension; - } - - $requestUrls[$apiRequestId] = $exampleUrl; - } - } - } - } - return $requestUrls; - } - - /** - * Will return all api urls for the given data - * - * @param string|array $formats String or array of formats to fetch from API - * @param int|bool $idSite Id site - * @param string|bool $dateTime Date time string of reports to request - * @param array|bool|string $periods String or array of strings of periods (day, week, month, year) - * @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 - * @param string|bool $language 2 letter language code to request data in - * @param string|bool $segment Custom Segment to query the data for - * @param string|bool $visitorId Only used for Live! API testing - * @param bool $abandonedCarts Only used in Goals API testing - * @param bool $idGoal - * @param bool $apiModule - * @param bool $apiAction - * @param array $otherRequestParameters - * @param array|bool $supertableApi - * @param array|bool $fileExtension - * - * @return array - */ - protected function _generateApiUrls($formats = 'xml', $idSite = false, $dateTime = false, $periods = false, - $setDateLastN = false, $language = false, $segment = false, $visitorId = false, - $abandonedCarts = false, $idGoal = false, $apiModule = false, $apiAction = false, - $otherRequestParameters = array(), $supertableApi = false, $fileExtension = false) + protected function _testApiUrl($testName, $apiId, $requestUrl, $compareAgainst, $xmlFieldsToRemove = array(), $params = array()) { - list($pathProcessed, $pathExpected) = static::getProcessedAndExpectedDirs(); - - if ($periods === false) { - $periods = 'day'; - } - if (!is_array($periods)) { - $periods = array($periods); - } - if (!is_array($formats)) { - $formats = array($formats); - } - if (!is_writable($pathProcessed)) { - $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/>'); - } - $parametersToSet = array( - 'idSite' => $idSite, - 'date' => ($periods == array('range') || strpos($dateTime, ',') !== false) ? - $dateTime : date('Y-m-d', strtotime($dateTime)), - 'expanded' => '1', - 'piwikUrl' => 'http://example.org/piwik/', - // Used in getKeywordsForPageUrl - 'url' => 'http://example.org/store/purchase.htm', - - // Used in Actions.getPageUrl, .getDownload, etc. - // tied to Main.test.php doTest_oneVisitorTwoVisits - // will need refactoring when these same API functions are tested in a new function - 'downloadUrl' => 'http://piwik.org/path/again/latest.zip?phpsessid=this is ignored when searching', - 'outlinkUrl' => 'http://dev.piwik.org/svn', - 'pageUrl' => 'http://example.org/index.htm?sessionid=this is also ignored by default', - 'pageName' => ' Checkout / Purchasing... ', - - // do not show the millisec timer in response or tests would always fail as value is changing - 'showTimer' => 0, - - 'language' => $language ? $language : 'en', - 'abandonedCarts' => $abandonedCarts ? 1 : 0, - 'idSites' => $idSite, - ); - $parametersToSet = array_merge($parametersToSet, $otherRequestParameters); - if (!empty($visitorId)) { - $parametersToSet['visitorId'] = $visitorId; - } - if (!empty($apiModule)) { - $parametersToSet['apiModule'] = $apiModule; - } - if (!empty($apiAction)) { - $parametersToSet['apiAction'] = $apiAction; - } - if (!empty($segment)) { - $parametersToSet['segment'] = urlencode($segment); - } - if ($idGoal !== false) { - $parametersToSet['idGoal'] = $idGoal; - } - - $requestUrls = $this->generateUrlsApi($parametersToSet, $formats, $periods, $supertableApi, $setDateLastN, $language, $fileExtension); - - $this->checkEnoughUrlsAreTested($requestUrls); - - return $requestUrls; - } - - protected function checkEnoughUrlsAreTested($requestUrls) - { - $countUrls = count($requestUrls); - $approximateCountApiToCall = count($this->apiToCall); - if (empty($requestUrls) - || $approximateCountApiToCall > $countUrls - ) { - throw new Exception("Only generated $countUrls API calls to test but was expecting more for this test.\n" . - "Want to test APIs: " . implode(", ", $this->apiToCall) . ")\n" . - "But only generated these URLs: \n" . implode("\n", $requestUrls) . ")\n" - ); - } - } - - protected function _testApiUrl($testName, $apiId, $requestUrl, $compareAgainst, $xmlFieldsToRemove = array()) - { - $isTestLogImportReverseChronological = strpos($testName, 'ImportedInRandomOrderTest') === false; - $isLiveMustDeleteDates = (strpos($requestUrl, 'Live.getLastVisits') !== false - || strpos($requestUrl, 'Live.getVisitorProfile') !== false) - // except for that particular test that we care about dates! - && $isTestLogImportReverseChronological; - - $request = new Request($requestUrl); - $dateTime = Common::getRequestVar('date', '', 'string', UrlHelper::getArrayFromQueryString($requestUrl)); - list($processedFilePath, $expectedFilePath) = $this->getProcessedAndExpectedPaths($testName, $apiId, $format = null, $compareAgainst); - // Cast as string is important. For example when calling - // with format=original, objects or php arrays can be returned. - // we also hide errors to prevent the 'headers already sent' in the ResponseBuilder (which sends Excel headers multiple times eg.) - $response = (string)$request->process(); - - if ($isLiveMustDeleteDates) { - $response = $this->removeAllLiveDatesFromXml($response); - } - $response = $this->normalizePdfContent($response); - - if (!empty($xmlFieldsToRemove)) { - $response = $this->removeXmlFields($response, $xmlFieldsToRemove); + $processedResponse = TestRequestResponse::loadFromApi($params, $requestUrl); + if (empty($compareAgainst)) { + $processedResponse->save($processedFilePath); } - $expected = $this->loadExpectedFile($expectedFilePath); - $expectedContent = $expected; - $expected = $this->normalizePdfContent($expected); - - if (empty($expected)) { - if (empty($compareAgainst)) { - file_put_contents($processedFilePath, $response); - } - - print("The expected file is not found at '$expectedFilePath'. The Processed response was:"); - print("\n----------------------------\n\n"); - var_dump($response); - print("\n----------------------------\n"); + try { + $expectedResponse = TestRequestResponse::loadFromFile($expectedFilePath, $params, $requestUrl); + } catch (Exception $ex) { + $this->handleMissingExpectedFile($expectedFilePath, $processedResponse); return; } - $expected = $this->removeXmlElement($expected, 'idsubdatatable', $testNotSmallAfter = false); - $response = $this->removeXmlElement($response, 'idsubdatatable', $testNotSmallAfter = false); - - if ($isLiveMustDeleteDates) { - $expected = $this->removeAllLiveDatesFromXml($expected); - } // If date=lastN the <prettyDate> element will change each day, we remove XML element before comparison - elseif (strpos($dateTime, 'last') !== false - || strpos($dateTime, 'today') !== false - || strpos($dateTime, 'now') !== false - ) { - if (strpos($requestUrl, 'API.getProcessedReport') !== false) { - $expected = $this->removePrettyDateFromXml($expected); - $response = $this->removePrettyDateFromXml($response); - } - - $expected = $this->removeXmlElement($expected, 'visitServerHour'); - $response = $this->removeXmlElement($response, 'visitServerHour'); - - if (strpos($requestUrl, 'date=') !== false) { - $regex = "/date=[-0-9,%Ca-z]+/"; // need to remove %2C which is encoded , - $expected = preg_replace($regex, 'date=', $expected); - $response = preg_replace($regex, 'date=', $response); - } - } - - // if idSubtable is in request URL, make sure idSubtable values are not in any urls - if (strpos($requestUrl, 'idSubtable=') !== false) { - $regex = "/idSubtable=[0-9]+/"; - $expected = preg_replace($regex, 'idSubtable=', $expected); - $response = preg_replace($regex, 'idSubtable=', $response); - } - - // Do not test for TRUNCATE(SUM()) returning .00 on mysqli since this is not working - // http://bugs.php.net/bug.php?id=54508 - $expected = str_replace('.000000</l', '</l', $expected); //lat/long - $response = str_replace('.000000</l', '</l', $response); //lat/long - $expected = str_replace('.00</revenue>', '</revenue>', $expected); - $response = str_replace('.00</revenue>', '</revenue>', $response); - $response = str_replace('.1</revenue>', '</revenue>', $response); - $expected = str_replace('.1</revenue>', '</revenue>', $expected); - $expected = str_replace('.11</revenue>', '</revenue>', $expected); - $response = str_replace('.11</revenue>', '</revenue>', $response); - - if (empty($compareAgainst)) { - file_put_contents($processedFilePath, $response); - } - try { - if (strpos($requestUrl, 'format=xml') !== false) { - $this->assertXmlStringEqualsXmlString($expected, $response, "Differences with expected in: $processedFilePath"); - } else { - $this->assertEquals(strlen($expected), strlen($response), "Differences with expected in: $processedFilePath"); - $this->assertEquals($expected, $response, "Differences with expected in: $processedFilePath"); - } - - if (trim($response) == trim($expected) - && empty($compareAgainst) - ) { - if(trim($expectedContent) != trim($expected)) { - file_put_contents($expectedFilePath, $expected); - } - } + TestRequestResponse::assertEquals($expectedResponse, $processedResponse, "Differences with expected in '$processedFilePath'"); } catch (Exception $ex) { $this->comparisonFailures[] = $ex; } } - protected function checkRequestResponse($response) + private function handleMissingExpectedFile($expectedFilePath, TestRequestResponse $processedResponse) { - if(!is_string($response)) { - $response = json_encode($response); - } - $this->assertTrue(stripos($response, 'error') === false, "error in $response"); - $this->assertTrue(stripos($response, 'exception') === false, "exception in $response"); - } + $this->missingExpectedFiles[] = $expectedFilePath; - protected function removeAllLiveDatesFromXml($input) - { - $toRemove = array( - 'serverDate', - 'firstActionTimestamp', - 'lastActionTimestamp', - 'lastActionDateTime', - 'serverTimestamp', - 'serverTimePretty', - 'serverDatePretty', - 'serverDatePrettyFirstAction', - 'serverTimePrettyFirstAction', - 'goalTimePretty', - 'serverTimePretty', - 'visitorId', - 'nextVisitorId', - 'previousVisitorId', - 'visitServerHour', - 'date', - 'prettyDate', - 'serverDateTimePrettyFirstAction' - ); - return $this->removeXmlFields($input, $toRemove); + print("The expected file is not found at '$expectedFilePath'. The Processed response was:"); + print("\n----------------------------\n\n"); + var_dump($processedResponse->getResponseText()); + print("\n----------------------------\n"); } - protected function removeXmlFields($input, $toRemove) + public static function assertApiResponseHasNoError($response) { - foreach ($toRemove as $xml) { - $input = $this->removeXmlElement($input, $xml); + if(!is_string($response)) { + $response = json_encode($response); } - return $input; - } - - protected function removePrettyDateFromXml($input) - { - return $this->removeXmlElement($input, 'prettyDate'); + self::assertTrue(stripos($response, 'error') === false, "error in $response"); + self::assertTrue(stripos($response, 'exception') === false, "exception in $response"); } - protected function removeXmlElement($input, $xmlElement, $testNotSmallAfter = true) + protected static function getProcessedAndExpectedDirs() { - // Only raise error if there was some data before - $testNotSmallAfter = strlen($input > 100) && $testNotSmallAfter; - - $oldInput = $input; - $input = preg_replace('/(<' . $xmlElement . '>.+?<\/' . $xmlElement . '>)/', '', $input); + $path = static::getPathToTestDirectory(); + $processedPath = $path . '/processed/'; - //check we didn't delete the whole string - if ($testNotSmallAfter && $input != $oldInput) { - $this->assertTrue(strlen($input) > 100); + if (!is_writable($processedPath)) { + $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 ' . $processedPath . '<br/>chmod 777 ' . $processedPath + . '</code><br/>'); } - return $input; - } - protected static function getProcessedAndExpectedDirs() - { - $path = static::getPathToTestDirectory(); - return array($path . '/processed/', $path . '/expected/'); + return array($processedPath, $path . '/expected/'); } private function getProcessedAndExpectedPaths($testName, $testId, $format = null, $compareAgainst = false) @@ -729,23 +334,15 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase } $processedFilename = $testName . $filenameSuffix; - $expectedFilename = ($compareAgainst ?: $testName) . $filenameSuffix; + + $expectedFilename = $compareAgainst ? ('test_' . $compareAgainst) : $testName; + $expectedFilename .= $filenameSuffix; list($processedDir, $expectedDir) = static::getProcessedAndExpectedDirs(); return array($processedDir . $processedFilename, $expectedDir . $expectedFilename); } - private function loadExpectedFile($filePath) - { - $result = @file_get_contents($filePath); - if (empty($result)) { - $this->missingExpectedFiles[] = $filePath; - return null; - } - return $result; - } - /** * Returns an array describing the API methods to call & compare with * expected output. @@ -761,26 +358,7 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase * ) * </code> * - * Valid test options: - * <ul> - * <li><b>testSuffix</b> The suffix added to the test name. Helps determine - * the filename of the expected output.</li> - * <li><b>format</b> The desired format of the output. Defaults to 'xml'.</li> - * <li><b>idSite</b> The id of the website to get data for.</li> - * <li><b>date</b> The date to get data for.</li> - * <li><b>periods</b> The period or periods to get data for. Can be an array.</li> - * <li><b>setDateLastN</b> Flag describing whether to query for a set of - * dates or not.</li> - * <li><b>language</b> The language to use.</li> - * <li><b>segment</b> The segment to use.</li> - * <li><b>visitorId</b> The visitor ID to use.</li> - * <li><b>abandonedCarts</b> Whether to look for abandoned carts or not.</li> - * <li><b>idGoal</b> The goal ID to use.</li> - * <li><b>apiModule</b> The value to use in the apiModule request parameter.</li> - * <li><b>apiAction</b> The value to use in the apiAction request parameter.</li> - * <li><b>otherRequestParameters</b> An array of extra request parameters to use.</li> - * <li><b>disableArchiving</b> Disable archiving before running tests.</li> - * </ul> + * Valid test options are described in the ApiTestConfig class docs. * * All test options are optional, except 'idSite' & 'date'. */ @@ -800,32 +378,13 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase return $result; } - protected function _setCallableApi($api) - { - if ($api == 'all') { - $this->apiToCall = array(); - $this->apiNotToCall = $this->defaultApiNotToCall; - } else { - if (!is_array($api)) { - $api = array($api); - } - - $this->apiToCall = $api; - - if (!in_array('UserCountry.getLocationFromIP', $api)) { - $this->apiNotToCall = array('API.getPiwikVersion', - 'UserCountry.getLocationFromIP'); - } else { - $this->apiNotToCall = array(); - } - } - } - /** * Runs API tests. */ protected function runApiTests($api, $params) { + $testConfig = new ApiTestConfig($params); + // make sure that the reports we process here are not directly deleted in ArchiveProcessor/PluginsArchiver // (because we process reports in the past, they would sometimes be invalid, and would have been deleted) \Piwik\ArchiveProcessor\Rules::disablePurgeOutdatedArchives(); @@ -834,9 +393,7 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase $this->missingExpectedFiles = array(); $this->comparisonFailures = array(); - $this->_setCallableApi($api); - - if (isset($params['disableArchiving']) && $params['disableArchiving'] === true) { + if ($testConfig->disableArchiving) { Rules::$archivingDisabledByTests = true; Config::getInstance()->General['browser_archiving_disabled_enforce'] = 1; } else { @@ -844,46 +401,14 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase Config::getInstance()->General['browser_archiving_disabled_enforce'] = 0; } - if(!empty($params['hackDeleteRangeArchivesBefore'])) { - Db::query('delete from '. Common::prefixTable('archive_numeric_2009_12') . ' where period = 5'); - Db::query('delete from '. Common::prefixTable('archive_blob_2009_12') . ' where period = 5'); + if ($testConfig->language) { + $this->changeLanguage($testConfig->language); } - if (isset($params['language'])) { - $this->changeLanguage($params['language']); - } + $testRequests = new TestRequestCollection($api, $testConfig, $api); - $testSuffix = isset($params['testSuffix']) ? $params['testSuffix'] : ''; - - $requestUrls = $this->_generateApiUrls( - isset($params['format']) ? $params['format'] : 'xml', - isset($params['idSite']) ? $params['idSite'] : false, - isset($params['date']) ? $params['date'] : false, - isset($params['periods']) ? $params['periods'] : (isset($params['period']) ? $params['period'] : false), - isset($params['setDateLastN']) ? $params['setDateLastN'] : false, - isset($params['language']) ? $params['language'] : false, - isset($params['segment']) ? $params['segment'] : false, - isset($params['visitorId']) ? $params['visitorId'] : false, - isset($params['abandonedCarts']) ? $params['abandonedCarts'] : false, - isset($params['idGoal']) ? $params['idGoal'] : false, - isset($params['apiModule']) ? $params['apiModule'] : false, - isset($params['apiAction']) ? $params['apiAction'] : false, - isset($params['otherRequestParameters']) ? $params['otherRequestParameters'] : array(), - isset($params['supertableApi']) ? $params['supertableApi'] : false, - isset($params['fileExtension']) ? $params['fileExtension'] : false); - - $compareAgainst = isset($params['compareAgainst']) ? ('test_' . $params['compareAgainst']) : false; - $xmlFieldsToRemove = @$params['xmlFieldsToRemove']; - - foreach ($requestUrls as $apiId => $requestUrl) { - // this is a hack - if(isset($params['skipGetPageTitles'])) { - if($apiId == 'Actions.getPageTitles_day.xml') { - continue; - } - } - - $this->_testApiUrl($testName . $testSuffix, $apiId, $requestUrl, $compareAgainst, $xmlFieldsToRemove); + foreach ($testRequests->getRequestUrls() as $apiId => $requestUrl) { + $this->_testApiUrl($testName . $testConfig->testSuffix, $apiId, $requestUrl, $testConfig->compareAgainst, $testConfig->xmlFieldsToRemove, $params); } // Restore normal purge behavior @@ -904,22 +429,26 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase // Display as one error all sub-failures if (!empty($this->comparisonFailures)) { - $messages = ''; - $i = 1; - foreach ($this->comparisonFailures as $failure) { - $msg = $failure->getMessage(); - $msg = strtok($msg, "\n"); - $messages .= "\n#" . $i++ . ": " . $msg; - } - $messages .= " \n "; - print($messages); - $first = reset($this->comparisonFailures); - throw $first; + $this->printComparisonFailures(); + throw reset($this->comparisonFailures); } return count($this->comparisonFailures) == 0; } + private function printComparisonFailures() + { + $messages = ''; + foreach ($this->comparisonFailures as $index => $failure) { + $msg = $failure->getMessage(); + $msg = strtok($msg, "\n"); + $messages .= "\n#" . ($index + 1) . ": " . $msg; + } + $messages .= " \n "; + + print($messages); + } + /** * changing the language within one request is a bit fancy * in order to keep the core clean, we need a little hack here @@ -1024,26 +553,4 @@ abstract class IntegrationTestCase extends PHPUnit_Framework_TestCase ArchiveTableCreator::refreshTableList($forceReload = true); } - - /** - * Removes content from PDF binary the content that changes with the datetime or other random Ids - */ - protected function normalizePdfContent($response) - { - // normalize date markups and document ID in pdf files : - // - /LastModified (D:20120820204023+00'00') - // - /CreationDate (D:20120820202226+00'00') - // - /ModDate (D:20120820202226+00'00') - // - /M (D:20120820202226+00'00') - // - /ID [ <0f5cc387dc28c0e13e682197f485fe65> <0f5cc387dc28c0e13e682197f485fe65> ] - $response = preg_replace('/\(D:[0-9]{14}/', '(D:19700101000000', $response); - $response = preg_replace('/\/ID \[ <.*> ]/', '', $response); - $response = preg_replace('/\/id:\[ <.*> ]/', '', $response); - $response = $this->removeXmlElement($response, "xmp:CreateDate"); - $response = $this->removeXmlElement($response, "xmp:ModifyDate"); - $response = $this->removeXmlElement($response, "xmp:MetadataDate"); - $response = $this->removeXmlElement($response, "xmpMM:DocumentID"); - $response = $this->removeXmlElement($response, "xmpMM:InstanceID"); - return $response; - } } diff --git a/tests/PHPUnit/bootstrap.php b/tests/PHPUnit/bootstrap.php index 4058a867d4ade70529f0bb1fd2a5672a87b0f126..d32c604026d002fc203b56b0eb1cf2f6d7c4f9cf 100644 --- a/tests/PHPUnit/bootstrap.php +++ b/tests/PHPUnit/bootstrap.php @@ -41,6 +41,9 @@ require_once PIWIK_INCLUDE_PATH . '/tests/PHPUnit/ConsoleCommandTestCase.php'; require_once PIWIK_INCLUDE_PATH . '/tests/PHPUnit/FakeAccess.php'; require_once PIWIK_INCLUDE_PATH . '/tests/PHPUnit/MockPiwikOption.php'; require_once PIWIK_INCLUDE_PATH . '/tests/PHPUnit/TestingEnvironment.php'; +require_once PIWIK_INCLUDE_PATH . '/tests/PHPUnit/Impl/TestRequestCollection.php'; +require_once PIWIK_INCLUDE_PATH . '/tests/PHPUnit/Impl/TestRequestResponse.php'; +require_once PIWIK_INCLUDE_PATH . '/tests/PHPUnit/Impl/ApiTestConfig.php'; \Piwik\Profiler::setupProfilerXHProf( $mainRun = true ); diff --git a/tests/PHPUnit/travis.sh b/tests/PHPUnit/travis.sh index 331b59f4e46cd1b8cb1fc8b7dda0a52349386def..0c6631b55091ba5489643a39de8a9ae2c2b12819 100755 --- a/tests/PHPUnit/travis.sh +++ b/tests/PHPUnit/travis.sh @@ -60,5 +60,4 @@ then fi else travis_wait phpunit --configuration phpunit.xml --coverage-text --colors -fi - +fi \ No newline at end of file