diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/content')
13 files changed, 2707 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/content/ContentHandlerTest.php b/www/wiki/tests/phpunit/includes/content/ContentHandlerTest.php new file mode 100644 index 00000000..309b7b11 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/content/ContentHandlerTest.php @@ -0,0 +1,497 @@ +<?php +use MediaWiki\MediaWikiServices; + +/** + * @group ContentHandler + * @group Database + */ +class ContentHandlerTest extends MediaWikiTestCase { + + protected function setUp() { + global $wgContLang; + parent::setUp(); + + $this->setMwGlobals( [ + 'wgExtraNamespaces' => [ + 12312 => 'Dummy', + 12313 => 'Dummy_talk', + ], + // The below tests assume that namespaces not mentioned here (Help, User, MediaWiki, ..) + // default to CONTENT_MODEL_WIKITEXT. + 'wgNamespaceContentModels' => [ + 12312 => 'testing', + ], + 'wgContentHandlers' => [ + CONTENT_MODEL_WIKITEXT => WikitextContentHandler::class, + CONTENT_MODEL_JAVASCRIPT => JavaScriptContentHandler::class, + CONTENT_MODEL_JSON => JsonContentHandler::class, + CONTENT_MODEL_CSS => CssContentHandler::class, + CONTENT_MODEL_TEXT => TextContentHandler::class, + 'testing' => DummyContentHandlerForTesting::class, + 'testing-callbacks' => function ( $modelId ) { + return new DummyContentHandlerForTesting( $modelId ); + } + ], + ] ); + + // Reset namespace cache + MWNamespace::clearCaches(); + $wgContLang->resetNamespaces(); + // And LinkCache + MediaWikiServices::getInstance()->resetServiceForTesting( 'LinkCache' ); + } + + protected function tearDown() { + global $wgContLang; + + // Reset namespace cache + MWNamespace::clearCaches(); + $wgContLang->resetNamespaces(); + // And LinkCache + MediaWikiServices::getInstance()->resetServiceForTesting( 'LinkCache' ); + + parent::tearDown(); + } + + public function addDBDataOnce() { + $this->insertPage( 'Not_Main_Page', 'This is not a main page' ); + $this->insertPage( 'Smithee', 'A smithee is one who smiths. See also [[Alan Smithee]]' ); + } + + public static function dataGetDefaultModelFor() { + return [ + [ 'Help:Foo', CONTENT_MODEL_WIKITEXT ], + [ 'Help:Foo.js', CONTENT_MODEL_WIKITEXT ], + [ 'Help:Foo.css', CONTENT_MODEL_WIKITEXT ], + [ 'Help:Foo.json', CONTENT_MODEL_WIKITEXT ], + [ 'Help:Foo/bar.js', CONTENT_MODEL_WIKITEXT ], + [ 'User:Foo', CONTENT_MODEL_WIKITEXT ], + [ 'User:Foo.js', CONTENT_MODEL_WIKITEXT ], + [ 'User:Foo.css', CONTENT_MODEL_WIKITEXT ], + [ 'User:Foo.json', CONTENT_MODEL_WIKITEXT ], + [ 'User:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ], + [ 'User:Foo/bar.css', CONTENT_MODEL_CSS ], + [ 'User:Foo/bar.json', CONTENT_MODEL_JSON ], + [ 'User:Foo/bar.json.nope', CONTENT_MODEL_WIKITEXT ], + [ 'User talk:Foo/bar.css', CONTENT_MODEL_WIKITEXT ], + [ 'User:Foo/bar.js.xxx', CONTENT_MODEL_WIKITEXT ], + [ 'User:Foo/bar.xxx', CONTENT_MODEL_WIKITEXT ], + [ 'MediaWiki:Foo.js', CONTENT_MODEL_JAVASCRIPT ], + [ 'MediaWiki:Foo.JS', CONTENT_MODEL_WIKITEXT ], + [ 'MediaWiki:Foo.css', CONTENT_MODEL_CSS ], + [ 'MediaWiki:Foo.css.xxx', CONTENT_MODEL_WIKITEXT ], + [ 'MediaWiki:Foo.CSS', CONTENT_MODEL_WIKITEXT ], + [ 'MediaWiki:Foo.json', CONTENT_MODEL_JSON ], + [ 'MediaWiki:Foo.JSON', CONTENT_MODEL_WIKITEXT ], + ]; + } + + /** + * @dataProvider dataGetDefaultModelFor + * @covers ContentHandler::getDefaultModelFor + */ + public function testGetDefaultModelFor( $title, $expectedModelId ) { + $title = Title::newFromText( $title ); + $this->assertEquals( $expectedModelId, ContentHandler::getDefaultModelFor( $title ) ); + } + + /** + * @dataProvider dataGetDefaultModelFor + * @covers ContentHandler::getForTitle + */ + public function testGetForTitle( $title, $expectedContentModel ) { + $title = Title::newFromText( $title ); + LinkCache::singleton()->addBadLinkObj( $title ); + $handler = ContentHandler::getForTitle( $title ); + $this->assertEquals( $expectedContentModel, $handler->getModelID() ); + } + + public static function dataGetLocalizedName() { + return [ + [ null, null ], + [ "xyzzy", null ], + + // XXX: depends on content language + [ CONTENT_MODEL_JAVASCRIPT, '/javascript/i' ], + ]; + } + + /** + * @dataProvider dataGetLocalizedName + * @covers ContentHandler::getLocalizedName + */ + public function testGetLocalizedName( $id, $expected ) { + $name = ContentHandler::getLocalizedName( $id ); + + if ( $expected ) { + $this->assertNotNull( $name, "no name found for content model $id" ); + $this->assertTrue( preg_match( $expected, $name ) > 0, + "content model name for #$id did not match pattern $expected" + ); + } else { + $this->assertEquals( $id, $name, "localization of unknown model $id should have " + . "fallen back to use the model id directly." + ); + } + } + + public static function dataGetPageLanguage() { + global $wgLanguageCode; + + return [ + [ "Main", $wgLanguageCode ], + [ "Dummy:Foo", $wgLanguageCode ], + [ "MediaWiki:common.js", 'en' ], + [ "User:Foo/common.js", 'en' ], + [ "MediaWiki:common.css", 'en' ], + [ "User:Foo/common.css", 'en' ], + [ "User:Foo", $wgLanguageCode ], + + [ CONTENT_MODEL_JAVASCRIPT, 'javascript' ], + ]; + } + + /** + * @dataProvider dataGetPageLanguage + * @covers ContentHandler::getPageLanguage + */ + public function testGetPageLanguage( $title, $expected ) { + if ( is_string( $title ) ) { + $title = Title::newFromText( $title ); + LinkCache::singleton()->addBadLinkObj( $title ); + } + + $expected = wfGetLangObj( $expected ); + + $handler = ContentHandler::getForTitle( $title ); + $lang = $handler->getPageLanguage( $title ); + + $this->assertEquals( $expected->getCode(), $lang->getCode() ); + } + + public static function dataGetContentText_Null() { + return [ + [ 'fail' ], + [ 'serialize' ], + [ 'ignore' ], + ]; + } + + /** + * @dataProvider dataGetContentText_Null + * @covers ContentHandler::getContentText + */ + public function testGetContentText_Null( $contentHandlerTextFallback ) { + $this->setMwGlobals( 'wgContentHandlerTextFallback', $contentHandlerTextFallback ); + + $content = null; + + $text = ContentHandler::getContentText( $content ); + $this->assertEquals( '', $text ); + } + + public static function dataGetContentText_TextContent() { + return [ + [ 'fail' ], + [ 'serialize' ], + [ 'ignore' ], + ]; + } + + /** + * @dataProvider dataGetContentText_TextContent + * @covers ContentHandler::getContentText + */ + public function testGetContentText_TextContent( $contentHandlerTextFallback ) { + $this->setMwGlobals( 'wgContentHandlerTextFallback', $contentHandlerTextFallback ); + + $content = new WikitextContent( "hello world" ); + + $text = ContentHandler::getContentText( $content ); + $this->assertEquals( $content->getNativeData(), $text ); + } + + /** + * ContentHandler::getContentText should have thrown an exception for non-text Content object + * @expectedException MWException + * @covers ContentHandler::getContentText + */ + public function testGetContentText_NonTextContent_fail() { + $this->setMwGlobals( 'wgContentHandlerTextFallback', 'fail' ); + + $content = new DummyContentForTesting( "hello world" ); + + ContentHandler::getContentText( $content ); + } + + /** + * @covers ContentHandler::getContentText + */ + public function testGetContentText_NonTextContent_serialize() { + $this->setMwGlobals( 'wgContentHandlerTextFallback', 'serialize' ); + + $content = new DummyContentForTesting( "hello world" ); + + $text = ContentHandler::getContentText( $content ); + $this->assertEquals( $content->serialize(), $text ); + } + + /** + * @covers ContentHandler::getContentText + */ + public function testGetContentText_NonTextContent_ignore() { + $this->setMwGlobals( 'wgContentHandlerTextFallback', 'ignore' ); + + $content = new DummyContentForTesting( "hello world" ); + + $text = ContentHandler::getContentText( $content ); + $this->assertNull( $text ); + } + + public static function dataMakeContent() { + return [ + [ 'hallo', 'Help:Test', null, null, CONTENT_MODEL_WIKITEXT, 'hallo', false ], + [ 'hallo', 'MediaWiki:Test.js', null, null, CONTENT_MODEL_JAVASCRIPT, 'hallo', false ], + [ serialize( 'hallo' ), 'Dummy:Test', null, null, "testing", 'hallo', false ], + + [ + 'hallo', + 'Help:Test', + null, + CONTENT_FORMAT_WIKITEXT, + CONTENT_MODEL_WIKITEXT, + 'hallo', + false + ], + [ + 'hallo', + 'MediaWiki:Test.js', + null, + CONTENT_FORMAT_JAVASCRIPT, + CONTENT_MODEL_JAVASCRIPT, + 'hallo', + false + ], + [ serialize( 'hallo' ), 'Dummy:Test', null, "testing", "testing", 'hallo', false ], + + [ 'hallo', 'Help:Test', CONTENT_MODEL_CSS, null, CONTENT_MODEL_CSS, 'hallo', false ], + [ + 'hallo', + 'MediaWiki:Test.js', + CONTENT_MODEL_CSS, + null, + CONTENT_MODEL_CSS, + 'hallo', + false + ], + [ + serialize( 'hallo' ), + 'Dummy:Test', + CONTENT_MODEL_CSS, + null, + CONTENT_MODEL_CSS, + serialize( 'hallo' ), + false + ], + + [ 'hallo', 'Help:Test', CONTENT_MODEL_WIKITEXT, "testing", null, null, true ], + [ 'hallo', 'MediaWiki:Test.js', CONTENT_MODEL_CSS, "testing", null, null, true ], + [ 'hallo', 'Dummy:Test', CONTENT_MODEL_JAVASCRIPT, "testing", null, null, true ], + ]; + } + + /** + * @dataProvider dataMakeContent + * @covers ContentHandler::makeContent + */ + public function testMakeContent( $data, $title, $modelId, $format, + $expectedModelId, $expectedNativeData, $shouldFail + ) { + $title = Title::newFromText( $title ); + LinkCache::singleton()->addBadLinkObj( $title ); + try { + $content = ContentHandler::makeContent( $data, $title, $modelId, $format ); + + if ( $shouldFail ) { + $this->fail( "ContentHandler::makeContent should have failed!" ); + } + + $this->assertEquals( $expectedModelId, $content->getModel(), 'bad model id' ); + $this->assertEquals( $expectedNativeData, $content->getNativeData(), 'bads native data' ); + } catch ( MWException $ex ) { + if ( !$shouldFail ) { + $this->fail( "ContentHandler::makeContent failed unexpectedly: " . $ex->getMessage() ); + } else { + // dummy, so we don't get the "test did not perform any assertions" message. + $this->assertTrue( true ); + } + } + } + + /** + * @covers ContentHandler::getAutosummary + * + * Test if we become a "Created blank page" summary from getAutoSummary if no Content added to + * page. + */ + public function testGetAutosummary() { + $this->setMwGlobals( 'wgContLang', Language::factory( 'en' ) ); + + $content = new DummyContentHandlerForTesting( CONTENT_MODEL_WIKITEXT ); + $title = Title::newFromText( 'Help:Test' ); + // Create a new content object with no content + $newContent = ContentHandler::makeContent( '', $title, CONTENT_MODEL_WIKITEXT, null ); + // first check, if we become a blank page created summary with the right bitmask + $autoSummary = $content->getAutosummary( null, $newContent, 97 ); + $this->assertEquals( $autoSummary, + wfMessage( 'autosumm-newblank' )->inContentLanguage()->text() ); + // now check, what we become with another bitmask + $autoSummary = $content->getAutosummary( null, $newContent, 92 ); + $this->assertEquals( $autoSummary, '' ); + } + + /** + * Test software tag that is added when content model of the page changes + * @covers ContentHandler::getChangeTag + */ + public function testGetChangeTag() { + $this->setMwGlobals( 'wgSoftwareTags', [ 'mw-contentmodelchange' => true ] ); + $wikitextContentHandler = new DummyContentHandlerForTesting( CONTENT_MODEL_WIKITEXT ); + // Create old content object with javascript content model + $oldContent = ContentHandler::makeContent( '', null, CONTENT_MODEL_JAVASCRIPT, null ); + // Create new content object with wikitext content model + $newContent = ContentHandler::makeContent( '', null, CONTENT_MODEL_WIKITEXT, null ); + // Get the tag for this edit + $tag = $wikitextContentHandler->getChangeTag( $oldContent, $newContent, EDIT_UPDATE ); + $this->assertSame( $tag, 'mw-contentmodelchange' ); + } + + /** + * @covers ContentHandler::supportsCategories + */ + public function testSupportsCategories() { + $handler = new DummyContentHandlerForTesting( CONTENT_MODEL_WIKITEXT ); + $this->assertTrue( $handler->supportsCategories(), 'content model supports categories' ); + } + + /** + * @covers ContentHandler::supportsDirectEditing + */ + public function testSupportsDirectEditing() { + $handler = new DummyContentHandlerForTesting( CONTENT_MODEL_JSON ); + $this->assertFalse( $handler->supportsDirectEditing(), 'direct editing is not supported' ); + } + + public static function dummyHookHandler( $foo, &$text, $bar ) { + if ( $text === null || $text === false ) { + return false; + } + + $text = strtoupper( $text ); + + return true; + } + + public function provideGetModelForID() { + return [ + [ CONTENT_MODEL_WIKITEXT, WikitextContentHandler::class ], + [ CONTENT_MODEL_JAVASCRIPT, JavaScriptContentHandler::class ], + [ CONTENT_MODEL_JSON, JsonContentHandler::class ], + [ CONTENT_MODEL_CSS, CssContentHandler::class ], + [ CONTENT_MODEL_TEXT, TextContentHandler::class ], + [ 'testing', DummyContentHandlerForTesting::class ], + [ 'testing-callbacks', DummyContentHandlerForTesting::class ], + ]; + } + + /** + * @covers ContentHandler::getForModelID + * @dataProvider provideGetModelForID + */ + public function testGetModelForID( $modelId, $handlerClass ) { + $handler = ContentHandler::getForModelID( $modelId ); + + $this->assertInstanceOf( $handlerClass, $handler ); + } + + /** + * @covers ContentHandler::getFieldsForSearchIndex + */ + public function testGetFieldsForSearchIndex() { + $searchEngine = $this->newSearchEngine(); + + $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT ); + + $fields = $handler->getFieldsForSearchIndex( $searchEngine ); + + $this->assertArrayHasKey( 'category', $fields ); + $this->assertArrayHasKey( 'external_link', $fields ); + $this->assertArrayHasKey( 'outgoing_link', $fields ); + $this->assertArrayHasKey( 'template', $fields ); + $this->assertArrayHasKey( 'content_model', $fields ); + } + + private function newSearchEngine() { + $searchEngine = $this->getMockBuilder( SearchEngine::class ) + ->getMock(); + + $searchEngine->expects( $this->any() ) + ->method( 'makeSearchFieldMapping' ) + ->will( $this->returnCallback( function ( $name, $type ) { + return new DummySearchIndexFieldDefinition( $name, $type ); + } ) ); + + return $searchEngine; + } + + /** + * @covers ContentHandler::getDataForSearchIndex + */ + public function testDataIndexFields() { + $mockEngine = $this->createMock( SearchEngine::class ); + $title = Title::newFromText( 'Not_Main_Page', NS_MAIN ); + $page = new WikiPage( $title ); + + $this->setTemporaryHook( 'SearchDataForIndex', + function ( + &$fields, + ContentHandler $handler, + WikiPage $page, + ParserOutput $output, + SearchEngine $engine + ) { + $fields['testDataField'] = 'test content'; + } ); + + $output = $page->getContent()->getParserOutput( $title ); + $data = $page->getContentHandler()->getDataForSearchIndex( $page, $output, $mockEngine ); + $this->assertArrayHasKey( 'text', $data ); + $this->assertArrayHasKey( 'text_bytes', $data ); + $this->assertArrayHasKey( 'language', $data ); + $this->assertArrayHasKey( 'testDataField', $data ); + $this->assertEquals( 'test content', $data['testDataField'] ); + $this->assertEquals( 'wikitext', $data['content_model'] ); + } + + /** + * @covers ContentHandler::getParserOutputForIndexing + */ + public function testParserOutputForIndexing() { + $title = Title::newFromText( 'Smithee', NS_MAIN ); + $page = new WikiPage( $title ); + + $out = $page->getContentHandler()->getParserOutputForIndexing( $page ); + $this->assertInstanceOf( ParserOutput::class, $out ); + $this->assertContains( 'one who smiths', $out->getRawText() ); + } + + /** + * @covers ContentHandler::getContentModels + */ + public function testGetContentModelsHook() { + $this->setTemporaryHook( 'GetContentModels', function ( &$models ) { + $models[] = 'Ferrari'; + } ); + $this->assertContains( 'Ferrari', ContentHandler::getContentModels() ); + } +} diff --git a/www/wiki/tests/phpunit/includes/content/CssContentHandlerTest.php b/www/wiki/tests/phpunit/includes/content/CssContentHandlerTest.php new file mode 100644 index 00000000..7ca1afce --- /dev/null +++ b/www/wiki/tests/phpunit/includes/content/CssContentHandlerTest.php @@ -0,0 +1,41 @@ +<?php + +class CssContentHandlerTest extends MediaWikiLangTestCase { + + /** + * @dataProvider provideMakeRedirectContent + * @covers CssContentHandler::makeRedirectContent + */ + public function testMakeRedirectContent( $title, $expected ) { + $this->setMwGlobals( [ + 'wgServer' => '//example.org', + 'wgScript' => '/w/index.php', + ] ); + $ch = new CssContentHandler(); + $content = $ch->makeRedirectContent( Title::newFromText( $title ) ); + $this->assertInstanceOf( CssContent::class, $content ); + $this->assertEquals( $expected, $content->serialize( CONTENT_FORMAT_CSS ) ); + } + + /** + * Keep this in sync with CssContentTest::provideGetRedirectTarget() + */ + public static function provideMakeRedirectContent() { + // phpcs:disable Generic.Files.LineLength + return [ + [ + 'MediaWiki:MonoBook.css', + "/* #REDIRECT */@import url(//example.org/w/index.php?title=MediaWiki:MonoBook.css&action=raw&ctype=text/css);" + ], + [ + 'User:FooBar/common.css', + "/* #REDIRECT */@import url(//example.org/w/index.php?title=User:FooBar/common.css&action=raw&ctype=text/css);" + ], + [ + 'Gadget:FooBaz.css', + "/* #REDIRECT */@import url(//example.org/w/index.php?title=Gadget:FooBaz.css&action=raw&ctype=text/css);" + ], + ]; + // phpcs:enable + } +} diff --git a/www/wiki/tests/phpunit/includes/content/CssContentTest.php b/www/wiki/tests/phpunit/includes/content/CssContentTest.php new file mode 100644 index 00000000..f5cc05e0 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/content/CssContentTest.php @@ -0,0 +1,133 @@ +<?php + +/** + * @group ContentHandler + * @group Database + * ^--- needed, because we do need the database to test link updates + * + * @FIXME this should not extend JavaScriptContentTest. + */ +class CssContentTest extends JavaScriptContentTest { + + protected function setUp() { + parent::setUp(); + + // Anon user + $user = new User(); + $user->setName( '127.0.0.1' ); + + $this->setMwGlobals( [ + 'wgUser' => $user, + 'wgTextModelsToParse' => [ + CONTENT_MODEL_CSS, + ] + ] ); + } + + public function newContent( $text ) { + return new CssContent( $text ); + } + + public static function dataGetParserOutput() { + return [ + [ + 'MediaWiki:Test.css', + null, + "hello <world>\n", + "<pre class=\"mw-code mw-css\" dir=\"ltr\">\nhello <world>\n\n</pre>" + ], + [ + 'MediaWiki:Test.css', + null, + "/* hello [[world]] */\n", + "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n/* hello [[world]] */\n\n</pre>", + [ + 'Links' => [ + [ 'World' => 0 ] + ] + ] + ], + + // TODO: more...? + ]; + } + + /** + * @covers CssContent::getModel + */ + public function testGetModel() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( CONTENT_MODEL_CSS, $content->getModel() ); + } + + /** + * @covers CssContent::getContentHandler + */ + public function testGetContentHandler() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( CONTENT_MODEL_CSS, $content->getContentHandler()->getModelID() ); + } + + /** + * Redirects aren't supported + */ + public static function provideUpdateRedirect() { + return [ + [ + '#REDIRECT [[Someplace]]', + '#REDIRECT [[Someplace]]', + ], + ]; + } + + /** + * @covers CssContent::getRedirectTarget + * @dataProvider provideGetRedirectTarget + */ + public function testGetRedirectTarget( $title, $text ) { + $this->setMwGlobals( [ + 'wgServer' => '//example.org', + 'wgScriptPath' => '/w', + 'wgScript' => '/w/index.php', + ] ); + $content = new CssContent( $text ); + $target = $content->getRedirectTarget(); + $this->assertEquals( $title, $target ? $target->getPrefixedText() : null ); + } + + /** + * Keep this in sync with CssContentHandlerTest::provideMakeRedirectContent() + */ + public static function provideGetRedirectTarget() { + // phpcs:disable Generic.Files.LineLength + return [ + [ 'MediaWiki:MonoBook.css', "/* #REDIRECT */@import url(//example.org/w/index.php?title=MediaWiki:MonoBook.css&action=raw&ctype=text/css);" ], + [ 'User:FooBar/common.css', "/* #REDIRECT */@import url(//example.org/w/index.php?title=User:FooBar/common.css&action=raw&ctype=text/css);" ], + [ 'Gadget:FooBaz.css', "/* #REDIRECT */@import url(//example.org/w/index.php?title=Gadget:FooBaz.css&action=raw&ctype=text/css);" ], + # No #REDIRECT comment + [ null, "@import url(//example.org/w/index.php?title=Gadget:FooBaz.css&action=raw&ctype=text/css);" ], + # Wrong domain + [ null, "/* #REDIRECT */@import url(//example.com/w/index.php?title=Gadget:FooBaz.css&action=raw&ctype=text/css);" ], + ]; + // phpcs:enable + } + + public static function dataEquals() { + return [ + [ new CssContent( 'hallo' ), null, false ], + [ new CssContent( 'hallo' ), new CssContent( 'hallo' ), true ], + [ new CssContent( 'hallo' ), new WikitextContent( 'hallo' ), false ], + [ new CssContent( 'hallo' ), new CssContent( 'HALLO' ), false ], + ]; + } + + /** + * @dataProvider dataEquals + * @covers CssContent::equals + */ + public function testEquals( Content $a, Content $b = null, $equal = false ) { + $this->assertEquals( $equal, $a->equals( $b ) ); + } +} diff --git a/www/wiki/tests/phpunit/includes/content/FileContentHandlerTest.php b/www/wiki/tests/phpunit/includes/content/FileContentHandlerTest.php new file mode 100644 index 00000000..9149fc4f --- /dev/null +++ b/www/wiki/tests/phpunit/includes/content/FileContentHandlerTest.php @@ -0,0 +1,52 @@ +<?php + +/** + * @group ContentHandler + * + * @covers FileContentHandler + */ +class FileContentHandlerTest extends MediaWikiLangTestCase { + /** + * @var FileContentHandler + */ + private $handler; + + protected function setUp() { + parent::setUp(); + + $this->handler = new FileContentHandler(); + } + + public function testIndexMapping() { + $mockEngine = $this->createMock( SearchEngine::class ); + + $mockEngine->expects( $this->atLeastOnce() ) + ->method( 'makeSearchFieldMapping' ) + ->willReturnCallback( function ( $name, $type ) { + $mockField = + $this->getMockBuilder( SearchIndexFieldDefinition::class ) + ->setMethods( [ 'getMapping' ] ) + ->setConstructorArgs( [ $name, $type ] ) + ->getMock(); + return $mockField; + } ); + + $map = $this->handler->getFieldsForSearchIndex( $mockEngine ); + $expect = [ + 'file_media_type' => 1, + 'file_mime' => 1, + 'file_size' => 1, + 'file_width' => 1, + 'file_height' => 1, + 'file_bits' => 1, + 'file_resolution' => 1, + 'file_text' => 1, + ]; + foreach ( $map as $name => $field ) { + $this->assertInstanceOf( SearchIndexField::class, $field ); + $this->assertEquals( $name, $field->getName() ); + unset( $expect[$name] ); + } + $this->assertEmpty( $expect ); + } +} diff --git a/www/wiki/tests/phpunit/includes/content/JavaScriptContentHandlerTest.php b/www/wiki/tests/phpunit/includes/content/JavaScriptContentHandlerTest.php new file mode 100644 index 00000000..b5e3ab4a --- /dev/null +++ b/www/wiki/tests/phpunit/includes/content/JavaScriptContentHandlerTest.php @@ -0,0 +1,41 @@ +<?php + +class JavaScriptContentHandlerTest extends MediaWikiLangTestCase { + + /** + * @dataProvider provideMakeRedirectContent + * @covers JavaScriptContentHandler::makeRedirectContent + */ + public function testMakeRedirectContent( $title, $expected ) { + $this->setMwGlobals( [ + 'wgServer' => '//example.org', + 'wgScript' => '/w/index.php', + ] ); + $ch = new JavaScriptContentHandler(); + $content = $ch->makeRedirectContent( Title::newFromText( $title ) ); + $this->assertInstanceOf( JavaScriptContent::class, $content ); + $this->assertEquals( $expected, $content->serialize( CONTENT_FORMAT_JAVASCRIPT ) ); + } + + /** + * Keep this in sync with JavaScriptContentTest::provideGetRedirectTarget() + */ + public static function provideMakeRedirectContent() { + // phpcs:disable Generic.Files.LineLength + return [ + [ + 'MediaWiki:MonoBook.js', + '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=MediaWiki:MonoBook.js\u0026action=raw\u0026ctype=text/javascript");' + ], + [ + 'User:FooBar/common.js', + '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=User:FooBar/common.js\u0026action=raw\u0026ctype=text/javascript");' + ], + [ + 'Gadget:FooBaz.js', + '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=Gadget:FooBaz.js\u0026action=raw\u0026ctype=text/javascript");' + ], + ]; + // phpcs:enable + } +} diff --git a/www/wiki/tests/phpunit/includes/content/JavaScriptContentTest.php b/www/wiki/tests/phpunit/includes/content/JavaScriptContentTest.php new file mode 100644 index 00000000..823be6f7 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/content/JavaScriptContentTest.php @@ -0,0 +1,327 @@ +<?php + +/** + * @group ContentHandler + * @group Database + * ^--- needed, because we do need the database to test link updates + */ +class JavaScriptContentTest extends TextContentTest { + + public function newContent( $text ) { + return new JavaScriptContent( $text ); + } + + public static function dataGetParserOutput() { + return [ + [ + 'MediaWiki:Test.js', + null, + "hello <world>\n", + "<pre class=\"mw-code mw-js\" dir=\"ltr\">\nhello <world>\n\n</pre>" + ], + [ + 'MediaWiki:Test.js', + null, + "hello(); // [[world]]\n", + "<pre class=\"mw-code mw-js\" dir=\"ltr\">\nhello(); // [[world]]\n\n</pre>", + [ + 'Links' => [ + [ 'World' => 0 ] + ] + ] + ], + + // TODO: more...? + ]; + } + + // XXX: Unused function + public static function dataGetSection() { + return [ + [ WikitextContentTest::$sections, + '0', + null + ], + [ WikitextContentTest::$sections, + '2', + null + ], + [ WikitextContentTest::$sections, + '8', + null + ], + ]; + } + + // XXX: Unused function + public static function dataReplaceSection() { + return [ + [ WikitextContentTest::$sections, + '0', + 'No more', + null, + null + ], + [ WikitextContentTest::$sections, + '', + 'No more', + null, + null + ], + [ WikitextContentTest::$sections, + '2', + "== TEST ==\nmore fun", + null, + null + ], + [ WikitextContentTest::$sections, + '8', + 'No more', + null, + null + ], + [ WikitextContentTest::$sections, + 'new', + 'No more', + 'New', + null + ], + ]; + } + + /** + * @covers JavaScriptContent::addSectionHeader + */ + public function testAddSectionHeader() { + $content = $this->newContent( 'hello world' ); + $c = $content->addSectionHeader( 'test' ); + + $this->assertTrue( $content->equals( $c ) ); + } + + // XXX: currently, preSaveTransform is applied to scripts. this may change or become optional. + public static function dataPreSaveTransform() { + return [ + [ 'hello this is ~~~', + "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]", + ], + [ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>', + 'hello \'\'this\'\' is <nowiki>~~~</nowiki>', + ], + [ " Foo \n ", + " Foo", + ], + ]; + } + + public static function dataPreloadTransform() { + return [ + [ + 'hello this is ~~~', + 'hello this is ~~~', + ], + [ + 'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>', + 'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>', + ], + ]; + } + + public static function dataGetRedirectTarget() { + return [ + [ '#REDIRECT [[Test]]', + null, + ], + [ '#REDIRECT Test', + null, + ], + [ '* #REDIRECT [[Test]]', + null, + ], + ]; + } + + public static function dataIsCountable() { + return [ + [ '', + null, + 'any', + true + ], + [ 'Foo', + null, + 'any', + true + ], + [ 'Foo', + null, + 'link', + false + ], + [ 'Foo [[bar]]', + null, + 'link', + false + ], + [ 'Foo', + true, + 'link', + false + ], + [ 'Foo [[bar]]', + false, + 'link', + false + ], + [ '#REDIRECT [[bar]]', + true, + 'any', + true + ], + [ '#REDIRECT [[bar]]', + true, + 'link', + false + ], + ]; + } + + public static function dataGetTextForSummary() { + return [ + [ "hello\nworld.", + 16, + 'hello world.', + ], + [ 'hello world.', + 8, + 'hello...', + ], + [ '[[hello world]].', + 8, + '[[hel...', + ], + ]; + } + + /** + * @covers JavaScriptContent::matchMagicWord + */ + public function testMatchMagicWord() { + $mw = MagicWord::get( "staticredirect" ); + + $content = $this->newContent( "#REDIRECT [[FOO]]\n__STATICREDIRECT__" ); + $this->assertFalse( + $content->matchMagicWord( $mw ), + "should not have matched magic word, since it's not wikitext" + ); + } + + /** + * @covers JavaScriptContent::updateRedirect + * @dataProvider provideUpdateRedirect + */ + public function testUpdateRedirect( $oldText, $expectedText ) { + $this->setMwGlobals( [ + 'wgServer' => '//example.org', + 'wgScriptPath' => '/w', + 'wgScript' => '/w/index.php', + 'wgResourceBasePath' => '/w', + ] ); + $target = Title::newFromText( "testUpdateRedirect_target" ); + + $content = new JavaScriptContent( $oldText ); + $newContent = $content->updateRedirect( $target ); + + $this->assertEquals( $expectedText, $newContent->getNativeData() ); + } + + public static function provideUpdateRedirect() { + // phpcs:disable Generic.Files.LineLength + return [ + [ + '#REDIRECT [[Someplace]]', + '#REDIRECT [[Someplace]]', + ], + [ + '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=MediaWiki:MonoBook.js\u0026action=raw\u0026ctype=text/javascript");', + '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=TestUpdateRedirect_target\u0026action=raw\u0026ctype=text/javascript");' + ] + ]; + // phpcs:enable + } + + /** + * @covers JavaScriptContent::getModel + */ + public function testGetModel() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getModel() ); + } + + /** + * @covers JavaScriptContent::getContentHandler + */ + public function testGetContentHandler() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getContentHandler()->getModelID() ); + } + + public static function dataEquals() { + return [ + [ new JavaScriptContent( "hallo" ), null, false ], + [ new JavaScriptContent( "hallo" ), new JavaScriptContent( "hallo" ), true ], + [ new JavaScriptContent( "hallo" ), new CssContent( "hallo" ), false ], + [ new JavaScriptContent( "hallo" ), new JavaScriptContent( "HALLO" ), false ], + ]; + } + + /** + * @covers JavaScriptContent::getRedirectTarget + * @dataProvider provideGetRedirectTarget + */ + public function testGetRedirectTarget( $title, $text ) { + $this->setMwGlobals( [ + 'wgServer' => '//example.org', + 'wgScriptPath' => '/w', + 'wgScript' => '/w/index.php', + 'wgResourceBasePath' => '/w', + ] ); + $content = new JavaScriptContent( $text ); + $target = $content->getRedirectTarget(); + $this->assertEquals( $title, $target ? $target->getPrefixedText() : null ); + } + + /** + * Keep this in sync with JavaScriptContentHandlerTest::provideMakeRedirectContent() + */ + public static function provideGetRedirectTarget() { + // phpcs:disable Generic.Files.LineLength + return [ + [ + 'MediaWiki:MonoBook.js', + '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=MediaWiki:MonoBook.js\u0026action=raw\u0026ctype=text/javascript");' + ], + [ + 'User:FooBar/common.js', + '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=User:FooBar/common.js\u0026action=raw\u0026ctype=text/javascript");' + ], + [ + 'Gadget:FooBaz.js', + '/* #REDIRECT */mw.loader.load("//example.org/w/index.php?title=Gadget:FooBaz.js\u0026action=raw\u0026ctype=text/javascript");' + ], + // No #REDIRECT comment + [ + null, + 'mw.loader.load("//example.org/w/index.php?title=MediaWiki:NoRedirect.js\u0026action=raw\u0026ctype=text/javascript");' + ], + // Different domain + [ + null, + '/* #REDIRECT */mw.loader.load("//example.com/w/index.php?title=MediaWiki:OtherWiki.js\u0026action=raw\u0026ctype=text/javascript");' + ], + ]; + // phpcs:enable + } +} diff --git a/www/wiki/tests/phpunit/includes/content/JsonContentHandlerTest.php b/www/wiki/tests/phpunit/includes/content/JsonContentHandlerTest.php new file mode 100644 index 00000000..abfb6733 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/content/JsonContentHandlerTest.php @@ -0,0 +1,14 @@ +<?php + +class JsonContentHandlerTest extends MediaWikiTestCase { + + /** + * @covers JsonContentHandler::makeEmptyContent + */ + public function testMakeEmptyContent() { + $handler = new JsonContentHandler(); + $content = $handler->makeEmptyContent(); + $this->assertInstanceOf( JsonContent::class, $content ); + $this->assertTrue( $content->isValid() ); + } +} diff --git a/www/wiki/tests/phpunit/includes/content/JsonContentTest.php b/www/wiki/tests/phpunit/includes/content/JsonContentTest.php new file mode 100644 index 00000000..7cddbad2 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/content/JsonContentTest.php @@ -0,0 +1,152 @@ +<?php + +/** + * @author Addshore + * @covers JsonContent + */ +class JsonContentTest extends MediaWikiLangTestCase { + + public static function provideValidConstruction() { + return [ + [ 'foo', false, null ], + [ '[]', true, [] ], + [ '{}', true, (object)[] ], + [ '""', true, '' ], + [ '"0"', true, '0' ], + [ '"bar"', true, 'bar' ], + [ '0', true, '0' ], + [ '{ "0": "bar" }', true, (object)[ 'bar' ] ], + ]; + } + + /** + * @dataProvider provideValidConstruction + */ + public function testIsValid( $text, $isValid, $expected ) { + $obj = new JsonContent( $text, CONTENT_MODEL_JSON ); + $this->assertEquals( $isValid, $obj->isValid() ); + $this->assertEquals( $expected, $obj->getData()->getValue() ); + } + + public static function provideDataToEncode() { + return [ + [ + // Round-trip empty array + '[]', + '[]', + ], + [ + // Round-trip empty object + '{}', + '{}', + ], + [ + // Round-trip empty array/object (nested) + '{ "foo": {}, "bar": [] }', + "{\n \"foo\": {},\n \"bar\": []\n}", + ], + [ + '{ "foo": "bar" }', + "{\n \"foo\": \"bar\"\n}", + ], + [ + '{ "foo": 1000 }', + "{\n \"foo\": 1000\n}", + ], + [ + '{ "foo": 1000, "0": "bar" }', + "{\n \"foo\": 1000,\n \"0\": \"bar\"\n}", + ], + ]; + } + + /** + * @dataProvider provideDataToEncode + */ + public function testBeautifyJson( $input, $beautified ) { + $obj = new JsonContent( $input ); + $this->assertEquals( $beautified, $obj->beautifyJSON() ); + } + + /** + * @dataProvider provideDataToEncode + */ + public function testPreSaveTransform( $input, $transformed ) { + $obj = new JsonContent( $input ); + $newObj = $obj->preSaveTransform( + $this->getMockTitle(), + $this->getMockUser(), + $this->getMockParserOptions() + ); + $this->assertTrue( $newObj->equals( new JsonContent( $transformed ) ) ); + } + + private function getMockTitle() { + return $this->getMockBuilder( Title::class ) + ->disableOriginalConstructor() + ->getMock(); + } + + private function getMockUser() { + return $this->getMockBuilder( User::class ) + ->disableOriginalConstructor() + ->getMock(); + } + private function getMockParserOptions() { + return $this->getMockBuilder( ParserOptions::class ) + ->disableOriginalConstructor() + ->getMock(); + } + + public static function provideDataAndParserText() { + return [ + [ + [], + '<table class="mw-json"><tbody><tr><td>' . + '<table class="mw-json"><tbody><tr><td class="mw-json-empty">Empty array</td></tr>' + . '</tbody></table></td></tr></tbody></table>' + ], + [ + (object)[], + '<table class="mw-json"><tbody><tr><td class="mw-json-empty">Empty object</td></tr>' . + '</tbody></table>' + ], + [ + (object)[ 'foo' ], + '<table class="mw-json"><tbody><tr><th>0</th><td class="value">"foo"</td></tr>' . + '</tbody></table>' + ], + [ + (object)[ 'foo', 'bar' ], + '<table class="mw-json"><tbody><tr><th>0</th><td class="value">"foo"</td></tr>' . + '<tr><th>1</th><td class="value">"bar"</td></tr></tbody></table>' + ], + [ + (object)[ 'baz' => 'foo', 'bar' ], + '<table class="mw-json"><tbody><tr><th>baz</th><td class="value">"foo"</td></tr>' . + '<tr><th>0</th><td class="value">"bar"</td></tr></tbody></table>' + ], + [ + (object)[ 'baz' => 1000, 'bar' ], + '<table class="mw-json"><tbody><tr><th>baz</th><td class="value">1000</td></tr>' . + '<tr><th>0</th><td class="value">"bar"</td></tr></tbody></table>' + ], + [ + (object)[ '<script>alert("evil!")</script>' ], + '<table class="mw-json"><tbody><tr><th>0</th><td class="value">"' . + '<script>alert("evil!")</script>"' . + '</td></tr></tbody></table>', + ], + ]; + } + + /** + * @dataProvider provideDataAndParserText + */ + public function testFillParserOutput( $data, $expected ) { + $obj = new JsonContent( FormatJson::encode( $data ) ); + $parserOutput = $obj->getParserOutput( $this->getMockTitle(), null, null, true ); + $this->assertInstanceOf( ParserOutput::class, $parserOutput ); + $this->assertEquals( $expected, $parserOutput->getText() ); + } +} diff --git a/www/wiki/tests/phpunit/includes/content/TextContentHandlerTest.php b/www/wiki/tests/phpunit/includes/content/TextContentHandlerTest.php new file mode 100644 index 00000000..6d0a3d5c --- /dev/null +++ b/www/wiki/tests/phpunit/includes/content/TextContentHandlerTest.php @@ -0,0 +1,55 @@ +<?php + +/** + * @group ContentHandler + */ +class TextContentHandlerTest extends MediaWikiLangTestCase { + /** + * @covers TextContentHandler::supportsDirectEditing + */ + public function testSupportsDirectEditing() { + $handler = new TextContentHandler(); + $this->assertTrue( $handler->supportsDirectEditing(), 'direct editing is supported' ); + } + + /** + * @covers SearchEngine::makeSearchFieldMapping + * @covers ContentHandler::getFieldsForSearchIndex + */ + public function testFieldsForIndex() { + $handler = new TextContentHandler(); + + $mockEngine = $this->createMock( SearchEngine::class ); + + $mockEngine->expects( $this->atLeastOnce() ) + ->method( 'makeSearchFieldMapping' ) + ->willReturnCallback( function ( $name, $type ) { + $mockField = + $this->getMockBuilder( SearchIndexFieldDefinition::class ) + ->setConstructorArgs( [ $name, $type ] ) + ->getMock(); + $mockField->expects( $this->atLeastOnce() )->method( 'getMapping' )->willReturn( [ + 'testData' => 'test', + 'name' => $name, + 'type' => $type, + ] ); + return $mockField; + } ); + + /** + * @var $mockEngine SearchEngine + */ + $fields = $handler->getFieldsForSearchIndex( $mockEngine ); + $mappedFields = []; + foreach ( $fields as $name => $field ) { + $this->assertInstanceOf( SearchIndexField::class, $field ); + /** + * @var $field SearchIndexField + */ + $mappedFields[$name] = $field->getMapping( $mockEngine ); + } + $this->assertArrayHasKey( 'language', $mappedFields ); + $this->assertEquals( 'test', $mappedFields['language']['testData'] ); + $this->assertEquals( 'language', $mappedFields['language']['name'] ); + } +} diff --git a/www/wiki/tests/phpunit/includes/content/TextContentTest.php b/www/wiki/tests/phpunit/includes/content/TextContentTest.php new file mode 100644 index 00000000..406bc96b --- /dev/null +++ b/www/wiki/tests/phpunit/includes/content/TextContentTest.php @@ -0,0 +1,477 @@ +<?php + +/** + * @group ContentHandler + * @group Database + * ^--- needed, because we do need the database to test link updates + */ +class TextContentTest extends MediaWikiLangTestCase { + protected $context; + + protected function setUp() { + parent::setUp(); + + // Anon user + $user = new User(); + $user->setName( '127.0.0.1' ); + + $this->context = new RequestContext( new FauxRequest() ); + $this->context->setTitle( Title::newFromText( 'Test' ) ); + $this->context->setUser( $user ); + + $this->setMwGlobals( [ + 'wgUser' => $user, + 'wgTextModelsToParse' => [ + CONTENT_MODEL_WIKITEXT, + CONTENT_MODEL_CSS, + CONTENT_MODEL_JAVASCRIPT, + ], + 'wgUseTidy' => false, + 'wgCapitalLinks' => true, + 'wgHooks' => [], // bypass hook ContentGetParserOutput that force custom rendering + ] ); + + MWTidy::destroySingleton(); + } + + protected function tearDown() { + MWTidy::destroySingleton(); + parent::tearDown(); + } + + public function newContent( $text ) { + return new TextContent( $text ); + } + + public static function dataGetParserOutput() { + return [ + [ + 'TextContentTest_testGetParserOutput', + CONTENT_MODEL_TEXT, + "hello ''world'' & [[stuff]]\n", "hello ''world'' & [[stuff]]", + [ + 'Links' => [] + ] + ], + // TODO: more...? + ]; + } + + /** + * @dataProvider dataGetParserOutput + * @covers TextContent::getParserOutput + */ + public function testGetParserOutput( $title, $model, $text, $expectedHtml, + $expectedFields = null + ) { + $title = Title::newFromText( $title ); + $content = ContentHandler::makeContent( $text, $title, $model ); + + $po = $content->getParserOutput( $title ); + + $html = $po->getText(); + $html = preg_replace( '#<!--.*?-->#sm', '', $html ); // strip comments + + $this->assertEquals( $expectedHtml, trim( $html ) ); + + if ( $expectedFields ) { + foreach ( $expectedFields as $field => $exp ) { + $f = 'get' . ucfirst( $field ); + $v = call_user_func( [ $po, $f ] ); + + if ( is_array( $exp ) ) { + $this->assertArrayEquals( $exp, $v ); + } else { + $this->assertEquals( $exp, $v ); + } + } + } + + // TODO: assert more properties + } + + public static function dataPreSaveTransform() { + return [ + [ + # 0: no signature resolution + 'hello this is ~~~', + 'hello this is ~~~', + ], + [ + # 1: rtrim + " Foo \n ", + ' Foo', + ], + [ + # 2: newline normalization + "LF\n\nCRLF\r\n\r\nCR\r\rEND", + "LF\n\nCRLF\n\nCR\n\nEND", + ], + ]; + } + + /** + * @dataProvider dataPreSaveTransform + * @covers TextContent::preSaveTransform + */ + public function testPreSaveTransform( $text, $expected ) { + global $wgContLang; + + $options = ParserOptions::newFromUserAndLang( $this->context->getUser(), $wgContLang ); + + $content = $this->newContent( $text ); + $content = $content->preSaveTransform( + $this->context->getTitle(), + $this->context->getUser(), + $options + ); + + $this->assertEquals( $expected, $content->getNativeData() ); + } + + public static function dataPreloadTransform() { + return [ + [ + 'hello this is ~~~', + 'hello this is ~~~', + ], + ]; + } + + /** + * @dataProvider dataPreloadTransform + * @covers TextContent::preloadTransform + */ + public function testPreloadTransform( $text, $expected ) { + global $wgContLang; + $options = ParserOptions::newFromUserAndLang( $this->context->getUser(), $wgContLang ); + + $content = $this->newContent( $text ); + $content = $content->preloadTransform( $this->context->getTitle(), $options ); + + $this->assertEquals( $expected, $content->getNativeData() ); + } + + public static function dataGetRedirectTarget() { + return [ + [ '#REDIRECT [[Test]]', + null, + ], + ]; + } + + /** + * @dataProvider dataGetRedirectTarget + * @covers TextContent::getRedirectTarget + */ + public function testGetRedirectTarget( $text, $expected ) { + $content = $this->newContent( $text ); + $t = $content->getRedirectTarget(); + + if ( is_null( $expected ) ) { + $this->assertNull( $t, "text should not have generated a redirect target: $text" ); + } else { + $this->assertEquals( $expected, $t->getPrefixedText() ); + } + } + + /** + * @dataProvider dataGetRedirectTarget + * @covers TextContent::isRedirect + */ + public function testIsRedirect( $text, $expected ) { + $content = $this->newContent( $text ); + + $this->assertEquals( !is_null( $expected ), $content->isRedirect() ); + } + + public static function dataIsCountable() { + return [ + [ '', + null, + 'any', + true + ], + [ 'Foo', + null, + 'any', + true + ], + ]; + } + + /** + * @dataProvider dataIsCountable + * @covers TextContent::isCountable + */ + public function testIsCountable( $text, $hasLinks, $mode, $expected ) { + $this->setMwGlobals( 'wgArticleCountMethod', $mode ); + + $content = $this->newContent( $text ); + + $v = $content->isCountable( $hasLinks, $this->context->getTitle() ); + + $this->assertEquals( + $expected, + $v, + 'isCountable() returned unexpected value ' . var_export( $v, true ) + . ' instead of ' . var_export( $expected, true ) + . " in mode `$mode` for text \"$text\"" + ); + } + + public static function dataGetTextForSummary() { + return [ + [ "hello\nworld.", + 16, + 'hello world.', + ], + [ 'hello world.', + 8, + 'hello...', + ], + [ '[[hello world]].', + 8, + '[[hel...', + ], + ]; + } + + /** + * @dataProvider dataGetTextForSummary + * @covers TextContent::getTextForSummary + */ + public function testGetTextForSummary( $text, $maxlength, $expected ) { + $content = $this->newContent( $text ); + + $this->assertEquals( $expected, $content->getTextForSummary( $maxlength ) ); + } + + /** + * @covers TextContent::getTextForSearchIndex + */ + public function testGetTextForSearchIndex() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( 'hello world.', $content->getTextForSearchIndex() ); + } + + /** + * @covers TextContent::copy + */ + public function testCopy() { + $content = $this->newContent( 'hello world.' ); + $copy = $content->copy(); + + $this->assertTrue( $content->equals( $copy ), 'copy must be equal to original' ); + $this->assertEquals( 'hello world.', $copy->getNativeData() ); + } + + /** + * @covers TextContent::getSize + */ + public function testGetSize() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( 12, $content->getSize() ); + } + + /** + * @covers TextContent::getNativeData + */ + public function testGetNativeData() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( 'hello world.', $content->getNativeData() ); + } + + /** + * @covers TextContent::getWikitextForTransclusion + */ + public function testGetWikitextForTransclusion() { + $content = $this->newContent( 'hello world.' ); + + $this->assertEquals( 'hello world.', $content->getWikitextForTransclusion() ); + } + + /** + * @covers TextContent::getModel + */ + public function testGetModel() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_TEXT, $content->getModel() ); + } + + /** + * @covers TextContent::getContentHandler + */ + public function testGetContentHandler() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_TEXT, $content->getContentHandler()->getModelID() ); + } + + public static function dataIsEmpty() { + return [ + [ '', true ], + [ ' ', false ], + [ '0', false ], + [ 'hallo welt.', false ], + ]; + } + + /** + * @dataProvider dataIsEmpty + * @covers TextContent::isEmpty + */ + public function testIsEmpty( $text, $empty ) { + $content = $this->newContent( $text ); + + $this->assertEquals( $empty, $content->isEmpty() ); + } + + public static function dataEquals() { + return [ + [ new TextContent( "hallo" ), null, false ], + [ new TextContent( "hallo" ), new TextContent( "hallo" ), true ], + [ new TextContent( "hallo" ), new JavaScriptContent( "hallo" ), false ], + [ new TextContent( "hallo" ), new WikitextContent( "hallo" ), false ], + [ new TextContent( "hallo" ), new TextContent( "HALLO" ), false ], + ]; + } + + /** + * @dataProvider dataEquals + * @covers TextContent::equals + */ + public function testEquals( Content $a, Content $b = null, $equal = false ) { + $this->assertEquals( $equal, $a->equals( $b ) ); + } + + public static function dataGetDeletionUpdates() { + return [ + [ "TextContentTest_testGetSecondaryDataUpdates_1", + CONTENT_MODEL_TEXT, "hello ''world''\n", + [] + ], + [ "TextContentTest_testGetSecondaryDataUpdates_2", + CONTENT_MODEL_TEXT, "hello [[world test 21344]]\n", + [] + ], + // TODO: more...? + ]; + } + + /** + * @dataProvider dataGetDeletionUpdates + * @covers TextContent::getDeletionUpdates + */ + public function testDeletionUpdates( $title, $model, $text, $expectedStuff ) { + $ns = $this->getDefaultWikitextNS(); + $title = Title::newFromText( $title, $ns ); + + $content = ContentHandler::makeContent( $text, $title, $model ); + + $page = WikiPage::factory( $title ); + $page->doEditContent( $content, '' ); + + $updates = $content->getDeletionUpdates( $page ); + + // make updates accessible by class name + foreach ( $updates as $update ) { + $class = get_class( $update ); + $updates[$class] = $update; + } + + if ( !$expectedStuff ) { + $this->assertTrue( true ); // make phpunit happy + return; + } + + foreach ( $expectedStuff as $class => $fieldValues ) { + $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" ); + + $update = $updates[$class]; + + foreach ( $fieldValues as $field => $value ) { + $v = $update->$field; # if the field doesn't exist, just crash and burn + $this->assertEquals( $value, $v, "unexpected value for field $field in instance of $class" ); + } + } + + $page->doDeleteArticle( '' ); + } + + public static function provideConvert() { + return [ + [ // #0 + 'Hallo Welt', + CONTENT_MODEL_WIKITEXT, + 'lossless', + 'Hallo Welt' + ], + [ // #1 + 'Hallo Welt', + CONTENT_MODEL_WIKITEXT, + 'lossless', + 'Hallo Welt' + ], + [ // #1 + 'Hallo Welt', + CONTENT_MODEL_CSS, + 'lossless', + 'Hallo Welt' + ], + [ // #1 + 'Hallo Welt', + CONTENT_MODEL_JAVASCRIPT, + 'lossless', + 'Hallo Welt' + ], + ]; + } + + /** + * @dataProvider provideConvert + * @covers TextContent::convert + */ + public function testConvert( $text, $model, $lossy, $expectedNative ) { + $content = $this->newContent( $text ); + + $converted = $content->convert( $model, $lossy ); + + if ( $expectedNative === false ) { + $this->assertFalse( $converted, "conversion to $model was expected to fail!" ); + } else { + $this->assertInstanceOf( Content::class, $converted ); + $this->assertEquals( $expectedNative, $converted->getNativeData() ); + } + } + + /** + * @covers TextContent::normalizeLineEndings + * @dataProvider provideNormalizeLineEndings + */ + public function testNormalizeLineEndings( $input, $expected ) { + $this->assertEquals( $expected, TextContent::normalizeLineEndings( $input ) ); + } + + public static function provideNormalizeLineEndings() { + return [ + [ + "Foo\r\nbar", + "Foo\nbar" + ], + [ + "Foo\rbar", + "Foo\nbar" + ], + [ + "Foobar\n ", + "Foobar" + ] + ]; + } + +} diff --git a/www/wiki/tests/phpunit/includes/content/WikitextContentHandlerTest.php b/www/wiki/tests/phpunit/includes/content/WikitextContentHandlerTest.php new file mode 100644 index 00000000..59984d85 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/content/WikitextContentHandlerTest.php @@ -0,0 +1,365 @@ +<?php + +/** + * @group ContentHandler + */ +class WikitextContentHandlerTest extends MediaWikiLangTestCase { + /** + * @var ContentHandler + */ + private $handler; + + protected function setUp() { + parent::setUp(); + + $this->handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT ); + } + + /** + * @covers WikitextContentHandler::serializeContent + */ + public function testSerializeContent() { + $content = new WikitextContent( 'hello world' ); + + $this->assertEquals( 'hello world', $this->handler->serializeContent( $content ) ); + $this->assertEquals( + 'hello world', + $this->handler->serializeContent( $content, CONTENT_FORMAT_WIKITEXT ) + ); + + try { + $this->handler->serializeContent( $content, 'dummy/foo' ); + $this->fail( "serializeContent() should have failed on unknown format" ); + } catch ( MWException $e ) { + // ok, as expected + } + } + + /** + * @covers WikitextContentHandler::unserializeContent + */ + public function testUnserializeContent() { + $content = $this->handler->unserializeContent( 'hello world' ); + $this->assertEquals( 'hello world', $content->getNativeData() ); + + $content = $this->handler->unserializeContent( 'hello world', CONTENT_FORMAT_WIKITEXT ); + $this->assertEquals( 'hello world', $content->getNativeData() ); + + try { + $this->handler->unserializeContent( 'hello world', 'dummy/foo' ); + $this->fail( "unserializeContent() should have failed on unknown format" ); + } catch ( MWException $e ) { + // ok, as expected + } + } + + /** + * @covers WikitextContentHandler::makeEmptyContent + */ + public function testMakeEmptyContent() { + $content = $this->handler->makeEmptyContent(); + + $this->assertTrue( $content->isEmpty() ); + $this->assertEquals( '', $content->getNativeData() ); + } + + public static function dataIsSupportedFormat() { + return [ + [ null, true ], + [ CONTENT_FORMAT_WIKITEXT, true ], + [ 99887766, false ], + ]; + } + + /** + * @dataProvider provideMakeRedirectContent + * @param Title|string $title Title object or string for Title::newFromText() + * @param string $expected Serialized form of the content object built + * @covers WikitextContentHandler::makeRedirectContent + */ + public function testMakeRedirectContent( $title, $expected ) { + global $wgContLang; + $wgContLang->resetNamespaces(); + + MagicWord::clearCache(); + + if ( is_string( $title ) ) { + $title = Title::newFromText( $title ); + } + $content = $this->handler->makeRedirectContent( $title ); + $this->assertEquals( $expected, $content->serialize() ); + } + + public static function provideMakeRedirectContent() { + return [ + [ 'Hello', '#REDIRECT [[Hello]]' ], + [ 'Template:Hello', '#REDIRECT [[Template:Hello]]' ], + [ 'Hello#section', '#REDIRECT [[Hello#section]]' ], + [ 'user:john_doe#section', '#REDIRECT [[User:John doe#section]]' ], + [ 'MEDIAWIKI:FOOBAR', '#REDIRECT [[MediaWiki:FOOBAR]]' ], + [ 'Category:Foo', '#REDIRECT [[:Category:Foo]]' ], + [ Title::makeTitle( NS_MAIN, 'en:Foo' ), '#REDIRECT [[en:Foo]]' ], + [ Title::makeTitle( NS_MAIN, 'Foo', '', 'en' ), '#REDIRECT [[:en:Foo]]' ], + [ + Title::makeTitle( NS_MAIN, 'Bar', 'fragment', 'google' ), + '#REDIRECT [[google:Bar#fragment]]' + ], + ]; + } + + /** + * @dataProvider dataIsSupportedFormat + * @covers WikitextContentHandler::isSupportedFormat + */ + public function testIsSupportedFormat( $format, $supported ) { + $this->assertEquals( $supported, $this->handler->isSupportedFormat( $format ) ); + } + + /** + * @covers WikitextContentHandler::supportsDirectEditing + */ + public function testSupportsDirectEditing() { + $handler = new WikiTextContentHandler(); + $this->assertTrue( $handler->supportsDirectEditing(), 'direct editing is supported' ); + } + + public static function dataMerge3() { + return [ + [ + "first paragraph + + second paragraph\n", + + "FIRST paragraph + + second paragraph\n", + + "first paragraph + + SECOND paragraph\n", + + "FIRST paragraph + + SECOND paragraph\n", + ], + + [ "first paragraph + second paragraph\n", + + "Bla bla\n", + + "Blubberdibla\n", + + false, + ], + ]; + } + + /** + * @dataProvider dataMerge3 + * @covers WikitextContentHandler::merge3 + */ + public function testMerge3( $old, $mine, $yours, $expected ) { + $this->markTestSkippedIfNoDiff3(); + + // test merge + $oldContent = new WikitextContent( $old ); + $myContent = new WikitextContent( $mine ); + $yourContent = new WikitextContent( $yours ); + + $merged = $this->handler->merge3( $oldContent, $myContent, $yourContent ); + + $this->assertEquals( $expected, $merged ? $merged->getNativeData() : $merged ); + } + + public static function dataGetAutosummary() { + return [ + [ + 'Hello there, world!', + '#REDIRECT [[Foo]]', + 0, + '/^Redirected page .*Foo/' + ], + + [ + null, + 'Hello world!', + EDIT_NEW, + '/^Created page .*Hello/' + ], + + [ + null, + '', + EDIT_NEW, + '/^Created blank page$/' + ], + + [ + 'Hello there, world!', + '', + 0, + '/^Blanked/' + ], + + [ + 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy + eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam + voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet + clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', + 'Hello world!', + 0, + '/^Replaced .*Hello/' + ], + + [ + 'foo', + 'bar', + 0, + '/^$/' + ], + ]; + } + + /** + * @dataProvider dataGetAutosummary + * @covers WikitextContentHandler::getAutosummary + */ + public function testGetAutosummary( $old, $new, $flags, $expected ) { + $oldContent = is_null( $old ) ? null : new WikitextContent( $old ); + $newContent = is_null( $new ) ? null : new WikitextContent( $new ); + + $summary = $this->handler->getAutosummary( $oldContent, $newContent, $flags ); + + $this->assertTrue( + (bool)preg_match( $expected, $summary ), + "Autosummary didn't match expected pattern $expected: $summary" + ); + } + + public static function dataGetChangeTag() { + return [ + [ + null, + '#REDIRECT [[Foo]]', + 0, + 'mw-new-redirect' + ], + + [ + 'Lorem ipsum dolor', + '#REDIRECT [[Foo]]', + 0, + 'mw-new-redirect' + ], + + [ + '#REDIRECT [[Foo]]', + 'Lorem ipsum dolor', + 0, + 'mw-removed-redirect' + ], + + [ + '#REDIRECT [[Foo]]', + '#REDIRECT [[Bar]]', + 0, + 'mw-changed-redirect-target' + ], + + [ + null, + 'Lorem ipsum dolor', + EDIT_NEW, + null // mw-newpage is not defined as a tag + ], + + [ + null, + '', + EDIT_NEW, + null // mw-newblank is not defined as a tag + ], + + [ + 'Lorem ipsum dolor', + '', + 0, + 'mw-blank' + ], + + [ + 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy + eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam + voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet + clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', + 'Ipsum', + 0, + 'mw-replace' + ], + + [ + 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy + eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam + voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet + clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.', + 'Duis purus odio, rhoncus et finibus dapibus, facilisis ac urna. Pellentesque + arcu, tristique nec tempus nec, suscipit vel arcu. Sed non dolor nec ligula + congue tempor. Quisque pellentesque finibus orci a molestie. Nam maximus, purus + euismod finibus mollis, dui ante malesuada felis, dignissim rutrum diam sapien.', + 0, + null + ], + ]; + } + + /** + * @dataProvider dataGetChangeTag + * @covers WikitextContentHandler::getChangeTag + */ + public function testGetChangeTag( $old, $new, $flags, $expected ) { + $this->setMwGlobals( 'wgSoftwareTags', [ + 'mw-new-redirect' => true, + 'mw-removed-redirect' => true, + 'mw-changed-redirect-target' => true, + 'mw-newpage' => true, + 'mw-newblank' => true, + 'mw-blank' => true, + 'mw-replace' => true, + ] ); + $oldContent = is_null( $old ) ? null : new WikitextContent( $old ); + $newContent = is_null( $new ) ? null : new WikitextContent( $new ); + + $tag = $this->handler->getChangeTag( $oldContent, $newContent, $flags ); + + $this->assertSame( $expected, $tag ); + } + + /** + * @covers WikitextContentHandler::getDataForSearchIndex + */ + public function testDataIndexFieldsFile() { + $mockEngine = $this->createMock( SearchEngine::class ); + $title = Title::newFromText( 'Somefile.jpg', NS_FILE ); + $page = new WikiPage( $title ); + + $fileHandler = $this->getMockBuilder( FileContentHandler::class ) + ->disableOriginalConstructor() + ->setMethods( [ 'getDataForSearchIndex' ] ) + ->getMock(); + + $handler = $this->getMockBuilder( WikitextContentHandler::class ) + ->disableOriginalConstructor() + ->setMethods( [ 'getFileHandler' ] ) + ->getMock(); + + $handler->method( 'getFileHandler' )->will( $this->returnValue( $fileHandler ) ); + $fileHandler->expects( $this->once() ) + ->method( 'getDataForSearchIndex' ) + ->will( $this->returnValue( [ 'file_text' => 'This is file content' ] ) ); + + $data = $handler->getDataForSearchIndex( $page, new ParserOutput(), $mockEngine ); + $this->assertArrayHasKey( 'file_text', $data ); + $this->assertEquals( 'This is file content', $data['file_text'] ); + } +} diff --git a/www/wiki/tests/phpunit/includes/content/WikitextContentTest.php b/www/wiki/tests/phpunit/includes/content/WikitextContentTest.php new file mode 100644 index 00000000..1db6aab6 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/content/WikitextContentTest.php @@ -0,0 +1,443 @@ +<?php + +/** + * @group ContentHandler + * + * @group Database + * ^--- needed, because we do need the database to test link updates + */ +class WikitextContentTest extends TextContentTest { + public static $sections = "Intro + +== stuff == +hello world + +== test == +just a test + +== foo == +more stuff +"; + + public function newContent( $text ) { + return new WikitextContent( $text ); + } + + public static function dataGetParserOutput() { + return [ + [ + "WikitextContentTest_testGetParserOutput", + CONTENT_MODEL_WIKITEXT, + "hello ''world''\n", + "<div class=\"mw-parser-output\"><p>hello <i>world</i>\n</p>\n\n\n</div>" + ], + // TODO: more...? + ]; + } + + public static function dataGetSecondaryDataUpdates() { + return [ + [ "WikitextContentTest_testGetSecondaryDataUpdates_1", + CONTENT_MODEL_WIKITEXT, "hello ''world''\n", + [ + LinksUpdate::class => [ + 'mRecursive' => true, + 'mLinks' => [] + ] + ] + ], + [ "WikitextContentTest_testGetSecondaryDataUpdates_2", + CONTENT_MODEL_WIKITEXT, "hello [[world test 21344]]\n", + [ + LinksUpdate::class => [ + 'mRecursive' => true, + 'mLinks' => [ + [ 'World_test_21344' => 0 ] + ] + ] + ] + ], + // TODO: more...? + ]; + } + + /** + * @dataProvider dataGetSecondaryDataUpdates + * @group Database + * @covers WikitextContent::getSecondaryDataUpdates + */ + public function testGetSecondaryDataUpdates( $title, $model, $text, $expectedStuff ) { + $ns = $this->getDefaultWikitextNS(); + $title = Title::newFromText( $title, $ns ); + + $content = ContentHandler::makeContent( $text, $title, $model ); + + $page = WikiPage::factory( $title ); + $page->doEditContent( $content, '' ); + + $updates = $content->getSecondaryDataUpdates( $title ); + + // make updates accessible by class name + foreach ( $updates as $update ) { + $class = get_class( $update ); + $updates[$class] = $update; + } + + foreach ( $expectedStuff as $class => $fieldValues ) { + $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" ); + + $update = $updates[$class]; + + foreach ( $fieldValues as $field => $value ) { + $v = $update->$field; # if the field doesn't exist, just crash and burn + $this->assertEquals( + $value, + $v, + "unexpected value for field $field in instance of $class" + ); + } + } + + $page->doDeleteArticle( '' ); + } + + public static function dataGetSection() { + return [ + [ self::$sections, + "0", + "Intro" + ], + [ self::$sections, + "2", + "== test == +just a test" + ], + [ self::$sections, + "8", + false + ], + ]; + } + + /** + * @dataProvider dataGetSection + * @covers WikitextContent::getSection + */ + public function testGetSection( $text, $sectionId, $expectedText ) { + $content = $this->newContent( $text ); + + $sectionContent = $content->getSection( $sectionId ); + if ( is_object( $sectionContent ) ) { + $sectionText = $sectionContent->getNativeData(); + } else { + $sectionText = $sectionContent; + } + + $this->assertEquals( $expectedText, $sectionText ); + } + + public static function dataReplaceSection() { + return [ + [ self::$sections, + "0", + "No more", + null, + trim( preg_replace( '/^Intro/sm', 'No more', self::$sections ) ) + ], + [ self::$sections, + "", + "No more", + null, + "No more" + ], + [ self::$sections, + "2", + "== TEST ==\nmore fun", + null, + trim( preg_replace( + '/^== test ==.*== foo ==/sm', "== TEST ==\nmore fun\n\n== foo ==", + self::$sections + ) ) + ], + [ self::$sections, + "8", + "No more", + null, + self::$sections + ], + [ self::$sections, + "new", + "No more", + "New", + trim( self::$sections ) . "\n\n\n== New ==\n\nNo more" + ], + ]; + } + + /** + * @dataProvider dataReplaceSection + * @covers WikitextContent::replaceSection + */ + public function testReplaceSection( $text, $section, $with, $sectionTitle, $expected ) { + $content = $this->newContent( $text ); + $c = $content->replaceSection( $section, $this->newContent( $with ), $sectionTitle ); + + $this->assertEquals( $expected, is_null( $c ) ? null : $c->getNativeData() ); + } + + /** + * @covers WikitextContent::addSectionHeader + */ + public function testAddSectionHeader() { + $content = $this->newContent( 'hello world' ); + $content = $content->addSectionHeader( 'test' ); + + $this->assertEquals( "== test ==\n\nhello world", $content->getNativeData() ); + } + + public static function dataPreSaveTransform() { + return [ + [ 'hello this is ~~~', + "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]", + ], + [ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>', + 'hello \'\'this\'\' is <nowiki>~~~</nowiki>', + ], + [ // rtrim + " Foo \n ", + " Foo", + ], + ]; + } + + public static function dataPreloadTransform() { + return [ + [ + 'hello this is ~~~', + "hello this is ~~~", + ], + [ + 'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>', + 'hello \'\'this\'\' is bar', + ], + ]; + } + + public static function dataGetRedirectTarget() { + return [ + [ '#REDIRECT [[Test]]', + 'Test', + ], + [ '#REDIRECT Test', + null, + ], + [ '* #REDIRECT [[Test]]', + null, + ], + ]; + } + + public static function dataGetTextForSummary() { + return [ + [ "hello\nworld.", + 16, + 'hello world.', + ], + [ 'hello world.', + 8, + 'hello...', + ], + [ '[[hello world]].', + 8, + 'hel...', + ], + ]; + } + + public static function dataIsCountable() { + return [ + [ '', + null, + 'any', + true + ], + [ 'Foo', + null, + 'any', + true + ], + [ 'Foo', + null, + 'link', + false + ], + [ 'Foo [[bar]]', + null, + 'link', + true + ], + [ 'Foo', + true, + 'link', + true + ], + [ 'Foo [[bar]]', + false, + 'link', + false + ], + [ '#REDIRECT [[bar]]', + true, + 'any', + false + ], + [ '#REDIRECT [[bar]]', + true, + 'link', + false + ], + ]; + } + + /** + * @covers WikitextContent::matchMagicWord + */ + public function testMatchMagicWord() { + $mw = MagicWord::get( "staticredirect" ); + + $content = $this->newContent( "#REDIRECT [[FOO]]\n__STATICREDIRECT__" ); + $this->assertTrue( $content->matchMagicWord( $mw ), "should have matched magic word" ); + + $content = $this->newContent( "#REDIRECT [[FOO]]" ); + $this->assertFalse( + $content->matchMagicWord( $mw ), + "should not have matched magic word" + ); + } + + /** + * @covers WikitextContent::updateRedirect + */ + public function testUpdateRedirect() { + $target = Title::newFromText( "testUpdateRedirect_target" ); + + // test with non-redirect page + $content = $this->newContent( "hello world." ); + $newContent = $content->updateRedirect( $target ); + + $this->assertTrue( $content->equals( $newContent ), "content should be unchanged" ); + + // test with actual redirect + $content = $this->newContent( "#REDIRECT [[Someplace]]" ); + $newContent = $content->updateRedirect( $target ); + + $this->assertFalse( $content->equals( $newContent ), "content should have changed" ); + $this->assertTrue( $newContent->isRedirect(), "new content should be a redirect" ); + + $this->assertEquals( + $target->getFullText(), + $newContent->getRedirectTarget()->getFullText() + ); + } + + /** + * @covers WikitextContent::getModel + */ + public function testGetModel() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getModel() ); + } + + /** + * @covers WikitextContent::getContentHandler + */ + public function testGetContentHandler() { + $content = $this->newContent( "hello world." ); + + $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getContentHandler()->getModelID() ); + } + + public function testRedirectParserOption() { + $title = Title::newFromText( 'testRedirectParserOption' ); + + // Set up hook and its reporting variables + $wikitext = null; + $redirectTarget = null; + $this->mergeMwGlobalArrayValue( 'wgHooks', [ + 'InternalParseBeforeLinks' => [ + function ( &$parser, &$text, &$stripState ) use ( &$wikitext, &$redirectTarget ) { + $wikitext = $text; + $redirectTarget = $parser->getOptions()->getRedirectTarget(); + } + ] + ] ); + + // Test with non-redirect page + $wikitext = false; + $redirectTarget = false; + $content = $this->newContent( 'hello world.' ); + $options = $content->getContentHandler()->makeParserOptions( 'canonical' ); + $options->setRedirectTarget( $title ); + $content->getParserOutput( $title, null, $options ); + $this->assertEquals( 'hello world.', $wikitext, + 'Wikitext passed to hook was not as expected' + ); + $this->assertEquals( null, $redirectTarget, 'Redirect seen in hook was not null' ); + $this->assertEquals( $title, $options->getRedirectTarget(), + 'ParserOptions\' redirectTarget was changed' + ); + + // Test with a redirect page + $wikitext = false; + $redirectTarget = false; + $content = $this->newContent( + "#REDIRECT [[TestRedirectParserOption/redir]]\nhello redirect." + ); + $options = $content->getContentHandler()->makeParserOptions( 'canonical' ); + $content->getParserOutput( $title, null, $options ); + $this->assertEquals( + 'hello redirect.', + $wikitext, + 'Wikitext passed to hook was not as expected' + ); + $this->assertNotEquals( + null, + $redirectTarget, + 'Redirect seen in hook was null' ); + $this->assertEquals( + 'TestRedirectParserOption/redir', + $redirectTarget->getFullText(), + 'Redirect seen in hook was not the expected title' + ); + $this->assertEquals( + null, + $options->getRedirectTarget(), + 'ParserOptions\' redirectTarget was changed' + ); + } + + public static function dataEquals() { + return [ + [ new WikitextContent( "hallo" ), null, false ], + [ new WikitextContent( "hallo" ), new WikitextContent( "hallo" ), true ], + [ new WikitextContent( "hallo" ), new JavaScriptContent( "hallo" ), false ], + [ new WikitextContent( "hallo" ), new TextContent( "hallo" ), false ], + [ new WikitextContent( "hallo" ), new WikitextContent( "HALLO" ), false ], + ]; + } + + public static function dataGetDeletionUpdates() { + return [ + [ "WikitextContentTest_testGetSecondaryDataUpdates_1", + CONTENT_MODEL_WIKITEXT, "hello ''world''\n", + [ LinksDeletionUpdate::class => [] ] + ], + [ "WikitextContentTest_testGetSecondaryDataUpdates_2", + CONTENT_MODEL_WIKITEXT, "hello [[world test 21344]]\n", + [ LinksDeletionUpdate::class => [] ] + ], + // @todo more...? + ]; + } +} diff --git a/www/wiki/tests/phpunit/includes/content/WikitextStructureTest.php b/www/wiki/tests/phpunit/includes/content/WikitextStructureTest.php new file mode 100644 index 00000000..88f4d8f7 --- /dev/null +++ b/www/wiki/tests/phpunit/includes/content/WikitextStructureTest.php @@ -0,0 +1,110 @@ +<?php + +/** + * @covers WikiTextStructure + */ +class WikitextStructureTest extends MediaWikiLangTestCase { + + private function getMockTitle() { + return Title::newFromText( "TestTitle" ); + } + + /** + * Get parser output for Wiki text + * @param string $text + * @return ParserOutput + */ + private function getParserOutput( $text ) { + $content = new WikitextContent( $text ); + return $content->getParserOutput( $this->getMockTitle() ); + } + + /** + * Get WikitextStructure for given text + * @param string $text + * @return WikiTextStructure + */ + private function getStructure( $text ) { + return new WikiTextStructure( $this->getParserOutput( $text ) ); + } + + public function testHeadings() { + $text = <<<END +Some text here +== Heading one == +Some text +==== heading two ==== +More text +=== Applicability of the strict mass-energy equivalence formula, ''E'' = ''mc''<sup>2</sup> === +and more text +== Wikitext '''in''' [[Heading]] and also <b>html</b> == +more text +==== See also ==== +* Also things to see! +END; + $struct = $this->getStructure( $text ); + $headings = $struct->headings(); + $this->assertCount( 4, $headings ); + $this->assertContains( "Heading one", $headings ); + $this->assertContains( "heading two", $headings ); + $this->assertContains( "Applicability of the strict mass-energy equivalence formula, E = mc2", + $headings ); + $this->assertContains( "Wikitext in Heading and also html", $headings ); + } + + public function testDefaultSort() { + $text = <<<END +Louise Michel +== Heading one == +Some text +==== See also ==== +* Also things to see! +{{DEFAULTSORT:Michel, Louise}} +END; + $struct = $this->getStructure( $text ); + $this->assertEquals( "Michel, Louise", $struct->getDefaultSort() ); + } + + public function testHeadingsFirst() { + $text = <<<END +== Heading one == +Some text +==== heading two ==== +END; + $struct = $this->getStructure( $text ); + $headings = $struct->headings(); + $this->assertCount( 2, $headings ); + $this->assertContains( "Heading one", $headings ); + $this->assertContains( "heading two", $headings ); + } + + public function testHeadingsNone() { + $text = "This text is completely devoid of headings."; + $struct = $this->getStructure( $text ); + $headings = $struct->headings(); + $this->assertArrayEquals( [], $headings ); + } + + public function testTexts() { + $text = <<<END +Opening text is opening. +== Then comes header == +Then we got more<br>text +=== And more headers === +{| class="wikitable" +|- +! Header table +|- +| row in table +|- +| another row in table +|} +END; + $struct = $this->getStructure( $text ); + $this->assertEquals( "Opening text is opening.", $struct->getOpeningText() ); + $this->assertEquals( "Opening text is opening. Then we got more text", + $struct->getMainText() ); + $this->assertEquals( [ "Header table row in table another row in table" ], + $struct->getAuxiliaryText() ); + } +} |