ResourceTest.php 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451
  1. <?php
  2. namespace Illuminate\Tests\Integration\Http;
  3. use Illuminate\Foundation\Http\Middleware\ValidatePostSize;
  4. use Illuminate\Http\Exceptions\PostTooLargeException;
  5. use Illuminate\Http\Request;
  6. use Illuminate\Http\Resources\ConditionallyLoadsAttributes;
  7. use Illuminate\Http\Resources\Json\JsonResource;
  8. use Illuminate\Http\Resources\MergeValue;
  9. use Illuminate\Http\Resources\MissingValue;
  10. use Illuminate\Pagination\Cursor;
  11. use Illuminate\Pagination\CursorPaginator;
  12. use Illuminate\Pagination\LengthAwarePaginator;
  13. use Illuminate\Support\Collection;
  14. use Illuminate\Support\Facades\Route;
  15. use Illuminate\Tests\Integration\Http\Fixtures\Author;
  16. use Illuminate\Tests\Integration\Http\Fixtures\AuthorResourceWithOptionalRelationship;
  17. use Illuminate\Tests\Integration\Http\Fixtures\EmptyPostCollectionResource;
  18. use Illuminate\Tests\Integration\Http\Fixtures\ObjectResource;
  19. use Illuminate\Tests\Integration\Http\Fixtures\Post;
  20. use Illuminate\Tests\Integration\Http\Fixtures\PostCollectionResource;
  21. use Illuminate\Tests\Integration\Http\Fixtures\PostCollectionResourceWithPaginationInformation;
  22. use Illuminate\Tests\Integration\Http\Fixtures\PostResource;
  23. use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithAnonymousResourceCollectionWithPaginationInformation;
  24. use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithExtraData;
  25. use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithJsonOptions;
  26. use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithJsonOptionsAndTypeHints;
  27. use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalAppendedAttributes;
  28. use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalData;
  29. use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalMerging;
  30. use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalPivotRelationship;
  31. use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithOptionalRelationship;
  32. use Illuminate\Tests\Integration\Http\Fixtures\PostResourceWithoutWrap;
  33. use Illuminate\Tests\Integration\Http\Fixtures\ReallyEmptyPostResource;
  34. use Illuminate\Tests\Integration\Http\Fixtures\ResourceWithPreservedKeys;
  35. use Illuminate\Tests\Integration\Http\Fixtures\SerializablePostResource;
  36. use Illuminate\Tests\Integration\Http\Fixtures\Subscription;
  37. use Mockery;
  38. use Orchestra\Testbench\TestCase;
  39. class ResourceTest extends TestCase
  40. {
  41. public function testResourcesMayBeConvertedToJson()
  42. {
  43. Route::get('/', function () {
  44. return new PostResource(new Post([
  45. 'id' => 5,
  46. 'title' => 'Test Title',
  47. 'abstract' => 'Test abstract',
  48. ]));
  49. });
  50. $response = $this->withoutExceptionHandling()->get(
  51. '/', ['Accept' => 'application/json']
  52. );
  53. $response->assertStatus(200);
  54. $response->assertJson([
  55. 'data' => [
  56. 'id' => 5,
  57. 'title' => 'Test Title',
  58. ],
  59. ]);
  60. }
  61. public function testResourcesMayBeConvertedToJsonWithToJsonMethod()
  62. {
  63. $resource = new PostResource(new Post([
  64. 'id' => 5,
  65. 'title' => 'Test Title',
  66. 'abstract' => 'Test abstract',
  67. ]));
  68. $this->assertSame('{"id":5,"title":"Test Title","custom":true}', $resource->toJson());
  69. }
  70. public function testAnObjectsMayBeConvertedToJson()
  71. {
  72. Route::get('/', function () {
  73. return ObjectResource::make(
  74. (object) ['first_name' => 'Bob', 'age' => 40]
  75. );
  76. });
  77. $this->withoutExceptionHandling()
  78. ->get('/', ['Accept' => 'application/json'])
  79. ->assertStatus(200)
  80. ->assertExactJson([
  81. 'data' => [
  82. 'name' => 'Bob',
  83. 'age' => 40,
  84. ],
  85. ]);
  86. }
  87. public function testArraysWithObjectsMayBeConvertedToJson()
  88. {
  89. Route::get('/', function () {
  90. $objects = [
  91. (object) ['first_name' => 'Bob', 'age' => 40],
  92. (object) ['first_name' => 'Jack', 'age' => 25],
  93. ];
  94. return ObjectResource::collection($objects);
  95. });
  96. $this->withoutExceptionHandling()
  97. ->get('/', ['Accept' => 'application/json'])
  98. ->assertStatus(200)
  99. ->assertExactJson([
  100. 'data' => [
  101. ['name' => 'Bob', 'age' => 40],
  102. ['name' => 'Jack', 'age' => 25],
  103. ],
  104. ]);
  105. }
  106. public function testResourcesMayHaveNoWrap()
  107. {
  108. Route::get('/', function () {
  109. return new PostResourceWithoutWrap(new Post([
  110. 'id' => 5,
  111. 'title' => 'Test Title',
  112. ]));
  113. });
  114. $response = $this->withoutExceptionHandling()->get(
  115. '/', ['Accept' => 'application/json']
  116. );
  117. $response->assertJson([
  118. 'id' => 5,
  119. 'title' => 'Test Title',
  120. ]);
  121. }
  122. public function testResourcesMayHaveOptionalValues()
  123. {
  124. Route::get('/', function () {
  125. return new PostResourceWithOptionalData(new Post([
  126. 'id' => 5,
  127. ]));
  128. });
  129. $response = $this->withoutExceptionHandling()->get(
  130. '/', ['Accept' => 'application/json']
  131. );
  132. $response->assertStatus(200);
  133. $response->assertJson([
  134. 'data' => [
  135. 'id' => 5,
  136. 'second' => 'value',
  137. 'third' => 'value',
  138. 'fourth' => 'default',
  139. 'fifth' => 'default',
  140. ],
  141. ]);
  142. }
  143. public function testResourcesMayHaveOptionalAppendedAttributes()
  144. {
  145. Route::get('/', function () {
  146. $post = new Post([
  147. 'id' => 5,
  148. ]);
  149. $post->append('is_published');
  150. return new PostResourceWithOptionalAppendedAttributes($post);
  151. });
  152. $response = $this->withoutExceptionHandling()->get(
  153. '/', ['Accept' => 'application/json']
  154. );
  155. $response->assertStatus(200);
  156. $response->assertJson([
  157. 'data' => [
  158. 'id' => 5,
  159. 'first' => true,
  160. 'second' => 'override value',
  161. 'third' => 'override value',
  162. 'fourth' => true,
  163. 'fifth' => true,
  164. ],
  165. ]);
  166. }
  167. public function testResourcesWithOptionalAppendedAttributesReturnDefaultValuesAndNotMissingValues()
  168. {
  169. Route::get('/', function () {
  170. return new PostResourceWithOptionalAppendedAttributes(new Post([
  171. 'id' => 5,
  172. ]));
  173. });
  174. $response = $this->withoutExceptionHandling()->get(
  175. '/', ['Accept' => 'application/json']
  176. );
  177. $response->assertStatus(200);
  178. $response->assertExactJson([
  179. 'data' => [
  180. 'id' => 5,
  181. 'fourth' => 'default',
  182. 'fifth' => 'default',
  183. ],
  184. ]);
  185. }
  186. public function testResourcesMayHaveOptionalMerges()
  187. {
  188. Route::get('/', function () {
  189. return new PostResourceWithOptionalMerging(new Post([
  190. 'id' => 5,
  191. ]));
  192. });
  193. $response = $this->withoutExceptionHandling()->get(
  194. '/', ['Accept' => 'application/json']
  195. );
  196. $response->assertStatus(200);
  197. $response->assertExactJson([
  198. 'data' => [
  199. 'id' => 5,
  200. 'second' => 'value',
  201. ],
  202. ]);
  203. }
  204. public function testResourcesMayHaveOptionalRelationships()
  205. {
  206. Route::get('/', function () {
  207. return new PostResourceWithOptionalRelationship(new Post([
  208. 'id' => 5,
  209. 'title' => 'Test Title',
  210. ]));
  211. });
  212. $response = $this->withoutExceptionHandling()->get(
  213. '/', ['Accept' => 'application/json']
  214. );
  215. $response->assertStatus(200);
  216. $response->assertExactJson([
  217. 'data' => [
  218. 'id' => 5,
  219. ],
  220. ]);
  221. }
  222. public function testResourcesMayLoadOptionalRelationships()
  223. {
  224. Route::get('/', function () {
  225. $post = new Post([
  226. 'id' => 5,
  227. 'title' => 'Test Title',
  228. ]);
  229. $post->setRelation('author', new Author(['name' => 'jrrmartin']));
  230. return new PostResourceWithOptionalRelationship($post);
  231. });
  232. $response = $this->withoutExceptionHandling()->get(
  233. '/', ['Accept' => 'application/json']
  234. );
  235. $response->assertStatus(200);
  236. $response->assertExactJson([
  237. 'data' => [
  238. 'id' => 5,
  239. 'author' => ['name' => 'jrrmartin'],
  240. 'author_name' => 'jrrmartin',
  241. ],
  242. ]);
  243. }
  244. public function testResourcesMayShowsNullForLoadedRelationshipWithValueNull()
  245. {
  246. Route::get('/', function () {
  247. $post = new Post([
  248. 'id' => 5,
  249. 'title' => 'Test Title',
  250. ]);
  251. $post->setRelation('author', null);
  252. return new PostResourceWithOptionalRelationship($post);
  253. });
  254. $response = $this->withoutExceptionHandling()->get(
  255. '/', ['Accept' => 'application/json']
  256. );
  257. $response->assertStatus(200);
  258. $response->assertExactJson([
  259. 'data' => [
  260. 'id' => 5,
  261. 'author' => null,
  262. 'author_name' => null,
  263. ],
  264. ]);
  265. }
  266. public function testResourcesMayHaveOptionalRelationshipsWithDefaultValues()
  267. {
  268. Route::get('/', function () {
  269. return new AuthorResourceWithOptionalRelationship(new Author([
  270. 'name' => 'jrrmartin',
  271. ]));
  272. });
  273. $response = $this->withoutExceptionHandling()->get(
  274. '/', ['Accept' => 'application/json']
  275. );
  276. $response->assertStatus(200);
  277. $response->assertExactJson([
  278. 'data' => [
  279. 'name' => 'jrrmartin',
  280. 'posts_count' => 'not loaded',
  281. 'latest_post_title' => 'not loaded',
  282. ],
  283. ]);
  284. }
  285. public function testResourcesMayHaveOptionalPivotRelationships()
  286. {
  287. Route::get('/', function () {
  288. $post = new Post(['id' => 5]);
  289. $post->setRelation('pivot', new Subscription);
  290. return new PostResourceWithOptionalPivotRelationship($post);
  291. });
  292. $response = $this->withoutExceptionHandling()->get(
  293. '/', ['Accept' => 'application/json']
  294. );
  295. $response->assertStatus(200);
  296. $response->assertExactJson([
  297. 'data' => [
  298. 'id' => 5,
  299. 'subscription' => [
  300. 'foo' => 'bar',
  301. ],
  302. ],
  303. ]);
  304. }
  305. public function testResourcesMayHaveOptionalPivotRelationshipsWithCustomAccessor()
  306. {
  307. Route::get('/', function () {
  308. $post = new Post(['id' => 5]);
  309. $post->setRelation('accessor', new Subscription);
  310. return new PostResourceWithOptionalPivotRelationship($post);
  311. });
  312. $response = $this->withoutExceptionHandling()->get(
  313. '/', ['Accept' => 'application/json']
  314. );
  315. $response->assertStatus(200);
  316. $response->assertExactJson([
  317. 'data' => [
  318. 'id' => 5,
  319. 'custom_subscription' => [
  320. 'foo' => 'bar',
  321. ],
  322. ],
  323. ]);
  324. }
  325. public function testResourceIsUrlRoutable()
  326. {
  327. $post = new PostResource(new Post([
  328. 'id' => 5,
  329. 'title' => 'Test Title',
  330. ]));
  331. $this->assertSame('http://localhost/post/5', url('/post', $post));
  332. }
  333. public function testNamedRoutesAreUrlRoutable()
  334. {
  335. $post = new PostResource(new Post([
  336. 'id' => 5,
  337. 'title' => 'Test Title',
  338. ]));
  339. Route::get('/post/{id}', function () use ($post) {
  340. return route('post.show', $post);
  341. })->name('post.show');
  342. $response = $this->withoutExceptionHandling()->get('/post/1');
  343. $this->assertSame('http://localhost/post/5', $response->original);
  344. }
  345. public function testResourcesMayBeSerializable()
  346. {
  347. Route::get('/', function () {
  348. return new SerializablePostResource(new Post([
  349. 'id' => 5,
  350. 'title' => 'Test Title',
  351. ]));
  352. });
  353. $response = $this->withoutExceptionHandling()->get(
  354. '/', ['Accept' => 'application/json']
  355. );
  356. $response->assertStatus(200);
  357. $response->assertJson([
  358. 'data' => [
  359. 'id' => 5,
  360. ],
  361. ]);
  362. }
  363. public function testResourcesMayCustomizeResponses()
  364. {
  365. Route::get('/', function () {
  366. return new PostResource(new Post([
  367. 'id' => 5,
  368. 'title' => 'Test Title',
  369. ]));
  370. });
  371. $response = $this->withoutExceptionHandling()->get(
  372. '/', ['Accept' => 'application/json']
  373. );
  374. $response->assertStatus(200);
  375. $response->assertHeader('X-Resource', 'True');
  376. }
  377. public function testResourcesMayCustomizeExtraData()
  378. {
  379. Route::get('/', function () {
  380. return new PostResourceWithExtraData(new Post([
  381. 'id' => 5,
  382. 'title' => 'Test Title',
  383. ]));
  384. });
  385. $response = $this->withoutExceptionHandling()->get(
  386. '/', ['Accept' => 'application/json']
  387. );
  388. $response->assertJson([
  389. 'data' => [
  390. 'id' => 5,
  391. 'title' => 'Test Title',
  392. ],
  393. 'foo' => 'bar',
  394. ]);
  395. }
  396. public function testResourcesMayCustomizeExtraDataWhenBuildingResponse()
  397. {
  398. Route::get('/', function () {
  399. return (new PostResourceWithExtraData(new Post([
  400. 'id' => 5,
  401. 'title' => 'Test Title',
  402. ])))->additional(['baz' => 'qux']);
  403. });
  404. $response = $this->withoutExceptionHandling()->get(
  405. '/', ['Accept' => 'application/json']
  406. );
  407. $response->assertJson([
  408. 'data' => [
  409. 'id' => 5,
  410. 'title' => 'Test Title',
  411. ],
  412. 'foo' => 'bar',
  413. 'baz' => 'qux',
  414. ]);
  415. }
  416. public function testResourcesMayCustomizeJsonOptions()
  417. {
  418. Route::get('/', function () {
  419. return new PostResourceWithJsonOptions(new Post([
  420. 'id' => 5,
  421. 'title' => 'Test Title',
  422. 'reading_time' => 3.0,
  423. ]));
  424. });
  425. $response = $this->withoutExceptionHandling()->get(
  426. '/', ['Accept' => 'application/json']
  427. );
  428. $this->assertEquals(
  429. '{"data":{"id":5,"title":"Test Title","reading_time":3.0}}',
  430. $response->baseResponse->content()
  431. );
  432. }
  433. public function testCollectionResourcesMayCustomizeJsonOptions()
  434. {
  435. Route::get('/', function () {
  436. return PostResourceWithJsonOptions::collection(collect([
  437. new Post(['id' => 5, 'title' => 'Test Title', 'reading_time' => 3.0]),
  438. ]));
  439. });
  440. $response = $this->withoutExceptionHandling()->get(
  441. '/', ['Accept' => 'application/json']
  442. );
  443. $this->assertEquals(
  444. '{"data":[{"id":5,"title":"Test Title","reading_time":3.0}]}',
  445. $response->baseResponse->content()
  446. );
  447. }
  448. public function testResourcesMayCustomizeJsonOptionsOnPaginatedResponse()
  449. {
  450. Route::get('/', function () {
  451. $paginator = new LengthAwarePaginator(
  452. collect([new Post(['id' => 5, 'title' => 'Test Title', 'reading_time' => 3.0])]),
  453. 10, 15, 1
  454. );
  455. return PostResourceWithJsonOptions::collection($paginator);
  456. });
  457. $response = $this->withoutExceptionHandling()->get(
  458. '/', ['Accept' => 'application/json']
  459. );
  460. $this->assertEquals(
  461. '{"data":[{"id":5,"title":"Test Title","reading_time":3.0}],"links":{"first":"\/?page=1","last":"\/?page=1","prev":null,"next":null},"meta":{"current_page":1,"from":1,"last_page":1,"links":[{"url":null,"label":"&laquo; Previous","active":false},{"url":"\/?page=1","label":"1","active":true},{"url":null,"label":"Next &raquo;","active":false}],"path":"\/","per_page":15,"to":1,"total":10}}',
  462. $response->baseResponse->content()
  463. );
  464. }
  465. public function testResourcesMayCustomizeJsonOptionsWithTypeHintedConstructor()
  466. {
  467. Route::get('/', function () {
  468. return new PostResourceWithJsonOptionsAndTypeHints(new Post([
  469. 'id' => 5,
  470. 'title' => 'Test Title',
  471. 'reading_time' => 3.0,
  472. ]));
  473. });
  474. $response = $this->withoutExceptionHandling()->get(
  475. '/', ['Accept' => 'application/json']
  476. );
  477. $this->assertEquals(
  478. '{"data":{"id":5,"title":"Test Title","reading_time":3.0}}',
  479. $response->baseResponse->content()
  480. );
  481. }
  482. public function testCustomHeadersMayBeSetOnResponses()
  483. {
  484. Route::get('/', function () {
  485. return (new PostResource(new Post([
  486. 'id' => 5,
  487. 'title' => 'Test Title',
  488. ])))->response()->setStatusCode(202)->header('X-Custom', 'True');
  489. });
  490. $response = $this->withoutExceptionHandling()->get(
  491. '/', ['Accept' => 'application/json']
  492. );
  493. $response->assertStatus(202);
  494. $response->assertHeader('X-Custom', 'True');
  495. }
  496. public function testResourcesMayReceiveProperStatusCodeForFreshModels()
  497. {
  498. Route::get('/', function () {
  499. $post = new Post([
  500. 'id' => 5,
  501. 'title' => 'Test Title',
  502. ]);
  503. $post->wasRecentlyCreated = true;
  504. return new PostResource($post);
  505. });
  506. $response = $this->withoutExceptionHandling()->get(
  507. '/', ['Accept' => 'application/json']
  508. );
  509. $response->assertStatus(201);
  510. }
  511. public function testCollectionsAreNotDoubledWrapped()
  512. {
  513. Route::get('/', function () {
  514. return new PostCollectionResource(collect([new Post([
  515. 'id' => 5,
  516. 'title' => 'Test Title',
  517. ])]));
  518. });
  519. $response = $this->withoutExceptionHandling()->get(
  520. '/', ['Accept' => 'application/json']
  521. );
  522. $response->assertStatus(200);
  523. $response->assertJson([
  524. 'data' => [
  525. [
  526. 'id' => 5,
  527. 'title' => 'Test Title',
  528. ],
  529. ],
  530. ]);
  531. }
  532. public function testPaginatorsReceiveLinks()
  533. {
  534. Route::get('/', function () {
  535. $paginator = new LengthAwarePaginator(
  536. collect([new Post(['id' => 5, 'title' => 'Test Title'])]),
  537. 10, 15, 1
  538. );
  539. return new PostCollectionResource($paginator);
  540. });
  541. $response = $this->withoutExceptionHandling()->get(
  542. '/', ['Accept' => 'application/json']
  543. );
  544. $response->assertStatus(200);
  545. $response->assertJson([
  546. 'data' => [
  547. [
  548. 'id' => 5,
  549. 'title' => 'Test Title',
  550. ],
  551. ],
  552. 'links' => [
  553. 'first' => '/?page=1',
  554. 'last' => '/?page=1',
  555. 'prev' => null,
  556. 'next' => null,
  557. ],
  558. 'meta' => [
  559. 'current_page' => 1,
  560. 'from' => 1,
  561. 'last_page' => 1,
  562. 'path' => '/',
  563. 'per_page' => 15,
  564. 'to' => 1,
  565. 'total' => 10,
  566. ],
  567. ]);
  568. }
  569. public function testPaginatorResourceCanPreserveQueryParameters()
  570. {
  571. Route::get('/', function () {
  572. $collection = collect([new Post(['id' => 2, 'title' => 'Laravel Nova'])]);
  573. $paginator = new LengthAwarePaginator(
  574. $collection, 3, 1, 2
  575. );
  576. return PostCollectionResource::make($paginator)->preserveQuery();
  577. });
  578. $response = $this->withoutExceptionHandling()->get(
  579. '/?framework=laravel&author=Otwell&page=2', ['Accept' => 'application/json']
  580. );
  581. $response->assertStatus(200);
  582. $response->assertJson([
  583. 'data' => [
  584. [
  585. 'id' => 2,
  586. 'title' => 'Laravel Nova',
  587. ],
  588. ],
  589. 'links' => [
  590. 'first' => '/?framework=laravel&author=Otwell&page=1',
  591. 'last' => '/?framework=laravel&author=Otwell&page=3',
  592. 'prev' => '/?framework=laravel&author=Otwell&page=1',
  593. 'next' => '/?framework=laravel&author=Otwell&page=3',
  594. ],
  595. 'meta' => [
  596. 'current_page' => 2,
  597. 'from' => 2,
  598. 'last_page' => 3,
  599. 'path' => '/',
  600. 'per_page' => 1,
  601. 'to' => 2,
  602. 'total' => 3,
  603. ],
  604. ]);
  605. }
  606. public function testPaginatorResourceCanReceiveQueryParameters()
  607. {
  608. Route::get('/', function () {
  609. $collection = collect([new Post(['id' => 2, 'title' => 'Laravel Nova'])]);
  610. $paginator = new LengthAwarePaginator(
  611. $collection, 3, 1, 2
  612. );
  613. return PostCollectionResource::make($paginator)->withQuery(['author' => 'Taylor']);
  614. });
  615. $response = $this->withoutExceptionHandling()->get(
  616. '/?framework=laravel&author=Otwell&page=2', ['Accept' => 'application/json']
  617. );
  618. $response->assertStatus(200);
  619. $response->assertJson([
  620. 'data' => [
  621. [
  622. 'id' => 2,
  623. 'title' => 'Laravel Nova',
  624. ],
  625. ],
  626. 'links' => [
  627. 'first' => '/?author=Taylor&page=1',
  628. 'last' => '/?author=Taylor&page=3',
  629. 'prev' => '/?author=Taylor&page=1',
  630. 'next' => '/?author=Taylor&page=3',
  631. ],
  632. 'meta' => [
  633. 'current_page' => 2,
  634. 'from' => 2,
  635. 'last_page' => 3,
  636. 'path' => '/',
  637. 'per_page' => 1,
  638. 'to' => 2,
  639. 'total' => 3,
  640. ],
  641. ]);
  642. }
  643. public function testCursorPaginatorReceiveLinks()
  644. {
  645. Route::get('/', function () {
  646. $paginator = new CursorPaginator(
  647. collect([new Post(['id' => 5, 'title' => 'Test Title']), new Post(['id' => 6, 'title' => 'Hello'])]),
  648. 1, null, ['parameters' => ['id']]
  649. );
  650. return new PostCollectionResource($paginator);
  651. });
  652. $response = $this->withoutExceptionHandling()->get(
  653. '/', ['Accept' => 'application/json']
  654. );
  655. $response->assertStatus(200);
  656. $response->assertJson([
  657. 'data' => [
  658. [
  659. 'id' => 5,
  660. 'title' => 'Test Title',
  661. ],
  662. ],
  663. 'links' => [
  664. 'first' => null,
  665. 'last' => null,
  666. 'prev' => null,
  667. 'next' => '/?cursor='.(new Cursor(['id' => 5]))->encode(),
  668. ],
  669. 'meta' => [
  670. 'path' => '/',
  671. 'per_page' => 1,
  672. ],
  673. ]);
  674. }
  675. public function testCursorPaginatorResourceCanPreserveQueryParameters()
  676. {
  677. Route::get('/', function () {
  678. $collection = collect([new Post(['id' => 5, 'title' => 'Test Title']), new Post(['id' => 6, 'title' => 'Hello'])]);
  679. $paginator = new CursorPaginator(
  680. $collection, 1, null, ['parameters' => ['id']]
  681. );
  682. return PostCollectionResource::make($paginator)->preserveQuery();
  683. });
  684. $response = $this->withoutExceptionHandling()->get(
  685. '/?framework=laravel&author=Otwell', ['Accept' => 'application/json']
  686. );
  687. $response->assertStatus(200);
  688. $response->assertJson([
  689. 'data' => [
  690. [
  691. 'id' => 5,
  692. 'title' => 'Test Title',
  693. ],
  694. ],
  695. 'links' => [
  696. 'first' => null,
  697. 'last' => null,
  698. 'prev' => null,
  699. 'next' => '/?framework=laravel&author=Otwell&cursor='.(new Cursor(['id' => 5]))->encode(),
  700. ],
  701. 'meta' => [
  702. 'path' => '/',
  703. 'per_page' => 1,
  704. ],
  705. ]);
  706. }
  707. public function testCursorPaginatorResourceCanReceiveQueryParameters()
  708. {
  709. Route::get('/', function () {
  710. $collection = collect([new Post(['id' => 5, 'title' => 'Test Title']), new Post(['id' => 6, 'title' => 'Hello'])]);
  711. $paginator = new CursorPaginator(
  712. $collection, 1, null, ['parameters' => ['id']]
  713. );
  714. return PostCollectionResource::make($paginator)->withQuery(['author' => 'Taylor']);
  715. });
  716. $response = $this->withoutExceptionHandling()->get(
  717. '/?framework=laravel&author=Otwell', ['Accept' => 'application/json']
  718. );
  719. $response->assertStatus(200);
  720. $response->assertJson([
  721. 'data' => [
  722. [
  723. 'id' => 5,
  724. 'title' => 'Test Title',
  725. ],
  726. ],
  727. 'links' => [
  728. 'first' => null,
  729. 'last' => null,
  730. 'prev' => null,
  731. 'next' => '/?author=Taylor&cursor='.(new Cursor(['id' => 5]))->encode(),
  732. ],
  733. 'meta' => [
  734. 'path' => '/',
  735. 'per_page' => 1,
  736. ],
  737. ]);
  738. }
  739. public function testToJsonMayBeLeftOffOfCollection()
  740. {
  741. Route::get('/', function () {
  742. return new EmptyPostCollectionResource(new LengthAwarePaginator(
  743. collect([new Post(['id' => 5, 'title' => 'Test Title'])]),
  744. 10, 15, 1
  745. ));
  746. });
  747. $response = $this->withoutExceptionHandling()->get(
  748. '/', ['Accept' => 'application/json']
  749. );
  750. $response->assertStatus(200);
  751. $response->assertJson([
  752. 'data' => [
  753. [
  754. 'id' => 5,
  755. 'title' => 'Test Title',
  756. 'custom' => true,
  757. ],
  758. ],
  759. 'links' => [
  760. 'first' => '/?page=1',
  761. 'last' => '/?page=1',
  762. 'prev' => null,
  763. 'next' => null,
  764. ],
  765. 'meta' => [
  766. 'current_page' => 1,
  767. 'from' => 1,
  768. 'last_page' => 1,
  769. 'path' => '/',
  770. 'per_page' => 15,
  771. 'to' => 1,
  772. 'total' => 10,
  773. ],
  774. ]);
  775. }
  776. public function testToJsonMayBeLeftOffOfSingleResource()
  777. {
  778. Route::get('/', function () {
  779. return new ReallyEmptyPostResource(new Post([
  780. 'id' => 5,
  781. 'title' => 'Test Title',
  782. ]));
  783. });
  784. $response = $this->withoutExceptionHandling()->get(
  785. '/', ['Accept' => 'application/json']
  786. );
  787. $response->assertStatus(200);
  788. $response->assertJson([
  789. 'data' => [
  790. 'id' => 5,
  791. 'title' => 'Test Title',
  792. ],
  793. ]);
  794. }
  795. public function testOriginalOnResponseIsModelWhenSingleResource()
  796. {
  797. $createdPost = new Post(['id' => 5, 'title' => 'Test Title']);
  798. Route::get('/', function () use ($createdPost) {
  799. return new ReallyEmptyPostResource($createdPost);
  800. });
  801. $response = $this->withoutExceptionHandling()->get(
  802. '/', ['Accept' => 'application/json']
  803. );
  804. $this->assertTrue($createdPost->is($response->getOriginalContent()));
  805. }
  806. public function testOriginalOnResponseIsCollectionOfModelWhenCollectionResource()
  807. {
  808. $createdPosts = collect([
  809. new Post(['id' => 5, 'title' => 'Test Title']),
  810. new Post(['id' => 6, 'title' => 'Test Title 2']),
  811. ]);
  812. Route::get('/', function () use ($createdPosts) {
  813. return new EmptyPostCollectionResource(new LengthAwarePaginator($createdPosts, 10, 15, 1));
  814. });
  815. $response = $this->withoutExceptionHandling()->get(
  816. '/', ['Accept' => 'application/json']
  817. );
  818. $createdPosts->each(function ($post) use ($response) {
  819. $this->assertTrue($response->getOriginalContent()->contains($post));
  820. });
  821. }
  822. public function testCollectionResourceWithPaginationInfomation()
  823. {
  824. $posts = collect([
  825. new Post(['id' => 5, 'title' => 'Test Title']),
  826. ]);
  827. Route::get('/', function () use ($posts) {
  828. return new PostCollectionResourceWithPaginationInformation(new LengthAwarePaginator($posts, 10, 1, 1));
  829. });
  830. $response = $this->withoutExceptionHandling()->get(
  831. '/',
  832. ['Accept' => 'application/json']
  833. );
  834. $response->assertStatus(200);
  835. $response->assertJson([
  836. 'data' => [
  837. [
  838. 'id' => 5,
  839. 'title' => 'Test Title',
  840. ],
  841. ],
  842. 'current_page' => 1,
  843. 'per_page' => 1,
  844. 'total_page' => 10,
  845. 'total' => 10,
  846. ]);
  847. }
  848. public function testResourceWithPaginationInfomation()
  849. {
  850. $posts = collect([
  851. new Post(['id' => 5, 'title' => 'Test Title']),
  852. ]);
  853. Route::get('/', function () use ($posts) {
  854. return PostResourceWithAnonymousResourceCollectionWithPaginationInformation::collection(new LengthAwarePaginator($posts, 10, 1, 1));
  855. });
  856. $response = $this->withoutExceptionHandling()->get(
  857. '/',
  858. ['Accept' => 'application/json']
  859. );
  860. $response->assertStatus(200);
  861. $response->assertJson([
  862. 'data' => [
  863. [
  864. 'id' => 5,
  865. 'title' => 'Test Title',
  866. ],
  867. ],
  868. 'current_page' => 1,
  869. 'per_page' => 1,
  870. 'total_page' => 10,
  871. 'total' => 10,
  872. ]);
  873. }
  874. public function testCollectionResourcesAreCountable()
  875. {
  876. $posts = collect([
  877. new Post(['id' => 1, 'title' => 'Test title']),
  878. new Post(['id' => 2, 'title' => 'Test title 2']),
  879. ]);
  880. $collection = new PostCollectionResource($posts);
  881. $this->assertCount(2, $collection);
  882. $this->assertCount(2, $collection);
  883. }
  884. public function testKeysArePreservedIfTheResourceIsFlaggedToPreserveKeys()
  885. {
  886. $data = [
  887. 'authorBook' => [
  888. 'byId' => [
  889. 1 => [
  890. 'id' => 1,
  891. 'authorId' => 5,
  892. 'bookId' => 22,
  893. ],
  894. 2 => [
  895. 'id' => 2,
  896. 'authorId' => 5,
  897. 'bookId' => 15,
  898. ],
  899. 3 => [
  900. 'id' => 3,
  901. 'authorId' => 42,
  902. 'bookId' => 12,
  903. ],
  904. ],
  905. 'allIds' => [1, 2, 3],
  906. ],
  907. ];
  908. Route::get('/', function () use ($data) {
  909. return new ResourceWithPreservedKeys($data);
  910. });
  911. $response = $this->withoutExceptionHandling()->get(
  912. '/', ['Accept' => 'application/json']
  913. );
  914. $response->assertStatus(200);
  915. $response->assertJson(['data' => $data]);
  916. }
  917. public function testKeysArePreservedInAnAnonymousColletionIfTheResourceIsFlaggedToPreserveKeys()
  918. {
  919. $data = Collection::make([
  920. [
  921. 'id' => 1,
  922. 'authorId' => 5,
  923. 'bookId' => 22,
  924. ],
  925. [
  926. 'id' => 2,
  927. 'authorId' => 5,
  928. 'bookId' => 15,
  929. ],
  930. [
  931. 'id' => 3,
  932. 'authorId' => 42,
  933. 'bookId' => 12,
  934. ],
  935. ])->keyBy->id;
  936. Route::get('/', function () use ($data) {
  937. return ResourceWithPreservedKeys::collection($data);
  938. });
  939. $response = $this->withoutExceptionHandling()->get(
  940. '/', ['Accept' => 'application/json']
  941. );
  942. $response->assertStatus(200);
  943. $response->assertJson(['data' => $data->toArray()]);
  944. }
  945. public function testLeadingMergeKeyedValueIsMergedCorrectly()
  946. {
  947. $filter = new class
  948. {
  949. use ConditionallyLoadsAttributes;
  950. public function work()
  951. {
  952. return $this->filter([
  953. new MergeValue(['name' => 'mohamed', 'location' => 'hurghada']),
  954. ]);
  955. }
  956. };
  957. $results = $filter->work();
  958. $this->assertEquals([
  959. 'name' => 'mohamed', 'location' => 'hurghada',
  960. ], $results);
  961. }
  962. public function testPostTooLargeException()
  963. {
  964. $this->expectException(PostTooLargeException::class);
  965. $request = Mockery::mock(Request::class, ['server' => ['CONTENT_LENGTH' => '2147483640']]);
  966. $post = new ValidatePostSize;
  967. $post->handle($request, function () {
  968. });
  969. }
  970. public function testLeadingMergeKeyedValueIsMergedCorrectlyWhenFirstValueIsMissing()
  971. {
  972. $filter = new class
  973. {
  974. use ConditionallyLoadsAttributes;
  975. public function work()
  976. {
  977. return $this->filter([
  978. new MergeValue([
  979. 0 => new MissingValue,
  980. 'name' => 'mohamed',
  981. 'location' => 'hurghada',
  982. ]),
  983. ]);
  984. }
  985. };
  986. $results = $filter->work();
  987. $this->assertEquals([
  988. 'name' => 'mohamed', 'location' => 'hurghada',
  989. ], $results);
  990. }
  991. public function testLeadingMergeValueIsMergedCorrectly()
  992. {
  993. $filter = new class
  994. {
  995. use ConditionallyLoadsAttributes;
  996. public function work()
  997. {
  998. return $this->filter([
  999. new MergeValue(['First', 'Second']),
  1000. 'Taylor',
  1001. 'Mohamed',
  1002. new MergeValue(['Adam', 'Matt']),
  1003. 'Jeffrey',
  1004. new MergeValue(['Abigail', 'Lydia']),
  1005. ]);
  1006. }
  1007. };
  1008. $results = $filter->work();
  1009. $this->assertEquals([
  1010. 'First', 'Second', 'Taylor', 'Mohamed', 'Adam', 'Matt', 'Jeffrey', 'Abigail', 'Lydia',
  1011. ], $results);
  1012. }
  1013. public function testMergeValuesMayBeMissing()
  1014. {
  1015. $filter = new class
  1016. {
  1017. use ConditionallyLoadsAttributes;
  1018. public function work()
  1019. {
  1020. return $this->filter([
  1021. new MergeValue(['First', 'Second']),
  1022. 'Taylor',
  1023. 'Mohamed',
  1024. $this->mergeWhen(false, ['Adam', 'Matt']),
  1025. 'Jeffrey',
  1026. new MergeValue(['Abigail', 'Lydia']),
  1027. ]);
  1028. }
  1029. };
  1030. $results = $filter->work();
  1031. $this->assertEquals([
  1032. 'First', 'Second', 'Taylor', 'Mohamed', 'Jeffrey', 'Abigail', 'Lydia',
  1033. ], $results);
  1034. }
  1035. public function testInitialMergeValuesMayBeMissing()
  1036. {
  1037. $filter = new class
  1038. {
  1039. use ConditionallyLoadsAttributes;
  1040. public function work()
  1041. {
  1042. return $this->filter([
  1043. $this->mergeWhen(false, ['First', 'Second']),
  1044. 'Taylor',
  1045. 'Mohamed',
  1046. $this->mergeWhen(true, ['Adam', 'Matt']),
  1047. 'Jeffrey',
  1048. new MergeValue(['Abigail', 'Lydia']),
  1049. ]);
  1050. }
  1051. };
  1052. $results = $filter->work();
  1053. $this->assertEquals([
  1054. 'Taylor', 'Mohamed', 'Adam', 'Matt', 'Jeffrey', 'Abigail', 'Lydia',
  1055. ], $results);
  1056. }
  1057. public function testMergeValueCanMergeJsonSerializable()
  1058. {
  1059. $filter = new class
  1060. {
  1061. use ConditionallyLoadsAttributes;
  1062. public function work()
  1063. {
  1064. $postResource = new PostResource(new Post([
  1065. 'id' => 1,
  1066. 'title' => 'Test Title 1',
  1067. ]));
  1068. return $this->filter([
  1069. new MergeValue($postResource),
  1070. 'user' => 'test user',
  1071. 'age' => 'test age',
  1072. ]);
  1073. }
  1074. };
  1075. $results = $filter->work();
  1076. $this->assertEquals([
  1077. 'id' => 1,
  1078. 'title' => 'Test Title 1',
  1079. 'custom' => true,
  1080. 'user' => 'test user',
  1081. 'age' => 'test age',
  1082. ], $results);
  1083. }
  1084. public function testMergeValueCanMergeCollectionOfJsonSerializable()
  1085. {
  1086. $filter = new class
  1087. {
  1088. use ConditionallyLoadsAttributes;
  1089. public function work()
  1090. {
  1091. $posts = collect([
  1092. new Post(['id' => 1, 'title' => 'Test title 1']),
  1093. new Post(['id' => 2, 'title' => 'Test title 2']),
  1094. ]);
  1095. return $this->filter([
  1096. new MergeValue(PostResource::collection($posts)),
  1097. ]);
  1098. }
  1099. };
  1100. $results = $filter->work();
  1101. $this->assertEquals([
  1102. ['id' => 1, 'title' => 'Test title 1', 'custom' => true],
  1103. ['id' => 2, 'title' => 'Test title 2', 'custom' => true],
  1104. ], $results);
  1105. }
  1106. public function testAllMergeValuesMayBeMissing()
  1107. {
  1108. $filter = new class
  1109. {
  1110. use ConditionallyLoadsAttributes;
  1111. public function work()
  1112. {
  1113. return $this->filter([
  1114. $this->mergeWhen(false, ['First', 'Second']),
  1115. 'Taylor',
  1116. 'Mohamed',
  1117. $this->mergeWhen(false, ['Adam', 'Matt']),
  1118. 'Jeffrey',
  1119. $this->mergeWhen(false, ['Abigail', 'Lydia']),
  1120. ]);
  1121. }
  1122. };
  1123. $results = $filter->work();
  1124. $this->assertEquals([
  1125. 'Taylor', 'Mohamed', 'Jeffrey',
  1126. ], $results);
  1127. }
  1128. public function testNestedMerges()
  1129. {
  1130. $filter = new class
  1131. {
  1132. use ConditionallyLoadsAttributes;
  1133. public function work()
  1134. {
  1135. return $this->filter([
  1136. $this->mergeWhen(true, [['Something']]),
  1137. [
  1138. $this->mergeWhen(true, ['First', $this->mergeWhen(true, ['Second'])]),
  1139. 'Third',
  1140. ],
  1141. [
  1142. 'Fourth',
  1143. ],
  1144. ]);
  1145. }
  1146. };
  1147. $results = $filter->work();
  1148. $this->assertEquals([
  1149. [
  1150. 'Something',
  1151. ],
  1152. [
  1153. 'First', 'Second', 'Third',
  1154. ],
  1155. [
  1156. 'Fourth',
  1157. ],
  1158. ], $results);
  1159. }
  1160. public function testTheResourceCanBeAnArray()
  1161. {
  1162. $this->assertJsonResourceResponse([
  1163. 'user@example.com' => 'John',
  1164. 'admin@example.com' => 'Hank',
  1165. ], [
  1166. 'data' => [
  1167. 'user@example.com' => 'John',
  1168. 'admin@example.com' => 'Hank',
  1169. ],
  1170. ]);
  1171. }
  1172. public function testItWillReturnAsAnArrayWhenStringKeysAreStripped()
  1173. {
  1174. $this->assertJsonResourceResponse([
  1175. 1 => 'John',
  1176. 2 => 'Hank',
  1177. 'foo' => new MissingValue,
  1178. ], ['data' => ['John', 'Hank']]);
  1179. $this->assertJsonResourceResponse([
  1180. 1 => 'John',
  1181. 'foo' => new MissingValue,
  1182. 3 => 'Hank',
  1183. ], ['data' => ['John', 'Hank']]);
  1184. $this->assertJsonResourceResponse([
  1185. 'foo' => new MissingValue,
  1186. 2 => 'John',
  1187. 3 => 'Hank',
  1188. ], ['data' => ['John', 'Hank']]);
  1189. }
  1190. public function testItStripsNumericKeys()
  1191. {
  1192. $this->assertJsonResourceResponse([
  1193. 0 => 'John',
  1194. 1 => 'Hank',
  1195. ], ['data' => ['John', 'Hank']]);
  1196. $this->assertJsonResourceResponse([
  1197. 0 => 'John',
  1198. 1 => 'Hank',
  1199. 3 => 'Bill',
  1200. ], ['data' => ['John', 'Hank', 'Bill']]);
  1201. $this->assertJsonResourceResponse([
  1202. 5 => 'John',
  1203. 6 => 'Hank',
  1204. ], ['data' => ['John', 'Hank']]);
  1205. }
  1206. public function testItWontKeysIfAnyOfThemAreStrings()
  1207. {
  1208. $this->assertJsonResourceResponse([
  1209. '5' => 'John',
  1210. '6' => 'Hank',
  1211. 'a' => 'Bill',
  1212. ], ['data' => ['5' => 'John', '6' => 'Hank', 'a' => 'Bill']]);
  1213. $this->assertJsonResourceResponse([
  1214. 0 => 10,
  1215. 1 => 20,
  1216. 'total' => 30,
  1217. ], ['data' => [0 => 10, 1 => 20, 'total' => 30]]);
  1218. }
  1219. private function assertJsonResourceResponse($data, $expectedJson)
  1220. {
  1221. Route::get('/', function () use ($data) {
  1222. return new JsonResource($data);
  1223. });
  1224. $this->withoutExceptionHandling()
  1225. ->get('/', ['Accept' => 'application/json'])
  1226. ->assertStatus(200)
  1227. ->assertExactJson($expectedJson);
  1228. }
  1229. }