aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--autoload.php3
-rw-r--r--includes/Rest/Handler/AbstractContributionHandler.php79
-rw-r--r--includes/Rest/Handler/ContributionsCountHandler.php48
-rw-r--r--includes/Rest/Handler/UserContributionsHandler.php135
-rw-r--r--includes/Rest/coreDevelopmentRoutes.json36
-rw-r--r--tests/api-testing/REST/ContributionsCount.js170
-rw-r--r--tests/api-testing/REST/UserContributions.js427
-rw-r--r--tests/phpunit/unit/includes/Rest/Handler/ContributionsCountHandlerTest.php146
-rw-r--r--tests/phpunit/unit/includes/Rest/Handler/UserContributionsHandlerTest.php383
9 files changed, 0 insertions, 1427 deletions
diff --git a/autoload.php b/autoload.php
index b3046a8b5335..96d2bc4f6a2c 100644
--- a/autoload.php
+++ b/autoload.php
@@ -1947,10 +1947,8 @@ $wgAutoloadLocalClasses = [
'MediaWiki\\Rest\\CorsUtils' => __DIR__ . '/includes/Rest/CorsUtils.php',
'MediaWiki\\Rest\\EntryPoint' => __DIR__ . '/includes/Rest/EntryPoint.php',
'MediaWiki\\Rest\\Handler' => __DIR__ . '/includes/Rest/Handler.php',
- 'MediaWiki\\Rest\\Handler\\AbstractContributionHandler' => __DIR__ . '/includes/Rest/Handler/AbstractContributionHandler.php',
'MediaWiki\\Rest\\Handler\\ActionModuleBasedHandler' => __DIR__ . '/includes/Rest/Handler/ActionModuleBasedHandler.php',
'MediaWiki\\Rest\\Handler\\CompareHandler' => __DIR__ . '/includes/Rest/Handler/CompareHandler.php',
- 'MediaWiki\\Rest\\Handler\\ContributionsCountHandler' => __DIR__ . '/includes/Rest/Handler/ContributionsCountHandler.php',
'MediaWiki\\Rest\\Handler\\CreationHandler' => __DIR__ . '/includes/Rest/Handler/CreationHandler.php',
'MediaWiki\\Rest\\Handler\\EditHandler' => __DIR__ . '/includes/Rest/Handler/EditHandler.php',
'MediaWiki\\Rest\\Handler\\Helper\\HtmlInputTransformHelper' => __DIR__ . '/includes/Rest/Handler/Helper/HtmlInputTransformHelper.php',
@@ -1978,7 +1976,6 @@ $wgAutoloadLocalClasses = [
'MediaWiki\\Rest\\Handler\\SearchHandler' => __DIR__ . '/includes/Rest/Handler/SearchHandler.php',
'MediaWiki\\Rest\\Handler\\TransformHandler' => __DIR__ . '/includes/Rest/Handler/TransformHandler.php',
'MediaWiki\\Rest\\Handler\\UpdateHandler' => __DIR__ . '/includes/Rest/Handler/UpdateHandler.php',
- 'MediaWiki\\Rest\\Handler\\UserContributionsHandler' => __DIR__ . '/includes/Rest/Handler/UserContributionsHandler.php',
'MediaWiki\\Rest\\HeaderContainer' => __DIR__ . '/includes/Rest/HeaderContainer.php',
'MediaWiki\\Rest\\HeaderParser\\HeaderParserBase' => __DIR__ . '/includes/Rest/HeaderParser/HeaderParserBase.php',
'MediaWiki\\Rest\\HeaderParser\\HeaderParserError' => __DIR__ . '/includes/Rest/HeaderParser/HeaderParserError.php',
diff --git a/includes/Rest/Handler/AbstractContributionHandler.php b/includes/Rest/Handler/AbstractContributionHandler.php
deleted file mode 100644
index 3f3c96ce2825..000000000000
--- a/includes/Rest/Handler/AbstractContributionHandler.php
+++ /dev/null
@@ -1,79 +0,0 @@
-<?php
-
-namespace MediaWiki\Rest\Handler;
-
-use MediaWiki\Rest\Handler;
-use MediaWiki\Rest\LocalizedHttpException;
-use MediaWiki\Revision\ContributionsLookup;
-use MediaWiki\User\UserIdentity;
-use MediaWiki\User\UserNameUtils;
-use Wikimedia\Message\MessageValue;
-
-/**
- * @since 1.35
- */
-abstract class AbstractContributionHandler extends Handler {
-
- /**
- * @var ContributionsLookup
- */
- protected $contributionsLookup;
-
- /** 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 UserNameUtils $userNameUtils
- */
- public function __construct(
- ContributionsLookup $contributionsLookup,
- UserNameUtils $userNameUtils
- ) {
- $this->contributionsLookup = $contributionsLookup;
- $this->userNameUtils = $userNameUtils;
- }
-
- protected function postInitSetup() {
- $this->me = $this->getConfig()['mode'] === 'me';
- }
-
- /**
- * Returns the user whose contributions we are requesting.
- * Either me (requesting user) or another user.
- *
- * @return UserIdentity
- * @throws LocalizedHttpException
- */
- protected function getTargetUser() {
- if ( $this->me ) {
- $user = $this->getAuthority()->getUser();
- if ( !$user->isRegistered() ) {
- throw new LocalizedHttpException(
- new MessageValue( 'rest-permission-denied-anon' ), 401
- );
- }
- return $user;
- }
-
- /** @var UserIdentity $user */
- $user = $this->getValidatedParams()['user'];
- $name = $user->getName();
- if ( !$this->userNameUtils->isIP( $name ) && !$user->isRegistered() ) {
- throw new LocalizedHttpException(
- new MessageValue( 'rest-nonexistent-user', [ $name ] ), 404
- );
- }
- return $user;
- }
-}
diff --git a/includes/Rest/Handler/ContributionsCountHandler.php b/includes/Rest/Handler/ContributionsCountHandler.php
deleted file mode 100644
index 0f3605ebe121..000000000000
--- a/includes/Rest/Handler/ContributionsCountHandler.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-
-namespace MediaWiki\Rest\Handler;
-
-use MediaWiki\ParamValidator\TypeDef\UserDef;
-use MediaWiki\Rest\LocalizedHttpException;
-use MediaWiki\Rest\ResponseInterface;
-use Wikimedia\ParamValidator\ParamValidator;
-
-/**
- * @since 1.35
- */
-class ContributionsCountHandler extends AbstractContributionHandler {
-
- /**
- * @return array|ResponseInterface
- * @throws LocalizedHttpException
- */
- public function execute() {
- $target = $this->getTargetUser();
- $tag = $this->getValidatedParams()['tag'];
- $count = $this->contributionsLookup->getContributionCount( $target, $this->getAuthority(), $tag );
- $response = [ 'count' => $count ];
- return $response;
- }
-
- public function getParamSettings() {
- $settings = [
- 'tag' => [
- self::PARAM_SOURCE => 'query',
- ParamValidator::PARAM_TYPE => 'string',
- ParamValidator::PARAM_REQUIRED => false,
- ParamValidator::PARAM_DEFAULT => null,
- ]
- ];
- if ( $this->me === false ) {
- $settings['user'] = [
- self::PARAM_SOURCE => 'path',
- ParamValidator::PARAM_REQUIRED => true,
- ParamValidator::PARAM_TYPE => 'user',
- UserDef::PARAM_RETURN_OBJECT => true,
- UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp' ],
- ];
- }
- return $settings;
- }
-
-}
diff --git a/includes/Rest/Handler/UserContributionsHandler.php b/includes/Rest/Handler/UserContributionsHandler.php
deleted file mode 100644
index 54c651fac148..000000000000
--- a/includes/Rest/Handler/UserContributionsHandler.php
+++ /dev/null
@@ -1,135 +0,0 @@
-<?php
-
-namespace MediaWiki\Rest\Handler;
-
-use MediaWiki\ParamValidator\TypeDef\UserDef;
-use MediaWiki\Rest\LocalizedHttpException;
-use MediaWiki\Rest\ResponseInterface;
-use MediaWiki\Revision\ContributionsSegment;
-use MediaWiki\User\UserIdentity;
-use Wikimedia\ParamValidator\ParamValidator;
-use Wikimedia\ParamValidator\TypeDef\IntegerDef;
-
-/**
- * @since 1.35
- */
-class UserContributionsHandler extends AbstractContributionHandler {
-
- /**
- * @return array|ResponseInterface
- * @throws LocalizedHttpException
- */
- public function execute() {
- $target = $this->getTargetUser();
- $limit = $this->getValidatedParams()['limit'];
- $segment = $this->getValidatedParams()['segment'];
- $tag = $this->getValidatedParams()['tag'];
- $contributionsSegment =
- $this->contributionsLookup->getContributions( $target, $limit, $this->getAuthority(), $segment, $tag );
-
- $contributions = $this->getContributionsList( $contributionsSegment );
- $urls = $this->constructURLs( $contributionsSegment );
-
- $response = $urls + [ 'contributions' => $contributions ];
-
- return $response;
- }
-
- /**
- * Returns list of revisions
- *
- * @param ContributionsSegment $segment
- *
- * @return array[]
- */
- private function getContributionsList( ContributionsSegment $segment ): array {
- $revisionsData = [];
- foreach ( $segment->getRevisions() as $revision ) {
- $id = $revision->getId();
- $tags = [];
- foreach ( $segment->getTagsForRevision( $id ) as $tag => $message ) {
- $tags[] = [ 'name' => $tag, 'description' => $message->parse() ];
- }
- $revisionsData[] = [
- "id" => $id,
- "comment" => $revision->getComment()->text,
- "timestamp" => wfTimestamp( TS_ISO_8601, $revision->getTimestamp() ),
- "delta" => $segment->getDeltaForRevision( $id ),
- "size" => $revision->getSize(),
- "tags" => $tags,
- // Contribution type will always be MediaWiki revisions,
- // until we can reliably include contributions from other sources. See T257839.
- "type" => 'revision',
- "page" => [
- "id" => $revision->getPageId(),
- "key" => $revision->getPageAsLinkTarget()->getDBkey(),
- "title" => $revision->getPageAsLinkTarget()->getText()
- ]
- ];
- }
- return $revisionsData;
- }
-
- /**
- * @param ContributionsSegment $segment
- *
- * @return string[]
- */
- private function constructURLs( ContributionsSegment $segment ): array {
- $limit = $this->getValidatedParams()['limit'];
- $tag = $this->getValidatedParams()['tag'];
- /** @var UserIdentity $user */
- $user = $this->getValidatedParams()['user'] ?? null;
- $name = $user ? $user->getName() : null;
-
- $urls = [];
- $query = [ 'limit' => $limit, 'tag' => $tag ];
- $pathParams = [ 'user' => $name ];
-
- if ( $segment->isOldest() ) {
- $urls['older'] = null;
- } else {
- $urls['older'] = $this->getRouteUrl( $pathParams, $query + [ 'segment' => $segment->getBefore() ] );
- }
-
- $urls['newer'] = $this->getRouteUrl( $pathParams, $query + [ 'segment' => $segment->getAfter() ] );
- $urls['latest'] = $this->getRouteUrl( $pathParams, $query );
- return $urls;
- }
-
- public function getParamSettings() {
- $settings = [
- 'limit' => [
- self::PARAM_SOURCE => 'query',
- ParamValidator::PARAM_TYPE => 'integer',
- ParamValidator::PARAM_REQUIRED => false,
- ParamValidator::PARAM_DEFAULT => self::MAX_LIMIT,
- IntegerDef::PARAM_MIN => 1,
- IntegerDef::PARAM_MAX => self::MAX_LIMIT
- ],
- 'segment' => [
- self::PARAM_SOURCE => 'query',
- ParamValidator::PARAM_TYPE => 'string',
- ParamValidator::PARAM_REQUIRED => false,
- ParamValidator::PARAM_DEFAULT => ''
- ],
- 'tag' => [
- self::PARAM_SOURCE => 'query',
- ParamValidator::PARAM_TYPE => 'string',
- ParamValidator::PARAM_REQUIRED => false,
- ParamValidator::PARAM_DEFAULT => null
- ],
- ];
- if ( $this->me === false ) {
- $settings['user'] = [
- self::PARAM_SOURCE => 'path',
- ParamValidator::PARAM_REQUIRED => true,
- ParamValidator::PARAM_TYPE => 'user',
- UserDef::PARAM_RETURN_OBJECT => true,
- UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp' ],
- ];
- }
- return $settings;
- }
-
-}
diff --git a/includes/Rest/coreDevelopmentRoutes.json b/includes/Rest/coreDevelopmentRoutes.json
index 5fa9ffaba505..931e6f39e999 100644
--- a/includes/Rest/coreDevelopmentRoutes.json
+++ b/includes/Rest/coreDevelopmentRoutes.json
@@ -5,41 +5,5 @@
"services": [
"MainConfig"
]
- },
- {
- "path": "/coredev/v0/me/contributions",
- "class": "MediaWiki\\Rest\\Handler\\UserContributionsHandler",
- "services": [
- "ContributionsLookup",
- "UserNameUtils"
- ],
- "mode": "me"
- },
- {
- "path": "/coredev/v0/user/{user}/contributions",
- "class": "MediaWiki\\Rest\\Handler\\UserContributionsHandler",
- "services": [
- "ContributionsLookup",
- "UserNameUtils"
- ],
- "mode": "user"
- },
- {
- "path": "/coredev/v0/me/contributions/count",
- "class": "MediaWiki\\Rest\\Handler\\ContributionsCountHandler",
- "services": [
- "ContributionsLookup",
- "UserNameUtils"
- ],
- "mode": "me"
- },
- {
- "path": "/coredev/v0/user/{user}/contributions/count",
- "class": "MediaWiki\\Rest\\Handler\\ContributionsCountHandler",
- "services": [
- "ContributionsLookup",
- "UserNameUtils"
- ],
- "mode": "user"
}
]
diff --git a/tests/api-testing/REST/ContributionsCount.js b/tests/api-testing/REST/ContributionsCount.js
deleted file mode 100644
index be0fbafdd0c7..000000000000
--- a/tests/api-testing/REST/ContributionsCount.js
+++ /dev/null
@@ -1,170 +0,0 @@
-'use strict';
-const { REST, assert, action, utils, clientFactory } = require( 'api-testing' );
-
-describe( 'GET /contributions/count', () => {
- const basePath = 'rest.php/coredev/v0';
- let arnold, beth;
- let arnoldAction, bethAction, samAction;
- let editToDelete;
-
- before( async () => {
- // Sam will be the same Sam for all tests, even in other files
- samAction = await action.user( 'Sam', [ 'suppress' ] );
-
- // 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_' );
-
- // Create a tag.
- await action.makeTag( 'api-test' );
-
- // bob makes 1 edit
- const bobAction = await action.bob();
- await bobAction.edit( evenEditsPage, [ { summary: 'Bob made revision 1' } ] );
-
- // 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, {} );
-
- const pageDeleteRevs = utils.title( 'UserContribution_' );
- editToDelete = await bethAction.edit( pageDeleteRevs, [ { text: 'Beth edit 1' } ] );
- await bethAction.edit( pageDeleteRevs, [ { text: 'Beth edit 2' } ] );
- } );
-
- const testGetContributionsCount = async ( client, endpoint ) => {
- const { status, body, header } = await client.get( endpoint );
- assert.equal( status, 200 );
- assert.match( header[ 'content-type' ], /^application\/json/ );
- assert.property( body, 'count' );
-
- const { count } = body;
- assert.deepEqual( count, 2 );
- };
-
- const testGetContributionsCountByTag = async ( client, endpoint ) => {
- const { status, body, header } = await client.get( endpoint, { tag: 'api-test' } );
- assert.equal( status, 200 );
- assert.match( header[ 'content-type' ], /^application\/json/ );
-
- const { count } = body;
- assert.deepEqual( count, 1 );
- };
-
- const testSuppressedContributions = async ( client, endpoint ) => {
- const { body: body } = await client.get( endpoint );
- assert.deepEqual( body.count, 2 );
-
- await samAction.action( 'revisiondelete',
- {
- type: 'revision',
- token: await samAction.token(),
- target: editToDelete.title,
- hide: 'content|comment|user',
- ids: editToDelete.newrevid
- },
- 'POST'
- );
-
- // 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',
- {
- type: 'revision',
- token: await samAction.token(),
- target: editToDelete.title,
- show: 'content|comment|user',
- ids: editToDelete.newrevid
- },
- 'POST'
- );
-
- 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.match( response.header[ 'content-type' ], /^application\/json/ );
- assert.nestedProperty( response.body, 'messageTranslations' );
- } );
-
- it( 'Returns status OK', async () => {
- const response = await arnold.get( endpoint );
- assert.equal( response.status, 200 );
- } );
-
- // T301100
- it.skip( 'Returns a list of another user\'s edits', async () => {
- await testGetContributionsCount( arnold, endpoint );
- } );
-
- // T301100
- it.skip( '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 );
- } );
-
- } );
-
- 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.match( response.header[ 'content-type' ], /^application\/json/ );
- assert.nestedProperty( response.body, 'messageTranslations' );
- } );
-
- it( 'Returns status OK', async () => {
- const response = await arnold.get( endpoint );
- assert.equal( response.status, 200 );
- } );
-
- // T301100
- it.skip( 'Returns a list of another user\'s edits', async () => {
- await testGetContributionsCount( beth, endpoint );
- } );
-
- // T301100
- it.skip( '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 );
- } );
-
- } );
-
-} );
diff --git a/tests/api-testing/REST/UserContributions.js b/tests/api-testing/REST/UserContributions.js
deleted file mode 100644
index 15b21467efff..000000000000
--- a/tests/api-testing/REST/UserContributions.js
+++ /dev/null
@@ -1,427 +0,0 @@
-'use strict';
-const { REST, assert, action, utils, clientFactory } = require( 'api-testing' );
-
-describe( 'GET contributions', () => {
- const basePath = 'rest.php/coredev/v0';
- const anon = new REST( basePath );
- const limit = 2;
- const arnoldsRevisions = [];
- const arnoldsEdits = [];
- const arnoldsTags = [];
- let arnold, beth, mindy;
- let arnoldAction, samAction, mindyAction;
- const revisionText = { 0: '12345678', 1: 'A', 2: 'ABCD', 3: 'AB', 4: 'ABCDEFGH', 5: 'A' };
- const expectedRevisionDeltas = { 1: 1, 2: -4, 3: 1, 4: 4, 5: -1 };
- let editToDelete;
-
- before( async () => {
- // Sam will be the same Sam for all tests, even in other files
- samAction = await action.user( 'Sam', [ 'suppress' ] );
-
- // Arnold will be a different Arnold every time
- arnoldAction = await action.getAnon();
- await arnoldAction.account( 'Arnold_' );
-
- // Beth will be a different Beth every time
- const bethAction = await action.getAnon();
- await bethAction.account( 'Beth_' );
-
- arnold = clientFactory.getRESTClient( basePath, arnoldAction );
- mindy = clientFactory.getRESTClient( basePath, mindyAction );
- beth = clientFactory.getRESTClient( basePath, bethAction );
-
- const oddEditsPage = utils.title( 'UserContribution_' );
- const evenEditsPage = utils.title( 'UserContribution_' );
-
- // Create a tag.
- const tag = 'user-contribs-api-test';
- const tagDisplay = 'Api Test Display Text';
- await action.makeTag( tag, `''${ tagDisplay }''` );
-
- // Beth makes 2 edits, the first one is later suppressed
- const pageToDelete = utils.title( 'UserContribution_' );
- editToDelete = await bethAction.edit( pageToDelete, [ { text: 'Beth edit 1' } ] );
- await bethAction.edit( pageToDelete, [ { text: 'Beth edit 2' } ] );
-
- // Arnold makes 5 edits
- let page;
- for ( let i = 1; i <= 5; i++ ) {
- const oddEdit = i % 2;
- const tags = oddEdit ? tag : null;
- page = oddEdit ? oddEditsPage : evenEditsPage;
- arnoldsTags[ i ] = oddEdit ? [ { name: tag, description: `<i>${ tagDisplay }</i>` } ] : [];
- const revData = await arnoldAction.edit( page, { text: revisionText[ i ], tags } );
- await utils.sleep();
- arnoldsRevisions[ revData.newrevid ] = revData;
- arnoldsEdits[ i ] = revData;
- }
- } );
-
- const testGetEdits = async ( client, endpoint ) => {
- const { status, body, headers } = await client.get( endpoint, { limit } );
- assert.equal( status, 200 );
- assert.match( headers[ 'content-type' ], /^application\/json/ );
-
- // assert body has property contributions
- assert.property( body, 'contributions' );
- const { contributions } = body;
-
- // assert body.contributions is array
- assert.isArray( contributions );
-
- // assert body.contributions length is limit
- assert.lengthOf( contributions, limit );
-
- const lastRevision = arnoldsRevisions[ arnoldsRevisions.length - 1 ];
-
- // assert body.contributions object schema is correct
- assert.hasAllDeepKeys( contributions[ 0 ], [
- 'id', 'comment', 'timestamp', 'delta', 'size', 'page', 'tags', 'type'
- ] );
-
- assert.equal( contributions[ 0 ].page.key, utils.dbkey( lastRevision.title ) );
- assert.equal( contributions[ 0 ].page.title, lastRevision.title );
- assert.equal( contributions[ 0 ].comment, lastRevision.param_summary );
- assert.equal( contributions[ 0 ].timestamp, lastRevision.newtimestamp );
- assert.equal( contributions[ 0 ].size, revisionText[ 5 ].length );
- assert.equal( contributions[ 0 ].type, 'revision' );
- assert.equal( contributions[ 0 ].delta, expectedRevisionDeltas[ 5 ] );
- assert.isOk( Date.parse( contributions[ 0 ].timestamp ) );
- assert.isNotOk( Date.parse( 'xyz' ) );
- assert.isArray( contributions[ 0 ].tags );
-
- assert.isAbove( Date.parse( contributions[ 0 ].timestamp ),
- Date.parse( contributions[ 1 ].timestamp ) );
-
- assert.equal( contributions[ 1 ].size, revisionText[ 4 ].length );
- assert.equal( contributions[ 1 ].delta, expectedRevisionDeltas[ 4 ] );
-
- // assert body.contributions contains edits only by one user
- contributions.forEach( ( rev ) => {
- assert.property( arnoldsRevisions, rev.id );
- } );
- };
-
- const testGetEditsByTag = async ( client, endpoint ) => {
- const taggedRevisions = [ arnoldsEdits[ 1 ], arnoldsEdits[ 3 ], arnoldsEdits[ 5 ] ];
-
- const { status, body, headers } = await client.get( endpoint, { tag: 'user-contribs-api-test' } );
- assert.equal( status, 200 );
- assert.match( headers[ 'content-type' ], /^application\/json/ );
-
- // assert body has property contributions
- assert.property( body, 'contributions' );
- const { contributions } = body;
-
- // assert body.contributions length
- assert.lengthOf( contributions, taggedRevisions.length );
-
- // assert that there are no more contributions found
- assert.propertyVal( body, 'older', null );
-
- // assert body.contributions has the correct content
- assert.equal( contributions[ 0 ].id, arnoldsEdits[ 5 ].newrevid );
- assert.equal( contributions[ 1 ].id, arnoldsEdits[ 3 ].newrevid );
- assert.equal( contributions[ 2 ].id, arnoldsEdits[ 1 ].newrevid );
- };
-
- const testPagingForward = async ( client, endpoint ) => {
- // get latest segment
- const { body: latestSegment } = await client.get( endpoint, { limit } );
- assert.property( latestSegment, 'older' );
- assert.property( latestSegment, 'contributions' );
- assert.isArray( latestSegment.contributions );
- assert.lengthOf( latestSegment.contributions, 2 );
-
- // assert body.contributions has the correct content
- assert.equal( latestSegment.contributions[ 0 ].id, arnoldsEdits[ 5 ].newrevid );
- assert.equal( latestSegment.contributions[ 1 ].id, arnoldsEdits[ 4 ].newrevid );
-
- // Check whether the tags we applied manually are present.
- // MediaWiki can add additional software tags (such as mw-manual-revert),
- // hence the inclusion check and not equality check.
- const latestSegmentTag = latestSegment.contributions[ 0 ].tags.find( ( tag ) => tag.name === 'user-contribs-api-test' );
- assert.propertyVal( latestSegmentTag, 'description', arnoldsTags[ 5 ][ 0 ].description );
-
- const latestSegmentTag2 = latestSegment.contributions[ 1 ].tags.find( ( tag ) => tag.name === 'user-contribs-api-test' );
- assert.isUndefined( latestSegmentTag2 );
-
- // get older segment, using full url
- const req = clientFactory.getHttpClient( client );
-
- const { body: olderSegment } = await req.get( latestSegment.older );
- assert.property( olderSegment, 'older' );
- assert.property( olderSegment, 'contributions' );
- assert.isArray( olderSegment.contributions );
- assert.lengthOf( olderSegment.contributions, 2 );
-
- // assert body.contributions has the correct content
- assert.equal( olderSegment.contributions[ 0 ].id, arnoldsEdits[ 3 ].newrevid );
- assert.equal( olderSegment.contributions[ 1 ].id, arnoldsEdits[ 2 ].newrevid );
-
- // ensure first edit has tags and correct property values
- const olderSegmentTag = olderSegment.contributions[ 0 ].tags.find( ( tag ) => tag.name === 'user-contribs-api-test' );
- assert.propertyVal( olderSegmentTag, 'description', arnoldsTags[ 3 ][ 0 ].description );
-
- // ensure second edit does not have tags
- const olderSegmentTag2 = latestSegment.contributions[ 1 ].tags.find( ( tag ) => tag.name === 'user-contribs-api-test' );
- assert.isUndefined( olderSegmentTag2 );
-
- // get the next older segment
- const { body: finalSegment } = await req.get( olderSegment.older );
- assert.propertyVal( finalSegment, 'older', null );
- assert.property( finalSegment, 'contributions' );
- assert.isArray( finalSegment.contributions );
- assert.lengthOf( finalSegment.contributions, 1 );
-
- // assert body.contributions has the correct content
- assert.equal( finalSegment.contributions[ 0 ].id, arnoldsEdits[ 1 ].newrevid );
-
- const finalSegmentTags = olderSegment.contributions[ 0 ].tags.find( ( tag ) => tag.name === 'user-contribs-api-test' );
- assert.propertyVal( finalSegmentTags, 'description', arnoldsTags[ 1 ][ 0 ].description );
- };
-
- const testPagingBackwards = async ( client, endpoint ) => {
- const req = clientFactory.getHttpClient( client );
-
- // get latest segment
- const { body: latestSegment } = await client.get( endpoint, { limit } );
- assert.property( latestSegment, 'newer' );
-
- // get next older segment
- const { body: olderSegment } = await req.get( latestSegment.older );
- assert.property( olderSegment, 'newer' );
-
- // get the final segment
- const { body: finalSegment } = await req.get( olderSegment.older );
- assert.property( finalSegment, 'newer' );
-
- // Follow the chain of "newer" links back to the latest segment
- const { body: olderSegment2 } = await req.get( finalSegment.newer );
- assert.deepEqual( olderSegment, olderSegment2 );
-
- const { body: latestSegment2 } = await req.get( olderSegment.newer );
- assert.deepEqual( latestSegment, latestSegment2 );
- };
-
- const testHasLatest = async ( client, endpoint ) => {
- const req = clientFactory.getHttpClient( client );
-
- // get latest segment
- const { body: latestSegment } = await client.get( endpoint, { limit } );
- assert.property( latestSegment, 'latest' );
-
- // get next older segment
- const { body: olderSegment } = await req.get( latestSegment.older );
- assert.property( olderSegment, 'latest' );
-
- // get the final segment
- const { body: finalSegment } = await req.get( olderSegment.older );
- assert.property( finalSegment, 'latest' );
-
- // Follow all the "newer" links
- const { body: latestSegment2 } = await req.get( latestSegment.latest );
- assert.deepEqual( latestSegment, latestSegment2 );
-
- assert.deepEqual( latestSegment.latest, finalSegment.latest );
- assert.deepEqual( latestSegment.latest, olderSegment.latest );
-
- };
-
- const testPreserveTagFilter = async ( client, endpoint ) => {
- const req = clientFactory.getHttpClient( client );
-
- // get latest segment
- const { body: latestSegment } = await client.get( endpoint, { limit: 2, tag: 'user-contribs-api-test' } );
-
- // assert body.contributions has latest contributions with
- // "user-contribs-api-test" tag (odd edits)
- assert.equal( latestSegment.contributions[ 0 ].id, arnoldsEdits[ 5 ].newrevid );
- assert.equal( latestSegment.contributions[ 1 ].id, arnoldsEdits[ 3 ].newrevid );
-
- // get the final segment
- const { body: finalSegment } = await req.get( latestSegment.older );
-
- // assert body.contributions has oldest contributions with
- // "user-contribs-api-test" tag (odd edits)
- assert.equal( finalSegment.contributions[ 0 ].id, arnoldsEdits[ 1 ].newrevid );
-
- const { body: latestSegment2 } = await req.get( finalSegment.newer );
- assert.deepEqual( latestSegment, latestSegment2 );
-
- const { body: latestSegment3 } = await req.get( latestSegment.latest );
- assert.deepEqual( latestSegment, latestSegment3 );
-
- // assert that the "latest" links also preserve the "tag" parameter
- assert.deepEqual( finalSegment.latest, latestSegment.latest );
- };
-
- const testSuppressedRevisions = async ( client, endpoint ) => {
- await samAction.action( 'revisiondelete',
- {
- type: 'revision',
- token: await samAction.token(),
- target: editToDelete.title,
- hide: 'content|comment|user',
- ids: editToDelete.newrevid
- },
- 'POST'
- );
-
- // Users w/o appropriate permissions can't see suppressed contributions (even their own)
- const { body: clientGetBody } = await client.get( endpoint );
- assert.lengthOf( clientGetBody.contributions, 1 );
-
- await samAction.action( 'revisiondelete',
- {
- type: 'revision',
- token: await samAction.token(),
- target: editToDelete.title,
- show: 'content|comment|user',
- ids: editToDelete.newrevid
- },
- 'POST'
- );
-
- const { body: clientGetBody2 } = await client.get( endpoint );
- assert.lengthOf( clientGetBody2.contributions, 2 );
- };
-
- describe( 'GET /me/contributions', () => {
- const endpoint = '/me/contributions';
-
- it( 'Returns status 401 for anon', async () => {
- 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 400 if segment size is out of bounds', async () => {
- const { status: minLimitStatus } = await arnold.get( endpoint, { limit: 0 } );
- assert.equal( minLimitStatus, 400 );
-
- const { status: maxLimitStatus } = await arnold.get( endpoint, { limit: 30 } );
- assert.equal( maxLimitStatus, 400 );
- } );
-
- it( 'Returns a list of the user\'s own edits', async () => {
- await testGetEdits( arnold, endpoint );
- } );
-
- it( 'Returns edits filtered by tag', async () => {
- await testGetEditsByTag( arnold, endpoint );
- } );
-
- it( 'Can fetch a chain of segments following the "older" field in the response', async () => {
- await testPagingForward( arnold, endpoint );
- } );
-
- it( 'Can fetch a chain of segments following the "newer" field in the response', async () => {
- await testPagingBackwards( arnold, endpoint );
- } );
-
- it( 'Returns a valid link to the latest segment', async () => {
- await testHasLatest( 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 testSuppressedRevisions( beth, endpoint );
- } );
-
- it( 'Segment link preserves tag filtering', async () => {
- await testPreserveTagFilter( arnold, endpoint );
- } );
- } );
-
- describe( 'GET /user/{name}/contributions', () => {
- let endpoint;
-
- before( () => {
- endpoint = `/user/${ arnold.username }/contributions`;
- } );
-
- it( 'Returns 400 if segment size is out of bounds', async () => {
- const { status: minLimitStatus } = await arnold.get( endpoint, { limit: 0 } );
- assert.equal( minLimitStatus, 400 );
-
- const { status: maxLimitStatus } = await arnold.get( endpoint, { limit: 30 } );
- assert.equal( maxLimitStatus, 400 );
- } );
-
- it( 'Returns 400 if user name is invalid', async () => {
- const xyzzy = '|||'; // an invalid user name
- const xendpoint = `/user/${ xyzzy }/contributions`;
- const response = await anon.get( xendpoint );
- assert.equal( response.status, 400 );
- } );
-
- it( 'Returns 400 if user name is empty', async () => {
- const xendpoint = '/user//contributions';
- const response = await anon.get( xendpoint );
- assert.equal( response.status, 400 );
- } );
-
- it( 'Returns 404 if user is unknown', async () => {
- const xyzzy = utils.uniq(); // a non-existing user name
- const xendpoint = `/user/${ xyzzy }/contributions`;
- const response = await anon.get( xendpoint );
- assert.equal( response.status, 404 );
- } );
-
- it( 'Returns 200 if user is an IP address', async () => {
- const xyzzy = '127.111.222.111';
- const xendpoint = `/user/${ xyzzy }/contributions`;
- const response = await anon.get( xendpoint );
- assert.equal( response.status, 200 );
- assert.match( response.headers[ 'content-type' ], /^application\/json/ );
-
- assert.property( response.body, 'contributions' );
- assert.deepEqual( response.body.contributions, [] );
- } );
-
- it( 'Anon gets a list of arnold\'s edits', async () => {
- await testGetEdits( anon, endpoint );
- } );
-
- it( 'Returns Arnold\'s edits filtered by tag', async () => {
- await testGetEditsByTag( anon, endpoint );
- } );
-
- it( 'Arnold gets a list of arnold\'s edits', async () => {
- await testGetEdits( arnold, endpoint );
- } );
-
- it( 'Mindy gets a list of arnold\'s edits', async () => {
- await testGetEdits( mindy, endpoint );
- } );
-
- it( 'Can fetch a chain of segments following the "older" field in the response', async () => {
- await testPagingForward( anon, endpoint );
- } );
-
- it( 'Can fetch a chain of segments following the "newer" field in the response', async () => {
- await testPagingBackwards( anon, endpoint );
- } );
-
- it( 'Returns a valid link to the latest segment', async () => {
- await testHasLatest( anon, 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`;
- await testSuppressedRevisions( anon, bethsEndpoint );
- } );
-
- it( 'Segment link preserves tag filtering', async () => {
- await testPreserveTagFilter( anon, endpoint );
- } );
- } );
-
-} );
diff --git a/tests/phpunit/unit/includes/Rest/Handler/ContributionsCountHandlerTest.php b/tests/phpunit/unit/includes/Rest/Handler/ContributionsCountHandlerTest.php
deleted file mode 100644
index 8a9ad2650d4d..000000000000
--- a/tests/phpunit/unit/includes/Rest/Handler/ContributionsCountHandlerTest.php
+++ /dev/null
@@ -1,146 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Rest\Handler;
-
-use MediaWiki\Rest\Handler\ContributionsCountHandler;
-use MediaWiki\Rest\LocalizedHttpException;
-use MediaWiki\Rest\RequestData;
-use MediaWiki\Rest\RequestInterface;
-use MediaWiki\Revision\ContributionsLookup;
-use MediaWiki\Tests\Unit\DummyServicesTrait;
-use MediaWiki\User\UserIdentityValue;
-use MediaWikiUnitTestCase;
-use PHPUnit\Framework\MockObject\MockObject;
-use Wikimedia\Message\MessageValue;
-
-/**
- * @covers \MediaWiki\Rest\Handler\ContributionsCountHandler
- */
-class ContributionsCountHandlerTest extends MediaWikiUnitTestCase {
- use DummyServicesTrait;
- use HandlerTestTrait;
-
- private function newHandler( $numContributions = 5 ) {
- /** @var MockObject|ContributionsLookup $mockContributionsLookup */
- $mockContributionsLookup = $this->createNoOpMock( ContributionsLookup::class,
- [ 'getContributionCount' ]
- );
- $mockContributionsLookup->method( 'getContributionCount' )->willReturn( $numContributions );
-
- return new ContributionsCountHandler(
- $mockContributionsLookup,
- $this->getDummyUserNameUtils()
- );
- }
-
- public static function provideTestThatParametersAreHandledCorrectly() {
- yield [ new RequestData( [] ), 'me' ];
- yield [ new RequestData(
- [ 'queryParams' => [ 'tag' => 'test' ] ]
- ), 'me' ];
- yield [ new RequestData(
- [ 'queryParams' => [ 'tag' => null ] ]
- ), 'me' ];
- yield [ new RequestData(
- [ 'pathParams' => [ 'user' => 'someUser' ], 'queryParams' => [ 'tag' => '' ] ]
- ), 'user' ];
- yield [ new RequestData(
- [ 'pathParams' => [ 'user' => 'someUser' ] ]
- ), 'user' ];
- }
-
- /**
- * @dataProvider provideTestThatParametersAreHandledCorrectly
- */
- public function testThatParametersAreHandledCorrectly( RequestInterface $request, $mode ) {
- $mockContributionsLookup = $this->createNoOpMock( ContributionsLookup::class,
- [ 'getContributionCount' ]
- );
- $username = $request->getPathParams()['user'] ?? null;
- $user = $username ? new UserIdentityValue( 42, $username ) : null;
-
- $tag = $request->getQueryParams()['tag'] ?? null;
- $mockContributionsLookup->method( 'getContributionCount' )
- ->with( $user, $this->anything(), $tag )
- ->willReturn( 123 );
-
- $handler = $this->newHandler( 5 );
- $validatedParams = [
- 'user' => $user,
- 'tag' => $tag ?? null,
- ];
- $response = $this->executeHandler( $handler, $request, [ 'mode' => $mode ], [], $validatedParams, [],
- $mode === 'me' ? $this->mockRegisteredUltimateAuthority() : null );
-
- $this->assertSame( 200, $response->getStatusCode() );
- }
-
- public function testThatAnonymousUserReturns401() {
- $handler = $this->newHandler();
- $request = new RequestData( [] );
- $validatedParams = [ 'user' => null, 'tag' => null ];
-
- $this->expectExceptionObject(
- new LocalizedHttpException( new MessageValue( 'rest-permission-denied-anon' ), 401 )
- );
- $this->executeHandler( $handler, $request, [ 'mode' => 'me' ], [], $validatedParams, [],
- $this->mockAnonUltimateAuthority() );
- }
-
- public static function provideThatResponseConformsToSchema() {
- yield [ 0, [ 'count' => 0 ], [], 'me' ];
- yield [ 3, [ 'count' => 3 ], [], 'me' ];
- yield [ 0, [ 'count' => 0 ], [ 'pathParams' => [ 'user' => 'someName' ] ], 'user' ];
- yield [ 3, [ 'count' => 3 ], [ 'pathParams' => [ 'user' => 'someName' ] ], 'user' ];
- }
-
- /**
- * @dataProvider provideThatResponseConformsToSchema
- */
- public function testThatResponseConformsToSchema( $numContributions, $expectedResponse, $config, $mode ) {
- $handler = $this->newHandler( $numContributions );
- $request = new RequestData( $config );
- $username = $request->getPathParams()['user'] ?? null;
- $validatedParams = [
- 'user' => $username ? new UserIdentityValue( 42, $username ) : null,
- 'tag' => null
- ];
-
- $response = $this->executeHandlerAndGetBodyData(
- $handler, $request, [ 'mode' => $mode ], [], $validatedParams, [],
- $this->mockRegisteredUltimateAuthority()
- );
-
- $this->assertSame( $expectedResponse, $response );
- }
-
- public function testThatUnknownUserReturns404() {
- $username = 'UNKNOWN';
- $handler = $this->newHandler();
- $request = new RequestData( [ 'pathParams' => [ 'user' => $username ] ] );
-
- $validatedParams = [
- 'user' => new UserIdentityValue( 0, $username ),
- 'tag' => null
- ];
-
- $this->expectExceptionObject(
- new LocalizedHttpException( new MessageValue( 'rest-nonexistent-user' ), 404 )
- );
- $this->executeHandler( $handler, $request, [ 'mode' => 'user' ], [], $validatedParams );
- }
-
- public function testThatIpUserReturns200() {
- $handler = $this->newHandler();
- $ipAddr = '127.0.0.1';
- $request = new RequestData( [ 'pathParams' => [ 'user' => $ipAddr ] ] );
- $validatedParams = [
- 'user' => new UserIdentityValue( 0, $ipAddr ),
- 'tag' => null
- ];
-
- $data = $this->executeHandlerAndGetBodyData( $handler, $request, [ 'mode' => 'user' ], [], $validatedParams );
- $this->assertArrayHasKey( 'count', $data );
- }
-
-}
diff --git a/tests/phpunit/unit/includes/Rest/Handler/UserContributionsHandlerTest.php b/tests/phpunit/unit/includes/Rest/Handler/UserContributionsHandlerTest.php
deleted file mode 100644
index 46f215f6a1ac..000000000000
--- a/tests/phpunit/unit/includes/Rest/Handler/UserContributionsHandlerTest.php
+++ /dev/null
@@ -1,383 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Rest\Handler;
-
-use MediaWiki\CommentStore\CommentStoreComment;
-use MediaWiki\Message\Message;
-use MediaWiki\Permissions\Authority;
-use MediaWiki\Rest\Handler\UserContributionsHandler;
-use MediaWiki\Rest\LocalizedHttpException;
-use MediaWiki\Rest\RequestData;
-use MediaWiki\Rest\RequestInterface;
-use MediaWiki\Revision\ContributionsLookup;
-use MediaWiki\Revision\ContributionsSegment;
-use MediaWiki\Revision\MutableRevisionRecord;
-use MediaWiki\Tests\Unit\DummyServicesTrait;
-use MediaWiki\User\UserIdentity;
-use MediaWiki\User\UserIdentityValue;
-use MediaWikiUnitTestCase;
-use MockTitleTrait;
-use PHPUnit\Framework\MockObject\MockObject;
-use Wikimedia\Message\MessageValue;
-
-/**
- * @covers \MediaWiki\Rest\Handler\UserContributionsHandler
- */
-class UserContributionsHandlerTest extends MediaWikiUnitTestCase {
- use DummyServicesTrait;
- use HandlerTestTrait;
- use MockTitleTrait;
-
- private const DEFAULT_LIMIT = 20;
-
- private function makeFakeRevisions( int $numRevs, int $limit, int $segment = 1 ) {
- $revisions = [];
- $title = $this->makeMockTitle( 'Main_Page', [ 'id' => 1 ] );
- for ( $i = $numRevs; $i >= 1; $i-- ) {
- $rev = new MutableRevisionRecord( $title );
- $ogTimestamp = '2020010100000';
- $rev->setId( $i );
- $rev->setSize( 256 );
- $rev->setComment( CommentStoreComment::newUnsavedComment( 'Edit ' . $i ) );
- $rev->setTimestamp( $ogTimestamp . $i );
- $revisions[] = $rev;
- }
-
- return array_slice( $revisions, $segment - 1, $limit );
- }
-
- /**
- * @param int $numRevisions
- * @param array $tags
- * @param array $deltas
- * @param array $flags
- *
- * @return ContributionsLookup|MockObject
- */
- private function newContributionsLookup( $numRevisions = 5, $tags = [], $deltas = [], $flags = [] ) {
- /** @var MockObject|ContributionsLookup $mockContributionsLookup */
- $mockContributionsLookup = $this->createNoOpMock( ContributionsLookup::class,
- [ 'getContributions' ]
- );
- $fakeRevisions = $this->makeFakeRevisions( $numRevisions, 2 );
- foreach ( $tags as $revId => $tagArray ) {
- $tags[ $revId ] = [];
- foreach ( $tagArray as $name ) {
- $mockMessage = $this->createNoOpMock( Message::class, [ 'parse', 'getKey' ] );
- $mockMessage->method( 'parse' )->willReturn( "<i>$name</i>" );
- $mockMessage->method( 'getKey' )->willReturn( "tag-$name" );
- $tags[ $revId ][ $name ] = $mockMessage;
- }
- }
- $fakeSegment = $this->makeSegment( $fakeRevisions, $tags, $deltas, $flags );
- $mockContributionsLookup->method( 'getContributions' )->willReturn( $fakeSegment );
-
- return $mockContributionsLookup;
- }
-
- /**
- * Returns a mock ContributionLookup that asserts getContributions()
- * is called with the same params that were originally passed into the request.
- * @param RequestInterface $request
- * @param UserIdentity $target
- * @param Authority $performer
- * @return ContributionsLookup|MockObject
- */
- private function newContributionsLookupForRequest(
- RequestInterface $request,
- UserIdentity $target,
- Authority $performer
- ) {
- $limit = $request->getQueryParams()['limit'] ?? self::DEFAULT_LIMIT;
- $segment = $request->getQueryParams()['segment'] ?? '';
- $tag = $request->getQueryParams()['tag'] ?? null;
-
- $fakeRevisions = $this->makeFakeRevisions( 5, $limit );
- $fakeSegment = $this->makeSegment( $fakeRevisions );
-
- $mockContributionsLookup = $this->createNoOpMock( ContributionsLookup::class,
- [ 'getContributions' ]
- );
- $mockContributionsLookup->method( 'getContributions' )
- ->willReturnCallback(
- function (
- $actualTarget,
- $actualLimit,
- $actualPerformer,
- $actualSegment,
- $actualTag
- ) use ( $target, $limit, $performer, $segment, $tag, $fakeSegment ) {
- $this->assertSame( $target->getName(), $actualTarget->getName() );
- $this->assertSame( $limit, $actualLimit );
- $this->assertTrue(
- $performer->getUser()->equals( $actualPerformer->getUser() )
- );
- $this->assertSame( $segment, $actualSegment );
- $this->assertSame( $tag, $actualTag );
- return $fakeSegment;
- }
- );
-
- return $mockContributionsLookup;
- }
-
- /**
- * @param ContributionsLookup|null $contributionsLookup
- *
- * @return UserContributionsHandler
- */
- private function newHandler( ContributionsLookup $contributionsLookup = null ) {
- if ( !$contributionsLookup ) {
- $contributionsLookup = $this->newContributionsLookup();
- }
-
- return new UserContributionsHandler(
- $contributionsLookup,
- $this->getDummyUserNameUtils()
- );
- }
-
- private function makeSegment( $revisions, array $tags = [], $deltas = [], array $flags = [] ) {
- if ( $revisions !== [] ) {
- $latestRevision = $revisions[ count( $revisions ) - 1 ];
- $earliestRevision = $revisions[0];
- $before = 'before|' . $latestRevision->getTimestamp();
- $after = 'after|' . $earliestRevision->getTimestamp();
- return new ContributionsSegment( $revisions, $tags, $before, $after, $deltas, $flags );
- }
- return new ContributionsSegment( $revisions, $tags, null, null, $deltas, $flags );
- }
-
- public static function provideValidQueryParameters() {
- yield [ [] ];
- yield [ [ 'limit' => self::DEFAULT_LIMIT ] ];
- yield [ [ 'tag' => 'test', 'limit' => 7 ] ];
- yield [ [ 'segment' => 'before|20200101000005' ] ];
- yield [ [ 'segment' => 'after|20200101000001' ] ];
- }
-
- /**
- * @param array $queryParams
- * @dataProvider provideValidQueryParameters
- */
- public function testThatParametersAreHandledCorrectlyForMeEndpoint( $queryParams ) {
- $request = new RequestData( [ 'queryParams' => $queryParams ] );
- $performer = $this->mockRegisteredUltimateAuthority();
- $performingUser = $performer->getUser();
- $validatedParams = [
- 'user' => null,
- 'limit' => $queryParams['limit'] ?? self::DEFAULT_LIMIT,
- 'tag' => $queryParams['tag'] ?? null,
- 'segment' => $queryParams['segment'] ?? '',
- ];
- $mockContributionsLookup = $this->newContributionsLookupForRequest( $request, $performingUser, $performer );
- $handler = $this->newHandler( $mockContributionsLookup );
-
- $response = $this->executeHandler( $handler, $request, [ 'mode' => 'me' ],
- [], $validatedParams, [], $performer );
- $this->assertSame( 200, $response->getStatusCode() );
- }
-
- /**
- * @param array $queryParams
- * @dataProvider provideValidQueryParameters
- */
- public function testThatParametersAreHandledCorrectlyForUserEndpoint( $queryParams ) {
- $username = 'Test';
- $target = new UserIdentityValue( 7, $username );
- $performer = $this->mockRegisteredUltimateAuthority();
- $request = new RequestData( [
- 'pathParams' => [ 'user' => $target->getName() ],
- 'queryParams' => $queryParams ]
- );
- $validatedParams =
- [
- 'user' => $target,
- 'limit' => $queryParams['limit'] ?? self::DEFAULT_LIMIT,
- 'tag' => $queryParams['tag'] ?? null,
- 'segment' => $queryParams['segment'] ?? '',
- ];
- $mockContributionsLookup = $this->newContributionsLookupForRequest( $request, $target, $performer );
- $handler = $this->newHandler( $mockContributionsLookup );
-
- $response = $this->executeHandler( $handler, $request, [ 'mode' => 'user' ], [], $validatedParams, [],
- $performer );
-
- $this->assertSame( 200, $response->getStatusCode() );
- }
-
- public function testThatAnonymousUserReturns401() {
- $handler = $this->newHandler();
- $request = new RequestData( [] );
- // UserDef transforms parameter name to ip
- $validatedParams = [
- 'ip' => new UserIdentityValue( 0, '127.0.0.1' ),
- 'limit' => self::DEFAULT_LIMIT,
- 'tag' => null,
- 'segment' => ''
- ];
- $this->expectExceptionObject(
- new LocalizedHttpException( new MessageValue( 'rest-permission-denied-anon' ), 401 )
- );
- $this->executeHandler( $handler, $request, [ 'mode' => 'me' ], [], $validatedParams );
- }
-
- public function testThatUnknownUserReturns404() {
- $handler = $this->newHandler();
- $username = 'UNKNOWN';
- $request = new RequestData( [ 'pathParams' => [ 'user' => $username ] ] );
- $validatedParams = [
- 'user' => new UserIdentityValue( 0, $username ),
- 'limit' => self::DEFAULT_LIMIT,
- 'tag' => null,
- 'segment' => ''
- ];
-
- $this->expectExceptionObject(
- new LocalizedHttpException( new MessageValue( 'rest-nonexistent-user' ), 404 )
- );
- $this->executeHandler( $handler, $request, [ 'mode' => 'user' ], [], $validatedParams );
- }
-
- public function testThatIpUserReturns200() {
- $handler = $this->newHandler();
- $username = '127.0.0.1';
- $requestData = [ 'pathParams' => [ 'user' => $username ] ];
- $request = new RequestData( $requestData );
- $validatedParams = [
- 'user' => new UserIdentityValue( 0, $username ),
- 'limit' => self::DEFAULT_LIMIT,
- 'tag' => null,
- 'segment' => ''
- ];
-
- $data = $this->executeHandlerAndGetBodyData( $handler, $request, [ 'mode' => 'user' ], [], $validatedParams );
- $this->assertArrayHasKey( 'contributions', $data );
- }
-
- public static function provideThatResponseConformsToSchema() {
- $basePath = 'https://wiki.example.com/rest/me/contributions';
- yield [ 0,
- [],
- [],
- [ 'newest' => true, 'oldest' => true ],
- [],
- [
- 'older' => null,
- 'newer' => $basePath . '?limit=20',
- 'latest' => $basePath . '?limit=20',
- 'contributions' => []
- ]
- ];
- yield [ 0,
- [],
- [],
- [ 'newest' => true, 'oldest' => true ],
- [ 'tag' => 'test' ],
- [
- 'older' => null,
- 'newer' => $basePath . '?limit=20&tag=test',
- 'latest' => $basePath . '?limit=20&tag=test',
- 'contributions' => []
- ]
- ];
- yield [ 1,
- [ 1 => [ 'frob' ] ],
- [ 1 => 256 ],
- [ 'newest' => true, 'oldest' => true ],
- [ 'limit' => 7 ],
- [
- 'older' => null,
- 'newer' => $basePath . '?limit=7&segment=after%7C20200101000001',
- 'latest' => $basePath . '?limit=7',
- 'contributions' => [
- [
- 'id' => 1,
- 'comment' => 'Edit 1',
- 'timestamp' => '2020-01-01T00:00:01Z',
- 'delta' => 256,
- 'size' => 256,
- 'tags' => [ [ 'name' => 'frob', 'description' => '<i>frob</i>' ] ],
- 'type' => 'revision',
- 'page' => [
- 'id' => 1,
- 'key' => 'Main_Page',
- 'title' => 'Main Page'
- ]
- ]
- ]
- ]
- ];
- yield [ 5,
- [ 5 => [ 'frob', 'nitz' ] ],
- [ 1 => 256, 2 => 256, 3 => 256, 4 => null, 5 => 256 ],
- [ 'newest' => true ],
- [ 'tag' => 'test' ],
- [
- 'older' => $basePath . '?limit=20&tag=test&segment=before%7C20200101000004',
- 'newer' => $basePath . '?limit=20&tag=test&segment=after%7C20200101000005',
- 'latest' => $basePath . '?limit=20&tag=test',
- 'contributions' => [
- [
- 'id' => 5,
- 'comment' => 'Edit 5',
- 'timestamp' => '2020-01-01T00:00:05Z',
- 'delta' => 256,
- 'size' => 256,
- 'tags' => [
- [ 'name' => 'frob', 'description' => '<i>frob</i>' ],
- [ 'name' => 'nitz', 'description' => '<i>nitz</i>' ]
- ],
- 'type' => 'revision',
- 'page' => [
- 'id' => 1,
- 'key' => 'Main_Page',
- 'title' => 'Main Page'
- ]
- ],
- [
- 'id' => 4,
- 'comment' => 'Edit 4',
- 'timestamp' => '2020-01-01T00:00:04Z',
- 'delta' => null,
- 'size' => 256,
- 'tags' => [],
- 'type' => 'revision',
- 'page' => [
- 'id' => 1,
- 'key' => 'Main_Page',
- 'title' => 'Main Page'
- ]
- ]
- ]
- ]
- ];
- }
-
- /**
- * @dataProvider provideThatResponseConformsToSchema
- */
- public function testThatResponseConformsToSchema(
- $numRevisions,
- $tags,
- $deltas,
- $flags,
- $query,
- $expectedResponse
- ) {
- $lookup = $this->newContributionsLookup( $numRevisions, $tags, $deltas, $flags );
- $handler = $this->newHandler( $lookup );
- $request = new RequestData( [ 'queryParams' => $query ] );
-
- $validatedParams = [
- 'user' => null,
- 'limit' => $query['limit'] ?? self::DEFAULT_LIMIT,
- 'tag' => $query['tag'] ?? null,
- 'segment' => $query['segment'] ?? '',
- ];
- $config = [ 'path' => '/me/contributions', 'mode' => 'me' ];
- $response = $this->executeHandlerAndGetBodyData( $handler, $request, $config, [], $validatedParams, [],
- $this->mockRegisteredUltimateAuthority() );
- $this->assertSame( $expectedResponse, $response );
- }
-}