3 use MediaWiki\Context\RequestContext
;
4 use MediaWiki\Language\ILanguageConverter
;
5 use MediaWiki\Languages\LanguageConverterFactory
;
6 use MediaWiki\MainConfigNames
;
7 use MediaWiki\MediaWikiServices
;
8 use MediaWiki\Request\FauxRequest
;
9 use MediaWiki\Search\TitleMatcher
;
10 use MediaWiki\Specials\SpecialSearch
;
11 use MediaWiki\Title\Title
;
12 use MediaWiki\User\User
;
15 * Test class for SpecialSearch class
16 * Copyright © 2012, Antoine Musso
18 * @author Antoine Musso
21 class SpecialSearchTest
extends MediaWikiIntegrationTestCase
{
23 private function newSpecialPage() {
24 $services = $this->getServiceContainer();
25 return new SpecialSearch(
26 $services->getSearchEngineConfig(),
27 $services->getSearchEngineFactory(),
28 $services->getNamespaceInfo(),
29 $services->getContentHandlerFactory(),
30 $services->getInterwikiLookup(),
31 $services->getReadOnlyMode(),
32 $services->getUserOptionsManager(),
33 $services->getLanguageConverterFactory(),
34 $services->getRepoGroup(),
35 $services->getSearchResultThumbnailProvider(),
36 $services->getTitleMatcher()
41 * @covers \MediaWiki\Specials\SpecialSearch::load
43 public function testAlternativeBackend() {
44 $this->overrideConfigValue( MainConfigNames
::SearchTypeAlternatives
, [ 'MockSearchEngine' ] );
46 $ctx = new RequestContext();
47 $ctx->setRequest( new FauxRequest( [
49 'srbackend' => 'MockSearchEngine',
51 $search = $this->newSpecialPage();
52 $search->setContext( $ctx );
56 # Without the parameter srbackend it would be a SearchEngineDummy
57 $this->assertInstanceOf( MockSearchEngine
::class, $search->getSearchEngine() );
61 * @covers \MediaWiki\Specials\SpecialSearch::load
62 * @covers \MediaWiki\Specials\SpecialSearch::showResults
64 public function testValidateSortOrder() {
65 $ctx = new RequestContext();
66 $ctx->setRequest( new FauxRequest( [
71 $sp = Title
::makeTitle( NS_SPECIAL
, 'Search' );
72 $this->getServiceContainer()
73 ->getSpecialPageFactory()
74 ->executePath( $sp, $ctx );
75 $html = $ctx->getOutput()->getHTML();
76 $this->assertStringContainsString( 'cdx-message--warning', $html, 'must contain warnings' );
77 $this->assertMatchesRegularExpression( '/Sort order of invalid is unrecognized/',
78 $html, 'must tell user sort order is invalid' );
82 * @covers \MediaWiki\Specials\SpecialSearch::load
83 * @dataProvider provideSearchOptionsTests
84 * @param array $requested Request parameters. For example:
85 * [ 'ns5' => true, 'ns6' => true ]. Null to use default options.
86 * @param array $userOptions User options to test with. For example:
87 * [ 'searchNs5' => 1 ];. Null to use default options.
88 * @param string $expectedProfile An expected search profile name
89 * @param array $expectedNS Expected namespaces
90 * @param string $message
92 public function testProfileAndNamespaceLoading( $requested, $userOptions,
93 $expectedProfile, $expectedNS, $message = 'Profile name and namespaces mismatches!'
95 $context = new RequestContext
;
97 $this->newUserWithSearchNS( $userOptions )
100 $context->setRequest( new MediaWiki\Request\FauxRequest( [
105 $context->setRequest( new FauxRequest( $requested ) );
106 $search = $this->newSpecialPage();
107 $search->setContext( $context );
111 * Verify profile name and namespace in the same assertion to make
112 * sure we will be able to fully compare the above code. PHPUnit stop
113 * after an assertion fail.
117 'ProfileName' => $expectedProfile,
118 'Namespaces' => $expectedNS,
121 'ProfileName' => $search->getProfile(),
122 'Namespaces' => $search->getNamespaces(),
128 public static function provideSearchOptionsTests() {
129 $defaultNS = MediaWikiServices
::getInstance()->getSearchEngineConfig()->defaultNamespaces();
131 $NO_USER_PREF = null;
136 * <Web Request>, <User options>
137 * Followed by expected values:
138 * <ProfileName>, <NSList>
139 * Then an optional message.
142 $EMPTY_REQUEST, $NO_USER_PREF,
143 'default', $defaultNS,
144 'T35270: No request nor user preferences should give default profile'
147 [ 'ns5' => 1 ], $NO_USER_PREF,
149 'Web request with specific NS should override user preference'
155 ] +
array_fill_keys( array_map( static function ( $ns ) {
156 return "searchNs$ns";
157 }, $defaultNS ), 0 ),
158 'advanced', [ 2, 14 ],
159 'T35583: search with no option should honor User search preferences'
160 . ' and have all other namespace disabled'
166 * Helper to create a new User object with given options
167 * User remains anonymous though
168 * @param array|null $opt
171 protected function newUserWithSearchNS( $opt = null ) {
172 $u = User
::newFromId( 0 );
173 if ( $opt === null ) {
176 $userOptionsManager = $this->getServiceContainer()->getUserOptionsManager();
177 foreach ( $opt as $name => $value ) {
178 $userOptionsManager->setOption( $u, $name, $value );
185 * Verify we do not expand search term in <title> on search result page
186 * https://gerrit.wikimedia.org/r/4841
187 * @covers \MediaWiki\Specials\SpecialSearch::setupPage
189 public function testSearchTermIsNotExpanded() {
191 $this->markTestSkippedIfDbType( 'sqlite' );
192 $this->overrideConfigValue( MainConfigNames
::SearchType
, null );
194 # Initialize [[Special::Search]]
195 $ctx = new RequestContext();
196 $term = '{{SITENAME}}';
197 $ctx->setRequest( new FauxRequest( [ 'search' => $term, 'fulltext' => 1 ] ) );
198 $ctx->setTitle( Title
::makeTitle( NS_SPECIAL
, 'Search' ) );
199 $search = $this->newSpecialPage();
200 $search->setContext( $ctx );
202 # Simulate a user searching for a given term
203 $search->execute( '' );
205 # Lookup the HTML page title set for that page
212 $this->assertMatchesRegularExpression(
213 '/' . preg_quote( $term, '/' ) . '/',
215 "Search term '{$term}' should not be expanded in Special:Search <title>"
219 public static function provideRewriteQueryWithSuggestion() {
222 'With suggestion and no rewritten query shows did you mean',
223 '/Did you mean: <a[^>]+>first suggestion/',
226 [ Title
::newMainPage() ]
230 'With rewritten query informs user of change',
231 '/Showing results for <a[^>]+>first suggestion/',
234 [ Title
::newMainPage() ]
238 'When both queries have no results user gets no results',
239 '/There were no results matching the query/',
246 'Prev/next links are using the rewritten query',
247 '/search=rewritten\+query" rel="next" title="Next 20 results"/',
250 array_fill( 0, 100, Title
::newMainPage() )
254 'Show x results per page link uses the rewritten query',
255 '/search=rewritten\+query" title="Show \d+ results/',
258 array_fill( 0, 100, Title
::newMainPage() )
264 * @dataProvider provideRewriteQueryWithSuggestion
265 * @covers \MediaWiki\Specials\SpecialSearch::showResults
267 public function testRewriteQueryWithSuggestion(
274 $results = array_map( static function ( $title ) {
275 return SearchResult
::newFromTitle( $title );
278 $searchResults = new SpecialSearchTestMockResultSet(
284 $mockSearchEngine = $this->mockSearchEngine( $searchResults );
285 $services = $this->getServiceContainer();
286 $search = $this->getMockBuilder( SpecialSearch
::class )
287 ->setConstructorArgs( [
288 $services->getSearchEngineConfig(),
289 $services->getSearchEngineFactory(),
290 $services->getNamespaceInfo(),
291 $services->getContentHandlerFactory(),
292 $services->getInterwikiLookup(),
293 $services->getReadOnlyMode(),
294 $services->getUserOptionsManager(),
295 $services->getLanguageConverterFactory(),
296 $services->getRepoGroup(),
297 $services->getSearchResultThumbnailProvider(),
298 $services->getTitleMatcher()
300 ->onlyMethods( [ 'getSearchEngine' ] )
302 $search->method( 'getSearchEngine' )
303 ->willReturn( $mockSearchEngine );
305 $search->getContext()->setTitle( Title
::makeTitle( NS_SPECIAL
, 'Search' ) );
306 $search->getContext()->setLanguage( 'en' );
308 $search->showResults( 'this is a fake search' );
310 $html = $search->getContext()->getOutput()->getHTML();
311 foreach ( (array)$expectRegex as $regex ) {
312 $this->assertMatchesRegularExpression( $regex, $html, $message );
316 public static function provideLimitPreference() {
324 * @dataProvider provideLimitPreference
325 * @covers \MediaWiki\Specials\SpecialSearch::showResults
327 public function testLimitPreference(
331 $results = array_fill( 0, 100, SearchResult
::newFromTitle( Title
::newMainPage() ) );
333 $searchResults = new SpecialSearchTestMockResultSet(
339 $userOptionsManager = $this->getServiceContainer()->getUserOptionsManager();
341 $user = $this->getTestSysop()->getUser();
342 $userOptionsManager->setOption( $user, 'searchlimit', $optionValue );
343 $user->saveSettings();
345 $mockSearchEngine = $this->mockSearchEngine( $searchResults );
346 $services = $this->getServiceContainer();
347 $search = $this->getMockBuilder( SpecialSearch
::class )
348 ->setConstructorArgs( [
349 $services->getSearchEngineConfig(),
350 $services->getSearchEngineFactory(),
351 $services->getNamespaceInfo(),
352 $services->getContentHandlerFactory(),
353 $services->getInterwikiLookup(),
354 $services->getReadOnlyMode(),
356 $services->getLanguageConverterFactory(),
357 $services->getRepoGroup(),
358 $services->getSearchResultThumbnailProvider(),
359 $services->getTitleMatcher()
361 ->onlyMethods( [ 'getSearchEngine' ] )
363 $search->method( 'getSearchEngine' )
364 ->willReturn( $mockSearchEngine );
366 $search->getContext()->setTitle( Title
::makeTitle( NS_SPECIAL
, 'Search' ) );
367 $search->getContext()->setUser( $user );
368 $search->getContext()->setLanguage( 'en' );
370 $search->showResults( 'this is a fake search' );
372 $html = $search->getContext()->getOutput()->getHTML();
373 if ( $expectedLimit === null ) {
374 $this->assertDoesNotMatchRegularExpression( "/ title=\"Next \\d+ results\"/", $html );
376 $this->assertMatchesRegularExpression( "/ title=\"Next $expectedLimit results\"/", $html );
380 protected function mockSearchEngine( SpecialSearchTestMockResultSet
$results ) {
381 $mock = $this->getMockBuilder( SearchEngine
::class )
382 ->onlyMethods( [ 'searchText' ] )
385 $mock->method( 'searchText' )
386 ->willReturn( $results );
388 $mock->setHookContainer( $this->getServiceContainer()->getHookContainer() );
394 * @covers \MediaWiki\Specials\SpecialSearch::execute
396 public function testSubPageRedirect() {
397 $this->overrideConfigValue( MainConfigNames
::Script
, '/w/index.php' );
399 $ctx = new RequestContext
;
400 $sp = Title
::makeTitle( NS_SPECIAL
, 'Search/foo_bar' );
401 $this->getServiceContainer()->getSpecialPageFactory()->executePath( $sp, $ctx );
402 $url = $ctx->getOutput()->getRedirect();
404 $parts = parse_url( $url );
405 $this->assertEquals( '/w/index.php', $parts['path'] );
406 parse_str( $parts['query'], $query );
407 $this->assertEquals( 'Special:Search', $query['title'] );
408 $this->assertEquals( 'foo bar', $query['search'] );
412 * If the 'search-match-redirect' user pref is false, then SpecialSearch::goResult() should
415 * @covers \MediaWiki\Specials\SpecialSearch::goResult
417 public function testGoResult_userPrefRedirectOn() {
418 $context = new RequestContext
;
420 $this->newUserWithSearchNS( [ 'search-match-redirect' => false ] )
422 $context->setRequest(
423 new FauxRequest( [ 'search' => 'TEST_SEARCH_PARAM', 'fulltext' => 1 ] )
425 $search = $this->newSpecialPage();
426 $search->setContext( $context );
429 $this->assertNull( $search->goResult( 'TEST_SEARCH_PARAM' ) );
433 * If the 'search-match-redirect' user pref is true, then SpecialSearch::goResult() should
434 * NOT return null if there is a near match found for the search term
436 * @covers \MediaWiki\Specials\SpecialSearch::goResult
438 public function testGoResult_userPrefRedirectOff() {
439 // mock the search engine so it returns a near match for an arbitrary search term
440 $searchResults = new SpecialSearchTestMockResultSet(
441 'TEST_SEARCH_SUGGESTION',
443 [ SearchResult
::newFromTitle( Title
::newMainPage() ) ]
446 $nearMatcherMock = $this->getMockBuilder( TitleMatcher
::class )
447 ->disableOriginalConstructor()
448 ->onlyMethods( [ 'getNearMatch' ] )
451 $nearMatcherMock->method( 'getNearMatch' )
452 ->willReturn( $searchResults->getFirstResult() );
454 $mockSearchEngine = $this->mockSearchEngine( $searchResults );
455 $services = $this->getServiceContainer();
456 $search = $this->getMockBuilder( SpecialSearch
::class )
457 ->setConstructorArgs( [
458 $services->getSearchEngineConfig(),
459 $services->getSearchEngineFactory(),
460 $services->getNamespaceInfo(),
461 $services->getContentHandlerFactory(),
462 $services->getInterwikiLookup(),
463 $services->getReadOnlyMode(),
464 $services->getUserOptionsManager(),
465 $services->getLanguageConverterFactory(),
466 $services->getRepoGroup(),
467 $services->getSearchResultThumbnailProvider(),
470 ->onlyMethods( [ 'getSearchEngine' ] )
472 $search->method( 'getSearchEngine' )
473 ->willReturn( $mockSearchEngine );
475 // set up a mock user with 'search-match-redirect' set to true
476 $context = new RequestContext
;
478 $this->newUserWithSearchNS( [ 'search-match-redirect' => true ] )
480 $context->setRequest(
481 new FauxRequest( [ 'search' => 'TEST_SEARCH_PARAM', 'fulltext' => 1 ] )
483 $search->setContext( $context );
486 $this->assertNotNull( $search->goResult( 'TEST_SEARCH_PARAM' ) );
490 * @covers \MediaWiki\Specials\SpecialSearch::showResults
492 public function test_create_link_not_shown_if_variant_link_is_known() {
493 $searchTerm = "Test create link not shown if variant link is known";
494 $variantLink = "the replaced link variant text should not be visible";
496 $variantTitle = $this->createNoOpMock( Title
::class, [ 'isKnown', 'getPrefixedText',
497 'getDBkey', 'isExternal' ] );
499 $variantTitle->method( "isKnown" )->willReturn( true );
500 $variantTitle->method( "isExternal" )->willReturn( false );
501 $variantTitle->method( "getDBkey" )->willReturn( $searchTerm . " (variant)" );
502 $variantTitle->method( "getPrefixedText" )->willReturn( $searchTerm . " (variant)" );
504 $specialSearchFactory = function () use ( $variantTitle, $variantLink, $searchTerm ) {
505 $languageConverter = $this->createMock( ILanguageConverter
::class );
506 $languageConverter->method( 'hasVariants' )->willReturn( true );
507 $languageConverter->expects( $this->once() )
508 ->method( 'findVariantLink' )
509 ->willReturnCallback(
510 static function ( &$link, &$nt, $unused = false ) use ( $searchTerm, $variantTitle, $variantLink ) {
511 if ( $link === $searchTerm ) {
512 $link = $variantLink;
517 $languageConverterFactory = $this->createMock( LanguageConverterFactory
::class );
518 $languageConverterFactory->method( 'getLanguageConverter' )
519 ->willReturn( $languageConverter );
521 $mockSearchEngineFactory = $this->createMock( SearchEngineFactory
::class );
522 $mockSearchEngineFactory->method( "create" )
523 ->willReturn( $this->mockSearchEngine( new SpecialSearchTestMockResultSet() ) );
525 $services = $this->getServiceContainer();
526 $specialSearch = new SpecialSearch(
527 $services->getSearchEngineConfig(),
528 $mockSearchEngineFactory,
529 $services->getNamespaceInfo(),
530 $services->getContentHandlerFactory(),
531 $services->getInterwikiLookup(),
532 $services->getReadOnlyMode(),
533 $services->getUserOptionsManager(),
534 $languageConverterFactory,
535 $services->getRepoGroup(),
536 $services->getSearchResultThumbnailProvider(),
537 $services->getTitleMatcher()
539 $context = new RequestContext();
540 $context->setRequest( new FauxRequest() );
541 $context->setTitle( Title
::makeTitle( NS_SPECIAL
, 'Search' ) );
542 $specialSearch->setContext( $context );
543 $specialSearch->load();
544 return $specialSearch;
546 $specialSearch = $specialSearchFactory();
547 $specialSearch->showResults( $searchTerm );
548 $html = $specialSearch->getContext()->getOutput()->getHTML();
549 $this->assertStringNotContainsString( $variantLink, $html );
550 $this->assertStringContainsString( 'class="mw-search-exists"', $html );
551 $this->assertStringNotContainsString( 'class="mw-search-createlink"', $html );
553 $specialSearch = $specialSearchFactory();
554 $specialSearch->showResults( $searchTerm . "_search_create_link" );
555 $html = $specialSearch->getContext()->getOutput()->getHTML();
556 $this->assertStringContainsString( 'class="mw-search-createlink"', $html );
557 $this->assertStringNotContainsString( 'class="mw-search-exists"', $html );