diff options
Diffstat (limited to 'tests/phpunit/integration/includes/Rest/Handler/PageHTMLHandlerTest.php')
-rw-r--r-- | tests/phpunit/integration/includes/Rest/Handler/PageHTMLHandlerTest.php | 345 |
1 files changed, 345 insertions, 0 deletions
diff --git a/tests/phpunit/integration/includes/Rest/Handler/PageHTMLHandlerTest.php b/tests/phpunit/integration/includes/Rest/Handler/PageHTMLHandlerTest.php new file mode 100644 index 000000000000..10e5ccdc8905 --- /dev/null +++ b/tests/phpunit/integration/includes/Rest/Handler/PageHTMLHandlerTest.php @@ -0,0 +1,345 @@ +<?php + +namespace MediaWiki\Tests\Rest\Handler; + +use BagOStuff; +use DeferredUpdates; +use EmptyBagOStuff; +use Exception; +use ExtensionRegistry; +use HashBagOStuff; +use HashConfig; +use MediaWiki\Json\JsonUnserializer; +use MediaWiki\Parser\ParserCacheFactory; +use MediaWiki\Rest\Handler\PageHTMLHandler; +use MediaWiki\Rest\LocalizedHttpException; +use MediaWiki\Rest\RequestData; +use MediaWikiIntegrationTestCase; +use MWTimestamp; +use NullStatsdDataFactory; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\NullLogger; +use Wikimedia\Message\MessageValue; +use Wikimedia\Parsoid\Core\ClientError; +use Wikimedia\Parsoid\Core\PageBundle; +use Wikimedia\Parsoid\Core\ResourceLimitExceededException; +use Wikimedia\Parsoid\Parsoid; +use Wikimedia\TestingAccessWrapper; +use WikiPage; + +/** + * @covers \MediaWiki\Rest\Handler\PageHTMLHandler + * @group Database + */ +class PageHTMLHandlerTest extends MediaWikiIntegrationTestCase { + use HandlerTestTrait; + + private const WIKITEXT = 'Hello \'\'\'World\'\'\''; + + private const HTML = '<p>Hello <b>World</b></p>'; + + protected function setUp(): void { + parent::setUp(); + + // Clean up these tables after each test + $this->tablesUsed = [ + 'page', + 'revision', + 'comment', + 'text', + 'content' + ]; + } + + /** + * Checks whether Parsoid extension is installed and skips the test if it's not. + */ + private function checkParsoidInstalled() { + if ( !ExtensionRegistry::getInstance()->isLoaded( 'Parsoid' ) ) { + $this->markTestSkipped( 'Skip test, since parsoid is not configured' ); + } + } + + /** + * @param BagOStuff|null $cache + * @param Parsoid|MockObject|null $parsoid + * @return PageHTMLHandler + * @throws Exception + */ + private function newHandler( BagOStuff $cache = null, Parsoid $parsoid = null ): PageHTMLHandler { + $handler = new PageHTMLHandler( + new HashConfig( [ + 'RightsUrl' => 'https://example.com/rights', + 'RightsText' => 'some rights', + ] ), + $this->getServiceContainer()->getPermissionManager(), + $this->getServiceContainer()->getRevisionLookup(), + $this->getServiceContainer()->getTitleFormatter(), + $this->getServiceContainer()->getTitleFactory(), + new ParserCacheFactory( + $cache ?: new EmptyBagOStuff(), + "1234567", + $this->createHookContainer(), + new JsonUnserializer(), + new NullStatsdDataFactory(), + new NullLogger() + ), + $this->getServiceContainer()->getWikiPageFactory() + ); + + if ( $parsoid !== null ) { + $wrapper = TestingAccessWrapper::newFromObject( $handler ); + $wrapper->parsoid = $parsoid; + } + + return $handler; + } + + public function testExecuteBare() { + $page = $this->getExistingTestPage( 'HtmlEndpointTestPage/with/slashes' ); + $request = new RequestData( + [ 'pathParams' => [ 'title' => $page->getTitle()->getPrefixedText() ] ] + ); + + $htmlUrl = 'https://wiki.example.com/rest/v1/page/HtmlEndpointTestPage%2Fwith%2Fslashes/html'; + + $handler = $this->newHandler(); + $config = [ 'format' => 'bare' ]; + $data = $this->executeHandlerAndGetBodyData( $handler, $request, $config ); + + $this->assertResponseData( $page, $data ); + $this->assertSame( $htmlUrl, $data['html_url'] ); + } + + public function testExecuteWithHtml() { + $this->checkParsoidInstalled(); + $page = $this->getExistingTestPage( 'HtmlEndpointTestPage/with/slashes' ); + $this->assertTrue( + $this->editPage( $page, self::WIKITEXT )->isGood(), + 'Sanity: edited a page' + ); + + $request = new RequestData( + [ 'pathParams' => [ 'title' => $page->getTitle()->getPrefixedText() ] ] + ); + + $handler = $this->newHandler(); + $data = $this->executeHandlerAndGetBodyData( $handler, $request, [ + 'format' => 'with_html' + ] ); + + $this->assertResponseData( $page, $data ); + $this->assertStringContainsString( '<!DOCTYPE html>', $data['html'] ); + $this->assertStringContainsString( '<html', $data['html'] ); + $this->assertStringContainsString( self::HTML, $data['html'] ); + } + + public function testExecuteHtmlOnly() { + $this->checkParsoidInstalled(); + $page = $this->getExistingTestPage( 'HtmlEndpointTestPage/with/slashes' ); + $this->assertTrue( + $this->editPage( $page, self::WIKITEXT )->isGood(), + 'Sanity: edited a page' + ); + + $request = new RequestData( + [ 'pathParams' => [ 'title' => $page->getTitle()->getPrefixedText() ] ] + ); + + $handler = $this->newHandler(); + $response = $this->executeHandler( $handler, $request, [ + 'format' => 'html' + ] ); + + $htmlResponse = (string)$response->getBody(); + $this->assertStringContainsString( '<!DOCTYPE html>', $htmlResponse ); + $this->assertStringContainsString( '<html', $htmlResponse ); + $this->assertStringContainsString( self::HTML, $htmlResponse ); + } + + public function testHtmlIsCached() { + $this->checkParsoidInstalled(); + + $page = $this->getExistingTestPage( 'HtmlEndpointTestPage/with/slashes' ); + $request = new RequestData( + [ 'pathParams' => [ 'title' => $page->getTitle()->getPrefixedText() ] ] + ); + + $cache = new HashBagOStuff(); + $parsoid = $this->createNoOpMock( Parsoid::class, [ 'wikitext2html' ] ); + $parsoid->expects( $this->once() ) + ->method( 'wikitext2html' ) + ->willReturn( new PageBundle( 'mocked HTML', null, null, '1.0' ) ); + + $handler = $this->newHandler( $cache, $parsoid ); + $response = $this->executeHandler( $handler, $request, [ + 'format' => 'html' + ] ); + $htmlResponse = (string)$response->getBody(); + $this->assertStringContainsString( 'mocked HTML', $htmlResponse ); + + // check that we can run the test again and ensure that the parse is only run once + $handler = $this->newHandler( $cache, $parsoid ); + $response = $this->executeHandler( $handler, $request, [ + 'format' => 'html' + ] ); + $htmlResponse = (string)$response->getBody(); + $this->assertStringContainsString( 'mocked HTML', $htmlResponse ); + } + + public function testEtagLastModified() { + $this->checkParsoidInstalled(); + + $time = time(); + MWTimestamp::setFakeTime( $time ); + + $page = $this->getExistingTestPage( 'HtmlEndpointTestPage/with/slashes' ); + $request = new RequestData( + [ 'pathParams' => [ 'title' => $page->getTitle()->getPrefixedText() ] ] + ); + + $cache = new HashBagOStuff(); + + // First, test it works if nothing was cached yet. + // Make some time pass since page was created: + MWTimestamp::setFakeTime( $time + 10 ); + $handler = $this->newHandler( $cache ); + $response = $this->executeHandler( $handler, $request, [ + 'format' => 'html' + ] ); + $this->assertArrayHasKey( 'ETag', $response->getHeaders() ); + $etag = $response->getHeaderLine( 'ETag' ); + $this->assertArrayHasKey( 'Last-Modified', $response->getHeaders() ); + $this->assertSame( MWTimestamp::convert( TS_RFC2822, $time ), + $response->getHeaderLine( 'Last-Modified' ) ); + + // Now, test that headers work when getting from cache too. + $handler = $this->newHandler( $cache ); + $response = $this->executeHandler( $handler, $request, [ + 'format' => 'html' + ] ); + $this->assertArrayHasKey( 'ETag', $response->getHeaders() ); + $this->assertNotSame( $etag, $response->getHeaderLine( 'ETag' ) ); + $etag = $response->getHeaderLine( 'ETag' ); + $this->assertArrayHasKey( 'Last-Modified', $response->getHeaders() ); + $this->assertSame( MWTimestamp::convert( TS_RFC2822, $time + 10 ), + $response->getHeaderLine( 'Last-Modified' ) ); + + // Now, expire the cache + $time += 1000; + MWTimestamp::setFakeTime( $time ); + $this->assertTrue( + $page->getTitle()->invalidateCache( MWTimestamp::convert( TS_MW, $time ) ), + 'Sanity: can invalidate cache' + ); + DeferredUpdates::doUpdates(); + + $handler = $this->newHandler( $cache ); + $response = $this->executeHandler( $handler, $request, [ + 'format' => 'html' + ] ); + $this->assertArrayHasKey( 'ETag', $response->getHeaders() ); + $this->assertNotSame( $etag, $response->getHeaderLine( 'ETag' ) ); + $this->assertArrayHasKey( 'Last-Modified', $response->getHeaders() ); + $this->assertSame( MWTimestamp::convert( TS_RFC2822, $time ), + $response->getHeaderLine( 'Last-Modified' ) ); + } + + public function provideHandlesParsoidError() { + yield 'ClientError' => [ + new ClientError( 'TEST_TEST' ), + new LocalizedHttpException( + new MessageValue( 'rest-html-backend-error' ), + 400, + [ + 'reason' => 'TEST_TEST' + ] + ) + ]; + yield 'ResourceLimitExceededException' => [ + new ResourceLimitExceededException( 'TEST_TEST' ), + new LocalizedHttpException( + new MessageValue( 'rest-resource-limit-exceeded' ), + 413, + [ + 'reason' => 'TEST_TEST' + ] + ) + ]; + } + + /** + * @dataProvider provideHandlesParsoidError + * @param Exception $parsoidException + * @param Exception $expectedException + */ + public function testHandlesParsoidError( + Exception $parsoidException, + Exception $expectedException + ) { + $this->checkParsoidInstalled(); + + $page = $this->getExistingTestPage( 'HtmlEndpointTestPage/with/slashes' ); + $request = new RequestData( + [ 'pathParams' => [ 'title' => $page->getTitle()->getPrefixedText() ] ] + ); + + $parsoid = $this->createNoOpMock( Parsoid::class, [ 'wikitext2html' ] ); + $parsoid->expects( $this->once() ) + ->method( 'wikitext2html' ) + ->willThrowException( $parsoidException ); + + $handler = $this->newHandler( null, $parsoid ); + $this->expectExceptionObject( $expectedException ); + $this->executeHandler( $handler, $request, [ + 'format' => 'html' + ] ); + } + + public function testExecute_missingparam() { + $request = new RequestData(); + + $this->expectExceptionObject( + new LocalizedHttpException( + new MessageValue( "paramvalidator-missingparam", [ 'title' ] ), + 400 + ) + ); + + $handler = $this->newHandler(); + $this->executeHandler( $handler, $request ); + } + + public function testExecute_error() { + $request = new RequestData( [ 'pathParams' => [ 'title' => 'DoesNotExist8237456assda1234' ] ] ); + + $this->expectExceptionObject( + new LocalizedHttpException( + new MessageValue( "rest-nonexistent-title", [ 'testing' ] ), + 404 + ) + ); + + $handler = $this->newHandler(); + $this->executeHandler( $handler, $request ); + } + + /** + * @param WikiPage $page + * @param array $data + */ + private function assertResponseData( WikiPage $page, array $data ): void { + $this->assertSame( $page->getId(), $data['id'] ); + $this->assertSame( $page->getTitle()->getPrefixedDBkey(), $data['key'] ); + $this->assertSame( $page->getTitle()->getPrefixedText(), $data['title'] ); + $this->assertSame( $page->getLatest(), $data['latest']['id'] ); + $this->assertSame( + wfTimestampOrNull( TS_ISO_8601, $page->getTimestamp() ), + $data['latest']['timestamp'] + ); + $this->assertSame( CONTENT_MODEL_WIKITEXT, $data['content_model'] ); + $this->assertSame( 'https://example.com/rights', $data['license']['url'] ); + $this->assertSame( 'some rights', $data['license']['title'] ); + } + +} |