RevisionStoreDbTestBase.php 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665
  1. <?php
  2. namespace MediaWiki\Tests\Storage;
  3. use CommentStoreComment;
  4. use Content;
  5. use Exception;
  6. use HashBagOStuff;
  7. use InvalidArgumentException;
  8. use Language;
  9. use MediaWiki\Linker\LinkTarget;
  10. use MediaWiki\MediaWikiServices;
  11. use MediaWiki\Storage\BlobStoreFactory;
  12. use MediaWiki\Storage\IncompleteRevisionException;
  13. use MediaWiki\Storage\MutableRevisionRecord;
  14. use MediaWiki\Storage\RevisionRecord;
  15. use MediaWiki\Storage\RevisionStore;
  16. use MediaWiki\Storage\SlotRecord;
  17. use MediaWiki\Storage\SqlBlobStore;
  18. use MediaWiki\User\UserIdentityValue;
  19. use MediaWikiTestCase;
  20. use PHPUnit_Framework_MockObject_MockObject;
  21. use Revision;
  22. use TestUserRegistry;
  23. use Title;
  24. use WANObjectCache;
  25. use Wikimedia\Rdbms\Database;
  26. use Wikimedia\Rdbms\DatabaseSqlite;
  27. use Wikimedia\Rdbms\FakeResultWrapper;
  28. use Wikimedia\Rdbms\LoadBalancer;
  29. use Wikimedia\Rdbms\TransactionProfiler;
  30. use WikiPage;
  31. use WikitextContent;
  32. /**
  33. * @group Database
  34. * @group RevisionStore
  35. */
  36. abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
  37. /**
  38. * @var Title
  39. */
  40. private $testPageTitle;
  41. /**
  42. * @var WikiPage
  43. */
  44. private $testPage;
  45. /**
  46. * @return int
  47. */
  48. abstract protected function getMcrMigrationStage();
  49. /**
  50. * @return bool
  51. */
  52. protected function getContentHandlerUseDB() {
  53. return true;
  54. }
  55. /**
  56. * @return string[]
  57. */
  58. abstract protected function getMcrTablesToReset();
  59. public function needsDB() {
  60. return true;
  61. }
  62. public function setUp() {
  63. parent::setUp();
  64. $this->tablesUsed[] = 'archive';
  65. $this->tablesUsed[] = 'page';
  66. $this->tablesUsed[] = 'revision';
  67. $this->tablesUsed[] = 'comment';
  68. $this->tablesUsed += $this->getMcrTablesToReset();
  69. $this->setMwGlobals( [
  70. 'wgMultiContentRevisionSchemaMigrationStage' => $this->getMcrMigrationStage(),
  71. 'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(),
  72. 'wgCommentTableSchemaMigrationStage' => MIGRATION_OLD,
  73. 'wgActorTableSchemaMigrationStage' => MIGRATION_OLD,
  74. ] );
  75. $this->overrideMwServices();
  76. }
  77. protected function addCoreDBData() {
  78. // Blank out. This would fail with a modified schema, and we don't need it.
  79. }
  80. /**
  81. * @return Title
  82. */
  83. protected function getTestPageTitle() {
  84. if ( $this->testPageTitle ) {
  85. return $this->testPageTitle;
  86. }
  87. $this->testPageTitle = Title::newFromText( 'UTPage-' . __CLASS__ );
  88. return $this->testPageTitle;
  89. }
  90. /**
  91. * @return WikiPage
  92. */
  93. protected function getTestPage() {
  94. if ( $this->testPage ) {
  95. return $this->testPage;
  96. }
  97. $title = $this->getTestPageTitle();
  98. $this->testPage = WikiPage::factory( $title );
  99. if ( !$this->testPage->exists() ) {
  100. // Make sure we don't write to the live db.
  101. $this->ensureMockDatabaseConnection( wfGetDB( DB_MASTER ) );
  102. $user = static::getTestSysop()->getUser();
  103. $this->testPage->doEditContent(
  104. new WikitextContent( 'UTContent-' . __CLASS__ ),
  105. 'UTPageSummary-' . __CLASS__,
  106. EDIT_NEW | EDIT_SUPPRESS_RC,
  107. false,
  108. $user
  109. );
  110. }
  111. return $this->testPage;
  112. }
  113. /**
  114. * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject
  115. */
  116. private function getLoadBalancerMock( array $server ) {
  117. $lb = $this->getMockBuilder( LoadBalancer::class )
  118. ->setMethods( [ 'reallyOpenConnection' ] )
  119. ->setConstructorArgs( [ [ 'servers' => [ $server ] ] ] )
  120. ->getMock();
  121. $lb->method( 'reallyOpenConnection' )->willReturnCallback(
  122. function ( array $server, $dbNameOverride ) {
  123. return $this->getDatabaseMock( $server );
  124. }
  125. );
  126. return $lb;
  127. }
  128. /**
  129. * @return Database|PHPUnit_Framework_MockObject_MockObject
  130. */
  131. private function getDatabaseMock( array $params ) {
  132. $db = $this->getMockBuilder( DatabaseSqlite::class )
  133. ->setMethods( [ 'select', 'doQuery', 'open', 'closeConnection', 'isOpen' ] )
  134. ->setConstructorArgs( [ $params ] )
  135. ->getMock();
  136. $db->method( 'select' )->willReturn( new FakeResultWrapper( [] ) );
  137. $db->method( 'isOpen' )->willReturn( true );
  138. return $db;
  139. }
  140. public function provideDomainCheck() {
  141. yield [ false, 'test', '' ];
  142. yield [ 'test', 'test', '' ];
  143. yield [ false, 'test', 'foo_' ];
  144. yield [ 'test-foo_', 'test', 'foo_' ];
  145. yield [ false, 'dash-test', '' ];
  146. yield [ 'dash-test', 'dash-test', '' ];
  147. yield [ false, 'underscore_test', 'foo_' ];
  148. yield [ 'underscore_test-foo_', 'underscore_test', 'foo_' ];
  149. }
  150. /**
  151. * @dataProvider provideDomainCheck
  152. * @covers \MediaWiki\Storage\RevisionStore::checkDatabaseWikiId
  153. */
  154. public function testDomainCheck( $wikiId, $dbName, $dbPrefix ) {
  155. $this->setMwGlobals(
  156. [
  157. 'wgDBname' => $dbName,
  158. 'wgDBprefix' => $dbPrefix,
  159. ]
  160. );
  161. $loadBalancer = $this->getLoadBalancerMock(
  162. [
  163. 'host' => '*dummy*',
  164. 'dbDirectory' => '*dummy*',
  165. 'user' => 'test',
  166. 'password' => 'test',
  167. 'flags' => 0,
  168. 'variables' => [],
  169. 'schema' => '',
  170. 'cliMode' => true,
  171. 'agent' => '',
  172. 'load' => 100,
  173. 'profiler' => null,
  174. 'trxProfiler' => new TransactionProfiler(),
  175. 'connLogger' => new \Psr\Log\NullLogger(),
  176. 'queryLogger' => new \Psr\Log\NullLogger(),
  177. 'errorLogger' => function () {
  178. },
  179. 'deprecationLogger' => function () {
  180. },
  181. 'type' => 'test',
  182. 'dbname' => $dbName,
  183. 'tablePrefix' => $dbPrefix,
  184. ]
  185. );
  186. $db = $loadBalancer->getConnection( DB_REPLICA );
  187. /** @var SqlBlobStore $blobStore */
  188. $blobStore = $this->getMockBuilder( SqlBlobStore::class )
  189. ->disableOriginalConstructor()
  190. ->getMock();
  191. $store = new RevisionStore(
  192. $loadBalancer,
  193. $blobStore,
  194. new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ),
  195. MediaWikiServices::getInstance()->getCommentStore(),
  196. MediaWikiServices::getInstance()->getContentModelStore(),
  197. MediaWikiServices::getInstance()->getSlotRoleStore(),
  198. $this->getMcrMigrationStage(),
  199. MediaWikiServices::getInstance()->getActorMigration(),
  200. $wikiId
  201. );
  202. $count = $store->countRevisionsByPageId( $db, 0 );
  203. // Dummy check to make PhpUnit happy. We are really only interested in
  204. // countRevisionsByPageId not failing due to the DB domain check.
  205. $this->assertSame( 0, $count );
  206. }
  207. protected function assertLinkTargetsEqual( LinkTarget $l1, LinkTarget $l2 ) {
  208. $this->assertEquals( $l1->getDBkey(), $l2->getDBkey() );
  209. $this->assertEquals( $l1->getNamespace(), $l2->getNamespace() );
  210. $this->assertEquals( $l1->getFragment(), $l2->getFragment() );
  211. $this->assertEquals( $l1->getInterwiki(), $l2->getInterwiki() );
  212. }
  213. protected function assertRevisionRecordsEqual( RevisionRecord $r1, RevisionRecord $r2 ) {
  214. $this->assertEquals(
  215. $r1->getPageAsLinkTarget()->getNamespace(),
  216. $r2->getPageAsLinkTarget()->getNamespace()
  217. );
  218. $this->assertEquals(
  219. $r1->getPageAsLinkTarget()->getText(),
  220. $r2->getPageAsLinkTarget()->getText()
  221. );
  222. if ( $r1->getParentId() ) {
  223. $this->assertEquals( $r1->getParentId(), $r2->getParentId() );
  224. }
  225. $this->assertEquals( $r1->getUser()->getName(), $r2->getUser()->getName() );
  226. $this->assertEquals( $r1->getUser()->getId(), $r2->getUser()->getId() );
  227. $this->assertEquals( $r1->getComment(), $r2->getComment() );
  228. $this->assertEquals( $r1->getTimestamp(), $r2->getTimestamp() );
  229. $this->assertEquals( $r1->getVisibility(), $r2->getVisibility() );
  230. $this->assertEquals( $r1->getSha1(), $r2->getSha1() );
  231. $this->assertEquals( $r1->getSize(), $r2->getSize() );
  232. $this->assertEquals( $r1->getPageId(), $r2->getPageId() );
  233. $this->assertArrayEquals( $r1->getSlotRoles(), $r2->getSlotRoles() );
  234. $this->assertEquals( $r1->getWikiId(), $r2->getWikiId() );
  235. $this->assertEquals( $r1->isMinor(), $r2->isMinor() );
  236. foreach ( $r1->getSlotRoles() as $role ) {
  237. $this->assertSlotRecordsEqual( $r1->getSlot( $role ), $r2->getSlot( $role ) );
  238. $this->assertTrue( $r1->getContent( $role )->equals( $r2->getContent( $role ) ) );
  239. }
  240. foreach ( [
  241. RevisionRecord::DELETED_TEXT,
  242. RevisionRecord::DELETED_COMMENT,
  243. RevisionRecord::DELETED_USER,
  244. RevisionRecord::DELETED_RESTRICTED,
  245. ] as $field ) {
  246. $this->assertEquals( $r1->isDeleted( $field ), $r2->isDeleted( $field ) );
  247. }
  248. }
  249. protected function assertSlotRecordsEqual( SlotRecord $s1, SlotRecord $s2 ) {
  250. $this->assertSame( $s1->getRole(), $s2->getRole() );
  251. $this->assertSame( $s1->getModel(), $s2->getModel() );
  252. $this->assertSame( $s1->getFormat(), $s2->getFormat() );
  253. $this->assertSame( $s1->getSha1(), $s2->getSha1() );
  254. $this->assertSame( $s1->getSize(), $s2->getSize() );
  255. $this->assertTrue( $s1->getContent()->equals( $s2->getContent() ) );
  256. $s1->hasRevision() ? $this->assertSame( $s1->getRevision(), $s2->getRevision() ) : null;
  257. $s1->hasAddress() ? $this->assertSame( $s1->hasAddress(), $s2->hasAddress() ) : null;
  258. }
  259. protected function assertRevisionCompleteness( RevisionRecord $r ) {
  260. $this->assertTrue( $r->hasSlot( 'main' ) );
  261. $this->assertInstanceOf( SlotRecord::class, $r->getSlot( 'main' ) );
  262. $this->assertInstanceOf( Content::class, $r->getContent( 'main' ) );
  263. foreach ( $r->getSlotRoles() as $role ) {
  264. $this->assertSlotCompleteness( $r, $r->getSlot( $role ) );
  265. }
  266. }
  267. protected function assertSlotCompleteness( RevisionRecord $r, SlotRecord $slot ) {
  268. $this->assertTrue( $slot->hasAddress() );
  269. $this->assertSame( $r->getId(), $slot->getRevision() );
  270. $this->assertInstanceOf( Content::class, $slot->getContent() );
  271. }
  272. /**
  273. * @param mixed[] $details
  274. *
  275. * @return RevisionRecord
  276. */
  277. private function getRevisionRecordFromDetailsArray( $details = [] ) {
  278. // Convert some values that can't be provided by dataProviders
  279. if ( isset( $details['user'] ) && $details['user'] === true ) {
  280. $details['user'] = $this->getTestUser()->getUser();
  281. }
  282. if ( isset( $details['page'] ) && $details['page'] === true ) {
  283. $details['page'] = $this->getTestPage()->getId();
  284. }
  285. if ( isset( $details['parent'] ) && $details['parent'] === true ) {
  286. $details['parent'] = $this->getTestPage()->getLatest();
  287. }
  288. // Create the RevisionRecord with any available data
  289. $rev = new MutableRevisionRecord( $this->getTestPageTitle() );
  290. isset( $details['slot'] ) ? $rev->setSlot( $details['slot'] ) : null;
  291. isset( $details['parent'] ) ? $rev->setParentId( $details['parent'] ) : null;
  292. isset( $details['page'] ) ? $rev->setPageId( $details['page'] ) : null;
  293. isset( $details['size'] ) ? $rev->setSize( $details['size'] ) : null;
  294. isset( $details['sha1'] ) ? $rev->setSha1( $details['sha1'] ) : null;
  295. isset( $details['comment'] ) ? $rev->setComment( $details['comment'] ) : null;
  296. isset( $details['timestamp'] ) ? $rev->setTimestamp( $details['timestamp'] ) : null;
  297. isset( $details['minor'] ) ? $rev->setMinorEdit( $details['minor'] ) : null;
  298. isset( $details['user'] ) ? $rev->setUser( $details['user'] ) : null;
  299. isset( $details['visibility'] ) ? $rev->setVisibility( $details['visibility'] ) : null;
  300. isset( $details['id'] ) ? $rev->setId( $details['id'] ) : null;
  301. if ( isset( $details['content'] ) ) {
  302. foreach ( $details['content'] as $role => $content ) {
  303. $rev->setContent( $role, $content );
  304. }
  305. }
  306. return $rev;
  307. }
  308. public function provideInsertRevisionOn_successes() {
  309. yield 'Bare minimum revision insertion' => [
  310. [
  311. 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
  312. 'page' => true,
  313. 'comment' => $this->getRandomCommentStoreComment(),
  314. 'timestamp' => '20171117010101',
  315. 'user' => true,
  316. ],
  317. ];
  318. yield 'Detailed revision insertion' => [
  319. [
  320. 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
  321. 'parent' => true,
  322. 'page' => true,
  323. 'comment' => $this->getRandomCommentStoreComment(),
  324. 'timestamp' => '20171117010101',
  325. 'user' => true,
  326. 'minor' => true,
  327. 'visibility' => RevisionRecord::DELETED_RESTRICTED,
  328. ],
  329. ];
  330. }
  331. protected function getRandomCommentStoreComment() {
  332. return CommentStoreComment::newUnsavedComment( __METHOD__ . '.' . rand( 0, 1000 ) );
  333. }
  334. /**
  335. * @dataProvider provideInsertRevisionOn_successes
  336. * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
  337. * @covers \MediaWiki\Storage\RevisionStore::insertSlotRowOn
  338. * @covers \MediaWiki\Storage\RevisionStore::insertContentRowOn
  339. */
  340. public function testInsertRevisionOn_successes(
  341. array $revDetails = []
  342. ) {
  343. $title = $this->getTestPageTitle();
  344. $rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
  345. $this->overrideMwServices();
  346. $store = MediaWikiServices::getInstance()->getRevisionStore();
  347. $return = $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) );
  348. // is the new revision correct?
  349. $this->assertRevisionCompleteness( $return );
  350. $this->assertLinkTargetsEqual( $title, $return->getPageAsLinkTarget() );
  351. $this->assertRevisionRecordsEqual( $rev, $return );
  352. // can we load it from the store?
  353. $loaded = $store->getRevisionById( $return->getId() );
  354. $this->assertRevisionCompleteness( $loaded );
  355. $this->assertRevisionRecordsEqual( $return, $loaded );
  356. // can we find it directly in the database?
  357. $this->assertRevisionExistsInDatabase( $return );
  358. }
  359. protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) {
  360. $row = $this->revisionToRow( new Revision( $rev ), [] );
  361. // unset nulled fields
  362. unset( $row->rev_content_model );
  363. unset( $row->rev_content_format );
  364. // unset fake fields
  365. unset( $row->rev_comment_text );
  366. unset( $row->rev_comment_data );
  367. unset( $row->rev_comment_cid );
  368. unset( $row->rev_comment_id );
  369. $store = MediaWikiServices::getInstance()->getRevisionStore();
  370. $queryInfo = $store->getQueryInfo( [ 'user' ] );
  371. $row = get_object_vars( $row );
  372. $this->assertSelect(
  373. $queryInfo['tables'],
  374. array_keys( $row ),
  375. [ 'rev_id' => $rev->getId() ],
  376. [ array_values( $row ) ],
  377. [],
  378. $queryInfo['joins']
  379. );
  380. }
  381. /**
  382. * @param SlotRecord $a
  383. * @param SlotRecord $b
  384. */
  385. protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) {
  386. // Assert that the same blob address has been used.
  387. $this->assertSame( $a->getAddress(), $b->getAddress() );
  388. }
  389. /**
  390. * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
  391. */
  392. public function testInsertRevisionOn_blobAddressExists() {
  393. $title = $this->getTestPageTitle();
  394. $revDetails = [
  395. 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
  396. 'parent' => true,
  397. 'comment' => $this->getRandomCommentStoreComment(),
  398. 'timestamp' => '20171117010101',
  399. 'user' => true,
  400. ];
  401. $this->overrideMwServices();
  402. $store = MediaWikiServices::getInstance()->getRevisionStore();
  403. // Insert the first revision
  404. $revOne = $this->getRevisionRecordFromDetailsArray( $revDetails );
  405. $firstReturn = $store->insertRevisionOn( $revOne, wfGetDB( DB_MASTER ) );
  406. $this->assertLinkTargetsEqual( $title, $firstReturn->getPageAsLinkTarget() );
  407. $this->assertRevisionRecordsEqual( $revOne, $firstReturn );
  408. // Insert a second revision inheriting the same blob address
  409. $revDetails['slot'] = SlotRecord::newInherited( $firstReturn->getSlot( 'main' ) );
  410. $revTwo = $this->getRevisionRecordFromDetailsArray( $revDetails );
  411. $secondReturn = $store->insertRevisionOn( $revTwo, wfGetDB( DB_MASTER ) );
  412. $this->assertLinkTargetsEqual( $title, $secondReturn->getPageAsLinkTarget() );
  413. $this->assertRevisionRecordsEqual( $revTwo, $secondReturn );
  414. $firstMainSlot = $firstReturn->getSlot( 'main' );
  415. $secondMainSlot = $secondReturn->getSlot( 'main' );
  416. $this->assertSameSlotContent( $firstMainSlot, $secondMainSlot );
  417. // And that different revisions have been created.
  418. $this->assertNotSame( $firstReturn->getId(), $secondReturn->getId() );
  419. // Make sure the slot rows reference the correct revision
  420. $this->assertSame( $firstReturn->getId(), $firstMainSlot->getRevision() );
  421. $this->assertSame( $secondReturn->getId(), $secondMainSlot->getRevision() );
  422. }
  423. public function provideInsertRevisionOn_failures() {
  424. yield 'no slot' => [
  425. [
  426. 'comment' => $this->getRandomCommentStoreComment(),
  427. 'timestamp' => '20171117010101',
  428. 'user' => true,
  429. ],
  430. new InvalidArgumentException( 'main slot must be provided' )
  431. ];
  432. yield 'no main slot' => [
  433. [
  434. 'slot' => SlotRecord::newUnsaved( 'aux', new WikitextContent( 'Turkey' ) ),
  435. 'comment' => $this->getRandomCommentStoreComment(),
  436. 'timestamp' => '20171117010101',
  437. 'user' => true,
  438. ],
  439. new InvalidArgumentException( 'main slot must be provided' )
  440. ];
  441. yield 'no timestamp' => [
  442. [
  443. 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
  444. 'comment' => $this->getRandomCommentStoreComment(),
  445. 'user' => true,
  446. ],
  447. new IncompleteRevisionException( 'timestamp field must not be NULL!' )
  448. ];
  449. yield 'no comment' => [
  450. [
  451. 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
  452. 'timestamp' => '20171117010101',
  453. 'user' => true,
  454. ],
  455. new IncompleteRevisionException( 'comment must not be NULL!' )
  456. ];
  457. yield 'no user' => [
  458. [
  459. 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ),
  460. 'comment' => $this->getRandomCommentStoreComment(),
  461. 'timestamp' => '20171117010101',
  462. ],
  463. new IncompleteRevisionException( 'user must not be NULL!' )
  464. ];
  465. }
  466. /**
  467. * @dataProvider provideInsertRevisionOn_failures
  468. * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
  469. */
  470. public function testInsertRevisionOn_failures(
  471. array $revDetails = [],
  472. Exception $exception
  473. ) {
  474. $rev = $this->getRevisionRecordFromDetailsArray( $revDetails );
  475. $store = MediaWikiServices::getInstance()->getRevisionStore();
  476. $this->setExpectedException(
  477. get_class( $exception ),
  478. $exception->getMessage(),
  479. $exception->getCode()
  480. );
  481. $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) );
  482. }
  483. public function provideNewNullRevision() {
  484. yield [
  485. Title::newFromText( 'UTPage_notAutoCreated' ),
  486. [ 'content' => [ 'main' => new WikitextContent( 'Flubber1' ) ] ],
  487. CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment1' ),
  488. true,
  489. ];
  490. yield [
  491. Title::newFromText( 'UTPage_notAutoCreated' ),
  492. [ 'content' => [ 'main' => new WikitextContent( 'Flubber2' ) ] ],
  493. CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment2', [ 'a' => 1 ] ),
  494. false,
  495. ];
  496. }
  497. /**
  498. * @dataProvider provideNewNullRevision
  499. * @covers \MediaWiki\Storage\RevisionStore::newNullRevision
  500. * @covers \MediaWiki\Storage\RevisionStore::findSlotContentId
  501. */
  502. public function testNewNullRevision( Title $title, $revDetails, $comment, $minor = false ) {
  503. $this->overrideMwServices();
  504. $user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser();
  505. $page = WikiPage::factory( $title );
  506. if ( !$page->exists() ) {
  507. $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__, EDIT_NEW );
  508. }
  509. $revDetails['page'] = $page->getId();
  510. $revDetails['timestamp'] = wfTimestampNow();
  511. $revDetails['comment'] = CommentStoreComment::newUnsavedComment( 'Base' );
  512. $revDetails['user'] = $user;
  513. $baseRev = $this->getRevisionRecordFromDetailsArray( $revDetails );
  514. $store = MediaWikiServices::getInstance()->getRevisionStore();
  515. $dbw = wfGetDB( DB_MASTER );
  516. $baseRev = $store->insertRevisionOn( $baseRev, $dbw );
  517. $page->updateRevisionOn( $dbw, new Revision( $baseRev ), $page->getLatest() );
  518. $record = $store->newNullRevision(
  519. wfGetDB( DB_MASTER ),
  520. $title,
  521. $comment,
  522. $minor,
  523. $user
  524. );
  525. $this->assertEquals( $title->getNamespace(), $record->getPageAsLinkTarget()->getNamespace() );
  526. $this->assertEquals( $title->getDBkey(), $record->getPageAsLinkTarget()->getDBkey() );
  527. $this->assertEquals( $comment, $record->getComment() );
  528. $this->assertEquals( $minor, $record->isMinor() );
  529. $this->assertEquals( $user->getName(), $record->getUser()->getName() );
  530. $this->assertEquals( $baseRev->getId(), $record->getParentId() );
  531. $this->assertArrayEquals(
  532. $baseRev->getSlotRoles(),
  533. $record->getSlotRoles()
  534. );
  535. foreach ( $baseRev->getSlotRoles() as $role ) {
  536. $parentSlot = $baseRev->getSlot( $role );
  537. $slot = $record->getSlot( $role );
  538. $this->assertTrue( $slot->isInherited(), 'isInherited' );
  539. $this->assertSame( $parentSlot->getOrigin(), $slot->getOrigin(), 'getOrigin' );
  540. $this->assertSameSlotContent( $parentSlot, $slot );
  541. }
  542. }
  543. /**
  544. * @covers \MediaWiki\Storage\RevisionStore::newNullRevision
  545. */
  546. public function testNewNullRevision_nonExistingTitle() {
  547. $store = MediaWikiServices::getInstance()->getRevisionStore();
  548. $record = $store->newNullRevision(
  549. wfGetDB( DB_MASTER ),
  550. Title::newFromText( __METHOD__ . '.iDontExist!' ),
  551. CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment' ),
  552. false,
  553. TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser()
  554. );
  555. $this->assertNull( $record );
  556. }
  557. /**
  558. * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled
  559. */
  560. public function testGetRcIdIfUnpatrolled_returnsRecentChangesId() {
  561. $page = $this->getTestPage();
  562. $status = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
  563. /** @var Revision $rev */
  564. $rev = $status->value['revision'];
  565. $store = MediaWikiServices::getInstance()->getRevisionStore();
  566. $revisionRecord = $store->getRevisionById( $rev->getId() );
  567. $result = $store->getRcIdIfUnpatrolled( $revisionRecord );
  568. $this->assertGreaterThan( 0, $result );
  569. $this->assertSame(
  570. $store->getRecentChange( $revisionRecord )->getAttribute( 'rc_id' ),
  571. $result
  572. );
  573. }
  574. /**
  575. * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled
  576. */
  577. public function testGetRcIdIfUnpatrolled_returnsZeroIfPatrolled() {
  578. // This assumes that sysops are auto patrolled
  579. $sysop = $this->getTestSysop()->getUser();
  580. $page = $this->getTestPage();
  581. $status = $page->doEditContent(
  582. new WikitextContent( __METHOD__ ),
  583. __METHOD__,
  584. 0,
  585. false,
  586. $sysop
  587. );
  588. /** @var Revision $rev */
  589. $rev = $status->value['revision'];
  590. $store = MediaWikiServices::getInstance()->getRevisionStore();
  591. $revisionRecord = $store->getRevisionById( $rev->getId() );
  592. $result = $store->getRcIdIfUnpatrolled( $revisionRecord );
  593. $this->assertSame( 0, $result );
  594. }
  595. /**
  596. * @covers \MediaWiki\Storage\RevisionStore::getRecentChange
  597. */
  598. public function testGetRecentChange() {
  599. $page = $this->getTestPage();
  600. $content = new WikitextContent( __METHOD__ );
  601. $status = $page->doEditContent( $content, __METHOD__ );
  602. /** @var Revision $rev */
  603. $rev = $status->value['revision'];
  604. $store = MediaWikiServices::getInstance()->getRevisionStore();
  605. $revRecord = $store->getRevisionById( $rev->getId() );
  606. $recentChange = $store->getRecentChange( $revRecord );
  607. $this->assertEquals( $rev->getId(), $recentChange->getAttribute( 'rc_this_oldid' ) );
  608. $this->assertEquals( $rev->getRecentChange(), $recentChange );
  609. }
  610. /**
  611. * @covers \MediaWiki\Storage\RevisionStore::getRevisionById
  612. */
  613. public function testGetRevisionById() {
  614. $page = $this->getTestPage();
  615. $content = new WikitextContent( __METHOD__ );
  616. $status = $page->doEditContent( $content, __METHOD__ );
  617. /** @var Revision $rev */
  618. $rev = $status->value['revision'];
  619. $store = MediaWikiServices::getInstance()->getRevisionStore();
  620. $revRecord = $store->getRevisionById( $rev->getId() );
  621. $this->assertSame( $rev->getId(), $revRecord->getId() );
  622. $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) );
  623. $this->assertSame( __METHOD__, $revRecord->getComment()->text );
  624. }
  625. /**
  626. * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTitle
  627. */
  628. public function testGetRevisionByTitle() {
  629. $page = $this->getTestPage();
  630. $content = new WikitextContent( __METHOD__ );
  631. $status = $page->doEditContent( $content, __METHOD__ );
  632. /** @var Revision $rev */
  633. $rev = $status->value['revision'];
  634. $store = MediaWikiServices::getInstance()->getRevisionStore();
  635. $revRecord = $store->getRevisionByTitle( $page->getTitle() );
  636. $this->assertSame( $rev->getId(), $revRecord->getId() );
  637. $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) );
  638. $this->assertSame( __METHOD__, $revRecord->getComment()->text );
  639. }
  640. /**
  641. * @covers \MediaWiki\Storage\RevisionStore::getRevisionByPageId
  642. */
  643. public function testGetRevisionByPageId() {
  644. $page = $this->getTestPage();
  645. $content = new WikitextContent( __METHOD__ );
  646. $status = $page->doEditContent( $content, __METHOD__ );
  647. /** @var Revision $rev */
  648. $rev = $status->value['revision'];
  649. $store = MediaWikiServices::getInstance()->getRevisionStore();
  650. $revRecord = $store->getRevisionByPageId( $page->getId() );
  651. $this->assertSame( $rev->getId(), $revRecord->getId() );
  652. $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) );
  653. $this->assertSame( __METHOD__, $revRecord->getComment()->text );
  654. }
  655. /**
  656. * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTimestamp
  657. */
  658. public function testGetRevisionByTimestamp() {
  659. // Make sure there is 1 second between the last revision and the rev we create...
  660. // Otherwise we might not get the correct revision and the test may fail...
  661. // :(
  662. $page = $this->getTestPage();
  663. sleep( 1 );
  664. $content = new WikitextContent( __METHOD__ );
  665. $status = $page->doEditContent( $content, __METHOD__ );
  666. /** @var Revision $rev */
  667. $rev = $status->value['revision'];
  668. $store = MediaWikiServices::getInstance()->getRevisionStore();
  669. $revRecord = $store->getRevisionByTimestamp(
  670. $page->getTitle(),
  671. $rev->getTimestamp()
  672. );
  673. $this->assertSame( $rev->getId(), $revRecord->getId() );
  674. $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) );
  675. $this->assertSame( __METHOD__, $revRecord->getComment()->text );
  676. }
  677. protected function revisionToRow( Revision $rev, $options = [ 'page', 'user', 'comment' ] ) {
  678. // XXX: the WikiPage object loads another RevisionRecord from the database. Not great.
  679. $page = WikiPage::factory( $rev->getTitle() );
  680. $fields = [
  681. 'rev_id' => (string)$rev->getId(),
  682. 'rev_page' => (string)$rev->getPage(),
  683. 'rev_timestamp' => $this->db->timestamp( $rev->getTimestamp() ),
  684. 'rev_user_text' => (string)$rev->getUserText(),
  685. 'rev_user' => (string)$rev->getUser(),
  686. 'rev_minor_edit' => $rev->isMinor() ? '1' : '0',
  687. 'rev_deleted' => (string)$rev->getVisibility(),
  688. 'rev_len' => (string)$rev->getSize(),
  689. 'rev_parent_id' => (string)$rev->getParentId(),
  690. 'rev_sha1' => (string)$rev->getSha1(),
  691. ];
  692. if ( in_array( 'page', $options ) ) {
  693. $fields += [
  694. 'page_namespace' => (string)$page->getTitle()->getNamespace(),
  695. 'page_title' => $page->getTitle()->getDBkey(),
  696. 'page_id' => (string)$page->getId(),
  697. 'page_latest' => (string)$page->getLatest(),
  698. 'page_is_redirect' => $page->isRedirect() ? '1' : '0',
  699. 'page_len' => (string)$page->getContent()->getSize(),
  700. ];
  701. }
  702. if ( in_array( 'user', $options ) ) {
  703. $fields += [
  704. 'user_name' => (string)$rev->getUserText(),
  705. ];
  706. }
  707. if ( in_array( 'comment', $options ) ) {
  708. $fields += [
  709. 'rev_comment_text' => $rev->getComment(),
  710. 'rev_comment_data' => null,
  711. 'rev_comment_cid' => null,
  712. ];
  713. }
  714. if ( $rev->getId() ) {
  715. $fields += [
  716. 'rev_id' => (string)$rev->getId(),
  717. ];
  718. }
  719. return (object)$fields;
  720. }
  721. protected function assertRevisionRecordMatchesRevision(
  722. Revision $rev,
  723. RevisionRecord $record
  724. ) {
  725. $this->assertSame( $rev->getId(), $record->getId() );
  726. $this->assertSame( $rev->getPage(), $record->getPageId() );
  727. $this->assertSame( $rev->getTimestamp(), $record->getTimestamp() );
  728. $this->assertSame( $rev->getUserText(), $record->getUser()->getName() );
  729. $this->assertSame( $rev->getUser(), $record->getUser()->getId() );
  730. $this->assertSame( $rev->isMinor(), $record->isMinor() );
  731. $this->assertSame( $rev->getVisibility(), $record->getVisibility() );
  732. $this->assertSame( $rev->getSize(), $record->getSize() );
  733. /**
  734. * @note As of MW 1.31, the database schema allows the parent ID to be
  735. * NULL to indicate that it is unknown.
  736. */
  737. $expectedParent = $rev->getParentId();
  738. if ( $expectedParent === null ) {
  739. $expectedParent = 0;
  740. }
  741. $this->assertSame( $expectedParent, $record->getParentId() );
  742. $this->assertSame( $rev->getSha1(), $record->getSha1() );
  743. $this->assertSame( $rev->getComment(), $record->getComment()->text );
  744. $this->assertSame( $rev->getContentFormat(), $record->getContent( 'main' )->getDefaultFormat() );
  745. $this->assertSame( $rev->getContentModel(), $record->getContent( 'main' )->getModel() );
  746. $this->assertLinkTargetsEqual( $rev->getTitle(), $record->getPageAsLinkTarget() );
  747. $revRec = $rev->getRevisionRecord();
  748. $revMain = $revRec->getSlot( 'main' );
  749. $recMain = $record->getSlot( 'main' );
  750. $this->assertSame( $revMain->hasOrigin(), $recMain->hasOrigin(), 'hasOrigin' );
  751. $this->assertSame( $revMain->hasAddress(), $recMain->hasAddress(), 'hasAddress' );
  752. $this->assertSame( $revMain->hasContentId(), $recMain->hasContentId(), 'hasContentId' );
  753. if ( $revMain->hasOrigin() ) {
  754. $this->assertSame( $revMain->getOrigin(), $recMain->getOrigin(), 'getOrigin' );
  755. }
  756. if ( $revMain->hasAddress() ) {
  757. $this->assertSame( $revMain->getAddress(), $recMain->getAddress(), 'getAddress' );
  758. }
  759. if ( $revMain->hasContentId() ) {
  760. $this->assertSame( $revMain->getContentId(), $recMain->getContentId(), 'getContentId' );
  761. }
  762. }
  763. /**
  764. * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
  765. * @covers \MediaWiki\Storage\RevisionStore::getQueryInfo
  766. */
  767. public function testNewRevisionFromRow_getQueryInfo() {
  768. $page = $this->getTestPage();
  769. $text = __METHOD__ . 'a-ä';
  770. /** @var Revision $rev */
  771. $rev = $page->doEditContent(
  772. new WikitextContent( $text ),
  773. __METHOD__ . 'a'
  774. )->value['revision'];
  775. $store = MediaWikiServices::getInstance()->getRevisionStore();
  776. $info = $store->getQueryInfo();
  777. $row = $this->db->selectRow(
  778. $info['tables'],
  779. $info['fields'],
  780. [ 'rev_id' => $rev->getId() ],
  781. __METHOD__,
  782. [],
  783. $info['joins']
  784. );
  785. $record = $store->newRevisionFromRow(
  786. $row,
  787. [],
  788. $page->getTitle()
  789. );
  790. $this->assertRevisionRecordMatchesRevision( $rev, $record );
  791. $this->assertSame( $text, $rev->getContent()->serialize() );
  792. }
  793. /**
  794. * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
  795. */
  796. public function testNewRevisionFromRow_anonEdit() {
  797. $page = $this->getTestPage();
  798. $text = __METHOD__ . 'a-ä';
  799. /** @var Revision $rev */
  800. $rev = $page->doEditContent(
  801. new WikitextContent( $text ),
  802. __METHOD__ . 'a'
  803. )->value['revision'];
  804. $store = MediaWikiServices::getInstance()->getRevisionStore();
  805. $record = $store->newRevisionFromRow(
  806. $this->revisionToRow( $rev ),
  807. [],
  808. $page->getTitle()
  809. );
  810. $this->assertRevisionRecordMatchesRevision( $rev, $record );
  811. $this->assertSame( $text, $rev->getContent()->serialize() );
  812. }
  813. /**
  814. * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
  815. */
  816. public function testNewRevisionFromRow_anonEdit_legacyEncoding() {
  817. $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
  818. $this->overrideMwServices();
  819. $page = $this->getTestPage();
  820. $text = __METHOD__ . 'a-ä';
  821. /** @var Revision $rev */
  822. $rev = $page->doEditContent(
  823. new WikitextContent( $text ),
  824. __METHOD__ . 'a'
  825. )->value['revision'];
  826. $store = MediaWikiServices::getInstance()->getRevisionStore();
  827. $record = $store->newRevisionFromRow(
  828. $this->revisionToRow( $rev ),
  829. [],
  830. $page->getTitle()
  831. );
  832. $this->assertRevisionRecordMatchesRevision( $rev, $record );
  833. $this->assertSame( $text, $rev->getContent()->serialize() );
  834. }
  835. /**
  836. * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
  837. */
  838. public function testNewRevisionFromRow_userEdit() {
  839. $page = $this->getTestPage();
  840. $text = __METHOD__ . 'b-ä';
  841. /** @var Revision $rev */
  842. $rev = $page->doEditContent(
  843. new WikitextContent( $text ),
  844. __METHOD__ . 'b',
  845. 0,
  846. false,
  847. $this->getTestUser()->getUser()
  848. )->value['revision'];
  849. $store = MediaWikiServices::getInstance()->getRevisionStore();
  850. $record = $store->newRevisionFromRow(
  851. $this->revisionToRow( $rev ),
  852. [],
  853. $page->getTitle()
  854. );
  855. $this->assertRevisionRecordMatchesRevision( $rev, $record );
  856. $this->assertSame( $text, $rev->getContent()->serialize() );
  857. }
  858. /**
  859. * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow
  860. * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo
  861. */
  862. public function testNewRevisionFromArchiveRow_getArchiveQueryInfo() {
  863. $store = MediaWikiServices::getInstance()->getRevisionStore();
  864. $title = Title::newFromText( __METHOD__ );
  865. $text = __METHOD__ . '-bä';
  866. $page = WikiPage::factory( $title );
  867. /** @var Revision $orig */
  868. $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ )
  869. ->value['revision'];
  870. $page->doDeleteArticle( __METHOD__ );
  871. $db = wfGetDB( DB_MASTER );
  872. $arQuery = $store->getArchiveQueryInfo();
  873. $res = $db->select(
  874. $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
  875. __METHOD__, [], $arQuery['joins']
  876. );
  877. $this->assertTrue( is_object( $res ), 'query failed' );
  878. $row = $res->fetchObject();
  879. $res->free();
  880. $record = $store->newRevisionFromArchiveRow( $row );
  881. $this->assertRevisionRecordMatchesRevision( $orig, $record );
  882. $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
  883. }
  884. /**
  885. * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow
  886. */
  887. public function testNewRevisionFromArchiveRow_legacyEncoding() {
  888. $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' );
  889. $this->overrideMwServices();
  890. $store = MediaWikiServices::getInstance()->getRevisionStore();
  891. $title = Title::newFromText( __METHOD__ );
  892. $text = __METHOD__ . '-bä';
  893. $page = WikiPage::factory( $title );
  894. /** @var Revision $orig */
  895. $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ )
  896. ->value['revision'];
  897. $page->doDeleteArticle( __METHOD__ );
  898. $db = wfGetDB( DB_MASTER );
  899. $arQuery = $store->getArchiveQueryInfo();
  900. $res = $db->select(
  901. $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
  902. __METHOD__, [], $arQuery['joins']
  903. );
  904. $this->assertTrue( is_object( $res ), 'query failed' );
  905. $row = $res->fetchObject();
  906. $res->free();
  907. $record = $store->newRevisionFromArchiveRow( $row );
  908. $this->assertRevisionRecordMatchesRevision( $orig, $record );
  909. $this->assertSame( $text, $record->getContent( 'main' )->serialize() );
  910. }
  911. /**
  912. * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow
  913. */
  914. public function testNewRevisionFromArchiveRow_no_user() {
  915. $store = MediaWikiServices::getInstance()->getRevisionStore();
  916. $row = (object)[
  917. 'ar_id' => '1',
  918. 'ar_page_id' => '2',
  919. 'ar_namespace' => '0',
  920. 'ar_title' => 'Something',
  921. 'ar_rev_id' => '2',
  922. 'ar_text_id' => '47',
  923. 'ar_timestamp' => '20180528192356',
  924. 'ar_minor_edit' => '0',
  925. 'ar_deleted' => '0',
  926. 'ar_len' => '78',
  927. 'ar_parent_id' => '0',
  928. 'ar_sha1' => 'deadbeef',
  929. 'ar_comment_text' => 'whatever',
  930. 'ar_comment_data' => null,
  931. 'ar_comment_cid' => null,
  932. 'ar_user' => '0',
  933. 'ar_user_text' => '', // this is the important bit
  934. 'ar_actor' => null,
  935. 'ar_content_format' => null,
  936. 'ar_content_model' => null,
  937. ];
  938. \Wikimedia\suppressWarnings();
  939. $record = $store->newRevisionFromArchiveRow( $row );
  940. \Wikimedia\suppressWarnings( true );
  941. $this->assertInstanceOf( RevisionRecord::class, $record );
  942. $this->assertInstanceOf( UserIdentityValue::class, $record->getUser() );
  943. $this->assertSame( 'Unknown user', $record->getUser()->getName() );
  944. }
  945. /**
  946. * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow
  947. */
  948. public function testNewRevisionFromRow_no_user() {
  949. $store = MediaWikiServices::getInstance()->getRevisionStore();
  950. $title = Title::newFromText( __METHOD__ );
  951. $row = (object)[
  952. 'rev_id' => '2',
  953. 'rev_page' => '2',
  954. 'page_namespace' => '0',
  955. 'page_title' => $title->getText(),
  956. 'rev_text_id' => '47',
  957. 'rev_timestamp' => '20180528192356',
  958. 'rev_minor_edit' => '0',
  959. 'rev_deleted' => '0',
  960. 'rev_len' => '78',
  961. 'rev_parent_id' => '0',
  962. 'rev_sha1' => 'deadbeef',
  963. 'rev_comment_text' => 'whatever',
  964. 'rev_comment_data' => null,
  965. 'rev_comment_cid' => null,
  966. 'rev_user' => '0',
  967. 'rev_user_text' => '', // this is the important bit
  968. 'rev_actor' => null,
  969. 'rev_content_format' => null,
  970. 'rev_content_model' => null,
  971. ];
  972. \Wikimedia\suppressWarnings();
  973. $record = $store->newRevisionFromRow( $row, 0, $title );
  974. \Wikimedia\suppressWarnings( true );
  975. $this->assertNotNull( $record );
  976. $this->assertNotNull( $record->getUser() );
  977. $this->assertNotEmpty( $record->getUser()->getName() );
  978. }
  979. /**
  980. * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn
  981. */
  982. public function testInsertRevisionOn_archive() {
  983. // This is a round trip test for deletion and undeletion of a
  984. // revision row via the archive table.
  985. $store = MediaWikiServices::getInstance()->getRevisionStore();
  986. $title = Title::newFromText( __METHOD__ );
  987. $page = WikiPage::factory( $title );
  988. /** @var Revision $origRev */
  989. $page->doEditContent( new WikitextContent( "First" ), __METHOD__ . '-first' );
  990. $origRev = $page->doEditContent( new WikitextContent( "Foo" ), __METHOD__ )
  991. ->value['revision'];
  992. $orig = $origRev->getRevisionRecord();
  993. $page->doDeleteArticle( __METHOD__ );
  994. // re-create page, so we can later load revisions for it
  995. $page->doEditContent( new WikitextContent( 'Two' ), __METHOD__ );
  996. $db = wfGetDB( DB_MASTER );
  997. $arQuery = $store->getArchiveQueryInfo();
  998. $row = $db->selectRow(
  999. $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
  1000. __METHOD__, [], $arQuery['joins']
  1001. );
  1002. $this->assertNotFalse( $row, 'query failed' );
  1003. $record = $store->newRevisionFromArchiveRow(
  1004. $row,
  1005. 0,
  1006. $title,
  1007. [ 'page_id' => $title->getArticleID() ]
  1008. );
  1009. $restored = $store->insertRevisionOn( $record, $db );
  1010. // is the new revision correct?
  1011. $this->assertRevisionCompleteness( $restored );
  1012. $this->assertRevisionRecordsEqual( $record, $restored );
  1013. // does the new revision use the original slot?
  1014. $recMain = $record->getSlot( 'main' );
  1015. $restMain = $restored->getSlot( 'main' );
  1016. $this->assertSame( $recMain->getAddress(), $restMain->getAddress() );
  1017. $this->assertSame( $recMain->getContentId(), $restMain->getContentId() );
  1018. $this->assertSame( $recMain->getOrigin(), $restMain->getOrigin() );
  1019. $this->assertSame( 'Foo', $restMain->getContent()->serialize() );
  1020. // can we load it from the store?
  1021. $loaded = $store->getRevisionById( $restored->getId() );
  1022. $this->assertNotNull( $loaded );
  1023. $this->assertRevisionCompleteness( $loaded );
  1024. $this->assertRevisionRecordsEqual( $restored, $loaded );
  1025. // can we find it directly in the database?
  1026. $this->assertRevisionExistsInDatabase( $restored );
  1027. }
  1028. /**
  1029. * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromId
  1030. */
  1031. public function testLoadRevisionFromId() {
  1032. $title = Title::newFromText( __METHOD__ );
  1033. $page = WikiPage::factory( $title );
  1034. /** @var Revision $rev */
  1035. $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
  1036. ->value['revision'];
  1037. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1038. $result = $store->loadRevisionFromId( wfGetDB( DB_MASTER ), $rev->getId() );
  1039. $this->assertRevisionRecordMatchesRevision( $rev, $result );
  1040. }
  1041. /**
  1042. * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromPageId
  1043. */
  1044. public function testLoadRevisionFromPageId() {
  1045. $title = Title::newFromText( __METHOD__ );
  1046. $page = WikiPage::factory( $title );
  1047. /** @var Revision $rev */
  1048. $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
  1049. ->value['revision'];
  1050. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1051. $result = $store->loadRevisionFromPageId( wfGetDB( DB_MASTER ), $page->getId() );
  1052. $this->assertRevisionRecordMatchesRevision( $rev, $result );
  1053. }
  1054. /**
  1055. * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTitle
  1056. */
  1057. public function testLoadRevisionFromTitle() {
  1058. $title = Title::newFromText( __METHOD__ );
  1059. $page = WikiPage::factory( $title );
  1060. /** @var Revision $rev */
  1061. $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
  1062. ->value['revision'];
  1063. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1064. $result = $store->loadRevisionFromTitle( wfGetDB( DB_MASTER ), $title );
  1065. $this->assertRevisionRecordMatchesRevision( $rev, $result );
  1066. }
  1067. /**
  1068. * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTimestamp
  1069. */
  1070. public function testLoadRevisionFromTimestamp() {
  1071. $title = Title::newFromText( __METHOD__ );
  1072. $page = WikiPage::factory( $title );
  1073. /** @var Revision $revOne */
  1074. $revOne = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
  1075. ->value['revision'];
  1076. // Sleep to ensure different timestamps... )(evil)
  1077. sleep( 1 );
  1078. /** @var Revision $revTwo */
  1079. $revTwo = $page->doEditContent( new WikitextContent( __METHOD__ . 'a' ), '' )
  1080. ->value['revision'];
  1081. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1082. $this->assertNull(
  1083. $store->loadRevisionFromTimestamp( wfGetDB( DB_MASTER ), $title, '20150101010101' )
  1084. );
  1085. $this->assertSame(
  1086. $revOne->getId(),
  1087. $store->loadRevisionFromTimestamp(
  1088. wfGetDB( DB_MASTER ),
  1089. $title,
  1090. $revOne->getTimestamp()
  1091. )->getId()
  1092. );
  1093. $this->assertSame(
  1094. $revTwo->getId(),
  1095. $store->loadRevisionFromTimestamp(
  1096. wfGetDB( DB_MASTER ),
  1097. $title,
  1098. $revTwo->getTimestamp()
  1099. )->getId()
  1100. );
  1101. }
  1102. /**
  1103. * @covers \MediaWiki\Storage\RevisionStore::listRevisionSizes
  1104. */
  1105. public function testGetParentLengths() {
  1106. $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
  1107. /** @var Revision $revOne */
  1108. $revOne = $page->doEditContent(
  1109. new WikitextContent( __METHOD__ ), __METHOD__
  1110. )->value['revision'];
  1111. /** @var Revision $revTwo */
  1112. $revTwo = $page->doEditContent(
  1113. new WikitextContent( __METHOD__ . '2' ), __METHOD__
  1114. )->value['revision'];
  1115. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1116. $this->assertSame(
  1117. [
  1118. $revOne->getId() => strlen( __METHOD__ ),
  1119. ],
  1120. $store->listRevisionSizes(
  1121. wfGetDB( DB_MASTER ),
  1122. [ $revOne->getId() ]
  1123. )
  1124. );
  1125. $this->assertSame(
  1126. [
  1127. $revOne->getId() => strlen( __METHOD__ ),
  1128. $revTwo->getId() => strlen( __METHOD__ ) + 1,
  1129. ],
  1130. $store->listRevisionSizes(
  1131. wfGetDB( DB_MASTER ),
  1132. [ $revOne->getId(), $revTwo->getId() ]
  1133. )
  1134. );
  1135. }
  1136. /**
  1137. * @covers \MediaWiki\Storage\RevisionStore::getPreviousRevision
  1138. */
  1139. public function testGetPreviousRevision() {
  1140. $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
  1141. /** @var Revision $revOne */
  1142. $revOne = $page->doEditContent(
  1143. new WikitextContent( __METHOD__ ), __METHOD__
  1144. )->value['revision'];
  1145. /** @var Revision $revTwo */
  1146. $revTwo = $page->doEditContent(
  1147. new WikitextContent( __METHOD__ . '2' ), __METHOD__
  1148. )->value['revision'];
  1149. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1150. $this->assertNull(
  1151. $store->getPreviousRevision( $store->getRevisionById( $revOne->getId() ) )
  1152. );
  1153. $this->assertSame(
  1154. $revOne->getId(),
  1155. $store->getPreviousRevision( $store->getRevisionById( $revTwo->getId() ) )->getId()
  1156. );
  1157. }
  1158. /**
  1159. * @covers \MediaWiki\Storage\RevisionStore::getNextRevision
  1160. */
  1161. public function testGetNextRevision() {
  1162. $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
  1163. /** @var Revision $revOne */
  1164. $revOne = $page->doEditContent(
  1165. new WikitextContent( __METHOD__ ), __METHOD__
  1166. )->value['revision'];
  1167. /** @var Revision $revTwo */
  1168. $revTwo = $page->doEditContent(
  1169. new WikitextContent( __METHOD__ . '2' ), __METHOD__
  1170. )->value['revision'];
  1171. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1172. $this->assertSame(
  1173. $revTwo->getId(),
  1174. $store->getNextRevision( $store->getRevisionById( $revOne->getId() ) )->getId()
  1175. );
  1176. $this->assertNull(
  1177. $store->getNextRevision( $store->getRevisionById( $revTwo->getId() ) )
  1178. );
  1179. }
  1180. /**
  1181. * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId
  1182. */
  1183. public function testGetTimestampFromId_found() {
  1184. $page = $this->getTestPage();
  1185. /** @var Revision $rev */
  1186. $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
  1187. ->value['revision'];
  1188. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1189. $result = $store->getTimestampFromId(
  1190. $page->getTitle(),
  1191. $rev->getId()
  1192. );
  1193. $this->assertSame( $rev->getTimestamp(), $result );
  1194. }
  1195. /**
  1196. * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId
  1197. */
  1198. public function testGetTimestampFromId_notFound() {
  1199. $page = $this->getTestPage();
  1200. /** @var Revision $rev */
  1201. $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ )
  1202. ->value['revision'];
  1203. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1204. $result = $store->getTimestampFromId(
  1205. $page->getTitle(),
  1206. $rev->getId() + 1
  1207. );
  1208. $this->assertFalse( $result );
  1209. }
  1210. /**
  1211. * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByPageId
  1212. */
  1213. public function testCountRevisionsByPageId() {
  1214. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1215. $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
  1216. $this->assertSame(
  1217. 0,
  1218. $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
  1219. );
  1220. $page->doEditContent( new WikitextContent( 'a' ), 'a' );
  1221. $this->assertSame(
  1222. 1,
  1223. $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
  1224. );
  1225. $page->doEditContent( new WikitextContent( 'b' ), 'b' );
  1226. $this->assertSame(
  1227. 2,
  1228. $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() )
  1229. );
  1230. }
  1231. /**
  1232. * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByTitle
  1233. */
  1234. public function testCountRevisionsByTitle() {
  1235. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1236. $page = WikiPage::factory( Title::newFromText( __METHOD__ ) );
  1237. $this->assertSame(
  1238. 0,
  1239. $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
  1240. );
  1241. $page->doEditContent( new WikitextContent( 'a' ), 'a' );
  1242. $this->assertSame(
  1243. 1,
  1244. $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
  1245. );
  1246. $page->doEditContent( new WikitextContent( 'b' ), 'b' );
  1247. $this->assertSame(
  1248. 2,
  1249. $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() )
  1250. );
  1251. }
  1252. /**
  1253. * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit
  1254. */
  1255. public function testUserWasLastToEdit_false() {
  1256. $sysop = $this->getTestSysop()->getUser();
  1257. $page = $this->getTestPage();
  1258. $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
  1259. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1260. $result = $store->userWasLastToEdit(
  1261. wfGetDB( DB_MASTER ),
  1262. $page->getId(),
  1263. $sysop->getId(),
  1264. '20160101010101'
  1265. );
  1266. $this->assertFalse( $result );
  1267. }
  1268. /**
  1269. * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit
  1270. */
  1271. public function testUserWasLastToEdit_true() {
  1272. $startTime = wfTimestampNow();
  1273. $sysop = $this->getTestSysop()->getUser();
  1274. $page = $this->getTestPage();
  1275. $page->doEditContent(
  1276. new WikitextContent( __METHOD__ ),
  1277. __METHOD__,
  1278. 0,
  1279. false,
  1280. $sysop
  1281. );
  1282. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1283. $result = $store->userWasLastToEdit(
  1284. wfGetDB( DB_MASTER ),
  1285. $page->getId(),
  1286. $sysop->getId(),
  1287. $startTime
  1288. );
  1289. $this->assertTrue( $result );
  1290. }
  1291. /**
  1292. * @covers \MediaWiki\Storage\RevisionStore::getKnownCurrentRevision
  1293. */
  1294. public function testGetKnownCurrentRevision() {
  1295. $page = $this->getTestPage();
  1296. /** @var Revision $rev */
  1297. $rev = $page->doEditContent(
  1298. new WikitextContent( __METHOD__ . 'b' ),
  1299. __METHOD__ . 'b',
  1300. 0,
  1301. false,
  1302. $this->getTestUser()->getUser()
  1303. )->value['revision'];
  1304. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1305. $record = $store->getKnownCurrentRevision(
  1306. $page->getTitle(),
  1307. $rev->getId()
  1308. );
  1309. $this->assertRevisionRecordMatchesRevision( $rev, $record );
  1310. }
  1311. public function provideNewMutableRevisionFromArray() {
  1312. yield 'Basic array, content object' => [
  1313. [
  1314. 'id' => 2,
  1315. 'page' => 1,
  1316. 'timestamp' => '20171017114835',
  1317. 'user_text' => '111.0.1.2',
  1318. 'user' => 0,
  1319. 'minor_edit' => false,
  1320. 'deleted' => 0,
  1321. 'len' => 46,
  1322. 'parent_id' => 1,
  1323. 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
  1324. 'comment' => 'Goat Comment!',
  1325. 'content' => new WikitextContent( 'Some Content' ),
  1326. ]
  1327. ];
  1328. yield 'Basic array, serialized text' => [
  1329. [
  1330. 'id' => 2,
  1331. 'page' => 1,
  1332. 'timestamp' => '20171017114835',
  1333. 'user_text' => '111.0.1.2',
  1334. 'user' => 0,
  1335. 'minor_edit' => false,
  1336. 'deleted' => 0,
  1337. 'len' => 46,
  1338. 'parent_id' => 1,
  1339. 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
  1340. 'comment' => 'Goat Comment!',
  1341. 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(),
  1342. ]
  1343. ];
  1344. yield 'Basic array, serialized text, utf-8 flags' => [
  1345. [
  1346. 'id' => 2,
  1347. 'page' => 1,
  1348. 'timestamp' => '20171017114835',
  1349. 'user_text' => '111.0.1.2',
  1350. 'user' => 0,
  1351. 'minor_edit' => false,
  1352. 'deleted' => 0,
  1353. 'len' => 46,
  1354. 'parent_id' => 1,
  1355. 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
  1356. 'comment' => 'Goat Comment!',
  1357. 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(),
  1358. 'flags' => 'utf-8',
  1359. ]
  1360. ];
  1361. yield 'Basic array, with title' => [
  1362. [
  1363. 'title' => Title::newFromText( 'SomeText' ),
  1364. 'timestamp' => '20171017114835',
  1365. 'user_text' => '111.0.1.2',
  1366. 'user' => 0,
  1367. 'minor_edit' => false,
  1368. 'deleted' => 0,
  1369. 'len' => 46,
  1370. 'parent_id' => 1,
  1371. 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
  1372. 'comment' => 'Goat Comment!',
  1373. 'content' => new WikitextContent( 'Some Content' ),
  1374. ]
  1375. ];
  1376. yield 'Basic array, no user field' => [
  1377. [
  1378. 'id' => 2,
  1379. 'page' => 1,
  1380. 'timestamp' => '20171017114835',
  1381. 'user_text' => '111.0.1.3',
  1382. 'minor_edit' => false,
  1383. 'deleted' => 0,
  1384. 'len' => 46,
  1385. 'parent_id' => 1,
  1386. 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z',
  1387. 'comment' => 'Goat Comment!',
  1388. 'content' => new WikitextContent( 'Some Content' ),
  1389. ]
  1390. ];
  1391. }
  1392. /**
  1393. * @dataProvider provideNewMutableRevisionFromArray
  1394. * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
  1395. */
  1396. public function testNewMutableRevisionFromArray( array $array ) {
  1397. $store = MediaWikiServices::getInstance()->getRevisionStore();
  1398. // HACK: if $array['page'] is given, make sure that the page exists
  1399. if ( isset( $array['page'] ) ) {
  1400. $t = Title::newFromID( $array['page'] );
  1401. if ( !$t || !$t->exists() ) {
  1402. $t = Title::makeTitle( NS_MAIN, __METHOD__ );
  1403. $info = $this->insertPage( $t );
  1404. $array['page'] = $info['id'];
  1405. }
  1406. }
  1407. $result = $store->newMutableRevisionFromArray( $array );
  1408. if ( isset( $array['id'] ) ) {
  1409. $this->assertSame( $array['id'], $result->getId() );
  1410. }
  1411. if ( isset( $array['page'] ) ) {
  1412. $this->assertSame( $array['page'], $result->getPageId() );
  1413. }
  1414. $this->assertSame( $array['timestamp'], $result->getTimestamp() );
  1415. $this->assertSame( $array['user_text'], $result->getUser()->getName() );
  1416. if ( isset( $array['user'] ) ) {
  1417. $this->assertSame( $array['user'], $result->getUser()->getId() );
  1418. }
  1419. $this->assertSame( (bool)$array['minor_edit'], $result->isMinor() );
  1420. $this->assertSame( $array['deleted'], $result->getVisibility() );
  1421. $this->assertSame( $array['len'], $result->getSize() );
  1422. $this->assertSame( $array['parent_id'], $result->getParentId() );
  1423. $this->assertSame( $array['sha1'], $result->getSha1() );
  1424. $this->assertSame( $array['comment'], $result->getComment()->text );
  1425. if ( isset( $array['content'] ) ) {
  1426. foreach ( $array['content'] as $role => $content ) {
  1427. $this->assertTrue(
  1428. $result->getContent( $role )->equals( $content )
  1429. );
  1430. }
  1431. } elseif ( isset( $array['text'] ) ) {
  1432. $this->assertSame( $array['text'], $result->getSlot( 'main' )->getContent()->serialize() );
  1433. } elseif ( isset( $array['content_format'] ) ) {
  1434. $this->assertSame(
  1435. $array['content_format'],
  1436. $result->getSlot( 'main' )->getContent()->getDefaultFormat()
  1437. );
  1438. $this->assertSame( $array['content_model'], $result->getSlot( 'main' )->getModel() );
  1439. }
  1440. }
  1441. /**
  1442. * @dataProvider provideNewMutableRevisionFromArray
  1443. * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray
  1444. */
  1445. public function testNewMutableRevisionFromArray_legacyEncoding( array $array ) {
  1446. $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
  1447. $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
  1448. $blobStore = new SqlBlobStore( $lb, $cache );
  1449. $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) );
  1450. $factory = $this->getMockBuilder( BlobStoreFactory::class )
  1451. ->setMethods( [ 'newBlobStore', 'newSqlBlobStore' ] )
  1452. ->disableOriginalConstructor()
  1453. ->getMock();
  1454. $factory->expects( $this->any() )
  1455. ->method( 'newBlobStore' )
  1456. ->willReturn( $blobStore );
  1457. $factory->expects( $this->any() )
  1458. ->method( 'newSqlBlobStore' )
  1459. ->willReturn( $blobStore );
  1460. $this->setService( 'BlobStoreFactory', $factory );
  1461. $this->testNewMutableRevisionFromArray( $array );
  1462. }
  1463. }