diff options
Diffstat (limited to 'www/wiki/tests/phpunit/includes/json/FormatJsonTest.php')
-rw-r--r-- | www/wiki/tests/phpunit/includes/json/FormatJsonTest.php | 375 |
1 files changed, 375 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/json/FormatJsonTest.php b/www/wiki/tests/phpunit/includes/json/FormatJsonTest.php new file mode 100644 index 00000000..a4ab879f --- /dev/null +++ b/www/wiki/tests/phpunit/includes/json/FormatJsonTest.php @@ -0,0 +1,375 @@ +<?php + +/** + * @covers FormatJson + */ +class FormatJsonTest extends MediaWikiTestCase { + + public static function provideEncoderPrettyPrinting() { + return [ + // Four spaces + [ true, ' ' ], + [ ' ', ' ' ], + // Two spaces + [ ' ', ' ' ], + // One tab + [ "\t", "\t" ], + ]; + } + + /** + * @dataProvider provideEncoderPrettyPrinting + */ + public function testEncoderPrettyPrinting( $pretty, $expectedIndent ) { + $obj = [ + 'emptyObject' => new stdClass, + 'emptyArray' => [], + 'string' => 'foobar\\', + 'filledArray' => [ + [ + 123, + 456, + ], + // Nested json works without problems + '"7":["8",{"9":"10"}]', + // Whitespace clean up doesn't touch strings that look alike + "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}", + ], + ]; + + // No trailing whitespace, no trailing linefeed + $json = '{ + "emptyObject": {}, + "emptyArray": [], + "string": "foobar\\\\", + "filledArray": [ + [ + 123, + 456 + ], + "\"7\":[\"8\",{\"9\":\"10\"}]", + "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}" + ] +}'; + + $json = str_replace( "\r", '', $json ); // Windows compat + $json = str_replace( "\t", $expectedIndent, $json ); + $this->assertSame( $json, FormatJson::encode( $obj, $pretty ) ); + } + + public static function provideEncodeDefault() { + return self::getEncodeTestCases( [] ); + } + + /** + * @dataProvider provideEncodeDefault + */ + public function testEncodeDefault( $from, $to ) { + $this->assertSame( $to, FormatJson::encode( $from ) ); + } + + public static function provideEncodeUtf8() { + return self::getEncodeTestCases( [ 'unicode' ] ); + } + + /** + * @dataProvider provideEncodeUtf8 + */ + public function testEncodeUtf8( $from, $to ) { + $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::UTF8_OK ) ); + } + + public static function provideEncodeXmlMeta() { + return self::getEncodeTestCases( [ 'xmlmeta' ] ); + } + + /** + * @dataProvider provideEncodeXmlMeta + */ + public function testEncodeXmlMeta( $from, $to ) { + $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::XMLMETA_OK ) ); + } + + public static function provideEncodeAllOk() { + return self::getEncodeTestCases( [ 'unicode', 'xmlmeta' ] ); + } + + /** + * @dataProvider provideEncodeAllOk + */ + public function testEncodeAllOk( $from, $to ) { + $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::ALL_OK ) ); + } + + public function testEncodePhpBug46944() { + $this->assertNotEquals( + '\ud840\udc00', + strtolower( FormatJson::encode( "\xf0\xa0\x80\x80" ) ), + 'Test encoding an broken json_encode character (U+20000)' + ); + } + + public function testDecodeReturnType() { + $this->assertInternalType( + 'object', + FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}' ), + 'Default to object' + ); + + $this->assertInternalType( + 'array', + FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}', true ), + 'Optional array' + ); + } + + public static function provideParse() { + return [ + [ null ], + [ true ], + [ false ], + [ 0 ], + [ 1 ], + [ 1.2 ], + [ '' ], + [ 'str' ], + [ [ 0, 1, 2 ] ], + [ [ 'a' => 'b' ] ], + [ [ 'a' => 'b' ] ], + [ [ 'a' => 'b', 'x' => [ 'c' => 'd' ] ] ], + ]; + } + + /** + * Recursively convert arrays into stdClass + * @param array|string|bool|int|float|null $value + * @return stdClass|string|bool|int|float|null + */ + public static function toObject( $value ) { + return !is_array( $value ) ? $value : (object)array_map( __METHOD__, $value ); + } + + /** + * @dataProvider provideParse + * @param mixed $value + */ + public function testParse( $value ) { + $expected = self::toObject( $value ); + $json = FormatJson::encode( $expected, false, FormatJson::ALL_OK ); + $this->assertJson( $json ); + + $st = FormatJson::parse( $json ); + $this->assertInstanceOf( Status::class, $st ); + $this->assertTrue( $st->isGood() ); + $this->assertEquals( $expected, $st->getValue() ); + + $st = FormatJson::parse( $json, FormatJson::FORCE_ASSOC ); + $this->assertInstanceOf( Status::class, $st ); + $this->assertTrue( $st->isGood() ); + $this->assertEquals( $value, $st->getValue() ); + } + + /** + * Test data for testParseTryFixing. + * + * Some PHP interpreters use json-c rather than the JSON.org cannonical + * parser to avoid being encumbered by the "shall be used for Good, not + * Evil" clause of the JSON.org parser's license. By default, json-c + * parses in a non-strict mode which allows trailing commas for array and + * object delarations among other things, so our JSON_ERROR_SYNTAX rescue + * block is not always triggered. It however isn't lenient in exactly the + * same ways as our TRY_FIXING mode, so the assertions in this test are + * a bit more complicated than they ideally would be: + * + * Optional third argument: true if json-c parses the value without + * intervention, false otherwise. Defaults to true. + * + * Optional fourth argument: expected cannonical JSON serialization of + * json-c parsed result. Defaults to the second argument's value. + */ + public static function provideParseTryFixing() { + return [ + [ "[,]", '[]', false ], + [ "[ , ]", '[]', false ], + [ "[ , }", false ], + [ '[1],', false, true, '[1]' ], + [ "[1,]", '[1]' ], + [ "[1\n,]", '[1]' ], + [ "[1,\n]", '[1]' ], + [ "[1,]\n", '[1]' ], + [ "[1\n,\n]\n", '[1]' ], + [ '["a,",]', '["a,"]' ], + [ "[[1,]\n,[2,\n],[3\n,]]", '[[1],[2],[3]]' ], + // I wish we could parse this, but would need quote parsing + [ '[[1,],[2,],[3,]]', false, true, '[[1],[2],[3]]' ], + [ '[1,,]', false, false, '[1]' ], + ]; + } + + /** + * @dataProvider provideParseTryFixing + * @param string $value + * @param string|bool $expected Expected result with strict parser + * @param bool $jsoncParses Will json-c parse this value without TRY_FIXING? + * @param string|bool $expectedJsonc Expected result with lenient parser + * if different from the strict expectation + */ + public function testParseTryFixing( + $value, $expected, + $jsoncParses = true, $expectedJsonc = null + ) { + // PHP5 results are always expected to have isGood() === false + $expectedGoodStatus = false; + + // Check to see if json parser allows trailing commas + if ( json_decode( '[1,]' ) !== null ) { + // Use json-c specific expected result if provided + $expected = ( $expectedJsonc === null ) ? $expected : $expectedJsonc; + // If json-c parses the value natively, expect isGood() === true + $expectedGoodStatus = $jsoncParses; + } + + $st = FormatJson::parse( $value, FormatJson::TRY_FIXING ); + $this->assertInstanceOf( Status::class, $st ); + if ( $expected === false ) { + $this->assertFalse( $st->isOK(), 'Expected isOK() == false' ); + } else { + $this->assertSame( $expectedGoodStatus, $st->isGood(), + 'Expected isGood() == ' . ( $expectedGoodStatus ? 'true' : 'false' ) + ); + $this->assertTrue( $st->isOK(), 'Expected isOK == true' ); + $val = FormatJson::encode( $st->getValue(), false, FormatJson::ALL_OK ); + $this->assertEquals( $expected, $val ); + } + } + + public static function provideParseErrors() { + return [ + [ 'aaa' ], + [ '{"j": 1 ] }' ], + ]; + } + + /** + * @dataProvider provideParseErrors + * @param mixed $value + */ + public function testParseErrors( $value ) { + $st = FormatJson::parse( $value ); + $this->assertInstanceOf( Status::class, $st ); + $this->assertFalse( $st->isOK() ); + } + + public function provideStripComments() { + return [ + [ '{"a":"b"}', '{"a":"b"}' ], + [ "{\"a\":\"b\"}\n", "{\"a\":\"b\"}\n" ], + [ '/*c*/{"c":"b"}', '{"c":"b"}' ], + [ '{"a":"c"}/*c*/', '{"a":"c"}' ], + [ '/*c//d*/{"c":"b"}', '{"c":"b"}' ], + [ '{/*c*/"c":"b"}', '{"c":"b"}' ], + [ "/*\nc\r\n*/{\"c\":\"b\"}", '{"c":"b"}' ], + [ "//c\n{\"c\":\"b\"}", '{"c":"b"}' ], + [ "//c\r\n{\"c\":\"b\"}", '{"c":"b"}' ], + [ '{"a":"c"}//c', '{"a":"c"}' ], + [ "{\"a-c\"://c\n\"b\"}", '{"a-c":"b"}' ], + [ '{"/*a":"b"}', '{"/*a":"b"}' ], + [ '{"a":"//b"}', '{"a":"//b"}' ], + [ '{"a":"b/*c*/"}', '{"a":"b/*c*/"}' ], + [ "{\"\\\"/*a\":\"b\"}", "{\"\\\"/*a\":\"b\"}" ], + [ '', '' ], + [ '/*c', '' ], + [ '//c', '' ], + [ '"http://example.com"', '"http://example.com"' ], + [ "\0", "\0" ], + [ '"Blåbærsyltetøy"', '"Blåbærsyltetøy"' ], + ]; + } + + /** + * @covers FormatJson::stripComments + * @dataProvider provideStripComments + * @param string $json + * @param string $expect + */ + public function testStripComments( $json, $expect ) { + $this->assertSame( $expect, FormatJson::stripComments( $json ) ); + } + + public function provideParseStripComments() { + return [ + [ '/* blah */true', true ], + [ "// blah \ntrue", true ], + [ '[ "a" , /* blah */ "b" ]', [ 'a', 'b' ] ], + ]; + } + + /** + * @covers FormatJson::parse + * @covers FormatJson::stripComments + * @dataProvider provideParseStripComments + * @param string $json + * @param mixed $expect + */ + public function testParseStripComments( $json, $expect ) { + $st = FormatJson::parse( $json, FormatJson::STRIP_COMMENTS ); + $this->assertInstanceOf( Status::class, $st ); + $this->assertTrue( $st->isGood() ); + $this->assertEquals( $expect, $st->getValue() ); + } + + /** + * Generate a set of test cases for a particular combination of encoder options. + * + * @param array $unescapedGroups List of character groups to leave unescaped + * @return array Arrays of unencoded strings and corresponding encoded strings + */ + private static function getEncodeTestCases( array $unescapedGroups ) { + $groups = [ + 'always' => [ + // Forward slash (always unescaped) + '/' => '/', + + // Control characters + "\0" => '\u0000', + "\x08" => '\b', + "\t" => '\t', + "\n" => '\n', + "\r" => '\r', + "\f" => '\f', + "\x1f" => '\u001f', // representative example + + // Double quotes + '"' => '\"', + + // Backslashes + '\\' => '\\\\', + '\\\\' => '\\\\\\\\', + '\\u00e9' => '\\\u00e9', // security check for Unicode unescaping + + // Line terminators + "\xe2\x80\xa8" => '\u2028', + "\xe2\x80\xa9" => '\u2029', + ], + 'unicode' => [ + "\xc3\xa9" => '\u00e9', + "\xf0\x9d\x92\x9e" => '\ud835\udc9e', // U+1D49E, outside the BMP + ], + 'xmlmeta' => [ + '<' => '\u003C', // JSON_HEX_TAG uses uppercase hex digits + '>' => '\u003E', + '&' => '\u0026', + ], + ]; + + $cases = []; + foreach ( $groups as $name => $rules ) { + $leaveUnescaped = in_array( $name, $unescapedGroups ); + foreach ( $rules as $from => $to ) { + $cases[] = [ $from, '"' . ( $leaveUnescaped ? $from : $to ) . '"' ]; + } + } + + return $cases; + } +} |