Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4134672
JsonCodecTest.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
14 KB
Referenced Files
None
Subscribers
None
JsonCodecTest.php
View Options
<?php
namespace
MediaWiki\Tests\Json
;
use
InvalidArgumentException
;
use
JsonException
;
use
JsonSerializable
;
use
MediaWiki\Json\FormatJson
;
use
MediaWiki\Json\JsonCodec
;
use
MediaWiki\Json\JsonConstants
;
use
MediaWiki\Title\Title
;
use
MediaWiki\User\UserIdentityValue
;
use
MediaWikiUnitTestCase
;
use
Psr\Container\ContainerInterface
;
use
Wikimedia\Assert\PreconditionException
;
/**
* @covers \MediaWiki\Json\JsonCodec
* @covers \MediaWiki\Json\JsonDeserializableTrait
*/
class
JsonCodecTest
extends
MediaWikiUnitTestCase
{
/** Mock up a services interface. */
private
static
function
getServices
():
ContainerInterface
{
static
$services
=
null
;
if
(
!
$services
)
{
$services
=
new
class
implements
ContainerInterface
{
private
$storage
=
[];
public
function
get
(
$id
)
{
return
$this
->
storage
[
$id
];
}
public
function
has
(
$id
):
bool
{
return
isset
(
$this
->
storage
[
$id
]
);
}
public
function
set
(
$id
,
$value
)
{
$this
->
storage
[
$id
]
=
$value
;
}
};
$factory
=
new
ManagedObjectFactory
();
$factory
->
create
(
"a"
,
1
);
$factory
->
create
(
"b"
,
2
);
$services
->
set
(
'ManagedObjectFactory'
,
$factory
);
}
return
$services
;
}
private
function
getCodec
():
JsonCodec
{
return
new
JsonCodec
(
self
::
getServices
()
);
}
public
static
function
provideSimpleTypes
()
{
yield
'Integer'
=>
[
1
,
json_encode
(
1
)
];
yield
'Boolean'
=>
[
true
,
json_encode
(
true
)
];
yield
'Null'
=>
[
null
,
json_encode
(
null
)
];
yield
'Array'
=>
[
[
1
,
2
,
3
],
json_encode
(
[
1
,
2
,
3
]
)
];
yield
'Assoc array'
=>
[
[
'a'
=>
'b'
],
json_encode
(
[
'a'
=>
'b'
]
)
];
$object
=
(
object
)[
'c'
=>
'd'
];
yield
'Object'
=>
[
(
array
)
$object
,
json_encode
(
$object
)
];
}
public
static
function
provideInvalidJsonData
()
{
yield
'Bad string'
=>
[
'bad string'
];
yield
'No deserialization metadata'
=>
[
[
'test'
=>
'test'
]
];
yield
'Deserialization metadata, but class not exist'
=>
[
[
JsonConstants
::
TYPE_ANNOTATION
=>
'BadClassNotExist'
]
];
yield
'Deserialization metadata, but class is not JsonDeserializable'
=>
[
[
JsonConstants
::
TYPE_ANNOTATION
=>
Title
::
class
]
];
}
/**
* @dataProvider provideSimpleTypes
*/
public
function
testSimpleTypesDeserialize
(
$value
,
string
$serialization
)
{
$this
->
assertSame
(
$value
,
$this
->
getCodec
()->
deserialize
(
$serialization
)
);
}
public
function
testActualClassInstanceIsNotJsonObject
()
{
// Can be any trivial value class that's suitable to be used in pure unit tests
$object
=
UserIdentityValue
::
newAnonymous
(
''
);
// TODO: We probably want this to throw an exception as well
$array
=
[
$object
];
$this
->
assertSame
(
$array
,
$this
->
getCodec
()->
deserialize
(
$array
)
);
$this
->
expectException
(
InvalidArgumentException
::
class
);
$this
->
getCodec
()->
deserialize
(
$object
);
}
/**
* @dataProvider provideSimpleTypes
* @dataProvider provideInvalidJsonData
*/
public
function
testInvalidJsonDataForClassExpectation
(
$jsonData
,
$simpleJsonData
=
null
)
{
$jsonData
=
$simpleJsonData
??
$jsonData
;
$this
->
expectException
(
JsonException
::
class
);
$this
->
getCodec
()->
deserialize
(
$jsonData
,
JsonDeserializableSuperClass
::
class
);
}
public
function
testExpectedClassMustBeDeserializable
()
{
$this
->
expectException
(
PreconditionException
::
class
);
$this
->
getCodec
()->
deserialize
(
'{}'
,
self
::
class
);
}
public
function
testUnexpectedClassDeserialized
()
{
$this
->
expectException
(
JsonException
::
class
);
$superClassInstance
=
new
JsonDeserializableSuperClass
(
'Godzilla'
);
$this
->
getCodec
()->
deserialize
(
$superClassInstance
->
jsonSerialize
(),
JsonDeserializableSubClass
::
class
);
}
public
function
testExpectedClassDeserialized
()
{
$subClassInstance
=
new
JsonDeserializableSubClass
(
'Godzilla'
,
'But we are ready!'
);
$this
->
assertNotNull
(
$this
->
getCodec
()->
deserialize
(
$subClassInstance
->
jsonSerialize
(),
JsonDeserializableSuperClass
::
class
)
);
$this
->
assertNotNull
(
$this
->
getCodec
()->
deserialize
(
$subClassInstance
->
jsonSerialize
(),
JsonDeserializableSubClass
::
class
)
);
}
public
function
testRoundTripSuperClass
()
{
$superClassInstance
=
new
JsonDeserializableSuperClass
(
'Super Value'
);
$json
=
$superClassInstance
->
jsonSerialize
();
$superClassDeserialized
=
$this
->
getCodec
()->
deserialize
(
$json
);
$this
->
assertInstanceOf
(
JsonDeserializableSuperClass
::
class
,
$superClassInstance
);
$this
->
assertSame
(
$superClassInstance
->
getSuperClassField
(),
$superClassDeserialized
->
getSuperClassField
()
);
}
public
function
testRoundTripSuperClassObject
()
{
$superClassInstance
=
new
JsonDeserializableSuperClass
(
'Super Value'
);
$json
=
(
object
)
$superClassInstance
->
jsonSerialize
();
$superClassDeserialized
=
$this
->
getCodec
()->
deserialize
(
$json
);
$this
->
assertInstanceOf
(
JsonDeserializableSuperClass
::
class
,
$superClassInstance
);
$this
->
assertInstanceOf
(
JsonDeserializableSuperClass
::
class
,
$superClassDeserialized
);
$this
->
assertSame
(
$superClassInstance
->
getSuperClassField
(),
$superClassDeserialized
->
getSuperClassField
()
);
}
public
function
testRoundTripSubClass
()
{
$subClassInstance
=
new
JsonDeserializableSubClass
(
'Super Value'
,
'Sub Value'
);
$json
=
$subClassInstance
->
jsonSerialize
();
$superClassDeserialized
=
$this
->
getCodec
()->
deserialize
(
$json
);
$this
->
assertInstanceOf
(
JsonDeserializableSubClass
::
class
,
$subClassInstance
);
$this
->
assertSame
(
$subClassInstance
->
getSuperClassField
(),
$superClassDeserialized
->
getSuperClassField
()
);
$this
->
assertSame
(
$subClassInstance
->
getSubClassField
(),
$superClassDeserialized
->
getSubClassField
()
);
}
public
function
testRoundTripSubClassNested
()
{
$subClassInstance1
=
new
JsonDeserializableSubClass
(
'Super Value'
,
'Sub Value'
);
$superClassInstance1
=
new
JsonDeserializableSuperClass
(
'XYZ'
);
$superClassInstance2
=
new
JsonDeserializableSuperClass
(
// To be a bit tricky, wrap the embedded instance in an array
[
$superClassInstance1
]
);
$subClassInstance2
=
new
JsonDeserializableSubClass
(
$subClassInstance1
,
// Again, we're tricky and wrap this instance in a stdClass object
(
object
)[
'a'
=>
$superClassInstance1
]
);
$json
=
$this
->
getCodec
()->
serialize
(
$subClassInstance2
);
$deserialized
=
$this
->
getCodec
()->
deserialize
(
$json
);
$this
->
assertEquals
(
$subClassInstance2
,
$deserialized
);
}
public
function
testArrayRoundTrip
()
{
$array
=
[
new
JsonDeserializableSuperClass
(
'Super Value'
),
new
JsonDeserializableSubClass
(
'Super Value'
,
'Sub Value'
),
42
];
// This is pretty bogus, in that it tests the ability of JsonCodec
// to decode something which *wasn't* generated by JsonCodec, but
// instead used only json_encode/JsonSerializable. Still this should
// work as long as JsonDeserializableTrait is used and the arrays
// returned contain only primitive types (ie, not nested
// JsonSerializables)
$serialized
=
FormatJson
::
encode
(
$array
);
$deserialized
=
$this
->
getCodec
()->
deserializeArray
(
FormatJson
::
decode
(
$serialized
)
);
$this
->
assertArrayEquals
(
$array
,
$deserialized
);
}
public
static
function
provideValidateSerializable
()
{
$classInstance
=
new
class
()
{
};
$serializableClass
=
new
class
()
implements
JsonSerializable
{
public
function
jsonSerialize
():
array
{
return
[];
}
};
$badSerializable
=
new
class
()
implements
JsonSerializable
{
public
function
jsonSerialize
():
\stdClass
{
return
(
object
)[];
}
};
yield
'Number'
=>
[
1
,
true
,
null
];
yield
'Null'
=>
[
null
,
true
,
null
];
yield
'Class'
=>
[
$classInstance
,
false
,
'$'
];
yield
'Empty array'
=>
[
[],
true
,
null
];
yield
'Empty stdClass'
=>
[
(
object
)[],
true
,
null
];
yield
'Non-empty array'
=>
[
[
1
,
2
,
3
],
true
,
null
];
yield
'Non-empty map'
=>
[
[
'a'
=>
'b'
],
true
,
null
];
yield
'Nested stdClass'
=>
[
[
'a'
=>
[
'b'
=>
(
object
)[]
]
],
true
,
null
];
yield
'Nested, serializable'
=>
[
[
'a'
=>
[
'b'
=>
[
'c'
=>
'd'
]
]
],
true
,
null
];
yield
'Nested, serializable, with null'
=>
[
[
'a'
=>
[
'b'
=>
null
]
],
true
,
null
];
yield
'Nested, serializable, with stdClass'
=>
[
[
'a'
=>
(
object
)[
'b'
=>
[
'c'
=>
'd'
]
]
],
true
,
null
];
yield
'Nested, serializable, with stdClass, with null'
=>
[
[
'a'
=>
(
object
)[
'b'
=>
null
]
],
true
,
null
];
yield
'Nested, non-serializable'
=>
[
[
'a'
=>
[
'b'
=>
$classInstance
]
],
true
,
'$.a.b'
];
yield
'Nested, non-serializable, in array'
=>
[
[
'a'
=>
[
1
,
2
,
$classInstance
]
],
true
,
'$.a.2'
];
yield
'Nested, non-serializable, in stdClass'
=>
[
[
'a'
=>
(
object
)[
1
,
2
,
$classInstance
]
],
true
,
'$.a.2'
];
yield
'Nested, non-serializable, in JsonDeserializable'
=>
[
new
JsonDeserializableSuperClass
(
$classInstance
),
true
,
'$.super_class_field'
];
yield
'JsonDeserializable instance'
=>
[
new
JsonDeserializableSuperClass
(
'Test'
),
true
,
null
];
yield
'JsonDeserializable instance, in array'
=>
[
[
new
JsonDeserializableSuperClass
(
'Test'
)
],
true
,
null
];
yield
'JsonDeserializable instance, in stdClass'
=>
[
(
object
)[
new
JsonDeserializableSuperClass
(
'Test'
)
],
true
,
null
];
yield
'JsonDeserializable instance, in JsonCodecable'
=>
[
new
SampleContainerObject
(
new
JsonDeserializableSuperClass
(
'Test'
)
),
true
,
null
];
yield
'JsonSerializable instance'
=>
[
$serializableClass
,
false
,
null
];
yield
'JsonSerializable instance, in array'
=>
[
[
$serializableClass
],
false
,
null
];
yield
'JsonSerializable instance, in stdClass'
=>
[
(
object
)[
$serializableClass
],
false
,
null
];
yield
'JsonSerializable instance, in JsonCodecable'
=>
[
new
SampleContainerObject
(
$serializableClass
),
false
,
null
];
yield
'JsonSerializable instance, expect deserialize'
=>
[
$serializableClass
,
true
,
'$'
];
yield
'JsonSerializable instance, in array, expect deserialize'
=>
[
[
$serializableClass
],
true
,
'$.0'
];
yield
'JsonSerializable instance, in stdClass, expect deserialize'
=>
[
(
object
)[
$serializableClass
],
true
,
'$.0'
];
yield
'Bad JsonSerializable instance'
=>
[
$badSerializable
,
false
,
'$'
];
yield
'JsonCodecable instance'
=>
[
new
SampleObject
(
'a'
),
true
,
null
];
yield
'JsonCodecable instance, in array'
=>
[
[
new
SampleObject
(
'123'
)
],
true
,
null
];
yield
'JsonCodecable instance, in stdClass'
=>
[
(
object
)[
new
SampleObject
(
'Test'
)
],
true
,
null
];
yield
'JsonCodecable instance, in JsonCodecable'
=>
[
new
SampleContainerObject
(
new
SampleObject
(
'123'
)
),
true
,
null
];
// Managed values
$factory
=
self
::
getServices
()->
get
(
'ManagedObjectFactory'
);
yield
'Managed instance'
=>
[
$factory
->
lookup
(
'a'
),
true
,
null
];
}
/**
* @dataProvider provideValidateSerializable
* @covers \MediaWiki\Json\JsonCodec::detectNonSerializableData
* @covers \MediaWiki\Json\JsonCodec::detectNonSerializableDataInternal
*/
public
function
testValidateSerializable
(
$value
,
bool
$expectDeserialize
,
?
string
$expected
)
{
$actual
=
$this
->
getCodec
()
->
detectNonSerializableData
(
$value
,
$expectDeserialize
);
// Split off the details string from the detection location
if
(
$actual
!==
null
)
{
$actual
=
preg_replace
(
'/:.*$/'
,
''
,
$actual
);
}
$this
->
assertSame
(
$expected
,
$actual
);
}
/**
* @dataProvider provideValidateSerializable
* @covers \MediaWiki\Json\JsonCodec::detectNonSerializableData
* @covers \MediaWiki\Json\JsonCodec::detectNonSerializableDataInternal
*/
public
function
testValidateSerializable2
(
$value
,
bool
$expectDeserialize
,
?
string
$result
)
{
if
(
$result
!==
null
||
!
$expectDeserialize
)
{
$this
->
assertTrue
(
true
);
// skip this test
return
;
}
// Sanity check by ensuring that "serializable" things actually
// are deserializable w/o losing value or type
$json
=
$this
->
getCodec
()->
serialize
(
$value
);
$newValue
=
$this
->
getCodec
()->
deserialize
(
$json
);
$this
->
assertEquals
(
$value
,
$newValue
);
}
public
static
function
provideSerializeThrowsOnFailure
()
{
$classInstance
=
new
class
()
{
};
yield
'non-serializable class'
=>
[
$classInstance
];
yield
'invalid UTF-8'
=>
[
"
\x
1f
\x
8b
\x
08
\x
00
\x
00
\x
00
\x
00
\x
00
\x
00
\x
03
\x
cb
\x
48
\x
cd
\x
c9
\x
c9
\x
57
\x
28
\x
cf
\x
2f"
.
"
\x
ca
\x
49
\x
01
\x
00
\x
85
\x
11
\x
4a
\x
0d
\x
0b
\x
00
\x
00
\x
00"
];
}
/**
* @dataProvider provideSerializeThrowsOnFailure
* @covers \MediaWiki\Json\JsonCodec::serialize
*/
public
function
testSerializeThrowsOnFailure
(
$value
)
{
$this
->
expectException
(
JsonException
::
class
);
$this
->
getCodec
()->
serialize
(
$value
);
}
public
static
function
provideSerializeSuccess
()
{
$serializableInstance
=
new
class
()
implements
JsonSerializable
{
public
function
jsonSerialize
():
array
{
return
[
'c'
=>
'd'
];
}
};
yield
'array'
=>
[
[
'a'
=>
'b'
],
'{"a":"b"}'
];
yield
'JsonSerializable'
=>
[
$serializableInstance
,
json_encode
(
[
"c"
=>
"d"
,
"_type_"
=>
get_class
(
$serializableInstance
),
"_complex_"
=>
true
],
JSON_UNESCAPED_SLASHES
)
];
yield
'JsonCodecable'
=>
[
new
SampleObject
(
'a'
),
json_encode
(
[
'property'
=>
'a'
,
'_type_'
=>
SampleObject
::
class
,
'_complex_'
=>
true
,
]
)
];
}
/**
* @dataProvider provideSerializeSuccess
* @covers \MediaWiki\Json\JsonCodec::serialize
*/
public
function
testSerializeSuccess
(
$value
,
string
$expected
)
{
$this
->
assertSame
(
$expected
,
$this
->
getCodec
()->
serialize
(
$value
)
);
}
public
function
testManagedObjects
()
{
$codec
=
$this
->
getCodec
();
$factory
=
self
::
getServices
()->
get
(
'ManagedObjectFactory'
);
$a
=
$factory
->
lookup
(
'a'
);
$s
=
$codec
->
serialize
(
$a
);
$v
=
$codec
->
deserialize
(
$s
);
// Not just "equals", $v should be reference-identical to $a
$this
->
assertSame
(
$a
,
$v
);
}
public
function
testCodecableAliases
()
{
$codec
=
$this
->
getCodec
();
// Note that the class name in _type_ is an *alias*, not the
// *actual* class name.
$json
=
'{"property":"alias!","_type_":"MediaWiki
\\\\
Tests
\\\\
Json
\\\\
SampleObjectAlias","_complex_":true}'
;
$v
=
$codec
->
deserialize
(
$json
,
SampleObject
::
class
);
$this
->
assertInstanceOf
(
SampleObject
::
class
,
$v
);
}
public
function
testJsonDeserializableAliases
()
{
$codec
=
$this
->
getCodec
();
// Note that the class name in _type_ is an *alias*, not the
// *actual* class name.
$json
=
'{"super_class_field":1,"sub_class_field":"2","_type_":"MediaWiki
\\\\
Tests
\\\\
Json
\\\\
JsonDeserializableSubClassAlias","_complex_":true}'
;
$v
=
$codec
->
deserialize
(
$json
,
JsonDeserializableSubClass
::
class
);
$this
->
assertInstanceOf
(
JsonDeserializableSubClass
::
class
,
$v
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Wed, Aug 19, 12:45 (3 w, 2 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
79/9e/491c9cdfce19bc22ee542d3f98a1
Default Alt Text
JsonCodecTest.php (14 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment