diff options
author | jenkins-bot <jenkins-bot@gerrit.wikimedia.org> | 2024-10-16 00:54:33 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@wikimedia.org> | 2024-10-16 00:54:33 +0000 |
commit | 5579e0647c21c6708e41a29b4418195c27da0ce0 (patch) | |
tree | 39a130fa483d8bc312b86740a19ca2a53131fd2f /tests/phpunit | |
parent | 5c5d0882efabcb68cd246322f67a68c490cef0b9 (diff) | |
parent | 3bc172d0e4e8048a415b6992af3b6db84929cc02 (diff) | |
download | mediawikicore-5579e0647c21c6708e41a29b4418195c27da0ce0.tar.gz mediawikicore-5579e0647c21c6708e41a29b4418195c27da0ce0.zip |
Merge "[JsonCodec] Use wikimedia/json-codec to implement JsonCodec"
Diffstat (limited to 'tests/phpunit')
-rw-r--r-- | tests/phpunit/includes/page/ParserOutputAccessTest.php | 4 | ||||
-rw-r--r-- | tests/phpunit/mocks/json/JsonDeserializableSubClass.php | 4 | ||||
-rw-r--r-- | tests/phpunit/mocks/json/ManagedObject.php | 41 | ||||
-rw-r--r-- | tests/phpunit/mocks/json/ManagedObjectFactory.php | 61 | ||||
-rw-r--r-- | tests/phpunit/mocks/json/SampleContainerObject.php | 70 | ||||
-rw-r--r-- | tests/phpunit/mocks/json/SampleObject.php | 54 | ||||
-rw-r--r-- | tests/phpunit/unit/includes/json/JsonCodecTest.php | 101 |
7 files changed, 325 insertions, 10 deletions
diff --git a/tests/phpunit/includes/page/ParserOutputAccessTest.php b/tests/phpunit/includes/page/ParserOutputAccessTest.php index 35915ad905c5..d43f8f34ffff 100644 --- a/tests/phpunit/includes/page/ParserOutputAccessTest.php +++ b/tests/phpunit/includes/page/ParserOutputAccessTest.php @@ -104,7 +104,7 @@ class ParserOutputAccessTest extends MediaWikiIntegrationTestCase { $bag ?: new HashBagOStuff(), '19900220000000', $this->getServiceContainer()->getHookContainer(), - new JsonCodec(), + new JsonCodec( $this->getServiceContainer() ), StatsFactory::newNull(), new NullLogger(), $this->getServiceContainer()->getTitleFactory(), @@ -122,7 +122,7 @@ class ParserOutputAccessTest extends MediaWikiIntegrationTestCase { $wanCache, $expiry, '19900220000000', - new JsonCodec(), + new JsonCodec( $this->getServiceContainer() ), StatsFactory::newNull(), new NullLogger(), $this->getServiceContainer()->getGlobalIdGenerator() diff --git a/tests/phpunit/mocks/json/JsonDeserializableSubClass.php b/tests/phpunit/mocks/json/JsonDeserializableSubClass.php index 5026aa860342..24ed6167846a 100644 --- a/tests/phpunit/mocks/json/JsonDeserializableSubClass.php +++ b/tests/phpunit/mocks/json/JsonDeserializableSubClass.php @@ -33,3 +33,7 @@ class JsonDeserializableSubClass extends JsonDeserializableSuperClass { ]; } } + +// This class_alias exists for testing alias support in JsonCodec and +// should not be removed. +class_alias( JsonDeserializableSubClass::class, 'MediaWiki\\Tests\\Json\\JsonDeserializableSubClassAlias' ); diff --git a/tests/phpunit/mocks/json/ManagedObject.php b/tests/phpunit/mocks/json/ManagedObject.php new file mode 100644 index 000000000000..45f2638bf427 --- /dev/null +++ b/tests/phpunit/mocks/json/ManagedObject.php @@ -0,0 +1,41 @@ +<?php +namespace MediaWiki\Tests\Json; + +use Psr\Container\ContainerInterface; +use Wikimedia\JsonCodec\JsonClassCodec; +use Wikimedia\JsonCodec\JsonCodecable; +use Wikimedia\JsonCodec\JsonCodecInterface; + +/** + * Managed object which uses a factory in a service. + */ +class ManagedObject implements JsonCodecable { + + /** @var string */ + public string $name; + /** @var int */ + public int $data; + + /** + * Create a new ManagedObject which stores $property. This constructor + * shouldn't be invoked directly by anyone except ManagedObjectFactory. + * + * @param string $name + * @param int $data + * @internal + */ + public function __construct( string $name, int $data ) { + $this->name = $name; + $this->data = $data; + } + + // Implement JsonCodecable by delegating serialization/deserialization + // to the 'ManagedObjectFactory' service. + + /** @inheritDoc */ + public static function jsonClassCodec( + JsonCodecInterface $codec, ContainerInterface $serviceContainer + ): JsonClassCodec { + return $serviceContainer->get( 'ManagedObjectFactory' ); + } +} diff --git a/tests/phpunit/mocks/json/ManagedObjectFactory.php b/tests/phpunit/mocks/json/ManagedObjectFactory.php new file mode 100644 index 000000000000..91d0fdeb8e69 --- /dev/null +++ b/tests/phpunit/mocks/json/ManagedObjectFactory.php @@ -0,0 +1,61 @@ +<?php +namespace MediaWiki\Tests\Json; + +use Wikimedia\JsonCodec\JsonClassCodec; + +/** + * Managed object factory which also handles serialization/deserialization + * of the objects it manages. + * + * @implements JsonClassCodec<ManagedObject> + */ +class ManagedObjectFactory implements JsonClassCodec { + /** @var array<string,ManagedObject> Fake database */ + private $storage = []; + + /** + * Create and store an object with $name and $value in the database. + * @param string $name + * @param int $value + * @return ManagedObject + */ + public function create( string $name, int $value ) { + if ( isset( $this->storage[$name] ) ) { + throw new \Error( "duplicate name" ); + } + $this->storage[$name] = $o = new ManagedObject( $name, $value ); + return $o; + } + + /** + * Lookup $name in the database. + * @param string $name + * @return ManagedObject + */ + public function lookup( string $name ): ManagedObject { + if ( !isset( $this->storage[$name] ) ) { + throw new \Error( "not found" ); + } + return $this->storage[$name]; + } + + /** @inheritDoc */ + public function toJsonArray( $obj ): array { + '@phan-var ManagedObject $obj'; + // Not necessary to serialize all the properties, since they + // will be reloaded from the "database" during deserialization + return [ 'name' => $obj->name ]; + } + + /** @inheritDoc */ + public function newFromJsonArray( string $className, array $json ): ManagedObject { + // @phan-suppress-next-line PhanTypeMismatchReturn template limitations + return $this->lookup( $json['name'] ); + } + + /** @inheritDoc */ + public function jsonClassHintFor( string $className, string $key ): ?string { + // no hints + return null; + } +} diff --git a/tests/phpunit/mocks/json/SampleContainerObject.php b/tests/phpunit/mocks/json/SampleContainerObject.php new file mode 100644 index 000000000000..dc39b2371fff --- /dev/null +++ b/tests/phpunit/mocks/json/SampleContainerObject.php @@ -0,0 +1,70 @@ +<?php +namespace MediaWiki\Tests\Json; + +use stdClass; +use Wikimedia\Assert\Assert; +use Wikimedia\JsonCodec\JsonCodecable; +use Wikimedia\JsonCodec\JsonCodecableTrait; + +/** + * Sample container object which uses JsonCodecableTrait to directly implement + * serialization/deserialization and uses class hints. + */ +class SampleContainerObject implements JsonCodecable { + use JsonCodecableTrait; + + /** @var mixed */ + public $contents; + + /** + * Create a new SampleContainerObject which stores $contents + * @param mixed $contents + */ + public function __construct( $contents ) { + $this->contents = $contents; + } + + // Implement JsonCodecable using the JsonCodecableTrait + + /** @inheritDoc */ + public function toJsonArray(): array { + return [ + 'contents' => $this->contents, + // Note that json array keys need not correspond to property names + 'test' => (object)[], + // Test "array of" hints as well as regular hints + 'array' => [ $this->contents ], + ]; + } + + /** @inheritDoc */ + public static function newFromJsonArray( array $json ): SampleContainerObject { + Assert::invariant( + get_class( $json['test'] ) === stdClass::class && + count( get_object_vars( $json['test'] ) ) === 0, + "Ensure that the 'test' key is restored correctly" + ); + Assert::invariant( + is_array( $json['array'] ) && count( $json['array'] ) === 1, + "Ensure that the 'array' key is restored correctly" + ); + return new SampleContainerObject( $json['contents'] ); + } + + /** @inheritDoc */ + public static function jsonClassHintFor( string $keyName ): ?string { + if ( $keyName === 'contents' ) { + // Hint that the contained value is a SampleObject. It might be! + return SampleObject::class; + } elseif ( $keyName === 'array' ) { + // Hint that the contained value is a *array of* SampleObject. + // It might be! + return SampleObject::class . '[]'; + } elseif ( $keyName === 'test' ) { + // This hint will always be correct; note that this is a key + // name not a property of SampleContainerObject + return stdClass::class; + } + return null; + } +} diff --git a/tests/phpunit/mocks/json/SampleObject.php b/tests/phpunit/mocks/json/SampleObject.php new file mode 100644 index 000000000000..9b1faaea7c2f --- /dev/null +++ b/tests/phpunit/mocks/json/SampleObject.php @@ -0,0 +1,54 @@ +<?php +namespace MediaWiki\Tests\Json; + +use Wikimedia\Assert\Assert; +use Wikimedia\JsonCodec\JsonCodecable; +use Wikimedia\JsonCodec\JsonCodecableTrait; + +/** + * Sample object which uses JsonCodecableTrait to directly implement + * serialization/deserialization. + */ +class SampleObject implements JsonCodecable { + use JsonCodecableTrait; + + /** @var string */ + public string $property; + + /** + * Create a new SampleObject which stores $property. + * @param string $property + */ + public function __construct( string $property ) { + $this->property = $property; + } + + // Implement JsonCodecable using the JsonCodecableTrait + + /** @inheritDoc */ + public function toJsonArray(): array { + if ( $this->property !== 'use _type_' ) { + // Allow testing both with and without the '_type_' special case + return [ 'property' => $this->property ]; + } + return [ + 'property' => $this->property, + // Implementers shouldn't have to know which properties the + // codec is using for its own purposes; this will still work + // fine: + '_type_' => 'check123', + ]; + } + + /** @inheritDoc */ + public static function newFromJsonArray( array $json ): SampleObject { + if ( $json['property'] === 'use _type_' ) { + Assert::invariant( $json['_type_'] === 'check123', 'protected field' ); + } + return new SampleObject( $json['property'] ); + } +} + +// This class_alias exists for testing alias support in JsonCodec and +// should not be removed. +class_alias( SampleObject::class, 'MediaWiki\\Tests\\Json\\SampleObjectAlias' ); diff --git a/tests/phpunit/unit/includes/json/JsonCodecTest.php b/tests/phpunit/unit/includes/json/JsonCodecTest.php index 2854e4e87bf9..cbc50bc65045 100644 --- a/tests/phpunit/unit/includes/json/JsonCodecTest.php +++ b/tests/phpunit/unit/includes/json/JsonCodecTest.php @@ -11,6 +11,7 @@ use MediaWiki\Json\JsonConstants; use MediaWiki\Title\Title; use MediaWiki\User\UserIdentityValue; use MediaWikiUnitTestCase; +use Psr\Container\ContainerInterface; use Wikimedia\Assert\PreconditionException; /** @@ -19,8 +20,35 @@ use Wikimedia\Assert\PreconditionException; */ 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(); + return new JsonCodec( self::getServices() ); } public static function provideSimpleTypes() { @@ -36,11 +64,11 @@ class JsonCodecTest extends MediaWikiUnitTestCase { public static function provideInvalidJsonData() { yield 'Bad string' => [ 'bad string' ]; - yield 'No unserialization metadata' => [ [ 'test' => 'test' ] ]; - yield 'Unserialization metadata, but class not exist' => [ [ + yield 'No deserialization metadata' => [ [ 'test' => 'test' ] ]; + yield 'Deserialization metadata, but class not exist' => [ [ JsonConstants::TYPE_ANNOTATION => 'BadClassNotExist' ] ]; - yield 'Unserialization metadata, but class is not JsonDeserializable' => [ [ + yield 'Deserialization metadata, but class is not JsonDeserializable' => [ [ JsonConstants::TYPE_ANNOTATION => Title::class ] ]; } @@ -74,7 +102,7 @@ class JsonCodecTest extends MediaWikiUnitTestCase { $this->getCodec()->deserialize( $jsonData, JsonDeserializableSuperClass::class ); } - public function testExpectedClassMustBeUnserializable() { + public function testExpectedClassMustBeDeserializable() { $this->expectException( PreconditionException::class ); $this->getCodec()->deserialize( '{}', self::class ); } @@ -113,6 +141,7 @@ class JsonCodecTest extends MediaWikiUnitTestCase { $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() ); } @@ -148,6 +177,12 @@ class JsonCodecTest extends MediaWikiUnitTestCase { 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 ); @@ -188,14 +223,29 @@ class JsonCodecTest extends MediaWikiUnitTestCase { [ [ 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 ]; } /** @@ -217,7 +267,6 @@ class JsonCodecTest extends MediaWikiUnitTestCase { * @dataProvider provideValidateSerializable * @covers \MediaWiki\Json\JsonCodec::detectNonSerializableData * @covers \MediaWiki\Json\JsonCodec::detectNonSerializableDataInternal - * @covers \MediaWiki\Json\JsonCodec::detectNonSerializableDataInternal */ public function testValidateSerializable2( $value, bool $expectDeserialize, ?string $result ) { if ( $result !== null || !$expectDeserialize ) { @@ -225,7 +274,7 @@ class JsonCodecTest extends MediaWikiUnitTestCase { return; } // Sanity check by ensuring that "serializable" things actually - // are unserializable w/o losing value or type + // are deserializable w/o losing value or type $json = $this->getCodec()->serialize( $value ); $newValue = $this->getCodec()->deserialize( $json ); $this->assertEquals( $value, $newValue ); @@ -257,7 +306,15 @@ class JsonCodecTest extends MediaWikiUnitTestCase { } }; yield 'array' => [ [ 'a' => 'b' ], '{"a":"b"}' ]; - yield 'JsonSerializable' => [ $serializableInstance, '{"c":"d","_complex_":true}' ]; + 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, + ] ) ]; } /** @@ -267,4 +324,32 @@ class JsonCodecTest extends MediaWikiUnitTestCase { 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 ); + } } |