Completed
Push — master ( 3f65c6...83b473 )
by Vincent
02:48
created

TransactionalInterceptorTest::testDefaultOptions()   B

Complexity

Conditions 4
Paths 2

Size

Total Lines 40
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 40
rs 8.5806
cc 4
eloc 30
nc 2
nop 0
1
<?php
2
3
/**
4
 * Copyright 2016 Inneair
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 *
18
 * @license http://www.apache.org/licenses/LICENSE-2.0.html Apache-2.0
19
 */
20
21
namespace Inneair\TransactionBundle\Test\Aop;
22
23
use CG\Proxy\MethodInvocation;
24
use Doctrine\Common\Annotations\Reader;
25
use Doctrine\Common\Persistence\ObjectRepository;
26
use Doctrine\DBAL\Connection;
27
use Doctrine\ORM\EntityManagerInterface;
28
use Exception;
29
use PHPUnit_Framework_MockObject_MockObject;
30
use Psr\Log\LoggerInterface;
31
use ReflectionClass;
32
use Inneair\TransactionBundle\Annotation\Transactional;
33
use Inneair\TransactionBundle\Aop\TransactionalInterceptor;
34
use Inneair\TransactionBundle\DependencyInjection\Configuration;
35
use Inneair\TransactionBundle\Test\AbstractTest;
36
use Inneair\TransactionBundle\Test\Aop\Fixture\InterceptedClass;
37
use Symfony\Bridge\Doctrine\RegistryInterface;
38
use Symfony\Component\DependencyInjection\ContainerInterface;
39
40
/**
41
 * Class containing test suite for the {@link TransactionalInterceptor} class.
42
 */
