SlackLoggerTest   A
last analyzed

Complexity

Total Complexity 13

Size/Duplication

Total Lines 311
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 13
eloc 162
dl 0
loc 311
rs 10
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A testOptionsAreOptional() 0 7 1
A testConstructorThrowsAnExceptionIfTheWebhookUrlIsNotAValidUrl() 0 6 1
A testAllLogMethodsCallLog() 0 22 1
A providesInvalidWebhookUrls() 0 6 1
A testConstructor() 0 10 1
A testLogThrowsAnExceptionIfItFailedToCommunicateWithSlack() 0 7 1
A providesLogLevelsThatWillNotTriggerAction() 0 23 1
A testLogWillSendAMessageToSlackIfTheLogLevelIsHighEnough() 0 22 1
A providesLogMethodArguments() 0 42 1
A testLogWillNotSendAMessageToSlackIfTheLogLevelIsTooLow() 0 22 1
A testLogWillSendAMessageToSlack() 0 53 1
A providesLogLevelsThatWillTriggerAction() 0 47 1
A testIsAPsrAbstractlogger() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace DanBettles\Defence\Tests\Logger;
6
7
use DanBettles\Defence\Logger\SlackLogger;
8
use DanBettles\Defence\Tests\AbstractTestCase;
9
use Exception;
10
use InvalidArgumentException;
11
use Psr\Log\AbstractLogger;
12
use Psr\Log\LogLevel;
13
14
use function json_encode;
15
16
/**
17
 * @phpstan-import-type Context from \DanBettles\Defence\Logger\NullLogger
18
 */
