Allows us to reduce direct mocking in individual tests, or relying on MediaWikiServices, by having a reusable service instead that can be used in unit tests. Make use of it in a few places. This dummy service is also less coupled to individual tests; in the rest handler tests, replacing mocks that report that only a specific string is an ip address with the actual implementation that accepts other ip address, and in BlockUtilsTest use an actually invalid user name instead of making the mock UserNameUtils complain about a perfectly valid name. Change-Id: Iaa3195002ac4ae7e0c9c58ed57c80c02762d4070
382 lines
12 KiB
PHP
382 lines
12 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Tests\Rest\Handler;
|
|
|
|
use CommentStoreComment;
|
|
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\Storage\MutableRevisionRecord;
|
|
use MediaWiki\Tests\Unit\DummyServicesTrait;
|
|
use MediaWiki\User\UserIdentity;
|
|
use MediaWiki\User\UserIdentityValue;
|
|
use Message;
|
|
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 string[] $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 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 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 );
|
|
}
|
|
}
|