PaginatorTestTrait.php 41 KB

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