Compare commits

...
Author SHA1 Message Date
Prem Palanisamy 2c124171d6 Add E2E test for project variable migration
Covers plain + secret variables, asserts secret value is not exfiltrated
through the migration (source SDK strips it, destination writes empty).
2026-05-15 07:22:44 +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 226 additions and 14 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-project-variable-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
+36 -10
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": "5bf93473859f86fbf9438b17cb660331",
"packages": [
{
"name": "adhocore/jwt",
@@ -4606,16 +4606,16 @@
},
{
"name": "utopia-php/migration",
"version": "1.12.0",
"version": "dev-add-project-variable-migration",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "3ee6e12af256726bddc3a0402c94535132abecc6"
"reference": "3376bed6f797a8d48f0107e9a0d8e40745d6914b"
},
"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/3376bed6f797a8d48f0107e9a0d8e40745d6914b",
"reference": "3376bed6f797a8d48f0107e9a0d8e40745d6914b",
"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-project-variable-migration",
"issues": "https://github.com/utopia-php/migration/issues"
},
"time": "2026-05-14T07:30:09+00:00"
"time": "2026-05-14T19:00:51+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-project-variable-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,18 @@ 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_SITE, [
'type' => self::TYPE_INTEGER,
'description' => 'Number of sites to be migrated.',
@@ -2540,6 +2540,174 @@ 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 testAppwriteMigrationProjectVariable(): 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'],
];
// Source-side variable IDs and keys are uniquified so re-runs and parallel suites
// can't trip the source-side findOne('variables', [key=...]) skip path.
$plainKey = 'TEST_PLAIN_' . \strtoupper(ID::unique());
$secretKey = 'TEST_SECRET_' . \strtoupper(ID::unique());
// Non-secret variable: value should round-trip exactly.
$plainResp = $this->client->call(Client::METHOD_POST, '/project/variables', $sourceHeaders, [
'variableId' => ID::unique(),
'key' => $plainKey,
'value' => 'plain-value',
'secret' => false,
]);
$this->assertEquals(201, $plainResp['headers']['status-code']);
$plainVariable = $plainResp['body'];
// Secret variable: SDK strips `value` on subsequent reads, so the migration
// source sees empty and the destination writes empty. Test asserts that.
$secretResp = $this->client->call(Client::METHOD_POST, '/project/variables', $sourceHeaders, [
'variableId' => ID::unique(),
'key' => $secretKey,
'value' => 'real-secret-value',
'secret' => true,
]);
$this->assertEquals(201, $secretResp['headers']['status-code']);
$secretVariable = $secretResp['body'];
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_PROJECT_VARIABLE,
],
'endpoint' => $this->webEndpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
$this->assertEquals('completed', $result['status']);
$this->assertEquals([Resource::TYPE_PROJECT_VARIABLE], $result['resources']);
$this->assertArrayHasKey(Resource::TYPE_PROJECT_VARIABLE, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PROJECT_VARIABLE]['error']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PROJECT_VARIABLE]['pending']);
$this->assertGreaterThanOrEqual(2, $result['statusCounters'][Resource::TYPE_PROJECT_VARIABLE]['success']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PROJECT_VARIABLE]['processing']);
$this->assertEquals(0, $result['statusCounters'][Resource::TYPE_PROJECT_VARIABLE]['warning']);
$response = $this->client->call(Client::METHOD_GET, '/project/variables', $destinationHeaders);
$this->assertEquals(200, $response['headers']['status-code']);
$foundPlain = null;
$foundSecret = null;
foreach ($response['body']['variables'] as $v) {
if ($v['key'] === $plainKey) {
$foundPlain = $v;
} elseif ($v['key'] === $secretKey) {
$foundSecret = $v;
}
}
$this->assertNotNull($foundPlain, 'Plain variable not found on destination');
$this->assertEquals($plainKey, $foundPlain['key']);
$this->assertEquals('plain-value', $foundPlain['value']);
$this->assertFalse($foundPlain['secret']);
$this->assertNotNull($foundSecret, 'Secret variable not found on destination');
$this->assertEquals($secretKey, $foundSecret['key']);
// Secret variables: source SDK never returned the real value, so the destination
// also stores empty. The original 'real-secret-value' must not have leaked.
$this->assertNotEquals('real-secret-value', $foundSecret['value']);
$this->assertTrue($foundSecret['secret']);
// Cleanup on destination
$this->client->call(Client::METHOD_DELETE, '/project/variables/' . $foundPlain['$id'], $destinationHeaders);
$this->client->call(Client::METHOD_DELETE, '/project/variables/' . $foundSecret['$id'], $destinationHeaders);
// Cleanup on source
$this->client->call(Client::METHOD_DELETE, '/project/variables/' . $plainVariable['$id'], $sourceHeaders);
$this->client->call(Client::METHOD_DELETE, '/project/variables/' . $secretVariable['$id'], $sourceHeaders);
}
/**
* Import documents from a CSV file.
*/