diff --git a/tests/e2e/Services/Migrations/MigrationsBase.php b/tests/e2e/Services/Migrations/MigrationsBase.php index 9b44bc4fd3..6ba7a0604e 100644 --- a/tests/e2e/Services/Migrations/MigrationsBase.php +++ b/tests/e2e/Services/Migrations/MigrationsBase.php @@ -762,8 +762,12 @@ trait MigrationsBase } /** - * Appwrite → Appwrite row migration honoring onDuplicate=skip and onDuplicate=upsert. - * Exercises the row-buffer dispatch path via cross-project migration rather than CSV/JSON upload. + * Appwrite → Appwrite row re-migration honoring onDuplicate=skip and onDuplicate=upsert. + * + * onDuplicate only gates the row-write path in DestinationAppwrite. Re-running the + * migration with the full resource tree (database/table/column/row) always errors + * on schema creation because destination already has those. This test accepts those + * schema-level errors as expected noise and asserts row-level correctness directly. */ public function testAppwriteMigrationRowsOnDuplicate(): void { @@ -778,7 +782,6 @@ trait MigrationsBase 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], ]; - // Source setup: database + table + column + row $data = $this->setupMigrationTable(); $databaseId = $data['databaseId']; $tableId = $data['tableId']; @@ -797,7 +800,7 @@ trait MigrationsBase Resource::TYPE_ROW, ]; - // First migration: destination is empty, all resources copied + // First migration: destination is empty, strict completion expected. $first = $this->performMigrationSync([ 'resources' => $resources, 'endpoint' => $this->webEndpoint, @@ -806,51 +809,80 @@ trait MigrationsBase ]); $this->assertEquals('completed', $first['status']); - // Mutate destination row so we can prove skip preserves it + // Mutate destination row to prove onDuplicate=skip preserves it. $mutate = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $destHeaders, [ 'data' => ['name' => 'Mutated'], ]); $this->assertEquals(200, $mutate['headers']['status-code']); $this->assertEquals('Mutated', $mutate['body']['name']); - // Second migration with onDuplicate=skip: destination row must keep 'Mutated' - $second = $this->performMigrationSync([ + // Re-migration with onDuplicate=skip. Overall status is expected to be 'failed' + // because schema re-create errors (database/table/column already exist) — those + // are orthogonal to onDuplicate which only affects row writes. Assert row-level + // success counter instead. + $this->runMigrationAssertingRowSuccess([ 'resources' => $resources, 'endpoint' => $this->webEndpoint, 'projectId' => $this->getProject()['$id'], 'apiKey' => $this->getProject()['apiKey'], 'onDuplicate' => 'skip', ]); - $this->assertEquals('completed', $second['status']); $rowAfterSkip = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $destHeaders); $this->assertEquals(200, $rowAfterSkip['headers']['status-code']); $this->assertEquals('Mutated', $rowAfterSkip['body']['name'], 'onDuplicate=skip must not overwrite destination row'); - // Third migration with onDuplicate=upsert: destination row must be restored to 'Original' - $third = $this->performMigrationSync([ + // Re-migration with onDuplicate=upsert. Same status-tolerant approach; assert + // destination row was restored to source value. + $this->runMigrationAssertingRowSuccess([ 'resources' => $resources, 'endpoint' => $this->webEndpoint, 'projectId' => $this->getProject()['$id'], 'apiKey' => $this->getProject()['apiKey'], 'onDuplicate' => 'upsert', ]); - $this->assertEquals('completed', $third['status']); $rowAfterUpsert = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, $destHeaders); $this->assertEquals(200, $rowAfterUpsert['headers']['status-code']); $this->assertEquals('Original', $rowAfterUpsert['body']['name'], 'onDuplicate=upsert must restore source value'); - // Cleanup on destination $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $destHeaders); - - // Cleanup on source $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, $sourceHeaders); self::$cachedDatabaseData = []; self::$cachedTableData = []; } + /** + * Helper for Appwrite→Appwrite re-migrations where onDuplicate applies only to rows. + * Accepts migration stages of 'finished' regardless of overall status, then asserts + * the row-level counter has zero errors (and at least one success). + * + * @param array $body + */ + private function runMigrationAssertingRowSuccess(array $body): void + { + $migration = $this->client->call(Client::METHOD_POST, '/migrations/appwrite', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ], $body); + $this->assertEquals(202, $migration['headers']['status-code']); + + $this->assertEventually(function () use ($migration) { + $response = $this->client->call(Client::METHOD_GET, '/migrations/' . $migration['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getDestinationProject()['$id'], + 'x-appwrite-key' => $this->getDestinationProject()['apiKey'], + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('finished', $response['body']['stage']); + $this->assertArrayHasKey(Resource::TYPE_ROW, $response['body']['statusCounters']); + $this->assertEquals(0, $response['body']['statusCounters'][Resource::TYPE_ROW]['error']); + $this->assertGreaterThanOrEqual(1, $response['body']['statusCounters'][Resource::TYPE_ROW]['success']); + }, 60_000, 500); + } + /** * Storage */