summaryrefslogtreecommitdiff
path: root/www/wiki/tests/phpunit/includes/changes
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/tests/phpunit/includes/changes')
-rw-r--r--www/wiki/tests/phpunit/includes/changes/CategoryMembershipChangeTest.php156
-rw-r--r--www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterGroupTest.php96
-rw-r--r--www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterTest.php166
-rw-r--r--www/wiki/tests/phpunit/includes/changes/ChangesListFilterGroupTest.php79
-rw-r--r--www/wiki/tests/phpunit/includes/changes/ChangesListFilterTest.php116
-rw-r--r--www/wiki/tests/phpunit/includes/changes/ChangesListStringOptionsFilterGroupTest.php279
-rw-r--r--www/wiki/tests/phpunit/includes/changes/EnhancedChangesListTest.php228
-rw-r--r--www/wiki/tests/phpunit/includes/changes/OldChangesListTest.php234
-rw-r--r--www/wiki/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php236
-rw-r--r--www/wiki/tests/phpunit/includes/changes/RecentChangeTest.php224
-rw-r--r--www/wiki/tests/phpunit/includes/changes/TestRecentChangesHelper.php170
11 files changed, 1984 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/changes/CategoryMembershipChangeTest.php b/www/wiki/tests/phpunit/includes/changes/CategoryMembershipChangeTest.php
new file mode 100644
index 00000000..ca3ac1b6
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/changes/CategoryMembershipChangeTest.php
@@ -0,0 +1,156 @@
+<?php
+
+/**
+ * @covers CategoryMembershipChange
+ *
+ * @group Database
+ *
+ * @author Addshore
+ */
+class CategoryMembershipChangeTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var array|Title[]|User[]
+ */
+ private static $lastNotifyArgs;
+
+ /**
+ * @var int
+ */
+ private static $notifyCallCounter = 0;
+
+ /**
+ * @var RecentChange
+ */
+ private static $mockRecentChange;
+
+ /**
+ * @var Revision
+ */
+ private static $pageRev = null;
+
+ /**
+ * @var User
+ */
+ private static $revUser = null;
+
+ /**
+ * @var string
+ */
+ private static $pageName = 'CategoryMembershipChangeTestPage';
+
+ public static function newForCategorizationCallback() {
+ self::$lastNotifyArgs = func_get_args();
+ self::$notifyCallCounter += 1;
+ return self::$mockRecentChange;
+ }
+
+ public function setUp() {
+ parent::setUp();
+ self::$notifyCallCounter = 0;
+ self::$mockRecentChange = self::getMock( RecentChange::class );
+
+ $this->setContentLang( 'qqx' );
+ }
+
+ public function addDBDataOnce() {
+ $info = $this->insertPage( self::$pageName );
+ $title = $info['title'];
+
+ $page = WikiPage::factory( $title );
+ self::$pageRev = $page->getRevision();
+ self::$revUser = User::newFromId( self::$pageRev->getUser( Revision::RAW ) );
+ }
+
+ private function newChange( Revision $revision = null ) {
+ $change = new CategoryMembershipChange( Title::newFromText( self::$pageName ), $revision );
+ $change->overrideNewForCategorizationCallback(
+ 'CategoryMembershipChangeTest::newForCategorizationCallback'
+ );
+
+ return $change;
+ }
+
+ public function testChangeAddedNoRev() {
+ $change = $this->newChange();
+ $change->triggerCategoryAddedNotification( Title::newFromText( 'CategoryName', NS_CATEGORY ) );
+
+ $this->assertEquals( 1, self::$notifyCallCounter );
+
+ $this->assertTrue( strlen( self::$lastNotifyArgs[0] ) === 14 );
+ $this->assertEquals( 'Category:CategoryName', self::$lastNotifyArgs[1]->getPrefixedText() );
+ $this->assertEquals( '(autochange-username)', self::$lastNotifyArgs[2]->getName() );
+ $this->assertEquals( '(recentchanges-page-added-to-category: ' . self::$pageName . ')',
+ self::$lastNotifyArgs[3] );
+ $this->assertEquals( self::$pageName, self::$lastNotifyArgs[4]->getPrefixedText() );
+ $this->assertEquals( 0, self::$lastNotifyArgs[5] );
+ $this->assertEquals( 0, self::$lastNotifyArgs[6] );
+ $this->assertEquals( null, self::$lastNotifyArgs[7] );
+ $this->assertEquals( 1, self::$lastNotifyArgs[8] );
+ $this->assertEquals( null, self::$lastNotifyArgs[9] );
+ $this->assertEquals( 0, self::$lastNotifyArgs[10] );
+ }
+
+ public function testChangeRemovedNoRev() {
+ $change = $this->newChange();
+ $change->triggerCategoryRemovedNotification( Title::newFromText( 'CategoryName', NS_CATEGORY ) );
+
+ $this->assertEquals( 1, self::$notifyCallCounter );
+
+ $this->assertTrue( strlen( self::$lastNotifyArgs[0] ) === 14 );
+ $this->assertEquals( 'Category:CategoryName', self::$lastNotifyArgs[1]->getPrefixedText() );
+ $this->assertEquals( '(autochange-username)', self::$lastNotifyArgs[2]->getName() );
+ $this->assertEquals( '(recentchanges-page-removed-from-category: ' . self::$pageName . ')',
+ self::$lastNotifyArgs[3] );
+ $this->assertEquals( self::$pageName, self::$lastNotifyArgs[4]->getPrefixedText() );
+ $this->assertEquals( 0, self::$lastNotifyArgs[5] );
+ $this->assertEquals( 0, self::$lastNotifyArgs[6] );
+ $this->assertEquals( null, self::$lastNotifyArgs[7] );
+ $this->assertEquals( 1, self::$lastNotifyArgs[8] );
+ $this->assertEquals( null, self::$lastNotifyArgs[9] );
+ $this->assertEquals( 0, self::$lastNotifyArgs[10] );
+ }
+
+ public function testChangeAddedWithRev() {
+ $revision = Revision::newFromId( Title::newFromText( self::$pageName )->getLatestRevID() );
+ $change = $this->newChange( $revision );
+ $change->triggerCategoryAddedNotification( Title::newFromText( 'CategoryName', NS_CATEGORY ) );
+
+ $this->assertEquals( 1, self::$notifyCallCounter );
+
+ $this->assertTrue( strlen( self::$lastNotifyArgs[0] ) === 14 );
+ $this->assertEquals( 'Category:CategoryName', self::$lastNotifyArgs[1]->getPrefixedText() );
+ $this->assertEquals( self::$revUser->getName(), self::$lastNotifyArgs[2]->getName() );
+ $this->assertEquals( '(recentchanges-page-added-to-category: ' . self::$pageName . ')',
+ self::$lastNotifyArgs[3] );
+ $this->assertEquals( self::$pageName, self::$lastNotifyArgs[4]->getPrefixedText() );
+ $this->assertEquals( self::$pageRev->getParentId(), self::$lastNotifyArgs[5] );
+ $this->assertEquals( $revision->getId(), self::$lastNotifyArgs[6] );
+ $this->assertEquals( null, self::$lastNotifyArgs[7] );
+ $this->assertEquals( 0, self::$lastNotifyArgs[8] );
+ $this->assertEquals( '127.0.0.1', self::$lastNotifyArgs[9] );
+ $this->assertEquals( 0, self::$lastNotifyArgs[10] );
+ }
+
+ public function testChangeRemovedWithRev() {
+ $revision = Revision::newFromId( Title::newFromText( self::$pageName )->getLatestRevID() );
+ $change = $this->newChange( $revision );
+ $change->triggerCategoryRemovedNotification( Title::newFromText( 'CategoryName', NS_CATEGORY ) );
+
+ $this->assertEquals( 1, self::$notifyCallCounter );
+
+ $this->assertTrue( strlen( self::$lastNotifyArgs[0] ) === 14 );
+ $this->assertEquals( 'Category:CategoryName', self::$lastNotifyArgs[1]->getPrefixedText() );
+ $this->assertEquals( self::$revUser->getName(), self::$lastNotifyArgs[2]->getName() );
+ $this->assertEquals( '(recentchanges-page-removed-from-category: ' . self::$pageName . ')',
+ self::$lastNotifyArgs[3] );
+ $this->assertEquals( self::$pageName, self::$lastNotifyArgs[4]->getPrefixedText() );
+ $this->assertEquals( self::$pageRev->getParentId(), self::$lastNotifyArgs[5] );
+ $this->assertEquals( $revision->getId(), self::$lastNotifyArgs[6] );
+ $this->assertEquals( null, self::$lastNotifyArgs[7] );
+ $this->assertEquals( 0, self::$lastNotifyArgs[8] );
+ $this->assertEquals( '127.0.0.1', self::$lastNotifyArgs[9] );
+ $this->assertEquals( 0, self::$lastNotifyArgs[10] );
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterGroupTest.php b/www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterGroupTest.php
new file mode 100644
index 00000000..d80b6c10
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterGroupTest.php
@@ -0,0 +1,96 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers ChangesListBooleanFilterGroup
+ */
+class ChangesListBooleanFilterGroupTest extends MediaWikiTestCase {
+ public function testIsFullCoverage() {
+ $hideGroupDefault = TestingAccessWrapper::newFromObject(
+ new ChangesListBooleanFilterGroup( [
+ 'name' => 'groupName',
+ 'priority' => 1,
+ 'filters' => [],
+ ] )
+ );
+
+ $this->assertSame(
+ true,
+ $hideGroupDefault->isFullCoverage
+ );
+ }
+
+ public function testGetJsData() {
+ $definition = [
+ 'name' => 'some-group',
+ 'title' => 'some-group-title',
+ 'priority' => 1,
+ 'filters' => [
+ [
+ 'name' => 'hidefoo',
+ 'label' => 'foo-label',
+ 'description' => 'foo-description',
+ 'default' => true,
+ 'showHide' => 'showhidefoo',
+ 'priority' => 2,
+ ],
+ [
+ 'name' => 'hidebar',
+ 'label' => 'bar-label',
+ 'description' => 'bar-description',
+ 'default' => false,
+ 'priority' => 4,
+ ]
+ ],
+ ];
+
+ $group = new ChangesListBooleanFilterGroup( $definition );
+
+ $this->assertArrayEquals(
+ [
+ 'name' => 'some-group',
+ 'title' => 'some-group-title',
+ 'type' => ChangesListBooleanFilterGroup::TYPE,
+ 'priority' => 1,
+ 'filters' => [
+ [
+ 'name' => 'hidebar',
+ 'label' => 'bar-label',
+ 'description' => 'bar-description',
+ 'default' => false,
+ 'priority' => 4,
+ 'cssClass' => null,
+ 'conflicts' => [],
+ 'subset' => [],
+ 'defaultHighlightColor' => null,
+ ],
+ [
+ 'name' => 'hidefoo',
+ 'label' => 'foo-label',
+ 'description' => 'foo-description',
+ 'default' => true,
+ 'priority' => 2,
+ 'cssClass' => null,
+ 'conflicts' => [],
+ 'subset' => [],
+ 'defaultHighlightColor' => null,
+ ],
+ ],
+ 'conflicts' => [],
+ 'fullCoverage' => true,
+ 'messageKeys' => [
+ 'some-group-title',
+ 'bar-label',
+ 'bar-description',
+ 'foo-label',
+ 'foo-description',
+ ],
+ ],
+
+ $group->getJsData(),
+ /** ordered= */ false,
+ /** named= */ true
+ );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterTest.php b/www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterTest.php
new file mode 100644
index 00000000..35dc1a83
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/changes/ChangesListBooleanFilterTest.php
@@ -0,0 +1,166 @@
+<?php
+
+/**
+ * @covers ChangesListBooleanFilter
+ */
+class ChangesListBooleanFilterTest extends MediaWikiTestCase {
+ public function testGetJsData() {
+ $group = new ChangesListBooleanFilterGroup( [
+ 'name' => 'group',
+ 'priority' => 2,
+ 'filters' => [],
+ ] );
+
+ $definition = [
+ 'group' => $group,
+ 'label' => 'main-label',
+ 'description' => 'main-description',
+ 'default' => 1,
+ 'priority' => 1,
+ ];
+
+ $fooFilter = new ChangesListBooleanFilter(
+ $definition + [ 'name' => 'hidefoo' ]
+ );
+
+ $barFilter = new ChangesListBooleanFilter(
+ $definition + [ 'name' => 'hidebar' ]
+ );
+
+ $bazFilter = new ChangesListBooleanFilter(
+ $definition + [ 'name' => 'hidebaz' ]
+ );
+
+ $fooFilter->conflictsWith(
+ $barFilter,
+ 'foo-bar-global-conflict',
+ 'foo-conflicts-bar',
+ 'bar-conflicts-foo'
+ );
+
+ $fooFilter->setAsSupersetOf( $bazFilter, 'foo-superset-of-baz' );
+
+ $fooData = $fooFilter->getJsData();
+ $this->assertArrayEquals(
+ [
+ 'name' => 'hidefoo',
+ 'label' => 'main-label',
+ 'description' => 'main-description',
+ 'default' => 1,
+ 'priority' => 1,
+ 'cssClass' => null,
+ 'defaultHighlightColor' => null,
+ 'conflicts' => [
+ [
+ 'group' => 'group',
+ 'filter' => 'hidebar',
+ 'globalDescription' => 'foo-bar-global-conflict',
+ 'contextDescription' => 'foo-conflicts-bar',
+ ]
+ ],
+ 'subset' => [
+ [
+ 'group' => 'group',
+ 'filter' => 'hidebaz',
+ ],
+
+ ],
+ 'messageKeys' => [
+ 'main-label',
+ 'main-description',
+ 'foo-bar-global-conflict',
+ 'foo-conflicts-bar',
+ ],
+ ],
+ $fooData,
+ /** ordered= */ false,
+ /** named= */ true
+ );
+
+ $barData = $barFilter->getJsData();
+ $this->assertArrayEquals(
+ [
+ 'name' => 'hidebar',
+ 'label' => 'main-label',
+ 'description' => 'main-description',
+ 'default' => 1,
+ 'priority' => 1,
+ 'cssClass' => null,
+ 'defaultHighlightColor' => null,
+ 'conflicts' => [
+ [
+ 'group' => 'group',
+ 'filter' => 'hidefoo',
+ 'globalDescription' => 'foo-bar-global-conflict',
+ 'contextDescription' => 'bar-conflicts-foo',
+ ]
+ ],
+ 'subset' => [],
+ 'messageKeys' => [
+ 'main-label',
+ 'main-description',
+ 'foo-bar-global-conflict',
+ 'bar-conflicts-foo',
+ ],
+ ],
+ $barData,
+ /** ordered= */ false,
+ /** named= */ true
+ );
+ }
+
+ public function testIsFeatureAvailableOnStructuredUi() {
+ $groupA = new ChangesListBooleanFilterGroup( [
+ 'name' => 'groupA',
+ 'priority' => 1,
+ 'filters' => [],
+ ] );
+
+ $foo = new ChangesListBooleanFilter( [
+ 'name' => 'hidefoo',
+ 'group' => $groupA,
+ 'label' => 'foo-label',
+ 'description' => 'foo-description',
+ 'default' => true,
+ 'showHide' => 'showhidefoo',
+ 'priority' => 2,
+ ] );
+
+ $this->assertEquals(
+ true,
+ $foo->isFeatureAvailableOnStructuredUi(),
+ 'Same filter appears on both'
+ );
+
+ // Should only be legacy ones that haven't been ported yet
+ $bar = new ChangesListBooleanFilter( [
+ 'name' => 'hidebar',
+ 'default' => true,
+ 'group' => $groupA,
+ 'showHide' => 'showhidebar',
+ 'priority' => 2,
+ ] );
+
+ $this->assertEquals(
+ false,
+ $bar->isFeatureAvailableOnStructuredUi(),
+ 'Only on unstructured UI'
+ );
+
+ $baz = new ChangesListBooleanFilter( [
+ 'name' => 'hidebaz',
+ 'default' => true,
+ 'group' => $groupA,
+ 'showHide' => 'showhidebaz',
+ 'isReplacedInStructuredUi' => true,
+ 'priority' => 2,
+ ] );
+
+ $this->assertEquals(
+ true,
+ $baz->isFeatureAvailableOnStructuredUi(),
+ 'Legacy filter does not appear directly in new UI, but equivalent ' .
+ 'does and is marked with isReplacedInStructuredUi'
+ );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/changes/ChangesListFilterGroupTest.php b/www/wiki/tests/phpunit/includes/changes/ChangesListFilterGroupTest.php
new file mode 100644
index 00000000..6190516e
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/changes/ChangesListFilterGroupTest.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @covers ChangesListFilterGroup
+ */
+class ChangesListFilterGroupTest extends MediaWikiTestCase {
+ /**
+ * phpcs:disable Generic.Files.LineLength
+ * @expectedException MWException
+ * @expectedExceptionMessage Group names may not contain '_'. Use the naming convention: 'camelCase'
+ * phpcs:enable
+ */
+ public function testReservedCharacter() {
+ new MockChangesListFilterGroup(
+ [
+ 'type' => 'some_type',
+ 'name' => 'group_name',
+ 'priority' => 1,
+ 'filters' => [],
+ ]
+ );
+ }
+
+ public function testAutoPriorities() {
+ $group = new MockChangesListFilterGroup(
+ [
+ 'type' => 'some_type',
+ 'name' => 'groupName',
+ 'isFullCoverage' => true,
+ 'priority' => 1,
+ 'filters' => [
+ [ 'name' => 'hidefoo' ],
+ [ 'name' => 'hidebar' ],
+ [ 'name' => 'hidebaz' ],
+ ],
+ ]
+ );
+
+ $filters = $group->getFilters();
+ $this->assertEquals(
+ [
+ -2,
+ -3,
+ -4,
+ ],
+ array_map(
+ function ( $f ) {
+ return $f->getPriority();
+ },
+ array_values( $filters )
+ )
+ );
+ }
+
+ // Get without warnings
+ public function testGetFilter() {
+ $group = new MockChangesListFilterGroup(
+ [
+ 'type' => 'some_type',
+ 'name' => 'groupName',
+ 'isFullCoverage' => true,
+ 'priority' => 1,
+ 'filters' => [
+ [ 'name' => 'foo' ],
+ ],
+ ]
+ );
+
+ $this->assertEquals(
+ 'foo',
+ $group->getFilter( 'foo' )->getName()
+ );
+
+ $this->assertEquals(
+ null,
+ $group->getFilter( 'bar' )
+ );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/changes/ChangesListFilterTest.php b/www/wiki/tests/phpunit/includes/changes/ChangesListFilterTest.php
new file mode 100644
index 00000000..039658e2
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/changes/ChangesListFilterTest.php
@@ -0,0 +1,116 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers ChangesListFilter
+ */
+class ChangesListFilterTest extends MediaWikiTestCase {
+ protected $group;
+
+ public function setUp() {
+ $this->group = $this->getGroup( [ 'name' => 'group' ] );
+
+ parent::setUp();
+ }
+
+ protected function getGroup( $groupDefinition ) {
+ return new MockChangesListFilterGroup(
+ $groupDefinition + [
+ 'isFullCoverage' => true,
+ 'type' => 'some_type',
+ 'name' => 'group',
+ 'filters' => [],
+ ]
+ );
+ }
+
+ /**
+ * phpcs:disable Generic.Files.LineLength
+ * @expectedException MWException
+ * @expectedExceptionMessage Filter names may not contain '_'. Use the naming convention: 'lowercase'
+ * phpcs:enable
+ */
+ public function testReservedCharacter() {
+ $filter = new MockChangesListFilter(
+ [
+ 'group' => $this->group,
+ 'name' => 'some_name',
+ 'priority' => 1,
+ ]
+ );
+ }
+
+ /**
+ * @expectedException MWException
+ * @expectedExceptionMessage Two filters in a group cannot have the same name: 'somename'
+ */
+ public function testDuplicateName() {
+ new MockChangesListFilter(
+ [
+ 'group' => $this->group,
+ 'name' => 'somename',
+ 'priority' => 1,
+ ]
+ );
+
+ new MockChangesListFilter(
+ [
+ 'group' => $this->group,
+ 'name' => 'somename',
+ 'priority' => 2,
+ ]
+ );
+ }
+
+ /**
+ * @expectedException MWException
+ * @expectedExceptionMessage Supersets can only be defined for filters in the same group
+ */
+ public function testSetAsSupersetOf() {
+ $groupA = $this->getGroup(
+ [
+ 'name' => 'groupA',
+ 'filters' => [
+ [
+ 'name' => 'foo',
+ ],
+ [
+ 'name' => 'bar',
+ ]
+ ],
+ ]
+ );
+
+ $groupB = $this->getGroup(
+ [
+ 'name' => 'groupB',
+ 'filters' => [
+ [
+ 'name' => 'baz',
+ ],
+ ],
+ ]
+ );
+
+ $foo = TestingAccessWrapper::newFromObject( $groupA->getFilter( 'foo' ) );
+
+ $bar = $groupA->getFilter( 'bar' );
+
+ $baz = $groupB->getFilter( 'baz' );
+
+ $foo->setAsSupersetOf( $bar );
+ $this->assertArrayEquals( [
+ [
+ 'group' => 'groupA',
+ 'filter' => 'bar',
+ ],
+ ],
+ $foo->subsetFilters,
+ /** ordered= */ false,
+ /** named= */ true
+ );
+
+ $foo->setAsSupersetOf( $baz );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/changes/ChangesListStringOptionsFilterGroupTest.php b/www/wiki/tests/phpunit/includes/changes/ChangesListStringOptionsFilterGroupTest.php
new file mode 100644
index 00000000..b627178a
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/changes/ChangesListStringOptionsFilterGroupTest.php
@@ -0,0 +1,279 @@
+<?php
+
+use Wikimedia\TestingAccessWrapper;
+
+/**
+ * @covers ChangesListStringOptionsFilterGroup
+ */
+class ChangesListStringOptionsFilterGroupTest extends MediaWikiTestCase {
+ /**
+ * @expectedException MWException
+ */
+ public function testIsFullCoverage() {
+ $falseGroup = TestingAccessWrapper::newFromObject(
+ new ChangesListStringOptionsFilterGroup( [
+ 'name' => 'group',
+ 'filters' => [],
+ 'isFullCoverage' => false,
+ 'queryCallable' => function () {
+ }
+ ] )
+ );
+
+ $this->assertSame(
+ false,
+ $falseGroup->isFullCoverage
+ );
+
+ // Should throw due to missing isFullCoverage
+ $undefinedFullCoverageGroup = new ChangesListStringOptionsFilterGroup( [
+ 'name' => 'othergroup',
+ 'filters' => [],
+ ] );
+ }
+
+ /**
+ * @param array $filterDefinitions Array of filter definitions
+ * @param array $expectedValues Array of values callback should receive
+ * @param string $input Value in URL
+ *
+ * @dataProvider provideModifyQuery
+ */
+ public function testModifyQuery( $filterDefinitions, $expectedValues, $input ) {
+ $queryCallable = function (
+ $className,
+ $ctx,
+ $dbr,
+ &$tables,
+ &$fields,
+ &$conds,
+ &$query_options,
+ &$join_conds,
+ $actualSelectedValues
+ ) use ( $expectedValues ) {
+ $this->assertSame(
+ $expectedValues,
+ $actualSelectedValues
+ );
+ };
+
+ $groupDefinition = [
+ 'name' => 'group',
+ 'default' => '',
+ 'isFullCoverage' => true,
+ 'filters' => $filterDefinitions,
+ 'queryCallable' => $queryCallable,
+ ];
+
+ $this->modifyQueryHelper( $groupDefinition, $input );
+ }
+
+ public function provideModifyQuery() {
+ $mixedFilters = [
+ [
+ 'name' => 'foo',
+ ],
+ [
+ 'name' => 'baz',
+ ],
+ [
+ 'name' => 'goo'
+ ],
+ ];
+
+ return [
+ [
+ $mixedFilters,
+ [ 'baz', 'foo', ],
+ 'foo;bar;BaZ;invalid',
+ ],
+
+ [
+ $mixedFilters,
+ [ 'baz', 'foo', 'goo' ],
+ 'all',
+ ],
+ ];
+ }
+
+ /**
+ * @param array $filterDefinitions Array of filter definitions
+ * @param string $input Value in URL
+ * @param string $message Message thrown by exception
+ *
+ * @dataProvider provideNoOpModifyQuery
+ */
+ public function testNoOpModifyQuery( $filterDefinitions, $input, $message ) {
+ $noFiltersAllowedCallable = function (
+ $className,
+ $ctx,
+ $dbr,
+ &$tables,
+ &$fields,
+ &$conds,
+ &$query_options,
+ &$join_conds,
+ $actualSelectedValues
+ ) use ( $message ) {
+ throw new MWException( $message );
+ };
+
+ $groupDefinition = [
+ 'name' => 'group',
+ 'default' => '',
+ 'isFullCoverage' => true,
+ 'filters' => $filterDefinitions,
+ 'queryCallable' => $noFiltersAllowedCallable,
+ ];
+
+ $this->modifyQueryHelper( $groupDefinition, $input );
+
+ $this->assertTrue(
+ true,
+ 'Test successfully completed without calling queryCallable'
+ );
+ }
+
+ public function provideNoOpModifyQuery() {
+ $noFilters = [];
+
+ $normalFilters = [
+ [
+ 'name' => 'foo',
+ ],
+ [
+ 'name' => 'bar',
+ ]
+ ];
+
+ return [
+ [
+ $noFilters,
+ 'disallowed1;disallowed3',
+ 'The queryCallable should not be called if there are no filters',
+ ],
+
+ [
+ $normalFilters,
+ '',
+ 'The queryCallable should not be called if no filters are selected',
+ ],
+
+ [
+ $normalFilters,
+ 'invalid1',
+ 'The queryCallable should not be called if no valid filters are selected',
+ ],
+ ];
+ }
+
+ protected function getSpecialPage() {
+ return $this->getMockBuilder( ChangesListSpecialPage::class )
+ ->setConstructorArgs( [
+ 'ChangesListSpecialPage',
+ '',
+ ] )
+ ->getMockForAbstractClass();
+ }
+
+ /**
+ * @param array $groupDefinition Group definition
+ * @param string $input Value in URL
+ */
+ protected function modifyQueryHelper( $groupDefinition, $input ) {
+ $ctx = $this->createMock( IContextSource::class );
+ $dbr = $this->createMock( Wikimedia\Rdbms\IDatabase::class );
+ $tables = $fields = $conds = $query_options = $join_conds = [];
+
+ $group = new ChangesListStringOptionsFilterGroup( $groupDefinition );
+
+ $specialPage = $this->getSpecialPage();
+ $opts = new FormOptions();
+ $opts->add( $groupDefinition[ 'name' ], $input );
+
+ $group->modifyQuery(
+ $dbr,
+ $specialPage,
+ $tables,
+ $fields,
+ $conds,
+ $query_options,
+ $join_conds,
+ $opts,
+ true
+ );
+ }
+
+ public function testGetJsData() {
+ $definition = [
+ 'name' => 'some-group',
+ 'title' => 'some-group-title',
+ 'default' => 'foo',
+ 'priority' => 1,
+ 'isFullCoverage' => false,
+ 'queryCallable' => function () {
+ },
+ 'filters' => [
+ [
+ 'name' => 'foo',
+ 'label' => 'foo-label',
+ 'description' => 'foo-description',
+ 'priority' => 2,
+ ],
+ [
+ 'name' => 'bar',
+ 'label' => 'bar-label',
+ 'description' => 'bar-description',
+ 'priority' => 4,
+ ]
+ ],
+ ];
+
+ $group = new ChangesListStringOptionsFilterGroup( $definition );
+
+ $this->assertArrayEquals(
+ [
+ 'name' => 'some-group',
+ 'title' => 'some-group-title',
+ 'type' => ChangesListStringOptionsFilterGroup::TYPE,
+ 'default' => 'foo',
+ 'priority' => 1,
+ 'fullCoverage' => false,
+ 'filters' => [
+ [
+ 'name' => 'bar',
+ 'label' => 'bar-label',
+ 'description' => 'bar-description',
+ 'priority' => 4,
+ 'cssClass' => null,
+ 'conflicts' => [],
+ 'subset' => [],
+ 'defaultHighlightColor' => null,
+ ],
+ [
+ 'name' => 'foo',
+ 'label' => 'foo-label',
+ 'description' => 'foo-description',
+ 'priority' => 2,
+ 'cssClass' => null,
+ 'conflicts' => [],
+ 'subset' => [],
+ 'defaultHighlightColor' => null,
+ ],
+ ],
+ 'conflicts' => [],
+ 'separator' => ';',
+ 'messageKeys' => [
+ 'some-group-title',
+ 'bar-label',
+ 'bar-description',
+ 'foo-label',
+ 'foo-description',
+ ],
+ ],
+ $group->getJsData(),
+ /** ordered= */ false,
+ /** named= */ true
+ );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/changes/EnhancedChangesListTest.php b/www/wiki/tests/phpunit/includes/changes/EnhancedChangesListTest.php
new file mode 100644
index 00000000..420fe749
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/changes/EnhancedChangesListTest.php
@@ -0,0 +1,228 @@
+<?php
+
+/**
+ * @covers EnhancedChangesList
+ *
+ * @group Database
+ *
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class EnhancedChangesListTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var TestRecentChangesHelper
+ */
+ private $testRecentChangesHelper;
+
+ public function __construct( $name = null, array $data = [], $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->testRecentChangesHelper = new TestRecentChangesHelper();
+ }
+
+ public function testBeginRecentChangesList_styleModules() {
+ $enhancedChangesList = $this->newEnhancedChangesList();
+ $enhancedChangesList->beginRecentChangesList();
+
+ $styleModules = $enhancedChangesList->getOutput()->getModuleStyles();
+
+ $this->assertContains(
+ 'mediawiki.special.changeslist',
+ $styleModules,
+ 'has mediawiki.special.changeslist'
+ );
+
+ $this->assertContains(
+ 'mediawiki.special.changeslist.enhanced',
+ $styleModules,
+ 'has mediawiki.special.changeslist.enhanced'
+ );
+ }
+
+ public function testBeginRecentChangesList_jsModules() {
+ $enhancedChangesList = $this->newEnhancedChangesList();
+ $enhancedChangesList->beginRecentChangesList();
+
+ $modules = $enhancedChangesList->getOutput()->getModules();
+
+ $this->assertContains( 'jquery.makeCollapsible', $modules, 'has jquery.makeCollapsible' );
+ $this->assertContains( 'mediawiki.icon', $modules, 'has mediawiki.icon' );
+ }
+
+ public function testBeginRecentChangesList_html() {
+ $enhancedChangesList = $this->newEnhancedChangesList();
+ $html = $enhancedChangesList->beginRecentChangesList();
+
+ $this->assertEquals( '<div class="mw-changeslist">', $html );
+ }
+
+ /**
+ * @todo more tests
+ */
+ public function testRecentChangesLine() {
+ $enhancedChangesList = $this->newEnhancedChangesList();
+ $enhancedChangesList->beginRecentChangesList();
+
+ $recentChange = $this->getEditChange( '20131103092153' );
+ $html = $enhancedChangesList->recentChangesLine( $recentChange, false );
+
+ $this->assertInternalType( 'string', $html );
+
+ $recentChange2 = $this->getEditChange( '20131103092253' );
+ $html = $enhancedChangesList->recentChangesLine( $recentChange2, false );
+
+ $this->assertEquals( '', $html );
+ }
+
+ public function testRecentChangesPrefix() {
+ $mockContext = $this->getMockBuilder( RequestContext::class )
+ ->setMethods( [ 'getTitle' ] )
+ ->getMock();
+ $mockContext->method( 'getTitle' )
+ ->will( $this->returnValue( Title::newFromText( 'Expected Context Title' ) ) );
+
+ // One group of two lines
+ $enhancedChangesList = $this->newEnhancedChangesList();
+ $enhancedChangesList->setContext( $mockContext );
+ $enhancedChangesList->setChangeLinePrefixer( function ( $rc, $changesList ) {
+ // Make sure RecentChange and ChangesList objects are the same
+ $this->assertEquals( 'Expected Context Title', $changesList->getContext()->getTitle() );
+ $this->assertTrue( $rc->getTitle() == 'Cat' || $rc->getTitle() == 'Dog' );
+ return 'Hello world prefix';
+ } );
+ $enhancedChangesList->beginRecentChangesList();
+
+ $recentChange = $this->getEditChange( '20131103092153' );
+ $enhancedChangesList->recentChangesLine( $recentChange );
+ $recentChange = $this->getEditChange( '20131103092154' );
+ $enhancedChangesList->recentChangesLine( $recentChange );
+
+ $html = $enhancedChangesList->endRecentChangesList();
+
+ $this->assertRegExp( '/Hello world prefix/', $html );
+
+ // Two separate lines
+ $enhancedChangesList->beginRecentChangesList();
+
+ $recentChange = $this->getEditChange( '20131103092153' );
+ $enhancedChangesList->recentChangesLine( $recentChange );
+ $recentChange = $this->getEditChange( '20131103092154', 'Dog' );
+ $enhancedChangesList->recentChangesLine( $recentChange );
+
+ $html = $enhancedChangesList->endRecentChangesList();
+
+ preg_match_all( '/Hello world prefix/', $html, $matches );
+ $this->assertCount( 2, $matches[0] );
+ }
+
+ public function testCategorizationLineFormatting() {
+ $html = $this->createCategorizationLine(
+ $this->getCategorizationChange( '20150629191735', 0, 0 )
+ );
+ $this->assertNotContains( '(diff | hist)', strip_tags( $html ) );
+ }
+
+ public function testCategorizationLineFormattingWithRevision() {
+ $html = $this->createCategorizationLine(
+ $this->getCategorizationChange( '20150629191735', 1025, 1024 )
+ );
+ $this->assertContains( '(diff | hist)', strip_tags( $html ) );
+ }
+
+ /**
+ * @todo more tests for actual formatting, this is more of a smoke test
+ */
+ public function testEndRecentChangesList() {
+ $enhancedChangesList = $this->newEnhancedChangesList();
+ $enhancedChangesList->beginRecentChangesList();
+
+ $recentChange = $this->getEditChange( '20131103092153' );
+ $enhancedChangesList->recentChangesLine( $recentChange, false );
+
+ $html = $enhancedChangesList->endRecentChangesList();
+ $this->assertRegExp(
+ '/data-mw-revid="5" data-mw-ts="20131103092153" class="[^"]*mw-enhanced-rc[^"]*"/',
+ $html
+ );
+
+ $recentChange2 = $this->getEditChange( '20131103092253' );
+ $enhancedChangesList->recentChangesLine( $recentChange2, false );
+
+ $html = $enhancedChangesList->endRecentChangesList();
+
+ preg_match_all( '/td class="mw-enhanced-rc-nested"/', $html, $matches );
+ $this->assertCount( 2, $matches[0] );
+
+ preg_match_all( '/data-target-page="Cat"/', $html, $matches );
+ $this->assertCount( 2, $matches[0] );
+
+ $recentChange3 = $this->getLogChange();
+ $enhancedChangesList->recentChangesLine( $recentChange3, false );
+
+ $html = $enhancedChangesList->endRecentChangesList();
+ $this->assertContains( 'data-mw-logaction="foo/bar"', $html );
+ $this->assertContains( 'data-mw-logid="25"', $html );
+ $this->assertContains( 'data-target-page="Title"', $html );
+ }
+
+ /**
+ * @return EnhancedChangesList
+ */
+ private function newEnhancedChangesList() {
+ $user = User::newFromId( 0 );
+ $context = $this->testRecentChangesHelper->getTestContext( $user );
+
+ return new EnhancedChangesList( $context );
+ }
+
+ /**
+ * @return RecentChange
+ */
+ private function getEditChange( $timestamp, $pageTitle = 'Cat' ) {
+ $user = $this->getMutableTestUser()->getUser();
+ $recentChange = $this->testRecentChangesHelper->makeEditRecentChange(
+ $user, $pageTitle, 0, 5, 191, $timestamp, 0, 0
+ );
+
+ return $recentChange;
+ }
+
+ private function getLogChange() {
+ $user = $this->getMutableTestUser()->getUser();
+ $recentChange = $this->testRecentChangesHelper->makeLogRecentChange( 'foo', 'bar', $user,
+ 'Title', '20131103092153', 0, 0
+ );
+
+ return $recentChange;
+ }
+
+ /**
+ * @return RecentChange
+ */
+ private function getCategorizationChange( $timestamp, $thisId, $lastId ) {
+ $wikiPage = new WikiPage( Title::newFromText( 'Testpage' ) );
+ $wikiPage->doEditContent( new WikitextContent( 'Some random text' ), 'page created' );
+
+ $wikiPage = new WikiPage( Title::newFromText( 'Category:Foo' ) );
+ $wikiPage->doEditContent( new WikitextContent( 'Some random text' ), 'category page created' );
+
+ $user = $this->getMutableTestUser()->getUser();
+ $recentChange = $this->testRecentChangesHelper->makeCategorizationRecentChange(
+ $user, 'Category:Foo', $wikiPage->getId(), $thisId, $lastId, $timestamp
+ );
+
+ return $recentChange;
+ }
+
+ private function createCategorizationLine( $recentChange ) {
+ $enhancedChangesList = $this->newEnhancedChangesList();
+ $cacheEntry = $this->testRecentChangesHelper->getCacheEntry( $recentChange );
+
+ $reflection = new \ReflectionClass( get_class( $enhancedChangesList ) );
+ $method = $reflection->getMethod( 'recentChangesBlockLine' );
+ $method->setAccessible( true );
+
+ return $method->invokeArgs( $enhancedChangesList, [ $cacheEntry ] );
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/changes/OldChangesListTest.php b/www/wiki/tests/phpunit/includes/changes/OldChangesListTest.php
new file mode 100644
index 00000000..91dc7312
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/changes/OldChangesListTest.php
@@ -0,0 +1,234 @@
+<?php
+
+/**
+ * @covers OldChangesList
+ *
+ * @todo add tests to cover article link, timestamp, character difference,
+ * log entry, user tool links, direction marks, tags, rollback,
+ * watching users, and date header.
+ *
+ * @group Database
+ *
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class OldChangesListTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var TestRecentChangesHelper
+ */
+ private $testRecentChangesHelper;
+
+ public function __construct( $name = null, array $data = [], $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->testRecentChangesHelper = new TestRecentChangesHelper();
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( [
+ 'wgArticlePath' => '/wiki/$1',
+ ] );
+ $this->setUserLang( 'qqx' );
+ }
+
+ /**
+ * @dataProvider recentChangesLine_CssForLineNumberProvider
+ */
+ public function testRecentChangesLine_CssForLineNumber( $expected, $linenumber, $message ) {
+ $oldChangesList = $this->getOldChangesList();
+ $recentChange = $this->getEditChange();
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, $linenumber );
+
+ $this->assertRegExp( $expected, $line, $message );
+ }
+
+ public function recentChangesLine_CssForLineNumberProvider() {
+ return [
+ [ '/mw-line-odd/', 1, 'odd line number' ],
+ [ '/mw-line-even/', 2, 'even line number' ]
+ ];
+ }
+
+ public function testRecentChangesLine_NotWatchedCssClass() {
+ $oldChangesList = $this->getOldChangesList();
+ $recentChange = $this->getEditChange();
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+
+ $this->assertRegExp( '/mw-changeslist-line-not-watched/', $line );
+ }
+
+ public function testRecentChangesLine_WatchedCssClass() {
+ $oldChangesList = $this->getOldChangesList();
+ $recentChange = $this->getEditChange();
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, true, 1 );
+
+ $this->assertRegExp( '/mw-changeslist-line-watched/', $line );
+ }
+
+ public function testRecentChangesLine_LogTitle() {
+ $oldChangesList = $this->getOldChangesList();
+ $recentChange = $this->getLogChange( 'delete', 'delete' );
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+
+ $this->assertRegExp( '/href="\/wiki\/Special:Log\/delete/', $line, 'link has href attribute' );
+ $this->assertRegExp( '/title="Special:Log\/delete/', $line, 'link has title attribute' );
+ $this->assertRegExp( "/dellogpage/", $line, 'link text' );
+ }
+
+ public function testRecentChangesLine_DiffHistLinks() {
+ $oldChangesList = $this->getOldChangesList();
+ $recentChange = $this->getEditChange();
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+
+ $this->assertRegExp(
+ '/title=Cat&amp;curid=20131103212153&amp;diff=5&amp;oldid=191/',
+ $line,
+ 'assert diff link'
+ );
+
+ $this->assertRegExp(
+ '/title=Cat&amp;curid=20131103212153&amp;action=history"/',
+ $line,
+ 'assert history link'
+ );
+ }
+
+ public function testRecentChangesLine_Flags() {
+ $oldChangesList = $this->getOldChangesList();
+ $recentChange = $this->getNewBotEditChange();
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+
+ $this->assertContains(
+ '<abbr class="newpage" title="(recentchanges-label-newpage)">(newpageletter)</abbr>',
+ $line,
+ 'new page flag'
+ );
+
+ $this->assertContains(
+ '<abbr class="botedit" title="(recentchanges-label-bot)">(boteditletter)</abbr>',
+ $line,
+ 'bot flag'
+ );
+ }
+
+ public function testRecentChangesLine_Attribs() {
+ $recentChange = $this->getEditChange();
+ $recentChange->mAttribs['ts_tags'] = 'vandalism,newbie';
+
+ $oldChangesList = $this->getOldChangesList();
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+
+ $this->assertRegExp(
+ '/<li data-mw-revid="\d+" data-mw-ts="\d+" class="[\w\s-]*mw-tag-vandalism[\w\s-]*">/',
+ $line
+ );
+ $this->assertRegExp(
+ '/<li data-mw-revid="\d+" data-mw-ts="\d+" class="[\w\s-]*mw-tag-newbie[\w\s-]*">/',
+ $line
+ );
+ }
+
+ public function testRecentChangesLine_numberOfWatchingUsers() {
+ $oldChangesList = $this->getOldChangesList();
+
+ $recentChange = $this->getEditChange();
+ $recentChange->numberofWatchingusers = 100;
+
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+ $this->assertRegExp( "/(number_of_watching_users_RCview: 100)/", $line );
+ }
+
+ public function testRecentChangesLine_watchlistCssClass() {
+ $oldChangesList = $this->getOldChangesList();
+ $oldChangesList->setWatchlistDivs( true );
+
+ $recentChange = $this->getEditChange();
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+ $this->assertRegExp( "/watchlist-0-Cat/", $line );
+ }
+
+ public function testRecentChangesLine_dataAttribute() {
+ $oldChangesList = $this->getOldChangesList();
+ $oldChangesList->setWatchlistDivs( true );
+
+ $recentChange = $this->getEditChange();
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+ $this->assertRegExp( '/data-target-page=\"Cat\"/', $line );
+
+ $recentChange = $this->getLogChange( 'delete', 'delete' );
+ $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+ $this->assertRegExp( '/data-target-page="Abc"/', $line );
+ }
+
+ public function testRecentChangesLine_prefix() {
+ $mockContext = $this->getMockBuilder( RequestContext::class )
+ ->setMethods( [ 'getTitle' ] )
+ ->getMock();
+ $mockContext->method( 'getTitle' )
+ ->will( $this->returnValue( Title::newFromText( 'Expected Context Title' ) ) );
+
+ $oldChangesList = $this->getOldChangesList();
+ $oldChangesList->setContext( $mockContext );
+ $recentChange = $this->getEditChange();
+
+ $oldChangesList->setChangeLinePrefixer( function ( $rc, $changesList ) {
+ // Make sure RecentChange and ChangesList objects are the same
+ $this->assertEquals( 'Expected Context Title', $changesList->getContext()->getTitle() );
+ $this->assertEquals( 'Cat', $rc->getTitle() );
+ return 'I am a prefix';
+ } );
+ $line = $oldChangesList->recentChangesLine( $recentChange );
+ $this->assertRegExp( "/I am a prefix/", $line );
+ }
+
+ private function getNewBotEditChange() {
+ $user = $this->getMutableTestUser()->getUser();
+
+ $recentChange = $this->testRecentChangesHelper->makeNewBotEditRecentChange(
+ $user, 'Abc', '20131103212153', 5, 191, 190, 0, 0
+ );
+
+ return $recentChange;
+ }
+
+ private function getLogChange( $logType, $logAction ) {
+ $user = $this->getMutableTestUser()->getUser();
+
+ $recentChange = $this->testRecentChangesHelper->makeLogRecentChange(
+ $logType, $logAction, $user, 'Abc', '20131103212153', 0, 0
+ );
+
+ return $recentChange;
+ }
+
+ private function getEditChange() {
+ $user = $this->getMutableTestUser()->getUser();
+ $recentChange = $this->testRecentChangesHelper->makeEditRecentChange(
+ $user, 'Cat', '20131103212153', 5, 191, 190, 0, 0
+ );
+
+ return $recentChange;
+ }
+
+ private function getOldChangesList() {
+ $context = $this->getContext();
+ return new OldChangesList( $context );
+ }
+
+ private function getContext() {
+ $user = $this->getMutableTestUser()->getUser();
+ $context = $this->testRecentChangesHelper->getTestContext( $user );
+ $context->setLanguage( 'qqx' );
+
+ return $context;
+ }
+
+}
diff --git a/www/wiki/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php b/www/wiki/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php
new file mode 100644
index 00000000..b1857ccc
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/changes/RCCacheEntryFactoryTest.php
@@ -0,0 +1,236 @@
+<?php
+
+use MediaWiki\Linker\LinkRenderer;
+use MediaWiki\MediaWikiServices;
+
+/**
+ * @covers RCCacheEntryFactory
+ *
+ * @group Database
+ *
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class RCCacheEntryFactoryTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var TestRecentChangesHelper
+ */
+ private $testRecentChangesHelper;
+
+ /**
+ * @var LinkRenderer
+ */
+ private $linkRenderer;
+
+ public function __construct( $name = null, array $data = [], $dataName = '' ) {
+ parent::__construct( $name, $data, $dataName );
+
+ $this->testRecentChangesHelper = new TestRecentChangesHelper();
+ }
+
+ protected function setUp() {
+ parent::setUp();
+
+ $this->setMwGlobals( [
+ 'wgArticlePath' => '/wiki/$1'
+ ] );
+
+ $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
+ }
+
+ public function testNewFromRecentChange() {
+ $user = $this->getMutableTestUser()->getUser();
+ $recentChange = $this->testRecentChangesHelper->makeEditRecentChange(
+ $user,
+ 'Xyz',
+ 5, // curid
+ 191, // thisid
+ 190, // lastid
+ '20131103212153',
+ 0, // counter
+ 0 // number of watching users
+ );
+ $cacheEntryFactory = new RCCacheEntryFactory(
+ $this->getContext(),
+ $this->getMessages(),
+ $this->linkRenderer
+ );
+ $cacheEntry = $cacheEntryFactory->newFromRecentChange( $recentChange, false );
+
+ $this->assertInstanceOf( RCCacheEntry::class, $cacheEntry );
+
+ $this->assertEquals( false, $cacheEntry->watched, 'watched' );
+ $this->assertEquals( '21:21', $cacheEntry->timestamp, 'timestamp' );
+ $this->assertEquals( 0, $cacheEntry->numberofWatchingusers, 'watching users' );
+ $this->assertEquals( false, $cacheEntry->unpatrolled, 'unpatrolled' );
+
+ $this->assertUserLinks( $user->getName(), $cacheEntry );
+ $this->assertTitleLink( 'Xyz', $cacheEntry );
+
+ $diff = [ 'curid' => 5, 'diff' => 191, 'oldid' => 190 ];
+ $cur = [ 'curid' => 5, 'diff' => 0, 'oldid' => 191 ];
+ $this->assertQueryLink( 'cur', $cur, $cacheEntry->curlink );
+ $this->assertQueryLink( 'prev', $diff, $cacheEntry->lastlink );
+ $this->assertQueryLink( 'diff', $diff, $cacheEntry->difflink );
+ }
+
+ public function testNewForDeleteChange() {
+ $user = $this->getMutableTestUser()->getUser();
+ $recentChange = $this->testRecentChangesHelper->makeLogRecentChange(
+ 'delete',
+ 'delete',
+ $user,
+ 'Abc',
+ '20131103212153',
+ 0, // counter
+ 0 // number of watching users
+ );
+ $cacheEntryFactory = new RCCacheEntryFactory(
+ $this->getContext(),
+ $this->getMessages(),
+ $this->linkRenderer
+ );
+ $cacheEntry = $cacheEntryFactory->newFromRecentChange( $recentChange, false );
+
+ $this->assertInstanceOf( RCCacheEntry::class, $cacheEntry );
+
+ $this->assertEquals( false, $cacheEntry->watched, 'watched' );
+ $this->assertEquals( '21:21', $cacheEntry->timestamp, 'timestamp' );
+ $this->assertEquals( 0, $cacheEntry->numberofWatchingusers, 'watching users' );
+ $this->assertEquals( false, $cacheEntry->unpatrolled, 'unpatrolled' );
+
+ $this->assertDeleteLogLink( $cacheEntry );
+ $this->assertUserLinks( $user->getName(), $cacheEntry );
+
+ $this->assertEquals( 'cur', $cacheEntry->curlink, 'cur link for delete log or rev' );
+ $this->assertEquals( 'diff', $cacheEntry->difflink, 'diff link for delete log or rev' );
+ $this->assertEquals( 'prev', $cacheEntry->lastlink, 'pref link for delete log or rev' );
+ }
+
+ public function testNewForRevUserDeleteChange() {
+ $user = $this->getMutableTestUser()->getUser();
+ $recentChange = $this->testRecentChangesHelper->makeDeletedEditRecentChange(
+ $user,
+ 'Zzz',
+ '20131103212153',
+ 191, // thisid
+ 190, // lastid
+ '20131103212153',
+ 0, // counter
+ 0 // number of watching users
+ );
+ $cacheEntryFactory = new RCCacheEntryFactory(
+ $this->getContext(),
+ $this->getMessages(),
+ $this->linkRenderer
+ );
+ $cacheEntry = $cacheEntryFactory->newFromRecentChange( $recentChange, false );
+
+ $this->assertInstanceOf( RCCacheEntry::class, $cacheEntry );
+
+ $this->assertEquals( false, $cacheEntry->watched, 'watched' );
+ $this->assertEquals( '21:21', $cacheEntry->timestamp, 'timestamp' );
+ $this->assertEquals( 0, $cacheEntry->numberofWatchingusers, 'watching users' );
+ $this->assertEquals( false, $cacheEntry->unpatrolled, 'unpatrolled' );
+
+ $this->assertRevDel( $cacheEntry );
+ $this->assertTitleLink( 'Zzz', $cacheEntry );
+
+ $this->assertEquals( 'cur', $cacheEntry->curlink, 'cur link for delete log or rev' );
+ $this->assertEquals( 'diff', $cacheEntry->difflink, 'diff link for delete log or rev' );
+ $this->assertEquals( 'prev', $cacheEntry->lastlink, 'pref link for delete log or rev' );
+ }
+
+ private function assertValidHTML( $actual ) {
+ // Throws if invalid
+ $doc = PHPUnit_Util_XML::load( $actual, /* isHtml */ true );
+ }
+
+ private function assertUserLinks( $user, $cacheEntry ) {
+ $this->assertValidHTML( $cacheEntry->userlink );
+ $this->assertRegExp(
+ '#^<a .*class="new mw-userlink".*><bdi>' . $user . '</bdi></a>#',
+ $cacheEntry->userlink,
+ 'verify user link'
+ );
+
+ $this->assertValidHTML( $cacheEntry->usertalklink );
+ $this->assertRegExp(
+ '#^ <span class="mw-usertoollinks">\(.*<a .+>talk</a>.*\)</span>#',
+ $cacheEntry->usertalklink,
+ 'verify user talk link'
+ );
+
+ $this->assertValidHTML( $cacheEntry->usertalklink );
+ $this->assertRegExp(
+ '#^ <span class="mw-usertoollinks">\(.*<a .+>contribs</a>.*\)</span>$#',
+ $cacheEntry->usertalklink,
+ 'verify user tool links'
+ );
+ }
+
+ private function assertDeleteLogLink( $cacheEntry ) {
+ $this->assertEquals(
+ '(<a href="/wiki/Special:Log/delete" title="Special:Log/delete">Deletion log</a>)',
+ $cacheEntry->link,
+ 'verify deletion log link'
+ );
+
+ $this->assertValidHTML( $cacheEntry->link );
+ }
+
+ private function assertRevDel( $cacheEntry ) {
+ $this->assertEquals(
+ ' <span class="history-deleted">(username removed)</span>',
+ $cacheEntry->userlink,
+ 'verify user link for change with deleted revision and user'
+ );
+ $this->assertValidHTML( $cacheEntry->userlink );
+ }
+
+ private function assertTitleLink( $title, $cacheEntry ) {
+ $this->assertEquals(
+ '<a href="/wiki/' . $title . '" title="' . $title . '">' . $title . '</a>',
+ $cacheEntry->link,
+ 'verify title link'
+ );
+ $this->assertValidHTML( $cacheEntry->link );
+ }
+
+ private function assertQueryLink( $content, $params, $link ) {
+ $this->assertRegExp(
+ "#^<a .+>$content</a>$#",
+ $link,
+ 'verify query link element'
+ );
+ $this->assertValidHTML( $link );
+
+ foreach ( $params as $key => $value ) {
+ $this->assertRegExp( '/' . $key . '=' . $value . '/', $link, "verify $key link params" );
+ }
+ }
+
+ private function getMessages() {
+ return [
+ 'cur' => 'cur',
+ 'diff' => 'diff',
+ 'hist' => 'hist',
+ 'enhancedrc-history' => 'history',
+ 'last' => 'prev',
+ 'blocklink' => 'block',
+ 'history' => 'Page history',
+ 'semicolon-separator' => '; ',
+ 'pipe-separator' => ' | '
+ ];
+ }
+
+ private function getContext() {
+ $user = $this->getMutableTestUser()->getUser();
+ $context = $this->testRecentChangesHelper->getTestContext( $user );
+
+ $title = Title::newFromText( 'RecentChanges', NS_SPECIAL );
+ $context->setTitle( $title );
+
+ return $context;
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/changes/RecentChangeTest.php b/www/wiki/tests/phpunit/includes/changes/RecentChangeTest.php
new file mode 100644
index 00000000..333eb286
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/changes/RecentChangeTest.php
@@ -0,0 +1,224 @@
+<?php
+use Wikimedia\ScopedCallback;
+
+/**
+ * @group Database
+ */
+class RecentChangeTest extends MediaWikiTestCase {
+ protected $title;
+ protected $target;
+ protected $user;
+ protected $user_comment;
+ protected $context;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->title = Title::newFromText( 'SomeTitle' );
+ $this->target = Title::newFromText( 'TestTarget' );
+ $this->user = User::newFromName( 'UserName' );
+
+ $this->user_comment = '<User comment about action>';
+ $this->context = RequestContext::newExtraneousContext( $this->title );
+ }
+
+ /**
+ * @covers RecentChange::newFromRow
+ * @covers RecentChange::loadFromRow
+ */
+ public function testNewFromRow() {
+ $user = $this->getTestUser()->getUser();
+ $actorId = $user->getActorId();
+
+ $row = new stdClass();
+ $row->rc_foo = 'AAA';
+ $row->rc_timestamp = '20150921134808';
+ $row->rc_deleted = 'bar';
+ $row->rc_comment_text = 'comment';
+ $row->rc_comment_data = null;
+ $row->rc_user = $user->getId();
+
+ $rc = RecentChange::newFromRow( $row );
+
+ $expected = [
+ 'rc_foo' => 'AAA',
+ 'rc_timestamp' => '20150921134808',
+ 'rc_deleted' => 'bar',
+ 'rc_comment' => 'comment',
+ 'rc_comment_text' => 'comment',
+ 'rc_comment_data' => null,
+ 'rc_user' => $user->getId(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_actor' => $actorId,
+ ];
+ $this->assertEquals( $expected, $rc->getAttributes() );
+
+ $row = new stdClass();
+ $row->rc_foo = 'AAA';
+ $row->rc_timestamp = '20150921134808';
+ $row->rc_deleted = 'bar';
+ $row->rc_comment = 'comment';
+ $row->rc_user = $user->getId();
+
+ Wikimedia\suppressWarnings();
+ $rc = RecentChange::newFromRow( $row );
+ Wikimedia\restoreWarnings();
+
+ $expected = [
+ 'rc_foo' => 'AAA',
+ 'rc_timestamp' => '20150921134808',
+ 'rc_deleted' => 'bar',
+ 'rc_comment' => 'comment',
+ 'rc_comment_text' => 'comment',
+ 'rc_comment_data' => null,
+ 'rc_user' => $user->getId(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_actor' => $actorId,
+ ];
+ $this->assertEquals( $expected, $rc->getAttributes() );
+ }
+
+ /**
+ * @covers RecentChange::parseParams
+ */
+ public function testParseParams() {
+ $params = [
+ 'root' => [
+ 'A' => 1,
+ 'B' => 'two'
+ ]
+ ];
+
+ $this->assertParseParams(
+ $params,
+ 'a:1:{s:4:"root";a:2:{s:1:"A";i:1;s:1:"B";s:3:"two";}}'
+ );
+
+ $this->assertParseParams(
+ null,
+ null
+ );
+
+ $this->assertParseParams(
+ null,
+ serialize( false )
+ );
+
+ $this->assertParseParams(
+ null,
+ 'not-an-array'
+ );
+ }
+
+ /**
+ * @param array $expectedParseParams
+ * @param string|null $rawRcParams
+ */
+ protected function assertParseParams( $expectedParseParams, $rawRcParams ) {
+ $rc = new RecentChange;
+ $rc->setAttribs( [ 'rc_params' => $rawRcParams ] );
+
+ $actualParseParams = $rc->parseParams();
+
+ $this->assertEquals( $expectedParseParams, $actualParseParams );
+ }
+
+ /**
+ * @return array
+ */
+ public function provideIsInRCLifespan() {
+ return [
+ [ 6000, -3000, 0, true ],
+ [ 3000, -6000, 0, false ],
+ [ 6000, -3000, 6000, true ],
+ [ 3000, -6000, 6000, true ],
+ ];
+ }
+
+ /**
+ * @covers RecentChange::isInRCLifespan
+ * @dataProvider provideIsInRCLifespan
+ */
+ public function testIsInRCLifespan( $maxAge, $offset, $tolerance, $expected ) {
+ $this->setMwGlobals( 'wgRCMaxAge', $maxAge );
+ // Calculate this here instead of the data provider because the provider
+ // is expanded early on and the full test suite may take longer than 100 minutes
+ // when coverage is enabled.
+ $timestamp = time() + $offset;
+ $this->assertEquals( $expected, RecentChange::isInRCLifespan( $timestamp, $tolerance ) );
+ }
+
+ public function provideRCTypes() {
+ return [
+ [ RC_EDIT, 'edit' ],
+ [ RC_NEW, 'new' ],
+ [ RC_LOG, 'log' ],
+ [ RC_EXTERNAL, 'external' ],
+ [ RC_CATEGORIZE, 'categorize' ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideRCTypes
+ * @covers RecentChange::parseFromRCType
+ */
+ public function testParseFromRCType( $rcType, $type ) {
+ $this->assertEquals( $type, RecentChange::parseFromRCType( $rcType ) );
+ }
+
+ /**
+ * @dataProvider provideRCTypes
+ * @covers RecentChange::parseToRCType
+ */
+ public function testParseToRCType( $rcType, $type ) {
+ $this->assertEquals( $rcType, RecentChange::parseToRCType( $type ) );
+ }
+
+ /**
+ * @return PHPUnit_Framework_MockObject_MockObject|PageProps
+ */
+ private function getMockPageProps() {
+ return $this->getMockBuilder( PageProps::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ }
+
+ public function provideCategoryContent() {
+ return [
+ [ true ],
+ [ false ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideCategoryContent
+ * @covers RecentChange::newForCategorization
+ */
+ public function testHiddenCategoryChange( $isHidden ) {
+ $categoryTitle = Title::newFromText( 'CategoryPage', NS_CATEGORY );
+
+ $pageProps = $this->getMockPageProps();
+ $pageProps->expects( $this->once() )
+ ->method( 'getProperties' )
+ ->with( $categoryTitle, 'hiddencat' )
+ ->will( $this->returnValue( $isHidden ? [ $categoryTitle->getArticleID() => '' ] : [] ) );
+
+ $scopedOverride = PageProps::overrideInstance( $pageProps );
+
+ $rc = RecentChange::newForCategorization(
+ '0',
+ $categoryTitle,
+ $this->user,
+ $this->user_comment,
+ $this->title,
+ $categoryTitle->getLatestRevID(),
+ $categoryTitle->getLatestRevID(),
+ '0',
+ false
+ );
+
+ $this->assertEquals( $isHidden, $rc->getParam( 'hidden-cat' ) );
+
+ ScopedCallback::consume( $scopedOverride );
+ }
+}
diff --git a/www/wiki/tests/phpunit/includes/changes/TestRecentChangesHelper.php b/www/wiki/tests/phpunit/includes/changes/TestRecentChangesHelper.php
new file mode 100644
index 00000000..2c309487
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/changes/TestRecentChangesHelper.php
@@ -0,0 +1,170 @@
+<?php
+
+use MediaWiki\MediaWikiServices;
+
+/**
+ * Helper for generating test recent changes entries.
+ *
+ * @author Katie Filbert < aude.wiki@gmail.com >
+ */
+class TestRecentChangesHelper {
+
+ public function makeEditRecentChange( User $user, $titleText, $curid, $thisid, $lastid,
+ $timestamp, $counter, $watchingUsers
+ ) {
+ $attribs = array_merge(
+ $this->getDefaultAttributes( $titleText, $timestamp ),
+ [
+ 'rc_user' => $user->getId(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_this_oldid' => $thisid,
+ 'rc_last_oldid' => $lastid,
+ 'rc_cur_id' => $curid
+ ]
+ );
+
+ return $this->makeRecentChange( $attribs, $counter, $watchingUsers );
+ }
+
+ public function makeLogRecentChange(
+ $logType, $logAction, User $user, $titleText, $timestamp, $counter, $watchingUsers
+ ) {
+ $attribs = array_merge(
+ $this->getDefaultAttributes( $titleText, $timestamp ),
+ [
+ 'rc_cur_id' => 0,
+ 'rc_user' => $user->getId(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_this_oldid' => 0,
+ 'rc_last_oldid' => 0,
+ 'rc_old_len' => null,
+ 'rc_new_len' => null,
+ 'rc_type' => 3,
+ 'rc_logid' => 25,
+ 'rc_log_type' => $logType,
+ 'rc_log_action' => $logAction,
+ 'rc_source' => 'mw.log'
+ ]
+ );
+
+ return $this->makeRecentChange( $attribs, $counter, $watchingUsers );
+ }
+
+ public function makeDeletedEditRecentChange( User $user, $titleText, $timestamp, $curid,
+ $thisid, $lastid, $counter, $watchingUsers
+ ) {
+ $attribs = array_merge(
+ $this->getDefaultAttributes( $titleText, $timestamp ),
+ [
+ 'rc_user' => $user->getId(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_deleted' => 5,
+ 'rc_cur_id' => $curid,
+ 'rc_this_oldid' => $thisid,
+ 'rc_last_oldid' => $lastid
+ ]
+ );
+
+ return $this->makeRecentChange( $attribs, $counter, $watchingUsers );
+ }
+
+ public function makeNewBotEditRecentChange( User $user, $titleText, $curid, $thisid, $lastid,
+ $timestamp, $counter, $watchingUsers
+ ) {
+ $attribs = array_merge(
+ $this->getDefaultAttributes( $titleText, $timestamp ),
+ [
+ 'rc_user' => $user->getId(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_this_oldid' => $thisid,
+ 'rc_last_oldid' => $lastid,
+ 'rc_cur_id' => $curid,
+ 'rc_type' => 1,
+ 'rc_bot' => 1,
+ 'rc_source' => 'mw.new'
+ ]
+ );
+
+ return $this->makeRecentChange( $attribs, $counter, $watchingUsers );
+ }
+
+ private function makeRecentChange( $attribs, $counter, $watchingUsers ) {
+ $change = new RecentChange();
+ $change->setAttribs( $attribs );
+ $change->counter = $counter;
+ $change->numberofWatchingusers = $watchingUsers;
+
+ return $change;
+ }
+
+ public function getCacheEntry( $recentChange ) {
+ $rcCacheFactory = new RCCacheEntryFactory(
+ new RequestContext(),
+ [ 'diff' => 'diff', 'cur' => 'cur', 'last' => 'last' ],
+ MediaWikiServices::getInstance()->getLinkRenderer()
+ );
+ return $rcCacheFactory->newFromRecentChange( $recentChange, false );
+ }
+
+ public function makeCategorizationRecentChange(
+ User $user, $titleText, $curid, $thisid, $lastid, $timestamp
+ ) {
+ $attribs = array_merge(
+ $this->getDefaultAttributes( $titleText, $timestamp ),
+ [
+ 'rc_type' => RC_CATEGORIZE,
+ 'rc_user' => $user->getId(),
+ 'rc_user_text' => $user->getName(),
+ 'rc_this_oldid' => $thisid,
+ 'rc_last_oldid' => $lastid,
+ 'rc_cur_id' => $curid,
+ 'rc_comment' => '[[:Testpage]] added to category',
+ 'rc_comment_text' => '[[:Testpage]] added to category',
+ 'rc_comment_data' => null,
+ 'rc_old_len' => 0,
+ 'rc_new_len' => 0,
+ ]
+ );
+
+ return $this->makeRecentChange( $attribs, 0, 0 );
+ }
+
+ private function getDefaultAttributes( $titleText, $timestamp ) {
+ return [
+ 'rc_id' => 545,
+ 'rc_user' => 0,
+ 'rc_user_text' => '127.0.0.1',
+ 'rc_ip' => '127.0.0.1',
+ 'rc_title' => $titleText,
+ 'rc_namespace' => 0,
+ 'rc_timestamp' => $timestamp,
+ 'rc_old_len' => 212,
+ 'rc_new_len' => 188,
+ 'rc_comment' => '',
+ 'rc_comment_text' => '',
+ 'rc_comment_data' => null,
+ 'rc_minor' => 0,
+ 'rc_bot' => 0,
+ 'rc_type' => 0,
+ 'rc_patrolled' => 1,
+ 'rc_deleted' => 0,
+ 'rc_logid' => 0,
+ 'rc_log_type' => null,
+ 'rc_log_action' => '',
+ 'rc_params' => '',
+ 'rc_source' => 'mw.edit'
+ ];
+ }
+
+ public function getTestContext( User $user ) {
+ $context = new RequestContext();
+ $context->setLanguage( 'en' );
+
+ $context->setUser( $user );
+
+ $title = Title::newFromText( 'RecentChanges', NS_SPECIAL );
+ $context->setTitle( $title );
+
+ return $context;
+ }
+}