[user preference => preference value]] */ protected $testUsers = [ 'Werdna' => [ 'nickname' => '', 'fancysig' => '0', ], 'Werdna2' => [ 'nickname' => '[[User:Werdna2|Andrew]]', 'fancysig' => '1', ], 'Werdna3' => [ 'nickname' => '[[User talk:Werdna3|Andrew]]', 'fancysig' => '1', ], 'Werdna4' => [ 'nickname' => '[[User:Werdna4|wer]dna]]', 'fancysig' => '1', ], 'We buried our secrets in the garden' => [ 'nickname' => '[[User talk:We buried our secrets in the garden#top|wbositg]]', 'fancysig' => '1', ], 'I Heart Spaces' => [ 'nickname' => '[[User:I_Heart_Spaces]] ([[User_talk:I_Heart_Spaces]])', 'fancysig' => '1', ], 'Jam' => [ 'nickname' => '[[User:Jam]]', 'fancysig' => '1', ], 'Reverta-me' => [ 'nickname' => "[[User:Reverta-me|Aaaaa Bbbbbbb]]'' [[User Talk:Reverta-me|Discussão]]''", 'fancysig' => '1', ], 'Jorm' => [ 'nickname' => '', 'fancysig' => '0', ], 'Jdforrester' => [ 'nickname' => '', 'fancysig' => '0', ], 'DarTar' => [ 'nickname' => '', 'fancysig' => '0', ], 'Bsitu' => [ 'nickname' => '', 'fancysig' => '0', ], 'JarJar' => [ 'nickname' => '', 'fancysig' => '0', ], 'Schnark' => [ 'nickname' => '[[Benutzer:Schnark]] ([[Benutzer:Schnark/js|js]])', 'fancysig' => '1', ], 'Cwobeel' => [ 'nickname' => '[[User:Cwobeel|Cwobeel]] [[User_talk:Cwobeel|(talk)]]', 'fancysig' => '1', ], 'Bob K31416' => [ 'nickname' => '', 'fancysig' => '0', ], 'X" onclick="alert(\'XSS\');" title="y' => [ 'nickname' => '', 'fancysig' => '0', ], 'He7d3r' => [ 'nickname' => '', 'fancysig' => '0', ], 'PauloEduardo' => [ 'nickname' => "[[User:PauloEduardo|Paulo Eduardo]]'' [[User Talk:PauloEduardo|Discussão]]''", 'fancysig' => '1', ], 'PatHadley' => [ 'nickname' => '', 'fancysig' => '0', ], 'Samwalton9' => [ 'nickname' => '', 'fancysig' => '0', ], 'Kudpung' => [ 'nickname' => '[[User:Kudpung|Kudpung กุดผึ้ง]] ([[User talk:Kudpung#top|talk]])', 'fancysig' => '1', ], 'Jim Carter' => [ 'nickname' => '', 'fancysig' => '0', ], 'Buster7' => [ 'nickname' => '', 'fancysig' => '0', ], 'Admin' => [ 'nickname' => '[[:User:Admin|Admin]]', 'fancysig' => '1', ], 'Test11' => [ 'nickname' => '', 'fancysig' => '0', ], ]; protected function setUp() : void { parent::setUp(); $this->setMwGlobals( [ 'wgDiff' => false ] ); } protected function tearDown() : void { parent::tearDown(); global $wgHooks; unset( $wgHooks['BeforeEchoEventInsert'][999] ); } private function setupAllTestUsers() { foreach ( array_keys( $this->testUsers ) as $username ) { $this->setupTestUser( $username ); } } private function setupTestUser( $username ) { // Skip user creation requests that are not in the list (such as IPs) if ( !array_key_exists( $username, $this->testUsers ) ) { return; } $preferences = $this->testUsers[$username]; $user = User::createNew( $username ); // Set preferences if ( $user ) { foreach ( $preferences as $option => $value ) { $user->setOption( $option, $value ); } $user->saveSettings(); } } public function provideHeaderExtractions() { return [ [ '', false ], [ '== Grand jury no bill reception ==', 'Grand jury no bill reception' ], [ '=== Echo-Test ===', 'Echo-Test' ], [ '==== Notificações ====', 'Notificações' ], [ '=====Me?=====', 'Me?' ], ]; } /** * @dataProvider provideHeaderExtractions */ public function testExtractHeader( $text, $expected ) { $this->assertEquals( $expected, EchoDiscussionParser::extractHeader( $text ) ); } public function generateEventsForRevisionData() { return [ [ 'new' => 637638133, 'old' => 637637213, 'username' => 'Cwobeel', 'lang' => 'en', 'pages' => [ // pages expected to exist (e.g. templates to be expanded) 'Template:u' => '[[User:{{{1}}}|{{safesubst:#if:{{{2|}}}|{{{2}}}|{{{1}}}}}]]{{documentation}}', ], 'title' => 'UTPage', // can't remember, not important here 'expected' => [ // events expected to be fired going from old revision to new [ 'type' => 'mention', 'agent' => 'Cwobeel', 'section-title' => 'Grand jury no bill reception', /* * I wish I could also compare EchoEvent::$extra data to * compare user ids of mentioned users. However, due to * How PHPUnit works, setUp won't be run by the time * this dataset is generated, so we don't yet know the * user ids of the folks we're about to insert... * I'll skip that part for now. */ ], ], ], [ 'new' => 138275105, 'old' => 138274875, 'username' => 'Schnark', 'lang' => 'de', 'pages' => [], 'title' => 'UTPage', // can't remember, not important here 'expected' => [ [ 'type' => 'mention', 'agent' => 'Schnark', 'section-title' => 'Echo-Test', ], ], ], [ 'new' => 40610292, 'old' => 40608353, 'username' => 'PauloEduardo', 'lang' => 'pt', 'pages' => [ 'Predefinição:U' => '[[User:{{{1|Exemplo}}}|{{{{{|safesubst:}}}#if:{{{2|}}}|{{{2}}}|{{{1|Exemplo}}}}}]]{{Atalho|Predefinição:U}}{{Documentação|Predefinição:Usuário/doc}}', ], 'title' => 'UTPage', // can't remember, not important here 'expected' => [ [ 'type' => 'mention', 'agent' => 'PauloEduardo', 'section-title' => 'Notificações', ], ], ], [ 'new' => 646792804, 'old' => 646790570, 'username' => 'PatHadley', 'lang' => 'en', 'pages' => [ 'Template:ping' => '{{SAFESUBST:#if:{{{1|$}}} |@[[:User:{{SAFESUBST:BASEPAGENAME:{{{1|Example}}}}}|{{SAFESUBST:BASEPAGENAME:{{{label1|{{{1|Example}}}}}}}}]]{{SAFESUBST:#if:{{{2|}}} |, [[:User:{{SAFESUBST:BASEPAGENAME:{{{2|Example}}}}}|{{SAFESUBST:BASEPAGENAME:{{{label2|{{{2|Example}}}}}}}}]]{{SAFESUBST:#if:{{{3|}}} |, [[:User:{{SAFESUBST:BASEPAGENAME:{{{3|Example}}}}}|{{SAFESUBST:BASEPAGENAME:{{{label3|{{{3|Example}}}}}}}}]]{{SAFESUBST:#if:{{{4|}}} |, [[:User:{{SAFESUBST:BASEPAGENAME:{{{4|Example}}}}}|{{SAFESUBST:BASEPAGENAME:{{{label4|{{{4|Example}}}}}}}}]]{{SAFESUBST:#if:{{{5|}}} |, [[:User:{{SAFESUBST:BASEPAGENAME:{{{5|Example}}}}}|{{SAFESUBST:BASEPAGENAME:{{{label5|{{{5|Example}}}}}}}}]]{{SAFESUBST:#if:{{{6|}}} |, [[:User:{{SAFESUBST:BASEPAGENAME:{{{6|Example}}}}}|{{SAFESUBST:BASEPAGENAME:{{{label6|{{{6|Example}}}}}}}}]]{{SAFESUBST:#if:{{{7|}}} |, [[:User:{{SAFESUBST:BASEPAGENAME:{{{7|Example}}}}}|{{SAFESUBST:BASEPAGENAME:{{{label7|{{{7|Example}}}}}}}}]] }} }} }} }} }} }}{{{p|:}}} |{{SAFESUBST:Error|Error in [[Template:Replyto]]: Username not given.}} }} {{documentation}} ', 'MediaWiki:Signature' => '[[User:$1|$2]] {{#ifeq:{{FULLPAGENAME}}|User talk:$1|([[User talk:$1#top|talk]])|([[User talk:$1|talk]])}}', ], 'title' => 'User_talk:PatHadley', 'expected' => [ [ 'type' => 'mention', 'agent' => 'PatHadley', 'section-title' => 'Wizardry required', ], [ 'type' => 'edit-user-talk', 'agent' => 'PatHadley', 'section-title' => 'Wizardry required', ], ], 'precondition' => 'isParserFunctionsInstalled', ], [ 'new' => 647260329, 'old' => 647258025, 'username' => 'Kudpung', 'lang' => 'en', 'pages' => [ 'Template:U' => '[[User:{{{1}}}|{{safesubst:#if:{{{2|}}}|{{{2}}}|{{{1}}}}}]]{{documentation}}', ], 'title' => 'User_talk:Kudpung', 'expected' => [ [ 'type' => 'mention', 'agent' => 'Kudpung', 'section-title' => 'Me?', ], [ 'type' => 'edit-user-talk', 'agent' => 'Kudpung', 'section-title' => 'Me?', ], ], ], // T68512, leading colon in user page link in signature [ 'new' => 612485855, 'old' => 612485595, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'User_talk:Admin', 'expected' => [ [ 'type' => 'mention', 'agent' => 'Admin', 'section-title' => 'Hi', ], [ 'type' => 'edit-user-talk', 'agent' => 'Admin', 'section-title' => 'Hi', ], ], 'precondition' => 'isParserFunctionsInstalled', ], // T154406 unintended mentions when changing content [ 'new' => 987667999, 'old' => 987667998, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'MultipleSignatureMentions', 'expected' => [], ], [ 'new' => 1234, 'old' => 123, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'Pings in summary', 'expected' => [ [ 'type' => 'mention-summary', 'agent' => 'Admin', 'section-title' => null, ] ], 'precondition' => '', 'summary' => 'Hey [[User:Werdna|Werdna]] and [[User:Jorm]], [[User:Admin]] here', ], ]; } /** * @dataProvider generateEventsForRevisionData */ public function testGenerateEventsForRevision( $newId, $oldId, $username, $lang, $pages, $title, $expected, $precondition = '', $summary = '' ) { if ( $precondition !== '' ) { $result = $this->$precondition(); if ( $result !== true ) { $this->markTestSkipped( $result ); return; } } $this->setupAllTestUsers(); $revision = $this->setupTestRevisionsForEventGeneration( $newId, $oldId, $username, $lang, $pages, $title, $summary ); $events = []; $this->setupEventCallbackForEventGeneration( function ( EchoEvent $event ) use ( &$events ) { $events[] = [ 'type' => $event->getType(), 'agent' => $event->getAgent()->getName(), 'section-title' => $event->getExtraParam( 'section-title' ), ]; return false; } ); $this->setMwGlobals( [ // disable mention failure and success notifications 'wgEchoMentionStatusNotifications' => false, // enable pings from summary 'wgEchoMaxMentionsInEditSummary' => 5, ] ); EchoDiscussionParser::generateEventsForRevision( $revision, false ); $this->assertEquals( $expected, $events ); } public function provider_generateEventsForRevision_mentionStatus() { return [ [ 'new' => 747747748, 'old' => 747747747, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'UTPage', 'expected' => [ [ 'type' => 'mention-failure', 'agent' => 'Admin', 'section-title' => 'Hello Users', 'subject-name' => 'Ping', ], [ 'type' => 'mention-failure', 'agent' => 'Admin', 'section-title' => 'Hello Users', 'subject-name' => 'Po?ng', ], ], ], [ 'new' => 747747750, 'old' => 747747747, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'UTPage', 'expected' => [ [ 'type' => 'mention', 'agent' => 'Admin', 'section-title' => 'Hello Users', 'subject-name' => null, ], [ 'type' => 'mention-success', 'agent' => 'Admin', 'section-title' => 'Hello Users', 'subject-name' => 'Test11', ], ], ], [ 'new' => 747798766, 'old' => 747798765, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'UTPage', 'expected' => [ [ 'type' => 'mention-failure', 'agent' => 'Admin', 'section-title' => 'Section 2', 'subject-name' => 'NoUser', ], ], ], [ 'new' => 747798767, 'old' => 747798765, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'UTPage', 'expected' => [ [ 'type' => 'mention-failure', 'agent' => 'Admin', 'section-title' => 'Section 2', 'subject-name' => 'NoUser', ], ], ], [ 'new' => 747798768, 'old' => 747798765, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'UTPage', 'expected' => [], ], [ 'new' => 747798770, 'old' => 747798765, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'UTPage', 'expected' => [ [ 'type' => 'mention', 'agent' => 'Admin', 'section-title' => 'Section 1.5', 'subject-name' => null, ], [ 'type' => 'mention-success', 'agent' => 'Admin', 'section-title' => 'Section 1.5', 'subject-name' => 'Test11', ], ], ], [ 'new' => 747798771, 'old' => 747798765, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'UTPage', 'expected' => [ [ 'type' => 'mention-failure', 'agent' => 'Admin', 'section-title' => 'Section 1.5', 'subject-name' => 'NoUser1.5', ], [ 'type' => 'mention-failure', 'agent' => 'Admin', 'section-title' => 'Section 2', 'subject-name' => 'NoUser2', ], ], ], [ 'new' => 747798772, 'old' => 747798765, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'UTPage', 'expected' => [ [ 'type' => 'mention-failure', 'agent' => 'Admin', 'section-title' => 'Section 1', 'subject-name' => 'NoUser1', ], [ 'type' => 'mention-failure', 'agent' => 'Admin', 'section-title' => 'Section 1.75', 'subject-name' => 'NoUser1.75', ], [ 'type' => 'mention-failure', 'agent' => 'Admin', 'section-title' => 'Section 2', 'subject-name' => 'NoUser2', ], ], [ 'new' => 987654322, 'old' => 987654321, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'User_talk:Admin', 'expected' => [ [ 'type' => 'edit-user-talk', 'agent' => 'Admin', 'section-title' => false, 'subject-name' => null, ] ], ], [ 'new' => 987654323, 'old' => 987654321, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'User_talk:Admin', 'expected' => [ [ 'type' => 'mention', 'agent' => 'Admin', 'section-title' => 'Section 1', 'subject-name' => null, ], [ 'type' => 'mention-success', 'agent' => 'Admin', 'section-title' => 'Section 1', 'subject-name' => 'Test11', ], [ 'type' => 'edit-user-talk', 'agent' => 'Admin', 'section-title' => 'Section 1', 'subject-name' => null, ], ], ], ], [ 'new' => 987654324, 'old' => 987654321, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'User_talk:Admin', 'expected' => [ [ 'type' => 'mention', 'agent' => 'Admin', 'section-title' => 'Section 1', 'subject-name' => null, ], [ 'type' => 'mention-success', 'agent' => 'Admin', 'section-title' => 'Section 1', 'subject-name' => 'Test11', ], [ 'type' => 'edit-user-talk', 'agent' => 'Admin', 'section-title' => false, 'subject-name' => null, ], ], ], [ 'new' => 987654325, 'old' => 987654321, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'User_talk:Admin', 'expected' => [ [ 'type' => 'mention', 'agent' => 'Admin', 'section-title' => 'Section 2', 'subject-name' => null, ], [ 'type' => 'mention-success', 'agent' => 'Admin', 'section-title' => 'Section 2', 'subject-name' => 'Test11', ], [ 'type' => 'edit-user-talk', 'agent' => 'Admin', 'section-title' => 'Section 2', 'subject-name' => null, ], ], ], [ 'new' => 987654401, 'old' => 987654400, 'username' => 'Admin', 'lang' => 'en', 'pages' => [], 'title' => 'UTPage2', 'expected' => [ [ 'type' => 'mention', 'agent' => 'Admin', 'section-title' => false, 'subject-name' => null, ], [ 'type' => 'mention-success', 'agent' => 'Admin', 'section-title' => false, 'subject-name' => 'Test11', ], ], ], ]; } /** * @dataProvider provider_generateEventsForRevision_mentionStatus */ public function testGenerateEventsForRevision_mentionStatus( $newId, $oldId, $username, $lang, $pages, $title, $expected ) { $this->setupAllTestUsers(); $revision = $this->setupTestRevisionsForEventGeneration( $newId, $oldId, $username, $lang, $pages, $title ); $events = []; $this->setupEventCallbackForEventGeneration( function ( EchoEvent $event ) use ( &$events ) { $events[] = [ 'type' => $event->getType(), 'agent' => $event->getAgent()->getName(), 'section-title' => $event->getExtraParam( 'section-title' ), 'subject-name' => $event->getExtraParam( 'subject-name' ), ]; return false; } ); // enable mention failure and success notifications $this->setMwGlobals( 'wgEchoMentionStatusNotifications', true ); // enable multiple sections mentions $this->setMwGlobals( 'wgEchoMentionsOnMultipleSectionEdits', true ); EchoDiscussionParser::generateEventsForRevision( $revision, false ); $this->assertEquals( $expected, $events ); } public function provider_extractSections() { return [ [ 'content' => 'Just Text.', 'result' => [ [ 'header' => false, 'content' => 'Just Text.', ], ], ], [ 'content' => << [ [ 'header' => false, 'content' => << 'Headline', 'content' => << << [ [ 'header' => 'Headline', 'content' => << << [ [ 'header' => false, 'content' => 'Content 0', ], [ 'header' => 'Headline 1', 'content' => << 'Headline 2', 'content' => << << [ [ 'header' => 'Headline 1', 'content' => << 'Headline 2', 'content' => << << [ [ 'header' => false, 'content' => << 'Headline 1', 'content' => <<extractSections( $content ); $this->assertEquals( $result, $sections ); } public function testGenerateEventsForRevision_tooManyMentionsFailure() { $expected = [ [ 'type' => 'mention-failure-too-many', 'agent' => 'Admin', 'section-title' => 'Hello Users', 'max-mentions' => 5, ], ]; $this->setupTestUser( 'Admin' ); $revision = $this->setupTestRevisionsForEventGeneration( 747747749, 747747747, 'Admin', 'en', [], 'UTPage' ); $events = []; $this->setupEventCallbackForEventGeneration( function ( EchoEvent $event ) use ( &$events ) { $events[] = [ 'type' => $event->getType(), 'agent' => $event->getAgent()->getName(), 'section-title' => $event->getExtraParam( 'section-title' ), 'max-mentions' => $event->getExtraParam( 'max-mentions' ), ]; return false; } ); $this->setMwGlobals( [ // enable mention failure and success notifications 'wgEchoMentionStatusNotifications' => true, // lower limit for the mention-failure-too-many notification 'wgEchoMaxMentionsCount' => 5 ] ); EchoDiscussionParser::generateEventsForRevision( $revision, false ); $this->assertEquals( $expected, $events ); } private function setupTestRevisionsForEventGeneration( $newId, $oldId, $username, $lang, $pages, $title, $summary = '' ) { $store = MediaWikiServices::getInstance()->getRevisionStore(); // Content language is used by the code that interprets the namespace part of titles // (Title::getTitleParser), so should be the fake language ;) $this->setContentLang( $lang ); $this->setMwGlobals( [ // this one allows Mediawiki:xyz pages to be set as messages 'wgUseDatabaseMessages' => true ] ); $this->resetServices(); // pages to be created: templates may be used to ping users (e.g. // {{u|...}}) but if we don't have that template, it just won't work! $pages += [ $title => '' ]; foreach ( $pages as $pageTitle => $pageText ) { $template = WikiPage::factory( Title::newFromText( $pageTitle ) ); $template->doEditContent( new WikitextContent( $pageText ), '' ); } // force i18n messages to be reloaded from MessageCache (from DB, where a new message // might have been created as page) $this->resetServices(); // grab revision excerpts (didn't include them in this src file because // they can be pretty long) $oldText = file_get_contents( __DIR__ . '/revision_txt/' . $oldId . '.txt' ); $newText = file_get_contents( __DIR__ . '/revision_txt/' . $newId . '.txt' ); // revision texts can be in different languages, where links etc are // different (e.g. User: becomes Benutzer: in German), so let's pretend // the page they belong to is from that language $title = Title::newFromText( $title ); $object = new ReflectionObject( $title ); $property = $object->getProperty( 'mDbPageLanguage' ); $property->setAccessible( true ); $property->setValue( $title, $lang ); // create stub MutableRevisionRecord object $row = [ 'id' => $newId, 'user_text' => $username, 'user' => User::newFromName( $username )->getId(), 'parent_id' => $oldId, 'text' => $newText, 'title' => $title, 'comment' => $summary, ]; $revision = $store->newMutableRevisionFromArray( $row ); $userName = $revision->getUser()->getName(); // generate diff between 2 revisions $changes = EchoDiscussionParser::getMachineReadableDiff( $oldText, $newText ); $output = EchoDiscussionParser::interpretDiff( $changes, $userName, $title ); // store diff in some local cache var, to circumvent // EchoDiscussionParser::getChangeInterpretationForRevision's attempt to // retrieve parent revision from DB $class = new ReflectionClass( EchoDiscussionParser::class ); $property = $class->getProperty( 'revisionInterpretationCache' ); $property->setAccessible( true ); $property->setValue( [ $revision->getId() => $output ] ); return $revision; } private function setupEventCallbackForEventGeneration( callable $callback ) { // to catch the generated event, I'm going to attach a callback to the // hook that's being run just prior to sending the notifications out // can't use setMwGlobals here, so I'll just re-attach to the same key // for every dataProvider value (and don't worry, I'm removing it on // tearDown too - I just felt the attaching should be happening here // instead of on setUp, or code would get too messy) global $wgHooks; $wgHooks['BeforeEchoEventInsert'][999] = $callback; } // TODO test cases for: // - stripHeader // - stripSignature public function testTimestampRegex() { $exemplarTimestamp = self::getExemplarTimestamp(); $timestampRegex = EchoDiscussionParser::getTimestampRegex(); $match = preg_match( '/' . $timestampRegex . '/u', $exemplarTimestamp ); $this->assertSame( 1, $match ); } public function testTimestampRegex_T264922() { $this->setMwGlobals( 'wgLanguageCode', 'skr' ); $this->assertIsString( EchoDiscussionParser::getTimestampRegex(), 'does not fail' ); } public function testGetTimestampPosition() { $line = 'Hello World. ' . self::getExemplarTimestamp(); $pos = EchoDiscussionParser::getTimestampPosition( $line ); $this->assertSame( 13, $pos ); } /** * @dataProvider signingDetectionData * FIXME some of the app logic is in the test... */ public function testSigningDetection( $line, $expectedUser ) { if ( is_array( $expectedUser ) ) { $this->setupTestUser( $expectedUser[1] ); } if ( !EchoDiscussionParser::isSignedComment( $line ) ) { $this->assertFalse( $expectedUser ); return; } $output = EchoDiscussionParser::getUserFromLine( $line ); if ( $output === false ) { $this->assertFalse( $expectedUser ); } elseif ( is_array( $expectedUser ) ) { // Sometimes testing for correct user detection, // sometimes testing for offset detection $this->assertEquals( $expectedUser, $output ); } else { $this->assertEquals( $expectedUser, $output[1] ); } } public function signingDetectionData() { $ts = self::getExemplarTimestamp(); return [ // Basic [ "I like this. [[User:Werdna|Werdna]] ([[User talk:Werdna|talk]]) $ts", [ 13, 'Werdna' ], ], // Confounding [ "[[User:Jorm]] is a meanie. --[[User:Werdna2|Andrew]] $ts", [ 29, "Werdna2" ], ], // Talk page link only [ "[[User:Swalling|Steve]] is the best person I have ever met. --[[User talk:Werdna3|Andrew]] $ts", [ 62, 'Werdna3' ], ], // Anonymous user [ "I am anonymous because I like my IP address. --[[Special:Contributions/127.0.0.1|127.0.0.1]] $ts", [ 47, '127.0.0.1' ], ], // No signature [ "Well, \nI do think that [[User:Newyorkbrad]] is pretty cool, but what do I know?", false ], // Hash symbols in usernames [ "What do you think? [[User talk:We buried our secrets in the garden#top|wbositg]] $ts", [ 19, 'We buried our secrets in the garden' ], ], // Title that gets normalized different than it is provided in the wikitext [ "Beep boop [[User:I_Heart_Spaces]] ([[User_talk:I_Heart_Spaces]]) $ts", [ strlen( "Beep boop " ), 'I Heart Spaces' ], ], // Accepts ] in the pipe [ "Shake n Bake --[[User:Werdna4|wer]dna]] $ts", [ strlen( "Shake n Bake --" ), 'Werdna4', ], ], [ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxã? [[User:Jam]] $ts", [ strlen( "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxã? " ), "Jam" ], ], // extra long signature [ "{{U|He7d3r}}, xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxã? [[User:Reverta-me|Aaaaa Bbbbbbb]]'' [[User Talk:Reverta-me|Discussão]]''", [ strlen( "{{U|He7d3r}}, xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxã? " ), 'Reverta-me', ], ], // Bug: T87852 [ "Test --[[Benutzer:Schnark]] ([[Benutzer:Schnark/js|js]])", [ strlen( "Test --" ), 'Schnark', ], ], // when adding additional tests, make sure to add the non-anon users // to EchoDiscussionParserTest::$testusers - the DiscussionParser // needs the users to exist, because it'll generate a comparison // signature, which is different when the user is considered anon ]; } /** @dataProvider diffData */ public function testDiff( $oldText, $newText, $expected ) { $actual = EchoDiscussionParser::getMachineReadableDiff( $oldText, $newText ); unset( $actual['_info'] ); unset( $expected['_info'] ); $this->assertEquals( $expected, $actual ); } public function diffData() { return [ [ << 'subtract', 'content' => 'line 2', 'left-pos' => 2, 'right-pos' => 2, ] ] ], [ << 'add', 'content' => 'line 2.5', 'left-pos' => 3, 'right-pos' => 3, ] ] ], [ << 'change', 'old_content' => 'line 2', 'new_content' => 'line b', 'left-pos' => 2, 'right-pos' => 2, ] ] ], [ << 'change', 'old_content' => 'line 2', 'new_content' => 'line b', 'left-pos' => 2, 'right-pos' => 2, ], [ 'action' => 'add', 'content' => 'line c line d', 'left-pos' => 3, 'right-pos' => 3, ], ], ], ]; } /** @dataProvider annotationData */ public function testAnnotation( $message, $diff, $user, $expectedAnnotation ) { $this->setupTestUser( $user ); $actual = EchoDiscussionParser::interpretDiff( $diff, $user ); $this->assertEquals( $expectedAnnotation, $actual, $message ); } public function annotationData() { $ts = self::getExemplarTimestamp(); return [ [ 'Must detect added comments', // Diff [ // Action [ 'action' => 'add', 'content' => ":What do you think? [[User:Werdna|Werdna]] ([[User talk:Werdna|talk]]) $ts", 'left-pos' => 3, 'right-pos' => 3, ], '_info' => [ 'lhs' => [ '== Section 1 ==', "I do not like you. [[User:Jorm|Jorm]] ([[User talk:Jorm|talk]]) $ts", ], 'rhs' => [ '== Section 1 ==', "I do not like you. [[User:Jorm|Jorm]] ([[User talk:Jorm|talk]]) $ts", ":What do you think? [[User:Werdna|Werdna]] ([[User talk:Werdna|talk]]) $ts", ], ], ], // User 'Werdna', // Expected annotation [ [ 'type' => 'add-comment', 'content' => ":What do you think? [[User:Werdna|Werdna]] ([[User talk:Werdna|talk]]) $ts", 'full-section' => << 'add', 'content' => ":What do you think? [[User:Werdna|Werdna]] ([[User talk:Werdna|talk]]) $ts", 'left-pos' => 3, 'right-pos' => 3, ], '_info' => [ 'lhs' => [ '== Section 1 ==', "I do not like you. [[User:Jorm|Jorm]] ([[User talk:Jorm|talk]]) $ts", '== Section 2 ==', "Well well well. [[User:DarTar|DarTar]] ([[User talk:DarTar|talk]]) $ts", ], 'rhs' => [ '== Section 1 ==', "I do not like you. [[User:Jorm|Jorm]] ([[User talk:Jorm|talk]]) $ts", ":What do you think? [[User:Werdna|Werdna]] ([[User talk:Werdna|talk]]) $ts", '== Section 2 ==', "Well well well. [[User:DarTar|DarTar]] ([[User talk:DarTar|talk]]) $ts", ], ], ], // User 'Werdna', // Expected annotation [ [ 'type' => 'add-comment', 'content' => ":What do you think? [[User:Werdna|Werdna]] ([[User talk:Werdna|talk]]) $ts", 'full-section' => << 'add', 'content' => << 4, 'right-pos' => 4, ], '_info' => [ 'lhs' => [ '== Section 1 ==', "I do not like you. [[User:Jorm|Jorm]] ([[User talk:Jorm|talk]]) $ts", ":What do you think? [[User:Werdna|Werdna]] ([[User talk:Werdna|talk]]) $ts", '== Section 2 ==', "Well well well. [[User:DarTar|DarTar]] ([[User talk:DarTar|talk]]) $ts", ], 'rhs' => [ '== Section 1 ==', "I do not like you. [[User:Jorm|Jorm]] ([[User talk:Jorm|talk]]) $ts", ":What do you think? [[User:Werdna|Werdna]] ([[User talk:Werdna|talk]]) $ts", '== Section 1a ==', 'Hmmm? [[User:Jdforrester|Jdforrester]] ([[User talk:Jdforrested|talk]]) $ts', '== Section 2 ==', "Well well well. [[User:DarTar|DarTar]] ([[User talk:DarTar|talk]]) $ts", ], ], ], // User 'Jdforrester', // Expected annotation [ [ 'type' => 'new-section-with-comment', 'content' => << 'add-comment', 'content' => ":New Comment [[User:JarJar|JarJar]] ([[User talk:JarJar|talk]]) $ts", 'full-section' => << 'add-comment', 'content' => ":Other New Comment [[User:JarJar|JarJar]] ([[User talk:JarJar|talk]]) $ts", 'full-section' => <<Cwobeel]] [[User_talk:Cwobeel|(talk)]] 16:02, 11 December 2014 (UTC) TEXT ), // User 'Cwobeel', // Expected annotation [ [ 'type' => 'new-section-with-comment', 'content' => '== Grand jury no bill reception == {{u|Bob K31416}} has started a process of summarizing that section, in a manner that I believe it to be counter productive. We have expert opinions from legal, law enforcement, politicians, and media outlets all of which are notable and informative. [[WP:NOTPAPER|Wikipedia is not paper]] – If the section is too long, the correct process to avoid losing good content that is well sources, is to create a sub-article with all the detail, and summarize here per [[WP:SUMMARY]]. But deleting useful and well sourced material, is not acceptable. We are here to build an encyclopedia. - [[User:Cwobeel|Cwobeel]] [[User_talk:Cwobeel|(talk)]] 16:02, 11 December 2014 (UTC)', ], ], ], // when adding additional tests, make sure to add the non-anon users // to EchoDiscussionParserTest::$testusers - the DiscussionParser // needs the users to exist, because it'll generate a comparison // signature, which is different when the user is considered anon ]; } public function getExemplarTimestamp() { $title = $this->createMock( Title::class ); $user = $this->createMock( User::class ); $options = ParserOptions::newFromAnon(); $parser = MediaWikiServices::getInstance()->getParser(); $exemplarTimestamp = $parser->preSaveTransform( '~~~~~', $title, $user, $options ); return $exemplarTimestamp; } public static function provider_detectSectionTitleAndText() { $name = 'Werdna'; // See EchoDiscussionParserTest::$testusers $comment = self::signedMessage( $name ); return [ [ 'Must detect first sub heading when inserting in the middle of two sub headings', // expected header content 'Sub Heading 1', // test content format " == Heading == $comment == Sub Heading 1 == $comment %s == Sub Heading 2 == $comment ", // user signing new comment $name ], [ 'Must detect second sub heading when inserting in the end of two sub headings', // expected header content 'Sub Heading 2', // test content format " == Heading == $comment == Sub Heading 1 == $comment == Sub Heading 2 == $comment %s ", // user signing new comment $name ], [ 'Commenting in multiple sub-headings must result in no section link', // expected header content '', // test content format " == Heading == $comment == Sub Heading 1 == $comment %s == Sub Heading 2 == $comment %s ", // user signing new comment $name ], [ 'Must accept headings without a space between the = and the section name', // expected header content 'Heading', // test content format " ==Heading== $comment %s ", // user signing new comment $name ], [ 'Must not accept invalid headings split with a return', // expected header content '', // test content format " ==Some Heading== $comment %s ", // user signing new comment $name ], ]; } /** * @dataProvider provider_detectSectionTitleAndText */ public function testDetectSectionTitleAndText( $message, $expect, $format, $name ) { $this->setupTestUser( $name ); // str_replace because we want to replace multiple instances of '%s' with the same value $before = str_replace( '%s', '', $format ); $after = str_replace( '%s', self::signedMessage( $name ), $format ); $diff = EchoDiscussionParser::getMachineReadableDiff( $before, $after ); $interp = EchoDiscussionParser::interpretDiff( $diff, $name ); // There should be a section-text only if there is section-title $expectText = $expect ? self::message( $name ) : ''; $this->assertEquals( [ 'section-title' => $expect, 'section-text' => $expectText ], EchoDiscussionParser::detectSectionTitleAndText( $interp ), $message ); } protected static function signedMessage( $name ) { return ": " . self::message() . " [[User:$name|$name]] ([[User talk:$name|talk]]) 00:17, 7 May 2013 (UTC)"; } protected static function message() { return 'foo'; } public static function provider_getFullSection() { $tests = [ [ 'Extracts full section', // Full document content << "==Header 1==\nfoo", 2 => "==Header 1==\nfoo", 3 => "===Header 2===\nbar", 4 => "===Header 2===\nbar", 5 => "==Header 3==\nbaz", 6 => "==Header 3==\nbaz", ], ], ]; // Allow for setting an array of line numbers to expand from rather than // just a single line number $retval = []; foreach ( $tests as $test ) { foreach ( $test[2] as $lineNum => $expected ) { $retval[] = [ $test[0], $expected, $test[1], $lineNum, ]; } } return $retval; } /** * @dataProvider provider_getFullSection */ public function testGetFullSection( $message, $expect, $lines, $startLineNum ) { $section = EchoDiscussionParser::getFullSection( explode( "\n", $lines ), $startLineNum ); $this->assertEquals( $expect, $section, $message ); } public function testGetSectionCount() { $one = "==Zomg==\nfoobar\n"; $two = "===SubZomg===\nHi there\n"; $three = "==Header==\nOh Hai!\n"; $this->assertSame( 1, EchoDiscussionParser::getSectionCount( $one ) ); $this->assertSame( 2, EchoDiscussionParser::getSectionCount( $one . $two ) ); $this->assertSame( 2, EchoDiscussionParser::getSectionCount( $one . $three ) ); $this->assertSame( 3, EchoDiscussionParser::getSectionCount( $one . $two . $three ) ); $this->assertSame( 30, EchoDiscussionParser::getSectionCount( file_get_contents( __DIR__ . '/revision_txt/637638133.txt' ) ) ); } public function testGetOverallUserMentionsCount() { $userMentions = [ 'validMentions' => [ 1 => 1 ], 'unknownUsers' => [ 'NotKnown1', 'NotKnown2' ], 'anonymousUsers' => [ '127.0.0.1' ], ]; $discussionParser = TestingAccessWrapper::newFromClass( EchoDiscussionParser::class ); $this->assertSame( 4, $discussionParser->getOverallUserMentionsCount( $userMentions ) ); } public function provider_getUserMentions() { return [ [ [ 'NotKnown1' => 0 ], [ 'validMentions' => [], 'unknownUsers' => [ 'NotKnown1' ], 'anonymousUsers' => [], ], 1 ], [ [ '127.0.0.1' => 0 ], [ 'validMentions' => [], 'unknownUsers' => [], 'anonymousUsers' => [ '127.0.0.1' ], ], 1 ], ]; } /** * @dataProvider provider_getUserMentions */ public function testGetUserMentions( $userLinks, $expectedUserMentions, $agent ) { $title = Title::newFromText( 'Test' ); $discussionParser = TestingAccessWrapper::newFromClass( EchoDiscussionParser::class ); $this->assertEquals( $expectedUserMentions, $discussionParser->getUserMentions( $title, $agent, $userLinks ) ); } public function testGetUserMentions_validMention() { $userName = 'Admin'; $this->setupTestUser( $userName ); $userId = User::newFromName( $userName )->getId(); $expectedUserMentions = [ 'validMentions' => [ $userId => $userId ], 'unknownUsers' => [], 'anonymousUsers' => [], ]; $userLinks = [ $userName => $userId ]; $this->testGetUserMentions( $userLinks, $expectedUserMentions, 1 ); } public function testGetUserMentions_ownMention() { $userName = 'Admin'; $this->setupTestUser( $userName ); $userId = User::newFromName( 'Admin' )->getId(); $expectedUserMentions = [ 'validMentions' => [], 'unknownUsers' => [], 'anonymousUsers' => [], ]; $userLinks = [ $userName => $userId ]; $this->testGetUserMentions( $userLinks, $expectedUserMentions, $userId ); } public function testGetUserMentions_tooManyMentions() { $userLinks = [ 'NotKnown1' => 0, 'NotKnown2' => 0, 'NotKnown3' => 0, '127.0.0.1' => 0, '127.0.0.2' => 0, ]; $this->setMwGlobals( [ // lower limit for the mention-too-many notification 'wgEchoMaxMentionsCount' => 3 ] ); $title = Title::newFromText( 'Test' ); $discussionParser = TestingAccessWrapper::newFromClass( EchoDiscussionParser::class ); $this->assertSame( 4, $discussionParser->getOverallUserMentionsCount( $discussionParser->getUserMentions( $title, 1, $userLinks ) ) ); } protected function isParserFunctionsInstalled() { if ( ExtensionRegistry::getInstance()->isLoaded( 'ParserFunctions' ) ) { return true; } else { return "ParserFunctions not enabled"; } } public function testGetTextSnippet() { $this->assertEquals( 'Page001', EchoDiscussionParser::getTextSnippet( '[[:{{BASEPAGENAME}}]]', Language::factory( 'en' ), EchoDiscussionParser::DEFAULT_SNIPPET_LENGTH, Title::newFromText( 'Page001' ) ) ); } }