wiki.techinc.nl/tests/phpunit/includes/search/PrefixSearchTest.php
Peter Fischer 64e6a78af3 search: Exclude unlisted special pages from search completion
Pages that are "unlisted", i.e. SpecialPage::isListed returns false,
are now excluded from auto-completion. These are essentially pages
that are for scripted/non-human purposes (e.g. Special:RunJobs) and
redirect pages (e.g. Special:AllMyUploads).

Note that redirect pages can opt-in to being listed, and thus do
appear on Special:SpecialPages and thus remain included in search
suggestions, such as Special:Diff.

This patch creates a getListedPages utility instead of re-using
SpecialPageFactory::getRegularPages for two reasons:

1. Consistency with Special:SpecialPages.
2. Avoid hiding major and popular user-facing features that require
   logging in and/or some user right, such as Special:Watchlist,
   Special:Block, and Special:Upload.

This patch doesn't use SpecialPageFactory::getUsablePages because it
is unsafe outside index.php page views (invokes user session), and
make the results incompatible with HTTP caching of OpenSearch API.

Bug: T358938
Change-Id: I1cb5f4c3cedb02eb41fcac3b8f785616ce54d73c
2024-03-26 17:52:33 -07:00

358 lines
8.3 KiB
PHP

<?php
use MediaWiki\MainConfigNames;
use MediaWiki\Title\Title;
/**
* @group Search
* @group Database
* @covers \PrefixSearch
*/
class PrefixSearchTest extends MediaWikiLangTestCase {
private const NS_NONCAP = 12346;
public function addDBDataOnce() {
if ( !$this->isWikitextNS( NS_MAIN ) ) {
// tests are skipped if NS_MAIN is not wikitext
return;
}
$this->insertPage( 'Sandbox' );
$this->insertPage( 'Bar' );
$this->insertPage( 'Example' );
$this->insertPage( 'Example Bar' );
$this->insertPage( 'Example Foo' );
$this->insertPage( 'Example Foo/Bar' );
$this->insertPage( 'Example/Baz' );
$this->insertPage( 'Redirect test', '#REDIRECT [[Redirect Test]]' );
$this->insertPage( 'Redirect Test' );
$this->insertPage( 'Redirect Test Worse Result' );
$this->insertPage( 'Redirect test2', '#REDIRECT [[Redirect Test2]]' );
$this->insertPage( 'Redirect TEST2', '#REDIRECT [[Redirect Test2]]' );
$this->insertPage( 'Redirect Test2' );
$this->insertPage( 'Redirect Test2 Worse Result' );
$this->insertPage( 'Talk:Sandbox' );
$this->insertPage( 'Talk:Example' );
$this->insertPage( 'User:Example' );
$this->overrideConfigValues( [
MainConfigNames::ExtraNamespaces => [ self::NS_NONCAP => 'NonCap' ],
MainConfigNames::CapitalLinkOverrides => [ self::NS_NONCAP => false ],
] );
$this->insertPage( Title::makeTitle( self::NS_NONCAP, 'Bar' ) );
$this->insertPage( Title::makeTitle( self::NS_NONCAP, 'Upper' ) );
$this->insertPage( Title::makeTitle( self::NS_NONCAP, 'sandbox' ) );
}
protected function setUp(): void {
parent::setUp();
if ( !$this->isWikitextNS( NS_MAIN ) ) {
$this->markTestSkipped( 'Main namespace does not support wikitext.' );
}
// Avoid special pages from extensions interfering with the tests
$this->overrideConfigValues( [
MainConfigNames::SpecialPages => [],
MainConfigNames::Hooks => [],
MainConfigNames::ExtraNamespaces => [ self::NS_NONCAP => 'NonCap' ],
MainConfigNames::CapitalLinkOverrides => [ self::NS_NONCAP => false ],
] );
}
protected function searchProvision( array $results = null ) {
if ( $results === null ) {
$this->overrideConfigValue( MainConfigNames::Hooks, [] );
} else {
$this->setTemporaryHook(
'PrefixSearchBackend',
static function ( $namespaces, $search, $limit, &$srchres ) use ( $results ) {
$srchres = $results;
return false;
}
);
}
}
public static function provideSearch() {
return [
[ [
'Empty string',
'query' => '',
'results' => [],
] ],
[ [
'Main namespace with title prefix',
'query' => 'Ex',
'results' => [
'Example',
'Example/Baz',
'Example Bar',
],
// Third result when testing offset
'offsetresult' => [
'Example Foo',
],
] ],
[ [
'Talk namespace prefix',
'query' => 'Talk:',
'results' => [
'Talk:Example',
'Talk:Sandbox',
],
] ],
[ [
'User namespace prefix',
'query' => 'User:',
'results' => [
'User:Example',
],
] ],
[ [
'Special namespace prefix',
'query' => 'Special:',
'results' => [
'Special:ActiveUsers',
'Special:AllMessages',
'Special:AllPages',
],
// Third result when testing offset
'offsetresult' => [
'Special:AncientPages',
],
] ],
[ [
'Special namespace with prefix',
'query' => 'Special:Un',
'results' => [
'Special:Unblock',
'Special:UncategorizedCategories',
'Special:UncategorizedFiles',
],
// Third result when testing offset
'offsetresult' => [
'Special:UncategorizedPages',
],
] ],
[ [
'Special page name',
'query' => 'Special:EditWatchlist',
'results' => [],
] ],
[ [
'Special page subpages',
'query' => 'Special:EditWatchlist/',
'results' => [
'Special:EditWatchlist/clear',
'Special:EditWatchlist/raw',
],
] ],
[ [
'Special page subpages with prefix',
'query' => 'Special:EditWatchlist/cl',
'results' => [
'Special:EditWatchlist/clear',
],
] ],
[ [
'Namespace with case sensitive first letter',
'query' => 'NonCap:upper',
'results' => []
] ],
[ [
'Multinamespace search',
'query' => 'B',
'results' => [
'Bar',
'NonCap:Bar',
],
'namespaces' => [ NS_MAIN, self::NS_NONCAP ],
] ],
[ [
'Multinamespace search with lowercase first letter',
'query' => 'sand',
'results' => [
'Sandbox',
'NonCap:sandbox',
],
'namespaces' => [ NS_MAIN, self::NS_NONCAP ],
] ],
];
}
/**
* @dataProvider provideSearch
* @covers \PrefixSearch::search
* @covers \PrefixSearch::searchBackend
*/
public function testSearch( array $case ) {
$this->searchProvision( null );
$namespaces = $case['namespaces'] ?? [];
$searcher = new StringPrefixSearch;
$results = $searcher->search( $case['query'], 3, $namespaces );
$this->assertEquals(
$case['results'],
$results,
$case[0]
);
}
/**
* @dataProvider provideSearch
* @covers \PrefixSearch::search
* @covers \PrefixSearch::searchBackend
*/
public function testSearchWithOffset( array $case ) {
$this->searchProvision( null );
$namespaces = $case['namespaces'] ?? [];
$searcher = new StringPrefixSearch;
$results = $searcher->search( $case['query'], 3, $namespaces, 1 );
// We don't expect the first result when offsetting
array_shift( $case['results'] );
// And sometimes we expect a different last result
$expected = isset( $case['offsetresult'] ) ?
array_merge( $case['results'], $case['offsetresult'] ) :
$case['results'];
$this->assertEquals(
$expected,
$results,
$case[0]
);
}
public static function provideSearchBackend() {
return [
[ [
'Simple case',
'provision' => [
'Bar',
'Barcelona',
'Barbara',
],
'query' => 'Bar',
'results' => [
'Bar',
'Barcelona',
'Barbara',
],
] ],
[ [
'Exact match not on top (T72958)',
'provision' => [
'Barcelona',
'Bar',
'Barbara',
],
'query' => 'Bar',
'results' => [
'Bar',
'Barcelona',
'Barbara',
],
] ],
[ [
'Exact match missing (T72958)',
'provision' => [
'Barcelona',
'Barbara',
'Bart',
],
'query' => 'Bar',
'results' => [
'Bar',
'Barcelona',
'Barbara',
],
] ],
[ [
'Exact match missing and not existing',
'provision' => [
'Exile',
'Exist',
'External',
],
'query' => 'Ex',
'results' => [
'Exile',
'Exist',
'External',
],
] ],
[ [
"Exact match shouldn't override already found match if " .
"exact is redirect and found isn't",
'provision' => [
// Target of the exact match is low in the list
'Redirect Test Worse Result',
'Redirect Test',
],
'query' => 'redirect test',
'results' => [
// Redirect target is pulled up and exact match isn't added
'Redirect Test',
'Redirect Test Worse Result',
],
] ],
[ [
"Exact match should override already found match if " .
"both exact match and found match are redirect",
'provision' => [
// Another redirect to the same target as the exact match
// is low in the list
'Redirect Test2 Worse Result',
'Redirect test2',
],
'query' => 'redirect TEST2',
'results' => [
// Found redirect is pulled to the top and exact match isn't
// added
'Redirect TEST2',
'Redirect Test2 Worse Result',
],
] ],
[ [
"Exact match should override any already found matches that " .
"are redirects to it",
'provision' => [
// Another redirect to the same target as the exact match
// is low in the list
'Redirect Test Worse Result',
'Redirect test',
],
'query' => 'Redirect Test',
'results' => [
// Found redirect is pulled to the top and exact match isn't
// added
'Redirect Test',
'Redirect Test Worse Result',
],
] ],
];
}
/**
* @dataProvider provideSearchBackend
* @covers \PrefixSearch::searchBackend
*/
public function testSearchBackend( array $case ) {
$this->filterDeprecated( '/Use of PrefixSearchBackend hook/' );
$this->searchProvision( $case['provision'] );
$searcher = new StringPrefixSearch;
$results = $searcher->search( $case['query'], 3 );
$this->assertEquals(
$case['results'],
$results,
$case[0]
);
}
}