diff options
-rw-r--r-- | autoload.php | 3 | ||||
-rw-r--r-- | includes/Rest/Handler/AbstractContributionHandler.php | 79 | ||||
-rw-r--r-- | includes/Rest/Handler/ContributionsCountHandler.php | 48 | ||||
-rw-r--r-- | includes/Rest/Handler/UserContributionsHandler.php | 135 | ||||
-rw-r--r-- | includes/Rest/coreDevelopmentRoutes.json | 36 | ||||
-rw-r--r-- | tests/api-testing/REST/ContributionsCount.js | 170 | ||||
-rw-r--r-- | tests/api-testing/REST/UserContributions.js | 427 | ||||
-rw-r--r-- | tests/phpunit/unit/includes/Rest/Handler/ContributionsCountHandlerTest.php | 146 | ||||
-rw-r--r-- | tests/phpunit/unit/includes/Rest/Handler/UserContributionsHandlerTest.php | 383 |
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 ); - } -} |