[user preference => preference value]] */ protected $testUsers = array( // username 'Werdna' => array( // user preferences 'nickname' => '', 'fancysig' => '0', ), 'Werdna2' => array( 'nickname' => '[[User:Werdna2|Andrew]]', 'fancysig' => '1', ), 'Werdna3' => array( 'nickname' => '[[User talk:Werdna3|Andrew]]', 'fancysig' => '1', ), 'Werdna4' => array( 'nickname' => '[[User:Werdna4|wer]dna]]', 'fancysig' => '1', ), 'We buried our secrets in the garden' => array( 'nickname' => '[[User talk:We buried our secrets in the garden#top|wbositg]]', 'fancysig' => '1', ), 'I Heart Spaces' => array( 'nickname' => '[[User:I_Heart_Spaces]] ([[User_talk:I_Heart_Spaces]])', 'fancysig' => '1', ), 'Jam' => array( 'nickname' => '[[User:Jam]]', 'fancysig' => '1', ), 'Reverta-me' => array( 'nickname' => "[[User:Reverta-me|Aaaaa Bbbbbbb]]'' [[User Talk:Reverta-me|Discussão]]''", 'fancysig' => '1', ), 'Jorm' => array( 'nickname' => '', 'fancysig' => '0', ), 'Jdforrester' => array( 'nickname' => '', 'fancysig' => '0', ), 'DarTar' => array( 'nickname' => '', 'fancysig' => '0', ), 'Bsitu' => array( 'nickname' => '', 'fancysig' => '0', ), 'JarJar' => array( 'nickname' => '', 'fancysig' => '0', ), 'Schnark' => array( 'nickname' => '[[Benutzer:Schnark]] ([[Benutzer:Schnark/js|js]])', 'fancysig' => '1', ), 'Cwobeel' => array( 'nickname' => '[[User:Cwobeel|Cwobeel]] [[User_talk:Cwobeel|(talk)]]', 'fancysig' => '1', ), 'Bob K31416' => array( 'nickname' => '', 'fancysig' => '0', ), 'X" onclick="alert(\'XSS\');" title="y' => array( 'nickname' => '', 'fancysig' => '0', ), 'He7d3r' => array( 'nickname' => '', 'fancysig' => '0', ), 'PauloEduardo' => array( 'nickname' => "[[User:PauloEduardo|Paulo Eduardo]]'' [[User Talk:PauloEduardo|Discussão]]''", 'fancysig' => '1', ), ); protected function setUp() { parent::setUp(); // we only need to add these users once, we won't (can't) tear them down anyway static $executed = false; if ( $executed === true ) { return; } foreach ( $this->testUsers as $username => $preferences ) { $user = User::createNew( $username ); // set signature preferences if ( $user ) { foreach ( $preferences as $option => $value ) { $user->setOption( $option, $value ); } $user->saveSettings(); } } $executed = true; } protected function tearDown() { parent::tearDown(); global $wgHooks; unset( $wgHooks['BeforeEchoEventInsert'][999] ); } public function generateEventsForRevisionData() { return array( array( 'new' => 637638133, 'old' => 637637213, 'username' => 'Cwobeel', 'lang' => 'en', 'pages' => array( // pages expected to exist (e.g. templates to be expanded) 'Template:u' => '[[User:{{{1}}}|{{safesubst:#if:{{{2|}}}|{{{2}}}|{{{1}}}}}]]{{documentation}}', ), 'expected' => array( // events expected to be fired going from old revision to new array( 'type' => 'mention', 'agent' => 'Cwobeel', /* * 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. */ ), ), ), array( 'new' => 138275105, 'old' => 138274875, 'username' => 'Schnark', 'lang' => 'de', 'pages' => array(), 'expected' => array( array( 'type' => 'mention', 'agent' => 'Schnark', ), ), ), array( 'new' => 40610292, 'old' => 40608353, 'username' => 'PauloEduardo', 'lang' => 'pt', 'pages' => array( 'Predefinição:U' => '[[User:{{{1|Exemplo}}}|{{{{{|safesubst:}}}#if:{{{2|}}}|{{{2}}}|{{{1|Exemplo}}}}}]]{{Atalho|Predefinição:U}}{{Documentação|Predefinição:Usuário/doc}}', ), 'expected' => array( array( 'type' => 'mention', 'agent' => 'PauloEduardo', ), ), ), ); } /** * @dataProvider generateEventsForRevisionData */ public function testGenerateEventsForRevision( $newId, $oldId, $username, $lang, $pages, $expected ) { // this global is used by the code that interprets the namespace part of // titles (Title::getTitleParser), so should be the fake language ;) $this->setMwGlobals( 'wgContLang', Language::factory( $lang ) ); // 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! foreach ( $pages as $title => $text ) { $template = WikiPage::factory( Title::newFromText( $title ) ); $template->doEditContent( new WikitextContent( $text ), '' ); } // 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( 'UTPage' ); $object = new ReflectionObject( $title ); $property = $object->getProperty( 'mDbPageLanguage' ); $property->setAccessible( true ); $property->setValue( $title, $lang ); // create stub Revision object $row = array( 'id' => $newId, 'user_text' => $username, 'user' => User::newFromName( $username )->getId(), 'parent_id' => $oldId, 'text' => $newText, 'title' => $title, ); $revision = Revision::newFromRow( $row ); // generate diff between 2 revisions $changes = EchoDiscussionParser::getMachineReadableDiff( $oldText, $newText ); $output = EchoDiscussionParser::interpretDiff( $changes, $revision->getUserText() ); // store diff in some local cache var, to circumvent // EchoDiscussionParser::getChangeInterpretationForRevision's attempt to // retrieve parent revision from DB $class = new ReflectionClass( 'EchoDiscussionParser' ); $property = $class->getProperty( 'revisionInterpretationCache' ); $property->setAccessible( true ); $property->setValue( array( $revision->getId() => $output ) ); // 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 $events = array(); $callback = function( EchoEvent $event ) use ( &$events ) { $events[] = array( 'type' => $event->getType(), 'agent' => $event->getAgent()->getName(), ); // don't let the event go out, abort from within this hook return false; }; // 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; // finally, dear god, start generating the events already! EchoDiscussionParser::generateEventsForRevision( $revision ); $this->assertEquals( $expected, $events ); } // TODO test cases for: // - stripHeader // - stripIndents // - stripSignature // - getNotifiedUsersForComment public function testDiscussionParserAcceptsInternalDiff() { global $wgDiff; $origWgDiff = $wgDiff; $wgDiff = '/does/not/exist/or/at/least/we/hope/not'; try { $res = EchoDiscussionParser::getMachineReadableDiff( <<assertTrue( true ); } public function testTimestampRegex() { $exemplarTimestamp = self::getExemplarTimestamp(); $timestampRegex = EchoDiscussionParser::getTimestampRegex(); $match = preg_match( '/' . $timestampRegex . '/u', $exemplarTimestamp ); $this->assertEquals( 1, $match ); } public function testGetTimestampPosition() { $line = 'Hello World. '. self::getExemplarTimestamp(); $pos = EchoDiscussionParser::getTimestampPosition( $line ); $this->assertEquals( 13, $pos ); } /** * @dataProvider signingDetectionData * FIXME some of the app logic is in the test... */ public function testSigningDetection( $line, $expectedUser ) { if ( !EchoDiscussionParser::isSignedComment( $line ) ) { $this->assertEquals( $expectedUser, false ); return; } $output = EchoDiscussionParser::getUserFromLine( $line ); if ( $output === false ) { $this->assertEquals( false, $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 array( // Basic array( "I like this. [[User:Werdna|Werdna]] ([[User talk:Werdna|talk]]) $ts", array( 13, 'Werdna' ), ), // Confounding array( "[[User:Jorm]] is a meanie. --[[User:Werdna2|Andrew]] $ts", array( 29, "Werdna2" ), ), // Talk page link only array( "[[User:Swalling|Steve]] is the best person I have ever met. --[[User talk:Werdna3|Andrew]] $ts", array( 62, 'Werdna3' ), ), // Anonymous user array( "I am anonymous because I like my IP address. --[[Special:Contributions/127.0.0.1|127.0.0.1]] $ts", array( 47, '127.0.0.1' ), ), // No signature array( "Well, \nI do think that [[User:Newyorkbrad]] is pretty cool, but what do I know?", false ), // Hash symbols in usernames array( "What do you think? [[User talk:We buried our secrets in the garden#top|wbositg]] $ts", array( 19, 'We buried our secrets in the garden' ), ), // Title that gets normalized different than it is provided in the wikitext array( "Beep boop [[User:I_Heart_Spaces]] ([[User_talk:I_Heart_Spaces]]) $ts", array( strlen( "Beep boop " ), 'I Heart Spaces' ), ), // Accepts ] in the pipe array( "Shake n Bake --[[User:Werdna4|wer]dna]] $ts", array( strlen( "Shake n Bake --" ), 'Werdna4', ), ), array( "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxã? [[User:Jam]] $ts", array( strlen( "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxã? " ), "Jam" ), ), // extra long signature array( "{{U|He7d3r}}, xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxã? [[User:Reverta-me|Aaaaa Bbbbbbb]]'' [[User Talk:Reverta-me|Discussão]]''", array( strlen( "{{U|He7d3r}}, xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxã? " ), 'Reverta-me', ), ), // Bug: T87852 array( "Test --[[Benutzer:Schnark]] ([[Benutzer:Schnark/js|js]])", array( 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 array( array( << 'subtract', 'content' => 'line 2', 'left-pos' => 2, 'right-pos' => 2, ) ) ), array( << 'add', 'content' => 'line 2.5', 'left-pos' => 3, 'right-pos' => 3, ) ) ), array( << 'change', 'old_content' => 'line 2', 'new_content' => 'line b', 'left-pos' => 2, 'right-pos' => 2, ) ) ), array( << 'change', 'old_content' => 'line 2', 'new_content' => 'line b', 'left-pos' => 2, 'right-pos' => 2, ), array( 'action' => 'add', 'content' => 'line c line d', 'left-pos' => 3, 'right-pos' => 3, ), ), ), ); } /** @dataProvider annotationData */ public function testAnnotation( $message, $diff, $user, $expectedAnnotation ) { $actual = EchoDiscussionParser::interpretDiff( $diff, $user ); $this->assertEquals( $expectedAnnotation, $actual, $message ); } public function annotationData() { $ts = self::getExemplarTimestamp(); return array( array( 'Must detect added comments', // Diff array( // Action array( 'action' => 'add', 'content' => ":What do you think? [[User:Werdna|Werdna]] ([[User talk:Werdna|talk]]) $ts", 'left-pos' => 3, 'right-pos' => 3, ), '_info' => array( 'lhs' => array( '== Section 1 ==', "I do not like you. [[User:Jorm|Jorm]] ([[User talk:Jorm|talk]]) $ts", ), 'rhs' => array( '== 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 array( array( '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' => array( 'lhs' => array( '== 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' => array( '== 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 array( array( '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' => array( 'lhs' => array( '== 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' => array( '== 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 array( array( '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 array( array( '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 static function getExemplarTimestamp() { $title = Title::newMainPage(); $user = User::newFromName( 'Test' ); $options = new ParserOptions; global $wgParser; $exemplarTimestamp = $wgParser->preSaveTransform( '~~~~~', $title, $user, $options ); return $exemplarTimestamp; } static public function provider_detectSectionTitleAndText() { $name = 'Werdna'; // See EchoDiscussionParserTest::$testusers $comment = self::signedMessage( $name ); return array( array( '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 ), array( '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 ), array( '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 ), array( '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 ), array( '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 ) { // 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( array( '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'; } static public function provider_getFullSection() { $tests = array( array( '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 = array(); foreach ( $tests as $test ) { foreach ( $test[2] as $lineNum => $expected ) { $retval[] = array( $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->assertEquals( 1, EchoDiscussionParser::getSectionCount( $one ) ); $this->assertEquals( 2, EchoDiscussionParser::getSectionCount( $one . $two ) ); $this->assertEquals( 2, EchoDiscussionParser::getSectionCount( $one . $three ) ); $this->assertEquals( 3, EchoDiscussionParser::getSectionCount( $one . $two . $three ) ); } }