From: Brad Jorsch Date: Wed, 3 Jan 2018 18:27:37 +0000 (-0500) Subject: Add tests for ApiFormatBase X-Git-Tag: 1.31.0-rc.0~1001^2 X-Git-Url: http://git.cyclocoop.org/%24image?a=commitdiff_plain;h=a8f5964cfef4e6209cf6dd55a28b5ed91ee3a04b;p=lhc%2Fweb%2Fwiklou.git Add tests for ApiFormatBase Ensuring proper behavior of the base class lets comprehensive tests of subclasses be simpler. This also adjusts ApiFormatTestBase to be a bit more usable, passing an array of options through to encodeData() instead of just a class name. And removes the unused 'SKIP' from testGeneralEncoding, but allows expecting an exception (for use in I63ce42dd). Change-Id: Ib2a1fa0b04860b09105376881ff8411f9534c453 --- diff --git a/tests/phpunit/includes/api/format/ApiFormatBaseTest.php b/tests/phpunit/includes/api/format/ApiFormatBaseTest.php new file mode 100644 index 0000000000..d6a13904eb --- /dev/null +++ b/tests/phpunit/includes/api/format/ApiFormatBaseTest.php @@ -0,0 +1,345 @@ +setRequest( new FauxRequest( [], true ) ); + $main = new ApiMain( $context ); + } + + $mock = $this->getMockBuilder( ApiFormatBase::class ) + ->setConstructorArgs( [ $main, $format ] ) + ->setMethods( array_unique( array_merge( $methods, [ 'getMimeType', 'execute' ] ) ) ) + ->getMock(); + if ( !in_array( 'getMimeType', $methods, true ) ) { + $mock->method( 'getMimeType' )->willReturn( 'text/x-mock' ); + } + return $mock; + } + + protected function encodeData( array $params, array $data, $options = [] ) { + $options += [ + 'name' => 'mock', + 'class' => ApiFormatBase::class, + 'factory' => function ( ApiMain $main, $format ) use ( $options ) { + $mock = $this->getMockFormatter( $main, $format ); + $mock->expects( $this->once() )->method( 'execute' ) + ->willReturnCallback( function () use ( $mock ) { + $mock->printText( "Format {$mock->getFormat()}: " ); + $mock->printText( "ok" ); + } ); + + if ( isset( $options['status'] ) ) { + $mock->setHttpStatus( $options['status'] ); + } + + return $mock; + }, + 'returnPrinter' => true, + ]; + + $this->setMwGlobals( [ + 'wgApiFrameOptions' => 'DENY', + ] ); + + $ret = parent::encodeData( $params, $data, $options ); + $printer = TestingAccessWrapper::newFromObject( $ret['printer'] ); + $text = $ret['text']; + + if ( $options['name'] !== 'mockfm' ) { + $ct = 'text/x-mock'; + $file = 'api-result.mock'; + $status = isset( $options['status'] ) ? $options['status'] : null; + } elseif ( isset( $params['wrappedhtml'] ) ) { + $ct = 'text/mediawiki-api-prettyprint-wrapped'; + $file = 'api-result-wrapped.json'; + $status = null; + + // Replace varying field + $text = preg_replace( '/"time":\d+/', '"time":1234', $text ); + } else { + $ct = 'text/html'; + $file = 'api-result.html'; + $status = null; + + // Strip OutputPage-generated HTML + if ( preg_match( '!
.*
!s', $text, $m ) ) { + $text = $m[0]; + } + } + + $response = $printer->getMain()->getRequest()->response(); + $this->assertSame( "$ct; charset=utf-8", strtolower( $response->getHeader( 'Content-Type' ) ) ); + $this->assertSame( 'DENY', $response->getHeader( 'X-Frame-Options' ) ); + $this->assertSame( $file, $printer->getFilename() ); + $this->assertSame( "inline; filename=\"$file\"", $response->getHeader( 'Content-Disposition' ) ); + $this->assertSame( $status, $response->getStatusCode() ); + + return $text; + } + + public static function provideGeneralEncoding() { + return [ + 'normal' => [ + [], + "Format MOCK: ok", + [], + [ 'name' => 'mock' ] + ], + 'normal ignores wrappedhtml' => [ + [], + "Format MOCK: ok", + [ 'wrappedhtml' => 1 ], + [ 'name' => 'mock' ] + ], + 'HTML format' => [ + [], + '
Format MOCK: <b>ok</b>
', + [], + [ 'name' => 'mockfm' ] + ], + 'wrapped HTML format' => [ + [], + // phpcs:ignore Generic.Files.LineLength.TooLong + '{"status":200,"statustext":"OK","html":"
Format MOCK: <b>ok</b>
","modules":["mediawiki.apipretty"],"continue":null,"time":1234}', + [ 'wrappedhtml' => 1 ], + [ 'name' => 'mockfm' ] + ], + 'normal, with set status' => [ + [], + "Format MOCK: ok", + [], + [ 'name' => 'mock', 'status' => 400 ] + ], + 'HTML format, with set status' => [ + [], + '
Format MOCK: <b>ok</b>
', + [], + [ 'name' => 'mockfm', 'status' => 400 ] + ], + 'wrapped HTML format, with set status' => [ + [], + // phpcs:ignore Generic.Files.LineLength.TooLong + '{"status":400,"statustext":"Bad Request","html":"
Format MOCK: <b>ok</b>
","modules":["mediawiki.apipretty"],"continue":null,"time":1234}', + [ 'wrappedhtml' => 1 ], + [ 'name' => 'mockfm', 'status' => 400 ] + ], + 'wrapped HTML format, cross-domain-policy' => [ + [ 'continue' => '< CrOsS-DoMaIn-PoLiCy >' ], + // phpcs:ignore Generic.Files.LineLength.TooLong + '{"status":200,"statustext":"OK","html":"
Format MOCK: <b>ok</b>
","modules":["mediawiki.apipretty"],"continue":"\u003C CrOsS-DoMaIn-PoLiCy \u003E","time":1234}', + [ 'wrappedhtml' => 1 ], + [ 'name' => 'mockfm' ] + ], + ]; + } + + public function testBasics() { + $printer = $this->getMockFormatter( null, 'mock' ); + $this->assertTrue( $printer->canPrintErrors() ); + $this->assertSame( + 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Data_formats', + $printer->getHelpUrls() + ); + } + + public function testDisable() { + $this->setMwGlobals( [ + 'wgApiFrameOptions' => 'DENY', + ] ); + + $printer = $this->getMockFormatter( null, 'mock' ); + $printer->method( 'execute' )->willReturnCallback( function () use ( $printer ) { + $printer->printText( 'Foo' ); + } ); + $this->assertFalse( $printer->isDisabled() ); + $printer->disable(); + $this->assertTrue( $printer->isDisabled() ); + + $printer->setHttpStatus( 400 ); + $printer->initPrinter(); + $printer->execute(); + ob_start(); + $printer->closePrinter(); + $this->assertSame( '', ob_get_clean() ); + $response = $printer->getMain()->getRequest()->response(); + $this->assertNull( $response->getHeader( 'Content-Type' ) ); + $this->assertNull( $response->getHeader( 'X-Frame-Options' ) ); + $this->assertNull( $response->getHeader( 'Content-Disposition' ) ); + $this->assertNull( $response->getStatusCode() ); + } + + public function testNullMimeType() { + $this->setMwGlobals( [ + 'wgApiFrameOptions' => 'DENY', + ] ); + + $printer = $this->getMockFormatter( null, 'mock', [ 'getMimeType' ] ); + $printer->method( 'execute' )->willReturnCallback( function () use ( $printer ) { + $printer->printText( 'Foo' ); + } ); + $printer->method( 'getMimeType' )->willReturn( null ); + $this->assertNull( $printer->getMimeType(), 'sanity check' ); + + $printer->initPrinter(); + $printer->execute(); + ob_start(); + $printer->closePrinter(); + $this->assertSame( 'Foo', ob_get_clean() ); + $response = $printer->getMain()->getRequest()->response(); + $this->assertNull( $response->getHeader( 'Content-Type' ) ); + $this->assertNull( $response->getHeader( 'X-Frame-Options' ) ); + $this->assertNull( $response->getHeader( 'Content-Disposition' ) ); + + $printer = $this->getMockFormatter( null, 'mockfm', [ 'getMimeType' ] ); + $printer->method( 'execute' )->willReturnCallback( function () use ( $printer ) { + $printer->printText( 'Foo' ); + } ); + $printer->method( 'getMimeType' )->willReturn( null ); + $this->assertNull( $printer->getMimeType(), 'sanity check' ); + $this->assertTrue( $printer->getIsHtml(), 'sanity check' ); + + $printer->initPrinter(); + $printer->execute(); + ob_start(); + $printer->closePrinter(); + $this->assertSame( 'Foo', ob_get_clean() ); + $response = $printer->getMain()->getRequest()->response(); + $this->assertSame( + 'text/html; charset=utf-8', strtolower( $response->getHeader( 'Content-Type' ) ) + ); + $this->assertSame( 'DENY', $response->getHeader( 'X-Frame-Options' ) ); + $this->assertSame( + 'inline; filename="api-result.html"', $response->getHeader( 'Content-Disposition' ) + ); + } + + public function testApiFrameOptions() { + $this->setMwGlobals( [ 'wgApiFrameOptions' => 'DENY' ] ); + $printer = $this->getMockFormatter( null, 'mock' ); + $printer->initPrinter(); + $this->assertSame( + 'DENY', + $printer->getMain()->getRequest()->response()->getHeader( 'X-Frame-Options' ) + ); + + $this->setMwGlobals( [ 'wgApiFrameOptions' => 'SAMEORIGIN' ] ); + $printer = $this->getMockFormatter( null, 'mock' ); + $printer->initPrinter(); + $this->assertSame( + 'SAMEORIGIN', + $printer->getMain()->getRequest()->response()->getHeader( 'X-Frame-Options' ) + ); + + $this->setMwGlobals( [ 'wgApiFrameOptions' => false ] ); + $printer = $this->getMockFormatter( null, 'mock' ); + $printer->initPrinter(); + $this->assertNull( + $printer->getMain()->getRequest()->response()->getHeader( 'X-Frame-Options' ) + ); + } + + public function testForceDefaultParams() { + $context = new RequestContext; + $context->setRequest( new FauxRequest( [ 'foo' => '1', 'bar' => '2', 'baz' => '3' ], true ) ); + $main = new ApiMain( $context ); + $allowedParams = [ + 'foo' => [], + 'bar' => [ ApiBase::PARAM_DFLT => 'bar?' ], + 'baz' => 'baz!', + ]; + + $printer = $this->getMockFormatter( $main, 'mock', [ 'getAllowedParams' ] ); + $printer->method( 'getAllowedParams' )->willReturn( $allowedParams ); + $this->assertEquals( + [ 'foo' => '1', 'bar' => '2', 'baz' => '3' ], + $printer->extractRequestParams(), + 'sanity check' + ); + + $printer = $this->getMockFormatter( $main, 'mock', [ 'getAllowedParams' ] ); + $printer->method( 'getAllowedParams' )->willReturn( $allowedParams ); + $printer->forceDefaultParams(); + $this->assertEquals( + [ 'foo' => null, 'bar' => 'bar?', 'baz' => 'baz!' ], + $printer->extractRequestParams() + ); + } + + public function testGetAllowedParams() { + $printer = $this->getMockFormatter( null, 'mock' ); + $this->assertSame( [], $printer->getAllowedParams() ); + + $printer = $this->getMockFormatter( null, 'mockfm' ); + $this->assertSame( [ + 'wrappedhtml' => [ + ApiBase::PARAM_DFLT => false, + ApiBase::PARAM_HELP_MSG => 'apihelp-format-param-wrappedhtml', + ] + ], $printer->getAllowedParams() ); + } + + public function testGetExamplesMessages() { + $printer = TestingAccessWrapper::newFromObject( $this->getMockFormatter( null, 'mock' ) ); + $this->assertSame( [ + 'action=query&meta=siteinfo&siprop=namespaces&format=mock' + => [ 'apihelp-format-example-generic', 'MOCK' ] + ], $printer->getExamplesMessages() ); + + $printer = TestingAccessWrapper::newFromObject( $this->getMockFormatter( null, 'mockfm' ) ); + $this->assertSame( [ + 'action=query&meta=siteinfo&siprop=namespaces&format=mockfm' + => [ 'apihelp-format-example-generic', 'MOCK' ] + ], $printer->getExamplesMessages() ); + } + + /** + * @dataProvider provideHtmlHeader + */ + public function testHtmlHeader( $post, $registerNonHtml, $expect ) { + $context = new RequestContext; + $request = new FauxRequest( [ 'a' => 1, 'b' => 2 ], $post ); + $request->setRequestURL( 'http://example.org/wx/api.php' ); + $context->setRequest( $request ); + $context->setLanguage( 'qqx' ); + $main = new ApiMain( $context ); + $printer = $this->getMockFormatter( $main, 'mockfm' ); + $mm = $printer->getMain()->getModuleManager(); + $mm->addModule( 'mockfm', 'format', ApiFormatBase::class, function () { + return $mock; + } ); + if ( $registerNonHtml ) { + $mm->addModule( 'mock', 'format', ApiFormatBase::class, function () { + return $mock; + } ); + } + + $printer->initPrinter(); + $printer->execute(); + ob_start(); + $printer->closePrinter(); + $text = ob_get_clean(); + $this->assertContains( $expect, $text ); + } + + public static function provideHtmlHeader() { + return [ + [ false, false, '(api-format-prettyprint-header-only-html: MOCK)' ], + [ true, false, '(api-format-prettyprint-header-only-html: MOCK)' ], + // phpcs:ignore Generic.Files.LineLength.TooLong + [ false, true, '(api-format-prettyprint-header-hyperlinked: MOCK, mock, http://example.org/wx/api.php?a=1&b=2&format=mock)' ], + [ true, true, '(api-format-prettyprint-header: MOCK, mock)' ], + ]; + } + +} diff --git a/tests/phpunit/includes/api/format/ApiFormatTestBase.php b/tests/phpunit/includes/api/format/ApiFormatTestBase.php index fb086e9985..4169dab2be 100644 --- a/tests/phpunit/includes/api/format/ApiFormatTestBase.php +++ b/tests/phpunit/includes/api/format/ApiFormatTestBase.php @@ -11,26 +11,40 @@ abstract class ApiFormatTestBase extends MediaWikiTestCase { /** * Return general data to be encoded for testing * @return array See self::testGeneralEncoding - * @throws Exception + * @throws BadMethodCallException */ public static function provideGeneralEncoding() { - throw new Exception( 'Subclass must implement ' . __METHOD__ ); + throw new BadMethodCallException( static::class . ' must implement ' . __METHOD__ ); } /** * Get the formatter output for the given input data * @param array $params Query parameters * @param array $data Data to encode - * @param string $class Printer class to use instead of the normal one - * @return string + * @param array $options Options. If passed a string, the string is treated + * as the 'class' option. + * - name: Format name, rather than $this->printerName + * - class: If set, register 'name' with this class (and 'factory', if that's set) + * - factory: Used with 'class' to register at runtime + * - returnPrinter: Return the printer object + * @param callable|null $factory Factory to use instead of the normal one + * @return string|array The string if $options['returnPrinter'] isn't set, or an array if it is: + * - text: Output text string + * - printer: ApiFormatBase * @throws Exception */ - protected function encodeData( array $params, array $data, $class = null ) { + protected function encodeData( array $params, array $data, $options = [] ) { + if ( is_string( $options ) ) { + $options = [ 'class' => $options ]; + } + $printerName = isset( $options['name'] ) ? $options['name'] : $this->printerName; + $context = new RequestContext; $context->setRequest( new FauxRequest( $params, true ) ); $main = new ApiMain( $context ); - if ( $class !== null ) { - $main->getModuleManager()->addModule( $this->printerName, 'format', $class ); + if ( isset( $options['class'] ) ) { + $factory = isset( $options['factory'] ) ? $options['factory'] : null; + $main->getModuleManager()->addModule( $printerName, 'format', $options['class'], $factory ); } $result = $main->getResult(); $result->addArrayType( null, 'default' ); @@ -38,27 +52,42 @@ abstract class ApiFormatTestBase extends MediaWikiTestCase { $result->addValue( null, $k, $v ); } - $printer = $main->createPrinterByName( $this->printerName ); + $ret = []; + $printer = $main->createPrinterByName( $printerName ); $printer->initPrinter(); $printer->execute(); ob_start(); try { $printer->closePrinter(); - return ob_get_clean(); + $ret['text'] = ob_get_clean(); } catch ( Exception $ex ) { ob_end_clean(); throw $ex; } + + if ( !empty( $options['returnPrinter'] ) ) { + $ret['printer'] = $printer; + } + + return count( $ret ) === 1 ? $ret['text'] : $ret; } /** * @dataProvider provideGeneralEncoding + * @param array $data Data to be encoded + * @param string|Exception $expect String to expect, or exception expected to be thrown + * @param array $params Query parameters to set in the FauxRequest + * @param array $options Options to pass to self::encodeData() */ - public function testGeneralEncoding( array $data, $expect, array $params = [] ) { - if ( isset( $params['SKIP'] ) ) { - $this->markTestSkipped( $expect ); + public function testGeneralEncoding( + array $data, $expect, array $params = [], array $options = [] + ) { + if ( $expect instanceof Exception ) { + $this->setExpectedException( get_class( $expect ), $expect->getMessage() ); + $this->encodeData( $params, $data, $options ); // Should throw + } else { + $this->assertSame( $expect, $this->encodeData( $params, $data, $options ) ); } - $this->assertSame( $expect, $this->encodeData( $params, $data ) ); } }