Completed
Push — master ( 791922...f9d483 )
by Alex
16s queued 15s
created

testFindOneByWillThrowEntityRepositoryExceptionIfTheQueryCannotBePerformed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 34
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 22
c 1
b 0
f 0
dl 0
loc 34
rs 9.568
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace ArpTest\DoctrineEntityRepository;
6
7
use Arp\DoctrineEntityRepository\Constant\EntityEventOption;
8
use Arp\DoctrineEntityRepository\Constant\QueryServiceOption;
9
use Arp\DoctrineEntityRepository\EntityRepository;
10
use Arp\DoctrineEntityRepository\EntityRepositoryInterface;
11
use Arp\DoctrineEntityRepository\Exception\EntityRepositoryException;
12
use Arp\DoctrineEntityRepository\Persistence\PersistServiceInterface;
13
use Arp\DoctrineEntityRepository\Query\QueryServiceInterface;
14
use Arp\Entity\EntityInterface;
15
use Doctrine\Persistence\ObjectRepository;
16
use PHPUnit\Framework\MockObject\MockObject;
17
use PHPUnit\Framework\TestCase;
18
use Psr\Log\LoggerInterface;
19
20
/**
21
 * @author  Alex Patterson <[email protected]>
22
 * @package ArpTest\DoctrineEntityRepository
23
 */
