[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' )
)
);
}
}