aboutsummaryrefslogtreecommitdiffstats
path: root/tests/phpunit
diff options
context:
space:
mode:
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>2024-10-16 00:54:33 +0000
committerGerrit Code Review <gerrit@wikimedia.org>2024-10-16 00:54:33 +0000
commit5579e0647c21c6708e41a29b4418195c27da0ce0 (patch)
tree39a130fa483d8bc312b86740a19ca2a53131fd2f /tests/phpunit
parent5c5d0882efabcb68cd246322f67a68c490cef0b9 (diff)
parent3bc172d0e4e8048a415b6992af3b6db84929cc02 (diff)
downloadmediawikicore-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.php4
-rw-r--r--tests/phpunit/mocks/json/JsonDeserializableSubClass.php4
-rw-r--r--tests/phpunit/mocks/json/ManagedObject.php41
-rw-r--r--tests/phpunit/mocks/json/ManagedObjectFactory.php61
-rw-r--r--tests/phpunit/mocks/json/SampleContainerObject.php70
-rw-r--r--tests/phpunit/mocks/json/SampleObject.php54
-rw-r--r--tests/phpunit/unit/includes/json/JsonCodecTest.php101
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 );
+ }
}