PaginatorComponentTest.php 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192
  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. /**
  746. * @return array
  747. */
  748. public function checkLimitProvider()
  749. {
  750. return [
  751. 'out of bounds' => [
  752. ['limit' => 1000000, 'maxLimit' => 100],
  753. 100,
  754. ],
  755. 'limit is nan' => [
  756. ['limit' => 'sheep!', 'maxLimit' => 100],
  757. 1,
  758. ],
  759. 'negative limit' => [
  760. ['limit' => '-1', 'maxLimit' => 100],
  761. 1,
  762. ],
  763. 'unset limit' => [
  764. ['limit' => null, 'maxLimit' => 100],
  765. 1,
  766. ],
  767. 'limit = 0' => [
  768. ['limit' => 0, 'maxLimit' => 100],
  769. 1,
  770. ],
  771. 'limit = 0 v2' => [
  772. ['limit' => 0, 'maxLimit' => 0],
  773. 1,
  774. ],
  775. 'limit = null' => [
  776. ['limit' => null, 'maxLimit' => 0],
  777. 1,
  778. ],
  779. 'bad input, results in 1' => [
  780. ['limit' => null, 'maxLimit' => null],
  781. 1,
  782. ],
  783. 'bad input, results in 1 v2' => [
  784. ['limit' => false, 'maxLimit' => false],
  785. 1,
  786. ],
  787. ];
  788. }
  789. /**
  790. * test that maxLimit is respected
  791. *
  792. * @dataProvider checkLimitProvider
  793. * @return void
  794. */
  795. public function testCheckLimit($input, $expected)
  796. {
  797. $result = $this->Paginator->checkLimit($input);
  798. $this->assertSame($expected, $result['limit']);
  799. }
  800. /**
  801. * Integration test for checkLimit() being applied inside paginate()
  802. *
  803. * @return void
  804. */
  805. public function testPaginateMaxLimit()
  806. {
  807. $this->loadFixtures('Posts');
  808. $table = TableRegistry::get('PaginatorPosts');
  809. $settings = [
  810. 'maxLimit' => 100,
  811. ];
  812. $this->request->query = [
  813. 'limit' => '1000'
  814. ];
  815. $this->Paginator->paginate($table, $settings);
  816. $this->assertEquals(100, $this->request->params['paging']['PaginatorPosts']['limit']);
  817. $this->assertEquals(100, $this->request->params['paging']['PaginatorPosts']['perPage']);
  818. $this->request->query = [
  819. 'limit' => '10'
  820. ];
  821. $this->Paginator->paginate($table, $settings);
  822. $this->assertEquals(10, $this->request->params['paging']['PaginatorPosts']['limit']);
  823. $this->assertEquals(10, $this->request->params['paging']['PaginatorPosts']['perPage']);
  824. }
  825. /**
  826. * test paginate() and custom find, to make sure the correct count is returned.
  827. *
  828. * @return void
  829. */
  830. public function testPaginateCustomFind()
  831. {
  832. $this->loadFixtures('Posts');
  833. $titleExtractor = function ($result) {
  834. $ids = [];
  835. foreach ($result as $record) {
  836. $ids[] = $record->title;
  837. }
  838. return $ids;
  839. };
  840. $table = TableRegistry::get('PaginatorPosts');
  841. $data = ['author_id' => 3, 'title' => 'Fourth Post', 'body' => 'Article Body, unpublished', 'published' => 'N'];
  842. $result = $table->save(new \Cake\ORM\Entity($data));
  843. $this->assertNotEmpty($result);
  844. $result = $this->Paginator->paginate($table);
  845. $this->assertCount(4, $result, '4 rows should come back');
  846. $this->assertEquals(['First Post', 'Second Post', 'Third Post', 'Fourth Post'], $titleExtractor($result));
  847. $result = $this->request->params['paging']['PaginatorPosts'];
  848. $this->assertEquals(4, $result['current']);
  849. $this->assertEquals(4, $result['count']);
  850. $settings = ['finder' => 'published'];
  851. $result = $this->Paginator->paginate($table, $settings);
  852. $this->assertCount(3, $result, '3 rows should come back');
  853. $this->assertEquals(['First Post', 'Second Post', 'Third Post'], $titleExtractor($result));
  854. $result = $this->request->params['paging']['PaginatorPosts'];
  855. $this->assertEquals(3, $result['current']);
  856. $this->assertEquals(3, $result['count']);
  857. $settings = ['finder' => 'published', 'limit' => 2, 'page' => 2];
  858. $result = $this->Paginator->paginate($table, $settings);
  859. $this->assertCount(1, $result, '1 rows should come back');
  860. $this->assertEquals(['Third Post'], $titleExtractor($result));
  861. $result = $this->request->params['paging']['PaginatorPosts'];
  862. $this->assertEquals(1, $result['current']);
  863. $this->assertEquals(3, $result['count']);
  864. $this->assertEquals(2, $result['pageCount']);
  865. $settings = ['finder' => 'published', 'limit' => 2];
  866. $result = $this->Paginator->paginate($table, $settings);
  867. $this->assertCount(2, $result, '2 rows should come back');
  868. $this->assertEquals(['First Post', 'Second Post'], $titleExtractor($result));
  869. $result = $this->request->params['paging']['PaginatorPosts'];
  870. $this->assertEquals(2, $result['current']);
  871. $this->assertEquals(3, $result['count']);
  872. $this->assertEquals(2, $result['pageCount']);
  873. $this->assertTrue($result['nextPage']);
  874. $this->assertFalse($result['prevPage']);
  875. $this->assertEquals(2, $result['perPage']);
  876. $this->assertNull($result['limit']);
  877. }
  878. /**
  879. * test paginate() and custom find with fields array, to make sure the correct count is returned.
  880. *
  881. * @return void
  882. */
  883. public function testPaginateCustomFindFieldsArray()
  884. {
  885. $this->loadFixtures('Posts');
  886. $table = TableRegistry::get('PaginatorPosts');
  887. $data = ['author_id' => 3, 'title' => 'Fourth Article', 'body' => 'Article Body, unpublished', 'published' => 'N'];
  888. $table->save(new \Cake\ORM\Entity($data));
  889. $settings = [
  890. 'finder' => 'list',
  891. 'conditions' => ['PaginatorPosts.published' => 'Y'],
  892. 'limit' => 2
  893. ];
  894. $results = $this->Paginator->paginate($table, $settings);
  895. $result = $results->toArray();
  896. $expected = [
  897. 1 => 'First Post',
  898. 2 => 'Second Post',
  899. ];
  900. $this->assertEquals($expected, $result);
  901. $result = $this->request->params['paging']['PaginatorPosts'];
  902. $this->assertEquals(2, $result['current']);
  903. $this->assertEquals(3, $result['count']);
  904. $this->assertEquals(2, $result['pageCount']);
  905. $this->assertTrue($result['nextPage']);
  906. $this->assertFalse($result['prevPage']);
  907. }
  908. /**
  909. * test paginate() and custom finders to ensure the count + find
  910. * use the custom type.
  911. *
  912. * @return void
  913. */
  914. public function testPaginateCustomFindCount()
  915. {
  916. $settings = [
  917. 'finder' => 'published',
  918. 'limit' => 2
  919. ];
  920. $table = $this->_getMockPosts(['query']);
  921. $query = $this->_getMockFindQuery();
  922. $table->expects($this->once())
  923. ->method('query')
  924. ->will($this->returnValue($query));
  925. $query->expects($this->once())->method('applyOptions')
  926. ->with([
  927. 'limit' => 2,
  928. 'page' => 1,
  929. 'order' => [],
  930. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  931. 'scope' => null,
  932. ]);
  933. $this->Paginator->paginate($table, $settings);
  934. }
  935. /**
  936. * Tests that it is possible to pass an already made query object to
  937. * paginate()
  938. *
  939. * @return void
  940. */
  941. public function testPaginateQuery()
  942. {
  943. $this->request->query = ['page' => '-1'];
  944. $settings = [
  945. 'PaginatorPosts' => [
  946. 'contain' => ['PaginatorAuthor'],
  947. 'maxLimit' => 10,
  948. 'group' => 'PaginatorPosts.published',
  949. 'order' => ['PaginatorPosts.id' => 'ASC']
  950. ]
  951. ];
  952. $table = $this->_getMockPosts(['find']);
  953. $query = $this->_getMockFindQuery($table);
  954. $table->expects($this->never())->method('find');
  955. $query->expects($this->once())
  956. ->method('applyOptions')
  957. ->with([
  958. 'contain' => ['PaginatorAuthor'],
  959. 'group' => 'PaginatorPosts.published',
  960. 'limit' => 10,
  961. 'order' => ['PaginatorPosts.id' => 'ASC'],
  962. 'page' => 1,
  963. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  964. 'scope' => null,
  965. ]);
  966. $this->Paginator->paginate($query, $settings);
  967. }
  968. /**
  969. * test paginate() with bind()
  970. *
  971. * @return void
  972. */
  973. public function testPaginateQueryWithBindValue()
  974. {
  975. $config = ConnectionManager::config('test');
  976. $this->skipIf(strpos($config['driver'], 'Sqlserver') !== false, 'Test temporarily broken in SQLServer');
  977. $this->loadFixtures('Posts');
  978. $table = TableRegistry::get('PaginatorPosts');
  979. $query = $table->find()
  980. ->where(['PaginatorPosts.author_id BETWEEN :start AND :end'])
  981. ->bind(':start', 1)
  982. ->bind(':end', 2);
  983. $results = $this->Paginator->paginate($query, []);
  984. $result = $results->toArray();
  985. $this->assertCount(2, $result);
  986. $this->assertEquals('First Post', $result[0]->title);
  987. $this->assertEquals('Third Post', $result[1]->title);
  988. }
  989. /**
  990. * Tests that passing a query object with a limit clause set will
  991. * overwrite it with the passed defaults.
  992. *
  993. * @return void
  994. */
  995. public function testPaginateQueryWithLimit()
  996. {
  997. $this->request->query = ['page' => '-1'];
  998. $settings = [
  999. 'PaginatorPosts' => [
  1000. 'contain' => ['PaginatorAuthor'],
  1001. 'maxLimit' => 10,
  1002. 'limit' => 5,
  1003. 'group' => 'PaginatorPosts.published',
  1004. 'order' => ['PaginatorPosts.id' => 'ASC']
  1005. ]
  1006. ];
  1007. $table = $this->_getMockPosts(['find']);
  1008. $query = $this->_getMockFindQuery($table);
  1009. $query->limit(2);
  1010. $table->expects($this->never())->method('find');
  1011. $query->expects($this->once())
  1012. ->method('applyOptions')
  1013. ->with([
  1014. 'contain' => ['PaginatorAuthor'],
  1015. 'group' => 'PaginatorPosts.published',
  1016. 'limit' => 5,
  1017. 'order' => ['PaginatorPosts.id' => 'ASC'],
  1018. 'page' => 1,
  1019. 'whitelist' => ['limit', 'sort', 'page', 'direction'],
  1020. 'scope' => null,
  1021. ]);
  1022. $this->Paginator->paginate($query, $settings);
  1023. }
  1024. /**
  1025. * Helper method for making mocks.
  1026. *
  1027. * @param array $methods
  1028. * @return \Cake\ORM\Table
  1029. */
  1030. protected function _getMockPosts($methods = [])
  1031. {
  1032. return $this->getMockBuilder('TestApp\Model\Table\PaginatorPostsTable')
  1033. ->setMethods($methods)
  1034. ->setConstructorArgs([[
  1035. 'connection' => ConnectionManager::get('test'),
  1036. 'alias' => 'PaginatorPosts',
  1037. 'schema' => [
  1038. 'id' => ['type' => 'integer'],
  1039. 'author_id' => ['type' => 'integer', 'null' => false],
  1040. 'title' => ['type' => 'string', 'null' => false],
  1041. 'body' => 'text',
  1042. 'published' => ['type' => 'string', 'length' => 1, 'default' => 'N'],
  1043. '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]]
  1044. ]
  1045. ]])
  1046. ->getMock();
  1047. }
  1048. /**
  1049. * Helper method for mocking queries.
  1050. *
  1051. * @return \Cake\ORM\Query
  1052. */
  1053. protected function _getMockFindQuery($table = null)
  1054. {
  1055. $query = $this->getMockBuilder('Cake\ORM\Query')
  1056. ->setMethods(['total', 'all', 'count', 'applyOptions'])
  1057. ->disableOriginalConstructor()
  1058. ->getMock();
  1059. $results = $this->getMockBuilder('Cake\ORM\ResultSet')
  1060. ->disableOriginalConstructor()
  1061. ->getMock();
  1062. $query->expects($this->any())
  1063. ->method('count')
  1064. ->will($this->returnValue(2));
  1065. $query->expects($this->any())
  1066. ->method('all')
  1067. ->will($this->returnValue($results));
  1068. $query->expects($this->any())
  1069. ->method('count')
  1070. ->will($this->returnValue(2));
  1071. $query->repository($table);
  1072. return $query;
  1073. }
  1074. }