This moves a code out of file scope into classes to make it testable. The code is left in the same structure as it was before, global functions have been converted into methods on the new ThumbnailEntryPoint and Thumbnail404EntryPoint classes. This test introduces comprehensive phpunit tests covering all functional code paths in ThumbnailEntryPoint. This is intended to support refactoring of this code. Change-Id: I459abc7b11d0ab4ee682a863c9525a945048296f
812 lines
20 KiB
PHP
812 lines
20 KiB
PHP
<?php
|
|
|
|
use MediaWiki\FileRepo\ThumbnailEntryPoint;
|
|
use MediaWiki\MainConfigNames;
|
|
use MediaWiki\Permissions\SimpleAuthority;
|
|
use MediaWiki\Request\FauxRequest;
|
|
use MediaWiki\Tests\FileRepo\TestRepoTrait;
|
|
use MediaWiki\Tests\MockEnvironment;
|
|
use MediaWiki\User\UserIdentityValue;
|
|
|
|
/**
|
|
* @covers \MediaWiki\FileRepo\ThumbnailEntryPoint
|
|
* @group Database
|
|
*/
|
|
class ThumbnailEntryPointTest extends MediaWikiIntegrationTestCase {
|
|
|
|
use TestRepoTrait;
|
|
use MockHttpTrait;
|
|
|
|
private const PNG_MAGIC = "\x89\x50\x4e\x47";
|
|
private const JPEG_MAGIC = "\xff\xd8\xff\xe0";
|
|
|
|
private const IMAGES_DIR = __DIR__ . '/../../data/media';
|
|
|
|
// Counter for getting unique width values
|
|
private static $uniqueWidth = 20;
|
|
|
|
/**
|
|
* will be called only once per test class
|
|
*/
|
|
public function addDBDataOnce() {
|
|
// Create mock repo with test files
|
|
$this->initTestRepoGroup();
|
|
|
|
$this->importFileToTestRepo( self::IMAGES_DIR . '/greyscale-png.png', 'Test.png' );
|
|
$this->importFileToTestRepo( self::IMAGES_DIR . '/test.jpg', 'Icon.jpg' );
|
|
|
|
// Create a second version of Test.png
|
|
$this->importFileToTestRepo( self::IMAGES_DIR . '/greyscale-na-png.png', 'Test.png' );
|
|
$this->importFileToTestRepo( self::IMAGES_DIR . '/portrait-rotated.jpg', 'Icon.jpg' );
|
|
|
|
// Create a redirect
|
|
$title = Title::makeTitle( NS_FILE, 'Redirect_to_Test.png' );
|
|
$this->editPage( $title, '#REDIRECT [[File:Test.png]]' );
|
|
|
|
// Suppress the old version of Icon
|
|
$file = $this->getTestRepo()->newFile( 'Icon.jpg' );
|
|
$history = $file->getHistory();
|
|
$oldFile = $history[0];
|
|
|
|
$this->db->newUpdateQueryBuilder()
|
|
->table( 'oldimage' )
|
|
->set( [ 'oi_deleted' => 1 ] )
|
|
->where( [ 'oi_archive_name' => $oldFile->getArchiveName() ] )
|
|
->caller( __METHOD__ )
|
|
->execute();
|
|
}
|
|
|
|
public static function tearDownAfterClass(): void {
|
|
self::destroyTestRepo();
|
|
parent::tearDownAfterClass();
|
|
}
|
|
|
|
public function setUp(): void {
|
|
parent::setUp();
|
|
|
|
$this->overrideConfigValue( MainConfigNames::ThumbLimits, [ 16, 24 ] );
|
|
$this->installTestRepoGroup();
|
|
}
|
|
|
|
/**
|
|
* @param FauxRequest|string|array|null $request
|
|
*
|
|
* @return MockEnvironment
|
|
*/
|
|
private function makeEnvironment( $request ): MockEnvironment {
|
|
if ( !$request ) {
|
|
$request = new FauxRequest();
|
|
}
|
|
|
|
if ( is_string( $request ) ) {
|
|
$request = [ 'f' => $request, 'width' => self::$uniqueWidth++ ];
|
|
}
|
|
|
|
if ( is_array( $request ) ) {
|
|
$request = new FauxRequest( $request );
|
|
$request->setRequestURL( '/w/img.php' );
|
|
}
|
|
|
|
return new MockEnvironment( $request );
|
|
}
|
|
|
|
/**
|
|
* @param MockEnvironment|null $environment
|
|
* @param FauxRequest|RequestContext|string|array|null $request
|
|
*
|
|
* @return ThumbnailEntryPoint
|
|
*/
|
|
private function getEntryPoint(
|
|
MockEnvironment $environment = null,
|
|
$request = null
|
|
) {
|
|
if ( !$request && $environment ) {
|
|
$request = $environment->getFauxRequest();
|
|
}
|
|
|
|
if ( $request instanceof RequestContext ) {
|
|
$context = $request;
|
|
$request = $context->getRequest();
|
|
} else {
|
|
$context = new RequestContext();
|
|
$context->setRequest( $request );
|
|
$context->setUser( $this->getTestUser()->getUser() );
|
|
}
|
|
|
|
if ( !$environment ) {
|
|
$environment = $this->makeEnvironment( $request );
|
|
}
|
|
|
|
$context->setLanguage( 'qqx' );
|
|
|
|
$entryPoint = new ThumbnailEntryPoint(
|
|
$context,
|
|
$environment,
|
|
$this->getServiceContainer()
|
|
);
|
|
|
|
$entryPoint->enableOutputCapture();
|
|
return $entryPoint;
|
|
}
|
|
|
|
public function testNotFound() {
|
|
$env = $this->makeEnvironment( 'Missing_puppy.jpeg' );
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
|
|
$env->assertStatusCode( 404 );
|
|
$env->assertHeaderValue(
|
|
'text/html; charset=utf-8',
|
|
'Content-Type'
|
|
);
|
|
|
|
$output = $entryPoint->getCapturedOutput();
|
|
$this->assertStringContainsString(
|
|
'<title>Error generating thumbnail</title>',
|
|
$output
|
|
);
|
|
}
|
|
|
|
public function testGenerateAndStreamThumbnail() {
|
|
$env = $this->makeEnvironment(
|
|
[
|
|
'f' => 'Test.png',
|
|
'width' => 12 // Must match the width in testStreamExistingThumbnail
|
|
]
|
|
);
|
|
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
// TODO: Assert Content-Type and Content-Length headers.
|
|
// Needs FileStreamer to use WebResponse.
|
|
|
|
$env->assertStatusCode( 200, $output );
|
|
|
|
$this->assertThumbnail(
|
|
[ 'magic' => self::PNG_MAGIC, 'width' => 12, ],
|
|
$output
|
|
);
|
|
|
|
return [ 'data' => $output, 'width' => 12 ];
|
|
}
|
|
|
|
/**
|
|
* @depends testGenerateAndStreamThumbnail
|
|
*/
|
|
public function testStreamExistingThumbnail() {
|
|
// Sabotage transformations, so this test will fail if we do not
|
|
// use the existing thumbnail generated by testGenerateAndStreamThumbnail.
|
|
$handler = $this->getMockBuilder( BitmapHandler::class )
|
|
->onlyMethods( [ 'doTransform' ] )
|
|
->getMock();
|
|
|
|
$handler->expects( $this->never() )->method( 'doTransform' );
|
|
|
|
$factory = $this->createNoOpMock( MediaHandlerFactory::class, [ 'getHandler' ] );
|
|
$factory->method( 'getHandler' )->willReturn( $handler );
|
|
$this->setService( 'MediaHandlerFactory', $factory );
|
|
|
|
$env = $this->makeEnvironment(
|
|
[
|
|
'f' => 'Test.png',
|
|
'width' => 12 // Must match the width in testGenerateAndStreamThumbnail
|
|
]
|
|
);
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$env->assertStatusCode( 200 );
|
|
|
|
$this->assertThumbnail(
|
|
[ 'magic' => self::PNG_MAGIC, 'width' => 12, ],
|
|
$output
|
|
);
|
|
}
|
|
|
|
public function testNoThumbName() {
|
|
// Make sure no handler is set, so that File::generateThumbName() returns null
|
|
$factory = $this->createNoOpMock( MediaHandlerFactory::class, [ 'getHandler' ] );
|
|
$factory->method( 'getHandler' )->willReturn( false );
|
|
$this->setService( 'MediaHandlerFactory', $factory );
|
|
|
|
$env = $this->makeEnvironment(
|
|
[
|
|
'f' => 'Test.png',
|
|
'width' => self::$uniqueWidth++
|
|
]
|
|
);
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$env->assertStatusCode( 400, $output );
|
|
}
|
|
|
|
public function testTransformError() {
|
|
// Mock transformations to return an error
|
|
$handler = $this->getMockBuilder( BitmapHandler::class )
|
|
->onlyMethods( [ 'doTransform' ] )
|
|
->getMock();
|
|
|
|
$transformOutput = new MediaTransformError( 'testing', 200, 100 );
|
|
$handler->method( 'doTransform' )->willReturn( $transformOutput );
|
|
|
|
$factory = $this->createNoOpMock( MediaHandlerFactory::class, [ 'getHandler' ] );
|
|
$factory->method( 'getHandler' )->willReturn( $handler );
|
|
$this->setService( 'MediaHandlerFactory', $factory );
|
|
|
|
$env = $this->makeEnvironment(
|
|
[
|
|
'f' => 'Test.png',
|
|
'width' => self::$uniqueWidth++
|
|
]
|
|
);
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$env->assertStatusCode( 500, $output );
|
|
}
|
|
|
|
public function testContentDisposition() {
|
|
// TODO...
|
|
$this->markTestSkipped( 'Needs refactoring of HTTPFileStreamer to capture headers' );
|
|
|
|
$env = $this->makeEnvironment(
|
|
[
|
|
'f' => 'Test.png',
|
|
'width' => 12,
|
|
'download' => 1
|
|
]
|
|
);
|
|
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$env->assertStatusCode( 200 );
|
|
$this->assertThumbnail( [ 'magic' => self::PNG_MAGIC, ], $output );
|
|
|
|
$env->assertHeaderValue( 'attachment', 'Content-Disposition' );
|
|
}
|
|
|
|
public static function provideThumbNameParam() {
|
|
yield [ '12px-Test.png' ];
|
|
yield [ 'page123456-12px-xyz' ];
|
|
yield [ '12px-xyz' ];
|
|
yield [ 'xyzzy', 400 ];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideThumbNameParam
|
|
*/
|
|
public function testThumbNameParam( $thumbName, $expected = 200 ) {
|
|
$env = $this->makeEnvironment(
|
|
[
|
|
'f' => 'Test.png',
|
|
'thumbName' => $thumbName,
|
|
]
|
|
);
|
|
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$env->assertStatusCode( $expected, $output );
|
|
|
|
if ( $expected < 300 ) {
|
|
$expectedProps = [ 'magic' => self::PNG_MAGIC ];
|
|
|
|
// get expected width
|
|
if ( preg_match( '/\b(\d+)px/', $thumbName, $matches ) ) {
|
|
$expectedProps['width'] = (int)$matches[1];
|
|
}
|
|
|
|
$this->assertThumbnail(
|
|
$expectedProps,
|
|
$output
|
|
);
|
|
}
|
|
}
|
|
|
|
public function testAccessDenied() {
|
|
// Make the wiki non-public
|
|
$groupPermissions = $this->getConfVar( MainConfigNames::GroupPermissions );
|
|
$groupPermissions['*']['read'] = false;
|
|
|
|
$this->overrideConfigValue(
|
|
'GroupPermissions',
|
|
$groupPermissions
|
|
);
|
|
|
|
// Make the user have no rights
|
|
$authority = new SimpleAuthority(
|
|
new UserIdentityValue( 7, 'Heather' ),
|
|
[]
|
|
);
|
|
|
|
$env = $this->makeEnvironment( 'Test.png' );
|
|
|
|
$context = $env->makeFauxContext();
|
|
$context->setAuthority( $authority );
|
|
|
|
$entryPoint = $this->getEntryPoint(
|
|
$env,
|
|
$context
|
|
);
|
|
|
|
$entryPoint->run();
|
|
|
|
$env->assertStatusCode( 403 );
|
|
$env->assertHeaderValue(
|
|
'text/html; charset=utf-8',
|
|
'Content-Type'
|
|
);
|
|
|
|
$output = $entryPoint->getCapturedOutput();
|
|
$this->assertStringContainsString(
|
|
'<title>Error generating thumbnail</title>',
|
|
$output
|
|
);
|
|
}
|
|
|
|
public function testAccessOnPrivateWiki() {
|
|
// Make the wiki non-public, so we don't use the short-circuit code
|
|
$groupPermissions = $this->getConfVar( MainConfigNames::GroupPermissions );
|
|
$groupPermissions['*']['read'] = false;
|
|
|
|
$this->overrideConfigValue(
|
|
'GroupPermissions',
|
|
$groupPermissions
|
|
);
|
|
|
|
// Make a user who is allowed to read
|
|
$authority = new SimpleAuthority(
|
|
new UserIdentityValue( 7, 'Heather' ),
|
|
[ 'read', 'renderfile', 'renderfile-nonstandard' ]
|
|
);
|
|
|
|
$env = $this->makeEnvironment( 'Test.png' );
|
|
|
|
$context = $env->makeFauxContext();
|
|
$context->setAuthority( $authority );
|
|
|
|
$entryPoint = $this->getEntryPoint(
|
|
$env,
|
|
$context
|
|
);
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$env->assertStatusCode( 200 );
|
|
$this->assertThumbnail( [ 'magic' => self::PNG_MAGIC, ], $output );
|
|
}
|
|
|
|
public static function provideRateLimit() {
|
|
// NOTE: The 12px thumbnail will have been generated at this point.
|
|
// We force 16 and 24 to be standard sizes during setup.
|
|
// Once the thumbnail is generated, the rate limit is no longer
|
|
// triggered.
|
|
yield [ '16', '24', 'renderfile' ];
|
|
yield [ self::$uniqueWidth++, self::$uniqueWidth++, 'renderfile-nonstandard' ];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideRateLimit
|
|
*/
|
|
public function testRateLimited( $width1, $width2, $limit ) {
|
|
// Set up rate limit config
|
|
$rateLimits = $this->getConfVar( MainConfigNames::RateLimits );
|
|
$rateLimits[$limit] = [
|
|
'ip' => [ 1, 60 ],
|
|
'newbie' => [ 1, 60 ],
|
|
'user' => [ 1, 60 ],
|
|
];
|
|
|
|
$this->overrideConfigValue( MainConfigNames::RateLimits, $rateLimits );
|
|
|
|
// First run should pass
|
|
$env = $this->makeEnvironment( [ 'f' => 'Test.png', 'width' => $width1 ] );
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$entryPoint->getCapturedOutput();
|
|
$env->assertStatusCode( 200 );
|
|
|
|
// Second run should fail
|
|
$env = $this->makeEnvironment( [ 'f' => 'Test.png', 'width' => $width2 ] );
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$entryPoint->getCapturedOutput();
|
|
$env->assertStatusCode( 429 );
|
|
|
|
$env->assertHeaderValue(
|
|
'text/html; charset=utf-8',
|
|
'Content-Type'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @depends testGenerateAndStreamThumbnail
|
|
*/
|
|
public function testStreamOldFile( array $latestThumbnailInfo ) {
|
|
$file = $this->getTestRepo()->newFile( 'Test.png' );
|
|
$history = $file->getHistory();
|
|
$oldFile = $history[0];
|
|
|
|
$env = $this->makeEnvironment(
|
|
[
|
|
'f' => $oldFile->getArchiveName(),
|
|
'width' => '12px', // use "px" suffix, just so we also cover that code path
|
|
'archived' => 1,
|
|
]
|
|
);
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
$env->assertStatusCode( 200 );
|
|
|
|
$this->assertNotSame(
|
|
$latestThumbnailInfo['data'],
|
|
$output,
|
|
'Thumbnail for the old version should not be the same as the ' .
|
|
'thumbnail for the latest version'
|
|
);
|
|
|
|
$this->assertThumbnail(
|
|
[ 'magic' => self::PNG_MAGIC, 'width' => 12, ],
|
|
$output
|
|
);
|
|
}
|
|
|
|
public function testOldDeletedFile() {
|
|
// Note that we manually set oi_deleted for this revision
|
|
// in addDBDataOnce().
|
|
$file = $this->getTestRepo()->newFile( 'Icon.jpg' );
|
|
$history = $file->getHistory();
|
|
$oldFile = $history[0];
|
|
|
|
$env = $this->makeEnvironment(
|
|
[
|
|
'f' => $oldFile->getArchiveName(),
|
|
'width' => '12px', // use "px" suffix, just so we also cover that code path
|
|
'archived' => 1,
|
|
]
|
|
);
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
$env->assertStatusCode( 404, $output );
|
|
}
|
|
|
|
/**
|
|
* @depends testGenerateAndStreamThumbnail
|
|
*/
|
|
public function testStreamOldFileRedirect( array $latestThumbnailInfo ) {
|
|
$file = $this->getTestRepo()->newFile( 'Test.png' );
|
|
$history = $file->getHistory();
|
|
$oldFile = $history[0];
|
|
|
|
// Try accessing the old revision using a redirected title
|
|
$archiveName = str_replace(
|
|
'Test.png',
|
|
'Redirect_to_Test.png',
|
|
$oldFile->getArchiveName()
|
|
);
|
|
|
|
$env = $this->makeEnvironment(
|
|
[
|
|
'f' => $archiveName,
|
|
'width' => 12,
|
|
'archived' => 1,
|
|
]
|
|
);
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$response = $env->getFauxResponse();
|
|
|
|
$this->assertSame( 302, $response->getStatusCode() );
|
|
|
|
$expected = '/' . urlencode( $oldFile->getArchiveName() ) . '/12px-Test.png';
|
|
$this->assertStringEndsWith(
|
|
$expected,
|
|
$response->getHeader( 'Location' )
|
|
);
|
|
|
|
$this->assertSame( '', $output );
|
|
}
|
|
|
|
public function testStreamTempFile() {
|
|
$user = $this->getTestUser()->getUser();
|
|
$stash = new UploadStash( $this->getTestRepo(), $user );
|
|
$file = $stash->stashFile( self::IMAGES_DIR . '/adobergb.jpg' );
|
|
|
|
$env = $this->makeEnvironment(
|
|
[
|
|
'f' => $file->getName(),
|
|
'width' => 12,
|
|
'temp' => 'yes',
|
|
]
|
|
);
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$env->assertStatusCode( 200 );
|
|
$this->assertThumbnail(
|
|
[ 'magic' => self::JPEG_MAGIC, 'width' => 12, ],
|
|
$output
|
|
);
|
|
}
|
|
|
|
public function testRedirect() {
|
|
$this->overrideConfigValue( MainConfigNames::VaryOnXFP, true );
|
|
|
|
$env = $this->makeEnvironment(
|
|
[
|
|
'f' => 'Redirect_to_Test.png',
|
|
'w' => 12
|
|
]
|
|
);
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$response = $env->getFauxResponse();
|
|
|
|
$this->assertSame( 302, $response->getStatusCode() );
|
|
$this->assertStringEndsWith(
|
|
'/Test.png/12px-Test.png',
|
|
$response->getHeader( 'Location' )
|
|
);
|
|
|
|
$this->assertSame( '', $output );
|
|
$env->assertHeaderValue( 'X-Forwarded-Proto', 'Vary' );
|
|
}
|
|
|
|
public function testBadTitle() {
|
|
$env = $this->makeEnvironment( '_/_' );
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$env->assertStatusCode( 404 );
|
|
$env->assertHeaderValue(
|
|
'text/html; charset=utf-8',
|
|
'Content-Type'
|
|
);
|
|
|
|
$this->assertStringContainsString(
|
|
'(badtitletext)',
|
|
$output
|
|
);
|
|
}
|
|
|
|
public static function provideOldFileWithBadTitle() {
|
|
yield 'invalid title' => [ '_/_' ];
|
|
yield 'valid title without timestamp' => [ 'Test.png' ];
|
|
yield 'invalid title with timestamp' => [ '20200101002233!_/_' ];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideOldFileWithBadTitle
|
|
*/
|
|
public function testOldFileWithBadTitle( $badTitle ) {
|
|
$env = $this->makeEnvironment( [
|
|
'f' => $badTitle,
|
|
'width' => 12,
|
|
'archived' => 1
|
|
] );
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$env->assertStatusCode( 404 );
|
|
$env->assertHeaderValue(
|
|
'text/html; charset=utf-8',
|
|
'Content-Type'
|
|
);
|
|
|
|
$this->assertStringContainsString(
|
|
'(badtitletext)',
|
|
$output
|
|
);
|
|
}
|
|
|
|
public function testTooMuchWidth() {
|
|
// Set the width larger than the size of the image
|
|
$env = $this->makeEnvironment( [ 'f' => 'Test.png', 'width' => 1200 ] );
|
|
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$env->assertStatusCode( 400 );
|
|
$env->assertHeaderValue(
|
|
'text/html; charset=utf-8',
|
|
'Content-Type'
|
|
);
|
|
|
|
$this->assertStringContainsString(
|
|
'(thumbnail_error: ',
|
|
$output
|
|
);
|
|
$this->assertStringContainsString(
|
|
'bigger than the source',
|
|
$output
|
|
);
|
|
}
|
|
|
|
public function testDeletedFile() {
|
|
// Delete Icon.jpg
|
|
$icon = $this->getTestRepo()->newFile( 'Icon.jpg' );
|
|
|
|
$this->assertTrue( $icon->exists() );// sanity
|
|
$icon->deleteFile( 'testing', new UserIdentityValue( 0, 'Test' ) );
|
|
|
|
$env = $this->makeEnvironment( 'Icon.jpg' );
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$env->assertStatusCode( 404 );
|
|
$env->assertHeaderValue(
|
|
'text/html; charset=utf-8',
|
|
'Content-Type'
|
|
);
|
|
|
|
$this->assertStringContainsString(
|
|
'<title>Error generating thumbnail</title>',
|
|
$output
|
|
);
|
|
}
|
|
|
|
public function testNotModified() {
|
|
$env = $this->makeEnvironment(
|
|
[
|
|
'f' => 'Test.png',
|
|
'width' => 12
|
|
]
|
|
);
|
|
|
|
$env->setServerInfo( 'HTTP_IF_MODIFIED_SINCE', '25250101001122' );
|
|
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$response = $env->getFauxResponse();
|
|
$this->assertSame( 304, $response->getStatusCode() );
|
|
$this->assertSame( '', $output );
|
|
}
|
|
|
|
public function testProxy() {
|
|
$this->installTestRepoGroup( [ 'thumbProxyUrl' => 'https://images.acme.test/thumbnails/' ] );
|
|
$this->installMockHttp( 'PROXY RESPONSE' );
|
|
|
|
$env = $this->makeEnvironment(
|
|
[
|
|
'f' => 'Test.png',
|
|
'width' => self::$uniqueWidth++
|
|
]
|
|
);
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$this->assertSame( 'PROXY RESPONSE', $output );
|
|
}
|
|
|
|
public static function provideRepoCouldNotStreamFile() {
|
|
// The width must match the one generated by testGenerateAndStreamThumbnail
|
|
// This error comes from FileBackend::doStreamFile.
|
|
yield 'existing thumbnail' => [ 12, 'xyzzy-error' ];
|
|
|
|
// TODO: also test the case where we fail to stream a newly created
|
|
// thumbnail. In that case, the expected error comes from
|
|
// MediaTransformOutput::streamFileWithStatus, not FileBackend::doStreamFile.
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideRepoCouldNotStreamFile
|
|
* @depends testGenerateAndStreamThumbnail
|
|
*/
|
|
public function testRepoCouldNotStreamFile( int $width, string $expectedError ) {
|
|
// Sabotage streaming in file backend
|
|
$backend = $this->createFileBackend( [
|
|
'overrides' => [
|
|
'doStreamFile' => Status::newFatal( 'xyzzy-error' )
|
|
]
|
|
] );
|
|
|
|
$this->installTestRepoGroup(
|
|
[ 'backend' => $backend ]
|
|
);
|
|
|
|
// TODO: figure out how to provoke an error in
|
|
// MediaTransformOutput::streamFileWithStatus.
|
|
// The below causes an error to be triggered too early.
|
|
// Since MediaTransformOutput uses StreamFile directly, we have to also
|
|
// sabotage transformations in the handler to return a ThumbnailImage
|
|
// with no path. This is unfortunately brittle to implementation changes.
|
|
$handler = $this->getMockBuilder( BitmapHandler::class )
|
|
->onlyMethods( [ 'doTransform' ] )
|
|
->getMock();
|
|
|
|
$file = $this->getTestRepo()->newFile( 'Test.png' );
|
|
$params = [ 'width' => $width, 'height' => $width ];
|
|
$handler->method( 'doTransform' )->willReturn(
|
|
new ThumbnailImage( $file, '', false, $params )
|
|
);
|
|
|
|
$factory = $this->createNoOpMock( MediaHandlerFactory::class, [ 'getHandler' ] );
|
|
$factory->method( 'getHandler' )->willReturn( $handler );
|
|
$this->setService( 'MediaHandlerFactory', $factory );
|
|
|
|
$env = $this->makeEnvironment(
|
|
[
|
|
'f' => 'Test.png',
|
|
'width' => $width
|
|
]
|
|
);
|
|
$entryPoint = $this->getEntryPoint( $env );
|
|
|
|
$entryPoint->run();
|
|
$output = $entryPoint->getCapturedOutput();
|
|
|
|
$env->assertStatusCode( 500, $output );
|
|
$env->assertHeaderValue(
|
|
'text/html; charset=utf-8',
|
|
'Content-Type'
|
|
);
|
|
|
|
// TODO: check the log for the specific error.
|
|
$this->assertStringContainsString( 'Could not stream the file', $output );
|
|
}
|
|
|
|
/**
|
|
* @param array $props
|
|
* @param string $output binary data
|
|
*/
|
|
private function assertThumbnail( array $props, string $output ): void {
|
|
if ( isset( $props['magic'] ) ) {
|
|
$this->assertStringStartsWith(
|
|
$props['magic'],
|
|
$output,
|
|
'Magic number should match'
|
|
);
|
|
}
|
|
|
|
if ( isset( $props['width'] ) && function_exists( 'getimagesizefromstring' ) ) {
|
|
[ $width, ] = getimagesizefromstring( $output );
|
|
$this->assertSame(
|
|
$props['width'],
|
|
$width
|
|
);
|
|
}
|
|
}
|
|
|
|
}
|