Merge "Drop cache interwiki"
[mediawiki.git] / tests / phpunit / includes / specials / SpecialSearchTest.php
blob7052a8214e789a86ecc9cfb4e321452b451404ab
1 <?php
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;
14 /**
15 * Test class for SpecialSearch class
16 * Copyright © 2012, Antoine Musso
18 * @author Antoine Musso
19 * @group Database
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()
40 /**
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( [
48 'search' => 'foo',
49 'srbackend' => 'MockSearchEngine',
50 ] ) );
51 $search = $this->newSpecialPage();
52 $search->setContext( $ctx );
54 $search->load();
56 # Without the parameter srbackend it would be a SearchEngineDummy
57 $this->assertInstanceOf( MockSearchEngine::class, $search->getSearchEngine() );
60 /**
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( [
67 'search' => 'foo',
68 'fulltext' => 1,
69 'sort' => 'invalid',
70 ] ) );
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' );
81 /**
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!'
94 ) {
95 $context = new RequestContext;
96 $context->setUser(
97 $this->newUserWithSearchNS( $userOptions )
100 $context->setRequest( new MediaWiki\Request\FauxRequest( [
101 'ns5'=>true,
102 'ns6'=>true,
103 ] ));
105 $context->setRequest( new FauxRequest( $requested ) );
106 $search = $this->newSpecialPage();
107 $search->setContext( $context );
108 $search->load();
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.
115 $this->assertEquals(
116 [ /** Expected: */
117 'ProfileName' => $expectedProfile,
118 'Namespaces' => $expectedNS,
120 [ /** Actual: */
121 'ProfileName' => $search->getProfile(),
122 'Namespaces' => $search->getNamespaces(),
124 $message
128 public static function provideSearchOptionsTests() {
129 $defaultNS = MediaWikiServices::getInstance()->getSearchEngineConfig()->defaultNamespaces();
130 $EMPTY_REQUEST = [];
131 $NO_USER_PREF = null;
133 return [
135 * Parameters:
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,
148 'advanced', [ 5 ],
149 'Web request with specific NS should override user preference'
152 $EMPTY_REQUEST, [
153 'searchNs2' => 1,
154 'searchNs14' => 1,
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
169 * @return User
171 protected function newUserWithSearchNS( $opt = null ) {
172 $u = User::newFromId( 0 );
173 if ( $opt === null ) {
174 return $u;
176 $userOptionsManager = $this->getServiceContainer()->getUserOptionsManager();
177 foreach ( $opt as $name => $value ) {
178 $userOptionsManager->setOption( $u, $name, $value );
181 return $u;
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() {
190 // T303046
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
206 $pageTitle = $search
207 ->getContext()
208 ->getOutput()
209 ->getHTMLTitle();
211 # Compare :-]
212 $this->assertMatchesRegularExpression(
213 '/' . preg_quote( $term, '/' ) . '/',
214 $pageTitle,
215 "Search term '{$term}' should not be expanded in Special:Search <title>"
219 public static function provideRewriteQueryWithSuggestion() {
220 return [
222 'With suggestion and no rewritten query shows did you mean',
223 '/Did you mean: <a[^>]+>first suggestion/',
224 'first suggestion',
225 null,
226 [ Title::newMainPage() ]
230 'With rewritten query informs user of change',
231 '/Showing results for <a[^>]+>first suggestion/',
232 'asdf',
233 'first suggestion',
234 [ Title::newMainPage() ]
238 'When both queries have no results user gets no results',
239 '/There were no results matching the query/',
240 'first suggestion',
241 'first suggestion',
246 'Prev/next links are using the rewritten query',
247 '/search=rewritten\+query" rel="next" title="Next 20 results"/',
248 'original query',
249 'rewritten query',
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/',
256 'original query',
257 'rewritten query',
258 array_fill( 0, 100, Title::newMainPage() )
264 * @dataProvider provideRewriteQueryWithSuggestion
265 * @covers \MediaWiki\Specials\SpecialSearch::showResults
267 public function testRewriteQueryWithSuggestion(
268 $message,
269 $expectRegex,
270 $suggestion,
271 $rewrittenQuery,
272 array $resultTitles
274 $results = array_map( static function ( $title ) {
275 return SearchResult::newFromTitle( $title );
276 }, $resultTitles );
278 $searchResults = new SpecialSearchTestMockResultSet(
279 $suggestion,
280 $rewrittenQuery,
281 $results
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' ] )
301 ->getMock();
302 $search->method( 'getSearchEngine' )
303 ->willReturn( $mockSearchEngine );
305 $search->getContext()->setTitle( Title::makeTitle( NS_SPECIAL, 'Search' ) );
306 $search->getContext()->setLanguage( 'en' );
307 $search->load();
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() {
317 return [
318 [ 20, 20 ],
319 [ 101, null ],
324 * @dataProvider provideLimitPreference
325 * @covers \MediaWiki\Specials\SpecialSearch::showResults
327 public function testLimitPreference(
328 $optionValue,
329 $expectedLimit
331 $results = array_fill( 0, 100, SearchResult::newFromTitle( Title::newMainPage() ) );
333 $searchResults = new SpecialSearchTestMockResultSet(
334 '?',
335 '!',
336 $results
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(),
355 $userOptionsManager,
356 $services->getLanguageConverterFactory(),
357 $services->getRepoGroup(),
358 $services->getSearchResultThumbnailProvider(),
359 $services->getTitleMatcher()
361 ->onlyMethods( [ 'getSearchEngine' ] )
362 ->getMock();
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' );
369 $search->load();
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 );
375 } else {
376 $this->assertMatchesRegularExpression( "/ title=\"Next $expectedLimit results\"/", $html );
380 protected function mockSearchEngine( SpecialSearchTestMockResultSet $results ) {
381 $mock = $this->getMockBuilder( SearchEngine::class )
382 ->onlyMethods( [ 'searchText' ] )
383 ->getMock();
385 $mock->method( 'searchText' )
386 ->willReturn( $results );
388 $mock->setHookContainer( $this->getServiceContainer()->getHookContainer() );
390 return $mock;
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
413 * return null
415 * @covers \MediaWiki\Specials\SpecialSearch::goResult
417 public function testGoResult_userPrefRedirectOn() {
418 $context = new RequestContext;
419 $context->setUser(
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 );
427 $search->load();
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' ] )
449 ->getMock();
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(),
468 $nearMatcherMock
470 ->onlyMethods( [ 'getSearchEngine' ] )
471 ->getMock();
472 $search->method( 'getSearchEngine' )
473 ->willReturn( $mockSearchEngine );
475 // set up a mock user with 'search-match-redirect' set to true
476 $context = new RequestContext;
477 $context->setUser(
478 $this->newUserWithSearchNS( [ 'search-match-redirect' => true ] )
480 $context->setRequest(
481 new FauxRequest( [ 'search' => 'TEST_SEARCH_PARAM', 'fulltext' => 1 ] )
483 $search->setContext( $context );
484 $search->load();
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;
513 $nt = $variantTitle;
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 );