wiki.techinc.nl/tests/phpunit/unit/includes/user/WatchlistNotificationManagerTest.php
DannyS712 c243c1b06e Add new WatchlistNotificationManager service
Replaces watchlist notification methods in Title and User classes:
* Title::getNotificationTimestamp -> ::getTitleNotificationTimestamp
* User::clearNotification -> ::clearTitleUserNotifications
* User::clearAllNotifications -> ::clearAllUserNotifications

New service has 67.90% code coverage with pure Unit tests; as well
as integration tests for the DeferredUpdates part

A follow-up patch will deprecate the replaced methods, as well
as document that the `UserClearNewTalkNotification` hook now only
provides a UserIdentity (typehint added in T253435 but until now
a full User was still provided).

Bug: T208777
Change-Id: I6f388c04cb9dc65b20ff028ece607c3dc131dfc5
2020-06-02 23:22:02 +00:00

518 lines
17 KiB
PHP

<?php
use MediaWiki\Config\ServiceOptions;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Revision\RevisionLookup;
use MediaWiki\User\TalkPageNotificationManager;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\WatchlistNotificationManager;
/**
* @covers \MediaWiki\User\WatchlistNotificationManager
*
* Cannot use the name `WatchlistNotificationManagerTest`, already used by the integration test
* @phpcs:disable MediaWiki.Files.ClassMatchesFilename
*
* @author DannyS712
*/
class WatchlistNotificationManagerUnitTest extends MediaWikiUnitTestCase {
private function getManager( array $params ) {
$config = $params['config'] ?? [
'UseEnotif' => false,
'ShowUpdatedMarker' => false
];
$options = new ServiceOptions(
WatchlistNotificationManager::CONSTRUCTOR_OPTIONS,
$config
);
$hookContainer = $params['hookContainer'] ??
$this->createNoOpMock( HookContainer::class );
$permissionManager = $params['permissionManager'] ??
$this->createNoOpMock( PermissionManager::class );
$readOnly = $params['readOnly'] ?? false;
$readOnlyMode = $this->createMock( ReadOnlyMode::class );
if ( $readOnly !== 'never' ) {
// Set 'never' for testing getTitleNotificationTimestamp, which doesn't call
$readOnlyMode->expects( $this->once() )
->method( 'isReadOnly' )
->willReturn( $readOnly );
}
$revisionLookup = $params['revisionLookup'] ??
$this->createNoOpAbstractMock( RevisionLookup::class );
$talkPageNotificationManager = $params['talkPageNotificationManager'] ??
$this->createNoOpMock( TalkPageNotificationManager::class );
$watchedItemStore = $params['watchedItemStore'] ??
$this->createNoOpAbstractMock( WatchedItemStoreInterface::class );
return new WatchlistNotificationManager(
$options,
$hookContainer,
$permissionManager,
$readOnlyMode,
$revisionLookup,
$talkPageNotificationManager,
$watchedItemStore
);
}
public function testClearAllUserNotifications_readOnly() {
$user = $this->createNoOpAbstractMock( UserIdentity::class );
// ********** Code path #1 **********
// Early return: read only mode
$manager = $this->getManager( [
'readOnly' => true
] );
$manager->clearAllUserNotifications( $user );
}
public function testClearAllUserNotifications_noPerms() {
$user = $this->createNoOpAbstractMock( UserIdentity::class );
// ********** Code path #2 **********
// Early return: User lacks `editmywatchlist`
$permissionManager = $this->createMock( PermissionManager::class );
$permissionManager->expects( $this->once() )
->method( 'userHasRight' )
->with(
$this->equalTo( $user ),
$this->equalTo( 'editmywatchlist' )
)
->willReturn( false );
$manager = $this->getManager( [
'permissionManager' => $permissionManager
] );
$manager->clearAllUserNotifications( $user );
}
public function testClearAllUserNotifications_configDisabled() {
$user = $this->createNoOpAbstractMock( UserIdentity::class );
// ********** Code path #3 **********
// Early return: config with `UseEnotif` and `ShowUpdatedMarker` both false
$permissionManager = $this->createMock( PermissionManager::class );
$permissionManager->expects( $this->once() )
->method( 'userHasRight' )
->with(
$this->equalTo( $user ),
$this->equalTo( 'editmywatchlist' )
)
->willReturn( true );
$talkPageNotificationManager = $this->createMock( TalkPageNotificationManager::class );
$talkPageNotificationManager->expects( $this->once() )
->method( 'removeUserHasNewMessages' )
->with( $this->equalTo( $user ) );
$manager = $this->getManager( [
'permissionManager' => $permissionManager,
'talkPageNotificationManager' => $talkPageNotificationManager
] );
$manager->clearAllUserNotifications( $user );
}
public function testClearAllUserNotifications_falseyId() {
// ********** Code path #4 **********
// Early return: user's id is falsey
$config = [
'UseEnotif' => true,
'ShowUpdatedMarker' => true
];
$user = $this->getMockBuilder( UserIdentity::class )
->setMethods( [ 'getId' ] )
->getMockForAbstractClass();
$user->expects( $this->once() )
->method( 'getId' )
->willReturn( 0 );
$permissionManager = $this->createMock( PermissionManager::class );
$permissionManager->expects( $this->once() )
->method( 'userHasRight' )
->with(
$this->equalTo( $user ),
$this->equalTo( 'editmywatchlist' )
)
->willReturn( true );
$manager = $this->getManager( [
'config' => $config,
'permissionManager' => $permissionManager
] );
$manager->clearAllUserNotifications( $user );
}
public function testClearAllUserNotifications() {
// ********** Code path #5 **********
// No early returns
$config = [
'UseEnotif' => true,
'ShowUpdatedMarker' => true
];
$user = $this->getMockBuilder( UserIdentity::class )
->setMethods( [ 'getId' ] )
->getMockForAbstractClass();
$user->expects( $this->once() )
->method( 'getId' )
->willReturn( 1 );
$permissionManager = $this->createMock( PermissionManager::class );
$permissionManager->expects( $this->once() )
->method( 'userHasRight' )
->with(
$this->equalTo( $user ),
$this->equalTo( 'editmywatchlist' )
)
->willReturn( true );
$watchedItemStore = $this->getMockBuilder( WatchedItemStoreInterface::class )
->setMethods( [ 'resetAllNotificationTimestampsForUser' ] )
->getMockForAbstractClass();
$watchedItemStore->expects( $this->once() )
->method( 'resetAllNotificationTimestampsForUser' )
->with( $this->equalTo( $user ) );
$manager = $this->getManager( [
'config' => $config,
'permissionManager' => $permissionManager,
'watchedItemStore' => $watchedItemStore
] );
$manager->clearAllUserNotifications( $user );
}
public function testClearTitleUserNotifications_readOnly() {
$user = $this->createNoOpAbstractMock( UserIdentity::class );
$title = $this->createNoOpAbstractMock( LinkTarget::class );
// ********** Code path #1 **********
// Early return: read only mode
$manager = $this->getManager( [
'readOnly' => true
] );
$manager->clearTitleUserNotifications( $user, $title );
}
public function testClearTitleUserNotifications_noPerms() {
$user = $this->createNoOpAbstractMock( UserIdentity::class );
$title = $this->createNoOpAbstractMock( LinkTarget::class );
// ********** Code path #2 **********
// Early return: User lacks `editmywatchlist`
// readOnlyMode returned false, and since we'll use the same mock again don't only
// expect it once
$permissionManager = $this->createMock( PermissionManager::class );
$permissionManager->expects( $this->once() )
->method( 'userHasRight' )
->with(
$this->equalTo( $user ),
$this->equalTo( 'editmywatchlist' )
)
->willReturn( false );
$manager = $this->getManager( [
'permissionManager' => $permissionManager
] );
$manager->clearTitleUserNotifications( $user, $title );
}
public function testClearTitleUserNotifications_configDisabled() {
// ********** Code path #3 **********
// Early return: config with `UseEnotif` and `ShowUpdatedMarker` both false
// $user and $title are now checked for if the page is the user's talk page
// Currently only tests for when the title isn't the user's talk page, since
// DeferredUpdates::addCallableUpdate doesn't work in unit tests
$user = $this->getMockBuilder( UserIdentity::class )
->setMethods( [ 'getName' ] )
->getMockForAbstractClass();
$user->expects( $this->once() )
->method( 'getName' )
->willReturn( 'UserNameGoesHere' );
$title = $this->getMockBuilder( LinkTarget::class )
->setMethods( [ 'getNamespace', 'getText' ] )
->getMockForAbstractClass();
$title->expects( $this->once() )
->method( 'getNamespace' )
->willReturn( NS_USER_TALK );
$title->expects( $this->once() )
->method( 'getText' )
->willReturn( 'PageTitleGoesHere' );
$permissionManager = $this->createMock( PermissionManager::class );
$permissionManager->expects( $this->once() )
->method( 'userHasRight' )
->with(
$this->equalTo( $user ),
$this->equalTo( 'editmywatchlist' )
)
->willReturn( true );
$manager = $this->getManager( [
'permissionManager' => $permissionManager
] );
$manager->clearTitleUserNotifications( $user, $title );
}
public function testClearTitleUserNotifications_notRegistered() {
// ********** Code path #4 **********
// Early return: user is not registered
$config = [
'UseEnotif' => true,
'ShowUpdatedMarker' => true
];
$user = $this->getMockBuilder( UserIdentity::class )
->setMethods( [ 'getName', 'isRegistered' ] )
->getMockForAbstractClass();
$user->expects( $this->once() )
->method( 'getName' )
->willReturn( 'UserNameGoesHere' );
$user->expects( $this->once() )
->method( 'isRegistered' )
->willReturn( false );
$title = $this->getMockBuilder( LinkTarget::class )
->setMethods( [ 'getNamespace', 'getText' ] )
->getMockForAbstractClass();
$title->expects( $this->once() )
->method( 'getNamespace' )
->willReturn( NS_USER_TALK );
$title->expects( $this->once() )
->method( 'getText' )
->willReturn( 'PageTitleGoesHere' );
$permissionManager = $this->createMock( PermissionManager::class );
$permissionManager->expects( $this->once() )
->method( 'userHasRight' )
->with(
$this->equalTo( $user ),
$this->equalTo( 'editmywatchlist' )
)
->willReturn( true );
$manager = $this->getManager( [
'config' => $config,
'permissionManager' => $permissionManager
] );
$manager->clearTitleUserNotifications( $user, $title );
}
public function testClearTitleUserNotifications() {
// ********** Code path #5 **********
// No early returns; resetNotificationTimestamp is called
// PermissionManager now expects a different user object
$config = [
'UseEnotif' => true,
'ShowUpdatedMarker' => true
];
$user = $this->getMockBuilder( UserIdentity::class )
->setMethods( [ 'getName', 'isRegistered' ] )
->getMockForAbstractClass();
$user->expects( $this->once() )
->method( 'getName' )
->willReturn( 'UserNameGoesHere' );
$user->expects( $this->once() )
->method( 'isRegistered' )
->willReturn( true );
$title = $this->getMockBuilder( LinkTarget::class )
->setMethods( [ 'getNamespace', 'getText' ] )
->getMockForAbstractClass();
$title->expects( $this->once() )
->method( 'getNamespace' )
->willReturn( NS_USER_TALK );
$title->expects( $this->once() )
->method( 'getText' )
->willReturn( 'PageTitleGoesHere' );
$permissionManager = $this->createMock( PermissionManager::class );
$permissionManager->expects( $this->once() )
->method( 'userHasRight' )
->with(
$this->equalTo( $user ),
$this->equalTo( 'editmywatchlist' )
)
->willReturn( true );
$watchedItemStore = $this->getMockBuilder( WatchedItemStoreInterface::class )
->setMethods( [ 'resetNotificationTimestamp' ] )
->getMockForAbstractClass();
$watchedItemStore->expects( $this->once() )
->method( 'resetNotificationTimestamp' )
->with(
$this->equalTo( $user ),
$this->equalTo( $title ),
$this->equalTo( '' ),
$this->equalTo( 0 )
);
$manager = $this->getManager( [
'config' => $config,
'permissionManager' => $permissionManager,
'watchedItemStore' => $watchedItemStore
] );
$manager->clearTitleUserNotifications( $user, $title );
}
public function testGetTitleNotificationTimestamp_falseyId() {
// ********** Code path #1 **********
// Early return: user id is falsey
$user = $this->getMockBuilder( UserIdentity::class )
->setMethods( [ 'getId' ] )
->getMockForAbstractClass();
$user->expects( $this->once() )
->method( 'getId' )
->willReturn( 0 );
$title = $this->createNoOpAbstractMock( LinkTarget::class );
$manager = $this->getManager( [
'readOnly' => 'never'
] );
$res = $manager->getTitleNotificationTimestamp( $user, $title );
$this->assertFalse( $res, 'Early return for anonymous users is false' );
}
public function testGetTitleNotificationTimestamp_timestamp() {
// ********** Code path #2 **********
// Early return: value is already cached - will be tested after #3 because
// an entry in the cache is needed (duh)
// ********** Code path #3 **********
// Actually check watchedItemStore, v.1-a - returns a WatchedItem with a timestamp
// From here on a cache key will be generated each time
$user = $this->getMockBuilder( UserIdentity::class )
->setMethods( [ 'getId' ] )
->getMockForAbstractClass();
$user->expects( $this->exactly( 2 ) )
->method( 'getId' )
->willReturn( 1 );
$title = $this->getMockBuilder( LinkTarget::class )
->setMethods( [ 'getNamespace', 'getDBkey' ] )
->getMockForAbstractClass();
$title->expects( $this->exactly( 2 ) )
->method( 'getNamespace' )
->willReturn( NS_MAIN );
$title->expects( $this->exactly( 2 ) )
->method( 'getDBkey' )
->willReturn( 'Page_db_Key_goesHere' );
$watchedItem = $this->createMock( WatchedItem::class );
$watchedItem->expects( $this->once() )
->method( 'getNotificationTimestamp' )
->willReturn( 'stringTimestamp' );
$watchedItemStore = $this->getMockBuilder( WatchedItemStoreInterface::class )
->setMethods( [ 'getWatchedItem' ] )
->getMockForAbstractClass();
$watchedItemStore->expects( $this->once() )
->method( 'getWatchedItem' )
->with(
$this->equalTo( $user ),
$this->equalTo( $title )
)
->willReturn( $watchedItem );
$manager = $this->getManager( [
'readOnly' => 'never',
'watchedItemStore' => $watchedItemStore
] );
$res = $manager->getTitleNotificationTimestamp( $user, $title );
$this->assertSame(
'stringTimestamp',
$res,
'if getWatchedItem returns a WatchedItem, that object\'s timestamp is returned'
);
// ********** Code path #2 **********
// Actually test code path #2 now that there is something in the cache
// use the same $manager instance (so the value is in the cache, duh)
// all of the same expectations apply - getWatchedItem shouldn't be called again, and
// so was only expecting to be called ->once() above
$res = $manager->getTitleNotificationTimestamp( $user, $title );
$this->assertSame(
'stringTimestamp',
$res,
'if the timestamp is cached getWatchedItem is not called again'
);
}
public function testGetTitleNotificationTimestamp_null() {
$user = $this->getMockBuilder( UserIdentity::class )
->setMethods( [ 'getId' ] )
->getMockForAbstractClass();
$user->expects( $this->once() )
->method( 'getId' )
->willReturn( 1 );
$title = $this->getMockBuilder( LinkTarget::class )
->setMethods( [ 'getNamespace', 'getDBkey' ] )
->getMockForAbstractClass();
$title->expects( $this->once() )
->method( 'getNamespace' )
->willReturn( NS_MAIN );
$title->expects( $this->once() )
->method( 'getDBkey' )
->willReturn( 'Page_db_Key_goesHere' );
// ********** Code path #4 **********
// Actually check watchedItemStore, v.1-b - returns a WatchedItem with null
$watchedItem = $this->createMock( WatchedItem::class );
$watchedItem->expects( $this->once() )
->method( 'getNotificationTimestamp' )
->willReturn( null );
$watchedItemStore = $this->getMockBuilder( WatchedItemStoreInterface::class )
->setMethods( [ 'getWatchedItem' ] )
->getMockForAbstractClass();
$watchedItemStore->expects( $this->once() )
->method( 'getWatchedItem' )
->with(
$this->equalTo( $user ),
$this->equalTo( $title )
)
->willReturn( $watchedItem );
$manager = $this->getManager( [
'readOnly' => 'never',
'watchedItemStore' => $watchedItemStore
] );
$res = $manager->getTitleNotificationTimestamp( $user, $title );
$this->assertNull( $res, 'WatchedItem can return null instead of a timestamp' );
}
public function testGetTitleNotificationTimestamp_false() {
$user = $this->getMockBuilder( UserIdentity::class )
->setMethods( [ 'getId' ] )
->getMockForAbstractClass();
$user->expects( $this->once() )
->method( 'getId' )
->willReturn( 1 );
$title = $this->getMockBuilder( LinkTarget::class )
->setMethods( [ 'getNamespace', 'getDBkey' ] )
->getMockForAbstractClass();
$title->expects( $this->once() )
->method( 'getNamespace' )
->willReturn( NS_MAIN );
$title->expects( $this->once() )
->method( 'getDBkey' )
->willReturn( 'Page_db_Key_goesHere' );
// ********** Code path #5 **********
// Actually check watchedItemStore, v.2 - returns false
$watchedItemStore = $this->getMockBuilder( WatchedItemStoreInterface::class )
->setMethods( [ 'getWatchedItem' ] )
->getMockForAbstractClass();
$watchedItemStore->expects( $this->once() )
->method( 'getWatchedItem' )
->with(
$this->equalTo( $user ),
$this->equalTo( $title )
)
->willReturn( false );
$manager = $this->getManager( [
'readOnly' => 'never',
'watchedItemStore' => $watchedItemStore
] );
$res = $manager->getTitleNotificationTimestamp( $user, $title );
$this->assertFalse(
$res,
'getWatchedItem can return false if the item is not watched'
);
}
}