Failed Conditions
Branch master (8bf861)
by Arnold
02:57
created

ErrorHandlerTest::testInvokeLog()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 28
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 28
rs 8.8571
c 0
b 0
f 0
cc 1
eloc 19
nc 1
nop 0
1
<?php
2
3
namespace Jasny;
4
5
use Jasny\ErrorHandler;
6
use Psr\Http\Message\ServerRequestInterface;
7
use Psr\Http\Message\ResponseInterface;
8
use Psr\Http\Message\StreamInterface;
9
use Psr\Log\LoggerInterface;
10
use Psr\Log\LogLevel;
11
12
use PHPUnit_Framework_MockObject_MockObject as MockObject;
13
use PHPUnit_Framework_MockObject_Matcher_InvokedCount as InvokedCount;
14
15
/**
16
 * @covers Jasny\ErrorHandler
17
 */
18
class ErrorHandlerTest extends \PHPUnit_Framework_TestCase
19
{
20
    /**
21
     * @var ErrorHandler|MockObject
22
     */
23
    protected $errorHandler;
24
    
25
    public function setUp()
26
    {
27
        $this->errorHandler = $this->getMockBuilder(ErrorHandler::class)
28
            ->setMethods(['errorReporting', 'errorGetLast', 'setErrorHandler', 'registerShutdownFunction',
29
                'clearOutputBuffer'])
30
            ->getMock();
31
    }
32
    
33
    /**
34
     * Test invoke with invalid 'next' param
35
     * 
36
     * @expectedException \InvalidArgumentException
37
     */
38
    public function testInvokeInvalidNext()
39
    {
40
        $request = $this->createMock(ServerRequestInterface::class);
41
        $response = $this->createMock(ResponseInterface::class);
42
        
43
        $errorHandler = $this->errorHandler;
44
        
45
        $errorHandler($request, $response, 'not callable');
46
    }
47
48
    /**
49
     * Test case when there is no error
50
     */
51
    public function testInvokeNoError()
52
    {
53
        $request = $this->createMock(ServerRequestInterface::class);
54
        $response = $this->createMock(ResponseInterface::class);
55
        $finalResponse = $this->createMock(ResponseInterface::class);
56
57
        $next = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock();
58
        $next->expects($this->once())->method('__invoke')
59
            ->with($request, $response)
60
            ->willReturn($finalResponse);
61
        
62
        $errorHandler = $this->errorHandler;
63
64
        $result = $errorHandler($request, $response, $next);        
65
66
        $this->assertSame($finalResponse, $result);
67
    }
68
    
69
    /**
70
     * Test that Exception in 'next' callback is caught
71
     */
72
    public function testInvokeCatchException()
73
    {
74
        $request = $this->createMock(ServerRequestInterface::class);
75
        $response = $this->createMock(ResponseInterface::class);
76
        $errorResponse = $this->createMock(ResponseInterface::class);
77
        $stream = $this->createMock(StreamInterface::class);
78
        
79
        $exception = $this->createMock(\Exception::class);
80
81
        $stream->expects($this->once())->method('write')->with('Unexpected error');
82
        $response->expects($this->once())->method('withStatus')->with(500)->willReturn($errorResponse);
83
84
        $errorResponse->expects($this->once())->method('getBody')->willReturn($stream);
85
        
86
        $next = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock();
87
        $next->expects($this->once())->method('__invoke')
88
            ->with($request, $response)
89
            ->willThrowException($exception);
90
        
91
        $errorHandler = $this->errorHandler;
92
        
93
        $result = $errorHandler($request, $response, $next);
94
95
        $this->assertSame($errorResponse, $result);
96
        $this->assertSame($exception, $errorHandler->getError());
97
    }
98
    
99
    /**
100
     * Test that an error in 'next' callback is caught
101
     */
102
    public function testInvokeCatchError()
103
    {
104
        if (!class_exists('Error')) {
105
            $this->markTestSkipped(PHP_VERSION . " doesn't throw errors yet");
106
        }
107
        
108
        $request = $this->createMock(ServerRequestInterface::class);
109
        $response = $this->createMock(ResponseInterface::class);
110
        $errorResponse = $this->createMock(ResponseInterface::class);
111
        $stream = $this->createMock(StreamInterface::class);
112
        
113
        $stream->expects($this->once())->method('write')->with('Unexpected error');
114
        $response->expects($this->once())->method('withStatus')->with(500)->willReturn($errorResponse);
115
116
        $errorResponse->expects($this->once())->method('getBody')->willReturn($stream);
117
        
118
        $next = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock();
119
        $next->expects($this->once())->method('__invoke')
120
            ->with($request, $response)
121
            ->willReturnCallback(function() {
122
                \this_function_does_not_exist();
123
            });
124
        
125
        $errorHandler = $this->errorHandler;
126
        
127
        $result = $errorHandler($request, $response, $next);
128
129
        $this->assertSame($errorResponse, $result);
130
        
131
        $error = $errorHandler->getError();
132
        $this->assertEquals("Call to undefined function this_function_does_not_exist()", $error->getMessage());
133
    }
134
    
135
    
136
    public function testSetLogger()
137
    {
138
        $logger = $this->createMock(LoggerInterface::class);
139
        
140
        $errorHandler = $this->errorHandler;
141
        $errorHandler->setLogger($logger);
142
        
143
        $this->assertSame($logger, $errorHandler->getLogger());
144
    }
145
    
146
    
147
    public function testInvokeLog()
148
    {
149
        $request = $this->createMock(ServerRequestInterface::class);
150
        $response = $this->createMock(ResponseInterface::class);
151
        $stream = $this->createMock(StreamInterface::class);
152
        
153
        $response->method('withStatus')->willReturnSelf();
154
        $response->method('getBody')->willReturn($stream);
155
        
156
        $exception = $this->createMock(\Exception::class);
157
        
158
        $message = $this->stringStartsWith('Uncaught Exception ' . get_class($exception));
159
        $context = ['exception' => $exception];
160
        
161
        $logger = $this->createMock(LoggerInterface::class);
162
        $logger->expects($this->once())->method('log')
163
            ->with(LogLevel::ERROR, $message, $context);
164
        
165
        $errorHandler = $this->errorHandler;
166
        $errorHandler->setLogger($logger);
167
        
168
        $next = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock();
169
        $next->expects($this->once())->method('__invoke')
170
            ->with($request, $response)
171
            ->willThrowException($exception);
172
        
173
        $errorHandler($request, $response, $next);
174
    }
175
    
176
    public function errorProvider()
177
    {
178
        return [
179
            [E_ERROR, LogLevel::ERROR, 'Fatal error'],
180
            [E_USER_ERROR, LogLevel::ERROR, 'Fatal error'],
181
            [E_RECOVERABLE_ERROR, LogLevel::ERROR, 'Fatal error'],
182
            [E_WARNING, LogLevel::WARNING, 'Warning'],
183
            [E_USER_WARNING, LogLevel::WARNING, 'Warning'],
184
            [E_PARSE, LogLevel::CRITICAL, 'Parse error'],
185
            [E_NOTICE, LogLevel::NOTICE, 'Notice'],
186
            [E_USER_NOTICE, LogLevel::NOTICE, 'Notice'],
187
            [E_CORE_ERROR, LogLevel::CRITICAL, 'Core error'],
188
            [E_CORE_WARNING, LogLevel::WARNING, 'Core warning'],
189
            [E_COMPILE_ERROR, LogLevel::CRITICAL, 'Compile error'],
190
            [E_COMPILE_WARNING, LogLevel::WARNING, 'Compile warning'],
191
            [E_STRICT, LogLevel::INFO, 'Strict standards'],
192
            [E_DEPRECATED, LogLevel::INFO, 'Deprecated'],
193
            [E_USER_DEPRECATED, LogLevel::INFO, 'Deprecated'],
194
            [99999999, LogLevel::ERROR, 'Unknown error']
195
        ];
196
    }
197
    
198
    /**
199
     * @dataProvider errorProvider
200
     * 
201
     * @param int    $code
202
     * @param string $level
203
     * @param string $type
204
     */
205
    public function testLogError($code, $level, $type)
206
    {
207
        $error = new \ErrorException("no good", 0, $code, "foo.php", 42);
208
        $context = ['error' => $error, 'code' => $code, 'message' => "no good", 'file' => 'foo.php', 'line' => 42];
209
        
210
        $logger = $this->createMock(LoggerInterface::class);
211
        $logger->expects($this->once())->method('log')
212
            ->with($level, "$type: no good at foo.php line 42", $context);
213
        
214
        $errorHandler = $this->errorHandler;
215
        $errorHandler->setLogger($logger);
216
217
        $errorHandler->log($error);
218
    }
219
    
220
    public function testLogException()
221
    {
222
        $exception = $this->createMock(\Exception::class);
223
        
224
        $message = $this->stringStartsWith('Uncaught Exception ' . get_class($exception));
225
        $context = ['exception' => $exception];
226
        
227
        $logger = $this->createMock(LoggerInterface::class);
228
        $logger->expects($this->once())->method('log')
229
            ->with(LogLevel::ERROR, $message, $context);
230
        
231
        $errorHandler = $this->errorHandler;
232
        $errorHandler->setLogger($logger);
233
234
        $errorHandler->log($exception);
235
    }
236
    
237
    public function testLogString()
238
    {
239
        $logger = $this->createMock(LoggerInterface::class);
240
        $logger->expects($this->once())->method('log')->with(LogLevel::WARNING, "Unable to log a string");
241
        
242
        $errorHandler = $this->errorHandler;
243
        $errorHandler->setLogger($logger);
244
245
        $errorHandler->log('foo');
246
    }
247
    
248
    public function testLogObject()
249
    {
250
        $logger = $this->createMock(LoggerInterface::class);
251
        $logger->expects($this->once())->method('log')->with(LogLevel::WARNING, "Unable to log a stdClass object");
252
        
253
        $errorHandler = $this->errorHandler;
254
        $errorHandler->setLogger($logger);
255
256
        $errorHandler->log(new \stdClass());
257
    }
258
    
259
    
260
    public function testConverErrorsToExceptions()
261
    {
262
        $errorHandler = $this->errorHandler;
263
264
        $errorHandler->expects($this->once())->method('setErrorHandler')
265
            ->with([$errorHandler, 'handleError'])
266
            ->willReturn(null);
267
        
268
        $errorHandler->converErrorsToExceptions();
269
        
270
        $this->assertSame(0, $errorHandler->getLoggedErrorTypes());
271
    }
272
    
273
    
274
    public function alsoLogProvider()
275
    {
276
        return [
277
            [E_ALL, $this->once(), $this->once()],
278
            [E_WARNING | E_USER_WARNING, $this->once(), $this->never()],
279
            [E_NOTICE | E_USER_NOTICE, $this->once(), $this->never()],
280
            [E_STRICT, $this->once(), $this->never()],
281
            [E_DEPRECATED | E_USER_DEPRECATED, $this->once(), $this->never()],
282
            [E_PARSE, $this->never(), $this->once()],
283
            [E_ERROR, $this->never(), $this->once()],
284
            [E_ERROR | E_USER_ERROR, $this->never(), $this->once()],
285
            [E_RECOVERABLE_ERROR | E_USER_ERROR, $this->never(), $this->never()]
286
        ];
287
    }
288
    
289
    /**
290
     * @dataProvider alsoLogProvider
291
     * 
292
     * @param int          $code
293
     * @param InvokedCount $expectErrorHandler
294
     * @param InvokedCount $expectShutdownFunction
295
     */
296
    public function testAlsoLog($code, InvokedCount $expectErrorHandler, InvokedCount $expectShutdownFunction)
297
    {
298
        $errorHandler = $this->errorHandler;
299
        
300
        $errorHandler->expects($expectErrorHandler)->method('setErrorHandler')
301
            ->with([$errorHandler, 'handleError'])
302
            ->willReturn(null);
303
        
304
        $errorHandler->expects($expectShutdownFunction)->method('registerShutdownFunction')
305
            ->with([$errorHandler, 'shutdownFunction']);
306
        
307
        $errorHandler->alsoLog($code);
308
        
309
        $this->assertSame($code, $errorHandler->getLoggedErrorTypes());
310
    }
311
    
312
    public function testAlsoLogCombine()
313
    {
314
        $errorHandler = $this->errorHandler;
315
        
316
        $errorHandler->alsoLog(E_NOTICE | E_USER_NOTICE);
317
        $errorHandler->alsoLog(E_WARNING | E_USER_WARNING);
318
        $errorHandler->alsoLog(E_ERROR);
319
        $errorHandler->alsoLog(E_PARSE);
320
        
321
        $expected = E_NOTICE | E_USER_NOTICE | E_WARNING | E_USER_WARNING | E_ERROR | E_PARSE;
322
        $this->assertSame($expected, $errorHandler->getLoggedErrorTypes());
323
    }
324
325
    public function testInitErrorHandler()
326
    {
327
        $errorHandler = $this->errorHandler;
328
        
329
        $callback = function() {};
330
        
331
        $errorHandler->expects($this->once())->method('setErrorHandler')
332
            ->with([$errorHandler, 'handleError'])
333
            ->willReturn($callback);
334
        
335
        $errorHandler->alsoLog(E_WARNING);
336
        
337
        // Subsequent calls should have no effect
338
        $errorHandler->alsoLog(E_WARNING);
339
        
340
        $this->assertSame($callback, $errorHandler->getChainedErrorHandler());
341
    }
342
    
343
    public function testInitShutdownFunction()
344
    {
345
        $errorHandler = $this->errorHandler;
346
347
        $errorHandler->expects($this->once())->method('registerShutdownFunction')
348
            ->with([$errorHandler, 'shutdownFunction']);
349
        
350
        $errorHandler->alsoLog(E_PARSE);
351
        
352
        // Subsequent calls should have no effect
353
        $errorHandler->alsoLog(E_PARSE);
354
        
355
        $this->assertAttributeNotEmpty('reservedMemory', $errorHandler);
356
    }
357
    
358
359
    public function errorHandlerProvider()
360
    {
361
        return [
362
            [0, E_WARNING, $this->never(), false],
363
            
364
            [E_ALL, E_RECOVERABLE_ERROR, $this->once(), true],
365
            [E_ALL, E_WARNING, $this->once(), false],
366
            [E_ALL, E_NOTICE, $this->once(), false],
367
            
368
            [E_WARNING | E_USER_WARNING, E_RECOVERABLE_ERROR, $this->never(), true],
369
            [E_WARNING | E_USER_WARNING, E_WARNING, $this->once(), false],
370
            [E_WARNING | E_USER_WARNING, E_NOTICE, $this->never(), false],
371
            
372
            [E_STRICT, E_RECOVERABLE_ERROR, $this->never(), true],
373
            [E_STRICT, E_STRICT, $this->once(), false],
374
            
375
            [E_RECOVERABLE_ERROR | E_USER_ERROR, E_RECOVERABLE_ERROR, $this->once(), true],
376
            [E_RECOVERABLE_ERROR | E_USER_ERROR, E_WARNING, $this->never(), false],
377
            [E_RECOVERABLE_ERROR | E_USER_ERROR, E_NOTICE, $this->never(), false],
378
            [E_RECOVERABLE_ERROR | E_USER_ERROR, E_STRICT, $this->never(), false]
379
        ];
380
    }
381
    
382
    /**
383
     * @dataProvider errorHandlerProvider
384
     * 
385
     * @param int          $alsoLog
386
     * @param int          $code
387
     * @param InvokedCount $expectLog
388
     */
389
    public function testHandleErrorWithLogging($alsoLog, $code, InvokedCount $expectLog)
390
    {
391
        $logger = $this->createMock(LoggerInterface::class);
392
        $logger->expects($expectLog)->method('log')
393
            ->with($this->isType('string'), $this->stringEndsWith("no good at foo.php line 42"), $this->anything());
394
        
395
        $errorHandler = $this->errorHandler;
396
        $errorHandler->expects($this->once())->method('errorReporting')->willReturn(E_ALL | E_STRICT);
397
        
398
        $errorHandler->setLogger($logger);
399
        $errorHandler->alsoLog($alsoLog);
400
        
401
        $this->errorHandler->handleError($code, 'no good', 'foo.php', 42, []);
402
    }
403
    
404
    /**
405
     * @dataProvider errorHandlerProvider
406
     * 
407
     * @param int          $alsoLog          Ignored
408
     * @param int          $code
409
     * @param InvokedCount $expectLog       Ignored
410
     * @param boolean      $expectException
411
     */
412
    public function testHandleErrorWithConvertError($alsoLog, $code, InvokedCount $expectLog, $expectException)
413
    {
414
        $logger = $this->createMock(LoggerInterface::class);
415
        $logger->expects($this->never())->method('log');
416
        
417
        $errorHandler = $this->errorHandler;
418
        $errorHandler->expects($this->once())->method('errorReporting')->willReturn(E_ALL | E_STRICT);
419
        
420
        $errorHandler->setLogger($logger);
421
        
422
        $errorHandler->converErrorsToExceptions();
423
        
424
        try {
425
            $this->errorHandler->handleError($code, 'no good', 'foo.php', 42, []);
426
            
427
            if ($expectException) {
428
                $this->fail("Expected error exception wasn't thrown");
429
            }
430
        } catch (\ErrorException $exception) {
431
            if (!$expectException) {
432
                $this->fail("Error exception shouldn't have been thrown");
433
            }
434
            
435
            $this->assertInstanceOf(\ErrorException::class, $exception);
436
            $this->assertEquals('no good', $exception->getMessage());
437
            $this->assertEquals('foo.php', $exception->getFile());
438
            $this->assertEquals(42, $exception->getLine());
439
        }
440
    }
441
    
442
    public function shutdownFunctionProvider()
443
    {
444
        return [
445
            [E_ALL, E_PARSE, $this->once()],
446
            [E_ERROR | E_WARNING, E_PARSE, $this->never()],
447
            [E_ALL, E_ERROR, $this->once()],
448
            [E_ALL, E_USER_ERROR, $this->never()],
449
            [E_ALL, E_WARNING, $this->never()],
450
            [E_ALL, null, $this->never()]
451
        ];
452
    }
453
    
454
    /**
455
     * @dataProvider shutdownFunctionProvider
456
     * 
457
     * @param int          $alsoLog         Ignored
458
     * @param int          $code
459
     * @param InvokedCount $expectLog       Ignored
460
     */
461
    public function testShutdownFunction($alsoLog, $code, InvokedCount $expectLog)
462
    {
463
        $logger = $this->createMock(LoggerInterface::class);
464
        $logger->expects($expectLog)->method('log')
465
            ->with($this->isType('string'), $this->stringEndsWith("no good at foo.php line 42"), $this->anything());
466
        
467
        $errorHandler = $this->errorHandler;
468
        
469
        $error = [
470
            'type' => $code,
471
            'message' => 'no good',
472
            'file' => 'foo.php',
473
            'line' => 42
474
        ];
475
        
476
        $errorHandler->expects($this->once())->method('errorGetLast')
477
            ->willReturn($code ? $error : null);
478
        
479
        $errorHandler->setLogger($logger);
480
        $errorHandler->alsoLog($alsoLog);
481
        
482
        $this->assertAttributeNotEmpty('reservedMemory', $errorHandler);
483
        
484
        $errorHandler->shutdownFunction();
485
        
486
        $this->assertAttributeEmpty('reservedMemory', $errorHandler);
487
    }
488
    
489
    public function shutdownFunctionWithCallbackProvider()
490
    {
491
        return [
492
            [true, $this->once()],
493
            [false, $this->never()]
494
        ];
495
    }
496
    
497
    /**
498
     * @dataProvider shutdownFunctionWithCallbackProvider
499
     * 
500
     * @param boolean      $clearOutput
501
     * @param InvokedCount $expectClear
502
     */
503
    public function testShutdownFunctionWithCallback($clearOutput, InvokedCount $expectClear)
504
    {
505
        $errorHandler = $this->errorHandler;
506
        
507
        $error = [
508
            'type' => E_ERROR,
509
            'message' => 'no good',
510
            'file' => 'foo.php',
511
            'line' => 42
512
        ];
513
        
514
        $errorHandler->expects($this->once())->method('errorGetLast')->willReturn($error);
515
516
        $errorHandler->expects($expectClear)->method('clearOutputBuffer');
517
        
518
        $callback = $this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock();
519
        $callback->expects($this->once())->method('__invoke')
520
            ->with($this->callback(function($error){
521
                $this->assertInstanceOf(\ErrorException::class, $error);
522
                $this->assertEquals('no good', $error->getMessage());
523
                $this->assertEquals('foo.php', $error->getFile());
524
                $this->assertEquals(42, $error->getLine());
525
                
526
                return true;
527
            }));
528
        
529
        $errorHandler->onFatalError($callback, $clearOutput);
530
        
531
        $errorHandler->shutdownFunction();
532
    }
533
}
534