3 use MediaWiki\Context\RequestContext
;
4 use MediaWiki\Language\RawMessage
;
5 use MediaWiki\Message\Message
;
6 use MediaWiki\Parser\ParserOutput
;
7 use MediaWiki\Status\StatusFormatter
;
8 use MediaWiki\User\User
;
9 use Psr\Log\Test\TestLogger
;
10 use Wikimedia\Message\MessageValue
;
11 use Wikimedia\TestingAccessWrapper
;
14 * @covers \MediaWiki\Status\StatusFormatter
16 class StatusFormatterTest
extends MediaWikiLangTestCase
{
18 private ?TestLogger
$logger;
20 protected function setUp(): void
{
23 $this->logger
= new TestLogger();
26 protected function tearDown(): void
{
31 private function getFormatter( $lang = 'en' ) {
32 $localizer = new class() implements MessageLocalizer
{
35 public function msg( $key, ...$params ) {
36 return wfMessage( $key, ...$params )->inLanguage( $this->lang
);
40 $cache = $this->createNoOpMock( MessageCache
::class, [ 'parseWithPostprocessing' ] );
41 $cache->method( 'parseWithPostprocessing' )->willReturnCallback(
42 static function ( $text, ...$args ) {
43 $text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5
);
44 return new ParserOutput( "<p>" . trim( $text ) . "\n</p>" );
48 $localizer->lang
= $lang;
50 return new StatusFormatter( $localizer, $cache, $this->logger
);
54 * @dataProvider provideCleanParams
56 public function testCleanParams( $cleanCallback, $params, $expected, $unexpected ) {
57 $status = new StatusValue();
58 $status->warning( 'ok', ...$params );
60 $formatter = $this->getFormatter( 'qqx' );
61 $options = [ 'cleanCallback' => $cleanCallback ];
63 $wikitext = $formatter->getWikiText( $status, $options );
64 $this->assertStringContainsString( $expected, $wikitext );
65 $this->assertStringNotContainsString( $unexpected, $wikitext );
67 $html = $formatter->getHTML( $status, $options );
68 $this->assertStringContainsString( $expected, $html );
69 $this->assertStringNotContainsString( $unexpected, $html );
72 public static function provideCleanParams() {
73 $cleanCallback = static function ( $value ) {
78 [ false, [ 'secret' ], 'secret', 'xxx' ],
79 [ $cleanCallback, [ 'secret' ], 'xxx', 'secret' ],
84 * @dataProvider provideGetWikiTextAndHtml
86 public function testGetWikiText(
87 StatusValue
$status, $wikitext, $wrappedWikitext, $html, $wrappedHtml
89 $formatter = $this->getFormatter();
90 $this->assertEquals( $wikitext, $formatter->getWikiText( $status ) );
94 $formatter->getWikiText(
97 'shortContext' => 'wrap-short',
98 'longContext' => 'wrap-long',
106 * @dataProvider provideGetWikiTextAndHtml
108 public function testGetHtml(
114 ?
string $expectedWarning = null
116 $formatter = $this->getFormatter();
117 $this->assertEquals( $html, $formatter->getHTML( $status ) );
124 'shortContext' => 'wrap-short',
125 'longContext' => 'wrap-long',
131 if ( $expectedWarning !== null ) {
132 $this->assertTrue( $this->logger
->hasWarningThatContains( $expectedWarning ) );
134 $this->assertFalse( $this->logger
->hasWarningRecords() );
139 * @return array Array of arrays with values;
141 * 1 => expected string (with no context)
143 public static function provideGetWikiTextAndHtml() {
146 $testCases['GoodStatus'] = [
148 "Internal error: MediaWiki\Status\StatusFormatter::getWikiText called for a good result, this is incorrect ",
149 "(wrap-short: (internalerror_info: MediaWiki\Status\StatusFormatter::getWikiText called for a good result, " .
150 "this is incorrect ))",
151 "<p>Internal error: MediaWiki\Status\StatusFormatter::getWikiText called for a good result, this is incorrect\n</p>",
152 "<p>(wrap-short: (internalerror_info: MediaWiki\Status\StatusFormatter::getWikiText called for a good result, " .
153 "this is incorrect\n))\n</p>",
154 'MediaWiki\Status\StatusFormatter::getWikiText called for a good result, this is incorrect'
157 $status = new StatusValue();
158 $status->setOK( false );
159 $testCases['GoodButNoError'] = [
161 "Internal error: MediaWiki\Status\StatusFormatter::getWikiText: Invalid result object: no error text but not OK ",
162 "(wrap-short: (internalerror_info: MediaWiki\Status\StatusFormatter::getWikiText: Invalid result object: " .
163 "no error text but not OK ))",
164 "<p>Internal error: MediaWiki\Status\StatusFormatter::getWikiText: Invalid result object: no error text but not OK\n</p>",
165 "<p>(wrap-short: (internalerror_info: MediaWiki\Status\StatusFormatter::getWikiText: Invalid result object: " .
166 "no error text but not OK\n))\n</p>",
167 'MediaWiki\Status\StatusFormatter::getWikiText: Invalid result object: no error text but not OK'
170 $status = new StatusValue();
171 $status->warning( 'fooBar!' );
172 $testCases['1StringWarning'] = [
175 "(wrap-short: (fooBar!))",
176 "<p>⧼fooBar!⧽\n</p>",
177 "<p>(wrap-short: (fooBar!))\n</p>",
180 $status = new StatusValue();
181 $status->warning( 'fooBar!' );
182 $status->warning( 'fooBar2!' );
183 $testCases['2StringWarnings'] = [
185 "<ul>\n<li>\n⧼fooBar!⧽\n</li>\n<li>\n⧼fooBar2!⧽\n</li>\n</ul>\n",
186 "(wrap-long: <ul>\n<li>\n(fooBar!)\n</li>\n<li>\n(fooBar2!)\n</li>\n</ul>\n)",
187 "<p><ul>\n<li>\n⧼fooBar!⧽\n</li>\n<li>\n⧼fooBar2!⧽\n</li>\n</ul>\n</p>",
188 "<p>(wrap-long: <ul>\n<li>\n(fooBar!)\n</li>\n<li>\n(fooBar2!)\n</li>\n</ul>\n)\n</p>",
191 $status = new StatusValue();
192 $status->warning( new Message( 'fooBar!', [ 'foo', 'bar' ] ) );
193 $testCases['1MessageWarning'] = [
196 "(wrap-short: (fooBar!: foo, bar))",
197 "<p>⧼fooBar!⧽\n</p>",
198 "<p>(wrap-short: (fooBar!: foo, bar))\n</p>",
201 $status = new StatusValue();
202 $status->warning( new Message( 'fooBar!', [ 'foo', 'bar' ] ) );
203 $status->warning( new Message( 'fooBar2!' ) );
204 $testCases['2MessageWarnings'] = [
206 "<ul>\n<li>\n⧼fooBar!⧽\n</li>\n<li>\n⧼fooBar2!⧽\n</li>\n</ul>\n",
207 "(wrap-long: <ul>\n<li>\n(fooBar!: foo, bar)\n</li>\n<li>\n(fooBar2!)\n</li>\n</ul>\n)",
208 "<p><ul>\n<li>\n⧼fooBar!⧽\n</li>\n<li>\n⧼fooBar2!⧽\n</li>\n</ul>\n</p>",
209 "<p>(wrap-long: <ul>\n<li>\n(fooBar!: foo, bar)\n</li>\n<li>\n(fooBar2!)\n</li>\n</ul>\n)\n</p>",
215 private static function sanitizedMessageParams( Message
$message ) {
216 return array_map( static function ( $p ) {
217 return $p instanceof Message
219 'key' => $p->getKey(),
220 'params' => self
::sanitizedMessageParams( $p ),
221 'lang' => $p->getLanguage()->getCode(),
224 }, $message instanceof RawMessage ?
$message->getParamsOfRawMessage() : $message->getParams() );
227 private static function sanitizedMessageKey( Message
$message ) {
228 return $message instanceof RawMessage ?
$message->getTextOfRawMessage() : $message->getKey();
232 * @dataProvider provideGetMessage
234 public function testGetMessage(
239 ?
string $expectedWarning = null
241 $formatter = $this->getFormatter();
242 $message = $formatter->getMessage( $status, [ 'lang' => 'qqx' ] );
243 $this->assertInstanceOf( Message
::class, $message );
244 $this->assertEquals( $expectedParams, self
::sanitizedMessageParams( $message ),
245 'Message::getParams' );
246 $this->assertEquals( $expectedKey, self
::sanitizedMessageKey( $message ), 'Message::getKey' );
248 $message = $formatter->getMessage(
251 'shortContext' => 'wrapper-short',
252 'longContext' => 'wrapper-long',
255 $this->assertInstanceOf( Message
::class, $message );
256 $this->assertEquals( $expectedWrapper, $message->getKey(), 'Message::getKey with wrappers' );
257 $this->assertCount( 1, $message->getParams(), 'Message::getParams with wrappers' );
259 $message = $formatter->getMessage( $status, [ 'shortContext' => 'wrapper' ] );
260 $this->assertInstanceOf( Message
::class, $message );
261 $this->assertEquals( 'wrapper', $message->getKey(), 'Message::getKey with wrappers' );
262 $this->assertCount( 1, $message->getParams(), 'Message::getParams with wrappers' );
264 $message = $formatter->getMessage( $status, [ 'longContext' => 'wrapper' ] );
265 $this->assertInstanceOf( Message
::class, $message );
266 $this->assertEquals( 'wrapper', $message->getKey(), 'Message::getKey with wrappers' );
267 $this->assertCount( 1, $message->getParams(), 'Message::getParams with wrappers' );
269 if ( $expectedWarning !== null ) {
270 $this->assertTrue( $this->logger
->hasWarningThatContains( $expectedWarning ) );
272 $this->assertFalse( $this->logger
->hasWarningRecords() );
277 * @return array Array of arrays with values;
279 * 1 => expected Message parameters (with no context)
280 * 2 => expected Message key
282 public static function provideGetMessage() {
285 $testCases['GoodStatus'] = [
287 [ "MediaWiki\Status\StatusFormatter::getMessage called for a good result, this is incorrect " ],
288 'internalerror_info',
290 'MediaWiki\Status\StatusFormatter::getMessage called for a good result, this is incorrect'
293 $status = new StatusValue();
294 $status->setOK( false );
295 $testCases['GoodButNoError'] = [
297 [ "MediaWiki\Status\StatusFormatter::getMessage: Invalid result object: no error text but not OK " ],
298 'internalerror_info',
300 'MediaWiki\Status\StatusFormatter::getMessage: Invalid result object: no error text but not OK'
303 $status = new StatusValue();
304 $status->warning( 'fooBar!' );
305 $testCases['1StringWarning'] = [
312 $status = new StatusValue();
313 $status->warning( 'fooBar!' );
314 $status->warning( 'fooBar2!' );
315 $testCases[ '2StringWarnings' ] = [
318 [ 'key' => 'fooBar!', 'params' => [], 'lang' => 'qqx' ],
319 [ 'key' => 'fooBar2!', 'params' => [], 'lang' => 'qqx' ]
325 $status = new StatusValue();
326 $status->warning( new Message( 'fooBar!', [ 'foo', 'bar' ] ) );
327 $testCases['1MessageWarning'] = [
334 $status = new StatusValue();
335 $status->warning( new MessageValue( 'fooBar!', [ 'foo', 'bar' ] ) );
336 $status->warning( new MessageValue( 'fooBar2!' ) );
337 $testCases['2MessageWarnings'] = [
340 [ 'key' => 'fooBar!', 'params' => [ 'foo', 'bar' ], 'lang' => 'qqx' ],
341 [ 'key' => 'fooBar2!', 'params' => [], 'lang' => 'qqx' ]
351 * @dataProvider provideGetPsr3MessageAndContext
353 public function testGetPsr3MessageAndContext(
355 string $expectedMessage,
356 array $expectedContext
358 // set up a rawmessage_2 message, which is just like rawmessage but doesn't trigger
359 // the special-casing in StatusFormatter::getPsr3MessageAndContext
360 $this->setTemporaryHook( 'MessageCacheFetchOverrides', static function ( &$overrides ) {
361 $overrides['rawmessage_2'] = 'rawmessage';
364 $status = new StatusValue();
365 foreach ( $errors as $error ) {
366 $status->error( ...$error );
369 $formatter = $this->getFormatter();
371 [ $actualMessage, $actualContext ] = $formatter->getPsr3MessageAndContext( $status );
372 $this->assertSame( $expectedMessage, $actualMessage );
373 $this->assertSame( $expectedContext, $actualContext );
376 public static function provideGetPsr3MessageAndContext() {
378 // parameters to StatusValue::error() calls as array of arrays; expected message; expected context
381 "Internal error: MediaWiki\Status\StatusFormatter::getWikiText called for a good result, this is incorrect ",
384 // make sure that the rawmessage_2 hack works as the following tests rely on it
386 [ [ 'rawmessage_2', 'foo' ] ],
388 [ 'parameter1' => 'foo' ],
391 [ [ 'rawmessage_2', 'foo' ], [ 'rawmessage_2', 'bar' ] ],
392 "<ul>\n<li>\nfoo\n</li>\n<li>\nbar\n</li>\n</ul>\n",
395 'unknown subclass' => [
396 // phpcs:ignore Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore
397 [ [ new class( 'rawmessage_2', [ 'foo' ] ) extends Message
{} ] ],
401 'non-scalar parameter' => [
402 [ [ new Message( 'rawmessage_2', [ new Message( 'rawmessage_2', [ 'foo' ] ) ] ) ] ],
407 [ [ 'apiwarn-invalidtitle', 'foo' ] ],
408 '"{parameter1}" is not a valid title.',
409 [ 'parameter1' => 'foo' ],
411 'multiple parameters' => [
412 [ [ 'api-exception-trace', 'foo', 'bar', 'baz', 'boom' ] ],
413 "{parameter1} at {parameter2}({parameter3})\n{parameter4}",
414 [ 'parameter1' => 'foo', 'parameter2' => 'bar', 'parameter3' => 'baz', 'parameter4' => 'boom' ],
416 'formatted parameter' => [
417 [ [ 'apiwarn-invalidtitle', Message
::numParam( 1000000 ) ] ],
418 '"{parameter1}" is not a valid title.',
419 [ 'parameter1' => 1000000 ],
422 [ [ 'rawmessage', 'foo' ] ],
427 [ [ new RawMessage( 'foo $1 baz', [ 'bar' ] ) ] ],
428 'foo {parameter1} baz',
429 [ 'parameter1' => 'bar' ],
434 public function testGetErrorMessage() {
435 $formatter = $this->getFormatter();
436 /** @var StatusFormatter $formatter */
437 $formatter = TestingAccessWrapper
::newFromObject( $formatter );
441 $message = $formatter->getErrorMessage( [ $key, ...$params ] );
442 $this->assertInstanceOf( Message
::class, $message );
443 $this->assertEquals( $key, $message->getKey() );
444 $this->assertEquals( $params, $message->getParams() );
447 public function testGetErrorMessageComplexParam() {
448 $formatter = $this->getFormatter();
449 /** @var StatusFormatter $formatter */
450 $formatter = TestingAccessWrapper
::newFromObject( $formatter );
452 $params = [ 'bar', Message
::numParam( 5 ) ];
454 $message = $formatter->getErrorMessage( [ $key, ...$params ] );
455 $this->assertInstanceOf( Message
::class, $message );
456 $this->assertEquals( $key, $message->getKey() );
457 $this->assertEquals( $params, $message->getParams() );
460 public function testGetErrorMessageArray() {
461 $formatter = $this->getFormatter();
462 $formatter = TestingAccessWrapper
::newFromObject( $formatter );
466 /** @var Message[] $messageArray */
467 $messageArray = $formatter->getErrorMessageArray(
469 [ $key, ...$params ],
470 [ $key, ...$params ],
474 $this->assertIsArray( $messageArray );
475 $this->assertCount( 2, $messageArray );
476 foreach ( $messageArray as $message ) {
477 $this->assertInstanceOf( Message
::class, $message );
478 $this->assertEquals( $key, $message->getKey() );
479 $this->assertEquals( $params, $message->getParams() );
483 public function testUserLanguageNotLoaded() {
484 // Confirm that the user language is not loaded from the database when
485 // formatting an error in a specific language
486 $this->getServiceContainer()->disableService( 'UserOptionsLookup' );
487 $context = RequestContext
::getMain();
489 $user->setName( 'Test' );
490 $context->setUser( $user );
491 $this->getServiceContainer()
492 ->getFormatterFactory()
493 ->getStatusFormatter( RequestContext
::getMain() )
495 StatusValue
::newFatal( 'apierror-badquery' ),
498 $this->assertTrue( true );