19
class SlackLoggerTest extends AbstractTestCase
20
{
21
    public function testIsAPsrAbstractlogger(): void
22
    {
23
        $this->assertSubclassOf(AbstractLogger::class, SlackLogger::class);
24
    }
25
26
    public function testConstructor(): void
27
    {
28
        $logger = new SlackLogger('https://hooks.slack.com/services/foo/bar/baz', ['foo' => 'bar']);
29
30
        $this->assertSame('https://hooks.slack.com/services/foo/bar/baz', $logger->getWebhookUrl());
31
32
        $this->assertSame([
33
            'min_log_level' => LogLevel::DEBUG,
34
            'foo' => 'bar',
35
        ], $logger->getOptions());
36
    }
37
38
    /** @return array<mixed[]> */
39
    public function providesInvalidWebhookUrls(): array
40
    {
41
        return [[
42
            '',
43
        ], [
44
            'foo'
45
        ]];
46
    }
47
48
    /** @dataProvider providesInvalidWebhookUrls */
49
    public function testConstructorThrowsAnExceptionIfTheWebhookUrlIsNotAValidUrl(string $invalidWebhookUrl): void
50
    {
51
        $this->expectException(InvalidArgumentException::class);
52
        $this->expectExceptionMessage('The webhook URL is not a valid URL.');
53
54
        new SlackLogger($invalidWebhookUrl, []);
55
    }
56
57
    public function testOptionsAreOptional(): void
58
    {
59
        $logger = new SlackLogger('https://hooks.slack.com/services/foo/bar/baz');
60
61
        $this->assertSame([
62
            'min_log_level' => LogLevel::DEBUG,
63
        ], $logger->getOptions());
64
    }
65
66
    /** @return array<mixed[]> */
67
    public function providesLogMethodArguments(): array
68
    {
69
        return [[
70
            LogLevel::EMERGENCY,
71
            'message' => 'System is unusable.',
72
            'context' => ['foo' => 'bar'],
73
            'emergency',
74
        ], [
75
            LogLevel::ALERT,
76
            'message' => 'Action must be taken immediately.',
77
            'context' => ['baz' => 'qux'],
78
            'alert',
79
        ], [
80
            LogLevel::CRITICAL,
81
            'message' => 'Critical condition.',
82
            'context' => ['quux' => 'quuz'],
83
            'critical',
84
        ], [
85
            LogLevel::ERROR,
86
            'message' => 'Runtime error that does not require immediate action.',
87
            'context' => ['corge' => 'grault'],
88
            'error',
89
        ], [
90
            LogLevel::WARNING,
91
            'message' => 'Exceptional occurrence that is not an error.',
92
            'context' => ['garply' => 'waldo'],
93
            'warning',
94
        ], [
95
            LogLevel::NOTICE,
96
            'message' => 'Normal but significant event.',
97
            'context' => ['fred' => 'plugh'],
98
            'notice',
99
        ], [
100
            LogLevel::INFO,
101
            'message' => 'Interesting event.',
102
            'context' => ['xyzzy' => 'thud'],
103
            'info',
104
        ], [
105
            LogLevel::DEBUG,
106
            'message' => 'Detailed debug information.',
107
            'context' => ['wibble' => 'wobble'],
108
            'debug',
109
        ]];
110
    }
111
112
    /**
113
     * @dataProvider providesLogMethodArguments
114
     * @phpstan-param Context $context
115
     */
116
    public function testAllLogMethodsCallLog(
117
        string $level,
118
        string $message,
119
        array $context,
120
        string $loggerMethodName
121
    ): void {
122
        $slackLoggerMock = $this
123
            ->getMockBuilder(SlackLogger::class)
124
            ->setConstructorArgs([
125
                'https://hooks.slack.com/services/foo/bar/baz',
126
            ])
127
            ->onlyMethods(['log'])
128
            ->getMock()
129
        ;
130
131
        $slackLoggerMock
132
            ->expects($this->once())
133
            ->method('log')
134
            ->with($level, $message, $context)
135
        ;
136
137
        $slackLoggerMock->{$loggerMethodName}($message, $context);
138
    }
139
140
    public function testLogWillSendAMessageToSlack(): void
141
    {
142
        $level = LogLevel::ERROR;
143
        $message = 'Runtime error that does not require immediate action.';
144
        $context = ['xyzzy' => 'thud'];
145
146
        $expectedJson = json_encode([
147
            'text' => ":bangbang: Defence handled suspicious request",
148
            'blocks' => [
149
                [
150
                    'type' => 'section',
151
                    'text' => [
152
                        'type' => 'mrkdwn',
153
                        'text' => 'Handled suspicious request.',
154
                    ],
155
                ],
156
                [
157
                    'type' => 'section',
158
                    'text' => [
159
                        'type' => 'mrkdwn',
160
                        'text' => '*:bangbang: Error: Runtime error that does not require immediate action.*',
161
                    ],
162
                ],
163
                [
164
                    'type' => 'context',
165
                    'elements' => [
166
                        [
167
                            'type' => 'mrkdwn',
168
                            'text' => "xyzzy: thud",
169
                        ],
170
                    ],
171
                ],
172
            ],
173
        ]);
174
175
        $slackLoggerMock = $this
176
            ->getMockBuilder(SlackLogger::class)
177
            ->setConstructorArgs([
178
                'https://hooks.slack.com/services/foo/bar/baz',
179
            ])
180
            ->onlyMethods(['sendJsonToSlack'])
181
            ->getMock()
182
        ;
183
184
        $slackLoggerMock
185
            ->expects($this->once())
186
            ->method('sendJsonToSlack')
187
            ->with($expectedJson)
188
        ;
189
190
        /** @var SlackLogger $slackLoggerMock */
191
192
        $slackLoggerMock->log($level, $message, $context);
193
    }
194
195
    public function testLogThrowsAnExceptionIfItFailedToCommunicateWithSlack(): void
196
    {
197
        $this->expectException(Exception::class);
198
        $this->expectExceptionMessage('Failed to send the JSON to Slack.');
199
200
        $logger = new SlackLogger('http://localhost');
201
        $logger->log(LogLevel::ERROR, 'bar');
202
    }
203
204
    /** @return array<mixed[]> */
205
    public function providesLogLevelsThatWillTriggerAction(): array
206
    {
207
        return [[
208
            'logLevel' => LogLevel::DEBUG,
209
            'minLogLevel' => LogLevel::DEBUG,
210
        ], [
211
            'logLevel' => LogLevel::INFO,
212
            'minLogLevel' => LogLevel::DEBUG,
213
        ], [
214
            'logLevel' => LogLevel::INFO,
215
            'minLogLevel' => LogLevel::INFO,
216
        ], [
217
            'logLevel' => LogLevel::NOTICE,
218
            'minLogLevel' => LogLevel::INFO,
219
        ], [
220
            'logLevel' => LogLevel::NOTICE,
221
            'minLogLevel' => LogLevel::NOTICE,
222
        ], [
223
            'logLevel' => LogLevel::WARNING,
224
            'minLogLevel' => LogLevel::NOTICE,
225
        ], [
226
            'logLevel' => LogLevel::WARNING,
227
            'minLogLevel' => LogLevel::WARNING,
228
        ], [
229
            'logLevel' => LogLevel::ERROR,
230
            'minLogLevel' => LogLevel::WARNING,
231
        ], [
232
            'logLevel' => LogLevel::ERROR,
233
            'minLogLevel' => LogLevel::ERROR,
234
        ], [
235
            'logLevel' => LogLevel::CRITICAL,
236
            'minLogLevel' => LogLevel::ERROR,
237
        ], [
238
            'logLevel' => LogLevel::CRITICAL,
239
            'minLogLevel' => LogLevel::CRITICAL,
240
        ], [
241
            'logLevel' => LogLevel::ALERT,
242
            'minLogLevel' => LogLevel::CRITICAL,
243
        ], [
244
            'logLevel' => LogLevel::ALERT,
245
            'minLogLevel' => LogLevel::ALERT,
246
        ], [
247
            'logLevel' => LogLevel::EMERGENCY,
248
            'minLogLevel' => LogLevel::ALERT,
249
        ], [
250
            'logLevel' => LogLevel::EMERGENCY,
251
            'minLogLevel' => LogLevel::EMERGENCY,
252
        ]];
253
    }
254
255
    /** @dataProvider providesLogLevelsThatWillTriggerAction */
256
    public function testLogWillSendAMessageToSlackIfTheLogLevelIsHighEnough(
257
        string $logLevel,
258
        string $minLogLevel
259
    ): void {
260
        $slackLoggerMock = $this
261
            ->getMockBuilder(SlackLogger::class)
262
            ->setConstructorArgs([
263
                'https://hooks.slack.com/services/foo/bar/baz',
264
                ['min_log_level' => $minLogLevel],
265
            ])
266
            ->onlyMethods(['sendJsonToSlack'])
267
            ->getMock()
268
        ;
269
270
        $slackLoggerMock
271
            ->expects($this->once())
272
            ->method('sendJsonToSlack')
273
        ;
274
275
        /** @var SlackLogger $slackLoggerMock */
276
277
        $slackLoggerMock->log($logLevel, 'Foo');
278
    }
279
280
    /** @return array<mixed[]> */
281
    public function providesLogLevelsThatWillNotTriggerAction(): array
282
    {
283
        return [[
284
            'logLevel' => LogLevel::DEBUG,
285
            'minLogLevel' => LogLevel::INFO,
286
        ], [
287
            'logLevel' => LogLevel::INFO,
288
            'minLogLevel' => LogLevel::NOTICE,
289
        ], [
290
            'logLevel' => LogLevel::NOTICE,
291
            'minLogLevel' => LogLevel::WARNING,
292
        ], [
293
            'logLevel' => LogLevel::WARNING,
294
            'minLogLevel' => LogLevel::ERROR,
295
        ], [
296
            'logLevel' => LogLevel::ERROR,
297
            'minLogLevel' => LogLevel::CRITICAL,
298
        ], [
299
            'logLevel' => LogLevel::CRITICAL,
300
            'minLogLevel' => LogLevel::ALERT,
301
        ], [
302
            'logLevel' => LogLevel::ALERT,
303
            'minLogLevel' => LogLevel::EMERGENCY,
304
        ]];
305
    }
306
307
    /** @dataProvider providesLogLevelsThatWillNotTriggerAction */
308
    public function testLogWillNotSendAMessageToSlackIfTheLogLevelIsTooLow(
309
        string $logLevel,
310
        string $minLogLevel
311
    ): void {
312
        $slackLoggerMock = $this
313
            ->getMockBuilder(SlackLogger::class)
314
            ->setConstructorArgs([
315
                'https://hooks.slack.com/services/foo/bar/baz',
316
                ['min_log_level' => $minLogLevel],
317
            ])
318
            ->onlyMethods(['sendJsonToSlack'])
319
            ->getMock()
320
        ;
321
322
        $slackLoggerMock
323
            ->expects($this->never())
324
            ->method('sendJsonToSlack')
325
        ;
326
327
        /** @var SlackLogger $slackLoggerMock */
328
329
        $slackLoggerMock->log($logLevel, 'Foo');
330
    }
331
}
332