aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNikki Nikkhoui <nnikkhoui@wikimedia.org>2020-07-22 14:40:51 -0700
committerNikki Nikkhoui <nnikkhoui@wikimedia.org>2020-07-28 18:36:41 +0000
commit3ba476102d120bf130cf101df4f53088eb17f7bf (patch)
treec2e38fca8d18b1ef76d1ad2d2952b542c81c25a1
parent54cff558a60df7c58b900837ff8afad014c05c36 (diff)
downloadmediawikicore-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
-rw-r--r--includes/Rest/Handler/AbstractContributionHandler.php104
-rw-r--r--includes/Rest/Handler/ContributionsCountHandler.php32
-rw-r--r--includes/Rest/Handler/UserContributionsHandler.php103
-rw-r--r--includes/Rest/coreDevelopmentRoutes.json17
-rw-r--r--tests/api-testing/REST/ContributionsCount.js147
-rw-r--r--tests/api-testing/REST/UserContributions.js1
-rw-r--r--tests/common/TestsAutoLoader.php1
-rw-r--r--tests/phpunit/unit/includes/Rest/Handler/ContributionsCountHandlerTest.php129
-rw-r--r--tests/phpunit/unit/includes/Rest/Handler/ContributionsTestTrait.php30
-rw-r--r--tests/phpunit/unit/includes/Rest/Handler/UserContributionsHandlerTest.php36
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,