PaginatorTestTrait.php 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  12. * @link http://cakephp.org CakePHP(tm) Project
  13. * @since 3.9.0
  14. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\Datasource;
  17. use Cake\Core\Configure;
  18. use Cake\Datasource\ConnectionManager;
  19. use Cake\Datasource\EntityInterface;
  20. use Cake\Datasource\Exception\PageOutOfBoundsException;
  21. use Cake\Datasource\Paginator;
  22. use Cake\Datasource\RepositoryInterface;
  23. trait PaginatorTestTrait
  24. {
  25. /**
  26. * @var \Cake\Datasource\Paginator
  27. */
  28. protected $Paginator;
  29. /**
  30. * setup
  31. *
  32. * @return void
  33. */
  34. public function setUp(): void
  35. {
  36. parent::setUp();
  37. Configure::write('App.namespace', 'TestApp');
  38. $this->Paginator = new Paginator();
  39. $this->Post = $this->getMockRepository();
  40. }
  41. /**
  42. * tearDown
  43. *
  44. * @return void
  45. */
  46. public function tearDown(): void
  47. {
  48. parent::tearDown();
  49. $this->getTableLocator()->clear();
  50. }
  51. /**
  52. * Test that non-numeric values are rejected for page, and limit
  53. *
  54. * @return void
  55. */
  56. public function testPageParamCasting()
  57. {
  58. $this->Post->expects($this->any())
  59. ->method('getAlias')
  60. ->will($this->returnValue('Posts'));
  61. $query = $this->_getMockFindQuery();
  62. $this->Post->expects($this->any())
  63. ->method('find')
  64. ->will($this->returnValue($query));
  65. $params = ['page' => '1 " onclick="alert(\'xss\');">'];
  66. $settings = ['limit' => 1, 'maxLimit' => 10];
  67. $this->Paginator->paginate($this->Post, $params, $settings);
  68. $pagingParams = $this->Paginator->getPagingParams();
  69. $this->assertSame(1, $pagingParams['Posts']['page'], 'XSS exploit opened');
  70. }
  71. /**
  72. * test that unknown keys in the default settings are
  73. * passed to the find operations.
  74. *
  75. * @return void
  76. */
  77. public function testPaginateExtraParams()
  78. {
  79. $params = ['page' => '-1'];
  80. $settings = [
  81. 'PaginatorPosts' => [
  82. 'contain' => ['PaginatorAuthor'],
  83. 'maxLimit' => 10,
  84. 'group' => 'PaginatorPosts.published',
  85. 'order' => ['PaginatorPosts.id' => 'ASC'],
  86. ],
  87. ];
  88. $table = $this->_getMockPosts(['query']);
  89. $query = $this->_getMockFindQuery();
  90. $table->expects($this->once())
  91. ->method('query')
  92. ->will($this->returnValue($query));
  93. $query->expects($this->once())
  94. ->method('applyOptions')
  95. ->with([
  96. 'contain' => ['PaginatorAuthor'],
  97. 'group' => 'PaginatorPosts.published',
  98. 'limit' => 10,
  99. 'order' => ['PaginatorPosts.id' => 'ASC'],
  100. 'page' => 1,
  101. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  102. 'scope' => null,
  103. 'sort' => 'PaginatorPosts.id',
  104. ]);
  105. $this->Paginator->paginate($table, $params, $settings);
  106. }
  107. /**
  108. * Test to make sure options get sent to custom finder methods via paginate
  109. *
  110. * @return void
  111. */
  112. public function testPaginateCustomFinderOptions()
  113. {
  114. $this->loadFixtures('Posts');
  115. $settings = [
  116. 'PaginatorPosts' => [
  117. 'finder' => ['author' => ['author_id' => 1]],
  118. ],
  119. ];
  120. $table = $this->getTableLocator()->get('PaginatorPosts');
  121. $expected = $table
  122. ->find('author', [
  123. 'conditions' => [
  124. 'PaginatorPosts.author_id' => 1,
  125. ],
  126. ])
  127. ->count();
  128. $result = $this->Paginator->paginate($table, [], $settings)->count();
  129. $this->assertEquals($expected, $result);
  130. }
  131. /**
  132. * Test that nested eager loaders don't trigger invalid SQL errors.
  133. *
  134. * @return void
  135. */
  136. public function testPaginateNestedEagerLoader()
  137. {
  138. $this->loadFixtures('Articles', 'Tags', 'Authors', 'ArticlesTags', 'AuthorsTags');
  139. $articles = $this->getTableLocator()->get('Articles');
  140. $articles->belongsToMany('Tags');
  141. $tags = $this->getTableLocator()->get('Tags');
  142. $tags->belongsToMany('Authors');
  143. $articles->getEventManager()->on('Model.beforeFind', function ($event, $query) {
  144. $query ->matching('Tags', function ($q) {
  145. return $q->matching('Authors', function ($q) {
  146. return $q->where(['Authors.name' => 'larry']);
  147. });
  148. });
  149. });
  150. $results = $this->Paginator->paginate($articles);
  151. $result = $results->first();
  152. $this->assertInstanceOf(EntityInterface::class, $result);
  153. $this->assertInstanceOf(EntityInterface::class, $result->_matchingData['Tags']);
  154. $this->assertInstanceOf(EntityInterface::class, $result->_matchingData['Authors']);
  155. }
  156. /**
  157. * test that flat default pagination parameters work.
  158. *
  159. * @return void
  160. */
  161. public function testDefaultPaginateParams()
  162. {
  163. $settings = [
  164. 'order' => ['PaginatorPosts.id' => 'DESC'],
  165. 'maxLimit' => 10,
  166. ];
  167. $table = $this->_getMockPosts(['query']);
  168. $query = $this->_getMockFindQuery();
  169. $table->expects($this->once())
  170. ->method('query')
  171. ->will($this->returnValue($query));
  172. $query->expects($this->once())
  173. ->method('applyOptions')
  174. ->with([
  175. 'limit' => 10,
  176. 'page' => 1,
  177. 'order' => ['PaginatorPosts.id' => 'DESC'],
  178. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  179. 'scope' => null,
  180. 'sort' => 'PaginatorPosts.id',
  181. ]);
  182. $this->Paginator->paginate($table, [], $settings);
  183. }
  184. /**
  185. * Tests that flat default pagination parameters work for multi order.
  186. *
  187. * @return void
  188. */
  189. public function testDefaultPaginateParamsMultiOrder()
  190. {
  191. $settings = [
  192. 'order' => ['PaginatorPosts.id' => 'DESC', 'PaginatorPosts.title' => 'ASC'],
  193. ];
  194. $table = $this->_getMockPosts(['query']);
  195. $query = $this->_getMockFindQuery();
  196. $table->expects($this->once())
  197. ->method('query')
  198. ->will($this->returnValue($query));
  199. $query->expects($this->once())
  200. ->method('applyOptions')
  201. ->with([
  202. 'limit' => 20,
  203. 'page' => 1,
  204. 'order' => $settings['order'],
  205. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  206. 'scope' => null,
  207. 'sort' => null,
  208. ]);
  209. $this->Paginator->paginate($table, [], $settings);
  210. $pagingParams = $this->Paginator->getPagingParams();
  211. $this->assertNull($pagingParams['PaginatorPosts']['direction']);
  212. $this->assertFalse($pagingParams['PaginatorPosts']['sortDefault']);
  213. $this->assertFalse($pagingParams['PaginatorPosts']['directionDefault']);
  214. }
  215. /**
  216. * test that default sort and default direction are injected into request
  217. *
  218. * @return void
  219. */
  220. public function testDefaultPaginateParamsIntoRequest()
  221. {
  222. $settings = [
  223. 'order' => ['PaginatorPosts.id' => 'DESC'],
  224. 'maxLimit' => 10,
  225. ];
  226. $table = $this->_getMockPosts(['query']);
  227. $query = $this->_getMockFindQuery();
  228. $table->expects($this->once())
  229. ->method('query')
  230. ->will($this->returnValue($query));
  231. $query->expects($this->once())
  232. ->method('applyOptions')
  233. ->with([
  234. 'limit' => 10,
  235. 'page' => 1,
  236. 'order' => ['PaginatorPosts.id' => 'DESC'],
  237. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  238. 'scope' => null,
  239. 'sort' => 'PaginatorPosts.id',
  240. ]);
  241. $this->Paginator->paginate($table, [], $settings);
  242. $pagingParams = $this->Paginator->getPagingParams();
  243. $this->assertEquals('PaginatorPosts.id', $pagingParams['PaginatorPosts']['sortDefault']);
  244. $this->assertEquals('DESC', $pagingParams['PaginatorPosts']['directionDefault']);
  245. }
  246. /**
  247. * test that option merging prefers specific models
  248. *
  249. * @return void
  250. */
  251. public function testMergeOptionsModelSpecific()
  252. {
  253. $settings = [
  254. 'page' => 1,
  255. 'limit' => 20,
  256. 'maxLimit' => 100,
  257. 'Posts' => [
  258. 'page' => 1,
  259. 'limit' => 10,
  260. 'maxLimit' => 50,
  261. ],
  262. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  263. ];
  264. $defaults = $this->Paginator->getDefaults('Silly', $settings);
  265. $result = $this->Paginator->mergeOptions([], $defaults);
  266. $this->assertEquals($settings, $result);
  267. $defaults = $this->Paginator->getDefaults('Posts', $settings);
  268. $result = $this->Paginator->mergeOptions([], $defaults);
  269. $expected = ['page' => 1, 'limit' => 10, 'maxLimit' => 50, 'whitelist' => ['limit', 'sort', 'page', 'direction']];
  270. $this->assertEquals($expected, $result);
  271. }
  272. /**
  273. * test mergeOptions with custom scope
  274. *
  275. * @return void
  276. */
  277. public function testMergeOptionsCustomScope()
  278. {
  279. $params = [
  280. 'page' => 10,
  281. 'limit' => 10,
  282. 'scope' => [
  283. 'page' => 2,
  284. 'limit' => 5,
  285. ],
  286. ];
  287. $settings = [
  288. 'page' => 1,
  289. 'limit' => 20,
  290. 'maxLimit' => 100,
  291. 'finder' => 'myCustomFind',
  292. ];
  293. $defaults = $this->Paginator->getDefaults('Post', $settings);
  294. $result = $this->Paginator->mergeOptions($params, $defaults);
  295. $expected = [
  296. 'page' => 10,
  297. 'limit' => 10,
  298. 'maxLimit' => 100,
  299. 'finder' => 'myCustomFind',
  300. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  301. ];
  302. $this->assertEquals($expected, $result);
  303. $settings = [
  304. 'page' => 1,
  305. 'limit' => 20,
  306. 'maxLimit' => 100,
  307. 'finder' => 'myCustomFind',
  308. 'scope' => 'non-existent',
  309. ];
  310. $defaults = $this->Paginator->getDefaults('Post', $settings);
  311. $result = $this->Paginator->mergeOptions($params, $defaults);
  312. $expected = [
  313. 'page' => 1,
  314. 'limit' => 20,
  315. 'maxLimit' => 100,
  316. 'finder' => 'myCustomFind',
  317. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  318. 'scope' => 'non-existent',
  319. ];
  320. $this->assertEquals($expected, $result);
  321. $settings = [
  322. 'page' => 1,
  323. 'limit' => 20,
  324. 'maxLimit' => 100,
  325. 'finder' => 'myCustomFind',
  326. 'scope' => 'scope',
  327. ];
  328. $defaults = $this->Paginator->getDefaults('Post', $settings);
  329. $result = $this->Paginator->mergeOptions($params, $defaults);
  330. $expected = [
  331. 'page' => 2,
  332. 'limit' => 5,
  333. 'maxLimit' => 100,
  334. 'finder' => 'myCustomFind',
  335. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  336. 'scope' => 'scope',
  337. ];
  338. $this->assertEquals($expected, $result);
  339. }
  340. /**
  341. * test mergeOptions with customFind key
  342. *
  343. * @return void
  344. */
  345. public function testMergeOptionsCustomFindKey()
  346. {
  347. $params = [
  348. 'page' => 10,
  349. 'limit' => 10,
  350. ];
  351. $settings = [
  352. 'page' => 1,
  353. 'limit' => 20,
  354. 'maxLimit' => 100,
  355. 'finder' => 'myCustomFind',
  356. ];
  357. $defaults = $this->Paginator->getDefaults('Post', $settings);
  358. $result = $this->Paginator->mergeOptions($params, $defaults);
  359. $expected = [
  360. 'page' => 10,
  361. 'limit' => 10,
  362. 'maxLimit' => 100,
  363. 'finder' => 'myCustomFind',
  364. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  365. ];
  366. $this->assertEquals($expected, $result);
  367. }
  368. /**
  369. * test merging options from the querystring.
  370. *
  371. * @return void
  372. */
  373. public function testMergeOptionsQueryString()
  374. {
  375. $params = [
  376. 'page' => 99,
  377. 'limit' => 75,
  378. ];
  379. $settings = [
  380. 'page' => 1,
  381. 'limit' => 20,
  382. 'maxLimit' => 100,
  383. ];
  384. $defaults = $this->Paginator->getDefaults('Post', $settings);
  385. $result = $this->Paginator->mergeOptions($params, $defaults);
  386. $expected = ['page' => 99, 'limit' => 75, 'maxLimit' => 100, 'whitelist' => ['limit', 'sort', 'page', 'direction']];
  387. $this->assertEquals($expected, $result);
  388. }
  389. /**
  390. * test that the default whitelist doesn't let people screw with things they should not be allowed to.
  391. *
  392. * @return void
  393. */
  394. public function testMergeOptionsDefaultWhiteList()
  395. {
  396. $params = [
  397. 'page' => 10,
  398. 'limit' => 10,
  399. 'fields' => ['bad.stuff'],
  400. 'recursive' => 1000,
  401. 'conditions' => ['bad.stuff'],
  402. 'contain' => ['bad'],
  403. ];
  404. $settings = [
  405. 'page' => 1,
  406. 'limit' => 20,
  407. 'maxLimit' => 100,
  408. ];
  409. $defaults = $this->Paginator->getDefaults('Post', $settings);
  410. $result = $this->Paginator->mergeOptions($params, $defaults);
  411. $expected = ['page' => 10, 'limit' => 10, 'maxLimit' => 100, 'whitelist' => ['limit', 'sort', 'page', 'direction']];
  412. $this->assertEquals($expected, $result);
  413. }
  414. /**
  415. * test that modifying the whitelist works.
  416. *
  417. * @return void
  418. */
  419. public function testMergeOptionsExtraWhitelist()
  420. {
  421. $params = [
  422. 'page' => 10,
  423. 'limit' => 10,
  424. 'fields' => ['bad.stuff'],
  425. 'recursive' => 1000,
  426. 'conditions' => ['bad.stuff'],
  427. 'contain' => ['bad'],
  428. ];
  429. $settings = [
  430. 'page' => 1,
  431. 'limit' => 20,
  432. 'maxLimit' => 100,
  433. ];
  434. $this->Paginator->setConfig('whitelist', ['fields']);
  435. $defaults = $this->Paginator->getDefaults('Post', $settings);
  436. $result = $this->Paginator->mergeOptions($params, $defaults);
  437. $expected = [
  438. 'page' => 10, 'limit' => 10, 'maxLimit' => 100, 'fields' => ['bad.stuff'], 'whitelist' => ['limit', 'sort', 'page', 'direction', 'fields'],
  439. ];
  440. $this->assertEquals($expected, $result);
  441. }
  442. /**
  443. * test mergeOptions with limit > maxLimit in code.
  444. *
  445. * @return void
  446. */
  447. public function testMergeOptionsMaxLimit()
  448. {
  449. $settings = [
  450. 'limit' => 200,
  451. 'paramType' => 'named',
  452. ];
  453. $defaults = $this->Paginator->getDefaults('Post', $settings);
  454. $result = $this->Paginator->mergeOptions([], $defaults);
  455. $expected = [
  456. 'page' => 1,
  457. 'limit' => 100,
  458. 'maxLimit' => 100,
  459. 'paramType' => 'named',
  460. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  461. ];
  462. $this->assertEquals($expected, $result);
  463. $settings = [
  464. 'maxLimit' => 10,
  465. 'paramType' => 'named',
  466. ];
  467. $defaults = $this->Paginator->getDefaults('Post', $settings);
  468. $result = $this->Paginator->mergeOptions([], $defaults);
  469. $expected = [
  470. 'page' => 1,
  471. 'limit' => 10,
  472. 'maxLimit' => 10,
  473. 'paramType' => 'named',
  474. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  475. ];
  476. $this->assertEquals($expected, $result);
  477. }
  478. /**
  479. * test getDefaults with limit > maxLimit in code.
  480. *
  481. * @return void
  482. */
  483. public function testGetDefaultMaxLimit()
  484. {
  485. $settings = [
  486. 'page' => 1,
  487. 'limit' => 2,
  488. 'maxLimit' => 10,
  489. 'order' => [
  490. 'Users.username' => 'asc',
  491. ],
  492. ];
  493. $defaults = $this->Paginator->getDefaults('Post', $settings);
  494. $result = $this->Paginator->mergeOptions([], $defaults);
  495. $expected = [
  496. 'page' => 1,
  497. 'limit' => 2,
  498. 'maxLimit' => 10,
  499. 'order' => [
  500. 'Users.username' => 'asc',
  501. ],
  502. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  503. ];
  504. $this->assertEquals($expected, $result);
  505. $settings = [
  506. 'page' => 1,
  507. 'limit' => 100,
  508. 'maxLimit' => 10,
  509. 'order' => [
  510. 'Users.username' => 'asc',
  511. ],
  512. ];
  513. $defaults = $this->Paginator->getDefaults('Post', $settings);
  514. $result = $this->Paginator->mergeOptions([], $defaults);
  515. $expected = [
  516. 'page' => 1,
  517. 'limit' => 10,
  518. 'maxLimit' => 10,
  519. 'order' => [
  520. 'Users.username' => 'asc',
  521. ],
  522. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  523. ];
  524. $this->assertEquals($expected, $result);
  525. }
  526. /**
  527. * Integration test to ensure that validateSort is being used by paginate()
  528. *
  529. * @return void
  530. */
  531. public function testValidateSortInvalid()
  532. {
  533. $table = $this->_getMockPosts(['query']);
  534. $query = $this->_getMockFindQuery();
  535. $table->expects($this->once())
  536. ->method('query')
  537. ->will($this->returnValue($query));
  538. $query->expects($this->once())->method('applyOptions')
  539. ->with([
  540. 'limit' => 20,
  541. 'page' => 1,
  542. 'order' => ['PaginatorPosts.id' => 'asc'],
  543. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  544. 'scope' => null,
  545. 'sort' => 'id',
  546. ]);
  547. $params = [
  548. 'page' => 1,
  549. 'sort' => 'id',
  550. 'direction' => 'herp',
  551. ];
  552. $this->Paginator->paginate($table, $params);
  553. $pagingParams = $this->Paginator->getPagingParams();
  554. $this->assertEquals('id', $pagingParams['PaginatorPosts']['sort']);
  555. $this->assertEquals('asc', $pagingParams['PaginatorPosts']['direction']);
  556. }
  557. /**
  558. * test that invalid directions are ignored.
  559. *
  560. * @return void
  561. */
  562. public function testValidateSortInvalidDirection()
  563. {
  564. $model = $this->getMockRepository();
  565. $model->expects($this->any())
  566. ->method('getAlias')
  567. ->will($this->returnValue('model'));
  568. $model->expects($this->any())
  569. ->method('hasField')
  570. ->will($this->returnValue(true));
  571. $options = ['sort' => 'something', 'direction' => 'boogers'];
  572. $result = $this->Paginator->validateSort($model, $options);
  573. $this->assertEquals('asc', $result['order']['model.something']);
  574. }
  575. /**
  576. * Test that "sort" and "direction" in paging params is properly set based
  577. * on initial value of "order" in paging settings.
  578. *
  579. * @return void
  580. */
  581. public function testValidaSortInitialSortAndDirection()
  582. {
  583. $table = $this->_getMockPosts(['query']);
  584. $query = $this->_getMockFindQuery();
  585. $table->expects($this->once())
  586. ->method('query')
  587. ->will($this->returnValue($query));
  588. $query->expects($this->once())->method('applyOptions')
  589. ->with([
  590. 'limit' => 20,
  591. 'page' => 1,
  592. 'order' => ['PaginatorPosts.id' => 'asc'],
  593. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  594. 'sort' => 'id',
  595. 'scope' => null,
  596. 'sortWhitelist' => ['id'],
  597. ]);
  598. $options = [
  599. 'order' => [
  600. 'id' => 'asc',
  601. ],
  602. 'sortWhitelist' => ['id'],
  603. ];
  604. $this->Paginator->paginate($table, [], $options);
  605. $pagingParams = $this->Paginator->getPagingParams();
  606. $this->assertEquals('id', $pagingParams['PaginatorPosts']['sort']);
  607. $this->assertEquals('asc', $pagingParams['PaginatorPosts']['direction']);
  608. }
  609. /**
  610. * Test that "sort" and "direction" in paging params is properly set based
  611. * on initial value of "order" in paging settings.
  612. *
  613. * @return void
  614. */
  615. public function testValidateSortAndDirectionAliased()
  616. {
  617. $table = $this->_getMockPosts(['query']);
  618. $query = $this->_getMockFindQuery();
  619. $table->expects($this->once())
  620. ->method('query')
  621. ->will($this->returnValue($query));
  622. $query->expects($this->once())->method('applyOptions')
  623. ->with([
  624. 'limit' => 20,
  625. 'page' => 1,
  626. 'order' => ['PaginatorPosts.title' => 'asc'],
  627. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  628. 'sort' => 'title',
  629. 'scope' => null,
  630. ]);
  631. $options = [
  632. 'order' => [
  633. 'Articles.title' => 'desc',
  634. ],
  635. ];
  636. $queryParams = [
  637. 'page' => 1,
  638. 'sort' => 'title',
  639. 'direction' => 'asc',
  640. ];
  641. $this->Paginator->paginate($table, $queryParams, $options);
  642. $pagingParams = $this->Paginator->getPagingParams();
  643. $this->assertEquals('title', $pagingParams['PaginatorPosts']['sort']);
  644. $this->assertEquals('asc', $pagingParams['PaginatorPosts']['direction']);
  645. $this->assertEquals('Articles.title', $pagingParams['PaginatorPosts']['sortDefault']);
  646. $this->assertEquals('desc', $pagingParams['PaginatorPosts']['directionDefault']);
  647. }
  648. /**
  649. * testValidateSortRetainsOriginalSortValue
  650. *
  651. * @return void
  652. * @see https://github.com/cakephp/cakephp/issues/11740
  653. */
  654. public function testValidateSortRetainsOriginalSortValue()
  655. {
  656. $table = $this->_getMockPosts(['query']);
  657. $query = $this->_getMockFindQuery();
  658. $table->expects($this->once())
  659. ->method('query')
  660. ->will($this->returnValue($query));
  661. $query->expects($this->once())->method('applyOptions')
  662. ->with([
  663. 'limit' => 20,
  664. 'page' => 1,
  665. 'order' => ['PaginatorPosts.id' => 'asc'],
  666. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  667. 'scope' => null,
  668. 'sortWhitelist' => ['id'],
  669. 'sort' => 'id',
  670. ]);
  671. $params = [
  672. 'page' => 1,
  673. 'sort' => 'id',
  674. 'direction' => 'herp',
  675. ];
  676. $options = [
  677. 'sortWhitelist' => ['id'],
  678. ];
  679. $this->Paginator->paginate($table, $params, $options);
  680. $pagingParams = $this->Paginator->getPagingParams();
  681. $this->assertEquals('id', $pagingParams['PaginatorPosts']['sort']);
  682. }
  683. /**
  684. * Test that a really large page number gets clamped to the max page size.
  685. *
  686. * @return void
  687. */
  688. public function testOutOfRangePageNumberGetsClamped()
  689. {
  690. $this->loadFixtures('Posts');
  691. $params['page'] = 3000;
  692. $table = $this->getTableLocator()->get('PaginatorPosts');
  693. try {
  694. $this->Paginator->paginate($table, $params);
  695. $this->fail('No exception raised');
  696. } catch (PageOutOfBoundsException $exception) {
  697. $this->assertEquals(
  698. 'Page number 3000 could not be found.',
  699. $exception->getMessage()
  700. );
  701. $this->assertSame(
  702. [
  703. 'requestedPage' => 3000,
  704. 'pagingParams' => $this->Paginator->getPagingParams(),
  705. ],
  706. $exception->getAttributes()
  707. );
  708. }
  709. }
  710. /**
  711. * Test that a really REALLY large page number gets clamped to the max page size.
  712. *
  713. * @return void
  714. */
  715. public function testOutOfVeryBigPageNumberGetsClamped()
  716. {
  717. $this->expectException(\Cake\Datasource\Exception\PageOutOfBoundsException::class);
  718. $this->loadFixtures('Posts');
  719. $params = [
  720. 'page' => '3000000000000000000000000',
  721. ];
  722. $table = $this->getTableLocator()->get('PaginatorPosts');
  723. $this->Paginator->paginate($table, $params);
  724. }
  725. /**
  726. * test that fields not in whitelist won't be part of order conditions.
  727. *
  728. * @return void
  729. */
  730. public function testValidateSortWhitelistFailure()
  731. {
  732. $model = $this->mockAliasHasFieldModel();
  733. $options = [
  734. 'sort' => 'body',
  735. 'direction' => 'asc',
  736. 'sortWhitelist' => ['title', 'id'],
  737. ];
  738. $result = $this->Paginator->validateSort($model, $options);
  739. $this->assertEquals([], $result['order']);
  740. }
  741. /**
  742. * test that fields in the whitelist are not validated
  743. *
  744. * @return void
  745. */
  746. public function testValidateSortWhitelistTrusted()
  747. {
  748. $model = $this->mockAliasHasFieldModel();
  749. $options = [
  750. 'sort' => 'body',
  751. 'direction' => 'asc',
  752. 'sortWhitelist' => ['body'],
  753. ];
  754. $result = $this->Paginator->validateSort($model, $options);
  755. $expected = ['model.body' => 'asc'];
  756. $this->assertEquals(
  757. $expected,
  758. $result['order'],
  759. 'Trusted fields in schema should be prefixed'
  760. );
  761. }
  762. /**
  763. * test that whitelist as empty array does not allow any sorting
  764. *
  765. * @return void
  766. */
  767. public function testValidateSortWhitelistEmpty()
  768. {
  769. $model = $this->mockAliasHasFieldModel();
  770. $options = [
  771. 'order' => [
  772. 'body' => 'asc',
  773. 'foo.bar' => 'asc',
  774. ],
  775. 'sort' => 'body',
  776. 'direction' => 'asc',
  777. 'sortWhitelist' => [],
  778. ];
  779. $result = $this->Paginator->validateSort($model, $options);
  780. $this->assertSame([], $result['order'], 'No sort should be applied');
  781. }
  782. /**
  783. * test that fields in the whitelist are not validated
  784. *
  785. * @return void
  786. */
  787. public function testValidateSortWhitelistNotInSchema()
  788. {
  789. $model = $this->getMockRepository();
  790. $model->expects($this->any())
  791. ->method('getAlias')
  792. ->will($this->returnValue('model'));
  793. $model->expects($this->once())->method('hasField')
  794. ->will($this->returnValue(false));
  795. $options = [
  796. 'sort' => 'score',
  797. 'direction' => 'asc',
  798. 'sortWhitelist' => ['score'],
  799. ];
  800. $result = $this->Paginator->validateSort($model, $options);
  801. $expected = ['score' => 'asc'];
  802. $this->assertEquals(
  803. $expected,
  804. $result['order'],
  805. 'Trusted fields not in schema should not be altered'
  806. );
  807. }
  808. /**
  809. * test that multiple fields in the whitelist are not validated and properly aliased.
  810. *
  811. * @return void
  812. */
  813. public function testValidateSortWhitelistMultiple()
  814. {
  815. $model = $this->mockAliasHasFieldModel();
  816. $options = [
  817. 'order' => [
  818. 'body' => 'asc',
  819. 'foo.bar' => 'asc',
  820. ],
  821. 'sortWhitelist' => ['body', 'foo.bar'],
  822. ];
  823. $result = $this->Paginator->validateSort($model, $options);
  824. $expected = [
  825. 'model.body' => 'asc',
  826. 'foo.bar' => 'asc',
  827. ];
  828. $this->assertEquals($expected, $result['order']);
  829. }
  830. /**
  831. * @return \Cake\Datasource\RepositoryInterface|\PHPUnit\Framework\MockObject\MockObject
  832. */
  833. protected function getMockRepository()
  834. {
  835. $model = $this->getMockBuilder(RepositoryInterface::class)
  836. ->getMock();
  837. return $model;
  838. }
  839. /**
  840. * @param string $modelAlias Model alias to use.
  841. * @return \Cake\Datasource\RepositoryInterface|\PHPUnit\Framework\MockObject\MockObject
  842. */
  843. protected function mockAliasHasFieldModel($modelAlias = 'model')
  844. {
  845. $model = $this->getMockRepository();
  846. $model->expects($this->any())
  847. ->method('getAlias')
  848. ->will($this->returnValue($modelAlias));
  849. $model->expects($this->any())
  850. ->method('hasField')
  851. ->will($this->returnValue(true));
  852. return $model;
  853. }
  854. /**
  855. * test that multiple sort works.
  856. *
  857. * @return void
  858. */
  859. public function testValidateSortMultiple()
  860. {
  861. $model = $this->mockAliasHasFieldModel();
  862. $options = [
  863. 'order' => [
  864. 'author_id' => 'asc',
  865. 'title' => 'asc',
  866. ],
  867. ];
  868. $result = $this->Paginator->validateSort($model, $options);
  869. $expected = [
  870. 'model.author_id' => 'asc',
  871. 'model.title' => 'asc',
  872. ];
  873. $this->assertEquals($expected, $result['order']);
  874. }
  875. /**
  876. * test that multiple sort adds in query data.
  877. *
  878. * @return void
  879. */
  880. public function testValidateSortMultipleWithQuery()
  881. {
  882. $model = $this->mockAliasHasFieldModel();
  883. $options = [
  884. 'sort' => 'created',
  885. 'direction' => 'desc',
  886. 'order' => [
  887. 'author_id' => 'asc',
  888. 'title' => 'asc',
  889. ],
  890. ];
  891. $result = $this->Paginator->validateSort($model, $options);
  892. $expected = [
  893. 'model.created' => 'desc',
  894. 'model.author_id' => 'asc',
  895. 'model.title' => 'asc',
  896. ];
  897. $this->assertEquals($expected, $result['order']);
  898. $options = [
  899. 'sort' => 'title',
  900. 'direction' => 'desc',
  901. 'order' => [
  902. 'author_id' => 'asc',
  903. 'title' => 'asc',
  904. ],
  905. ];
  906. $result = $this->Paginator->validateSort($model, $options);
  907. $expected = [
  908. 'model.title' => 'desc',
  909. 'model.author_id' => 'asc',
  910. ];
  911. $this->assertEquals($expected, $result['order']);
  912. }
  913. /**
  914. * Tests that sort query string and model prefixes default match on assoc merging.
  915. *
  916. * @return void
  917. */
  918. public function testValidateSortMultipleWithQueryAndAliasedDefault()
  919. {
  920. $model = $this->mockAliasHasFieldModel();
  921. $options = [
  922. 'sort' => 'created',
  923. 'direction' => 'desc',
  924. 'order' => [
  925. 'model.created' => 'asc',
  926. ],
  927. ];
  928. $result = $this->Paginator->validateSort($model, $options);
  929. $expected = [
  930. 'sort' => 'created',
  931. 'order' => [
  932. 'model.created' => 'desc',
  933. ],
  934. ];
  935. $this->assertEquals($expected, $result);
  936. }
  937. /**
  938. * Tests that order strings can used by Paginator
  939. *
  940. * @return void
  941. */
  942. public function testValidateSortWithString()
  943. {
  944. $model = $this->mockAliasHasFieldModel();
  945. $options = [
  946. 'order' => 'model.author_id DESC',
  947. ];
  948. $result = $this->Paginator->validateSort($model, $options);
  949. $expected = 'model.author_id DESC';
  950. $this->assertEquals($expected, $result['order']);
  951. }
  952. /**
  953. * Test that no sort doesn't trigger an error.
  954. *
  955. * @return void
  956. */
  957. public function testValidateSortNoSort()
  958. {
  959. $model = $this->mockAliasHasFieldModel();
  960. $options = [
  961. 'direction' => 'asc',
  962. 'sortWhitelist' => ['title', 'id'],
  963. ];
  964. $result = $this->Paginator->validateSort($model, $options);
  965. $this->assertEquals([], $result['order']);
  966. }
  967. /**
  968. * Test sorting with incorrect aliases on valid fields.
  969. *
  970. * @return void
  971. */
  972. public function testValidateSortInvalidAlias()
  973. {
  974. $model = $this->mockAliasHasFieldModel();
  975. $options = ['sort' => 'Derp.id'];
  976. $result = $this->Paginator->validateSort($model, $options);
  977. $this->assertEquals([], $result['order']);
  978. }
  979. /**
  980. * @return array
  981. */
  982. public function checkLimitProvider()
  983. {
  984. return [
  985. 'out of bounds' => [
  986. ['limit' => 1000000, 'maxLimit' => 100],
  987. 100,
  988. ],
  989. 'limit is nan' => [
  990. ['limit' => 'sheep!', 'maxLimit' => 100],
  991. 1,
  992. ],
  993. 'negative limit' => [
  994. ['limit' => '-1', 'maxLimit' => 100],
  995. 1,
  996. ],
  997. 'unset limit' => [
  998. ['limit' => null, 'maxLimit' => 100],
  999. 1,
  1000. ],
  1001. 'limit = 0' => [
  1002. ['limit' => 0, 'maxLimit' => 100],
  1003. 1,
  1004. ],
  1005. 'limit = 0 v2' => [
  1006. ['limit' => 0, 'maxLimit' => 0],
  1007. 1,
  1008. ],
  1009. 'limit = null' => [
  1010. ['limit' => null, 'maxLimit' => 0],
  1011. 1,
  1012. ],
  1013. 'bad input, results in 1' => [
  1014. ['limit' => null, 'maxLimit' => null],
  1015. 1,
  1016. ],
  1017. 'bad input, results in 1 v2' => [
  1018. ['limit' => false, 'maxLimit' => false],
  1019. 1,
  1020. ],
  1021. ];
  1022. }
  1023. /**
  1024. * test that maxLimit is respected
  1025. *
  1026. * @dataProvider checkLimitProvider
  1027. * @return void
  1028. */
  1029. public function testCheckLimit($input, $expected)
  1030. {
  1031. $result = $this->Paginator->checkLimit($input);
  1032. $this->assertSame($expected, $result['limit']);
  1033. }
  1034. /**
  1035. * Integration test for checkLimit() being applied inside paginate()
  1036. *
  1037. * @return void
  1038. */
  1039. public function testPaginateMaxLimit()
  1040. {
  1041. $this->loadFixtures('Posts');
  1042. $table = $this->getTableLocator()->get('PaginatorPosts');
  1043. $settings = [
  1044. 'maxLimit' => 100,
  1045. ];
  1046. $params = [
  1047. 'limit' => '1000',
  1048. ];
  1049. $this->Paginator->paginate($table, $params, $settings);
  1050. $pagingParams = $this->Paginator->getPagingParams();
  1051. $this->assertEquals(100, $pagingParams['PaginatorPosts']['limit']);
  1052. $this->assertEquals(100, $pagingParams['PaginatorPosts']['perPage']);
  1053. $params = [
  1054. 'limit' => '10',
  1055. ];
  1056. $this->Paginator->paginate($table, $params, $settings);
  1057. $pagingParams = $this->Paginator->getPagingParams();
  1058. $this->assertEquals(10, $pagingParams['PaginatorPosts']['limit']);
  1059. $this->assertEquals(10, $pagingParams['PaginatorPosts']['perPage']);
  1060. }
  1061. /**
  1062. * test paginate() and custom finders to ensure the count + find
  1063. * use the custom type.
  1064. *
  1065. * @return void
  1066. */
  1067. public function testPaginateCustomFindCount()
  1068. {
  1069. $settings = [
  1070. 'finder' => 'published',
  1071. 'limit' => 2,
  1072. ];
  1073. $table = $this->_getMockPosts(['query']);
  1074. $query = $this->_getMockFindQuery();
  1075. $table->expects($this->once())
  1076. ->method('query')
  1077. ->will($this->returnValue($query));
  1078. $query->expects($this->once())->method('applyOptions')
  1079. ->with([
  1080. 'limit' => 2,
  1081. 'page' => 1,
  1082. 'order' => [],
  1083. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  1084. 'scope' => null,
  1085. 'sort' => null,
  1086. ]);
  1087. $this->Paginator->paginate($table, [], $settings);
  1088. }
  1089. /**
  1090. * Tests that it is possible to pass an already made query object to
  1091. * paginate()
  1092. *
  1093. * @return void
  1094. */
  1095. public function testPaginateQuery()
  1096. {
  1097. $params = ['page' => '-1'];
  1098. $settings = [
  1099. 'PaginatorPosts' => [
  1100. 'contain' => ['PaginatorAuthor'],
  1101. 'maxLimit' => 10,
  1102. 'group' => 'PaginatorPosts.published',
  1103. 'order' => ['PaginatorPosts.id' => 'ASC'],
  1104. ],
  1105. ];
  1106. $table = $this->_getMockPosts(['find']);
  1107. $query = $this->_getMockFindQuery($table);
  1108. $table->expects($this->never())->method('find');
  1109. $query->expects($this->once())
  1110. ->method('applyOptions')
  1111. ->with([
  1112. 'contain' => ['PaginatorAuthor'],
  1113. 'group' => 'PaginatorPosts.published',
  1114. 'limit' => 10,
  1115. 'order' => ['PaginatorPosts.id' => 'ASC'],
  1116. 'page' => 1,
  1117. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  1118. 'scope' => null,
  1119. 'sort' => 'PaginatorPosts.id',
  1120. ]);
  1121. $this->Paginator->paginate($query, $params, $settings);
  1122. }
  1123. /**
  1124. * test paginate() with bind()
  1125. *
  1126. * @return void
  1127. */
  1128. public function testPaginateQueryWithBindValue()
  1129. {
  1130. $config = ConnectionManager::getConfig('test');
  1131. $this->skipIf(strpos($config['driver'], 'Sqlserver') !== false, 'Test temporarily broken in SQLServer');
  1132. $this->loadFixtures('Posts');
  1133. $table = $this->getTableLocator()->get('PaginatorPosts');
  1134. $query = $table->find()
  1135. ->where(['PaginatorPosts.author_id BETWEEN :start AND :end'])
  1136. ->bind(':start', 1)
  1137. ->bind(':end', 2);
  1138. $results = $this->Paginator->paginate($query, []);
  1139. $result = $results->toArray();
  1140. $this->assertCount(2, $result);
  1141. $this->assertEquals('First Post', $result[0]->title);
  1142. $this->assertEquals('Third Post', $result[1]->title);
  1143. }
  1144. /**
  1145. * Tests that passing a query object with a limit clause set will
  1146. * overwrite it with the passed defaults.
  1147. *
  1148. * @return void
  1149. */
  1150. public function testPaginateQueryWithLimit()
  1151. {
  1152. $params = ['page' => '-1'];
  1153. $settings = [
  1154. 'PaginatorPosts' => [
  1155. 'contain' => ['PaginatorAuthor'],
  1156. 'maxLimit' => 10,
  1157. 'limit' => 5,
  1158. 'group' => 'PaginatorPosts.published',
  1159. 'order' => ['PaginatorPosts.id' => 'ASC'],
  1160. ],
  1161. ];
  1162. $table = $this->_getMockPosts(['find']);
  1163. $query = $this->_getMockFindQuery($table);
  1164. $query->limit(2);
  1165. $table->expects($this->never())->method('find');
  1166. $query->expects($this->once())
  1167. ->method('applyOptions')
  1168. ->with([
  1169. 'contain' => ['PaginatorAuthor'],
  1170. 'group' => 'PaginatorPosts.published',
  1171. 'limit' => 5,
  1172. 'order' => ['PaginatorPosts.id' => 'ASC'],
  1173. 'page' => 1,
  1174. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  1175. 'scope' => null,
  1176. 'sort' => 'PaginatorPosts.id',
  1177. ]);
  1178. $this->Paginator->paginate($query, $params, $settings);
  1179. }
  1180. /**
  1181. * Helper method for making mocks.
  1182. *
  1183. * @param array $methods
  1184. * @return \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject
  1185. */
  1186. protected function _getMockPosts($methods = [])
  1187. {
  1188. return $this->getMockBuilder('TestApp\Model\Table\PaginatorPostsTable')
  1189. ->setMethods($methods)
  1190. ->setConstructorArgs([[
  1191. 'connection' => ConnectionManager::get('test'),
  1192. 'alias' => 'PaginatorPosts',
  1193. 'schema' => [
  1194. 'id' => ['type' => 'integer'],
  1195. 'author_id' => ['type' => 'integer', 'null' => false],
  1196. 'title' => ['type' => 'string', 'null' => false],
  1197. 'body' => 'text',
  1198. 'published' => ['type' => 'string', 'length' => 1, 'default' => 'N'],
  1199. '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]],
  1200. ],
  1201. ]])
  1202. ->getMock();
  1203. }
  1204. /**
  1205. * Helper method for mocking queries.
  1206. *
  1207. * @param string|null $table
  1208. * @return \Cake\ORM\Query|\PHPUnit\Framework\MockObject\MockObject
  1209. */
  1210. protected function _getMockFindQuery($table = null)
  1211. {
  1212. /** @var \Cake\ORM\Query|\PHPUnit\Framework\MockObject\MockObject $query */
  1213. $query = $this->getMockBuilder('Cake\ORM\Query')
  1214. ->setMethods(['total', 'all', 'count', 'applyOptions'])
  1215. ->disableOriginalConstructor()
  1216. ->getMock();
  1217. $results = $this->getMockBuilder('Cake\ORM\ResultSet')
  1218. ->disableOriginalConstructor()
  1219. ->getMock();
  1220. $query->expects($this->any())
  1221. ->method('count')
  1222. ->will($this->returnValue(2));
  1223. $query->expects($this->any())
  1224. ->method('all')
  1225. ->will($this->returnValue($results));
  1226. if ($table) {
  1227. $query->repository($table);
  1228. }
  1229. return $query;
  1230. }
  1231. }