setMwGlobals( [ 'wgSecretKey' => self::SECRET_KEY, ] ); } /** * @covers ::getTemplate */ public function testGetTemplateNeverCacheWithoutSecretKey() { $this->setMwGlobals( 'wgSecretKey', 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' ) ->will( $this->returnCallback( 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' ) ->will( $this->returnCallback( 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' ) ->will( $this->returnCallback( 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' ) ->will( $this->returnCallback( 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' ) ->will( $this->returnCallback( 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' ) ->will( $this->returnCallback( 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 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 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' ) ->will( $this->returnCallback( 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' ) ->will( $this->returnCallback( 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.' ); } }