PaginatorComponentTest.php 36 KB

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