24
class EntityRepositoryTest extends TestCase
25
{
26
    /**
27
     * @var string
28
     */
29
    private string $entityName;
30
31
    /**
32
     * @var QueryServiceInterface|MockObject
33
     */
34
    private $queryService;
35
36
    /**
37
     * @var PersistServiceInterface|MockObject
38
     */
39
    private $persistService;
40
41
    /**
42
     * @var LoggerInterface|MockObject
43
     */
44
    private $logger;
45
46
    /**
47
     * Set up the test case dependencies.
48
     */
49
    public function setUp(): void
50
    {
51
        $this->entityName = EntityInterface::class;
52
53
        $this->queryService = $this->getMockForAbstractClass(QueryServiceInterface::class);
54
55
        $this->persistService = $this->getMockForAbstractClass(PersistServiceInterface::class);
56
57
        $this->logger = $this->getMockForAbstractClass(LoggerInterface::class);
58
    }
59
60
    /**
61
     * Assert the EntityRepository implements the EntityRepositoryInterface
62
     *
63
     * @covers \Arp\DoctrineEntityRepository\EntityRepository::__construct
64
     */
65
    public function testImplementsEntityRepositoryInterface(): void
66
    {
67
        $repository = new EntityRepository(
68
            $this->entityName,
69
            $this->queryService,
70
            $this->persistService,
71
            $this->logger
72
        );
73
74
        $this->assertInstanceOf(EntityRepositoryInterface::class, $repository);
75
    }
76
77
    /**
78
     * Assert the EntityRepository implements the ObjectRepository
79
     *
80
     * @covers \Arp\DoctrineEntityRepository\EntityRepository::__construct
81
     */
82
    public function testImplementsObjectRepository(): void
83
    {
84
        $repository = new EntityRepository(
85
            $this->entityName,
86
            $this->queryService,
87
            $this->persistService,
88
            $this->logger
89
        );
90
91
        $this->assertInstanceOf(ObjectRepository::class, $repository);
92
    }
93
94
    /**
95
     * Assert that method getClassName() will return the FQCN of the managed entity.
96
     *
97
     * @covers \Arp\DoctrineEntityRepository\EntityRepository::getClassName
98
     */
99
    public function testGetClassNameWillReturnTheFullyQualifiedEntityClassName(): void
100
    {
101
        $repository = new EntityRepository(
102
            $this->entityName,
103
            $this->queryService,
104
            $this->persistService,
105
            $this->logger
106
        );
107
108
        $this->assertSame($this->entityName, $repository->getClassName());
109
    }
110
111
    /**
112
     * Assert that if find() cannot load/fails then any thrown exception/throwable will be caught and rethrown as
113
     * a EntityRepositoryException.
114
     *
115
     * @covers \Arp\DoctrineEntityRepository\EntityRepository::find
116
     *
117
     * @throws EntityRepositoryException
118
     */
119
    public function testFindCatchAndRethrowExceptionsAsEntityRepositoryExceptionIfTheFindQueryFails(): void
120
    {
121
        $repository = new EntityRepository(
122
            $this->entityName,
123
            $this->queryService,
124
            $this->persistService,
125
            $this->logger
126
        );
127
128
        $entityId = 'FOO123';
129
130
        $exceptionMessage = 'This is a test exception message for find()';
131
        $exception = new \Exception($exceptionMessage);
132
133
        $this->queryService->expects($this->once())
134
            ->method('findOneById')
135
            ->with($entityId)
136
            ->willThrowException($exception);
137
138
        $errorMessage = sprintf('Unable to find entity of type \'%s\': %s', $this->entityName, $exceptionMessage);
139
140
        $this->logger->expects($this->once())
141
            ->method('error')
142
            ->with($errorMessage, ['exception' => $exception, 'id' => $entityId]);
143
144
        $this->expectException(EntityRepositoryException::class);
145
        $this->expectExceptionMessage($errorMessage);
146
147
        $repository->find($entityId);
148
    }
149
150
    /**
151
     * Assert that find() will return NULL if the entity cannot be found by it's $id.
152
     *
153
     * @covers \Arp\DoctrineEntityRepository\EntityRepository::find
154
     *
155
     * @throws EntityRepositoryException
156
     */
157
    public function testFindWillReturnNullIfNotEntityCanBeFound(): void
158
    {
159
        $repository = new EntityRepository(
160
            $this->entityName,
161
            $this->queryService,
162
            $this->persistService,
163
            $this->logger
164
        );
165
166
        $entityId = 'FOO123';
167
168
        $this->queryService->expects($this->once())
169
            ->method('findOneById')
170
            ->with($entityId)
171
            ->willReturn(null);
172
173
        $this->assertNull($repository->find($entityId));
174
    }
175
176
    /**
177
     * Assert that find() will the found entity by it's $id.
178
     *
179
     * @covers \Arp\DoctrineEntityRepository\EntityRepository::find
180
     *
181
     * @throws EntityRepositoryException
182
     */
183
    public function testFindWillReturnTheMatchedEntity(): void
184
    {
185
        $repository = new EntityRepository(
186
            $this->entityName,
187
            $this->queryService,
188
            $this->persistService,
189
            $this->logger
190
        );
191
192
        /** @var EntityInterface|MockObject $entity */
193
        $entity = $this->getMockForAbstractClass(EntityInterface::class);
194
        $entityId = 'FOO123';
195
196
        $this->queryService->expects($this->once())
197
            ->method('findOneById')
198
            ->with($entityId)
199
            ->willReturn($entity);
200
201
        $this->assertSame($entity, $repository->find($entityId));
202
    }
203
204
    /**
205
     * Assert that if the findOneBy() will catch and rethrow exception messages as EntityRepositoryException.
206
     *
207
     * @covers \Arp\DoctrineEntityRepository\EntityRepository::findOneBy
208
     *
209
     * @throws EntityRepositoryException
210
     */
211
    public function testFindOneByWillThrowEntityRepositoryExceptionIfTheQueryCannotBePerformed(): void
212
    {
213
        $repository = new EntityRepository(
214
            $this->entityName,
215
            $this->queryService,
216
            $this->persistService,
217
            $this->logger
218
        );
219
220
        $criteria = [
221
            'name'  => 'Test',
222
            'hello' => 'World',
223
            'test'  => 123,
224
        ];
225
226
        $exceptionMessage = 'This is a test exception message for findOneById()';
227
        $exceptionCode = 456;
228
229
        $exception = new \Exception($exceptionMessage, $exceptionCode);
230
231
        $this->queryService->expects($this->once())
232
            ->method('findOne')
233
            ->with($criteria)
234
            ->willThrowException($exception);
235
236
        $errorMessage = sprintf('Unable to find entity of type \'%s\': %s', $this->entityName, $exceptionMessage);
237
238
        $this->logger->error($errorMessage, compact('exception', 'criteria'));
239
240
        $this->expectException(EntityRepositoryException::class);
241
        $this->expectExceptionMessage($errorMessage);
242
        $this->expectExceptionCode($exceptionCode);
243
244
        $repository->findOneBy($criteria);
245
    }
246
247
    /**
248
     * Assert that findOneBy() will return NULL if the provided criteria doesn't match any existing entities.
249
     *
250
     * @covers \Arp\DoctrineEntityRepository\EntityRepository::findOneBy
251
     *
252
     * @throws EntityRepositoryException
253
     */
254
    public function testFindOneByWillReturnNullIfAMatchingEntityCannotBeFound(): void
255
    {
256
        $repository = new EntityRepository(
257
            $this->entityName,
258
            $this->queryService,
259
            $this->persistService,
260
            $this->logger
261
        );
262
263
        $criteria = [
264
            'foo'  => 'bar',
265
            'test' => 789,
266
        ];
267
268
        $this->queryService->expects($this->once())
269
            ->method('findOne')
270
            ->with($criteria)
271
            ->willReturn(null);
272
273
        $this->assertNull($repository->findOneBy($criteria));
274
    }
275
276
    /**
277
     * Assert findOneBy() will return a single matched entity instance.
278
     *
279
     * @covers \Arp\DoctrineEntityRepository\EntityRepository::findOneBy
280
     *
281
     * @throws EntityRepositoryException
282
     */
283
    public function testFindOneByWillReturnASingleMatchEntity(): void
284
    {
285
        $repository = new EntityRepository(
286
            $this->entityName,
287
            $this->queryService,
288
            $this->persistService,
289
            $this->logger
290
        );
291
292
        $criteria = [
293
            'fred' => 'test',
294
            'bob'  => 123,
295
        ];
296
297
        /** @var EntityInterface|MockObject $entity */
298
        $entity = $this->getMockForAbstractClass(EntityInterface::class);
299
300
        $this->queryService->expects($this->once())
301
            ->method('findOne')
302
            ->with($criteria)
303
            ->willReturn($entity);
304
305
        $this->assertSame($entity, $repository->findOneBy($criteria));
306
    }
307
308
    /**
309
     * Assert that calls to findAll() will proxy to findBy() with an empty array.
310
     *
311
     * @covers \Arp\DoctrineEntityRepository\EntityRepository::findAll
312
     *
313
     * @throws EntityRepositoryException
314
     */
315
    public function testFindAllWillProxyAnEmptyArrayToFindBy(): void
316
    {
317
        /** @var EntityRepository|MockObject $repository */
318
        $repository = $this->getMockBuilder(EntityRepository::class)
319
            ->setConstructorArgs(
320
                [
321
                    $this->entityName,
322
                    $this->queryService,
323
                    $this->persistService,
324
                    $this->logger,
325
                ]
326
            )
327
            ->onlyMethods(['findBy'])
328
            ->getMock();
329
330
        /** @var EntityInterface[]|MockObject[] $entities */
331
        $entities = [
332
            $this->getMockForAbstractClass(EntityInterface::class),
333
            $this->getMockForAbstractClass(EntityInterface::class),
334
            $this->getMockForAbstractClass(EntityInterface::class),
335
        ];
336
337
        $repository->expects($this->once())
338
            ->method('findBy')
339
            ->with([])
340
            ->willReturn($entities);
341
342
        $this->assertSame($entities, $repository->findAll());
343
    }
344
345
    /**
346
     * Assert that the findBy() method will catch \Throwable exceptions, log the exception data and rethrow as a
347
     * EntityRepositoryException.
348
     *
349
     * @covers \Arp\DoctrineEntityRepository\EntityRepository::findBy
350
     *
351
     * @throws EntityRepositoryException
352
     */
353
    public function testFindByWillCatchAndRethrowEntityRepositoryExceptionOnFailure(): void
354
    {
355
        /** @var EntityRepository|MockObject $repository */
356
        $repository = new EntityRepository(
357
            $this->entityName,
358
            $this->queryService,
359
            $this->persistService,
360
            $this->logger
361
        );
362
363
        $criteria = [];
364
        $options = [];
365
366
        $exceptionMessage = 'This is a foo test exception';
367
        $exceptionCode = 456;
368
        $exception = new \Error($exceptionMessage, $exceptionCode);
369
370
        $this->queryService->expects($this->once())
371
            ->method('findMany')
372
            ->with($criteria, $options)
373
            ->willThrowException($exception);
374
375
        $errorMessage = sprintf(
376
            'Unable to return a collection of type \'%s\': %s',
377
            $this->entityName,
378
            $exceptionMessage
379
        );
380
381
        $this->logger->expects($this->once())
382
            ->method('error')
383
            ->with($errorMessage, compact('exception', 'criteria', 'options'));
384
385
        $this->expectException(EntityRepositoryException::class);
386
        $this->expectExceptionMessage($errorMessage);
387
388
        $repository->findBy([]);
389
    }
390
391
    /**
392
     * Assert the required search arguments are passed to findMany().
393
     *
394
     * @param array $data
395
     *
396
     * @covers \Arp\DoctrineEntityRepository\EntityRepository::findBy
397
     *
398
     * @dataProvider getFindByWithSearchArgumentsData
399
     *
400
     * @throws EntityRepositoryException
401
     */
402
    public function testFindByWillPassValidSearchArguments(array $data): void
403
    {
404
        /** @var EntityRepository|MockObject $repository */
405
        $repository = new EntityRepository(
406
            $this->entityName,
407
            $this->queryService,
408
            $this->persistService,
409
            $this->logger
410
        );
411
412
        $criteria = $data['criteria'] ?? [];
413
        $options = [];
414
415
        if (isset($data['order_by'])) {
416
            $options[QueryServiceOption::ORDER_BY] = $data['order_by'];
417
        }
418
419
        if (isset($data['limit'])) {
420
            $options[QueryServiceOption::LIMIT] = $data['limit'];
421
        }
422
423
        if (isset($data['offset'])) {
424
            $options[QueryServiceOption::OFFSET] = $data['offset'];
425
        }
426
427
        /** @var EntityInterface[]|MockObject[] $collection */
428
        $collection = [
429
            $this->getMockForAbstractClass(EntityInterface::class),
430
            $this->getMockForAbstractClass(EntityInterface::class),
431
            $this->getMockForAbstractClass(EntityInterface::class),
432
        ];
433
434
        $this->queryService->expects($this->once())
435
            ->method('findMany')
436
            ->with($criteria, $options)
437
            ->willReturn($collection);
438
439
        $result = $repository->findBy(
440
            $criteria,
441
            $data['order_by'] ?? null,
442
            $data['limit'] ?? null,
443
            $data['offset'] ?? null
444
        );
445
446
        $this->assertSame($collection, $result);
447
    }
448
449
    /**
450
     * @return array
451
     */
452
    public function getFindByWithSearchArgumentsData(): array
453
    {
454
        return [
455
            [
456
                // Empty Data
457
                [
458
459
                ],
460
            ],
461
462
            [
463
                [
464
                    'criteria' => [
465
                        'name' => 'test',
466
                        'hello' => 'foo',
467
                    ],
468
                ]
469
            ],
470
471
            [
472
                [
473
                    'limit' => 100,
474
                ]
475
            ],
476
477
            [
478
                [
479
                    'offset' => 10,
480
                ]
481
            ],
482
483
            [
484
                [
485
                    'order_by' => [
486
                        'name' => 'desc',
487
                        'foo' => 'asc',
488
                    ],
489
                ]
490
            ],
491
492
            [
493
                [
494
                    'criteria' => [
495
                        'name' => 'test',
496
                        'hello' => 'foo',
497
                        'foo' => 123
498
                    ],
499
                    'offset' => 7,
500
                    'limit' => 1000,
501
                    'order_by' => [
502
                        'hello' => 'asc',
503
                    ],
504
                ],
505
            ],
506
        ];
507
    }
508
509
    /**
510
     * Assert that errors during save that throw a \Throwable exception are caught, logged and rethrown as a
511
     * EntityRepositoryException.
512
     *
513
     * @covers \Arp\DoctrineEntityRepository\EntityRepository::save
514
     *
515
     * @throws EntityRepositoryException
516
     */
517
    public function testSaveWillCatchThrowableAndRethrowEntityRepositoryException(): void
518
    {
519
        /** @var EntityRepository|MockObject $repository */
520
        $repository = new EntityRepository(
521
            $this->entityName,
522
            $this->queryService,
523
            $this->persistService,
524
            $this->logger
525
        );
526
527
        /** @var EntityInterface|MockObject $entity */
528
        $entity = $this->getMockForAbstractClass(EntityInterface::class);
529
530
        $exceptionMessage = 'This is a test exception message for save()';
531
        $exceptionCode = 999;
532
        $exception = new \Error($exceptionMessage, $exceptionCode);
533
534
        $errorMessage = sprintf('Unable to save entity of type \'%s\': %s', $this->entityName, $exceptionMessage);
535
536
        $this->persistService->expects($this->once())
537
            ->method('save')
538
            ->with($entity)
539
            ->willThrowException($exception);
540
541
        $this->expectException(EntityRepositoryException::class);
542
        $this->expectExceptionMessage($errorMessage);
543
        $this->expectExceptionCode($exceptionCode);
544
545
        $this->assertSame($entity, $repository->save($entity));
546
    }
547
548
    /**
549
     * Assert that calls to save() will result in the entity being passed to the persist service.
550
     *
551
     * @covers \Arp\DoctrineEntityRepository\EntityRepository::save
552
     *
553
     * @throws EntityRepositoryException
554
     */
555
    public function testSaveAndReturnEntityWithProvidedOptions(): void
556
    {
557
        $options = [
558
            'foo' => 'bar',
559
        ];
560
561
        $repository = new EntityRepository(
562
            $this->entityName,
563
            $this->queryService,
564
            $this->persistService,
565
            $this->logger
566
        );
567
568
        /** @var EntityInterface|MockObject $entity */
569
        $entity = $this->getMockForAbstractClass(EntityInterface::class);
570
571
        $this->persistService->expects($this->once())
572
            ->method('save')
573
            ->with($entity, $options)
574
            ->willReturn($entity);
575
576
        $this->assertSame($entity, $repository->save($entity, $options));
577
    }
578
}
579