6 class FormatJsonUnitTest
extends MediaWikiUnitTestCase
{
8 public static function provideEncoderPrettyPrinting() {
21 * @dataProvider provideEncoderPrettyPrinting
23 public function testEncoderPrettyPrinting( $pretty, $expectedIndent ) {
25 'emptyObject' => new stdClass
,
27 'string' => 'foobar\\',
33 // Nested json works without problems
34 '"7":["8",{"9":"10"}]',
35 // Whitespace clean up doesn't touch strings that look alike
36 "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}",
40 // No trailing whitespace, no trailing linefeed
44 "string": "foobar\\\\",
50 "\"7\":[\"8\",{\"9\":\"10\"}]",
51 "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}"
55 $json = str_replace( "\r", '', $json ); // Windows compat
56 $json = str_replace( "\t", $expectedIndent, $json );
57 $this->assertSame( $json, FormatJson
::encode( $obj, $pretty ) );
60 public static function provideEncodeDefault() {
61 return self
::getEncodeTestCases( [] );
65 * @dataProvider provideEncodeDefault
67 public function testEncodeDefault( $from, $to ) {
68 $this->assertSame( $to, FormatJson
::encode( $from ) );
71 public static function provideEncodeUtf8() {
72 return self
::getEncodeTestCases( [ 'unicode' ] );
76 * @dataProvider provideEncodeUtf8
78 public function testEncodeUtf8( $from, $to ) {
79 $this->assertSame( $to, FormatJson
::encode( $from, false, FormatJson
::UTF8_OK
) );
82 public static function provideEncodeXmlMeta() {
83 return self
::getEncodeTestCases( [ 'xmlmeta' ] );
87 * @dataProvider provideEncodeXmlMeta
89 public function testEncodeXmlMeta( $from, $to ) {
90 $this->assertSame( $to, FormatJson
::encode( $from, false, FormatJson
::XMLMETA_OK
) );
93 public static function provideEncodeAllOk() {
94 return self
::getEncodeTestCases( [ 'unicode', 'xmlmeta' ] );
98 * @dataProvider provideEncodeAllOk
100 public function testEncodeAllOk( $from, $to ) {
101 $this->assertSame( $to, FormatJson
::encode( $from, false, FormatJson
::ALL_OK
) );
104 public function testEncodePhpBug46944() {
105 $this->assertNotEquals(
107 strtolower( FormatJson
::encode( "\xf0\xa0\x80\x80" ) ),
108 'Test encoding an broken json_encode character (U+20000)'
112 public function testEncodeFail() {
113 // Set up a recursive object that can't be encoded.
118 $this->assertFalse( FormatJson
::encode( $a ) );
121 public function testDecodeReturnType() {
122 $this->assertInternalType(
124 FormatJson
::decode( '{"Name": "Cheeso", "Rank": 7}' ),
128 $this->assertInternalType(
130 FormatJson
::decode( '{"Name": "Cheeso", "Rank": 7}', true ),
135 public static function provideParse() {
148 [ [ 'a' => 'b', 'x' => [ 'c' => 'd' ] ] ],
153 * Recursively convert arrays into stdClass
154 * @param array|string|bool|int|float|null $value
155 * @return stdClass|string|bool|int|float|null
157 public static function toObject( $value ) {
158 return !is_array( $value ) ?
$value : (object)array_map( __METHOD__
, $value );
162 * @dataProvider provideParse
163 * @param mixed $value
165 public function testParse( $value ) {
166 $expected = self
::toObject( $value );
167 $json = FormatJson
::encode( $expected, false, FormatJson
::ALL_OK
);
168 $this->assertJson( $json );
170 $st = FormatJson
::parse( $json );
171 $this->assertInstanceOf( Status
::class, $st );
172 $this->assertTrue( $st->isGood() );
173 $this->assertEquals( $expected, $st->getValue() );
175 $st = FormatJson
::parse( $json, FormatJson
::FORCE_ASSOC
);
176 $this->assertInstanceOf( Status
::class, $st );
177 $this->assertTrue( $st->isGood() );
178 $this->assertEquals( $value, $st->getValue() );
181 public static function provideParseErrors() {
189 * @dataProvider provideParseErrors
190 * @param mixed $value
192 public function testParseErrors( $value ) {
193 $st = FormatJson
::parse( $value );
194 $this->assertInstanceOf( Status
::class, $st );
195 $this->assertFalse( $st->isOK() );
198 public function provideStripComments() {
200 [ '{"a":"b"}', '{"a":"b"}' ],
201 [ "{\"a\":\"b\"}\n", "{\"a\":\"b\"}\n" ],
202 [ '/*c*/{"c":"b"}', '{"c":"b"}' ],
203 [ '{"a":"c"}/*c*/', '{"a":"c"}' ],
204 [ '/*c//d*/{"c":"b"}', '{"c":"b"}' ],
205 [ '{/*c*/"c":"b"}', '{"c":"b"}' ],
206 [ "/*\nc\r\n*/{\"c\":\"b\"}", '{"c":"b"}' ],
207 [ "//c\n{\"c\":\"b\"}", '{"c":"b"}' ],
208 [ "//c\r\n{\"c\":\"b\"}", '{"c":"b"}' ],
209 [ '{"a":"c"}//c', '{"a":"c"}' ],
210 [ "{\"a-c\"://c\n\"b\"}", '{"a-c":"b"}' ],
211 [ '{"/*a":"b"}', '{"/*a":"b"}' ],
212 [ '{"a":"//b"}', '{"a":"//b"}' ],
213 [ '{"a":"b/*c*/"}', '{"a":"b/*c*/"}' ],
214 [ "{\"\\\"/*a\":\"b\"}", "{\"\\\"/*a\":\"b\"}" ],
218 [ '"http://example.com"', '"http://example.com"' ],
220 [ '"Blåbærsyltetøy"', '"Blåbærsyltetøy"' ],
225 * @covers FormatJson::stripComments
226 * @dataProvider provideStripComments
227 * @param string $json
228 * @param string $expect
230 public function testStripComments( $json, $expect ) {
231 $this->assertSame( $expect, FormatJson
::stripComments( $json ) );
234 public function provideParseStripComments() {
236 [ '/* blah */true', true ],
237 [ "// blah \ntrue", true ],
238 [ '[ "a" , /* blah */ "b" ]', [ 'a', 'b' ] ],
243 * @covers FormatJson::parse
244 * @covers FormatJson::stripComments
245 * @dataProvider provideParseStripComments
246 * @param string $json
247 * @param mixed $expect
249 public function testParseStripComments( $json, $expect ) {
250 $st = FormatJson
::parse( $json, FormatJson
::STRIP_COMMENTS
);
251 $this->assertInstanceOf( Status
::class, $st );
252 $this->assertTrue( $st->isGood() );
253 $this->assertEquals( $expect, $st->getValue() );
257 * Generate a set of test cases for a particular combination of encoder options.
259 * @param array $unescapedGroups List of character groups to leave unescaped
260 * @return array Arrays of unencoded strings and corresponding encoded strings
262 private static function getEncodeTestCases( array $unescapedGroups ) {
265 // Forward slash (always unescaped)
268 // Control characters
275 "\x1f" => '\u001f', // representative example
282 '\\\\' => '\\\\\\\\',
283 '\\u00e9' => '\\\u00e9', // security check for Unicode unescaping
286 "\xe2\x80\xa8" => '\u2028',
287 "\xe2\x80\xa9" => '\u2029',
290 "\xc3\xa9" => '\u00e9',
291 "\xf0\x9d\x92\x9e" => '\ud835\udc9e', // U+1D49E, outside the BMP
294 '<' => '\u003C', // JSON_HEX_TAG uses uppercase hex digits
301 foreach ( $groups as $name => $rules ) {
302 $leaveUnescaped = in_array( $name, $unescapedGroups );
303 foreach ( $rules as $from => $to ) {
304 $cases[] = [ $from, '"' . ( $leaveUnescaped ?
$from : $to ) . '"' ];
311 public function provideEmptyJsonKeyStrings() {
323 '{"\u005F\u0065\u006D\u0070\u0074\u0079\u005F":"foo"}',
328 '{"_empty_":"bar","":"foo"}',
329 '{"_empty_":"bar","":"foo"}',
333 '{"":"bar","_empty_":"foo"}',
334 '{"":"bar","_empty_":"foo"}',
341 * @covers FormatJson::encode
342 * @covers FormatJson::decode
343 * @dataProvider provideEmptyJsonKeyStrings
344 * @param string $json
346 * Decoding behavior with empty keys can be surprising.
347 * See https://phabricator.wikimedia.org/T206411
349 public function testEmptyJsonKeyArray( $json, $expect, $php71Name ) {
350 // Decoding to array is consistent across supported PHP versions
351 $this->assertSame( $expect, FormatJson
::encode(
352 FormatJson
::decode( $json, true ) ) );
354 // Decoding to object differs between supported PHP versions
355 $obj = FormatJson
::decode( $json );
356 if ( version_compare( PHP_VERSION
, '7.1', '<' ) ) {
357 $this->assertEquals( 'foo', $obj->_empty_
);
359 $this->assertEquals( 'foo', $obj->{$php71Name} );