diff options
author | Umherirrender <umherirrender_de.wp@web.de> | 2023-09-12 20:28:21 +0200 |
---|---|---|
committer | Umherirrender <umherirrender_de.wp@web.de> | 2023-09-13 00:09:05 +0200 |
commit | 790ae736c165bc6b2e2cc893c1b0be2cbd2628b3 (patch) | |
tree | b420e7567fe0e701fabef992d16b6bc370b3f4e5 /tests/phpunit/integration/includes/Html/TemplateParserIntegrationTest.php | |
parent | 707450bc74891a125f8826b0a7c004ea3c79c48c (diff) | |
download | mediawikicore-790ae736c165bc6b2e2cc893c1b0be2cbd2628b3.tar.gz mediawikicore-790ae736c165bc6b2e2cc893c1b0be2cbd2628b3.zip |
tests: Move test cases from /includes/ into sub folder
Follow move of the tested class
Most moves are part of T321882
Change-Id: I74ab45d6a5331dcb2ff0b65dc2cc7c6315146646
Diffstat (limited to 'tests/phpunit/integration/includes/Html/TemplateParserIntegrationTest.php')
-rw-r--r-- | tests/phpunit/integration/includes/Html/TemplateParserIntegrationTest.php | 353 |
1 files changed, 353 insertions, 0 deletions
diff --git a/tests/phpunit/integration/includes/Html/TemplateParserIntegrationTest.php b/tests/phpunit/integration/includes/Html/TemplateParserIntegrationTest.php new file mode 100644 index 000000000000..6964af96415e --- /dev/null +++ b/tests/phpunit/integration/includes/Html/TemplateParserIntegrationTest.php @@ -0,0 +1,353 @@ +<?php + +use MediaWiki\Html\TemplateParser; +use MediaWiki\MainConfigNames; + +/** + * @group Templates + * @coversDefaultClass MediaWiki\Html\TemplateParser + */ +class TemplateParserIntegrationTest extends MediaWikiIntegrationTestCase { + + private const NAME = 'foobar'; + private const RESULT = "hello world!\n"; + private const DIR = __DIR__ . '/../../../data/templates'; + private const SECRET_KEY = 'foo'; + + protected function setUp(): void { + parent::setUp(); + + $this->overrideConfigValue( MainConfigNames::SecretKey, self::SECRET_KEY ); + } + + /** + * @covers ::getTemplate + */ + public function testGetTemplateNeverCacheWithoutSecretKey() { + $this->overrideConfigValue( MainConfigNames::SecretKey, false ); + + // Expect no cache interaction + $cache = $this->createMock( BagOStuff::class ); + $cache->expects( $this->never() )->method( 'get' ); + $cache->expects( $this->never() )->method( 'set' ); + + $tp = new TemplateParser( self::DIR, $cache ); + $this->assertEquals( self::RESULT, $tp->processTemplate( self::NAME, [] ) ); + } + + /** + * @covers ::getTemplate + */ + public function testGetTemplateCachesCompilationResult() { + $store = null; + + // 1. Expect a cache miss, compile, and cache set + $cache1 = $this->createMock( BagOStuff::class ); + $cache1->expects( $this->once() )->method( 'get' )->willReturn( false ); + $cache1->expects( $this->once() )->method( 'set' ) + ->willReturnCallback( static function ( $key, $val ) use ( &$store ) { + $store = [ 'key' => $key, 'val' => $val ]; + } ); + + $tp1 = new TemplateParser( self::DIR, $cache1 ); + $this->assertEquals( self::RESULT, $tp1->processTemplate( self::NAME, [] ) ); + + // Inspect cache + $this->assertEquals( + [ + 'phpCode', + 'files', + 'filesHash', + 'integrityHash', + ], + array_keys( $store['val'] ), + 'keys of the cached array' + ); + $this->assertEquals( + FileContentsHasher::getFileContentsHash( [ + self::DIR . '/' . self::NAME . '.mustache' + ] ), + $store['val']['filesHash'], + 'content hash for the compiled template' + ); + $this->assertEquals( + hash_hmac( 'sha256', $store['val']['phpCode'], self::SECRET_KEY ), + $store['val']['integrityHash'], + 'integrity hash for the compiled template' + ); + + // 2. Expect a cache hit that passes validation checks, and no compilation + $cache2 = $this->createMock( BagOStuff::class ); + $cache2->expects( $this->once() )->method( 'get' ) + ->willReturnCallback( static function ( $key ) use ( &$store ) { + return $key === $store['key'] ? $store['val'] : false; + } ); + $cache2->expects( $this->never() )->method( 'set' ); + + $tp2 = $this->getMockBuilder( TemplateParser::class ) + ->setConstructorArgs( [ self::DIR, $cache2 ] ) + ->onlyMethods( [ 'compile' ] ) + ->getMock(); + $tp2->expects( $this->never() )->method( 'compile' ); + + $this->assertEquals( self::RESULT, $tp2->processTemplate( self::NAME, [] ) ); + } + + /** + * @covers ::getTemplate + */ + public function testGetTemplateInvalidatesCacheWhenFilesHashIsInvalid() { + $store = null; + + // 1. Expect a cache miss, compile, and cache set + $cache1 = $this->createMock( BagOStuff::class ); + $cache1->expects( $this->once() )->method( 'get' )->willReturn( false ); + $cache1->expects( $this->once() )->method( 'set' ) + ->willReturnCallback( static function ( $key, $val ) use ( &$store ) { + $store = [ 'key' => $key, 'val' => $val ]; + } ); + + $tp1 = new TemplateParser( self::DIR, $cache1 ); + $this->assertEquals( self::RESULT, $tp1->processTemplate( self::NAME, [] ) ); + + // Invalidate file hash + $store['val']['filesHash'] = 'baz'; + + // 2. Expect a cache hit that fails validation, and a re-compilation + $cache2 = $this->createMock( BagOStuff::class ); + $cache2->expects( $this->once() )->method( 'get' ) + ->willReturnCallback( static function ( $key ) use ( &$store ) { + return $key === $store['key'] ? $store['val'] : false; + } ); + $cache2->expects( $this->once() )->method( 'set' ); + + $tp2 = $this->getMockBuilder( TemplateParser::class ) + ->setConstructorArgs( [ self::DIR, $cache2 ] ) + ->onlyMethods( [ 'compile' ] ) + ->getMock(); + $tp2->expects( $this->once() )->method( 'compile' ) + ->willReturn( $store['val'] ); + + $this->assertEquals( self::RESULT, $tp2->processTemplate( self::NAME, [] ) ); + } + + /** + * @covers ::getTemplate + */ + public function testGetTemplateInvalidatesCacheWhenIntegrityHashIsInvalid() { + $store = null; + + // 1. Cache miss, expect a compile and cache set + $cache1 = $this->createMock( BagOStuff::class ); + $cache1->expects( $this->once() )->method( 'get' )->willReturn( false ); + $cache1->expects( $this->once() )->method( 'set' ) + ->willReturnCallback( static function ( $key, $val ) use ( &$store ) { + $store = [ 'key' => $key, 'val' => $val ]; + } ); + + $tp1 = new TemplateParser( self::DIR, $cache1 ); + $this->assertEquals( self::RESULT, $tp1->processTemplate( self::NAME, [] ) ); + + // Invalidate integrity hash + $store['val']['integrityHash'] = 'foo'; + + // 2. Expect a cache hit that fails validation, and a re-compilation + $cache2 = $this->createMock( BagOStuff::class ); + $cache2->expects( $this->once() )->method( 'get' ) + ->willReturnCallback( static function ( $key ) use ( &$store ) { + return $key === $store['key'] ? $store['val'] : false; + } ); + $cache2->expects( $this->once() )->method( 'set' ); + + $tp2 = $this->getMockBuilder( TemplateParser::class ) + ->setConstructorArgs( [ self::DIR, $cache2 ] ) + ->onlyMethods( [ 'compile' ] ) + ->getMock(); + $tp2->expects( $this->once() )->method( 'compile' ) + ->willReturn( $store['val'] ); + + $this->assertEquals( self::RESULT, $tp2->processTemplate( self::NAME, [] ) ); + } + + /** + * @dataProvider provideProcessTemplate + * @covers MediaWiki\Html\TemplateParser + */ + public function testProcessTemplate( $name, $args, $result, $exception = false ) { + $tp = new TemplateParser( self::DIR, new EmptyBagOStuff ); + if ( $exception ) { + $this->expectException( $exception ); + } + $this->assertEquals( $result, $tp->processTemplate( $name, $args ) ); + } + + public static function provideProcessTemplate() { + return [ + [ + 'foobar', + [], + "hello world!\n" + ], + [ + 'foobar_args', + [ + 'planet' => 'world', + ], + self::RESULT, + ], + [ + '../foobar', + [], + false, + UnexpectedValueException::class + ], + [ + "\000../foobar", + [], + false, + UnexpectedValueException::class + ], + [ + '/', + [], + false, + UnexpectedValueException::class + ], + [ + // Allegedly this can strip ext in windows. + 'baz<', + [], + false, + UnexpectedValueException::class + ], + [ + '\\foo', + [], + false, + UnexpectedValueException::class + ], + [ + 'C:\bar', + [], + false, + UnexpectedValueException::class + ], + [ + "foo\000bar", + [], + false, + UnexpectedValueException::class + ], + [ + 'nonexistenttemplate', + [], + false, + RuntimeException::class, + ], + [ + 'has_partial', + [ + 'planet' => 'world', + ], + "Partial hello world!\n in here\n", + ], + [ + 'bad_partial', + [], + false, + Exception::class, + ], + [ + 'invalid_syntax', + [], + false, + Exception::class + ], + [ + 'parentvars', + [ + 'foo' => 'f', + 'bar' => [ + [ 'baz' => 'x' ], + [ 'baz' => 'y' ] + ] + ], + "f\n\tf x\n\tf y\n" + ] + ]; + } + + /** + * @covers ::enableRecursivePartials + */ + public function testEnableRecursivePartials() { + $tp = new TemplateParser( self::DIR, new EmptyBagOStuff ); + $data = [ 'r' => [ 'r' => [ 'r' => [] ] ] ]; + + $tp->enableRecursivePartials( true ); + $this->assertEquals( 'rrr', $tp->processTemplate( 'recurse', $data ) ); + + $tp->enableRecursivePartials( false ); + $this->expectException( Exception::class ); + $tp->processTemplate( 'recurse', $data ); + } + + /** + * @covers MediaWiki\Html\TemplateParser::compile + */ + public function testCompileReturnsPHPCodeAndMetadata() { + $store = null; + + // 1. Expect a compile and cache set + $cache = $this->createMock( BagOStuff::class ); + $cache->expects( $this->once() )->method( 'get' )->willReturn( false ); + $cache->expects( $this->once() )->method( 'set' ) + ->willReturnCallback( static function ( $key, $val ) use ( &$store ) { + $store = [ 'key' => $key, 'val' => $val ]; + } ); + $tp = new TemplateParser( self::DIR, $cache ); + $tp->processTemplate( 'has_partial', [] ); + + // 2. Inspect cache + $expectedFiles = [ + self::DIR . '/has_partial.mustache', + self::DIR . '/foobar_args.mustache', + ]; + $this->assertEquals( + $expectedFiles, + $store['val']['files'], + 'track all files read during the compilation' + ); + $this->assertEquals( + FileContentsHasher::getFileContentsHash( $expectedFiles ), + $store['val'][ 'filesHash' ], + 'hash of all files read during the compilation' + ); + } + + /** + * @covers ::getTemplate + */ + public function testGetTemplateCachingHandlesRecursivePartials() { + $store = null; + + $cache = $this->createMock( BagOStuff::class ); + $cache->expects( $this->once() )->method( 'get' )->willReturn( false ); + $cache->expects( $this->once() )->method( 'set' ) + ->willReturnCallback( static function ( $key, $val ) use ( &$store ) { + $store = [ 'key' => $key, 'val' => $val ]; + } ); + + $tp = new TemplateParser( self::DIR, $cache ); + $tp->enableRecursivePartials( true ); + + $data = [ 'r' => [ 'r' => [ 'r' => [] ] ] ]; + $tp->processTemplate( 'recurse', $data ); + + $this->assertArrayEquals( + [ self::DIR . '/recurse.mustache' ], + $store['val']['files'], + 'The hash is computed from unique template files.' + ); + } +} |