diff options
author | Nikki Nikkhoui <nnikkhoui@wikimedia.org> | 2020-07-22 14:40:51 -0700 |
---|---|---|
committer | Nikki Nikkhoui <nnikkhoui@wikimedia.org> | 2020-07-28 18:36:41 +0000 |
commit | 3ba476102d120bf130cf101df4f53088eb17f7bf (patch) | |
tree | c2e38fca8d18b1ef76d1ad2d2952b542c81c25a1 | |
parent | 54cff558a60df7c58b900837ff8afad014c05c36 (diff) | |
download | mediawikicore-3ba476102d120bf130cf101df4f53088eb17f7bf.tar.gz mediawikicore-3ba476102d120bf130cf101df4f53088eb17f7bf.zip |
/contributions/user/{user}/count
REST Endpoint for getting the number of contributions for a given
user (not yourself).
Change-Id: Ib3bfedcec0aa1af1983cec0a7bda28fc49ddc673
10 files changed, 358 insertions, 242 deletions
diff --git a/includes/Rest/Handler/AbstractContributionHandler.php b/includes/Rest/Handler/AbstractContributionHandler.php new file mode 100644 index 000000000000..c3b23ed90ba3 --- /dev/null +++ b/includes/Rest/Handler/AbstractContributionHandler.php @@ -0,0 +1,104 @@ +<?php + +namespace MediaWiki\Rest\Handler; + +use MediaWiki\Rest\Handler; +use MediaWiki\Rest\LocalizedHttpException; +use MediaWiki\Revision\ContributionsLookup; +use MediaWiki\User\UserFactory; +use MediaWiki\User\UserIdentity; +use MediaWiki\User\UserNameUtils; +use RequestContext; +use Wikimedia\Message\MessageValue; + +/** + * @since 1.35 + */ +abstract class AbstractContributionHandler extends Handler { + + /** + * @var ContributionsLookup + */ + protected $contributionsLookup; + + /** + * @var UserFactory + */ + protected $userFactory; + + /** Hard limit results to 20 contributions */ + protected const MAX_LIMIT = 20; + + /** + * @var bool User is requesting their own contributions + */ + protected $me; + + /** + * @var UserNameUtils + */ + protected $userNameUtils; + + /** + * @param ContributionsLookup $contributionsLookup + * @param UserFactory $userFactory + * @param UserNameUtils $userNameUtils + */ + public function __construct( + ContributionsLookup $contributionsLookup, + UserFactory $userFactory, + UserNameUtils $userNameUtils + ) { + $this->contributionsLookup = $contributionsLookup; + $this->userFactory = $userFactory; + $this->userNameUtils = $userNameUtils; + } + + protected function postInitSetup() { + $this->me = $this->getConfig()['mode'] === 'me'; + } + + /** + * Returns the user who's contributions we are requesting. + * Either me (requesting user) or another user. + * + * @return UserIdentity + * @throws LocalizedHttpException + */ + protected function getTargetUser() { + if ( $this->me ) { + $user = RequestContext::getMain()->getUser(); + if ( $user->isAnon() ) { + throw new LocalizedHttpException( + new MessageValue( 'rest-permission-denied-anon' ), 401 + ); + } + + return $user; + } + + $name = $this->getValidatedParams()['name'] ?? null; + if ( $this->userNameUtils->isIP( $name ) ) { + // Create an anonymous user instance for the given IP + // NOTE: We can't use a UserIdentityValue, because we might need the actor ID + $user = $this->userFactory->newAnonymous( $name ); + return $user; + } + + $user = $this->userFactory->newFromName( $name ); + if ( !$user ) { + throw new LocalizedHttpException( + new MessageValue( 'rest-invalid-user', [ $name ] ), 400 + ); + } + + if ( !$user->isRegistered() ) { + throw new LocalizedHttpException( + new MessageValue( 'rest-nonexistent-user', [ $user->getName() ] ), 404 + ); + } + + return $user; + } + +} diff --git a/includes/Rest/Handler/ContributionsCountHandler.php b/includes/Rest/Handler/ContributionsCountHandler.php index 21ef2c639c0e..d6d91ba93ee5 100644 --- a/includes/Rest/Handler/ContributionsCountHandler.php +++ b/includes/Rest/Handler/ContributionsCountHandler.php @@ -2,45 +2,26 @@ namespace MediaWiki\Rest\Handler; -use MediaWiki\Rest\Handler; use MediaWiki\Rest\LocalizedHttpException; use MediaWiki\Rest\ResponseInterface; -use MediaWiki\Revision\ContributionsLookup; use RequestContext; -use Wikimedia\Message\MessageValue; use Wikimedia\ParamValidator\ParamValidator; /** * @since 1.35 */ -class ContributionsCountHandler extends Handler { - - /** - * @var ContributionsLookup - */ - private $contributionsLookup; - - public function __construct( ContributionsLookup $contributionsLookup ) { - $this->contributionsLookup = $contributionsLookup; - } +class ContributionsCountHandler extends AbstractContributionHandler { /** * @return array|ResponseInterface * @throws LocalizedHttpException */ public function execute() { - $user = RequestContext::getMain()->getUser(); - if ( $user->isAnon() ) { - throw new LocalizedHttpException( - new MessageValue( 'rest-permission-denied-anon' ), 401 - ); - } - + $performer = RequestContext::getMain()->getUser(); + $target = $this->getTargetUser(); $tag = $this->getValidatedParams()['tag']; - $count = $this->contributionsLookup->getContributionCount( $user, $user, $tag ); - + $count = $this->contributionsLookup->getContributionCount( $target, $performer, $tag ); $response = [ 'count' => $count ]; - return $response; } @@ -52,6 +33,11 @@ class ContributionsCountHandler extends Handler { ParamValidator::PARAM_REQUIRED => false, ParamValidator::PARAM_DEFAULT => null, ], + 'name' => [ + self::PARAM_SOURCE => 'path', + ParamValidator::PARAM_TYPE => 'string', + ParamValidator::PARAM_REQUIRED => $this->me === false + ], ]; } diff --git a/includes/Rest/Handler/UserContributionsHandler.php b/includes/Rest/Handler/UserContributionsHandler.php index 5ff4911c24ea..4480b4761171 100644 --- a/includes/Rest/Handler/UserContributionsHandler.php +++ b/includes/Rest/Handler/UserContributionsHandler.php @@ -2,111 +2,17 @@ namespace MediaWiki\Rest\Handler; -use MediaWiki\Rest\Handler; use MediaWiki\Rest\LocalizedHttpException; use MediaWiki\Rest\ResponseInterface; -use MediaWiki\Revision\ContributionsLookup; use MediaWiki\Revision\ContributionsSegment; -use MediaWiki\User\UserFactory; -use MediaWiki\User\UserIdentity; -use MediaWiki\User\UserNameUtils; use RequestContext; -use Wikimedia\Assert\Assert; -use Wikimedia\Message\MessageValue; use Wikimedia\ParamValidator\ParamValidator; use Wikimedia\ParamValidator\TypeDef\IntegerDef; /** * @since 1.35 */ -class UserContributionsHandler extends Handler { - - /** - * @var ContributionsLookup - */ - private $contributionsLookup; - - /** - * @var UserFactory - */ - private $userFactory; - - /** Hard limit results to 20 contributions */ - private const MAX_LIMIT = 20; - - /** - * @var bool User is requesting their own contributions - */ - private $me; - - /** - * @var UserNameUtils - */ - private $userNameUtils; - - /** - * @param ContributionsLookup $contributionsLookup - * @param UserFactory $userFactory - * @param UserNameUtils $userNameUtils - */ - public function __construct( - ContributionsLookup $contributionsLookup, - UserFactory $userFactory, - UserNameUtils $userNameUtils - ) { - $this->contributionsLookup = $contributionsLookup; - $this->userFactory = $userFactory; - $this->userNameUtils = $userNameUtils; - } - - protected function postInitSetup() { - $this->me = $this->getConfig()['mode'] === 'me'; - } - - /** - * Returns the user who's contributions we are requesting. - * Either me (requesting user) or another user. - * - * @return UserIdentity - * @throws LocalizedHttpException - */ - private function getTargetUser() { - if ( $this->me ) { - $user = RequestContext::getMain()->getUser(); - if ( $user->isAnon() ) { - throw new LocalizedHttpException( - new MessageValue( 'rest-permission-denied-anon' ), 401 - ); - } - - return $user; - } - - $name = $this->getValidatedParams()['name'] ?? null; - Assert::invariant( $name !== null, '"name" parameter must be given if mode is not "me"' ); - - if ( $this->userNameUtils->isIP( $name ) ) { - // Create an anonymous user instance for the given IP - // NOTE: We can't use a UserIdentityValue, because we might need the actor ID - $user = $this->userFactory->newAnonymous( $name ); - return $user; - } - - $user = $this->userFactory->newFromName( $name ); - if ( !$user ) { - throw new LocalizedHttpException( - new MessageValue( 'rest-invalid-user', [ $name ] ), 400 - ); - } - - if ( !$user->isRegistered() ) { - throw new LocalizedHttpException( - new MessageValue( 'rest-nonexistent-user', [ $user->getName() ] ), 404 - ); - } - - return $user; - } +class UserContributionsHandler extends AbstractContributionHandler { /** * @return array|ResponseInterface @@ -115,7 +21,6 @@ class UserContributionsHandler extends Handler { public function execute() { $performer = RequestContext::getMain()->getUser(); $target = $this->getTargetUser(); - $limit = $this->getValidatedParams()['limit']; $segment = $this->getValidatedParams()['segment']; $tag = $this->getValidatedParams()['tag']; @@ -190,7 +95,7 @@ class UserContributionsHandler extends Handler { 'name' => [ self::PARAM_SOURCE => 'path', ParamValidator::PARAM_TYPE => 'string', - ParamValidator::PARAM_REQUIRED => false + ParamValidator::PARAM_REQUIRED => $this->me === false ], 'limit' => [ self::PARAM_SOURCE => 'query', @@ -198,7 +103,7 @@ class UserContributionsHandler extends Handler { ParamValidator::PARAM_REQUIRED => false, ParamValidator::PARAM_DEFAULT => self::MAX_LIMIT, IntegerDef::PARAM_MIN => 1, - IntegerDef::PARAM_MAX => self::MAX_LIMIT, + IntegerDef::PARAM_MAX => self::MAX_LIMIT ], 'segment' => [ self::PARAM_SOURCE => 'query', @@ -210,7 +115,7 @@ class UserContributionsHandler extends Handler { self::PARAM_SOURCE => 'query', ParamValidator::PARAM_TYPE => 'string', ParamValidator::PARAM_REQUIRED => false, - ParamValidator::PARAM_DEFAULT => null, + ParamValidator::PARAM_DEFAULT => null ], ]; } diff --git a/includes/Rest/coreDevelopmentRoutes.json b/includes/Rest/coreDevelopmentRoutes.json index a2fc40b040b7..3d7fead086f1 100644 --- a/includes/Rest/coreDevelopmentRoutes.json +++ b/includes/Rest/coreDevelopmentRoutes.json @@ -23,7 +23,20 @@ "path": "/coredev/v0/me/contributions/count", "class": "MediaWiki\\Rest\\Handler\\ContributionsCountHandler", "services": [ - "ContributionsLookup" - ] + "ContributionsLookup", + "UserFactory", + "UserNameUtils" + ], + "mode" : "me" + }, + { + "path": "/coredev/v0/user/{name}/contributions/count", + "class": "MediaWiki\\Rest\\Handler\\ContributionsCountHandler", + "services": [ + "ContributionsLookup", + "UserFactory", + "UserNameUtils" + ], + "mode" : "user" } ] diff --git a/tests/api-testing/REST/ContributionsCount.js b/tests/api-testing/REST/ContributionsCount.js index a45b1b2c6533..2f290a26a05c 100644 --- a/tests/api-testing/REST/ContributionsCount.js +++ b/tests/api-testing/REST/ContributionsCount.js @@ -1,11 +1,11 @@ 'use strict'; const { REST, assert, action, utils, clientFactory } = require( 'api-testing' ); -describe( 'GET /me/contributions/count', () => { +describe( 'GET /contributions/count', () => { const basePath = 'rest.php/coredev/v0'; - let arnold; - let arnoldAction; - let samAction; + let arnold, beth; + let arnoldAction, bethAction, samAction; + let editToDelete; before( async () => { // Sam will be the same Sam for all tests, even in other files @@ -14,7 +14,13 @@ describe( 'GET /me/contributions/count', () => { // Arnold will be a different Arnold every time arnoldAction = await action.getAnon(); await arnoldAction.account( 'Arnold_' ); + + // Beth will be a different Beth every time + bethAction = await action.getAnon(); + await bethAction.account( 'Beth_' ); + arnold = clientFactory.getRESTClient( basePath, arnoldAction ); + beth = clientFactory.getRESTClient( basePath, bethAction ); const oddEditsPage = utils.title( 'UserContribution_' ); const evenEditsPage = utils.title( 'UserContribution_' ); @@ -26,46 +32,37 @@ describe( 'GET /me/contributions/count', () => { const bobAction = await action.bob(); await bobAction.edit( evenEditsPage, [ { summary: 'Bob made revision 1' } ] ); - // arnold makes 2 edits - let page; - for ( let i = 1; i <= 2; i++ ) { - const oddEdit = i % 2; - const tags = oddEdit ? 'api-test' : null; - page = oddEdit ? oddEditsPage : evenEditsPage; - - await arnoldAction.edit( page, { tags } ); - await utils.sleep(); - } - } ); - - it( 'Returns status 401 for anon', async () => { - const anon = new REST( basePath ); - const { status, body } = await anon.get( '/me/contributions/count' ); - assert.equal( status, 401 ); - assert.nestedProperty( body, 'messageTranslations' ); - } ); + // arnold makes 2 edits. 1 with a tag, 1 without. + const tags = 'api-test'; + await arnoldAction.edit( oddEditsPage, { tags } ); + await utils.sleep(); + await arnoldAction.edit( evenEditsPage, {} ); - it( 'Returns status OK', async () => { - const response = await arnold.get( '/me/contributions/count' ); - assert.equal( response.status, 200 ); + const pageDeleteRevs = utils.title( 'UserContribution_' ); + editToDelete = await bethAction.edit( pageDeleteRevs, [ { text: 'Beth edit 1' } ] ); + await bethAction.edit( pageDeleteRevs, [ { text: 'Beth edit 2' } ] ); } ); - it( 'Returns the number of arnold\'s edits', async () => { - const { status, body } = await arnold.get( '/me/contributions/count' ); + const testGetContributionsCount = async ( client, endpoint ) => { + const { status, body } = await client.get( endpoint ); assert.equal( status, 200 ); + assert.property( body, 'count' ); - // assert body has property count with the correct value - assert.propertyVal( body, 'count', 2 ); - } ); + const { count } = body; + assert.deepEqual( count, 2 ); + }; - it( 'Does not return suppressed revisions when requesting user does not have appropriate permissions', async () => { - const { body: preDeleteBody } = await arnold.get( '/me/contributions/count' ); - assert.propertyVal( preDeleteBody, 'count', 2 ); + const testGetContributionsCountByTag = async ( client, endpoint ) => { + const { status, body } = await client.get( endpoint, { tag: 'api-test' } ); + assert.equal( status, 200 ); - const pageToDelete = utils.title( 'UserContribution_' ); + const { count } = body; + assert.deepEqual( count, 1 ); + }; - const editToDelete = await arnoldAction.edit( pageToDelete, [ { text: 'Delete me 1' } ] ); - await arnoldAction.edit( pageToDelete, [ { text: 'Delete me 2' } ] ); + const testSuppressedContributions = async ( client, endpoint ) => { + const { body: body } = await client.get( endpoint ); + assert.deepEqual( body.count, 2 ); await samAction.action( 'revisiondelete', { @@ -78,9 +75,9 @@ describe( 'GET /me/contributions/count', () => { 'POST' ); - // Users without appropriate permissions cannot see suppressed revisions (even their own) - const { body: arnoldGetBody } = await arnold.get( '/me/contributions/count' ); - assert.propertyVal( arnoldGetBody, 'count', 3 ); + // Users w/o appropriate permissions should not have suppressed revisions included in count + const { body: suppressedRevBody } = await client.get( endpoint ); + assert.deepEqual( suppressedRevBody.count, 1 ); await samAction.action( 'revisiondelete', { @@ -93,17 +90,73 @@ describe( 'GET /me/contributions/count', () => { 'POST' ); - // Users with appropriate permissions can see suppressed revisions - const { body: arnoldGetBody2 } = await arnold.get( '/me/contributions/count' ); - assert.propertyVal( arnoldGetBody2, 'count', 4 ); + const { body: unsuppressedRevBody } = await client.get( endpoint ); + assert.deepEqual( unsuppressedRevBody.count, 2 ); + }; + + describe( 'GET /me/contributions/count', () => { + const endpoint = '/me/contributions/count'; + + it( 'Returns status 404 for anon', async () => { + const anon = new REST( basePath ); + const response = await anon.get( endpoint ); + assert.equal( response.status, 401 ); + assert.nestedProperty( response.body, 'messageTranslations' ); + } ); + + it( 'Returns status OK', async () => { + const response = await arnold.get( endpoint ); + assert.equal( response.status, 200 ); + } ); + + it( 'Returns a list of another user\'s edits', async () => { + await testGetContributionsCount( arnold, endpoint ); + } ); + + it( 'Returns edits filtered by tag', async () => { + await testGetContributionsCountByTag( arnold, endpoint ); + } ); + + it( 'Does not return suppressed contributions when requesting user does not have appropriate permissions', async () => { + // Note that the suppressed contributions are Beth's contributions. + await testSuppressedContributions( beth, endpoint ); + } ); + } ); - it( 'Returns the number of arnold\'s edits filtered by tag', async () => { - const { status, body } = await arnold.get( '/me/contributions/count', { tag: 'api-test' } ); - assert.equal( status, 200 ); + describe( 'GET /user/{user}/contributions/count', () => { + let endpoint; + before( () => { + endpoint = `/user/${arnold.username}/contributions/count`; + } ); + + it( 'Returns status 404 for unknown user', async () => { + const anon = new REST( basePath ); + const unknownUser = `Unknown ${utils.uniq()}`; + const response = await anon.get( `/user/${unknownUser}/contributions/count` ); + assert.equal( response.status, 404 ); + assert.nestedProperty( response.body, 'messageTranslations' ); + } ); + + it( 'Returns status OK', async () => { + const response = await arnold.get( endpoint ); + assert.equal( response.status, 200 ); + } ); + + it( 'Returns a list of another user\'s edits', async () => { + await testGetContributionsCount( beth, endpoint ); + } ); + + it( 'Returns edits filtered by tag', async () => { + await testGetContributionsCountByTag( beth, endpoint ); + } ); + + it( 'Does not return suppressed contributions when requesting user does not have appropriate permissions', async () => { + // Note that the suppressed contributions are Beth's contributions. + const bethsEndpoint = `/user/${beth.username}/contributions/count`; + await testSuppressedContributions( arnold, bethsEndpoint ); + } ); - // assert body has property count with the correct number of tagged edits - assert.propertyVal( body, 'count', 1 ); } ); } ); diff --git a/tests/api-testing/REST/UserContributions.js b/tests/api-testing/REST/UserContributions.js index c320ba6b436b..4e2e531392dd 100644 --- a/tests/api-testing/REST/UserContributions.js +++ b/tests/api-testing/REST/UserContributions.js @@ -269,7 +269,6 @@ describe( 'GET contributions', () => { 'POST' ); - // Users with appropriate permissions can see suppressed contributions const { body: clientGetBody2 } = await client.get( endpoint ); assert.lengthOf( clientGetBody2.contributions, 2 ); }; diff --git a/tests/common/TestsAutoLoader.php b/tests/common/TestsAutoLoader.php index e03aeeb8da0b..78bc32b8c247 100644 --- a/tests/common/TestsAutoLoader.php +++ b/tests/common/TestsAutoLoader.php @@ -235,6 +235,7 @@ $wgAutoloadClasses += [ 'MediaWiki\Tests\Rest\Handler\HandlerTestTrait' => "$testDir/phpunit/unit/includes/Rest/Handler/HandlerTestTrait.php", 'MediaWiki\Tests\Rest\Handler\HelloHandler' => "$testDir/phpunit/unit/includes/Rest/Handler/HelloHandler.php", 'MediaWiki\Tests\Rest\Handler\MediaTestTrait' => "$testDir/phpunit/unit/includes/Rest/Handler/MediaTestTrait.php", + 'MediaWiki\Tests\Rest\Handler\ContributionsTestTrait' => "$testDir/phpunit/unit/includes/Rest/Handler/ContributionsTestTrait.php", # tests/suites 'ParserTestFileSuite' => "$testDir/phpunit/suites/ParserTestFileSuite.php", diff --git a/tests/phpunit/unit/includes/Rest/Handler/ContributionsCountHandlerTest.php b/tests/phpunit/unit/includes/Rest/Handler/ContributionsCountHandlerTest.php index ec70e86aba3a..354b7e912733 100644 --- a/tests/phpunit/unit/includes/Rest/Handler/ContributionsCountHandlerTest.php +++ b/tests/phpunit/unit/includes/Rest/Handler/ContributionsCountHandlerTest.php @@ -7,46 +7,71 @@ use MediaWiki\Rest\LocalizedHttpException; use MediaWiki\Rest\RequestData; use MediaWiki\Rest\RequestInterface; use MediaWiki\Revision\ContributionsLookup; +use MediaWiki\User\UserFactory; +use MediaWiki\User\UserNameUtils; use PHPUnit\Framework\MockObject\MockObject; use RequestContext; -use User; use Wikimedia\Message\MessageValue; /** * @covers \MediaWiki\Rest\Handler\ContributionsCountHandler */ class ContributionsCountHandlerTest extends \MediaWikiUnitTestCase { - + use ContributionsTestTrait; use HandlerTestTrait; - private function newHandler( $numRevisions = 5 ) { + private function newHandler( $numContributions = 5 ) { /** @var MockObject|ContributionsLookup $mockContributionsLookup */ $mockContributionsLookup = $this->createNoOpMock( ContributionsLookup::class, [ 'getContributionCount' ] ); - $mockContributionsLookup->method( 'getContributionCount' )->willReturn( $numRevisions ); - $handler = new ContributionsCountHandler( $mockContributionsLookup ); - return $handler; + + $mockContributionsLookup->method( 'getContributionCount' )->willReturn( $numContributions ); + + $mockUserFactory = $this->createNoOpMock( UserFactory::class, + [ 'newFromName', 'newAnonymous' ] + ); + $mockUserFactory->method( 'newFromName' ) + ->willReturnCallback( [ $this, 'makeMockUser' ] ); + $mockUserFactory->method( 'newAnonymous' ) + ->willReturnCallback( [ $this, 'makeMockUser' ] ); + + $mockUserNameUtils = $this->createNoOpMock( UserNameUtils::class, + [ 'isIP' ] + ); + $mockUserNameUtils->method( 'isIP' ) + ->willReturnCallback( function ( $name ) { + return $name === '127.0.0.1'; + } ); + + return new ContributionsCountHandler( + $mockContributionsLookup, + $mockUserFactory, + $mockUserNameUtils + ); } public function provideTestThatParametersAreHandledCorrectly() { - yield [ new RequestData( [] ) ]; + yield [ new RequestData( [] ), 'me' ]; yield [ new RequestData( [ 'queryParams' => [ 'tag' => 'test' ] ] - ) ]; + ), 'me' ]; yield [ new RequestData( [ 'queryParams' => [ 'tag' => null ] ] - ) ]; + ), 'me' ]; + yield [ new RequestData( + [ 'pathParams' => [ 'name' => 'someUser' ], 'queryParams' => [ 'tag' => '' ] ] + ), 'user' ]; yield [ new RequestData( - [ 'queryParams' => [ 'tag' => '' ] ] - ) ]; + [ 'pathParams' => [ 'name' => 'someUser' ] ] + ), 'user' ]; } /** * @param RequestInterface $request * @dataProvider provideTestThatParametersAreHandledCorrectly */ - public function testThatParametersAreHandledCorrectly( RequestInterface $request ) { + public function testThatParametersAreHandledCorrectly( RequestInterface $request, $mode ) { $mockContributionsLookup = $this->createNoOpMock( ContributionsLookup::class, [ 'getContributionCount' ] ); @@ -58,9 +83,8 @@ class ContributionsCountHandlerTest extends \MediaWikiUnitTestCase { ->with( $user, $user, $tag ) ->willReturn( 123 ); - $handler = new ContributionsCountHandler( $mockContributionsLookup ); - - $response = $this->executeHandler( $handler, $request ); + $handler = $this->newHandler( 5 ); + $response = $this->executeHandler( $handler, $request, [ 'mode' => $mode ] ); $this->assertSame( 200, $response->getStatusCode() ); } @@ -68,45 +92,72 @@ class ContributionsCountHandlerTest extends \MediaWikiUnitTestCase { public function testThatAnonymousUserReturns401() { $handler = $this->newHandler(); $request = new RequestData( [] ); - RequestContext::getMain()->setUser( new User() ); + + $user = $this->makeMockUser( '127.0.0.1' ); + RequestContext::getMain()->setUser( $user ); $this->expectExceptionObject( new LocalizedHttpException( new MessageValue( 'rest-permission-denied-anon' ), 401 ) ); - $response = $this->executeHandler( $handler, $request ); - $this->assertSame( 401, $response->getStatusCode() ); - } - - private function makeMockUser( $anon = false ) { - $user = $this->createNoOpMock( User::class, [ 'isAnon' ] ); - $user->method( 'isAnon' )->willReturn( $anon ); - return $user; + $this->executeHandler( $handler, $request, [ 'mode' => 'me' ] ); } public function provideThatResponseConformsToSchema() { - $basePath = 'https://wiki.example.com/rest/coredev/v0/me/contributions/count'; - yield [ 0, [ 'count' => 0 ] ]; - yield [ 3, [ 'count' => 3 ] ]; + yield [ 0, [ 'count' => 0 ], [], 'me' ]; + yield [ 3, [ 'count' => 3 ], [], 'me' ]; + yield [ 0, [ 'count' => 0 ], [ 'pathParams' => [ 'name' => 'someName' ] ], 'user' ]; + yield [ 3, [ 'count' => 3 ], [ 'pathParams' => [ 'name' => 'someName' ] ] , 'user' ]; } /** * @dataProvider provideThatResponseConformsToSchema */ - public function testThatResponseConformsToSchema( $numRevisions, $expectedResponse ) { - $handler = $this->newHandler( $numRevisions ); - $request = new RequestData( [] ); + public function testThatResponseConformsToSchema( $numContributions, $expectedResponse, $config, $mode ) { + $handler = $this->newHandler( $numContributions ); + $request = new RequestData( $config ); - $user = $this->makeMockUser(); + $user = $this->makeMockUser( 'Betty' ); RequestContext::getMain()->setUser( $user ); - $response = $this->executeHandlerAndGetBodyData( $handler, $request ); + $response = $this->executeHandlerAndGetBodyData( $handler, $request, [ 'mode' => $mode ] ); $this->assertSame( $expectedResponse, $response ); } -} -// Returns a list of page revisions by the current logged-in user -// There is a stable chronological order allowing the client to request -// the next or previous segments such that the client will eventually receive all contributions -// Returned list must segmented based on a LIMIT -// Response object must be JSON -// Response object must contain the following fields: + public function testThatInvalidUserReturns400() { + $handler = $this->newHandler(); + $request = new RequestData( [ 'pathParams' => [ 'name' => 'B/A/D' ] ] ); + + $user = $this->makeMockUser( 'Betty' ); + RequestContext::getMain()->setUser( $user ); + + $this->expectExceptionObject( + new LocalizedHttpException( new MessageValue( 'rest-invalid-user' ), 400 ) + ); + $this->executeHandler( $handler, $request, [ 'mode' => 'user' ] ); + } + + public function testThatUnknownUserReturns404() { + $handler = $this->newHandler(); + $request = new RequestData( [ 'pathParams' => [ 'name' => 'UNKNOWN' ] ] ); + + $user = $this->makeMockUser( 'Betty' ); + RequestContext::getMain()->setUser( $user ); + + $this->expectExceptionObject( + new LocalizedHttpException( new MessageValue( 'rest-nonexistent-user' ), 404 ) + ); + $this->executeHandler( $handler, $request, [ 'mode' => 'user' ] ); + } + + public function testThatIpUserReturns200() { + $handler = $this->newHandler(); + $request = new RequestData( [ 'pathParams' => [ 'name' => '127.0.0.1' ] ] ); + + $user = $this->makeMockUser( 'Betty' ); + RequestContext::getMain()->setUser( $user ); + + $data = $this->executeHandlerAndGetBodyData( $handler, $request, [ 'mode' => 'user' ] ); + $this->assertArrayHasKey( 'count', $data ); + } + +} diff --git a/tests/phpunit/unit/includes/Rest/Handler/ContributionsTestTrait.php b/tests/phpunit/unit/includes/Rest/Handler/ContributionsTestTrait.php new file mode 100644 index 000000000000..7468c7dcd9e6 --- /dev/null +++ b/tests/phpunit/unit/includes/Rest/Handler/ContributionsTestTrait.php @@ -0,0 +1,30 @@ +<?php + +namespace MediaWiki\Tests\Rest\Handler; + +use User; + +trait ContributionsTestTrait { + + public function makeMockUser( $name ) { + $isIP = ( $name === '127.0.0.1' ); + $isBad = ( $name === 'B/A/D' ); + $isUnknown = ( $name === 'UNKNOWN' ); + $isAnon = $isIP || $isBad || $isUnknown; + + if ( $isBad ) { + // per the contract of UserFactory::newFromName + return false; + } + + $user = $this->createNoOpMock( + User::class, + [ 'isAnon', 'getId', 'getName', 'isRegistered' ] + ); + $user->method( 'isAnon' )->willReturn( $isAnon ); + $user->method( 'isRegistered' )->willReturn( !$isAnon ); + $user->method( 'getId' )->willReturn( $isAnon ? 0 : 7 ); + $user->method( 'getName' )->willReturn( $name ); + return $user; + } +} diff --git a/tests/phpunit/unit/includes/Rest/Handler/UserContributionsHandlerTest.php b/tests/phpunit/unit/includes/Rest/Handler/UserContributionsHandlerTest.php index 6e6cfbb106ac..56116096dd45 100644 --- a/tests/phpunit/unit/includes/Rest/Handler/UserContributionsHandlerTest.php +++ b/tests/phpunit/unit/includes/Rest/Handler/UserContributionsHandlerTest.php @@ -11,24 +11,22 @@ use MediaWiki\Revision\ContributionsLookup; use MediaWiki\Revision\ContributionsSegment; use MediaWiki\Storage\MutableRevisionRecord; use MediaWiki\User\UserFactory; -use MediaWiki\User\UserIdentity; use MediaWiki\User\UserIdentityValue; use MediaWiki\User\UserNameUtils; use PHPUnit\Framework\MockObject\MockObject; use RequestContext; -use User; use Wikimedia\Message\MessageValue; /** * @covers \MediaWiki\Rest\Handler\UserContributionsHandler */ class UserContributionsHandlerTest extends \MediaWikiUnitTestCase { - + use ContributionsTestTrait; use HandlerTestTrait; private const DEFAULT_LIMIT = 20; - private function makeFakeRevisions( UserIdentity $user, int $numRevs, int $limit, int $segment = 1 ) { + private function makeFakeRevisions( int $numRevs, int $limit, int $segment = 1 ) { $revisions = []; $title = $this->makeMockTitle( 'Main_Page', [ 'id' => 1 ] ); for ( $i = $numRevs; $i >= 1; $i-- ) { @@ -58,8 +56,7 @@ class UserContributionsHandlerTest extends \MediaWikiUnitTestCase { [ 'getContributions' ] ); - $user = new UserIdentityValue( 0, 'test', 0 ); - $fakeRevisions = $this->makeFakeRevisions( $user, $numRevisions, 2 ); + $fakeRevisions = $this->makeFakeRevisions( $numRevisions, 2 ); $fakeSegment = $this->makeSegment( $fakeRevisions, $tags, $deltas, $flags ); $mockContributionsLookup->method( 'getContributions' )->willReturn( $fakeSegment ); @@ -78,7 +75,7 @@ class UserContributionsHandlerTest extends \MediaWikiUnitTestCase { $segment = $request->getQueryParams()['segment'] ?? ''; $tag = $request->getQueryParams()['tag'] ?? null; - $fakeRevisions = $this->makeFakeRevisions( $target, 5, $limit ); + $fakeRevisions = $this->makeFakeRevisions( 5, $limit ); $fakeSegment = $this->makeSegment( $fakeRevisions ); $mockContributionsLookup = $this->createNoOpMock( ContributionsLookup::class, @@ -189,7 +186,7 @@ class UserContributionsHandlerTest extends \MediaWikiUnitTestCase { $handler = $this->newHandler( $mockContributionsLookup ); RequestContext::getMain()->setUser( $performer ); - $response = $this->executeHandler( $handler, $request, [ 'mode' => 'name' ] ); + $response = $this->executeHandler( $handler, $request, [ 'mode' => 'user' ] ); $this->assertSame( 200, $response->getStatusCode() ); } @@ -244,29 +241,6 @@ class UserContributionsHandlerTest extends \MediaWikiUnitTestCase { $this->assertArrayHasKey( 'contributions', $data ); } - public function makeMockUser( $name ) { - $isIP = ( $name === '127.0.0.1' ); - $isBad = ( $name === 'B/A/D' ); - $isUnknown = ( $name === 'UNKNOWN' ); - $isAnon = $isIP || $isBad || $isUnknown; - - if ( $isBad ) { - // per the contract of UserFactory::newFromName - return false; - } - - $user = $this->createNoOpMock( - User::class, - [ 'isAnon', 'getId', 'getName', 'isRegistered', 'isLoggedIn' ] - ); - $user->method( 'isAnon' )->willReturn( $isAnon ); - $user->method( 'isRegistered' )->willReturn( !$isAnon ); - $user->method( 'isLoggedIn' )->willReturn( !$isAnon ); - $user->method( 'getId' )->willReturn( $isAnon ? 0 : 7 ); - $user->method( 'getName' )->willReturn( $name ); - return $user; - } - public function provideThatResponseConformsToSchema() { $basePath = 'https://wiki.example.com/rest/me/contributions'; yield [ 0, |