1<?php 2 3use MediaWiki\MediaWikiServices; 4 5/** 6 * @coversDefaultClass LocalRepo 7 * @group Database 8 */ 9class LocalRepoTest extends MediaWikiIntegrationTestCase { 10 /** 11 * @param array $extraInfo To pass to LocalRepo constructor 12 */ 13 private function newRepo( array $extraInfo = [] ) { 14 return new LocalRepo( $extraInfo + [ 15 'name' => 'local', 16 'backend' => 'local-backend', 17 ] ); 18 } 19 20 /** 21 * @param array $extraInfo To pass to constructor 22 * @param bool $expected 23 * @dataProvider provideHasSha1Storage 24 * @covers ::__construct 25 */ 26 public function testHasSha1Storage( array $extraInfo, $expected ) { 27 $this->assertSame( $expected, $this->newRepo( $extraInfo )->hasSha1Storage() ); 28 } 29 30 public static function provideHasSha1Storage() { 31 return [ 32 [ [], false ], 33 [ [ 'storageLayout' => 'sha256' ], false ], 34 [ [ 'storageLayout' => 'sha1' ], true ], 35 ]; 36 } 37 38 /** 39 * @param string $prefix 'img' or 'oi' 40 * @param string $expectedClass 'LocalFile' or 'OldLocalFile' 41 * @dataProvider provideNewFileFromRow 42 * @covers ::newFileFromRow 43 */ 44 public function testNewFileFromRow( $prefix, $expectedClass ) { 45 $this->editPage( 'File:Test_file', 'Some description' ); 46 47 $row = (object)[ 48 "{$prefix}_name" => 'Test_file', 49 // We cheat and include this for img_ too, it will be ignored 50 "{$prefix}_archive_name" => 'Archive_name', 51 "{$prefix}_user" => '1', 52 "{$prefix}_timestamp" => '12345678910111', 53 "{$prefix}_metadata" => '', 54 "{$prefix}_sha1" => sha1( '' ), 55 "{$prefix}_size" => '0', 56 "{$prefix}_height" => '0', 57 "{$prefix}_width" => '0', 58 "{$prefix}_bits" => '0', 59 "{$prefix}_description_text" => '', 60 "{$prefix}_description_data" => null, 61 ]; 62 $file = $this->newRepo()->newFileFromRow( $row ); 63 $this->assertInstanceOf( $expectedClass, $file ); 64 $this->assertSame( 'Test_file', $file->getName() ); 65 $this->assertSame( 1, $file->getUser( 'id' ) ); 66 } 67 68 public static function provideNewFileFromRow() { 69 return [ 70 'img' => [ 'img', LocalFile::class ], 71 'oi' => [ 'oi', OldLocalFile::class ], 72 ]; 73 } 74 75 /** 76 * @covers ::__construct 77 * @covers ::newFileFromRow 78 */ 79 public function testNewFileFromRow_invalid() { 80 $this->expectException( MWException::class ); 81 $this->expectExceptionMessage( 'LocalRepo::newFileFromRow: invalid row' ); 82 83 $row = (object)[ 84 "img_user" => '1', 85 "img_timestamp" => '12345678910111', 86 "img_metadata" => '', 87 "img_sha1" => sha1( '' ), 88 "img_size" => '0', 89 "img_height" => '0', 90 "img_width" => '0', 91 "img_bits" => '0', 92 ]; 93 $file = $this->newRepo()->newFileFromRow( $row ); 94 } 95 96 /** 97 * @covers ::__construct 98 * @covers ::newFromArchiveName 99 */ 100 public function testNewFromArchiveName() { 101 $this->editPage( 'File:Test_file', 'Some description' ); 102 103 $file = $this->newRepo()->newFromArchiveName( 'Test_file', 'b' ); 104 $this->assertInstanceOf( OldLocalFile::class, $file ); 105 $this->assertSame( 'Test_file', $file->getName() ); 106 } 107 108 // TODO cleanupDeletedBatch, deletedFileHasKey, hiddenFileHasKey 109 110 /** 111 * @covers ::__construct 112 * @covers ::cleanupDeletedBatch 113 */ 114 public function testCleanupDeletedBatch_sha1Storage() { 115 $this->assertEquals( Status::newGood(), 116 $this->newRepo( [ 'storageLayout' => 'sha1' ] )->cleanupDeletedBatch( [] ) ); 117 } 118 119 /** 120 * @param string $input 121 * @param string $expected 122 * @dataProvider provideGetHashFromKey 123 * @covers ::getHashFromKey 124 */ 125 public function testGetHashFromKey( $input, $expected ) { 126 $this->assertSame( $expected, LocalRepo::getHashFromKey( $input ) ); 127 } 128 129 public static function provideGetHashFromKey() { 130 return [ 131 [ '', false ], 132 [ '.', false ], 133 [ 'a.', 'a' ], 134 [ '.b', 'b' ], 135 [ '..c', 'c' ], 136 [ 'd.x', 'd' ], 137 [ '.e.x', 'e' ], 138 [ '..f.x', 'f' ], 139 [ 'g..x', 'g' ], 140 [ '01234567890123456789012345678901.x', '1234567890123456789012345678901' ], 141 ]; 142 } 143 144 /** 145 * @covers ::__construct 146 * @covers ::checkRedirect 147 */ 148 public function testCheckRedirect_nonRedirect() { 149 $this->editPage( 'File:Not a redirect', 'Not a redirect' ); 150 $this->assertFalse( 151 $this->newRepo()->checkRedirect( Title::makeTitle( NS_FILE, 'Not a redirect' ) ) ); 152 } 153 154 /** 155 * @covers ::__construct 156 * @covers ::checkRedirect 157 * @covers ::getSharedCacheKey 158 */ 159 public function testCheckRedirect_redirect() { 160 $this->editPage( 'File:Redirect', '#REDIRECT [[File:Target]]' ); 161 $this->assertEquals( 'File:Target', 162 $this->newRepo()->checkRedirect( Title::makeTitle( NS_FILE, 'Redirect' ) ) 163 ->getPrefixedText() ); 164 } 165 166 /** 167 * @covers ::__construct 168 * @covers ::checkRedirect 169 * @covers ::getSharedCacheKey 170 * @covers ::getLocalCacheKey 171 */ 172 public function testCheckRedirect_redirect_noWANCache() { 173 $this->markTestIncomplete( 'WANObjectCache::makeKey is final' ); 174 175 $mockWan = $this->getMockBuilder( WANObjectCache::class ) 176 ->setConstructorArgs( [ [ 'cache' => new EmptyBagOStuff ] ] ) 177 ->setMethods( [ 'makeKey' ] ) 178 ->getMock(); 179 $mockWan->expects( $this->exactly( 2 ) )->method( 'makeKey' )->withConsecutive( 180 [ 'file_redirect', md5( 'Redirect' ) ], 181 [ 'filerepo', 'local', 'file_redirect', md5( 'Redirect' ) ] 182 )->will( $this->onConsecutiveCalls( false, 'somekey' ) ); 183 184 $repo = $this->newRepo( [ 'wanCache' => $mockWan ] ); 185 186 $this->editPage( 'File:Redirect', '#REDIRECT [[File:Target]]' ); 187 $this->assertEquals( 'File:Target', 188 $repo->checkRedirect( Title::makeTitle( NS_FILE, 'Redirect' ) )->getPrefixedText() ); 189 } 190 191 /** 192 * @covers ::__construct 193 * @covers ::checkRedirect 194 */ 195 public function testCheckRedirect_invalidFile() { 196 $this->expectException( MWException::class ); 197 $this->expectExceptionMessage( '`Notafile` is not a valid file title.' ); 198 $this->newRepo()->checkRedirect( Title::makeTitle( NS_MAIN, 'Notafile' ) ); 199 } 200 201 /** 202 * @covers ::__construct 203 * @covers ::findBySha1 204 */ 205 public function testFindBySha1() { 206 $this->markTestIncomplete( "Haven't figured out how to upload files yet" ); 207 208 $repo = $this->newRepo(); 209 210 $tmpFileFactory = MediaWikiServices::getInstance()->getTempFSFileFactory(); 211 foreach ( [ 'File1', 'File2', 'File3' ] as $name ) { 212 $fsFile = $tmpFileFactory->newTempFSFile( '' ); 213 file_put_contents( $fsFile->getPath(), "$name contents" ); 214 $localFile = $repo->newFile( $name ); 215 $localFile->upload( $fsFile, 'Uploaded', "$name desc" ); 216 } 217 } 218 219 /** 220 * @covers ::__construct 221 * @covers ::getSharedCacheKey 222 * @covers ::checkRedirect 223 * @covers ::invalidateImageRedirect 224 */ 225 public function testInvalidateImageRedirect() { 226 global $wgTestMe; 227 $wgTestMe = true; 228 $repo = $this->newRepo( 229 [ 'wanCache' => new WANObjectCache( [ 'cache' => new HashBagOStuff ] ) ] ); 230 231 $title = Title::makeTitle( NS_FILE, 'Redirect' ); 232 233 $this->editPage( 'File:Redirect', '#REDIRECT [[File:Target]]' ); 234 235 $this->assertSame( 'File:Target', 236 $repo->checkRedirect( $title )->getPrefixedText() ); 237 238 $this->editPage( 'File:Redirect', 'No longer a redirect' ); 239 240 $this->assertSame( 'File:Target', 241 $repo->checkRedirect( $title )->getPrefixedText() ); 242 243 $repo->invalidateImageRedirect( $title ); 244 245 $this->markTestIncomplete( 246 "Can't figure out how to get image redirect validation to take effect" ); 247 248 $this->assertSame( false, $repo->checkRedirect( $title ) ); 249 } 250 251 /** 252 * @covers ::getInfo 253 */ 254 public function testGetInfo() { 255 $this->setMwGlobals( [ 256 'wgFavicon' => '//example.com/favicon.ico', 257 'wgSitename' => 'Test my site', 258 ] ); 259 260 $repo = $this->newRepo( [ 'favicon' => 'Hey, this option is ignored in LocalRepo!' ] ); 261 262 $this->assertSame( [ 263 'name' => 'local', 264 'displayname' => 'Test my site', 265 'rootUrl' => false, 266 'local' => true, 267 'url' => false, 268 'thumbUrl' => false, 269 'initialCapital' => true, 270 // XXX This assumes protocol-relative will get expanded to http instead of https 271 'favicon' => 'http://example.com/favicon.ico', 272 ], $repo->getInfo() ); 273 } 274 275 // XXX The following getInfo tests are really testing FileRepo, not LocalRepo, but we want to 276 // make sure they're true for LocalRepo too. How should we do this? A trait? 277 278 /** 279 * @covers ::getInfo 280 */ 281 public function testGetInfo_name() { 282 $this->assertSame( 'some-name', 283 $this->newRepo( [ 'name' => 'some-name' ] )->getInfo()['name'] ); 284 } 285 286 /** 287 * @covers ::getInfo 288 */ 289 public function testGetInfo_displayName() { 290 $this->assertSame( wfMessage( 'shared-repo' )->text(), 291 $this->newRepo( [ 'name' => 'not-local' ] )->getInfo()['displayname'] ); 292 } 293 294 /** 295 * @covers ::getInfo 296 */ 297 public function testGetInfo_displayNameCustomMsg() { 298 $this->editPage( 'MediaWiki:Shared-repo-name-not-local', 'Name to display please' ); 299 // Allow the message to take effect 300 MediaWikiServices::getInstance()->getMessageCache()->enable(); 301 302 $this->assertSame( 'Name to display please', 303 $this->newRepo( [ 'name' => 'not-local' ] )->getInfo()['displayname'] ); 304 } 305 306 /** 307 * @covers ::getInfo 308 */ 309 public function testGetInfo_rootUrl() { 310 $this->assertSame( 'https://my.url', 311 $this->newRepo( [ 'url' => 'https://my.url' ] )->getInfo()['rootUrl'] ); 312 } 313 314 /** 315 * @covers ::getInfo 316 */ 317 public function testGetInfo_rootUrlCustomized() { 318 $this->assertSame( 319 'https://my.url/some/sub/dir', 320 $this->newRepo( [ 321 'url' => 'https://my.url', 322 'zones' => [ 'public' => [ 'url' => 'https://my.url/some/sub/dir' ] ], 323 ] )->getInfo()['rootUrl'] 324 ); 325 } 326 327 /** 328 * @covers ::getInfo 329 */ 330 public function testGetInfo_local() { 331 $this->assertFalse( $this->newRepo( [ 'name' => 'not-local' ] )->getInfo()['local'] ); 332 } 333 334 /** 335 * @param string $setting 336 * @dataProvider provideGetInfo_optionalSettings 337 * @covers ::getInfo 338 */ 339 public function testGetInfo_optionalSettings( $setting ) { 340 $this->assertSame( 'dummy test value', 341 $this->newRepo( [ $setting => 'dummy test value' ] )->getInfo()[$setting] ); 342 } 343 344 public static function provideGetInfo_optionalSettings() { 345 return [ 346 [ 'url' ], 347 [ 'thumbUrl' ], 348 [ 'initialCapital' ], 349 [ 'descBaseUrl' ], 350 [ 'scriptDirUrl' ], 351 [ 'articleUrl' ], 352 [ 'fetchDescription' ], 353 [ 'descriptionCacheExpiry' ], 354 ]; 355 } 356 357 /** 358 * @param string $method 359 * @param mixed ...$args 360 * @dataProvider provideSkipWriteOperationIfSha1 361 * @covers ::store 362 * @covers ::storeBatch 363 * @covers ::cleanupBatch 364 * @covers ::publish 365 * @covers ::publishBatch 366 * @covers ::delete 367 * @covers ::deleteBatch 368 * @covers ::skipWriteOperationIfSha1 369 */ 370 public function testSkipWriteOperationIfSha1( $method, ...$args ) { 371 $repo = $this->newRepo( [ 'storageLayout' => 'sha1' ] ); 372 $this->assertEquals( Status::newGood(), $repo->$method( ...$args ) ); 373 } 374 375 public static function provideSkipWriteOperationIfSha1() { 376 return [ 377 [ 'store', '', '', '' ], 378 [ 'storeBatch', [ '' ] ], 379 [ 'cleanupBatch', [ '' ] ], 380 [ 'publish', '', '', '' ], 381 [ 'publishBatch', [ '' ] ], 382 [ 'delete', '', '' ], 383 [ 'deleteBatch', [ '' ] ], 384 ]; 385 } 386} 387