3 use MediaWiki\Config\ServiceOptions
;
4 use MediaWiki\Page\MovePageFactory
;
5 use MediaWiki\Permissions\PermissionManager
;
6 use Wikimedia\Rdbms\IDatabase
;
7 use Wikimedia\Rdbms\LoadBalancer
;
12 class MovePageTest
extends MediaWikiTestCase
{
14 * @param string $class
15 * @return object A mock that throws on any method call
17 private function getNoOpMock( $class ) {
18 $mock = $this->createMock( $class );
19 $mock->expects( $this->never() )->method( $this->anythingBut( '__destruct' ) );
24 * @param LinkTarget $old
25 * @param LinkTarget $new
26 * @param array $params Valid keys are: db, options, nsInfo, wiStore, repoGroup.
27 * options is an indexed array that will overwrite our defaults, not a ServiceOptions, so it
28 * need not contain all keys.
31 private function newMovePage( $old, $new, array $params = [] ) : MovePage
{
32 $mockLB = $this->createMock( LoadBalancer
::class );
33 $mockLB->method( 'getConnection' )
34 ->willReturn( $params['db'] ??
$this->getNoOpMock( IDatabase
::class ) );
35 $mockLB->expects( $this->never() )
36 ->method( $this->anythingBut( 'getConnection', '__destruct' ) );
38 $mockLocalFile = $this->createMock( LocalFile
::class );
39 $mockLocalFile->method( 'exists' )->willReturn( false );
40 $mockLocalFile->expects( $this->never() )
41 ->method( $this->anythingBut( 'exists', 'load', '__destruct' ) );
43 $mockLocalRepo = $this->createMock( LocalRepo
::class );
44 $mockLocalRepo->method( 'newFile' )->willReturn( $mockLocalFile );
45 $mockLocalRepo->expects( $this->never() )
46 ->method( $this->anythingBut( 'newFile', '__destruct' ) );
48 $mockRepoGroup = $this->createMock( RepoGroup
::class );
49 $mockRepoGroup->method( 'getLocalRepo' )->willReturn( $mockLocalRepo );
50 $mockRepoGroup->expects( $this->never() )
51 ->method( $this->anythingBut( 'getLocalRepo', '__destruct' ) );
57 MovePageFactory
::$constructorOptions,
58 $params['options'] ??
[],
60 'CategoryCollation' => 'uppercase',
61 'ContentHandlerUseDB' => true,
65 $params['nsInfo'] ??
$this->getNoOpMock( NamespaceInfo
::class ),
66 $params['wiStore'] ??
$this->getNoOpMock( WatchedItemStore
::class ),
67 $params['permMgr'] ??
$this->getNoOpMock( PermissionManager
::class ),
68 $params['repoGroup'] ??
$mockRepoGroup
72 public function setUp() {
74 $this->tablesUsed
[] = 'page';
75 $this->tablesUsed
[] = 'revision';
76 $this->tablesUsed
[] = 'comment';
80 * @dataProvider provideIsValidMove
81 * @covers MovePage::isValidMove
82 * @covers MovePage::isValidFileMove
84 public function testIsValidMove( $old, $new, $error ) {
85 global $wgMultiContentRevisionSchemaMigrationStage;
86 if ( $wgMultiContentRevisionSchemaMigrationStage === SCHEMA_COMPAT_OLD
) {
87 // We can only set this to false with the old schema
88 $this->setMwGlobals( 'wgContentHandlerUseDB', false );
90 $mp = $this->newMovePage(
91 Title
::newFromText( $old ),
92 Title
::newFromText( $new ),
93 [ 'options' => [ 'ContentHandlerUseDB' => false ] ]
95 $status = $mp->isValidMove();
96 if ( $error === true ) {
97 $this->assertTrue( $status->isGood() );
99 $this->assertTrue( $status->hasMessage( $error ) );
104 * This should be kept in sync with TitleTest::provideTestIsValidMoveOperation
106 public static function provideIsValidMove() {
107 global $wgMultiContentRevisionSchemaMigrationStage;
109 // for MovePage::isValidMove
110 [ 'Test', 'Test', 'selfmove' ],
111 [ 'Special:FooBar', 'Test', 'immobile-source-namespace' ],
112 [ 'Test', 'Special:FooBar', 'immobile-target-namespace' ],
113 [ 'Page', 'File:Test.jpg', 'nonfile-cannot-move-to-file' ],
114 // for MovePage::isValidFileMove
115 [ 'File:Test.jpg', 'Page', 'imagenocrossnamespace' ],
117 if ( $wgMultiContentRevisionSchemaMigrationStage === SCHEMA_COMPAT_OLD
) {
118 // The error can only occur if $wgContentHandlerUseDB is false, which doesn't work with
119 // the new schema, so omit the test in that case
121 [ 'MediaWiki:Common.js', 'Help:Some wikitext page', 'bad-target-model' ] );
127 * Integration test to catch regressions like T74870. Taken and modified
128 * from SemanticMediaWiki
130 * @covers Title::moveTo
131 * @covers MovePage::move
133 public function testTitleMoveCompleteIntegrationTest() {
134 $this->hideDeprecated( 'Title::moveTo' );
136 $oldTitle = Title
::newFromText( 'Help:Some title' );
137 WikiPage
::factory( $oldTitle )->doEditContent( new WikitextContent( 'foo' ), 'bar' );
138 $newTitle = Title
::newFromText( 'Help:Some other title' );
140 WikiPage
::factory( $newTitle )->getRevision()
143 $this->assertTrue( $oldTitle->moveTo( $newTitle, false, 'test1', true ) );
144 $this->assertNotNull(
145 WikiPage
::factory( $oldTitle )->getRevision()
147 $this->assertNotNull(
148 WikiPage
::factory( $newTitle )->getRevision()
153 * Test for the move operation being aborted via the TitleMove hook
154 * @covers MovePage::move
156 public function testMoveAbortedByTitleMoveHook() {
157 $error = 'Preventing move operation with TitleMove hook.';
158 $this->setTemporaryHook( 'TitleMove',
159 function ( $old, $new, $user, $reason, $status ) use ( $error ) {
160 $status->fatal( $error );
164 $oldTitle = Title
::newFromText( 'Some old title' );
165 WikiPage
::factory( $oldTitle )->doEditContent( new WikitextContent( 'foo' ), 'bar' );
166 $newTitle = Title
::newFromText( 'A brand new title' );
167 $mp = $this->newMovePage( $oldTitle, $newTitle );
168 $user = User
::newFromName( 'TitleMove tester' );
169 $status = $mp->move( $user, 'Reason', true );
170 $this->assertTrue( $status->hasMessage( $error ) );
174 * Test moving subpages from one page to another
175 * @covers MovePage::moveSubpages
177 public function testMoveSubpages() {
178 $name = ucfirst( __FUNCTION__
);
180 $subPages = [ "Talk:$name/1", "Talk:$name/2" ];
188 foreach ( array_merge( $pages, $subPages ) as $page ) {
189 $ids[$page] = $this->createPage( $page );
192 $oldTitle = Title
::newFromText( "Talk:$name" );
193 $newTitle = Title
::newFromText( "Talk:$name 2" );
194 $mp = new MovePage( $oldTitle, $newTitle );
195 $status = $mp->moveSubpages( $this->getTestUser()->getUser(), 'Reason', true );
197 $this->assertTrue( $status->isGood(),
198 "Moving subpages from Talk:{$name} to Talk:{$name} 2 was not completely successful." );
199 foreach ( $subPages as $page ) {
200 $this->assertMoved( $page, str_replace( $name, "$name 2", $page ), $ids[$page] );
205 * Test moving subpages from one page to another
206 * @covers MovePage::moveSubpagesIfAllowed
208 public function testMoveSubpagesIfAllowed() {
209 $name = ucfirst( __FUNCTION__
);
211 $subPages = [ "Talk:$name/1", "Talk:$name/2" ];
219 foreach ( array_merge( $pages, $subPages ) as $page ) {
220 $ids[$page] = $this->createPage( $page );
223 $oldTitle = Title
::newFromText( "Talk:$name" );
224 $newTitle = Title
::newFromText( "Talk:$name 2" );
225 $mp = new MovePage( $oldTitle, $newTitle );
226 $status = $mp->moveSubpagesIfAllowed( $this->getTestUser()->getUser(), 'Reason', true );
228 $this->assertTrue( $status->isGood(),
229 "Moving subpages from Talk:{$name} to Talk:{$name} 2 was not completely successful." );
230 foreach ( $subPages as $page ) {
231 $this->assertMoved( $page, str_replace( $name, "$name 2", $page ), $ids[$page] );
236 * Shortcut function to create a page and return its id.
238 * @param string $name Page to create
239 * @return int ID of created page
241 protected function createPage( $name ) {
242 return $this->editPage( $name, 'Content' )->value
['revision']->getPage();
246 * @param string $from Prefixed name of source
247 * @param string $to Prefixed name of destination
248 * @param string $id Page id of the page to move
249 * @param array|string|null $opts Options: 'noredirect' to expect no redirect
251 protected function assertMoved( $from, $to, $id, $opts = null ) {
252 $opts = (array)$opts;
254 Title
::clearCaches();
255 $fromTitle = Title
::newFromText( $from );
256 $toTitle = Title
::newFromText( $to );
258 $this->assertTrue( $toTitle->exists(),
259 "Destination {$toTitle->getPrefixedText()} does not exist" );
261 if ( in_array( 'noredirect', $opts ) ) {
262 $this->assertFalse( $fromTitle->exists(),
263 "Source {$fromTitle->getPrefixedText()} exists" );
265 $this->assertTrue( $fromTitle->exists(),
266 "Source {$fromTitle->getPrefixedText()} does not exist" );
267 $this->assertTrue( $fromTitle->isRedirect(),
268 "Source {$fromTitle->getPrefixedText()} is not a redirect" );
270 $target = Revision
::newFromTitle( $fromTitle )->getContent()->getRedirectTarget();
271 $this->assertSame( $toTitle->getPrefixedText(), $target->getPrefixedText() );
274 $this->assertSame( $id, $toTitle->getArticleID() );