PaginatorTestTrait.php 40 KB

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