diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 7c53b03b52..29aa9b0581 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -125,6 +125,7 @@ jobs:
Webhooks,
VCS,
Messaging,
+ Migrations
]
steps:
diff --git a/composer.lock b/composer.lock
index 5c7d48373e..6a2d373cb0 100644
--- a/composer.lock
+++ b/composer.lock
@@ -157,16 +157,16 @@
},
{
"name": "appwrite/php-runtimes",
- "version": "0.16.2",
+ "version": "0.16.4",
"source": {
"type": "git",
"url": "https://github.com/appwrite/runtimes.git",
- "reference": "c33005e3eaaf2d427e9fd1077d5335e31f4d36f9"
+ "reference": "7e4741337b9373f77210396e68eca539018cabd1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/appwrite/runtimes/zipball/c33005e3eaaf2d427e9fd1077d5335e31f4d36f9",
- "reference": "c33005e3eaaf2d427e9fd1077d5335e31f4d36f9",
+ "url": "https://api.github.com/repos/appwrite/runtimes/zipball/7e4741337b9373f77210396e68eca539018cabd1",
+ "reference": "7e4741337b9373f77210396e68eca539018cabd1",
"shasum": ""
},
"require": {
@@ -206,22 +206,22 @@
],
"support": {
"issues": "https://github.com/appwrite/runtimes/issues",
- "source": "https://github.com/appwrite/runtimes/tree/0.16.2"
+ "source": "https://github.com/appwrite/runtimes/tree/0.16.4"
},
- "time": "2024-10-09T15:02:52+00:00"
+ "time": "2024-10-26T10:39:59+00:00"
},
{
"name": "beberlei/assert",
- "version": "v3.3.2",
+ "version": "v3.3.3",
"source": {
"type": "git",
"url": "https://github.com/beberlei/assert.git",
- "reference": "cb70015c04be1baee6f5f5c953703347c0ac1655"
+ "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/beberlei/assert/zipball/cb70015c04be1baee6f5f5c953703347c0ac1655",
- "reference": "cb70015c04be1baee6f5f5c953703347c0ac1655",
+ "url": "https://api.github.com/repos/beberlei/assert/zipball/b5fd8eacd8915a1b627b8bfc027803f1939734dd",
+ "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd",
"shasum": ""
},
"require": {
@@ -229,7 +229,7 @@
"ext-json": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
- "php": "^7.0 || ^8.0"
+ "php": "^7.1 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "*",
@@ -273,9 +273,9 @@
],
"support": {
"issues": "https://github.com/beberlei/assert/issues",
- "source": "https://github.com/beberlei/assert/tree/v3.3.2"
+ "source": "https://github.com/beberlei/assert/tree/v3.3.3"
},
- "time": "2021-12-16T21:41:27+00:00"
+ "time": "2024-07-15T13:18:35+00:00"
},
{
"name": "chillerlan/php-qrcode",
@@ -2175,16 +2175,16 @@
},
{
"name": "utopia-php/migration",
- "version": "0.6.9",
+ "version": "0.6.11",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
- "reference": "ce97cdf2ca82e7cec78e2ed484ef2c71ebe8744b"
+ "reference": "4d167914d3f7fa1fe816b2b2c6f221e70166bfd7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/migration/zipball/ce97cdf2ca82e7cec78e2ed484ef2c71ebe8744b",
- "reference": "ce97cdf2ca82e7cec78e2ed484ef2c71ebe8744b",
+ "url": "https://api.github.com/repos/utopia-php/migration/zipball/4d167914d3f7fa1fe816b2b2c6f221e70166bfd7",
+ "reference": "4d167914d3f7fa1fe816b2b2c6f221e70166bfd7",
"shasum": ""
},
"require": {
@@ -2225,9 +2225,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
- "source": "https://github.com/utopia-php/migration/tree/0.6.9"
+ "source": "https://github.com/utopia-php/migration/tree/0.6.11"
},
- "time": "2024-10-16T08:33:21+00:00"
+ "time": "2024-10-31T06:19:57+00:00"
},
{
"name": "utopia-php/mongo",
@@ -2341,16 +2341,16 @@
},
{
"name": "utopia-php/platform",
- "version": "0.7.0",
+ "version": "0.7.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/platform.git",
- "reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52"
+ "reference": "3433a0f1a54988f2a59c735f507745cb2c24638a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/utopia-php/platform/zipball/beeea0f2c9bce14a6869fc5c87a1047cdecb5c52",
- "reference": "beeea0f2c9bce14a6869fc5c87a1047cdecb5c52",
+ "url": "https://api.github.com/repos/utopia-php/platform/zipball/3433a0f1a54988f2a59c735f507745cb2c24638a",
+ "reference": "3433a0f1a54988f2a59c735f507745cb2c24638a",
"shasum": ""
},
"require": {
@@ -2385,9 +2385,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/platform/issues",
- "source": "https://github.com/utopia-php/platform/tree/0.7.0"
+ "source": "https://github.com/utopia-php/platform/tree/0.7.1"
},
- "time": "2024-05-08T17:00:55+00:00"
+ "time": "2024-10-22T10:27:49+00:00"
},
{
"name": "utopia-php/pools",
@@ -5875,16 +5875,16 @@
},
{
"name": "symfony/console",
- "version": "v7.1.5",
+ "version": "v7.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee"
+ "reference": "bb5192af6edc797cbab5c8e8ecfea2fe5f421e57"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/0fa539d12b3ccf068a722bbbffa07ca7079af9ee",
- "reference": "0fa539d12b3ccf068a722bbbffa07ca7079af9ee",
+ "url": "https://api.github.com/repos/symfony/console/zipball/bb5192af6edc797cbab5c8e8ecfea2fe5f421e57",
+ "reference": "bb5192af6edc797cbab5c8e8ecfea2fe5f421e57",
"shasum": ""
},
"require": {
@@ -5948,7 +5948,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.1.5"
+ "source": "https://github.com/symfony/console/tree/v7.1.6"
},
"funding": [
{
@@ -5964,7 +5964,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-20T08:28:38+00:00"
+ "time": "2024-10-09T08:46:59+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -6035,16 +6035,16 @@
},
{
"name": "symfony/filesystem",
- "version": "v7.1.5",
+ "version": "v7.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a"
+ "reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/61fe0566189bf32e8cfee78335d8776f64a66f5a",
- "reference": "61fe0566189bf32e8cfee78335d8776f64a66f5a",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/c835867b3c62bb05c7fe3d637c871c7ae52024d4",
+ "reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4",
"shasum": ""
},
"require": {
@@ -6081,7 +6081,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v7.1.5"
+ "source": "https://github.com/symfony/filesystem/tree/v7.1.6"
},
"funding": [
{
@@ -6097,20 +6097,20 @@
"type": "tidelift"
}
],
- "time": "2024-09-17T09:16:35+00:00"
+ "time": "2024-10-25T15:11:02+00:00"
},
{
"name": "symfony/finder",
- "version": "v7.1.4",
+ "version": "v7.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "d95bbf319f7d052082fb7af147e0f835a695e823"
+ "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/d95bbf319f7d052082fb7af147e0f835a695e823",
- "reference": "d95bbf319f7d052082fb7af147e0f835a695e823",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/2cb89664897be33f78c65d3d2845954c8d7a43b8",
+ "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8",
"shasum": ""
},
"require": {
@@ -6145,7 +6145,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v7.1.4"
+ "source": "https://github.com/symfony/finder/tree/v7.1.6"
},
"funding": [
{
@@ -6161,20 +6161,20 @@
"type": "tidelift"
}
],
- "time": "2024-08-13T14:28:19+00:00"
+ "time": "2024-10-01T08:31:23+00:00"
},
{
"name": "symfony/options-resolver",
- "version": "v7.1.1",
+ "version": "v7.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
- "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55"
+ "reference": "85e95eeede2d41cd146146e98c9c81d9214cae85"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/options-resolver/zipball/47aa818121ed3950acd2b58d1d37d08a94f9bf55",
- "reference": "47aa818121ed3950acd2b58d1d37d08a94f9bf55",
+ "url": "https://api.github.com/repos/symfony/options-resolver/zipball/85e95eeede2d41cd146146e98c9c81d9214cae85",
+ "reference": "85e95eeede2d41cd146146e98c9c81d9214cae85",
"shasum": ""
},
"require": {
@@ -6212,7 +6212,7 @@
"options"
],
"support": {
- "source": "https://github.com/symfony/options-resolver/tree/v7.1.1"
+ "source": "https://github.com/symfony/options-resolver/tree/v7.1.6"
},
"funding": [
{
@@ -6228,7 +6228,7 @@
"type": "tidelift"
}
],
- "time": "2024-05-31T14:57:53+00:00"
+ "time": "2024-09-25T14:20:29+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -6546,16 +6546,16 @@
},
{
"name": "symfony/process",
- "version": "v7.1.5",
+ "version": "v7.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "5c03ee6369281177f07f7c68252a280beccba847"
+ "reference": "6aaa189ddb4ff6b5de8fa3210f2fb42c87b4d12e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847",
- "reference": "5c03ee6369281177f07f7c68252a280beccba847",
+ "url": "https://api.github.com/repos/symfony/process/zipball/6aaa189ddb4ff6b5de8fa3210f2fb42c87b4d12e",
+ "reference": "6aaa189ddb4ff6b5de8fa3210f2fb42c87b4d12e",
"shasum": ""
},
"require": {
@@ -6587,7 +6587,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.1.5"
+ "source": "https://github.com/symfony/process/tree/v7.1.6"
},
"funding": [
{
@@ -6603,7 +6603,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-19T21:48:23+00:00"
+ "time": "2024-09-25T14:20:29+00:00"
},
{
"name": "symfony/service-contracts",
@@ -6690,16 +6690,16 @@
},
{
"name": "symfony/string",
- "version": "v7.1.5",
+ "version": "v7.1.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306"
+ "reference": "61b72d66bf96c360a727ae6232df5ac83c71f626"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/d66f9c343fa894ec2037cc928381df90a7ad4306",
- "reference": "d66f9c343fa894ec2037cc928381df90a7ad4306",
+ "url": "https://api.github.com/repos/symfony/string/zipball/61b72d66bf96c360a727ae6232df5ac83c71f626",
+ "reference": "61b72d66bf96c360a727ae6232df5ac83c71f626",
"shasum": ""
},
"require": {
@@ -6757,7 +6757,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v7.1.5"
+ "source": "https://github.com/symfony/string/tree/v7.1.6"
},
"funding": [
{
@@ -6773,7 +6773,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-20T08:28:38+00:00"
+ "time": "2024-09-25T14:20:29+00:00"
},
{
"name": "textalk/websocket",
@@ -7005,7 +7005,7 @@
],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": [],
+ "stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
diff --git a/phpunit.xml b/phpunit.xml
index 90ebd4225f..4c4e55ea4e 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -33,6 +33,7 @@
./tests/e2e/Services/Storage
./tests/e2e/Services/Webhooks
./tests/e2e/Services/Messaging
+ ./tests/e2e/Services/Migrations
./tests/e2e/Services/Functions/FunctionsBase.php
./tests/e2e/Services/Functions/FunctionsCustomServerTest.php
./tests/e2e/Services/Functions/FunctionsCustomClientTest.php
diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php
index beff0b064b..d430d0eb67 100644
--- a/src/Appwrite/Platform/Workers/Migrations.php
+++ b/src/Appwrite/Platform/Workers/Migrations.php
@@ -124,7 +124,7 @@ class Migrations extends Action
),
SourceAppwrite::getName() => new SourceAppwrite(
$credentials['projectId'],
- $credentials['endpoint'],
+ $credentials['endpoint'] === 'http://localhost/v1' ? 'http://appwrite/v1' : $credentials['endpoint'],
$credentials['apiKey'],
),
default => throw new \Exception('Invalid source type'),
@@ -134,16 +134,15 @@ class Migrations extends Action
/**
* @throws Exception
*/
- protected function processDestination(Document $migration): Destination
+ protected function processDestination(Document $migration, string $apiKey): Destination
{
$destination = $migration->getAttribute('destination');
- $credentials = $migration->getAttribute('credentials');
return match ($destination) {
DestinationAppwrite::getName() => new DestinationAppwrite(
- $credentials['projectId'],
- $credentials['endpoint'],
- $credentials['apiKey'],
+ $this->project->getId(),
+ 'http://appwrite/v1',
+ $apiKey,
$this->dbForProject,
Config::getParam('collections', [])['databases']['collections'],
),
@@ -272,8 +271,8 @@ class Migrations extends Action
$migration = $this->dbForProject->getDocument('migrations', $migration->getId());
if (
- $migration->getAttribute('source') === SourceAppwrite::getName() ||
- $migration->getAttribute('destination') === DestinationAppwrite::getName()
+ $migration->getAttribute('source') === SourceAppwrite::getName() &&
+ empty($migration->getAttribute('credentials', []))
) {
$credentials = $migration->getAttribute('credentials', []);
@@ -291,7 +290,7 @@ class Migrations extends Action
$log->addTag('type', $migration->getAttribute('source'));
$source = $this->processSource($migration);
- $destination = $this->processDestination($migration);
+ $destination = $this->processDestination($migration, $tempAPIKey->getAttribute('secret'));
$source->report();
diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php
index ad2c93790c..7f84ace6f2 100644
--- a/tests/e2e/Scopes/ProjectCustom.php
+++ b/tests/e2e/Scopes/ProjectCustom.php
@@ -94,6 +94,8 @@ trait ProjectCustom
'topics.read',
'subscribers.write',
'subscribers.read',
+ 'migrations.write',
+ 'migrations.read'
],
]);
diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php
new file mode 100644
index 0000000000..e4c7ba7712
--- /dev/null
+++ b/tests/e2e/Services/Migrations/MigrationsBase.php
@@ -0,0 +1,895 @@
+getProject(true);
+ self::$project = $projectBackup;
+
+ return self::$destinationProject;
+ }
+
+ public function performMigrationSync(
+ array $body,
+ ): array {
+ $migration = $this->client->call(Client::METHOD_POST, '/migrations/appwrite', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ], $body);
+
+ $this->assertEquals(202, $migration['headers']['status-code']);
+ $this->assertNotEmpty($migration['body']);
+ $this->assertNotEmpty($migration['body']['$id']);
+
+ $attempts = 0;
+ while ($attempts < 5) {
+ $response = $this->client->call(Client::METHOD_GET, '/migrations/' . $migration['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+
+ if ($response['body']['status'] === 'failed') {
+ $this->fail('Migration failed', json_encode($response['body'], JSON_PRETTY_PRINT));
+ }
+
+ $this->assertNotEquals('failed', $response['body']['status']);
+
+ if ($response['body']['status'] === 'completed') {
+ return $response['body'];
+ }
+
+ if ($attempts === 4) {
+ $this->assertEquals('completed', $response['body']['status']);
+ }
+
+ $attempts++;
+ sleep(5);
+ }
+ }
+
+ /**
+ * Appwrite E2E Migration Tests
+ */
+ public function testCreateAppwriteMigration()
+ {
+ $response = $this->performMigrationSync([
+ 'resources' => Appwrite::getSupportedResources(),
+ 'endpoint' => 'http://localhost/v1',
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(Appwrite::getSupportedResources(), $response['resources']);
+ $this->assertEquals('Appwrite', $response['source']);
+ $this->assertEquals('Appwrite', $response['destination']);
+ $this->assertEmpty($response['statusCounters']);
+ }
+
+ /**
+ * Auth
+ */
+ public function testAppwriteMigrationAuthUserPassword()
+ {
+ $response = $this->client->call(Client::METHOD_POST, '/users', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'userId' => ID::unique(),
+ 'email' => 'test@test.com',
+ 'password' => 'password',
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+ $this->assertEquals('test@test.com', $response['body']['email']);
+
+ $user = $response['body'];
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_USER,
+ ],
+ 'endpoint' => 'http://localhost/v1',
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals('completed', $result['status']);
+ $this->assertEquals([Resource::TYPE_USER], $result['resources']);
+ $this->assertArrayHasKey(Resource::TYPE_USER, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_USER]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_USER]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_USER]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_USER]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_USER]['warning']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/users/' . $user['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+ $this->assertEquals($user['email'], $response['body']['email']);
+ $this->assertEquals($user['password'], $response['body']['password']);
+
+ // Cleanup
+ $this->client->call(Client::METHOD_DELETE, '/users/' . $user['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/users/' . $user['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ }
+
+ public function testAppwriteMigrationAuthUserPhone()
+ {
+ $response = $this->client->call(Client::METHOD_POST, '/users', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'userId' => ID::unique(),
+ 'phone' => '+12065550100',
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+ $this->assertEquals('+12065550100', $response['body']['phone']);
+
+ $user = $response['body'];
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_USER,
+ ],
+ 'endpoint' => 'http://localhost/v1',
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals('completed', $result['status']);
+ $this->assertEquals([Resource::TYPE_USER], $result['resources']);
+ $this->assertArrayHasKey(Resource::TYPE_USER, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_USER]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_USER]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_USER]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_USER]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_USER]['warning']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/users/' . $user['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+ $this->assertEquals($user['phone'], $response['body']['phone']);
+
+ // Cleanup
+ $this->client->call(Client::METHOD_DELETE, '/users/' . $user['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/users/' . $user['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+ }
+
+ public function testAppwriteMigrationAuthTeam()
+ {
+ $user = $this->client->call(Client::METHOD_POST, '/users', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'userId' => ID::unique(),
+ 'email' => 'test@test.com',
+ 'password' => 'password',
+ ]);
+
+ $this->assertEquals(201, $user['headers']['status-code']);
+ $this->assertNotEmpty($user['body']);
+ $this->assertNotEmpty($user['body']['$id']);
+ $this->assertEquals('test@test.com', $user['body']['email']);
+
+ $team = $this->client->call(Client::METHOD_POST, '/teams', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'teamId' => ID::unique(),
+ 'name' => 'Test Team',
+ ]);
+
+ $this->assertEquals(201, $team['headers']['status-code']);
+ $this->assertNotEmpty($team['body']);
+ $this->assertNotEmpty($team['body']['$id']);
+
+ $membership = $this->client->call(Client::METHOD_POST, '/teams/' . $team['body']['$id'] . '/memberships', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'teamId' => $team['body']['$id'],
+ 'userId' => $user['body']['$id'],
+ 'roles' => ['owner'],
+ ]);
+
+ $this->assertEquals(201, $membership['headers']['status-code']);
+ $this->assertNotEmpty($membership['body']);
+ $this->assertNotEmpty($membership['body']['$id']);
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_USER,
+ Resource::TYPE_TEAM,
+ Resource::TYPE_MEMBERSHIP,
+ ],
+ 'endpoint' => 'http://localhost/v1',
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals('completed', $result['status']);
+ $this->assertEquals([Resource::TYPE_USER, Resource::TYPE_TEAM, Resource::TYPE_MEMBERSHIP], $result['resources']);
+ $this->assertArrayHasKey(Resource::TYPE_USER, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_USER]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_USER]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_USER]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_USER]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_USER]['warning']);
+
+ $this->assertArrayHasKey(Resource::TYPE_TEAM, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TEAM]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TEAM]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_TEAM]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TEAM]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_TEAM]['warning']);
+
+ $this->assertArrayHasKey(Resource::TYPE_MEMBERSHIP, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_MEMBERSHIP]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_MEMBERSHIP]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_MEMBERSHIP]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_MEMBERSHIP]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_MEMBERSHIP]['warning']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/teams/' . $team['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+ $this->assertEquals($team['body']['name'], $response['body']['name']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/teams/' . $team['body']['$id'] . '/memberships', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+
+ $membership = $response['body']['memberships'][0];
+
+ $this->assertEquals($user['body']['$id'], $membership['userId']);
+ $this->assertEquals($team['body']['$id'], $membership['teamId']);
+ $this->assertEquals(['owner'], $membership['roles']);
+
+ // Cleanup
+ $this->client->call(Client::METHOD_DELETE, '/teams/' . $team['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/teams/' . $team['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/users/' . $user['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/users/' . $user['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/teams/' . $team['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/teams/' . $team['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+ }
+
+ /**
+ * Databases
+ */
+ public function testAppwriteMigrationDatabase()
+ {
+ $response = $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' => 'Test Database'
+ ]);
+
+ $this->assertEquals(201, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $databaseId = $response['body']['$id'];
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_DATABASE,
+ ],
+ 'endpoint' => 'http://localhost/v1',
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+
+ $this->assertEquals('completed', $result['status']);
+ $this->assertEquals([Resource::TYPE_DATABASE], $result['resources']);
+ $this->assertArrayHasKey(Resource::TYPE_DATABASE, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_DATABASE]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DATABASE]['warning']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $this->assertEquals($databaseId, $response['body']['$id']);
+ $this->assertEquals('Test Database', $response['body']['name']);
+
+ // Cleanup on destination
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ return [
+ 'databaseId' => $databaseId,
+ ];
+ }
+
+ /**
+ * @depends testAppwriteMigrationDatabase
+ */
+ public function testAppwriteMigrationDatabasesCollection(array $data)
+ {
+ $databaseId = $data['databaseId'];
+
+ $collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'collectionId' => ID::unique(),
+ 'name' => 'Test Collection',
+ ]);
+
+ $this->assertEquals(201, $collection['headers']['status-code']);
+
+ $collectionId = $collection['body']['$id'];
+
+ // Create Attribute
+ $response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'key' => 'name',
+ 'size' => 100,
+ 'encrypt' => false,
+ 'required' => true
+ ]);
+
+ $this->assertEquals(202, $response['headers']['status-code']);
+
+ // Wait for attribute to be ready
+ $this->assertEventually(function () use ($databaseId, $collectionId) {
+ $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/name', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertEquals('available', $response['body']['status']);
+ }, 5000, 500);
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_COLLECTION,
+ Resource::TYPE_ATTRIBUTE,
+ ],
+ 'endpoint' => 'http://localhost/v1',
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals('completed', $result['status']);
+ $this->assertEquals([Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_ATTRIBUTE], $result['resources']);
+
+ foreach ([Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_ATTRIBUTE] as $resource) {
+ $this->assertArrayHasKey($resource, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][$resource]['error']);
+ $this->assertEquals(0, $result['statusCounters'][$resource]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][$resource]['success']);
+ $this->assertEquals(0, $result['statusCounters'][$resource]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][$resource]['warning']);
+ }
+
+ $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+
+ $this->assertEquals($collectionId, $response['body']['$id']);
+ $this->assertEquals('Test Collection', $response['body']['name']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/name', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+
+ $this->assertEquals('name', $response['body']['key']);
+ $this->assertEquals(100, $response['body']['size']);
+ $this->assertEquals(true, $response['body']['required']);
+
+ // Cleanup
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ return [
+ 'databaseId' => $databaseId,
+ 'collectionId' => $collectionId,
+ ];
+ }
+
+ /**
+ * @depends testAppwriteMigrationDatabasesCollection
+ */
+ public function testAppwriteMigrationDatabasesDocument(array $data)
+ {
+ $databaseId = $data['databaseId'];
+ $collectionId = $data['collectionId'];
+
+ $document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'documentId' => ID::unique(),
+ 'data' => [
+ 'name' => 'Test Document',
+ ]
+ ]);
+
+ $this->assertEquals(201, $document['headers']['status-code']);
+ $this->assertNotEmpty($document['body']);
+ $this->assertNotEmpty($document['body']['$id']);
+
+ $documentId = $document['body']['$id'];
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_DATABASE,
+ Resource::TYPE_COLLECTION,
+ Resource::TYPE_ATTRIBUTE,
+ Resource::TYPE_DOCUMENT,
+ ],
+ 'endpoint' => 'http://localhost/v1',
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals('completed', $result['status']);
+ $this->assertEquals([Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_ATTRIBUTE, Resource::TYPE_DOCUMENT], $result['resources']);
+
+ //TODO: Add TYPE_DOCUMENT to the migration status counters once pending issue is resolved
+ foreach ([Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_ATTRIBUTE] as $resource) {
+ $this->assertArrayHasKey($resource, $result['statusCounters']);
+ $this->assertEquals(0, $result['statusCounters'][$resource]['error']);
+ $this->assertEquals(0, $result['statusCounters'][$resource]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][$resource]['success']);
+ $this->assertEquals(0, $result['statusCounters'][$resource]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][$resource]['warning']);
+ }
+
+ $response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+
+ $this->assertEquals($documentId, $response['body']['$id']);
+ $this->assertEquals('Test Document', $response['body']['name']);
+
+ // Cleanup
+ $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+ }
+
+ /**
+ * Storage
+ */
+ public function testAppwriteMigrationStorageBucket()
+ {
+ $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'bucketId' => ID::unique(),
+ 'name' => 'Test Bucket',
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ 'maximumFileSize' => 1000000,
+ 'allowedFileExtensions' => ['pdf'],
+ 'compression' => 'gzip',
+ 'encryption' => false,
+ 'antivirus' => false
+ ]);
+
+ $this->assertEquals(201, $bucket['headers']['status-code']);
+ $this->assertNotEmpty($bucket['body']);
+ $this->assertNotEmpty($bucket['body']['$id']);
+ $this->assertEquals('Test Bucket', $bucket['body']['name']);
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_BUCKET
+ ],
+ 'endpoint' => 'http://localhost/v1',
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals('completed', $result['status']);
+ $this->assertEquals([Resource::TYPE_BUCKET], $result['resources']);
+ $this->assertArrayHasKey(Resource::TYPE_BUCKET, $result['statusCounters']);
+
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_BUCKET]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_BUCKET]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_BUCKET]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_BUCKET]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_BUCKET]['warning']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucket['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $this->assertEquals($bucket['body']['$id'], $response['body']['$id']);
+ $this->assertEquals($bucket['body']['name'], $response['body']['name']);
+ $this->assertEquals($bucket['body']['$permissions'], $response['body']['$permissions']);
+ $this->assertEquals($bucket['body']['maximumFileSize'], $response['body']['maximumFileSize']);
+ $this->assertEquals($bucket['body']['allowedFileExtensions'], $response['body']['allowedFileExtensions']);
+ $this->assertEquals($bucket['body']['compression'], $response['body']['compression']);
+ $this->assertEquals($bucket['body']['encryption'], $response['body']['encryption']);
+ $this->assertEquals($bucket['body']['antivirus'], $response['body']['antivirus']);
+
+ // Cleanup
+ $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucket['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucket['body']['$id'], [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+ }
+
+ public function testAppwriteMigrationStorageFiles()
+ {
+ $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'bucketId' => ID::unique(),
+ 'name' => 'Test Bucket',
+ 'fileSecurity' => true,
+ 'maximumFileSize' => 2000000, //2MB
+ 'allowedFileExtensions' => ['jpg', 'png', 'jfif'],
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::create(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+ $this->assertEquals(201, $bucket['headers']['status-code']);
+ $this->assertNotEmpty($bucket['body']['$id']);
+
+ $bucketId = $bucket['body']['$id'];
+
+ $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', [
+ 'content-type' => 'multipart/form-data',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ], [
+ 'fileId' => ID::unique(),
+ 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
+ 'permissions' => [
+ Permission::read(Role::any()),
+ Permission::update(Role::any()),
+ Permission::delete(Role::any()),
+ ],
+ ]);
+
+ $this->assertEquals(201, $file['headers']['status-code']);
+ $this->assertNotEmpty($file['body']['$id']);
+
+ $fileId = $file['body']['$id'];
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_BUCKET,
+ Resource::TYPE_FILE
+ ],
+ 'endpoint' => 'http://localhost/v1',
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals('completed', $result['status']);
+ $this->assertEquals([Resource::TYPE_BUCKET, Resource::TYPE_FILE], $result['resources']);
+ $this->assertArrayHasKey(Resource::TYPE_BUCKET, $result['statusCounters']);
+
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_BUCKET]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_BUCKET]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_BUCKET]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_BUCKET]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_BUCKET]['warning']);
+
+ $this->assertArrayHasKey(Resource::TYPE_FILE, $result['statusCounters']);
+
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_FILE]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_FILE]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_FILE]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_FILE]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_FILE]['warning']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $this->assertEquals($fileId, $response['body']['$id']);
+
+ // Cleanup
+ $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId . '/files/' . $fileId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucketId . '/files/' . $fileId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+ }
+
+ /**
+ * Functions
+ */
+ public function testAppwriteMigrationFunction()
+ {
+ $functionId = $this->setupFunction([
+ 'functionId' => ID::unique(),
+ 'name' => 'Test',
+ 'runtime' => 'php-8.0',
+ 'entrypoint' => 'index.php'
+ ]);
+
+ $deploymentId = $this->setupDeployment($functionId, [
+ 'entrypoint' => 'index.php',
+ 'code' => $this->packageFunction('php'),
+ 'activate' => true
+ ]);
+
+ $result = $this->performMigrationSync([
+ 'resources' => [
+ Resource::TYPE_FUNCTION,
+ Resource::TYPE_DEPLOYMENT
+ ],
+ 'endpoint' => 'http://localhost/v1',
+ 'projectId' => $this->getProject()['$id'],
+ 'apiKey' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->assertEquals('completed', $result['status']);
+ $this->assertEquals([Resource::TYPE_FUNCTION, Resource::TYPE_DEPLOYMENT], $result['resources']);
+ $this->assertArrayHasKey(Resource::TYPE_FUNCTION, $result['statusCounters']);
+
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_FUNCTION]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_FUNCTION]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_FUNCTION]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_FUNCTION]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_FUNCTION]['warning']);
+
+ $this->assertArrayHasKey(Resource::TYPE_DEPLOYMENT, $result['statusCounters']);
+
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DEPLOYMENT]['error']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DEPLOYMENT]['pending']);
+ $this->assertEquals(1, $result['statusCounters'][Resource::TYPE_DEPLOYMENT]['success']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DEPLOYMENT]['processing']);
+ $this->assertEquals(0, $result['statusCounters'][Resource::TYPE_DEPLOYMENT]['warning']);
+
+ $response = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+
+ $this->assertEquals(200, $response['headers']['status-code']);
+ $this->assertNotEmpty($response['body']);
+ $this->assertNotEmpty($response['body']['$id']);
+
+ $this->assertEquals($functionId, $response['body']['$id']);
+ $this->assertEquals('Test', $response['body']['name']);
+ $this->assertEquals('php-8.0', $response['body']['runtime']);
+ $this->assertEquals('index.php', $response['body']['entrypoint']);
+
+
+ $this->assertEventually(function () use ($functionId) {
+ $deployments = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/', array_merge([
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]));
+
+ $this->assertEquals(200, $deployments['headers']['status-code']);
+ $this->assertNotEmpty($deployments['body']);
+ $this->assertEquals(1, $deployments['body']['total']);
+
+ $this->assertEquals('ready', $deployments['body']['deployments'][0]['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployments['body']['deployments'][0], JSON_PRETTY_PRINT));
+ }, 50000, 500);
+
+ // Attempt execution
+ $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ], [
+ 'body' => 'test'
+ ]);
+
+ $this->assertEquals(201, $execution['headers']['status-code']);
+ $this->assertStringContainsString('body-is-test', $execution['body']['logs']);
+
+ // Cleanup
+ $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getProject()['$id'],
+ 'x-appwrite-key' => $this->getProject()['apiKey'],
+ ]);
+
+ $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [
+ 'content-type' => 'application/json',
+ 'x-appwrite-project' => $this->getDesintationProject()['$id'],
+ 'x-appwrite-key' => $this->getDesintationProject()['apiKey'],
+ ]);
+ }
+}
diff --git a/tests/e2e/Services/Migrations/MigrationsConsoleClientTest.php b/tests/e2e/Services/Migrations/MigrationsConsoleClientTest.php
new file mode 100644
index 0000000000..2167ef9338
--- /dev/null
+++ b/tests/e2e/Services/Migrations/MigrationsConsoleClientTest.php
@@ -0,0 +1,12 @@
+