1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Logger\Monolog;
22
23use Kafka\Exception;
24use Monolog\Logger;
25
26/**
27 * @covers \MediaWiki\Logger\Monolog\KafkaHandler
28 */
29class KafkaHandlerTest extends \MediaWikiUnitTestCase {
30
31	protected function setUp(): void {
32		parent::setUp();
33		if ( !class_exists( \Monolog\Handler\AbstractProcessingHandler::class )
34			|| !class_exists( \Kafka\Produce::class )
35		) {
36			$this->markTestSkipped( 'Monolog and Kafka are required for the KafkaHandlerTest' );
37		}
38	}
39
40	public function topicNamingProvider() {
41		return [
42			[ [], 'monolog_foo' ],
43			[ [ 'alias' => [ 'foo' => 'bar' ] ], 'bar' ]
44		];
45	}
46
47	/**
48	 * @dataProvider topicNamingProvider
49	 */
50	public function testTopicNaming( $options, $expect ) {
51		$produce = $this->getMockBuilder( \Kafka\Produce::class )
52			->disableOriginalConstructor()
53			->getMock();
54		$produce->method( 'getAvailablePartitions' )
55			->willReturn( [ 'A' ] );
56		$produce->expects( $this->once() )
57			->method( 'setMessages' )
58			->with( $expect, $this->anything(), $this->anything() );
59		$produce->method( 'send' )
60			->willReturn( true );
61
62		$handler = new KafkaHandler( $produce, $options );
63		$handler->handle( [
64			'channel' => 'foo',
65			'level' => Logger::EMERGENCY,
66			'extra' => [],
67			'context' => [],
68		] );
69	}
70
71	public function swallowsExceptionsWhenRequested() {
72		return [
73			// defaults to false
74			[ [], true ],
75			// also try false explicitly
76			[ [ 'swallowExceptions' => false ], true ],
77			// turn it on
78			[ [ 'swallowExceptions' => true ], false ],
79		];
80	}
81
82	/**
83	 * @dataProvider swallowsExceptionsWhenRequested
84	 */
85	public function testGetAvailablePartitionsException( $options, $expectException ) {
86		$produce = $this->getMockBuilder( \Kafka\Produce::class )
87			->disableOriginalConstructor()
88			->getMock();
89		$produce->method( 'getAvailablePartitions' )
90			->will( $this->throwException( new Exception ) );
91		$produce->method( 'send' )
92			->willReturn( true );
93
94		if ( $expectException ) {
95			$this->expectException( Exception::class );
96		}
97
98		$handler = new KafkaHandler( $produce, $options );
99		$handler->handle( [
100			'channel' => 'foo',
101			'level' => Logger::EMERGENCY,
102			'extra' => [],
103			'context' => [],
104		] );
105
106		if ( !$expectException ) {
107			$this->assertTrue( true, 'no exception was thrown' );
108		}
109	}
110
111	/**
112	 * @dataProvider swallowsExceptionsWhenRequested
113	 */
114	public function testSendException( $options, $expectException ) {
115		$produce = $this->getMockBuilder( \Kafka\Produce::class )
116			->disableOriginalConstructor()
117			->getMock();
118		$produce->method( 'getAvailablePartitions' )
119			->willReturn( [ 'A' ] );
120		$produce->method( 'send' )
121			->will( $this->throwException( new Exception ) );
122
123		if ( $expectException ) {
124			$this->expectException( Exception::class );
125		}
126
127		$handler = new KafkaHandler( $produce, $options );
128		$handler->handle( [
129			'channel' => 'foo',
130			'level' => Logger::EMERGENCY,
131			'extra' => [],
132			'context' => [],
133		] );
134
135		if ( !$expectException ) {
136			$this->assertTrue( true, 'no exception was thrown' );
137		}
138	}
139
140	public function testHandlesNullFormatterResult() {
141		$produce = $this->getMockBuilder( \Kafka\Produce::class )
142			->disableOriginalConstructor()
143			->getMock();
144		$produce->method( 'getAvailablePartitions' )
145			->willReturn( [ 'A' ] );
146		$produce->expects( $this->exactly( 2 ) )
147			->method( 'setMessages' )
148			->will( $this->onConsecutiveCalls(
149				[ $this->anything(), $this->anything(), [ 'words' ] ],
150				[ $this->anything(), $this->anything(), [ 'lines' ] ]
151			) );
152		$produce->method( 'send' )
153			->willReturn( true );
154
155		$formatter = $this->createMock( \Monolog\Formatter\FormatterInterface::class );
156		$formatter->method( 'format' )
157			->will( $this->onConsecutiveCalls( 'words', null, 'lines' ) );
158
159		$handler = new KafkaHandler( $produce, [] );
160		$handler->setFormatter( $formatter );
161		for ( $i = 0; $i < 3; ++$i ) {
162			$handler->handle( [
163				'channel' => 'foo',
164				'level' => Logger::EMERGENCY,
165				'extra' => [],
166				'context' => [],
167			] );
168		}
169	}
170
171	public function testBatchHandlesNullFormatterResult() {
172		$produce = $this->getMockBuilder( \Kafka\Produce::class )
173			->disableOriginalConstructor()
174			->getMock();
175		$produce->method( 'getAvailablePartitions' )
176			->willReturn( [ 'A' ] );
177		$produce->expects( $this->once() )
178			->method( 'setMessages' )
179			->with( $this->anything(), $this->anything(), [ 'words', 'lines' ] );
180		$produce->method( 'send' )
181			->willReturn( true );
182
183		$formatter = $this->createMock( \Monolog\Formatter\FormatterInterface::class );
184		$formatter->method( 'format' )
185			->will( $this->onConsecutiveCalls( 'words', null, 'lines' ) );
186
187		$handler = new KafkaHandler( $produce, [] );
188		$handler->setFormatter( $formatter );
189		$handler->handleBatch( [
190			[
191				'channel' => 'foo',
192				'level' => Logger::EMERGENCY,
193				'extra' => [],
194				'context' => [],
195			],
196			[
197				'channel' => 'foo',
198				'level' => Logger::EMERGENCY,
199				'extra' => [],
200				'context' => [],
201			],
202			[
203				'channel' => 'foo',
204				'level' => Logger::EMERGENCY,
205				'extra' => [],
206				'context' => [],
207			],
208		] );
209	}
210}
211