From cd47b71752c2d1a7f87a9feb5ee214412b7bbd48 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Fri, 11 Sep 2026 17:35:17 +1200 Subject: [PATCH] test(attribute): assert subclass hydration through the public API The three hydration tests reached past the public surface: they stubbed the adapter and invoked the private createDocumentInstance() by reflection, so they asserted the shape of an internal method rather than the behaviour callers depend on. A rename of that method would have broken them while the feature stayed correct, and a regression in the surrounding read path would have gone unnoticed. Write one attribute row to the Memory adapter and read it back with getDocument(). Subclass hydration is now observed where it matters, and the last reflection in the file is gone. Co-Authored-By: Claude Opus 5 --- tests/unit/AttributeSubclassTest.php | 83 +++++++++++++--------------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/tests/unit/AttributeSubclassTest.php b/tests/unit/AttributeSubclassTest.php index 8427071c2..3a0ddaa98 100644 --- a/tests/unit/AttributeSubclassTest.php +++ b/tests/unit/AttributeSubclassTest.php @@ -4,10 +4,9 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use ReflectionMethod; use Utopia\Cache\Adapter\None as NoneAdapter; use Utopia\Cache\Cache; -use Utopia\Database\Adapter; +use Utopia\Database\Adapter\Memory; use Utopia\Database\Attribute; use Utopia\Database\Attribute\ArrayType; use Utopia\Database\Attribute\BigInteger; @@ -41,8 +40,12 @@ use Utopia\Database\Attribute\Uuid7; use Utopia\Database\Attribute\Varchar; use Utopia\Database\Attribute\Vector; +use Utopia\Database\Collection; use Utopia\Database\Database; use Utopia\Database\Document; +use Utopia\Database\Helpers\Permission; +use Utopia\Database\Helpers\Role; +use Utopia\Database\Validator\Authorization; use Utopia\Query\Schema\ColumnType; final class AttributeSubclassTest extends TestCase @@ -251,20 +254,13 @@ public function testFromArrayPreservesStoredSizeZero(): void * @param class-string $class */ #[DataProvider('types')] - public function testCreateDocumentInstanceHydratesMappedSubclass( + public function testReadingBackHydratesTheMappedSubclass( string $class, string $_factory, ColumnType $type, int $_defaultSize, ): void { - $database = $this->database(); - $database->setDocumentType('schema', $class); - - $document = $this->instantiate($database, 'schema', [ - '$id' => 'x', - 'key' => 'x', - 'type' => $type->value, - ]); + $document = $this->store($class, $type); $this->assertInstanceOf($class, $document); $this->assertSame($type, $document->type); @@ -275,38 +271,22 @@ public function testCreateDocumentInstanceHydratesMappedSubclass( * @param class-string $class */ #[DataProvider('types')] - public function testCreateDocumentInstanceHydratesSubclassFromAttributeType( + public function testReadingBackHydratesTheSubclassThatTheStoredTypeNames( string $class, string $_factory, ColumnType $type, int $_defaultSize, ): void { - $database = $this->database(); - $database->setDocumentType('schema', Attribute::class); - - $document = $this->instantiate($database, 'schema', [ - '$id' => 'x', - 'key' => 'x', - 'type' => $type->value, - ]); + $document = $this->store(Attribute::class, $type); $this->assertInstanceOf($class, $document); $this->assertSame($type, $document->type); $this->assertSame('x', $document->key); } - public function testCreateDocumentInstanceUsesStoredTypeNotMappedClass(): void + public function testStoredTypeWinsOverTheMappedClass(): void { - $database = $this->database(); - $database->setDocumentType('schema', StringType::class); - - $document = $this->instantiate($database, 'schema', [ - '$id' => 'age', - 'key' => 'age', - 'type' => ColumnType::Integer->value, - ]); - - $this->assertInstanceOf(Integer::class, $document); + $this->assertInstanceOf(Integer::class, $this->store(StringType::class, ColumnType::Integer)); } public function testFromDocumentMissingSizeIsZero(): void @@ -372,24 +352,37 @@ public function testEnumAndArrayAreSubclasses(): void $this->assertSame(0, $array->size); } - private function database(): Database - { - return new Database( - $this->createStub(Adapter::class), - new Cache(new NoneAdapter()), - ); - } - /** - * @param array $data + * Write one attribute row and read it back through the public API, under + * the given document type mapping. + * + * @param class-string $documentType */ - private function instantiate(Database $database, string $collection, array $data): Document + private function store(string $documentType, ColumnType $type): Document { - $method = new ReflectionMethod(Database::class, 'createDocumentInstance'); + $database = new Database(new Memory(), new Cache(new NoneAdapter())); + $database->setAuthorization(new Authorization()) + ->setDatabase('attribute_subclass') + ->setNamespace('subclass_'.\uniqid()); + $database->create(); + + $database->createCollection(new Collection(id: 'schema', attributes: [ + Attribute::string(key: 'key', size: 255), + Attribute::string(key: 'type', size: 64), + ], permissions: [ + Permission::create(Role::any()), + Permission::read(Role::any()), + ])); - /** @var Document $document */ - $document = $method->invoke($database, $collection, $data); + $database->setDocumentType('schema', $documentType); + + $database->createDocument('schema', new Document([ + '$id' => 'x', + '$permissions' => [Permission::read(Role::any())], + 'key' => 'x', + 'type' => $type->value, + ])); - return $document; + return $database->getDocument('schema', 'x'); } }