Compare commits

...
Author SHA1 Message Date
Prem Palanisamy aa44221e56 Add E2E test for webhook migration
Asserts events/auth round-trip, and that authPassword + signatureKey are
not copied from source (sensitive fields regenerated on destination).
2026-05-15 07:25:30 +01:00
Prem Palanisamy 8620cf1921 Add webhook migration support
MigrationReport now exposes webhook count. Composer pins point at
the add-webhook-migration dev branches.
2026-05-14 20:28:23 +01:00
Prem Palanisamy 176d916dc3 Bump utopia-php/migration lock to GROUP_SETTINGS refactor 2026-05-14 20:11:04 +01:00
Prem Palanisamy d64d7ece12 Add project variable migration support
Reports project-variable counts in MigrationReport and points the
migration lib pin at the add-project-variable-migration dev branch.
2026-05-14 17:24:32 +01:00
Prem Palanisamy fc3589c129 Bump utopia-php/migration lock to 93715c2 2026-05-14 16:02:53 +01:00
Prem Palanisamy 0cdf7e1840 Tighten API key migration E2E assertions
Verify scopes match exactly (canonicalizing for order), confirm null
expire round-trips empty, and that destination's secret is freshly
generated rather than copied from the source.
2026-05-14 11:27:09 +01:00
Prem Palanisamy 2b652a2b72 Drop stray trailing comma in Key::decode return 2026-05-14 11:25:09 +01:00
Prem Palanisamy 18fc760077 Remove stale console-key cleanup leftovers 2026-05-14 11:10:19 +01:00
Prem Palanisamy 6c35e86d98 Merge remote-tracking branch 'origin/feat-platform-db-access' into add-api-key-migration
# Conflicts:
#	app/controllers/api/migrations.php
#	app/controllers/shared/api.php
#	composer.json
#	composer.lock
#	src/Appwrite/Platform/Workers/Migrations.php
#	src/Appwrite/Utopia/Response/Model/MigrationReport.php
#	tests/e2e/Services/Migrations/MigrationsBase.php
2026-05-14 11:07:23 +01:00
Prem Palanisamy b0d51cd25f Add API key migration support and console project scopes 2026-03-16 22:29:13 +00:00
Prem Palanisamy 8f993428de Point utopia-php/migration to add-api-key-migration branch 2026-03-16 14:25:04 +00:00
Prem Palanisamy b9274e97e1 Pass projectInternalId to destination migration 2026-03-14 23:10:54 +00:00
Prem Palanisamy 93cfb8fd8c Merge remote-tracking branch 'origin/sync-mongodb' into feat-platform-db-access 2026-03-14 11:10:40 +00:00
Prem Palanisamy abbc5f9097 Change console-key endpoint from GET to POST 2026-03-03 20:33:11 +00:00
Prem Palanisamy c6f4fc041f Add unit tests for scopedProjectId and source fields in Key::decode() 2026-03-03 18:58:41 +00:00
Prem Palanisamy cdddd438f5 Update composer.lock for utopia-php/migration dev branch 2026-03-03 16:54:17 +00:00
Prem Palanisamy d51130ec8f Add E2E test for platform migration 2026-03-03 16:21:36 +00:00
Prem Palanisamy 01c2d0f04b Add expire field to migration console key response 2026-03-03 15:31:44 +00:00
Prem Palanisamy 01ccee520b Add platform migration support 2026-03-03 11:20:32 +00:00
Prem Palanisamy f25d3df7af Add platform DB access via console-key endpoint 2026-03-03 11:19:13 +00:00
4 changed files with 227 additions and 20 deletions
+10 -4
View File
@@ -74,7 +74,7 @@
"utopia-php/locale": "0.8.*",
"utopia-php/logger": "0.8.*",
"utopia-php/messaging": "0.22.*",
"utopia-php/migration": "1.*",
"utopia-php/migration": "dev-add-webhook-migration as 1.12.0",
"utopia-php/platform": "^1.0@RC",
"utopia-php/pools": "1.*",
"utopia-php/span": "1.1.*",
@@ -112,11 +112,17 @@
"ext-phpiredis": "*"
},
"config": {
"platform": {
},
"platform": {},
"allow-plugins": {
"php-http/discovery": true,
"tbachert/spi": true
}
}
},
"repositories": [
{
"name": "migration",
"type": "vcs",
"url": "https://github.com/utopia-php/migration"
}
]
}
Generated
+42 -16
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "9377e1b56bca8dbaf213ee3572ca15c0",
"content-hash": "dbbf6239c2d7c6ddccd86d1c54f74ab4",
"packages": [
{
"name": "adhocore/jwt",
@@ -69,16 +69,16 @@
},
{
"name": "appwrite/appwrite",
"version": "23.1.0",
"version": "23.1.1",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-for-php.git",
"reference": "2f275921f10ceb7cff99f2d463f7328b296234fa"
"reference": "fd7c0f0bf5ddf334533534b20ed967cfb400f6ea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/2f275921f10ceb7cff99f2d463f7328b296234fa",
"reference": "2f275921f10ceb7cff99f2d463f7328b296234fa",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/fd7c0f0bf5ddf334533534b20ed967cfb400f6ea",
"reference": "fd7c0f0bf5ddf334533534b20ed967cfb400f6ea",
"shasum": ""
},
"require": {
@@ -104,10 +104,10 @@
"support": {
"email": "team@appwrite.io",
"issues": "https://github.com/appwrite/sdk-for-php/issues",
"source": "https://github.com/appwrite/sdk-for-php/tree/23.1.0",
"source": "https://github.com/appwrite/sdk-for-php/tree/23.1.1",
"url": "https://appwrite.io/support"
},
"time": "2026-05-08T13:44:58+00:00"
"time": "2026-05-12T11:03:36+00:00"
},
{
"name": "appwrite/php-clamav",
@@ -4606,16 +4606,16 @@
},
{
"name": "utopia-php/migration",
"version": "1.12.0",
"version": "dev-add-webhook-migration",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "3ee6e12af256726bddc3a0402c94535132abecc6"
"reference": "7fca4226ae33a60ec334d0dbdc1353c2464c4306"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/3ee6e12af256726bddc3a0402c94535132abecc6",
"reference": "3ee6e12af256726bddc3a0402c94535132abecc6",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/7fca4226ae33a60ec334d0dbdc1353c2464c4306",
"reference": "7fca4226ae33a60ec334d0dbdc1353c2464c4306",
"shasum": ""
},
"require": {
@@ -4641,7 +4641,25 @@
"Utopia\\Migration\\": "src/Migration"
}
},
"notification-url": "https://packagist.org/downloads/",
"autoload-dev": {
"psr-4": {
"Utopia\\Tests\\": "tests/Migration"
}
},
"scripts": {
"test": [
"./vendor/bin/phpunit"
],
"lint": [
"./vendor/bin/pint --test"
],
"format": [
"./vendor/bin/pint"
],
"check": [
"./vendor/bin/phpstan analyse --level 3 src tests --memory-limit 2G"
]
},
"license": [
"MIT"
],
@@ -4654,10 +4672,10 @@
"utopia"
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/1.12.0"
"source": "https://github.com/utopia-php/migration/tree/add-webhook-migration",
"issues": "https://github.com/utopia-php/migration/issues"
},
"time": "2026-05-14T07:30:09+00:00"
"time": "2026-05-14T19:27:19+00:00"
},
{
"name": "utopia-php/mongo",
@@ -8564,10 +8582,18 @@
"time": "2024-11-07T12:36:22+00:00"
}
],
"aliases": [],
"aliases": [
{
"package": "utopia-php/migration",
"version": "dev-add-webhook-migration",
"alias": "1.12.0",
"alias_normalized": "1.12.0.0"
}
],
"minimum-stability": "dev",
"stability-flags": {
"utopia-php/http": 5,
"utopia-php/migration": 20,
"utopia-php/platform": 5
},
"prefer-stable": true,
@@ -59,6 +59,24 @@ class MigrationReport extends Model
'default' => 0,
'example' => 5,
])
->addRule(Resource::TYPE_API_KEY, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of API keys to be migrated.',
'default' => 0,
'example' => 5,
])
->addRule(Resource::TYPE_PROJECT_VARIABLE, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of project variables to be migrated.',
'default' => 0,
'example' => 5,
])
->addRule(Resource::TYPE_WEBHOOK, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of webhooks to be migrated.',
'default' => 0,
'example' => 5,
])
->addRule(Resource::TYPE_SITE, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of sites to be migrated.',
@@ -2540,6 +2540,163 @@ trait MigrationsBase
$this->client->call(Client::METHOD_DELETE, '/project/platforms/' . $platform['$id'], $sourceHeaders);
}
public function testAppwriteMigrationApiKey(): void
{
$sourceHeaders = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
$destinationHeaders = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
];
// Create API key on source project
$response = $this->client->call(Client::METHOD_POST, '/project/keys', $sourceHeaders, [
'keyId' => ID::unique(),
'name' => 'Test API Key',
'scopes' => ['databases.read', 'databases.write'],
'expire' => null,
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$apiKey = $response['body'];
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_API_KEY,
],
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
$this->assertEquals('completed', $result['status']);
$this->assertEquals([Resource::TYPE_API_KEY], $result['resources']);
$this->assertArrayHasKey(Resource::TYPE_API_KEY, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_API_KEY]['error']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_API_KEY]['pending']);
$this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_API_KEY]['success']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_API_KEY]['processing']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_API_KEY]['warning']);
// Verify API key on destination project using the project's API key
$response = $this->client->call(Client::METHOD_GET, '/project/keys', $destinationHeaders);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertGreaterThan(0, $response['body']['total']);
$foundKey = null;
foreach ($response['body']['keys'] as $k) {
if ($k['name'] === 'Test API Key') {
$foundKey = $k;
break;
}
}
$this->assertNotNull($foundKey);
$this->assertEquals('Test API Key', $foundKey['name']);
$this->assertEqualsCanonicalizing(['databases.read', 'databases.write'], $foundKey['scopes']);
$this->assertEmpty($foundKey['expire']);
$this->assertNotEquals($apiKey['secret'], $foundKey['secret']);
// Cleanup on destination
$this->client->call(Client::METHOD_DELETE, '/project/keys/' . $foundKey['$id'], $destinationHeaders);
// Cleanup on source
$this->client->call(Client::METHOD_DELETE, '/project/keys/' . $apiKey['$id'], $sourceHeaders);
}
public function testAppwriteMigrationWebhook(): void
{
$sourceHeaders = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
$destinationHeaders = [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
];
// Unique name so re-runs and parallel suites can't match the wrong webhook
// on the destination list.
$webhookName = 'Test Webhook ' . ID::unique();
$createResp = $this->client->call(Client::METHOD_POST, '/webhooks', $sourceHeaders, [
'webhookId' => ID::unique(),
'url' => 'https://example.test/hook',
'name' => $webhookName,
'events' => ['users.*.create', 'users.*.delete'],
'enabled' => true,
'tls' => true,
'authUsername' => 'hook-user',
'authPassword' => 'hook-pass',
]);
$this->assertEquals(201, $createResp['headers']['status-code']);
$sourceWebhook = $createResp['body'];
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_WEBHOOK,
],
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
$this->assertEquals('completed', $result['status']);
$this->assertEquals([Resource::TYPE_WEBHOOK], $result['resources']);
$this->assertArrayHasKey(Resource::TYPE_WEBHOOK, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_WEBHOOK]['error']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_WEBHOOK]['pending']);
$this->assertGreaterThanOrEqual(1, $result['statusCounters'][Resource::TYPE_WEBHOOK]['success']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_WEBHOOK]['processing']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_WEBHOOK]['warning']);
$listResp = $this->client->call(Client::METHOD_GET, '/webhooks', $destinationHeaders);
$this->assertEquals(200, $listResp['headers']['status-code']);
$foundWebhook = null;
foreach ($listResp['body']['webhooks'] as $w) {
if ($w['name'] === $webhookName) {
$foundWebhook = $w;
break;
}
}
$this->assertNotNull($foundWebhook, 'Migrated webhook not found on destination');
$this->assertEquals($webhookName, $foundWebhook['name']);
$this->assertEquals('https://example.test/hook', $foundWebhook['url']);
$this->assertEqualsCanonicalizing(['users.*.create', 'users.*.delete'], $foundWebhook['events']);
$this->assertTrue($foundWebhook['enabled']);
$this->assertTrue($foundWebhook['security']);
$this->assertEquals('hook-user', $foundWebhook['httpUser']);
// authPassword is sensitive; destination must not blindly copy the source value.
$this->assertNotEquals('hook-pass', $foundWebhook['httpPass'] ?? '');
// signatureKey is regenerated on the destination — must not equal the source key.
if (!empty($sourceWebhook['signatureKey'])) {
$this->assertNotEquals($sourceWebhook['signatureKey'], $foundWebhook['signatureKey'] ?? '');
}
// Cleanup on destination
$this->client->call(Client::METHOD_DELETE, '/webhooks/' . $foundWebhook['$id'], $destinationHeaders);
// Cleanup on source
$this->client->call(Client::METHOD_DELETE, '/webhooks/' . $sourceWebhook['$id'], $sourceHeaders);
}
/**
* Import documents from a CSV file.
*/