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
;
14 * @covers \MediaWiki\User\ActorMigration
15 * @covers \MediaWiki\User\ActorMigrationBase
17 class ActorMigrationTest
extends MediaWikiLangTestCase
{
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 ) {
32 __DIR__
. '/ActorMigrationTest.sql',
35 'create' => [ 'actormigration1', 'actormigration2' ],
40 private function getMigration( $stage, $actorStoreFactory = null ) {
41 $mwServices = $this->getServiceContainer();
42 return new ActorMigrationBase(
45 'textField' => 'am2_xxx_text',
46 'actorField' => 'am2_xxx_actor'
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(
70 * @dataProvider provideConstructor
72 * @param string|null $exceptionMsg
74 public function testConstructor( $stage, $exceptionMsg ) {
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() {
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
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() {
119 'Simple table' => [ 'am1_user' ],
120 'Special name' => [ 'am2_xxx' ],
125 [ 'old', 'read-old' ],
129 'am1_user' => 'am1_user',
130 'am1_user_text' => 'am1_user_text',
131 'am1_actor' => 'NULL',
137 [ 'read-new', 'new' ],
139 'tables' => [ 'actor_am1_user' => 'actor' ],
141 'am1_user' => 'actor_am1_user.actor_user',
142 'am1_user_text' => 'actor_am1_user.actor_name',
143 'am1_actor' => 'am1_actor',
146 'actor_am1_user' => [ 'JOIN', 'actor_am1_user.actor_id = am1_actor' ],
154 [ 'old', 'read-old' ],
158 'am2_xxx' => 'am2_xxx',
159 'am2_xxx_text' => 'am2_xxx_text',
160 'am2_xxx_actor' => 'NULL',
166 [ 'read-new', 'new' ],
168 'tables' => [ 'actor_am2_xxx' => 'actor' ],
170 'am2_xxx' => 'actor_am2_xxx.actor_user',
171 'am2_xxx_text' => 'actor_am2_xxx.actor_name',
172 'am2_xxx_actor' => 'am2_xxx_actor',
175 'actor_am2_xxx' => [ 'JOIN', 'actor_am2_xxx.actor_id = am2_xxx_actor' ],
182 return self
::makeActorCases( $inputs, $expected );
185 private const ACTORS
= [
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 ) {
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;
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 );
233 * @dataProvider provideGetWhere
234 * @param string $stageName
236 * @param UserIdentity|UserIdentity[]|null|false $users
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' ),
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 ],
276 [ 'old', 'read-old' ],
279 'orconds' => [ 'userid' => "am1_user = 1" ],
283 [ 'read-new', 'new' ],
286 'orconds' => [ 'newactor' => "am1_actor = 11" ],
294 [ 'old', 'read-old' ],
297 'orconds' => [ 'userid' => "am2_xxx = 1" ],
301 [ 'read-new', 'new' ],
304 'orconds' => [ 'newactor' => "am2_xxx_actor = 11" ],
310 'Multiple users' => [
312 [ 'old', 'read-old' ],
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') "
323 [ 'read-new', 'new' ],
326 'orconds' => [ 'newactor' => "am1_actor IN (11,12,34) " ],
332 'Multiple users, no use ID' => [
334 [ 'old', 'read-old' ],
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') "
344 [ 'read-new', 'new' ],
347 'orconds' => [ 'newactor' => "am1_actor IN (11,12,34) " ],
353 'Empty $users' => [ [
354 [ 'old', 'read-old', 'read-new', 'new' ],
364 [ 'old', 'read-old', 'read-new', 'new' ],
373 'False $users' => [ [
374 [ 'old', 'read-old', 'read-new', 'new' ],
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
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
);
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]}" );
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]}" );
453 $this->assertArrayNotHasKey( $actorKey, $fields,
454 "new field, stage={$stageNames[$writeStage]}" );
458 $this->getDb()->newInsertQueryBuilder()
459 ->insertInto( $table )
460 ->row( [ $pk => $id ] +
$fields )
461 ->caller( __METHOD__
)
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 )
472 ->where( [ $pk => $id ] )
473 ->caller( __METHOD__
)
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() {
486 'normal' => [ 'actormigration1', 'am1_user', 'am1_id' ],
487 'special name' => [ 'actormigration2', 'am2_xxx', 'am2_id' ],
491 public static function provideStages() {
493 foreach ( self
::STAGES_BY_NAME
as $name => $stage ) {
494 $cases[$name] = [ $stage ];
500 * @dataProvider provideStages
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 );
510 $this->getDb()->newInsertQueryBuilder()
511 ->insertInto( 'actormigration1' )
512 ->row( [ 'am1_id' => $id ] +
$fields )
513 ->caller( __METHOD__
)
516 $qi = $m->getJoin( 'am1_user' );
517 $row = $this->getDb()->newSelectQueryBuilder()
519 ->from( 'actormigration1' )
520 ->where( [ 'am1_id' => $id ] )
521 ->caller( __METHOD__
)
523 $this->assertSame( $user->getId(), (int)$row->am1_user
);
524 $this->assertSame( $user->getName(), $row->am1_user_text
);
526 ( $stage & SCHEMA_COMPAT_READ_NEW
) ?
$user->getActorId() : 0,
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'] );
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'] );
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() {
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() {
578 'deprecatedVersion' => null,
581 'deprecatedVersion' => '1.34',
584 'removedVersion' => '1.34',
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' );
601 $m->checkDeprecationForTest( 'gone' );
602 } catch ( InvalidArgumentException
$ex ) {
604 'Use of MediaWiki\User\ActorMigrationBase for \'gone\' was removed in MediaWiki 1.34',