This patch uses the recommended MW practice for injecting settings into MW services. Instead of passing in the full config object, pass in service options which MW service wiring will use to lookup settings/options from config sources. So in this patch, the various options have been identified and put into `CONSTRUCTOR_OPTIONS` constant and inject via service wiring. Test plan ========= Ensure that the special page: `Special:Search` still works with this patch checkout on your local test wiki. When you visit `Special:Search` on your local wiki, you should see a search form view. Type in something like: "Main Page" and then hit search. If everything works well, then local test passes. NOTE ==== This patch also resolves a TODO which mentions the removal of the `getConfig()` method from the SearchEngineConfig class. Change-Id: Ib5dfc10f3f210c6c35247f4f30f9549dd60e0af7
731 lines
25 KiB
PHP
731 lines
25 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Tests\Rest\Handler;
|
|
|
|
use InvalidArgumentException;
|
|
use Language;
|
|
use MediaWiki\Config\HashConfig;
|
|
use MediaWiki\Config\ServiceOptions;
|
|
use MediaWiki\HookContainer\HookContainer;
|
|
use MediaWiki\Language\FormatterFactory;
|
|
use MediaWiki\Language\RawMessage;
|
|
use MediaWiki\Linker\LinkTarget;
|
|
use MediaWiki\MainConfigNames;
|
|
use MediaWiki\Page\PageIdentity;
|
|
use MediaWiki\Page\PageIdentityValue;
|
|
use MediaWiki\Page\PageStore;
|
|
use MediaWiki\Page\ProperPageIdentity;
|
|
use MediaWiki\Page\RedirectLookup;
|
|
use MediaWiki\Permissions\PermissionManager;
|
|
use MediaWiki\Rest\Handler\SearchHandler;
|
|
use MediaWiki\Rest\LocalizedHttpException;
|
|
use MediaWiki\Rest\RequestData;
|
|
use MediaWiki\Search\Entity\SearchResultThumbnail;
|
|
use MediaWiki\Search\SearchResultThumbnailProvider;
|
|
use MediaWiki\Status\Status;
|
|
use MediaWiki\Status\StatusFormatter;
|
|
use MediaWiki\Tests\Unit\DummyServicesTrait;
|
|
use MediaWiki\Title\TitleFormatter;
|
|
use MediaWiki\Title\TitleValue;
|
|
use MediaWiki\User\Options\UserOptionsLookup;
|
|
use MediaWikiUnitTestCase;
|
|
use MockSearchResultSet;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use SearchEngine;
|
|
use SearchEngineConfig;
|
|
use SearchEngineFactory;
|
|
use SearchResult;
|
|
use SearchResultSet;
|
|
use SearchSuggestion;
|
|
use SearchSuggestionSet;
|
|
use Wikimedia\Message\MessageValue;
|
|
|
|
/**
|
|
* @covers \MediaWiki\Rest\Handler\SearchHandler
|
|
*/
|
|
class SearchHandlerTest extends MediaWikiUnitTestCase {
|
|
|
|
use DummyServicesTrait;
|
|
use MediaTestTrait;
|
|
|
|
/**
|
|
* @var SearchEngine|MockObject|null
|
|
*/
|
|
private $searchEngine = null;
|
|
|
|
/**
|
|
* @param string $query
|
|
* @param SearchResultSet|Status $titleResult
|
|
* @param SearchResultSet|Status $textResult
|
|
* @param SearchSuggestionSet|null $completionResult
|
|
* @param PermissionManager|null $permissionManager
|
|
* @param RedirectLookup|null $redirectLookup
|
|
* @param PageStore|null $pageStore
|
|
* @param TitleFormatter|null $mockTitleFormatter
|
|
* @param HookContainer|null $hookContainer
|
|
*
|
|
* @return SearchHandler
|
|
*/
|
|
private function newHandler(
|
|
$query,
|
|
$titleResult,
|
|
$textResult,
|
|
$completionResult = null,
|
|
$permissionManager = null,
|
|
$redirectLookup = null,
|
|
$pageStore = null,
|
|
$mockTitleFormatter = null,
|
|
HookContainer $hookContainer = null
|
|
) {
|
|
$sources = [
|
|
MainConfigNames::SearchType => 'test',
|
|
MainConfigNames::SearchTypeAlternatives => [],
|
|
MainConfigNames::NamespacesToBeSearchedDefault => [ NS_MAIN => true ],
|
|
MainConfigNames::SearchSuggestCacheExpiry => 1200,
|
|
];
|
|
$config = new HashConfig( $sources );
|
|
|
|
/** @var Language|MockObject $language */
|
|
$language = $this->createNoOpMock( Language::class );
|
|
$hookContainer ??= $this->createHookContainer();
|
|
/** @var UserOptionsLookup|MockObject $userOptionsLookup */
|
|
$userOptionsLookup = $this->createMock( UserOptionsLookup::class );
|
|
$searchEngineConfig = new SearchEngineConfig(
|
|
new ServiceOptions(
|
|
SearchEngineConfig::CONSTRUCTOR_OPTIONS,
|
|
$sources
|
|
),
|
|
$language,
|
|
$hookContainer,
|
|
[],
|
|
$userOptionsLookup
|
|
);
|
|
|
|
if ( !$permissionManager ) {
|
|
$permissionManager = $this->createMock( PermissionManager::class );
|
|
$permissionManager->method( 'isEveryoneAllowed' )
|
|
->with( 'read' )
|
|
->willReturn( true );
|
|
}
|
|
if ( !$pageStore ) {
|
|
$pageStore = $this->createMock( PageStore::class );
|
|
}
|
|
|
|
// Our mock RedirectLookup defaults to not finding a redirect for our given page
|
|
if ( !$redirectLookup ) {
|
|
$redirectLookup = $this->createMock( RedirectLookup::class );
|
|
$redirectLookup->method( 'getRedirectTarget' )
|
|
->willReturn( null );
|
|
}
|
|
|
|
if ( !$mockTitleFormatter ) {
|
|
$mockTitleFormatter = $this->getDummyTitleFormatter();
|
|
}
|
|
|
|
/** @var SearchEngine|MockObject $searchEngine */
|
|
$this->searchEngine = $this->createMock( SearchEngine::class );
|
|
$this->searchEngine->method( 'searchTitle' )
|
|
->with( $query )
|
|
->willReturn( $titleResult );
|
|
$this->searchEngine->method( 'searchText' )
|
|
->with( $query )
|
|
->willReturn( $textResult );
|
|
|
|
if ( $completionResult ) {
|
|
$this->searchEngine->method( 'completionSearchWithVariants' )
|
|
->with( $query )
|
|
->willReturn( $completionResult );
|
|
}
|
|
|
|
/** @var SearchEngineFactory|MockObject $searchEngineFactory */
|
|
$searchEngineFactory = $this->createNoOpMock( SearchEngineFactory::class, [ 'create' ] );
|
|
$searchEngineFactory->method( 'create' )
|
|
->willReturn( $this->searchEngine );
|
|
|
|
$searchResultThumbnailProvider = new SearchResultThumbnailProvider(
|
|
$this->makeMockRepoGroup( [] ),
|
|
$hookContainer
|
|
);
|
|
|
|
$mockStatusFormatter = $this->createNoOpMock( StatusFormatter::class, [ 'getMessage' ] );
|
|
$mockStatusFormatter->method( 'getMessage' )->willReturn(
|
|
new RawMessage( 'testing' )
|
|
);
|
|
|
|
$mockFormatterFactory = $this->createNoOpMock( FormatterFactory::class, [ 'getStatusFormatter' ] );
|
|
$mockFormatterFactory->method( 'getStatusFormatter' )->willReturn( $mockStatusFormatter );
|
|
|
|
return new SearchHandler(
|
|
$config,
|
|
$searchEngineFactory,
|
|
$searchEngineConfig,
|
|
$searchResultThumbnailProvider,
|
|
$permissionManager,
|
|
$redirectLookup,
|
|
$pageStore,
|
|
$mockTitleFormatter,
|
|
$mockFormatterFactory,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param string $pageName
|
|
* @param string $textSnippet
|
|
* @param bool $broken
|
|
* @param bool $missing
|
|
*
|
|
* @return SearchResult
|
|
*/
|
|
private function makeMockSearchResult(
|
|
$pageName,
|
|
$textSnippet = 'Lorem Ipsum',
|
|
$broken = false,
|
|
$missing = false
|
|
) {
|
|
$title = $this->makeMockTitle( $pageName );
|
|
|
|
/** @var SearchResult|MockObject $result */
|
|
$result = $this->createNoOpMock( SearchResult::class, [
|
|
'getTitle', 'isBrokenTitle', 'isMissingRevision', 'getTextSnippet'
|
|
] );
|
|
$result->method( 'getTitle' )->willReturn( $title );
|
|
$result->method( 'getTextSnippet' )->willReturn( $textSnippet );
|
|
$result->method( 'isBrokenTitle' )->willReturn( $broken );
|
|
$result->method( 'isMissingRevision' )->willReturn( $missing );
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* @param string $pageName
|
|
* @param MockObject|null $title
|
|
*
|
|
* @return SearchSuggestion
|
|
*/
|
|
private function makeMockSearchSuggestion( $pageName, $title = null ) {
|
|
if ( !$title ) {
|
|
$title = $this->makeMockTitle( $pageName );
|
|
}
|
|
|
|
/** @var SearchSuggestion|MockObject $suggestion */
|
|
$suggestion = $this->createNoOpMock(
|
|
SearchSuggestion::class,
|
|
[ 'getSuggestedTitle', 'getSuggestedTitleID', 'getText' ]
|
|
);
|
|
$suggestion->method( 'getSuggestedTitle' )->willReturn( $title );
|
|
$suggestion->method( 'getSuggestedTitleID' )->willReturn( $title->getArticleID() );
|
|
$suggestion->method( 'getText' )->willReturn( $title->getPrefixedText() );
|
|
|
|
return $suggestion;
|
|
}
|
|
|
|
public function testExecuteFulltextSearch() {
|
|
$titleResults = new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Foo', 'one' ),
|
|
$this->makeMockSearchResult( 'Forbidden Foo', 'two' ), // forbidden
|
|
$this->makeMockSearchResult( 'FooBar', 'three' ),
|
|
$this->makeMockSearchResult( 'Foo Moo', 'four', true, false ), // missing
|
|
] );
|
|
$textResults = new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Quux', 'one' ),
|
|
$this->makeMockSearchResult( 'Forbidden Quux', 'two' ), // forbidden
|
|
$this->makeMockSearchResult( 'Xyzzy', 'three' ),
|
|
$this->makeMockSearchResult( 'Yookoo', 'four', false, true ), // broken
|
|
] );
|
|
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
$handler = $this->newHandler( $query, $titleResults, $textResults );
|
|
$config = [ 'mode' => SearchHandler::FULLTEXT_MODE ];
|
|
$data = $this->executeHandlerAndGetBodyData( $handler, $request, $config, [], [], [],
|
|
$this->mockAnonAuthority( static function ( string $permission, ?PageIdentity $target ) {
|
|
return $target && !preg_match( '/Forbidden/', $target->getDBkey() );
|
|
} ) );
|
|
|
|
$this->assertArrayHasKey( 'pages', $data );
|
|
$this->assertCount( 4, $data['pages'] );
|
|
$this->assertSame( 'Foo', $data['pages'][0]['title'] );
|
|
$this->assertSame( 'one', $data['pages'][0]['excerpt'] );
|
|
$this->assertSame( 'FooBar', $data['pages'][1]['title'] );
|
|
$this->assertSame( 'three', $data['pages'][1]['excerpt'] );
|
|
$this->assertSame( 'Quux', $data['pages'][2]['title'] );
|
|
$this->assertSame( 'one', $data['pages'][2]['excerpt'] );
|
|
$this->assertSame( 'Xyzzy', $data['pages'][3]['title'] );
|
|
$this->assertSame( 'three', $data['pages'][3]['excerpt'] );
|
|
}
|
|
|
|
public function testExecuteCompletionSearch() {
|
|
$titleResults = new MockSearchResultSet( [] );
|
|
$textResults = new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Quux', 'one' ),
|
|
] );
|
|
$completionResults = new SearchSuggestionSet( [
|
|
$this->makeMockSearchSuggestion( 'Frob' ),
|
|
$this->makeMockSearchSuggestion( 'Frobnitz' ),
|
|
] );
|
|
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
|
|
$handler = $this->newHandler(
|
|
$query, $titleResults, $textResults, $completionResults );
|
|
$config = [ 'mode' => SearchHandler::COMPLETION_MODE ];
|
|
$response = $this->executeHandler( $handler, $request, $config );
|
|
|
|
$this->assertSame( 200, $response->getStatusCode() );
|
|
$this->assertSame( 'application/json', $response->getHeaderLine( 'Content-Type' ) );
|
|
$this->assertSame( 'public, max-age=1200', $response->getHeaderLine( 'Cache-Control' ) );
|
|
|
|
$data = json_decode( $response->getBody(), true );
|
|
$this->assertIsArray( $data, 'Body must be a JSON array' );
|
|
|
|
$this->assertArrayHasKey( 'pages', $data );
|
|
$this->assertCount( 2, $data['pages'] );
|
|
$this->assertSame( 'Frob', $data['pages'][0]['title'] );
|
|
$this->assertSame( 'Frob', $data['pages'][0]['excerpt'] );
|
|
$this->assertSame( 'Frobnitz', $data['pages'][1]['title'] );
|
|
$this->assertSame( 'Frobnitz', $data['pages'][1]['excerpt'] );
|
|
}
|
|
|
|
public function testCompletionSearchNotCachedForPublicPages() {
|
|
$titleResults = new MockSearchResultSet( [] );
|
|
$textResults = new MockSearchResultSet( [] );
|
|
$completionResults = new SearchSuggestionSet( [] );
|
|
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
|
|
$permissionManager = $this->createMock( PermissionManager::class );
|
|
$permissionManager->method( 'isEveryoneAllowed' )
|
|
->with( 'read' )
|
|
->willReturn( false );
|
|
|
|
$handler = $this->newHandler( $query, $titleResults, $textResults, $completionResults, $permissionManager );
|
|
$config = [ 'mode' => SearchHandler::COMPLETION_MODE ];
|
|
|
|
$response = $this->executeHandler( $handler, $request, $config );
|
|
$this->assertSame( 'no-store, max-age=0', $response->getHeaderLine( 'Cache-Control' ) );
|
|
$this->assertSame( 200, $response->getStatusCode() );
|
|
}
|
|
|
|
public function testExecute_limit() {
|
|
$titleResults = new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Foo' ),
|
|
$this->makeMockSearchResult( 'FooBar' ),
|
|
] );
|
|
$textResults = new MockSearchResultSet( [] );
|
|
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query, 'limit' => 7 ] ] );
|
|
|
|
$handler = $this->newHandler( $query, $titleResults, $textResults );
|
|
|
|
// Limits are enforced by the SearchEngine, which we mock.
|
|
// So we have to do assertions on the mock, not on the result data.
|
|
$this->searchEngine
|
|
->expects( $this->atLeastOnce() )
|
|
->method( 'setLimitOffset' )
|
|
->with( 7, 0 );
|
|
|
|
$this->executeHandler( $handler, $request );
|
|
}
|
|
|
|
public function testExecute_limit_default() {
|
|
$titleResults = new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Foo' ),
|
|
$this->makeMockSearchResult( 'FooBar' ),
|
|
] );
|
|
$textResults = new MockSearchResultSet( [] );
|
|
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
|
|
$handler = $this->newHandler( $query, $titleResults, $textResults );
|
|
|
|
// Limits are enforced by the SearchEngine, which we mock.
|
|
// So we have to do assertions on the mock, not on the result data.
|
|
$this->searchEngine
|
|
->expects( $this->atLeastOnce() )
|
|
->method( 'setLimitOffset' )
|
|
->with( 50, 0 );
|
|
|
|
$this->executeHandler( $handler, $request );
|
|
}
|
|
|
|
public static function provideExecute_limit_error() {
|
|
yield [ 0, 'paramvalidator-outofrange-minmax' ];
|
|
yield [ 123, 'paramvalidator-outofrange-minmax' ];
|
|
yield [ 'xyz', 'paramvalidator-badinteger' ];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideExecute_limit_error
|
|
* @param int $requestedLimit
|
|
* @param string $error
|
|
*/
|
|
public function testExecute_limit_error( $requestedLimit, $error ) {
|
|
$titleResults = new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Foo' ),
|
|
$this->makeMockSearchResult( 'FooBar' ),
|
|
] );
|
|
$textResults = new MockSearchResultSet( [] );
|
|
|
|
$query = 'foo';
|
|
$request =
|
|
new RequestData( [ 'queryParams' => [ 'q' => $query, 'limit' => $requestedLimit ] ] );
|
|
|
|
$handler = $this->newHandler( $query, $titleResults, $textResults );
|
|
|
|
$this->expectExceptionObject(
|
|
new LocalizedHttpException( new MessageValue( $error ), 400 )
|
|
);
|
|
|
|
$this->executeHandler( $handler, $request );
|
|
}
|
|
|
|
public function testExecute_status() {
|
|
$titleResults = Status::newGood( new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Foo', 'one' ),
|
|
$this->makeMockSearchResult( 'FooBar', 'three' ),
|
|
] ) );
|
|
$textResults = Status::newGood( new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Quux', 'one' ),
|
|
$this->makeMockSearchResult( 'Xyzzy', 'three' ),
|
|
] ) );
|
|
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
|
|
$handler = $this->newHandler( $query, $titleResults, $textResults );
|
|
$data = $this->executeHandlerAndGetBodyData( $handler, $request );
|
|
|
|
$this->assertArrayHasKey( 'pages', $data );
|
|
$this->assertCount( 4, $data['pages'] );
|
|
}
|
|
|
|
public function testExecute_missingparam() {
|
|
$titleResults = Status::newFatal( 'testing' );
|
|
$textResults = new MockSearchResultSet( [] );
|
|
|
|
$query = '';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
|
|
$this->expectExceptionObject(
|
|
new LocalizedHttpException(
|
|
new MessageValue( "paramvalidator-missingparam", [ 'q' ] ),
|
|
400
|
|
)
|
|
);
|
|
|
|
$handler = $this->newHandler( $query, $titleResults, $textResults );
|
|
$this->executeHandler( $handler, $request );
|
|
}
|
|
|
|
public function testExecute_error() {
|
|
$titleResults = Status::newFatal( 'testing' );
|
|
$textResults = new MockSearchResultSet( [] );
|
|
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
|
|
$this->expectExceptionObject(
|
|
new LocalizedHttpException( new MessageValue( "rest-search-error", [ 'testing' ] ) )
|
|
);
|
|
|
|
$handler = $this->newHandler( $query, $titleResults, $textResults );
|
|
$this->executeHandler( $handler, $request );
|
|
}
|
|
|
|
public function testNeedsWriteWriteAccess() {
|
|
$titleResults = new MockSearchResultSet( [] );
|
|
$textResults = new MockSearchResultSet( [] );
|
|
|
|
$handler = $this->newHandler( '', $titleResults, $textResults );
|
|
$this->assertTrue( $handler->needsReadAccess() );
|
|
$this->assertFalse( $handler->needsWriteAccess() );
|
|
}
|
|
|
|
public function testExecute_augmentedFields() {
|
|
$titleResults = Status::newGood( new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Foo', 'one' ),
|
|
$this->makeMockSearchResult( 'FooBar', 'three' ),
|
|
] ) );
|
|
$textResults = Status::newGood( new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Quux', 'one' ),
|
|
$this->makeMockSearchResult( 'Xyzzy', 'three' ),
|
|
] ) );
|
|
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
|
|
$handler = $this->newHandler( $query, $titleResults, $textResults );
|
|
$data = $this->executeHandlerAndGetBodyData( $handler, $request );
|
|
|
|
$this->assertArrayHasKey( 'pages', $data );
|
|
$this->assertCount( 4, $data['pages'] );
|
|
$this->assertArrayHasKey( 'thumbnail', $data['pages'][0] );
|
|
$this->assertNull( $data['pages'][0][ 'thumbnail' ] );
|
|
|
|
$this->assertArrayHasKey( 'description', $data['pages'][0] );
|
|
$this->assertNull( $data['pages'][0][ 'description' ] );
|
|
|
|
$this->assertArrayHasKey( 'matched_title', $data['pages'][0] );
|
|
$this->assertNull( $data['pages'][0][ 'matched_title' ] );
|
|
}
|
|
|
|
public function testExecute_augmentedFieldsDescriptionAndThumbnailProvided() {
|
|
$titleResults = Status::newGood( new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Foo', 'one' ),
|
|
] ) );
|
|
$textResults = Status::newGood( new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Quux', 'one' ),
|
|
] ) );
|
|
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
|
|
$hookContainer = $this->createHookContainer( [
|
|
'SearchResultProvideDescription' =>
|
|
static function ( array $pageIdentities, array &$result ) {
|
|
foreach ( $pageIdentities as $pageId => $pageIdentity ) {
|
|
$result[ $pageId ] = 'Description_' . $pageIdentity->getId();
|
|
}
|
|
},
|
|
'SearchResultProvideThumbnail' =>
|
|
static function ( array $pageIdentities, array &$result ) {
|
|
foreach ( $pageIdentities as $pageId => $pageIdentity ) {
|
|
$result[ $pageId ] = new SearchResultThumbnail(
|
|
'image/png',
|
|
null,
|
|
100,
|
|
125,
|
|
500,
|
|
'http:/example.org/url_' . $pageIdentity->getId(),
|
|
null
|
|
);
|
|
}
|
|
}
|
|
] );
|
|
|
|
$handler = $this->newHandler(
|
|
$query, $titleResults, $textResults, null, null, null, null, null, $hookContainer
|
|
);
|
|
|
|
$data = $this->executeHandlerAndGetBodyData( $handler, $request, [], $hookContainer );
|
|
|
|
$this->assertArrayHasKey( 'pages', $data );
|
|
$this->assertCount( 2, $data['pages'] );
|
|
$this->assertArrayHasKey( 'thumbnail', $data['pages'][0] );
|
|
|
|
$this->assertSame( 'http:/example.org/url_1', $data['pages'][0][ 'thumbnail' ]['url'] );
|
|
$this->assertSame( 125, $data['pages'][0][ 'thumbnail' ]['height'] );
|
|
$this->assertSame( 100, $data['pages'][0][ 'thumbnail' ]['width'] );
|
|
$this->assertSame( 'image/png', $data['pages'][0][ 'thumbnail' ]['mimetype'] );
|
|
$this->assertSame( 500, $data['pages'][0][ 'thumbnail' ]['duration'] );
|
|
$this->assertArrayHasKey( 'description', $data['pages'][0] );
|
|
$this->assertSame( 'Description_1', $data['pages'][0][ 'description' ] );
|
|
}
|
|
|
|
/**
|
|
* Tests the case where a search term matches a page with a redirect.
|
|
*/
|
|
public function testExecute_ResolvesRedirect() {
|
|
$textResults = new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Foo Redirect Source' ),
|
|
$this->makeMockSearchResult( 'FooBarBaz' ),
|
|
] );
|
|
|
|
$pageTarget = new PageIdentityValue( 1, NS_MAIN, 'Foo_Redirect_Target', PageIdentityValue::LOCAL );
|
|
|
|
$mockRedirectLinkTarget = $this->createMock( LinkTarget::class );
|
|
$mockPageStore = $this->createMock( PageStore::class );
|
|
$mockPageStore->method( 'getPageForLink' )->willReturn( $pageTarget );
|
|
$mockRedirectLookup = $this->createMock( RedirectLookup::class );
|
|
|
|
// first call has a redirect, second call does not
|
|
$mockRedirectLookup
|
|
->method( 'getRedirectTarget' )
|
|
->willReturnOnConsecutiveCalls( $mockRedirectLinkTarget, null );
|
|
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
$config = [];
|
|
$handler = $this->newHandler(
|
|
$query, null, $textResults, null, null,
|
|
$mockRedirectLookup, $mockPageStore
|
|
);
|
|
|
|
$data = $this->executeHandlerAndGetBodyData( $handler, $request, $config, [] );
|
|
|
|
$this->assertCount( 2, $data['pages'] );
|
|
$this->assertArrayHasKey( 'matched_title', $data['pages'][0] );
|
|
$this->assertArrayHasKey( 'matched_title', $data['pages'][1] );
|
|
|
|
$this->assertSame( 'Foo Redirect Source', $data['pages'][0]['matched_title'] );
|
|
$this->assertSame( 'Foo Redirect Target', $data['pages'][0]['title'] );
|
|
$this->assertSame( null, $data['pages'][1]['matched_title'] );
|
|
}
|
|
|
|
/**
|
|
* Tests the case where a search term matches both the redirect source and the redirect target page.
|
|
* We expect to remove the redirect source, and keep the redirect target.
|
|
*/
|
|
public function testExecute_RemovesDuplicateRedirectAndSource() {
|
|
$pageTarget = $this->createMock( ProperPageIdentity::class );
|
|
$pageTarget->method( 'getID' )->willReturn( 10 );
|
|
$pageTarget->method( 'getDBKey' )->willReturn( 'Foo_Redirect_Target' );
|
|
|
|
$mockRedirectLinkTarget = $this->createMock( LinkTarget::class );
|
|
$mockPageStore = $this->createMock( PageStore::class );
|
|
$mockPageStore->method( 'getPageForLink' )->willReturn( $pageTarget );
|
|
$mockRedirectLookup = $this->createMock( RedirectLookup::class );
|
|
|
|
// first call has a redirect, second call does not
|
|
$mockRedirectLookup
|
|
->method( 'getRedirectTarget' )
|
|
->willReturnOnConsecutiveCalls( $mockRedirectLinkTarget, null );
|
|
|
|
$mockTitle = $this->makeMockTitle( 'Foo Redirect Target', [ 'id' => 10 ] );
|
|
$completionConfig = [ 'mode' => SearchHandler::COMPLETION_MODE ];
|
|
$completionResults = new SearchSuggestionSet( [
|
|
$this->makeMockSearchSuggestion( 'Foo Redirect Source', $mockTitle ),
|
|
$this->makeMockSearchSuggestion( 'Foo Redirect Target', $mockTitle ),
|
|
] );
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
|
|
$handler = $this->newHandler(
|
|
$query, null, null, $completionResults, null,
|
|
$mockRedirectLookup, $mockPageStore
|
|
);
|
|
|
|
$data = $this->executeHandlerAndGetBodyData( $handler, $request, $completionConfig, [] );
|
|
|
|
$this->assertCount( 1, $data['pages'] );
|
|
$this->assertArrayHasKey( 'matched_title', $data['pages'][0] );
|
|
|
|
$this->assertSame( 'Foo Redirect Target', $data['pages'][0]['title'] );
|
|
}
|
|
|
|
public function testExecute_NonProperPage() {
|
|
$page = $this->makeMockTitle( 'Blankpage', [ 'namespace' => NS_SPECIAL ] );
|
|
$result = $this->createNoOpMock(
|
|
SearchResult::class,
|
|
[ 'getTitle', 'isBrokenTitle', 'isMissingRevision', 'getTextSnippet' ]
|
|
);
|
|
$result->method( 'getTitle' )->willReturn( $page );
|
|
$textResults = new MockSearchResultSet( [ $result ] );
|
|
|
|
$query = 'foo';
|
|
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
$handler = $this->newHandler(
|
|
$query,
|
|
null,
|
|
$textResults
|
|
);
|
|
|
|
$data = $this->executeHandlerAndGetBodyData( $handler, $request );
|
|
$this->assertArrayHasKey( 'pages', $data );
|
|
$this->assertCount( 1, $data['pages'] );
|
|
}
|
|
|
|
/**
|
|
* When a page in a real namespace redirects to a Special Page, it should come back in results
|
|
*/
|
|
public function testExecute_NonProperPageAsRedirect() {
|
|
$textResults = new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Special Page Redirect Source' ),
|
|
] );
|
|
|
|
$mockRedirectLinkTarget = new TitleValue( NS_SPECIAL, 'MyPage' );
|
|
$mockRedirectLookup = $this->createMock( RedirectLookup::class );
|
|
$mockRedirectLookup->method( 'getRedirectTarget' )
|
|
->willReturn( $mockRedirectLinkTarget );
|
|
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
|
|
$config = [];
|
|
$handler = $this->newHandler(
|
|
$query, null, $textResults, null, null,
|
|
$mockRedirectLookup
|
|
);
|
|
|
|
$data = $this->executeHandlerAndGetBodyData( $handler, $request, $config, [] );
|
|
$this->assertCount( 1, $data['pages'] );
|
|
$this->assertEquals( 'Special Page Redirect Source', $data['pages'][0]['title'] );
|
|
$this->assertNull( $data['pages'][0]['matched_title'] );
|
|
}
|
|
|
|
/**
|
|
* When a page in a real namespace redirects to an interwiki, it should come back in results
|
|
*/
|
|
public function testExecute_InterwikiLinkAsRedirect() {
|
|
$textResults = new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Interwiki Redirect Source' ),
|
|
] );
|
|
|
|
$mockRedirectLinkTarget = $this->createMock( LinkTarget::class );
|
|
$mockRedirectLinkTarget->method( 'getNamespace' )->willReturn( 0 );
|
|
$mockRedirectLinkTarget->method( 'isExternal' )->willReturn( true );
|
|
$mockRedirectLinkTarget->method( 'getDBkey' )->willReturn( 'Interwiki_Redirect_Target' );
|
|
|
|
$mockRedirectLookup = $this->createMock( RedirectLookup::class );
|
|
$mockRedirectLookup->method( 'getRedirectTarget' )
|
|
->willReturn( $mockRedirectLinkTarget );
|
|
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
|
|
$config = [];
|
|
$handler = $this->newHandler(
|
|
$query, null, $textResults, null, null,
|
|
$mockRedirectLookup
|
|
);
|
|
|
|
$data = $this->executeHandlerAndGetBodyData( $handler, $request, $config, [] );
|
|
|
|
$this->assertCount( 1, $data['pages'] );
|
|
$this->assertNull( $data['pages'][0]['matched_title'] );
|
|
$this->assertEquals( 'Interwiki Redirect Source', $data['pages'][0]['title'] );
|
|
}
|
|
|
|
public function testExecute_NullResults() {
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
|
|
$handler = $this->newHandler( $query, null, null );
|
|
$data = $this->executeHandlerAndGetBodyData( $handler, $request );
|
|
|
|
$this->assertArrayHasKey( 'pages', $data );
|
|
$this->assertCount( 0, $data['pages'] );
|
|
}
|
|
|
|
public function testInitWrongConfig() {
|
|
$titleResults = Status::newGood( new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Foo', 'one' ),
|
|
$this->makeMockSearchResult( 'FooBar', 'three' ),
|
|
] ) );
|
|
$textResults = Status::newGood( new MockSearchResultSet( [
|
|
$this->makeMockSearchResult( 'Quux', 'one' ),
|
|
$this->makeMockSearchResult( 'Xyzzy', 'three' ),
|
|
] ) );
|
|
|
|
$query = 'foo';
|
|
$request = new RequestData( [ 'queryParams' => [ 'q' => $query ] ] );
|
|
|
|
$this->expectException( InvalidArgumentException::class );
|
|
|
|
$handler = $this->newHandler( $query, $titleResults, $textResults );
|
|
$data = $this->executeHandlerAndGetBodyData(
|
|
$handler,
|
|
$request,
|
|
[
|
|
'mode' => 'SomethingWrong'
|
|
] );
|
|
|
|
$this->assertArrayHasKey( 'pages', $data );
|
|
$this->assertCount( 0, $data['pages'] );
|
|
}
|
|
}
|