diff options
author | daniel <dkinzler@wikimedia.org> | 2020-11-20 21:46:41 +0100 |
---|---|---|
committer | Ppchelko <ppchelko@wikimedia.org> | 2020-12-02 18:08:12 +0000 |
commit | b98f7a6fc1bd2b6ffd8e2df46d4f6449e14e9227 (patch) | |
tree | 4ee35a6f9a32217d06ee5e8f93cc23c39eed583d /tests/phpunit/integration | |
parent | d2565533c4c68ac27dd3a992d74a6a7f4dbd0a58 (diff) | |
download | mediawikicore-b98f7a6fc1bd2b6ffd8e2df46d4f6449e14e9227.tar.gz mediawikicore-b98f7a6fc1bd2b6ffd8e2df46d4f6449e14e9227.zip |
Extract helper classes from PageHTMLHandler
This extracts two helper classes from PageHTMLHandler:
* PageContentHelper for accessing page content. This replaces the
LatestRevisionContentHandler mase class.
* ParsoidHtmlHelper for generating HTML from wikitext using parsoid.
The idea is to decouple the functionality from the REST handlers, so we
can easily mix and match functionality to create a handler for the
new per-revision HTML endpoint.
Bug: T267981
Bug: T267982
Change-Id: I3226833d12e51c959712d642b0195de1fe1ef979
Diffstat (limited to 'tests/phpunit/integration')
3 files changed, 494 insertions, 2 deletions
diff --git a/tests/phpunit/integration/includes/Rest/Handler/PageContentHelperTest.php b/tests/phpunit/integration/includes/Rest/Handler/PageContentHelperTest.php new file mode 100644 index 000000000000..e3c06fbedddd --- /dev/null +++ b/tests/phpunit/integration/includes/Rest/Handler/PageContentHelperTest.php @@ -0,0 +1,277 @@ +<?php + +namespace MediaWiki\Tests\Rest\Helper; + +use HashConfig; +use MediaWiki\Rest\Handler\PageContentHelper; +use MediaWiki\Rest\HttpException; +use MediaWiki\Rest\Response; +use MediaWiki\Storage\RevisionRecord; +use MediaWiki\Storage\SlotRecord; +use MediaWikiIntegrationTestCase; +use Title; + +/** + * @covers \MediaWiki\Rest\Handler\PageContentHelper + * @group Database + */ +class PageContentHelperTest extends MediaWikiIntegrationTestCase { + + private const NO_REVISION_ETAG = '"b620cd7841f9ea8f545f11cc44ce794f848fa2d3"'; + + protected function setUp(): void { + parent::setUp(); + + // Clean up these tables after each test + $this->tablesUsed = [ + 'page', + 'revision', + 'comment', + 'text', + 'content' + ]; + } + + /** + * @return PageContentHelper + */ + private function newHelper( $params = [], $user = null ): PageContentHelper { + $helper = new PageContentHelper( + new HashConfig( [ + 'RightsUrl' => 'https://example.com/rights', + 'RightsText' => 'some rights', + ] ), + $this->getServiceContainer()->getPermissionManager(), + $this->getServiceContainer()->getRevisionLookup(), + $this->getServiceContainer()->getTitleFormatter(), + $this->getServiceContainer()->getTitleFactory() + ); + + $user = $user ?: $this->getTestUser()->getUser(); + $helper->init( $user, $params ); + return $helper; + } + + /** + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getRole() + */ + public function testGetRole() { + $helper = $this->newHelper(); + $this->assertSame( SlotRecord::MAIN, $helper->getRole() ); + } + + /** + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getTitleText() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getTitle() + */ + public function testGetTitle() { + $helper = $this->newHelper( [ 'title' => 'Foo' ] ); + $this->assertSame( 'Foo', $helper->getTitleText() ); + + $this->assertInstanceOf( Title::class, $helper->getTitle() ); + $this->assertSame( 'Foo', $helper->getTitle()->getPrefixedDBkey() ); + } + + /** + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getTargetRevision() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getPageContent() + */ + public function testGetTargetRevisionAndContent() { + $page = $this->getExistingTestPage( __METHOD__ ); + $rev = $page->getRevisionRecord(); + + $helper = $this->newHelper( [ 'title' => $page->getTitle()->getPrefixedDBkey() ] ); + + $targetRev = $helper->getTargetRevision(); + $this->assertInstanceOf( RevisionRecord::class, $targetRev ); + $this->assertSame( $rev->getId(), $targetRev->getId() ); + + $pageContent = $helper->getPageContent(); + $this->assertSame( + $rev->getContent( SlotRecord::MAIN )->serialize(), + $pageContent->serialize() + ); + } + + /** + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getTitleText() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getTitle() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::isAccessible() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::hasContent() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getTargetRevision() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getPageContent() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getLastModified() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getETag() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::checkAccess() + */ + public function testNoTitle() { + $helper = $this->newHelper(); + + $this->assertNull( $helper->getTitleText() ); + $this->assertFalse( $helper->getTitle() ); + + $this->assertFalse( $helper->hasContent() ); + $this->assertFalse( $helper->isAccessible() ); + + $this->assertFalse( $helper->getTargetRevision() ); + + $this->assertNull( $helper->getLastModified() ); + $this->assertSame( self::NO_REVISION_ETAG, $helper->getETag() ); + + try { + $helper->getPageContent(); + $this->fail( 'Expected HttpException' ); + } catch ( HttpException $ex ) { + $this->assertSame( 404, $ex->getCode() ); + } + + try { + $helper->checkAccess(); + $this->fail( 'Expected HttpException' ); + } catch ( HttpException $ex ) { + $this->assertSame( 404, $ex->getCode() ); + } + } + + /** + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getTitleText() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getTitle() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::isAccessible() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::hasContent() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getTargetRevision() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getPageContent() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getLastModified() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getETag() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::checkAccess() + */ + public function testNonExistingPage() { + $page = $this->getNonexistingTestPage( __METHOD__ ); + $title = $page->getTitle(); + $helper = $this->newHelper( [ 'title' => $title->getPrefixedDBkey() ] ); + + $this->assertSame( $title->getPrefixedDBkey(), $helper->getTitleText() ); + $this->assertSame( $title->getPrefixedDBkey(), $helper->getTitle()->getPrefixedDBkey() ); + + $this->assertFalse( $helper->hasContent() ); + $this->assertFalse( $helper->isAccessible() ); + + $this->assertFalse( $helper->getTargetRevision() ); + + $this->assertNull( $helper->getLastModified() ); + $this->assertSame( self::NO_REVISION_ETAG, $helper->getETag() ); + + try { + $helper->getPageContent(); + $this->fail( 'Expected HttpException' ); + } catch ( HttpException $ex ) { + $this->assertSame( 404, $ex->getCode() ); + } + + try { + $helper->checkAccess(); + $this->fail( 'Expected HttpException' ); + } catch ( HttpException $ex ) { + $this->assertSame( 404, $ex->getCode() ); + } + } + + /** + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getTitleText() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getTitle() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::isAccessible() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::hasContent() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getTargetRevision() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getPageContent() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getLastModified() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getETag() + * @covers \MediaWiki\Rest\Handler\PageContentHelper::checkAccess() + */ + public function testForbidenPage() { + $this->mergeMwGlobalArrayValue( + 'wgGroupPermissions', [ + '*' => [ 'read' => false ], + 'user' => [ 'read' => false ], + 'autoconfirmed' => [ 'read' => false ], + ] + ); + + $user = $this->getTestUser()->getUser(); + + $page = $this->getExistingTestPage( __METHOD__ ); + $title = $page->getTitle(); + $helper = $this->newHelper( [ 'title' => $title->getPrefixedDBkey() ], $user ); + + $this->assertSame( $title->getPrefixedDBkey(), $helper->getTitleText() ); + $this->assertSame( $title->getPrefixedDBkey(), $helper->getTitle()->getPrefixedDBkey() ); + + $this->assertTrue( $helper->hasContent() ); + $this->assertFalse( $helper->isAccessible() ); + + $this->assertNull( $helper->getLastModified() ); + + try { + $helper->checkAccess(); + $this->fail( 'Expected HttpException' ); + } catch ( HttpException $ex ) { + $this->assertSame( 403, $ex->getCode() ); + } + } + + /** + * @covers \MediaWiki\Rest\Handler\PageContentHelper::getParamSettings() + */ + public function testParameterSettings() { + $helper = $this->newHelper(); + $settings = $helper->getParamSettings(); + $this->assertArrayHasKey( 'title', $settings ); + } + + /** + * @covers \MediaWiki\Rest\Handler\PageContentHelper::setCacheControl() + */ + public function testCacheControl() { + $helper = $this->newHelper(); + + $response = new Response(); + + $helper->setCacheControl( $response ); // default + $this->assertStringContainsString( 'max-age=5', $response->getHeaderLine( 'Cache-Control' ) ); + + $helper->setCacheControl( $response, 2 ); // explicit + $this->assertStringContainsString( 'max-age=2', $response->getHeaderLine( 'Cache-Control' ) ); + + $helper->setCacheControl( $response, 1000 * 1000 ); // too big + $this->assertStringContainsString( 'max-age=5', $response->getHeaderLine( 'Cache-Control' ) ); + } + + /** + * @covers \MediaWiki\Rest\Handler\PageContentHelper::constructMetadata() + */ + public function testConstructMetadata() { + $page = $this->getExistingTestPage( __METHOD__ ); + $title = $page->getTitle(); + + $revision = $page->getRevisionRecord(); + $content = $revision->getContent( SlotRecord::MAIN ); + $expected = [ + 'id' => $title->getArticleID(), + 'key' => $title->getPrefixedDBkey(), + 'title' => $title->getPrefixedText(), + 'latest' => [ + 'id' => $revision->getId(), + 'timestamp' => wfTimestampOrNull( TS_ISO_8601, $revision->getTimestamp() ) + ], + 'content_model' => $content->getModel(), + 'license' => [ + 'url' => 'https://example.com/rights', + 'title' => 'some rights', + ] + ]; + + $helper = $this->newHelper( [ 'title' => $title->getPrefixedDBkey() ] ); + $data = $helper->constructMetadata(); + + $this->assertEquals( $expected, $data ); + } + +} diff --git a/tests/phpunit/integration/includes/Rest/Handler/PageHTMLHandlerTest.php b/tests/phpunit/integration/includes/Rest/Handler/PageHTMLHandlerTest.php index fecc42199f95..94bfa643999e 100644 --- a/tests/phpunit/integration/includes/Rest/Handler/PageHTMLHandlerTest.php +++ b/tests/phpunit/integration/includes/Rest/Handler/PageHTMLHandlerTest.php @@ -88,8 +88,9 @@ class PageHTMLHandlerTest extends MediaWikiIntegrationTestCase { ); if ( $parsoid !== null ) { - $wrapper = TestingAccessWrapper::newFromObject( $handler ); - $wrapper->parsoid = $parsoid; + $handlerWrapper = TestingAccessWrapper::newFromObject( $handler ); + $helperWrapper = TestingAccessWrapper::newFromObject( $handlerWrapper->htmlHelper ); + $helperWrapper->parsoid = $parsoid; } return $handler; diff --git a/tests/phpunit/integration/includes/Rest/Handler/ParsoidHTMLHelperTest.php b/tests/phpunit/integration/includes/Rest/Handler/ParsoidHTMLHelperTest.php new file mode 100644 index 000000000000..c295c28e7156 --- /dev/null +++ b/tests/phpunit/integration/includes/Rest/Handler/ParsoidHTMLHelperTest.php @@ -0,0 +1,214 @@ +<?php + +namespace MediaWiki\Tests\Rest\Helper; + +use BagOStuff; +use DeferredUpdates; +use EmptyBagOStuff; +use Exception; +use ExtensionRegistry; +use HashBagOStuff; +use MediaWiki\Rest\Handler\ParsoidHTMLHelper; +use MediaWiki\Rest\LocalizedHttpException; +use MediaWikiIntegrationTestCase; +use MWTimestamp; +use NullStatsdDataFactory; +use ParserCache; +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; + +/** + * @covers \MediaWiki\Rest\Handler\ParsoidHTMLHelper + * @group Database + */ +class ParsoidHTMLHelperTest extends MediaWikiIntegrationTestCase { + + private const WIKITEXT = 'Hello \'\'\'World\'\'\''; + + private const HTML = '<p>Hello <b>World</b></p>'; + + protected function setUp(): void { + parent::setUp(); + + if ( !ExtensionRegistry::getInstance()->isLoaded( 'Parsoid' ) ) { + $this->markTestSkipped( 'Parsoid is not configured' ); + } + + // Clean up these tables after each test + $this->tablesUsed = [ + 'page', + 'revision', + 'comment', + 'text', + 'content' + ]; + } + + /** + * @param BagOStuff|null $cache + * @param Parsoid|MockObject|null $parsoid + * @return ParsoidHTMLHelper + * @throws Exception + */ + private function newHelper( BagOStuff $cache = null, Parsoid $parsoid = null ): ParsoidHTMLHelper { + $parserCache = new ParserCache( + 'Test', + $cache ?: new EmptyBagOStuff(), + 0, + $this->getServiceContainer()->getHookContainer(), + $this->getServiceContainer()->getJsonCodec(), + new NullStatsdDataFactory(), + new NullLogger() + ); + + $helper = new ParsoidHTMLHelper( + $parserCache, + $this->getServiceContainer()->getWikiPageFactory() + ); + + if ( $parsoid !== null ) { + $wrapper = TestingAccessWrapper::newFromObject( $helper ); + $wrapper->parsoid = $parsoid; + } + + return $helper; + } + + public function testGetHtml() { + $page = $this->getExistingTestPage( 'HtmlHelperTestPage/with/slashes' ); + $this->assertTrue( + $this->editPage( $page, self::WIKITEXT )->isGood(), + 'Sanity: edited a page' + ); + + $helper = $this->newHelper(); + $helper->init( $page->getTitle() ); + + $htmlresult = $helper->getHtml()->getRawText(); + + $this->assertStringContainsString( '<!DOCTYPE html>', $htmlresult ); + $this->assertStringContainsString( '<html', $htmlresult ); + $this->assertStringContainsString( self::HTML, $htmlresult ); + } + + public function testHtmlIsCached() { + $page = $this->getExistingTestPage( 'HtmlHelperTestPage/with/slashes' ); + + $cache = new HashBagOStuff(); + $parsoid = $this->createNoOpMock( Parsoid::class, [ 'wikitext2html' ] ); + $parsoid->expects( $this->once() ) + ->method( 'wikitext2html' ) + ->willReturn( new PageBundle( 'mocked HTML', null, null, '1.0' ) ); + + $helper = $this->newHelper( $cache, $parsoid ); + + $helper->init( $page->getTitle() ); + $htmlresult = $helper->getHtml()->getRawText(); + $this->assertStringContainsString( 'mocked HTML', $htmlresult ); + + // check that we can run the test again and ensure that the parse is only run once + $helper = $this->newHelper( $cache, $parsoid ); + $helper->init( $page->getTitle() ); + $htmlresult = $helper->getHtml()->getRawText(); + $this->assertStringContainsString( 'mocked HTML', $htmlresult ); + } + + public function testEtagLastModified() { + $time = time(); + MWTimestamp::setFakeTime( $time ); + + $page = $this->getExistingTestPage( 'HtmlHelperTestPage/with/slashes' ); + + $cache = new HashBagOStuff(); + + // First, test it works if nothing was cached yet. + // Make some time pass since page was created: + MWTimestamp::setFakeTime( $time + 10 ); + $helper = $this->newHelper( $cache ); + $helper->init( $page->getTitle() ); + $etag = $helper->getETag(); // remember etag using LastModified + $helper->getHtml(); // put HTML into the cache + + // Now, test that headers work when getting from cache too. + $helper = $this->newHelper( $cache ); + $helper->init( $page->getTitle() ); + + $this->assertNotSame( $etag, $helper->getETag() ); + $etag = $helper->getETag(); + $this->assertSame( + MWTimestamp::convert( TS_RFC2822, $time + 10 ), + MWTimestamp::convert( TS_RFC2822, $helper->getLastModified() ) + ); + + // 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(); + + $helper = $this->newHelper( $cache ); + $helper->init( $page->getTitle() ); + + $this->assertNotSame( $etag, $helper->getETag() ); + $this->assertSame( + MWTimestamp::convert( TS_RFC2822, $time ), + MWTimestamp::convert( TS_RFC2822, $helper->getLastModified() ) + ); + } + + 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 + ) { + $page = $this->getExistingTestPage( 'HtmlHelperTestPage/with/slashes' ); + + $parsoid = $this->createNoOpMock( Parsoid::class, [ 'wikitext2html' ] ); + $parsoid->expects( $this->once() ) + ->method( 'wikitext2html' ) + ->willThrowException( $parsoidException ); + + $helper = $this->newHelper( null, $parsoid ); + $helper->init( $page->getTitle() ); + + $this->expectExceptionObject( $expectedException ); + $helper->getHtml(); + } + +} |