Introduce MovePageFactory
[lhc/web/wiklou.git] / tests / phpunit / includes / MovePageTest.php
1 <?php
2
3 use MediaWiki\Config\ServiceOptions;
4 use MediaWiki\Page\MovePageFactory;
5 use Wikimedia\Rdbms\IDatabase;
6 use Wikimedia\Rdbms\LoadBalancer;
7
8 /**
9 * @group Database
10 */
11 class MovePageTest extends MediaWikiTestCase {
12 /**
13 * @param string $class
14 * @return object A mock that throws on any method call
15 */
16 private function getNoOpMock( $class ) {
17 $mock = $this->createMock( $class );
18 $mock->expects( $this->never() )->method( $this->anythingBut( '__destruct' ) );
19 return $mock;
20 }
21
22 /**
23 * @param LinkTarget $old
24 * @param LinkTarget $new
25 * @param array $params Valid keys are: db, options, nsInfo, wiStore. options is an indexed
26 * array that will overwrite our defaults, not a ServiceOptions, so it need not contain all
27 * keys.
28 * @return MovePage
29 */
30 private function newMovePage( $old, $new, array $params = [] ) : MovePage {
31 $mockLB = $this->createMock( LoadBalancer::class );
32 $mockLB->method( 'getConnection' )
33 ->willReturn( $params['db'] ?? $this->getNoOpMock( IDatabase::class ) );
34 $mockLB->expects( $this->never() )
35 ->method( $this->anythingBut( 'getConnection', '__destruct' ) );
36
37 return new MovePage(
38 $old,
39 $new,
40 new ServiceOptions(
41 MovePageFactory::$constructorOptions,
42 $params['options'] ?? [],
43 [
44 'CategoryCollation' => 'uppercase',
45 'ContentHandlerUseDB' => true,
46 ]
47 ),
48 $mockLB,
49 $params['nsInfo'] ?? $this->getNoOpMock( NamespaceInfo::class ),
50 $params['wiStore'] ?? $this->getNoOpMock( WatchedItemStore::class )
51 );
52 }
53
54 public function setUp() {
55 parent::setUp();
56 $this->tablesUsed[] = 'page';
57 $this->tablesUsed[] = 'revision';
58 $this->tablesUsed[] = 'comment';
59 }
60
61 /**
62 * @dataProvider provideIsValidMove
63 * @covers MovePage::isValidMove
64 * @covers MovePage::isValidFileMove
65 */
66 public function testIsValidMove( $old, $new, $error ) {
67 global $wgMultiContentRevisionSchemaMigrationStage;
68 if ( $wgMultiContentRevisionSchemaMigrationStage === SCHEMA_COMPAT_OLD ) {
69 // We can only set this to false with the old schema
70 $this->setMwGlobals( 'wgContentHandlerUseDB', false );
71 }
72 $mp = $this->newMovePage(
73 Title::newFromText( $old ),
74 Title::newFromText( $new ),
75 [ 'options' => [ 'ContentHandlerUseDB' => false ] ]
76 );
77 $status = $mp->isValidMove();
78 if ( $error === true ) {
79 $this->assertTrue( $status->isGood() );
80 } else {
81 $this->assertTrue( $status->hasMessage( $error ) );
82 }
83 }
84
85 /**
86 * This should be kept in sync with TitleTest::provideTestIsValidMoveOperation
87 */
88 public static function provideIsValidMove() {
89 global $wgMultiContentRevisionSchemaMigrationStage;
90 $ret = [
91 // for MovePage::isValidMove
92 [ 'Test', 'Test', 'selfmove' ],
93 [ 'Special:FooBar', 'Test', 'immobile-source-namespace' ],
94 [ 'Test', 'Special:FooBar', 'immobile-target-namespace' ],
95 [ 'Page', 'File:Test.jpg', 'nonfile-cannot-move-to-file' ],
96 // for MovePage::isValidFileMove
97 [ 'File:Test.jpg', 'Page', 'imagenocrossnamespace' ],
98 ];
99 if ( $wgMultiContentRevisionSchemaMigrationStage === SCHEMA_COMPAT_OLD ) {
100 // The error can only occur if $wgContentHandlerUseDB is false, which doesn't work with
101 // the new schema, so omit the test in that case
102 array_push( $ret,
103 [ 'MediaWiki:Common.js', 'Help:Some wikitext page', 'bad-target-model' ] );
104 }
105 return $ret;
106 }
107
108 /**
109 * Integration test to catch regressions like T74870. Taken and modified
110 * from SemanticMediaWiki
111 *
112 * @covers Title::moveTo
113 * @covers MovePage::move
114 */
115 public function testTitleMoveCompleteIntegrationTest() {
116 $this->hideDeprecated( 'Title::moveTo' );
117
118 $oldTitle = Title::newFromText( 'Help:Some title' );
119 WikiPage::factory( $oldTitle )->doEditContent( new WikitextContent( 'foo' ), 'bar' );
120 $newTitle = Title::newFromText( 'Help:Some other title' );
121 $this->assertNull(
122 WikiPage::factory( $newTitle )->getRevision()
123 );
124
125 $this->assertTrue( $oldTitle->moveTo( $newTitle, false, 'test1', true ) );
126 $this->assertNotNull(
127 WikiPage::factory( $oldTitle )->getRevision()
128 );
129 $this->assertNotNull(
130 WikiPage::factory( $newTitle )->getRevision()
131 );
132 }
133
134 /**
135 * Test for the move operation being aborted via the TitleMove hook
136 * @covers MovePage::move
137 */
138 public function testMoveAbortedByTitleMoveHook() {
139 $error = 'Preventing move operation with TitleMove hook.';
140 $this->setTemporaryHook( 'TitleMove',
141 function ( $old, $new, $user, $reason, $status ) use ( $error ) {
142 $status->fatal( $error );
143 }
144 );
145
146 $oldTitle = Title::newFromText( 'Some old title' );
147 WikiPage::factory( $oldTitle )->doEditContent( new WikitextContent( 'foo' ), 'bar' );
148 $newTitle = Title::newFromText( 'A brand new title' );
149 $mp = $this->newMovePage( $oldTitle, $newTitle );
150 $user = User::newFromName( 'TitleMove tester' );
151 $status = $mp->move( $user, 'Reason', true );
152 $this->assertTrue( $status->hasMessage( $error ) );
153 }
154
155 /**
156 * Test moving subpages from one page to another
157 * @covers MovePage::moveSubpages
158 */
159 public function testMoveSubpages() {
160 $name = ucfirst( __FUNCTION__ );
161
162 $subPages = [ "Talk:$name/1", "Talk:$name/2" ];
163 $ids = [];
164 $pages = [
165 $name,
166 "Talk:$name",
167 "$name 2",
168 "Talk:$name 2",
169 ];
170 foreach ( array_merge( $pages, $subPages ) as $page ) {
171 $ids[$page] = $this->createPage( $page );
172 }
173
174 $oldTitle = Title::newFromText( "Talk:$name" );
175 $newTitle = Title::newFromText( "Talk:$name 2" );
176 $mp = new MovePage( $oldTitle, $newTitle );
177 $status = $mp->moveSubpages( $this->getTestUser()->getUser(), 'Reason', true );
178
179 $this->assertTrue( $status->isGood(),
180 "Moving subpages from Talk:{$name} to Talk:{$name} 2 was not completely successful." );
181 foreach ( $subPages as $page ) {
182 $this->assertMoved( $page, str_replace( $name, "$name 2", $page ), $ids[$page] );
183 }
184 }
185
186 /**
187 * Test moving subpages from one page to another
188 * @covers MovePage::moveSubpagesIfAllowed
189 */
190 public function testMoveSubpagesIfAllowed() {
191 $name = ucfirst( __FUNCTION__ );
192
193 $subPages = [ "Talk:$name/1", "Talk:$name/2" ];
194 $ids = [];
195 $pages = [
196 $name,
197 "Talk:$name",
198 "$name 2",
199 "Talk:$name 2",
200 ];
201 foreach ( array_merge( $pages, $subPages ) as $page ) {
202 $ids[$page] = $this->createPage( $page );
203 }
204
205 $oldTitle = Title::newFromText( "Talk:$name" );
206 $newTitle = Title::newFromText( "Talk:$name 2" );
207 $mp = new MovePage( $oldTitle, $newTitle );
208 $status = $mp->moveSubpagesIfAllowed( $this->getTestUser()->getUser(), 'Reason', true );
209
210 $this->assertTrue( $status->isGood(),
211 "Moving subpages from Talk:{$name} to Talk:{$name} 2 was not completely successful." );
212 foreach ( $subPages as $page ) {
213 $this->assertMoved( $page, str_replace( $name, "$name 2", $page ), $ids[$page] );
214 }
215 }
216
217 /**
218 * Shortcut function to create a page and return its id.
219 *
220 * @param string $name Page to create
221 * @return int ID of created page
222 */
223 protected function createPage( $name ) {
224 return $this->editPage( $name, 'Content' )->value['revision']->getPage();
225 }
226
227 /**
228 * @param string $from Prefixed name of source
229 * @param string $to Prefixed name of destination
230 * @param string $id Page id of the page to move
231 * @param array|string|null $opts Options: 'noredirect' to expect no redirect
232 */
233 protected function assertMoved( $from, $to, $id, $opts = null ) {
234 $opts = (array)$opts;
235
236 Title::clearCaches();
237 $fromTitle = Title::newFromText( $from );
238 $toTitle = Title::newFromText( $to );
239
240 $this->assertTrue( $toTitle->exists(),
241 "Destination {$toTitle->getPrefixedText()} does not exist" );
242
243 if ( in_array( 'noredirect', $opts ) ) {
244 $this->assertFalse( $fromTitle->exists(),
245 "Source {$fromTitle->getPrefixedText()} exists" );
246 } else {
247 $this->assertTrue( $fromTitle->exists(),
248 "Source {$fromTitle->getPrefixedText()} does not exist" );
249 $this->assertTrue( $fromTitle->isRedirect(),
250 "Source {$fromTitle->getPrefixedText()} is not a redirect" );
251
252 $target = Revision::newFromTitle( $fromTitle )->getContent()->getRedirectTarget();
253 $this->assertSame( $toTitle->getPrefixedText(), $target->getPrefixedText() );
254 }
255
256 $this->assertSame( $id, $toTitle->getArticleID() );
257 }
258 }