Passed
Push — master ( 2320b3...46a99a )
by Vladimir
10:20
created

DeferredFieldsTest   A

Complexity

Total Complexity 10

Size/Duplication

Total Lines 635
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 10
eloc 374
c 3
b 0
f 0
dl 0
loc 635
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
B testComplexRecursiveDeferredFields() 0 150 2
B setUp() 0 180 1
A testDeferredChaining() 0 81 1
A findUserById() 0 6 1
B testDeferredFields() 0 92 2
A findStoryById() 0 6 1
A testNestedDeferredFields() 0 77 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Tests\Executor;
6
7
use GraphQL\Deferred;
8
use GraphQL\Executor\Executor;
9
use GraphQL\Language\Parser;
10
use GraphQL\Type\Definition\ObjectType;
11
use GraphQL\Type\Definition\ResolveInfo;
12
use GraphQL\Type\Definition\Type;
13
use GraphQL\Type\Schema;
14
use GraphQL\Utils\Utils;
15
use PHPUnit\Framework\TestCase;
16
use function count;
17
use function in_array;
18
use function json_encode;
19
20
class DeferredFieldsTest extends TestCase
21
{
22
    /** @var ObjectType */
23
    private $userType;
24
25
    /** @var ObjectType */
26
    private $storyType;
27
28
    /** @var ObjectType */
29
    private $categoryType;
30
31
    /** @var mixed */
32
    private $paths;
33
34
    /** @var mixed[][] */
35
    private $storyDataSource;
36
37
    /** @var mixed[][] */
38
    private $userDataSource;
39
40
    /** @var mixed[][] */
41
    private $categoryDataSource;
42
43
    /** @var ObjectType */
44
    private $queryType;
45
46
    public function setUp()
47
    {
48
        $this->storyDataSource = [
49
            ['id' => 1, 'authorId' => 1, 'title' => 'Story #1', 'categoryIds' => [2, 3]],
50
            ['id' => 2, 'authorId' => 2, 'title' => 'Story #2', 'categoryIds' => [1, 2]],
51
            ['id' => 3, 'authorId' => 3, 'title' => 'Story #3', 'categoryIds' => [2]],
52
            ['id' => 4, 'authorId' => 3, 'title' => 'Story #4', 'categoryIds' => [1]],
53
            ['id' => 5, 'authorId' => 1, 'title' => 'Story #5', 'categoryIds' => [3]],
54
            ['id' => 6, 'authorId' => 2, 'title' => 'Story #6', 'categoryIds' => [1]],
55
            ['id' => 7, 'authorId' => 3, 'title' => 'Story #7', 'categoryIds' => [2]],
56
            ['id' => 8, 'authorId' => 1, 'title' => 'Story #8', 'categoryIds' => [1, 2, 3]],
57
            ['id' => 9, 'authorId' => 2, 'title' => 'Story #9', 'categoryIds' => [2, 3]],
58
        ];
59
60
        $this->userDataSource = [
61
            ['id' => 1, 'name' => 'John', 'bestFriendId' => 4],
62
            ['id' => 2, 'name' => 'Jane', 'bestFriendId' => 3],
63
            ['id' => 3, 'name' => 'Joe', 'bestFriendId' => 2],
64
            ['id' => 4, 'name' => 'Dirk', 'bestFriend' => 1],
65
        ];
66
67
        $this->categoryDataSource = [
68
            ['id' => 1, 'name' => 'Category #1', 'topStoryId' => 8],
69
            ['id' => 2, 'name' => 'Category #2', 'topStoryId' => 3],
70
            ['id' => 3, 'name' => 'Category #3', 'topStoryId' => 9],
71
        ];
72
73
        $this->paths    = [];
74
        $this->userType = new ObjectType([
75
            'name'   => 'User',
76
            'fields' => function () {
77
                return [
78
                    'name'       => [
79
                        'type'    => Type::string(),
80
                        'resolve' => function ($user, $args, $context, ResolveInfo $info) {
81
                            $this->paths[] = $info->path;
82
83
                            return $user['name'];
84
                        },
85
                    ],
86
                    'bestFriend' => [
87
                        'type'    => $this->userType,
88
                        'resolve' => function ($user, $args, $context, ResolveInfo $info) {
89
                            $this->paths[] = $info->path;
90
91
                            return new Deferred(function () use ($user) {
92
                                $this->paths[] = 'deferred-for-best-friend-of-' . $user['id'];
93
94
                                return Utils::find(
95
                                    $this->userDataSource,
96
                                    static function ($entry) use ($user) {
97
                                        return $entry['id'] === $user['bestFriendId'];
98
                                    }
99
                                );
100
                            });
101
                        },
102
                    ],
103
                ];
104
            },
105
        ]);
106
107
        $this->storyType = new ObjectType([
108
            'name'   => 'Story',
109
            'fields' => [
110
                'title'  => [
111
                    'type'    => Type::string(),
112
                    'resolve' => function ($story, $args, $context, ResolveInfo $info) {
113
                        $this->paths[] = $info->path;
114
115
                        return $story['title'];
116
                    },
117
                ],
118
                'author' => [
119
                    'type'    => $this->userType,
120
                    'resolve' => function ($story, $args, $context, ResolveInfo $info) {
121
                        $this->paths[] = $info->path;
122
123
                        return new Deferred(function () use ($story) {
124
                            $this->paths[] = 'deferred-for-story-' . $story['id'] . '-author';
125
126
                            return $this->findUserById($story['authorId']);
127
                        });
128
                    },
129
                ],
130
            ],
131
        ]);
132
133
        $this->categoryType = new ObjectType([
134
            'name'   => 'Category',
135
            'fields' => [
136
                'name' => [
137
                    'type'    => Type::string(),
138
                    'resolve' => function ($category, $args, $context, ResolveInfo $info) {
139
                        $this->paths[] = $info->path;
140
141
                        return $category['name'];
142
                    },
143
                ],
144
145
                'stories'  => [
146
                    'type'    => Type::listOf($this->storyType),
147
                    'resolve' => function ($category, $args, $context, ResolveInfo $info) {
148
                        $this->paths[] = $info->path;
149
150
                        return Utils::filter(
151
                            $this->storyDataSource,
152
                            static function ($story) use ($category) {
153
                                return in_array($category['id'], $story['categoryIds'], true);
154
                            }
155
                        );
156
                    },
157
                ],
158
                'topStory' => [
159
                    'type'    => $this->storyType,
160
                    'resolve' => function ($category, $args, $context, ResolveInfo $info) {
161
                        $this->paths[] = $info->path;
162
163
                        return new Deferred(function () use ($category) {
164
                            $this->paths[] = 'deferred-for-category-' . $category['id'] . '-topStory';
165
166
                            return $this->findStoryById($category['topStoryId']);
167
                        });
168
                    },
169
                ],
170
                'topStoryAuthor' => [
171
                    'type' => $this->userType,
172
                    'resolve' => function ($category, $args, $context, ResolveInfo $info) {
173
                        $this->paths[] = $info->path;
174
175
                        return new Deferred(function () use ($category) {
176
                            $this->paths[] = 'deferred-for-category-' . $category['id'] . '-topStoryAuthor1';
177
                            $story         = $this->findStoryById($category['topStoryId']);
178
179
                            return new Deferred(function () use ($category, $story) {
180
                                $this->paths[] = 'deferred-for-category-' . $category['id'] . '-topStoryAuthor2';
181
182
                                return $this->findUserById($story['authorId']);
183
                            });
184
                        });
185
                    },
186
                ],
187
            ],
188
        ]);
189
190
        $this->queryType = new ObjectType([
191
            'name'   => 'Query',
192
            'fields' => [
193
                'topStories'       => [
194
                    'type'    => Type::listOf($this->storyType),
195
                    'resolve' => function ($rootValue, $args, $context, ResolveInfo $info) {
196
                        $this->paths[] = $info->path;
197
198
                        return Utils::filter(
199
                            $this->storyDataSource,
200
                            static function ($story) {
201
                                return $story['id'] % 2 === 1;
202
                            }
203
                        );
204
                    },
205
                ],
206
                'featuredCategory' => [
207
                    'type'    => $this->categoryType,
208
                    'resolve' => function ($rootValue, $args, $context, ResolveInfo $info) {
209
                        $this->paths[] = $info->path;
210
211
                        return $this->categoryDataSource[0];
212
                    },
213
                ],
214
                'categories'       => [
215
                    'type'    => Type::listOf($this->categoryType),
216
                    'resolve' => function ($rootValue, $args, $context, ResolveInfo $info) {
217
                        $this->paths[] = $info->path;
218
219
                        return $this->categoryDataSource;
220
                    },
221
                ],
222
            ],
223
        ]);
224
225
        parent::setUp();
226
    }
227
228
    public function testDeferredFields() : void
229
    {
230
        $query = Parser::parse('
231
            {
232
                topStories {
233
                    title
234
                    author {
235
                        name
236
                    }
237
                }
238
                featuredCategory {
239
                    stories {
240
                        title
241
                        author {
242
                            name
243
                        }
244
                    }
245
                }
246
            }
247
        ');
248
249
        $schema = new Schema([
250
            'query' => $this->queryType,
251
        ]);
252
253
        $expected = [
254
            'data' => [
255
                'topStories'       => [
256
                    ['title' => 'Story #1', 'author' => ['name' => 'John']],
257
                    ['title' => 'Story #3', 'author' => ['name' => 'Joe']],
258
                    ['title' => 'Story #5', 'author' => ['name' => 'John']],
259
                    ['title' => 'Story #7', 'author' => ['name' => 'Joe']],
260
                    ['title' => 'Story #9', 'author' => ['name' => 'Jane']],
261
                ],
262
                'featuredCategory' => [
263
                    'stories' => [
264
                        ['title' => 'Story #2', 'author' => ['name' => 'Jane']],
265
                        ['title' => 'Story #4', 'author' => ['name' => 'Joe']],
266
                        ['title' => 'Story #6', 'author' => ['name' => 'Jane']],
267
                        ['title' => 'Story #8', 'author' => ['name' => 'John']],
268
                    ],
269
                ],
270
            ],
271
        ];
272
273
        $result = Executor::execute($schema, $query);
274
        self::assertEquals($expected, $result->toArray());
0 ignored issues
show
Bug introduced by
The method toArray() does not exist on GraphQL\Executor\Promise\Promise. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

274
        self::assertEquals($expected, $result->/** @scrutinizer ignore-call */ toArray());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
275
276
        $expectedPaths = [
277
            ['topStories'],
278
            ['topStories', 0, 'title'],
279
            ['topStories', 0, 'author'],
280
            ['topStories', 1, 'title'],
281
            ['topStories', 1, 'author'],
282
            ['topStories', 2, 'title'],
283
            ['topStories', 2, 'author'],
284
            ['topStories', 3, 'title'],
285
            ['topStories', 3, 'author'],
286
            ['topStories', 4, 'title'],
287
            ['topStories', 4, 'author'],
288
            ['featuredCategory'],
289
            ['featuredCategory', 'stories'],
290
            ['featuredCategory', 'stories', 0, 'title'],
291
            ['featuredCategory', 'stories', 0, 'author'],
292
            ['featuredCategory', 'stories', 1, 'title'],
293
            ['featuredCategory', 'stories', 1, 'author'],
294
            ['featuredCategory', 'stories', 2, 'title'],
295
            ['featuredCategory', 'stories', 2, 'author'],
296
            ['featuredCategory', 'stories', 3, 'title'],
297
            ['featuredCategory', 'stories', 3, 'author'],
298
            'deferred-for-story-1-author',
299
            'deferred-for-story-3-author',
300
            'deferred-for-story-5-author',
301
            'deferred-for-story-7-author',
302
            'deferred-for-story-9-author',
303
            'deferred-for-story-2-author',
304
            'deferred-for-story-4-author',
305
            'deferred-for-story-6-author',
306
            'deferred-for-story-8-author',
307
            ['topStories', 0, 'author', 'name'],
308
            ['topStories', 1, 'author', 'name'],
309
            ['topStories', 2, 'author', 'name'],
310
            ['topStories', 3, 'author', 'name'],
311
            ['topStories', 4, 'author', 'name'],
312
            ['featuredCategory', 'stories', 0, 'author', 'name'],
313
            ['featuredCategory', 'stories', 1, 'author', 'name'],
314
            ['featuredCategory', 'stories', 2, 'author', 'name'],
315
            ['featuredCategory', 'stories', 3, 'author', 'name'],
316
        ];
317
        self::assertCount(count($expectedPaths), $this->paths);
318
        foreach ($expectedPaths as $expectedPath) {
319
            self::assertTrue(in_array($expectedPath, $this->paths, true), 'Missing path: ' . json_encode($expectedPath));
320
        }
321
    }
322
323
    public function testNestedDeferredFields() : void
324
    {
325
        $query = Parser::parse('
326
            {
327
                categories {
328
                    name
329
                    topStory {
330
                        title
331
                        author {
332
                            name
333
                            bestFriend {
334
                                name
335
                            }
336
                        }
337
                    }
338
                }
339
            }
340
        ');
341
342
        $schema = new Schema([
343
            'query' => $this->queryType,
344
        ]);
345
346
        $author1 = ['name' => 'John', 'bestFriend' => ['name' => 'Dirk']];
347
        $author2 = ['name' => 'Jane', 'bestFriend' => ['name' => 'Joe']];
348
        $author3 = ['name' => 'Joe', 'bestFriend' => ['name' => 'Jane']];
349
        $author4 = ['name' => 'Dirk', 'bestFriend' => ['name' => 'John']];
0 ignored issues
show
Unused Code introduced by
The assignment to $author4 is dead and can be removed.
Loading history...
350
351
        $expected = [
352
            'data' => [
353
                'categories' => [
354
                    ['name' => 'Category #1', 'topStory' => ['title' => 'Story #8', 'author' => $author1]],
355
                    ['name' => 'Category #2', 'topStory' => ['title' => 'Story #3', 'author' => $author3]],
356
                    ['name' => 'Category #3', 'topStory' => ['title' => 'Story #9', 'author' => $author2]],
357
                ],
358
            ],
359
        ];
360
361
        $result = Executor::execute($schema, $query);
362
        self::assertEquals($expected, $result->toArray());
363
364
        $expectedPaths = [
365
            ['categories'],
366
            ['categories', 0, 'name'],
367
            ['categories', 0, 'topStory'],
368
            ['categories', 1, 'name'],
369
            ['categories', 1, 'topStory'],
370
            ['categories', 2, 'name'],
371
            ['categories', 2, 'topStory'],
372
            'deferred-for-category-1-topStory',
373
            'deferred-for-category-2-topStory',
374
            'deferred-for-category-3-topStory',
375
            ['categories', 0, 'topStory', 'title'],
376
            ['categories', 0, 'topStory', 'author'],
377
            ['categories', 1, 'topStory', 'title'],
378
            ['categories', 1, 'topStory', 'author'],
379
            ['categories', 2, 'topStory', 'title'],
380
            ['categories', 2, 'topStory', 'author'],
381
            'deferred-for-story-8-author',
382
            'deferred-for-story-3-author',
383
            'deferred-for-story-9-author',
384
            ['categories', 0, 'topStory', 'author', 'name'],
385
            ['categories', 0, 'topStory', 'author', 'bestFriend'],
386
            ['categories', 1, 'topStory', 'author', 'name'],
387
            ['categories', 1, 'topStory', 'author', 'bestFriend'],
388
            ['categories', 2, 'topStory', 'author', 'name'],
389
            ['categories', 2, 'topStory', 'author', 'bestFriend'],
390
            'deferred-for-best-friend-of-1',
391
            'deferred-for-best-friend-of-3',
392
            'deferred-for-best-friend-of-2',
393
            ['categories', 0, 'topStory', 'author', 'bestFriend', 'name'],
394
            ['categories', 1, 'topStory', 'author', 'bestFriend', 'name'],
395
            ['categories', 2, 'topStory', 'author', 'bestFriend', 'name'],
396
        ];
397
        self::assertCount(count($expectedPaths), $this->paths);
398
        foreach ($expectedPaths as $expectedPath) {
399
            self::assertTrue(in_array($expectedPath, $this->paths, true), 'Missing path: ' . json_encode($expectedPath));
400
        }
401
    }
402
403
    public function testComplexRecursiveDeferredFields() : void
404
    {
405
        $complexType = new ObjectType([
406
            'name'   => 'ComplexType',
407
            'fields' => function () use (&$complexType) {
408
                return [
409
                    'sync'         => [
410
                        'type'    => Type::string(),
411
                        'resolve' => function ($complexType, $args, $context, ResolveInfo $info) {
412
                            $this->paths[] = $info->path;
413
414
                            return 'sync';
415
                        },
416
                    ],
417
                    'deferred'     => [
418
                        'type'    => Type::string(),
419
                        'resolve' => function ($complexType, $args, $context, ResolveInfo $info) {
420
                            $this->paths[] = $info->path;
421
422
                            return new Deferred(function () use ($info) {
423
                                $this->paths[] = ['!dfd for: ', $info->path];
424
425
                                return 'deferred';
426
                            });
427
                        },
428
                    ],
429
                    'nest'         => [
430
                        'type'    => $complexType,
431
                        'resolve' => function ($complexType, $args, $context, ResolveInfo $info) {
432
                            $this->paths[] = $info->path;
433
434
                            return [];
435
                        },
436
                    ],
437
                    'deferredNest' => [
438
                        'type'    => $complexType,
439
                        'resolve' => function ($complexType, $args, $context, ResolveInfo $info) {
440
                            $this->paths[] = $info->path;
441
442
                            return new Deferred(function () use ($info) {
443
                                $this->paths[] = ['!dfd nest for: ', $info->path];
444
445
                                return [];
446
                            });
447
                        },
448
                    ],
449
                ];
450
            },
451
        ]);
452
453
        $schema = new Schema(['query' => $complexType]);
454
455
        $query    = Parser::parse('
456
            {
457
                nest {
458
                    sync
459
                    deferred
460
                    nest {
461
                        sync
462
                        deferred
463
                    }
464
                    deferredNest {
465
                        sync
466
                        deferred
467
                    }
468
                }
469
                deferredNest {
470
                    sync
471
                    deferred
472
                    nest {
473
                        sync
474
                        deferred
475
                    }
476
                    deferredNest {
477
                        sync
478
                        deferred
479
                    }
480
                }
481
            }
482
        ');
483
        $result   = Executor::execute($schema, $query);
484
        $expected = [
485
            'data' => [
486
                'nest'         => [
487
                    'sync'         => 'sync',
488
                    'deferred'     => 'deferred',
489
                    'nest'         => [
490
                        'sync'     => 'sync',
491
                        'deferred' => 'deferred',
492
                    ],
493
                    'deferredNest' => [
494
                        'sync'     => 'sync',
495
                        'deferred' => 'deferred',
496
                    ],
497
                ],
498
                'deferredNest' => [
499
                    'sync'         => 'sync',
500
                    'deferred'     => 'deferred',
501
                    'nest'         => [
502
                        'sync'     => 'sync',
503
                        'deferred' => 'deferred',
504
                    ],
505
                    'deferredNest' => [
506
                        'sync'     => 'sync',
507
                        'deferred' => 'deferred',
508
                    ],
509
                ],
510
            ],
511
        ];
512
513
        self::assertEquals($expected, $result->toArray());
514
515
        $expectedPaths = [
516
            ['nest'],
517
            ['nest', 'sync'],
518
            ['nest', 'deferred'],
519
            ['nest', 'nest'],
520
            ['nest', 'nest', 'sync'],
521
            ['nest', 'nest', 'deferred'],
522
            ['nest', 'deferredNest'],
523
            ['deferredNest'],
524
525
            ['!dfd for: ', ['nest', 'deferred']],
526
            ['!dfd for: ', ['nest', 'nest', 'deferred']],
527
            ['!dfd nest for: ', ['nest', 'deferredNest']],
528
            ['!dfd nest for: ', ['deferredNest']],
529
530
            ['nest', 'deferredNest', 'sync'],
531
            ['nest', 'deferredNest', 'deferred'],
532
            ['deferredNest', 'sync'],
533
            ['deferredNest', 'deferred'],
534
            ['deferredNest', 'nest'],
535
            ['deferredNest', 'nest', 'sync'],
536
            ['deferredNest', 'nest', 'deferred'],
537
            ['deferredNest', 'deferredNest'],
538
539
            ['!dfd for: ', ['nest', 'deferredNest', 'deferred']],
540
            ['!dfd for: ', ['deferredNest', 'deferred']],
541
            ['!dfd for: ', ['deferredNest', 'nest', 'deferred']],
542
            ['!dfd nest for: ', ['deferredNest', 'deferredNest']],
543
544
            ['deferredNest', 'deferredNest', 'sync'],
545
            ['deferredNest', 'deferredNest', 'deferred'],
546
            ['!dfd for: ', ['deferredNest', 'deferredNest', 'deferred']],
547
        ];
548
549
        // Note: not using self::assertEquals() because coroutineExecutor has a different sequence of calls
550
        self::assertCount(count($expectedPaths), $this->paths);
551
        foreach ($expectedPaths as $expectedPath) {
552
            self::assertTrue(in_array($expectedPath, $this->paths, true), 'Missing path: ' . json_encode($expectedPath));
553
        }
554
    }
555
556
    public function testDeferredChaining()
557
    {
558
        $schema = new Schema([
559
            'query' => $this->queryType,
560
        ]);
561
562
        $query = Parser::parse('
563
            {
564
                categories {
565
                    name
566
                    topStory {
567
                        title
568
                        author {
569
                            name
570
                        }
571
                    }
572
                    topStoryAuthor {
573
                        name
574
                    }
575
                }
576
            }
577
        ');
578
579
        $author1 = ['name' => 'John'/*, 'bestFriend' => ['name' => 'Dirk']*/];
580
        $author2 = ['name' => 'Jane'/*, 'bestFriend' => ['name' => 'Joe']*/];
581
        $author3 = ['name' => 'Joe'/*, 'bestFriend' => ['name' => 'Jane']*/];
582
        $author4 = ['name' => 'Dirk'/*, 'bestFriend' => ['name' => 'John']*/];
0 ignored issues
show
Unused Code introduced by
The assignment to $author4 is dead and can be removed.
Loading history...
583
584
        $story1 = ['title' => 'Story #8', 'author' => $author1];
585
        $story2 = ['title' => 'Story #3', 'author' => $author3];
586
        $story3 = ['title' => 'Story #9', 'author' => $author2];
587
588
        $result   = Executor::execute($schema, $query);
589
        $expected = [
590
            'data' => [
591
                'categories' => [
592
                    ['name' => 'Category #1', 'topStory' => $story1, 'topStoryAuthor' => $author1],
593
                    ['name' => 'Category #2', 'topStory' => $story2, 'topStoryAuthor' => $author3],
594
                    ['name' => 'Category #3', 'topStory' => $story3, 'topStoryAuthor' => $author2],
595
                ],
596
            ],
597
        ];
598
        self::assertEquals($expected, $result->toArray());
599
600
        $expectedPaths = [
601
            ['categories'],
602
            ['categories', 0, 'name'],
603
            ['categories', 0, 'topStory'],
604
            ['categories', 0, 'topStoryAuthor'],
605
            ['categories', 1, 'name'],
606
            ['categories', 1, 'topStory'],
607
            ['categories', 1, 'topStoryAuthor'],
608
            ['categories', 2, 'name'],
609
            ['categories', 2, 'topStory'],
610
            ['categories', 2, 'topStoryAuthor'],
611
            'deferred-for-category-1-topStory',
612
            'deferred-for-category-1-topStoryAuthor1',
613
            'deferred-for-category-2-topStory',
614
            'deferred-for-category-2-topStoryAuthor1',
615
            'deferred-for-category-3-topStory',
616
            'deferred-for-category-3-topStoryAuthor1',
617
            'deferred-for-category-1-topStoryAuthor2',
618
            'deferred-for-category-2-topStoryAuthor2',
619
            'deferred-for-category-3-topStoryAuthor2',
620
            ['categories', 0, 'topStory', 'title'],
621
            ['categories', 0, 'topStory', 'author'],
622
            ['categories', 1, 'topStory', 'title'],
623
            ['categories', 1, 'topStory', 'author'],
624
            ['categories', 2, 'topStory', 'title'],
625
            ['categories', 2, 'topStory', 'author'],
626
            ['categories', 0, 'topStoryAuthor', 'name'],
627
            ['categories', 1, 'topStoryAuthor', 'name'],
628
            ['categories', 2, 'topStoryAuthor', 'name'],
629
            'deferred-for-story-8-author',
630
            'deferred-for-story-3-author',
631
            'deferred-for-story-9-author',
632
            ['categories', 0, 'topStory', 'author', 'name'],
633
            ['categories', 1, 'topStory', 'author', 'name'],
634
            ['categories', 2, 'topStory', 'author', 'name'],
635
        ];
636
        self::assertEquals($expectedPaths, $this->paths);
637
    }
638
639
    private function findStoryById($id)
640
    {
641
        return Utils::find(
642
            $this->storyDataSource,
643
            static function ($story) use ($id) {
644
                return $story['id'] === $id;
645
            }
646
        );
647
    }
648
649
    private function findUserById($id)
650
    {
651
        return Utils::find(
652
            $this->userDataSource,
653
            static function ($user) use ($id) {
654
                return $user['id'] === $id;
655
            }
656
        );
657
    }
658
}
659