43
class TransactionalInterceptorTest extends AbstractTest
44
{
45
    /**
46
     * Mocked service container.
47
     * @var PHPUnit_Framework_MockObject_MockObject
48
     */
49
    private $container;
50
    /**
51
     * Mocked entity manager.
52
     * @var PHPUnit_Framework_MockObject_MockObject
53
     */
54
    private $entityManager;
55
    /**
56
     * Mocked entity manager registry.
57
     * @var PHPUnit_Framework_MockObject_MockObject
58
     */
59
    private $entityManagerRegistry;
60
    /**
61
     * Mocked DB connection.
62
     * @var PHPUnit_Framework_MockObject_MockObject
63
     */
64
    private $connection;
65
    /**
66
     * Mocked logger.
67
     * @var PHPUnit_Framework_MockObject_MockObject
68
     */
69
    private $logger;
70
    /**
71
     * Mocked annotation reader.
72
     * @var PHPUnit_Framework_MockObject_MockObject
73
     */
74
    private $reader;
75
    /**
76
     * Mocked repository.
77
     * @var PHPUnit_Framework_MockObject_MockObject
78
     */
79
    private $repository;
80
    /**
81
     * Transactional interceptor.
82
     * @var TransactionalInterceptor
83
     */
84
    private $transactionalInterceptor;
85
86
    /**
87
     * {@inheritDoc}
88
     */
89
    public function setUp()
90
    {
91
        parent::setUp();
92
93
        $this->container = $this->getMock(ContainerInterface::class);
94
95
        $this->connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock();
96
97
        $this->repository = $this->getMockBuilder(ObjectRepository::class)->disableOriginalConstructor()->getMock();
98
99
        $this->entityManager = $this->getMock(EntityManagerInterface::class);
100
        $this->entityManager->expects($this->any())->method('getConnection')->willReturn($this->connection);
101
        $this->entityManager->expects($this->any())->method('getRepository')->willReturn($this->repository);
102
103
        $this->entityManagerRegistry = $this->getMock(RegistryInterface::class);
104
        $this->entityManagerRegistry->expects(static::any())->method('getManager')->willReturn($this->entityManager);
105
106
        $this->logger = $this->getMock(LoggerInterface::class);
107
108
        $this->reader = $this->getMock(Reader::class);
109
110
        $this->transactionalInterceptor = new TransactionalInterceptor(
111
            $this->container,
112
            $this->entityManagerRegistry,
113
            $this->reader,
114
            $this->logger
115
        );
116
    }
117
118
    /**
119
     * Invokes the interceptor for an annotated method (transaction required), in an unannotated class. The method will
120
     * invoke a nested method (new transaction required), that should lead to opening two nested transactions.
121
     */
122
    public function testNestedNewRequiredTransactionForAnnotatedMethodInUnannotatedClass()
123
    {
124
        $class = new ReflectionClass(InterceptedClass::class);
125
        $instance = $class->newInstance();
126
127
        $this->reader->expects(static::never())->method('getClassAnnotation');
128
129
        // Method 'cMethod' requires a transaction.
130
        $annotationRequired = new Transactional($this->buildTransactionalOptions(Transactional::REQUIRED));
131
        // Nested method 'aMethod' requires a new transaction whatever the transactional context.
132
        $annotationRequiresNew = new Transactional($this->buildTransactionalOptions(Transactional::NESTED));
133
134
        $this->reader->expects(static::exactly(2))->method('getMethodAnnotation')->willReturnOnConsecutiveCalls(
135
            $annotationRequired,
136
            $annotationRequiresNew
137
        );
138
139
        $this->entityManager->expects(static::exactly(2))->method('beginTransaction');
140
        $this->entityManager->expects(static::exactly(2))->method('commit');
141
        $this->entityManager->expects(static::never())->method('rollback');
142
143
        $nestedTransaction = function ($class, $instance)
144
        {
145
            $this->transactionalInterceptor->intercept(
146
                new MethodInvocation($class->getMethod('aMethod'), $instance, array(), array()));
147
        };
148
149
        $this->assertNull(
150
            $this->transactionalInterceptor->intercept(
151
                new MethodInvocation($class->getMethod('cMethod'), $instance,
152
                    array($nestedTransaction, array($class, $instance)), array())
153
            )
154
        );
155
    }
156
157
    /**
158
     * Invokes the interceptor for an annotated method (transaction required), in an unannotated class. The method will
159
     * invoke a nested method (new transaction required), that should lead to opening two nested transactions.
160
     */
161
    public function testNestedNewRequiredTransactionForUnannotatedMethodInAnnotatedClass()
162
    {
163
        $class = new ReflectionClass(InterceptedClass::class);
164
        $instance = $class->newInstance();
165
166
        $this->reader->expects(static::exactly(2))->method('getMethodAnnotation')->willReturn(null);
167
168
        // Method 'cMethod' requires a transaction.
169
        $annotationRequired = new Transactional($this->buildTransactionalOptions(Transactional::REQUIRED));
170
171
        // Nested method 'aMethod' requires a new transaction whatever the transactional context.
172
        $annotationRequiresNew = new Transactional($this->buildTransactionalOptions(Transactional::NESTED));
173
174
        $this->reader->expects(static::exactly(2))->method('getClassAnnotation')->willReturnOnConsecutiveCalls(
175
            $annotationRequired,
176
            $annotationRequiresNew
177
        );
178
179
        $this->entityManager->expects(static::exactly(2))->method('beginTransaction');
180
        $this->entityManager->expects(static::exactly(2))->method('commit');
181
        $this->entityManager->expects(static::never())->method('rollback');
182
183
        $nestedTransaction = function ($class, $instance)
184
        {
185
            $this->transactionalInterceptor->intercept(
186
                new MethodInvocation($class->getMethod('aMethod'), $instance, array(), array()));
187
        };
188
189
        $this->assertNull(
190
            $this->transactionalInterceptor->intercept(
191
                new MethodInvocation($class->getMethod('cMethod'), $instance,
192
                    array($nestedTransaction, array($class, $instance)), array())
193
            )
194
        );
195
    }
196
197
    /**
198
     * Invokes the interceptor for an annotated method (transaction not required), in an unannotated class.
199
     */
200
    public function testNotRequiredTransactionForAnnotatedMethodInUnannotatedClass()
201
    {
202
        $class = new ReflectionClass(InterceptedClass::class);
203
        $instance = $class->newInstance();
204
205
        $annotation = new Transactional($this->buildTransactionalOptions(Transactional::NOT_REQUIRED));
206
        $this->reader->expects(static::once())->method('getMethodAnnotation')->willReturn($annotation);
207
        $this->reader->expects(static::never())->method('getClassAnnotation');
208
        $this->entityManager->expects(static::never())->method('beginTransaction');
209
        $this->entityManager->expects(static::never())->method('commit');
210
        $this->entityManager->expects(static::never())->method('rollback');
211
212
        $this->assertNull(
213
            $this->transactionalInterceptor->intercept(
214
                new MethodInvocation($class->getMethod('aMethod'), $instance, array(), array())
215
            )
216
        );
217
    }
218
219
    /**
220
     * Invokes the interceptor for an annotated method (transaction not required), in an unannotated class.
221
     */
222
    public function testNotRequiredTransactionForUnannotatedMethodInAnnotatedClass()
223
    {
224
        $class = new ReflectionClass(InterceptedClass::class);
225
        $instance = $class->newInstance();
226
227
        $annotation = new Transactional($this->buildTransactionalOptions(Transactional::NOT_REQUIRED));
228
        $this->reader->expects(static::once())->method('getMethodAnnotation')->willReturn(null);
229
        $this->reader->expects(static::once())->method('getClassAnnotation')->willReturn($annotation);
230
        $this->entityManager->expects(static::never())->method('beginTransaction');
231
        $this->entityManager->expects(static::never())->method('commit');
232
        $this->entityManager->expects(static::never())->method('rollback');
233
234
        $this->assertNull(
235
            $this->transactionalInterceptor->intercept(
236
                new MethodInvocation($class->getMethod('aMethod'), $instance, array(), array())
237
            )
238
        );
239
    }
240
241
    public function testDefaultOptions()
242
    {
243
        $class = new ReflectionClass(InterceptedClass::class);
244
        $instance = $class->newInstance();
245
        $exceptionClassName = Exception::class;
246
247
        $annotation = new Transactional();
248
        $this->reader->expects(static::once())->method('getMethodAnnotation')->willReturn(null);
249
        $this->reader->expects(static::once())->method('getClassAnnotation')->willReturn($annotation);
250
        $this->container->expects(static::exactly(2))->method('getParameter')->willReturnCallback(
251
            function ($parameter) use($exceptionClassName)
252
            {
253
                if ($parameter === (Configuration::ROOT_NODE_NAME . '.' . Configuration::DEFAULT_POLICY)) {
254
                    $value = Transactional::REQUIRED;
255
                } elseif ($parameter === (Configuration::ROOT_NODE_NAME . '.' . Configuration::NO_ROLLBACK_EXCEPTIONS)) {
256
                    $value = array($exceptionClassName);
257
                } else {
258
                    $value = null;
259
                }
260
                return $value;
261
            });
262
        $this->entityManager->expects(static::once())->method('beginTransaction');
263
        $this->entityManager->expects(static::once())->method('commit');
264
        $this->entityManager->expects(static::never())->method('rollback');
265
266
        $hasException = false;
267
        try {
268
            $this->transactionalInterceptor->intercept(
269
                new MethodInvocation(
270
                    $class->getMethod('bMethodThrowException'),
271
                    $instance,
272
                    array($exceptionClassName),
273
                    array()
274
                )
275
            );
276
        } catch (Exception $e) {
277
            $hasException = true;
278
        }
279
        $this->assertTrue($hasException);
280
    }
281
282
    /**
283
     * Invokes the interceptor for an annotated method (transaction required), in an unannotated class.
284
     */
285
    public function testRequiredTransactionForAnnotatedMethodInUnannotatedClass()
286
    {
287
        $class = new ReflectionClass(InterceptedClass::class);
288
        $instance = $class->newInstance();
289
290
        $annotation = new Transactional($this->buildTransactionalOptions(Transactional::REQUIRED));
291
        $this->reader->expects(static::once())->method('getMethodAnnotation')->willReturn($annotation);
292
        $this->reader->expects(static::never())->method('getClassAnnotation');
293
        $this->entityManager->expects(static::once())->method('beginTransaction');
294
        $this->entityManager->expects(static::once())->method('commit');
295
        $this->entityManager->expects(static::never())->method('rollback');
296
297
        $this->assertNull(
298
            $this->transactionalInterceptor->intercept(
299
                new MethodInvocation($class->getMethod('aMethod'), $instance, array(), array())
300
            )
301
        );
302
    }
303
304
    /**
305
     * Invokes the interceptor for an annotated method (transaction required), in an unannotated class. The method
306
     * throws an exception that is ignored by the interceptor, and the transaction is committed.
307
     */
308
    public function testRequiredTransactionForAnnotatedMethodInUnannotatedClassWithIgnoredException()
309
    {
310
        $class = new ReflectionClass(InterceptedClass::class);
311
        $instance = $class->newInstance();
312
313
        $annotation = new Transactional(
314
            $this->buildTransactionalOptions(Transactional::REQUIRED, array(Exception::class))
315
        );
316
        $this->reader->expects(static::once())->method('getMethodAnnotation')->willReturn($annotation);
317
        $this->reader->expects(static::never())->method('getClassAnnotation');
318
        $this->entityManager->expects(static::once())->method('beginTransaction');
319
        $this->entityManager->expects(static::once())->method('commit');
320
        $this->entityManager->expects(static::never())->method('rollback');
321
322
        $hasException = false;
323
        try {
324
            $this->transactionalInterceptor->intercept(
325
                new MethodInvocation(
326
                    $class->getMethod('bMethodThrowException'),
327
                    $instance,
328
                    array(Exception::class),
329
                    array()
330
                )
331
            );
332
        } catch (Exception $e) {
333
            $hasException = true;
334
        }
335
        $this->assertTrue($hasException);
336
    }
337
338
    /**
339
     * Invokes the interceptor for an annotated method (transaction required), in an unannotated class. The method
340
     * throws an exception that rollbacks the transaction.
341
     */
342
    public function testRequiredTransactionForAnnotatedMethodInUnannotatedClassWithException()
343
    {
344
        $class = new ReflectionClass(InterceptedClass::class);
345
        $instance = $class->newInstance();
346
347
        $annotation = new Transactional($this->buildTransactionalOptions(Transactional::REQUIRED));
348
        $this->reader->expects(static::once())->method('getMethodAnnotation')->willReturn($annotation);
349
        $this->reader->expects(static::never())->method('getClassAnnotation');
350
        $this->entityManager->expects(static::once())->method('beginTransaction');
351
        $this->entityManager->expects(static::never())->method('commit');
352
        $this->entityManager->expects(static::once())->method('rollback');
353
354
        $hasException = false;
355
        try {
356
            $this->transactionalInterceptor->intercept(
357
                new MethodInvocation($class->getMethod('bMethodThrowException'), $instance, array(Exception::class),
358
                    array()
359
                )
360
            );
361
        } catch (Exception $e) {
362
            $hasException = true;
363
        }
364
        $this->assertTrue($hasException);
365
    }
366
367
    /**
368
     * Invokes the interceptor for an unannotated method, in an annotated class (transaction required).
369
     */
370
    public function testRequiredTransactionForUnannotatedMethodInAnnotatedClass()
371
    {
372
        $class = new ReflectionClass(InterceptedClass::class);
373
        $instance = $class->newInstance();
374
375
        $annotation = new Transactional($this->buildTransactionalOptions(Transactional::REQUIRED));
376
        $this->reader->expects(static::once())->method('getMethodAnnotation')->willReturn(null);
377
        $this->reader->expects(static::once())->method('getClassAnnotation')->willReturn($annotation);
378
        $this->entityManager->expects(static::once())->method('beginTransaction');
379
        $this->entityManager->expects(static::once())->method('commit');
380
        $this->entityManager->expects(static::never())->method('rollback');
381
382
        $this->assertNull(
383
            $this->transactionalInterceptor->intercept(
384
                new MethodInvocation($class->getMethod('aMethod'), $instance, array(), array())
385
            )
386
        );
387
    }
388
389
    /**
390
     * Invokes the interceptor for an annotated method (transaction required), in an unannotated class. The method
391
     * throws an exception that is ignored by the interceptor, and the transaction is committed.
392
     */
393
    public function testRequiredTransactionForUnannotatedMethodInAnnotatedClassWithIgnoredException()
394
    {
395
        $class = new ReflectionClass(InterceptedClass::class);
396
        $instance = $class->newInstance();
397
398
        $annotation = new Transactional(
399
            $this->buildTransactionalOptions(Transactional::REQUIRED, array(Exception::class)));
400
        $this->reader->expects(static::once())->method('getMethodAnnotation')->willReturn(null);
401
        $this->reader->expects(static::once())->method('getClassAnnotation')->willReturn($annotation);
402
        $this->entityManager->expects(static::once())->method('beginTransaction');
403
        $this->entityManager->expects(static::once())->method('commit');
404
        $this->entityManager->expects(static::never())->method('rollback');
405
406
        $hasException = false;
407
        try {
408
            $this->transactionalInterceptor->intercept(
409
                new MethodInvocation($class->getMethod('bMethodThrowException'), $instance, array(Exception::class),
410
                    array()
411
                )
412
            );
413
        } catch (Exception $e) {
414
            $hasException = true;
415
        }
416
        $this->assertTrue($hasException);
417
    }
418
419
    /**
420
     * Invokes the interceptor for an annotated method (transaction required), in an unannotated class. The method
421
     * throws an exception that rollbacks the transaction.
422
     */
423
    public function testRequiredTransactionForUnannotatedMethodInAnnotatedClassWithException()
424
    {
425
        $class = new ReflectionClass(InterceptedClass::class);
426
        $instance = $class->newInstance();
427
428
        $annotation = new Transactional($this->buildTransactionalOptions(Transactional::REQUIRED));
429
        $this->reader->expects(static::once())->method('getMethodAnnotation')->willReturn(null);
430
        $this->reader->expects(static::once())->method('getClassAnnotation')->willReturn($annotation);
431
        $this->entityManager->expects(static::once())->method('beginTransaction');
432
        $this->entityManager->expects(static::never())->method('commit');
433
        $this->entityManager->expects(static::once())->method('rollback');
434
435
        $hasException = false;
436
        try {
437
            $this->transactionalInterceptor->intercept(
438
                new MethodInvocation($class->getMethod('bMethodThrowException'), $instance, array(Exception::class),
439
                    array()
440
                )
441
            );
442
        } catch (Exception $e) {
443
            $hasException = true;
444
        }
445
        $this->assertTrue($hasException);
446
    }
447
448
    /**
449
     * Invokes the interceptor for a method in a class, both unannotated.
450
     */
451
    public function testUnannotatedMethodInUnannotatedClass()
452
    {
453
        $class = new ReflectionClass(InterceptedClass::class);
454
        $instance = $class->newInstance();
455
456
        $this->reader->expects(static::once())->method('getMethodAnnotation')->willReturn(null);
457
        $this->reader->expects(static::once())->method('getClassAnnotation')->willReturn(null);
458
        $this->logger->expects(static::once())->method('warning');
459
        $this->entityManager->expects(static::never())->method('beginTransaction');
460
        $this->entityManager->expects(static::never())->method('commit');
461
        $this->entityManager->expects(static::never())->method('rollback');
462
463
        $this->assertNull(
464
            $this->transactionalInterceptor->intercept(
465
                new MethodInvocation($class->getMethod('aMethod'), $instance, array(), array())
466
            )
467
        );
468
    }
469
470
    /**
471
     * Invokes the interceptor for a method in a class, both unannotated, which throws an exception.
472
     */
473
    public function testUnannotatedMethodInUnannotatedClassWithException()
474
    {
475
        $class = new ReflectionClass(InterceptedClass::class);
476
        $instance = $class->newInstance();
477
478
        $this->reader->expects(static::once())->method('getMethodAnnotation')->willReturn(null);
479
        $this->reader->expects(static::once())->method('getClassAnnotation')->willReturn(null);
480
        $this->logger->expects(static::once())->method('warning');
481
        $this->entityManager->expects(static::never())->method('beginTransaction');
482
        $this->entityManager->expects(static::never())->method('commit');
483
        $this->entityManager->expects(static::never())->method('rollback');
484
485
        $hasException = false;
486
        try {
487
            $this->assertNull(
488
                $this->transactionalInterceptor->intercept(
489
                    new MethodInvocation(
490
                        $class->getMethod('bMethodThrowException'),
491
                        $instance,
492
                        array(Exception::class),
493
                        array()
494
                    )
495
                )
496
            );
497
        } catch (Exception $e) {
498
            $hasException = true;
499
        }
500
        $this->assertTrue($hasException);
501
    }
502
503
    /**
504
     * Builds the array of options for the {@link Transactional} annotation.
505
     *
506
     * @param integer $policy Policy.
507
     * @param string[] $noRollbackExceptions Array of exception class names.
508
     * @return array
509
     */
510
    private function buildTransactionalOptions($policy = null, array $noRollbackExceptions = null)
511
    {
512
        $options = array();
513
        if ($policy !== null) {
514
            $options['policy'] = $policy;
515
        }
516
        if ($noRollbackExceptions !== null) {
517
            $options['noRollbackExceptions'] = $noRollbackExceptions;
518
        }
519
        return $options;
520
    }
521
}
522