diff --git a/app/controllers/api/databases.php b/app/controllers/api/databases.php index 2a1884e6b2..4f9abbbe65 100644 --- a/app/controllers/api/databases.php +++ b/app/controllers/api/databases.php @@ -196,16 +196,14 @@ function createAttribute(string $databaseId, string $collectionId, Document $att ->setType(DATABASE_TYPE_CREATE_ATTRIBUTE) ->setDatabase($db) ->setCollection($collection) - ->setDocument($attribute) - ; + ->setDocument($attribute); $events ->setContext('collection', $collection) ->setContext('database', $db) ->setParam('databaseId', $databaseId) ->setParam('collectionId', $collection->getId()) - ->setParam('attributeId', $attribute->getId()) - ; + ->setParam('attributeId', $attribute->getId()); $response->setStatusCode(Response::STATUS_CODE_CREATED); @@ -682,13 +680,11 @@ App::delete('/v1/databases/:databaseId') $deletes ->setType(DELETE_TYPE_DOCUMENT) - ->setDocument($database) - ; + ->setDocument($database); $events ->setParam('databaseId', $database->getId()) - ->setPayload($response->output($database, Response::MODEL_DATABASE)) - ; + ->setPayload($response->output($database, Response::MODEL_DATABASE)); $response->noContent(); }); @@ -3250,28 +3246,13 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum } $data = \array_merge($document->getArrayCopy(), $data); // Merge existing data with new data - $data['$collection'] = $collection->getId(); // Make sure user doesn't switch collectionID + $data['$collection'] = $document->getAttribute('$collection'); // Make sure user doesn't switch collectionID $data['$createdAt'] = $document->getCreatedAt(); // Make sure user doesn't switch createdAt $data['$id'] = $document->getId(); // Make sure user doesn't switch document unique ID $data['$permissions'] = $permissions; $newDocument = new Document($data); - $checkPermissions = function (Document $collection, Document $document, Document $old, string $permission) use (&$checkPermissions, $dbForProject, $database) { - $documentSecurity = $collection->getAttribute('documentSecurity', false); - $validator = new Authorization($permission); - - $valid = $validator->isValid($collection->getPermissionsByType($permission)); - if (!$documentSecurity && !$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } - - if ($permission === Database::PERMISSION_UPDATE) { - $valid = $valid || $validator->isValid($old->getPermissionsByType($permission)); - if ($documentSecurity && !$valid) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } - } - + $setCollection = (function (Document $collection, Document $document) use (&$setCollection, $dbForProject, $database) { $relationships = \array_filter( $collection->getAttribute('attributes', []), fn($attribute) => $attribute->getAttribute('type') === Database::VAR_RELATIONSHIP @@ -3298,6 +3279,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum ); foreach ($relations as &$relation) { + // If the relation is an array it can be either update or create a child document. if ( \is_array($relation) && \array_values($relation) !== $relation @@ -3311,21 +3293,20 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum 'database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId(), $relation->getId() )); + $relation->removeAttribute('$collectionId'); + $relation->removeAttribute('$databaseId'); + // Attribute $collection is required for Utopia. + $relation->setAttribute( + '$collection', + 'database_' . $database->getInternalId() . '_collection_' . $relatedCollection->getInternalId() + ); if ($oldDocument->isEmpty()) { - $type = Database::PERMISSION_CREATE; - if (isset($relation['$id']) && $relation['$id'] === 'unique()') { $relation['$id'] = ID::unique(); } - } else { - $relation->removeAttribute('$collectionId'); - $relation->removeAttribute('$databaseId'); - $relation->setAttribute('$collection', $relatedCollection->getId()); - $type = Database::PERMISSION_UPDATE; } - - $checkPermissions($relatedCollection, $relation, $oldDocument, $type); + $setCollection($relatedCollection, $relation); } } @@ -3335,26 +3316,26 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum $document->setAttribute($relationship->getAttribute('key'), \reset($relations)); } } - }; + }); - $checkPermissions($collection, $newDocument, $document, Database::PERMISSION_UPDATE); + $setCollection($collection, $newDocument); - try { - $document = $dbForProject->withRequestTimestamp( - $requestTimestamp, - fn() => $dbForProject->updateDocument( - 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), - $document->getId(), - $newDocument - ) - ); - } catch (AuthorizationException) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } catch (DuplicateException) { - throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); - } catch (StructureException $exception) { - throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $exception->getMessage()); - } + try { + $document = $dbForProject->withRequestTimestamp( + $requestTimestamp, + fn() => $dbForProject->updateDocument( + 'database_' . $database->getInternalId() . '_collection_' . $collection->getInternalId(), + $document->getId(), + $newDocument + ) + ); + } catch (AuthorizationException) { + throw new Exception(Exception::USER_UNAUTHORIZED); + } catch (DuplicateException) { + throw new Exception(Exception::DOCUMENT_ALREADY_EXISTS); + } catch (StructureException $exception) { + throw new Exception(Exception::DOCUMENT_INVALID_STRUCTURE, $exception->getMessage()); + } // Add $collectionId and $databaseId for all documents $processDocument = function (Document $collection, Document $document) use (&$processDocument, $dbForProject, $database) { diff --git a/composer.lock b/composer.lock index 4a0862679d..0e72c867d2 100644 --- a/composer.lock +++ b/composer.lock @@ -1516,16 +1516,16 @@ }, { "name": "utopia-php/database", - "version": "0.42.0", + "version": "0.42.1", "source": { "type": "git", "url": "https://github.com/utopia-php/database.git", - "reference": "5631f151ce2b454517f11c1914e50a1fa70c7bd1" + "reference": "9ff69a9b9eadc581771798833d423829c9d8cc90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/database/zipball/5631f151ce2b454517f11c1914e50a1fa70c7bd1", - "reference": "5631f151ce2b454517f11c1914e50a1fa70c7bd1", + "url": "https://api.github.com/repos/utopia-php/database/zipball/9ff69a9b9eadc581771798833d423829c9d8cc90", + "reference": "9ff69a9b9eadc581771798833d423829c9d8cc90", "shasum": "" }, "require": { @@ -1566,9 +1566,9 @@ ], "support": { "issues": "https://github.com/utopia-php/database/issues", - "source": "https://github.com/utopia-php/database/tree/0.42.0" + "source": "https://github.com/utopia-php/database/tree/0.42.1" }, - "time": "2023-08-10T23:18:38+00:00" + "time": "2023-08-14T16:09:09+00:00" }, { "name": "utopia-php/domains", @@ -2993,16 +2993,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.16.0", + "version": "v4.17.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "19526a33fb561ef417e822e85f08a00db4059c17" + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17", - "reference": "19526a33fb561ef417e822e85f08a00db4059c17", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", "shasum": "" }, "require": { @@ -3043,9 +3043,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" }, - "time": "2023-06-25T14:52:30+00:00" + "time": "2023-08-13T19:53:39+00:00" }, { "name": "phar-io/manifest", @@ -3270,16 +3270,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.7.2", + "version": "1.7.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "b2fe4d22a5426f38e014855322200b97b5362c0d" + "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/b2fe4d22a5426f38e014855322200b97b5362c0d", - "reference": "b2fe4d22a5426f38e014855322200b97b5362c0d", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", + "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", "shasum": "" }, "require": { @@ -3322,9 +3322,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.2" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.3" }, - "time": "2023-05-30T18:13:47+00:00" + "time": "2023-08-12T11:01:26+00:00" }, { "name": "phpspec/prophecy", diff --git a/tests/e2e/Services/Databases/DatabasesCustomClientTest.php b/tests/e2e/Services/Databases/DatabasesCustomClientTest.php index b0ab884a07..743df9e53a 100644 --- a/tests/e2e/Services/Databases/DatabasesCustomClientTest.php +++ b/tests/e2e/Services/Databases/DatabasesCustomClientTest.php @@ -316,4 +316,420 @@ class DatabasesCustomClientTest extends Scope $this->assertEquals($relation['body']['relatedCollection'], $collection1RelationAttribute['relatedCollection']); $this->assertEquals('restrict', $collection1RelationAttribute['onDelete']); } + + public function testUpdateWithoutRelationPermission(): void + { + $userId = $this->getUser()['$id']; + $database = $this->client->call(Client::METHOD_POST, '/databases', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ], [ + 'databaseId' => ID::unique(), + 'name' => ID::unique(), + ]); + + $databaseId = $database['body']['$id']; + + // Creating collection 1 + $collection1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::custom('collection1'), + 'name' => ID::custom('collection1'), + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::user($userId)), + Permission::read(Role::user($userId)), + Permission::delete(Role::user($userId)), + ] + ]); + + // Creating collection 2 + $collection2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::custom('collection2'), + 'name' => ID::custom('collection2'), + 'documentSecurity' => false, + 'permissions' => [ + Permission::read(Role::user($userId)), + ] + ]); + + $collection3 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::custom('collection3'), + 'name' => ID::custom('collection3'), + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::user($userId)), + Permission::read(Role::user($userId)), + Permission::delete(Role::user($userId)), + ] + ]); + + $collection4 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::custom('collection4'), + 'name' => ID::custom('collection4'), + 'documentSecurity' => false, + 'permissions' => [ + Permission::read(Role::user($userId)), + ] + ]); + + $collection5 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::custom('collection5'), + 'name' => ID::custom('collection5'), + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::user($userId)), + Permission::read(Role::user($userId)), + Permission::delete(Role::user($userId)), + ] + ]); + + // Creating one to one relationship from collection 1 to colletion 2 + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection1['body']['$id'] . '/attributes/relationship', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'relatedCollectionId' => $collection2['body']['$id'], + 'type' => 'oneToOne', + 'twoWay' => false, + 'onDelete' => 'setNull', + 'key' => $collection2['body']['$id'] + ]); + + // Creating one to one relationship from collection 2 to colletion 3 + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection2['body']['$id'] . '/attributes/relationship', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'relatedCollectionId' => $collection3['body']['$id'], + 'type' => 'oneToOne', + 'twoWay' => false, + 'onDelete' => 'setNull', + 'key' => $collection3['body']['$id'] + ]); + + // Creating one to one relationship from collection 3 to colletion 4 + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection3['body']['$id'] . '/attributes/relationship', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'relatedCollectionId' => $collection4['body']['$id'], + 'type' => 'oneToOne', + 'twoWay' => false, + 'onDelete' => 'setNull', + 'key' => $collection4['body']['$id'] + ]); + + // Creating one to one relationship from collection 4 to colletion 5 + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection4['body']['$id'] . '/attributes/relationship', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'relatedCollectionId' => $collection5['body']['$id'], + 'type' => 'oneToOne', + 'twoWay' => false, + 'onDelete' => 'setNull', + 'key' => $collection5['body']['$id'] + ]); + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection1['body']['$id'] . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => "Title", + 'size' => 100, + 'required' => false, + 'array' => false, + 'default' => null, + ]); + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection2['body']['$id'] . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => "Rating", + 'size' => 100, + 'required' => false, + 'array' => false, + 'default' => null, + ]); + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection3['body']['$id'] . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => "Rating", + 'size' => 100, + 'required' => false, + 'array' => false, + 'default' => null, + ]); + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection4['body']['$id'] . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => "Rating", + 'size' => 100, + 'required' => false, + 'array' => false, + 'default' => null, + ]); + + $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection5['body']['$id'] . '/attributes/string', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'key' => "Rating", + 'size' => 100, + 'required' => false, + 'array' => false, + 'default' => null, + ]); + + \sleep(2); + // Creating parent document with a child reference to test the permissions + $parentDocument = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection1['body']['$id'] . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => ID::custom($collection1['body']['$id']), + 'data' => [ + 'Title' => 'Captain America', + $collection2['body']['$id'] => [ + '$id' => ID::custom($collection2['body']['$id']), + 'Rating' => '10', + $collection3['body']['$id'] => [ + '$id' => ID::custom($collection3['body']['$id']), + 'Rating' => '10', + $collection4['body']['$id'] => [ + '$id' => ID::custom($collection4['body']['$id']), + 'Rating' => '10', + $collection5['body']['$id'] => [ + '$id' => ID::custom($collection5['body']['$id']), + 'Rating' => '10' + ] + ] + ] + ] + ] + ]); + + $this->assertEquals(201, $parentDocument['headers']['status-code']); + // This is the point of the test. We should not need any authorization permission to update the document with same data. + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collection1['body']['$id'] . '/documents/' . $collection1['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'documentId' => ID::custom($collection1['body']['$id']), + 'data' => [ + 'Title' => 'Captain America', + $collection2['body']['$id'] => [ + '$id' => $collection2['body']['$id'], + 'Rating' => '10', + $collection3['body']['$id'] => [ + '$id' => $collection3['body']['$id'], + 'Rating' => '10', + $collection4['body']['$id'] => [ + '$id' => $collection4['body']['$id'], + 'Rating' => '10', + $collection5['body']['$id'] => [ + '$id' => $collection5['body']['$id'], + 'Rating' => '10' + ] + ] + ] + ] + ] + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($parentDocument['body'], $response['body']); + + // Giving update permission of collection 3 to user. + $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/collection3', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::custom('collection3'), + 'name' => ID::custom('collection3'), + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::user($userId)), + Permission::read(Role::user($userId)), + Permission::update(Role::user($userId)), + Permission::delete(Role::user($userId)), + ] + ]); + + // This is the point of this test. We should be allowed to do this action, and it should not fail on permission check + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collection1['body']['$id'] . '/documents/' . $collection1['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'Title' => 'Captain America', + $collection2['body']['$id'] => [ + '$id' => ID::custom($collection2['body']['$id']), + 'Rating' => '10', + $collection3['body']['$id'] => [ + '$id' => ID::custom($collection3['body']['$id']), + 'Rating' => '11', + $collection4['body']['$id'] => [ + '$id' => ID::custom($collection4['body']['$id']), + 'Rating' => '10', + $collection5['body']['$id'] => [ + '$id' => ID::custom($collection5['body']['$id']), + 'Rating' => '11' + ] + ] + ] + ] + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals(11, $response['body'][$collection2['body']['$id']]['collection3']['Rating']); + + // We should not be allowed to update the document as we do not have permission for collection 2. + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collection1['body']['$id'] . '/documents/' . $collection1['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'Title' => 'Captain America', + $collection2['body']['$id'] => [ + '$id' => ID::custom($collection2['body']['$id']), + 'Rating' => '11', + $collection3['body']['$id'] => null, + ] + ] + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + + // We should not be allowed to update the document as we do not have permission for collection 2. + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collection2['body']['$id'] . '/documents/' . $collection2['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'Rating' => '11', + ] + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + + // Removing update permission from collection 3. + $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/collection3', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::custom('collection3'), + 'name' => ID::custom('collection3'), + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::user($userId)), + Permission::read(Role::user($userId)), + Permission::delete(Role::user($userId)), + ] + ]); + + // Giving update permission to collection 2. + $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/collection2', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'collectionId' => ID::custom('collection2'), + 'name' => ID::custom('collection2'), + 'documentSecurity' => false, + 'permissions' => [ + Permission::create(Role::user($userId)), + Permission::update(Role::user($userId)), + Permission::read(Role::user($userId)), + Permission::delete(Role::user($userId)), + ] + ]); + + // Creating collection 3 new document + $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection3['body']['$id'] . '/documents', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'] + ]), [ + 'documentId' => ID::custom('collection3Doc1'), + 'data' => [ + 'Rating' => '20' + ] + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // We should be allowed to link a new document from collection 3 to collection 2. + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collection1['body']['$id'] . '/documents/' . $collection1['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'Title' => 'Captain America', + $collection2['body']['$id'] => [ + '$id' => ID::custom($collection2['body']['$id']), + $collection3['body']['$id'] => 'collection3Doc1', + ] + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + + + // We should be allowed to link and create a new document from collection 3 to collection 2. + $response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collection1['body']['$id'] . '/documents/' . $collection1['body']['$id'], array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'data' => [ + 'Title' => 'Captain America', + $collection2['body']['$id'] => [ + '$id' => ID::custom($collection2['body']['$id']), + $collection3['body']['$id'] => [ + '$id' => ID::custom('collection3Doc2') + ], + ] + ] + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + } }