Merge "mediawiki.content.json: Remove file and author annotations"
[mediawiki.git] / tests / phpunit / includes / user / ActorMigrationTest.php
blob76b94afca5e97473ca5901e7d56391b1f3241bfd
1 <?php
3 use MediaWiki\User\ActorMigration;
4 use MediaWiki\User\ActorMigrationBase;
5 use MediaWiki\User\ActorStore;
6 use MediaWiki\User\ActorStoreFactory;
7 use MediaWiki\User\UserIdentity;
8 use MediaWiki\User\UserIdentityValue;
9 use PHPUnit\Framework\MockObject\MockObject;
10 use Wikimedia\Rdbms\IMaintainableDatabase;
12 /**
13 * @group Database
14 * @covers \MediaWiki\User\ActorMigration
15 * @covers \MediaWiki\User\ActorMigrationBase
17 class ActorMigrationTest extends MediaWikiLangTestCase {
19 /** @var int */
20 protected static $amId = 0;
22 private const STAGES_BY_NAME = [
23 'old' => SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_OLD,
24 'read-old' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_OLD,
25 'read-new' => SCHEMA_COMPAT_WRITE_BOTH | SCHEMA_COMPAT_READ_NEW,
26 'new' => SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_NEW
29 protected function getSchemaOverrides( IMaintainableDatabase $db ) {
30 return [
31 'scripts' => [
32 __DIR__ . '/ActorMigrationTest.sql',
34 'drop' => [],
35 'create' => [ 'actormigration1', 'actormigration2' ],
36 'alter' => [],
40 private function getMigration( $stage, $actorStoreFactory = null ) {
41 $mwServices = $this->getServiceContainer();
42 return new ActorMigrationBase(
44 'am2_xxx' => [
45 'textField' => 'am2_xxx_text',
46 'actorField' => 'am2_xxx_actor'
49 $stage,
50 $actorStoreFactory ?? $mwServices->getActorStoreFactory()
54 private static function makeActorCases( $inputs, $expected ) {
55 foreach ( $expected as $inputName => $expectedCases ) {
56 foreach ( $expectedCases as [ $stages, $expected ] ) {
57 foreach ( $stages as $stage ) {
58 $cases[$inputName . ', ' . $stage] = array_merge(
59 [ $stage ],
60 $inputs[$inputName],
61 [ $expected ]
66 return $cases;
69 /**
70 * @dataProvider provideConstructor
71 * @param int $stage
72 * @param string|null $exceptionMsg
74 public function testConstructor( $stage, $exceptionMsg ) {
75 try {
76 $m = $this->getMigration( $stage );
77 if ( $exceptionMsg !== null ) {
78 $this->fail( 'Expected exception not thrown' );
80 $this->assertInstanceOf( ActorMigrationBase::class, $m );
81 } catch ( InvalidArgumentException $ex ) {
82 $this->assertSame( $exceptionMsg, $ex->getMessage() );
86 public static function provideConstructor() {
87 return [
88 [ 0, '$stage must include a write mode' ],
89 [ SCHEMA_COMPAT_READ_OLD, '$stage must include a write mode' ],
90 [ SCHEMA_COMPAT_READ_NEW, '$stage must include a write mode' ],
92 [ SCHEMA_COMPAT_WRITE_OLD, '$stage must include a read mode' ],
93 [ SCHEMA_COMPAT_OLD, null ],
94 [ SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_BOTH, 'Cannot read multiple schemas' ],
96 [ SCHEMA_COMPAT_WRITE_NEW, '$stage must include a read mode' ],
98 SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_OLD,
99 'Cannot read the old schema without also writing it'
105 * @dataProvider provideGetJoin
106 * @param string $stageName
107 * @param string $key
108 * @param array $expect
110 public function testGetJoin( $stageName, $key, $expect ) {
111 $stage = self::STAGES_BY_NAME[$stageName];
112 $m = $this->getMigration( $stage );
113 $result = $m->getJoin( $key );
114 $this->assertEquals( $expect, $result );
117 public static function provideGetJoin() {
118 $inputs = [
119 'Simple table' => [ 'am1_user' ],
120 'Special name' => [ 'am2_xxx' ],
122 $expected = [
123 'Simple table' => [
125 [ 'old', 'read-old' ],
127 'tables' => [],
128 'fields' => [
129 'am1_user' => 'am1_user',
130 'am1_user_text' => 'am1_user_text',
131 'am1_actor' => 'NULL',
133 'joins' => [],
137 [ 'read-new', 'new' ],
139 'tables' => [ 'actor_am1_user' => 'actor' ],
140 'fields' => [
141 'am1_user' => 'actor_am1_user.actor_user',
142 'am1_user_text' => 'actor_am1_user.actor_name',
143 'am1_actor' => 'am1_actor',
145 'joins' => [
146 'actor_am1_user' => [ 'JOIN', 'actor_am1_user.actor_id = am1_actor' ],
152 'Special name' => [
154 [ 'old', 'read-old' ],
156 'tables' => [],
157 'fields' => [
158 'am2_xxx' => 'am2_xxx',
159 'am2_xxx_text' => 'am2_xxx_text',
160 'am2_xxx_actor' => 'NULL',
162 'joins' => [],
166 [ 'read-new', 'new' ],
168 'tables' => [ 'actor_am2_xxx' => 'actor' ],
169 'fields' => [
170 'am2_xxx' => 'actor_am2_xxx.actor_user',
171 'am2_xxx_text' => 'actor_am2_xxx.actor_name',
172 'am2_xxx_actor' => 'am2_xxx_actor',
174 'joins' => [
175 'actor_am2_xxx' => [ 'JOIN', 'actor_am2_xxx.actor_id = am2_xxx_actor' ],
182 return self::makeActorCases( $inputs, $expected );
185 private const ACTORS = [
186 [ 1, 'User1', 11 ],
187 [ 2, 'User2', 12 ],
188 [ 0, '192.168.12.34', 34 ],
191 private static function findRow( $table, $index, $value ) {
192 foreach ( $table as $row ) {
193 if ( $row[$index] === $value ) {
194 return $row;
198 return null;
202 * @return ActorStore
204 private function getMockActorStore() {
205 /** @var MockObject|ActorStore $mock */
206 $mock = $this->createNoOpMock( ActorStore::class, [ 'findActorId' ] );
208 $mock->method( 'findActorId' )
209 ->willReturnCallback( static function ( UserIdentity $user ) {
210 $row = self::findRow( self::ACTORS, 1, $user->getName() );
211 return $row ? $row[2] : null;
212 } );
214 return $mock;
218 * @return ActorStoreFactory
220 private function getMockActorStoreFactory() {
221 $store = $this->getMockActorStore();
223 /** @var MockObject|ActorStoreFactory $mock */
224 $mock = $this->createNoOpMock( ActorStoreFactory::class, [ 'getActorNormalization' ] );
226 $mock->method( 'getActorNormalization' )
227 ->willReturn( $store );
229 return $mock;
233 * @dataProvider provideGetWhere
234 * @param string $stageName
235 * @param string $key
236 * @param UserIdentity|UserIdentity[]|null|false $users
237 * @param bool $useId
238 * @param array $expect
240 public function testGetWhere( $stageName, $key, $users, $useId, $expect ) {
241 $stage = self::STAGES_BY_NAME[$stageName];
242 if ( !isset( $expect['conds'] ) ) {
243 $expect['conds'] = '(' . implode( ') OR (', $expect['orconds'] ) . ')';
246 $m = $this->getMigration( $stage, $this->getMockActorStoreFactory() );
247 $result = $m->getWhere( $this->getDb(), $key, $users, $useId );
248 $this->assertEquals( $expect, $result );
251 public static function provideGetWhere() {
252 $genericUser = new UserIdentityValue( 1, 'User1' );
253 $complicatedUsers = [
254 new UserIdentityValue( 1, 'User1' ),
255 new UserIdentityValue( 2, 'User2' ),
256 new UserIdentityValue( 3, 'User3' ),
257 new UserIdentityValue( 0, '192.168.12.34' ),
258 new UserIdentityValue( 0, '192.168.12.35' ),
259 // test handling of non-normalized IPv6 IP
260 new UserIdentityValue( 0, '2600:1004:b14a:5ddd:3ebe:bba4:bfba:f37e' ),
263 $inputs = [
264 'Simple table' => [ 'am1_user', $genericUser, true ],
265 'Special name' => [ 'am2_xxx', $genericUser, true ],
266 'Multiple users' => [ 'am1_user', $complicatedUsers, true ],
267 'Multiple users, no use ID' => [ 'am1_user', $complicatedUsers, false ],
268 'Empty $users' => [ 'am1_user', [], true ],
269 'Null $users' => [ 'am1_user', null, true ],
270 'False $users' => [ 'am1_user', false, true ],
273 $expected = [
274 'Simple table' => [
276 [ 'old', 'read-old' ],
278 'tables' => [],
279 'orconds' => [ 'userid' => "am1_user = 1" ],
280 'joins' => [],
282 ], [
283 [ 'read-new', 'new' ],
285 'tables' => [],
286 'orconds' => [ 'newactor' => "am1_actor = 11" ],
287 'joins' => [],
292 'Special name' => [
294 [ 'old', 'read-old' ],
296 'tables' => [],
297 'orconds' => [ 'userid' => "am2_xxx = 1" ],
298 'joins' => [],
300 ], [
301 [ 'read-new', 'new' ],
303 'tables' => [],
304 'orconds' => [ 'newactor' => "am2_xxx_actor = 11" ],
305 'joins' => [],
310 'Multiple users' => [
312 [ 'old', 'read-old' ],
314 'tables' => [],
315 'orconds' => [
316 'userid' => "am1_user IN (1,2,3) ",
317 'username' => "am1_user_text IN ('192.168.12.34','192.168.12.35',"
318 . "'2600:1004:B14A:5DDD:3EBE:BBA4:BFBA:F37E') "
320 'joins' => [],
322 ], [
323 [ 'read-new', 'new' ],
325 'tables' => [],
326 'orconds' => [ 'newactor' => "am1_actor IN (11,12,34) " ],
327 'joins' => [],
332 'Multiple users, no use ID' => [
334 [ 'old', 'read-old' ],
336 'tables' => [],
337 'orconds' => [
338 'username' => "am1_user_text IN ('User1','User2','User3','192.168.12.34',"
339 . "'192.168.12.35','2600:1004:B14A:5DDD:3EBE:BBA4:BFBA:F37E') "
341 'joins' => [],
343 ], [
344 [ 'read-new', 'new' ],
346 'tables' => [],
347 'orconds' => [ 'newactor' => "am1_actor IN (11,12,34) " ],
348 'joins' => [],
353 'Empty $users' => [ [
354 [ 'old', 'read-old', 'read-new', 'new' ],
356 'tables' => [],
357 'conds' => '1=0',
358 'orconds' => [],
359 'joins' => [],
361 ] ],
363 'Null $users' => [ [
364 [ 'old', 'read-old', 'read-new', 'new' ],
366 'tables' => [],
367 'conds' => '1=0',
368 'orconds' => [],
369 'joins' => [],
371 ] ],
373 'False $users' => [ [
374 [ 'old', 'read-old', 'read-new', 'new' ],
376 'tables' => [],
377 'conds' => '1=0',
378 'orconds' => [],
379 'joins' => [],
381 ] ],
384 return self::makeActorCases( $inputs, $expected );
388 * @dataProvider provideStages
390 public function testGetWhere_exception( $stage ) {
391 $this->expectException( InvalidArgumentException::class );
392 $this->expectExceptionMessage(
393 'ActorMigrationBase::getWhere: Value for $users must be a UserIdentity or array, got string'
396 $m = $this->getMigration( $stage );
397 $m->getWhere( $this->getDb(), 'am1_user', 'Foo' );
401 * @dataProvider provideInsertRoundTrip
402 * @param string $table
403 * @param string $key
404 * @param string $pk
406 public function testInsertRoundTrip( $table, $key, $pk ) {
407 $u = $this->getTestUser()->getUser();
408 $user = new UserIdentityValue( $u->getId(), $u->getName() );
410 $stageNames = array_flip( self::STAGES_BY_NAME );
412 $stages = [
413 'old' => [
414 'old',
415 'read-old',
417 'read-old' => [
418 'old',
419 'read-old',
421 'read-new' => [
422 'read-new',
423 'new'
425 'new' => [
426 'read-new',
427 'new'
431 $nameKey = $key . '_text';
432 $actorKey = ( $key === 'am2_xxx' ? $key : substr( $key, 0, -5 ) ) . '_actor';
434 foreach ( $stages as $writeStageName => $possibleReadStages ) {
435 $writeStage = self::STAGES_BY_NAME[$writeStageName];
436 $w = $this->getMigration( $writeStage );
438 $fields = $w->getInsertValues( $this->getDb(), $key, $user );
440 if ( $writeStage & SCHEMA_COMPAT_WRITE_OLD ) {
441 $this->assertSame( $user->getId(), $fields[$key],
442 "old field, stage={$stageNames[$writeStage]}" );
443 $this->assertSame( $user->getName(), $fields[$nameKey],
444 "old field, stage={$stageNames[$writeStage]}" );
445 } else {
446 $this->assertArrayNotHasKey( $key, $fields, "old field, stage={$stageNames[$writeStage]}" );
447 $this->assertArrayNotHasKey( $nameKey, $fields, "old field, stage={$stageNames[$writeStage]}" );
449 if ( $writeStage & SCHEMA_COMPAT_WRITE_NEW ) {
450 $this->assertArrayHasKey( $actorKey, $fields,
451 "new field, stage={$stageNames[$writeStage]}" );
452 } else {
453 $this->assertArrayNotHasKey( $actorKey, $fields,
454 "new field, stage={$stageNames[$writeStage]}" );
457 $id = ++self::$amId;
458 $this->getDb()->newInsertQueryBuilder()
459 ->insertInto( $table )
460 ->row( [ $pk => $id ] + $fields )
461 ->caller( __METHOD__ )
462 ->execute();
464 foreach ( $possibleReadStages as $readStageName ) {
465 $readStage = self::STAGES_BY_NAME[$readStageName];
466 $r = $this->getMigration( $readStage );
468 $queryInfo = $r->getJoin( $key );
469 $row = $this->getDb()->newSelectQueryBuilder()
470 ->queryInfo( $queryInfo )
471 ->from( $table )
472 ->where( [ $pk => $id ] )
473 ->caller( __METHOD__ )
474 ->fetchRow();
476 $this->assertSame( $user->getId(), (int)$row->$key,
477 "w={$stageNames[$writeStage]}, r={$stageNames[$readStage]}, id" );
478 $this->assertSame( $user->getName(), $row->$nameKey,
479 "w={$stageNames[$writeStage]}, r={$stageNames[$readStage]}, name" );
484 public static function provideInsertRoundTrip() {
485 return [
486 'normal' => [ 'actormigration1', 'am1_user', 'am1_id' ],
487 'special name' => [ 'actormigration2', 'am2_xxx', 'am2_id' ],
491 public static function provideStages() {
492 $cases = [];
493 foreach ( self::STAGES_BY_NAME as $name => $stage ) {
494 $cases[$name] = [ $stage ];
496 return $cases;
500 * @dataProvider provideStages
501 * @param int $stage
503 public function testInsertUserIdentity( $stage ) {
504 $user = $this->getMutableTestUser()->getUser();
505 $userIdentity = new UserIdentityValue( $user->getId(), $user->getName() );
507 $m = $this->getMigration( $stage );
508 $fields = $m->getInsertValues( $this->getDb(), 'am1_user', $userIdentity );
509 $id = ++self::$amId;
510 $this->getDb()->newInsertQueryBuilder()
511 ->insertInto( 'actormigration1' )
512 ->row( [ 'am1_id' => $id ] + $fields )
513 ->caller( __METHOD__ )
514 ->execute();
516 $qi = $m->getJoin( 'am1_user' );
517 $row = $this->getDb()->newSelectQueryBuilder()
518 ->queryInfo( $qi )
519 ->from( 'actormigration1' )
520 ->where( [ 'am1_id' => $id ] )
521 ->caller( __METHOD__ )
522 ->fetchRow();
523 $this->assertSame( $user->getId(), (int)$row->am1_user );
524 $this->assertSame( $user->getName(), $row->am1_user_text );
525 $this->assertSame(
526 ( $stage & SCHEMA_COMPAT_READ_NEW ) ? $user->getActorId() : 0,
527 (int)$row->am1_actor
530 $m = $this->getMigration( $stage );
531 $fields = $m->getInsertValues( $this->getDb(), 'dummy_user', $userIdentity );
532 if ( $stage & SCHEMA_COMPAT_WRITE_OLD ) {
533 $this->assertSame( $user->getId(), $fields['dummy_user'] );
534 $this->assertSame( $user->getName(), $fields['dummy_user_text'] );
535 } else {
536 $this->assertArrayNotHasKey( 'dummy_user', $fields );
537 $this->assertArrayNotHasKey( 'dummy_user_text', $fields );
539 if ( $stage & SCHEMA_COMPAT_WRITE_NEW ) {
540 $this->assertSame( $user->getActorId(), $fields['dummy_actor'] );
541 } else {
542 $this->assertArrayNotHasKey( 'dummy_actor', $fields );
546 public function testNewMigration() {
547 $m = ActorMigration::newMigration();
548 $this->assertInstanceOf( ActorMigration::class, $m );
549 $this->assertSame( $m, ActorMigration::newMigration() );
553 * @dataProvider provideIsAnon
554 * @param string $stage
555 * @param string $isAnon
556 * @param string $isNotAnon
558 public function testIsAnon( $stage, $isAnon, $isNotAnon ) {
559 $numericStage = self::STAGES_BY_NAME[$stage];
560 $m = $this->getMigration( $numericStage );
561 $this->assertSame( $isAnon, $m->isAnon( 'foo' ) );
562 $this->assertSame( $isNotAnon, $m->isNotAnon( 'foo' ) );
565 public static function provideIsAnon() {
566 return [
567 'old' => [ 'old', 'foo = 0', 'foo != 0' ],
568 'read-old' => [ 'read-old', 'foo = 0', 'foo != 0' ],
569 'read-new' => [ 'read-new', 'foo IS NULL', 'foo IS NOT NULL' ],
570 'new' => [ 'new', 'foo IS NULL', 'foo IS NOT NULL' ],
574 public function testCheckDeprecation() {
575 $m = new class(
577 'soft' => [
578 'deprecatedVersion' => null,
580 'hard' => [
581 'deprecatedVersion' => '1.34',
583 'gone' => [
584 'removedVersion' => '1.34',
587 SCHEMA_COMPAT_NEW,
588 $this->getServiceContainer()->getActorStoreFactory()
589 ) extends ActorMigrationBase {
590 public function checkDeprecationForTest( $key ) {
591 $this->checkDeprecation( $key );
595 $this->hideDeprecated( 'MediaWiki\User\ActorMigrationBase for \'hard\'' );
597 $m->checkDeprecationForTest( 'valid' );
598 $m->checkDeprecationForTest( 'soft' );
599 $m->checkDeprecationForTest( 'hard' );
600 try {
601 $m->checkDeprecationForTest( 'gone' );
602 } catch ( InvalidArgumentException $ex ) {
603 $this->assertSame(
604 'Use of MediaWiki\User\ActorMigrationBase for \'gone\' was removed in MediaWiki 1.34',
605 $ex->getMessage()