client->call(Client::METHOD_POST, '/databases', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'databaseId' => ID::unique(), 'name' => 'TransactionTestDatabase' ]); $this->assertEquals(201, $database['headers']['status-code']); $databaseId = $database['body']['$id']; // Test creating a transaction with default TTL $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(201, $response['headers']['status-code']); $this->assertArrayHasKey('$id', $response['body']); $this->assertArrayHasKey('status', $response['body']); $this->assertArrayHasKey('operations', $response['body']); $this->assertArrayHasKey('expiresAt', $response['body']); $this->assertEquals('pending', $response['body']['status']); $this->assertEquals(0, $response['body']['operations']); $transactionId1 = $response['body']['$id']; // Test creating a transaction with custom TTL $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'ttl' => 900 ]); $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals('pending', $response['body']['status']); $expiresAt = new \DateTime($response['body']['expiresAt']); $now = new \DateTime(); $diff = $expiresAt->getTimestamp() - $now->getTimestamp(); $this->assertGreaterThan(800, $diff); $this->assertLessThan(1000, $diff); $transactionId2 = $response['body']['$id']; // Test invalid TTL values $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'ttl' => 30 // Below minimum ]); $this->assertEquals(400, $response['headers']['status-code']); $response = $this->client->call(Client::METHOD_POST, '/databases/transactions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'ttl' => 4000 // Above maximum ]); $this->assertEquals(400, $response['headers']['status-code']); return [ 'databaseId' => $databaseId, 'transactionId1' => $transactionId1, 'transactionId2' => $transactionId2 ]; } /** * @depends testCreate */ public function testAddOperations(array $data): array { $databaseId = $data['databaseId']; $transactionId = $data['transactionId1']; // Create a collection for testing $collection = $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::unique(), 'name' => 'TransactionOperationsTest', 'documentSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $this->assertEquals(201, $collection['headers']['status-code']); $collectionId = $collection['body']['$id']; // Add attributes $attribute = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'name', 'size' => 256, 'required' => true, ]); $this->assertEquals(202, $attribute['headers']['status-code']); // Wait for attribute to be created sleep(2); // Add valid operations $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [ [ 'databaseId' => $databaseId, 'collectionId' => $collectionId, 'action' => 'create', 'documentId' => 'doc1', 'data' => [ '$id' => 'doc1', 'name' => 'Test Document 1' ] ], [ 'databaseId' => $databaseId, 'collectionId' => $collectionId, 'action' => 'create', 'documentId' => 'doc2', 'data' => [ '$id' => 'doc2', 'name' => 'Test Document 2' ] ] ] ]); $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals(2, $response['body']['operations']); // Test adding more operations $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [ [ 'databaseId' => $databaseId, 'collectionId' => $collectionId, 'action' => 'update', 'documentId' => 'doc1', 'data' => [ 'name' => 'Updated Document 1' ] ] ] ]); $this->assertEquals(201, $response['headers']['status-code']); $this->assertEquals(3, $response['body']['operations']); // Test invalid database ID $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [ [ 'databaseId' => 'invalid_database', 'collectionId' => $collectionId, 'action' => 'create', 'data' => ['name' => 'Test'] ] ] ]); $this->assertEquals(404, $response['headers']['status-code']); // Test invalid collection ID $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [ [ 'databaseId' => $databaseId, 'collectionId' => 'invalid_collection', 'action' => 'create', 'data' => ['name' => 'Test'] ] ] ]); $this->assertEquals(404, $response['headers']['status-code']); return array_merge($data, [ 'collectionId' => $collectionId ]); } /** * @depends testAddOperations */ public function testCommit(array $data): void { $databaseId = $data['databaseId']; $collectionId = $data['collectionId']; $transactionId = $data['transactionId1']; // Commit the transaction $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'commit' => true ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('committed', $response['body']['status']); // Verify documents were created $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $documents['headers']['status-code']); $this->assertEquals(2, $documents['body']['total']); // Verify the update was applied $doc1Found = false; foreach ($documents['body']['documents'] as $doc) { if ($doc['$id'] === 'doc1') { $this->assertEquals('Updated Document 1', $doc['name']); $doc1Found = true; } } $this->assertTrue($doc1Found, 'Document doc1 should exist with updated name'); // Test committing already committed transaction $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'commit' => true ]); $this->assertEquals(400, $response['headers']['status-code']); } /** * @depends testCreate */ public function testRollback(array $data): void { $databaseId = $data['databaseId']; $transactionId = $data['transactionId2']; // Create a collection for rollback test $collection = $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::unique(), 'name' => 'TransactionRollbackTest', 'documentSecurity' => false, 'permissions' => [ Permission::create(Role::any()), Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any()), ], ]); $collectionId = $collection['body']['$id']; // Add attribute $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-key' => $this->getProject()['apiKey'] ]), [ 'key' => 'value', 'size' => 256, 'required' => true, ]); sleep(2); // Add operations $response = $this->client->call(Client::METHOD_POST, "/databases/transactions/{$transactionId}/operations", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'operations' => [ [ 'databaseId' => $databaseId, 'collectionId' => $collectionId, 'action' => 'create', 'data' => [ '$id' => 'rollback_doc', 'value' => 'Should not exist' ] ] ] ]); $this->assertEquals(201, $response['headers']['status-code']); // Rollback the transaction $response = $this->client->call(Client::METHOD_PATCH, "/databases/transactions/{$transactionId}", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders()), [ 'rollback' => true ]); $this->assertEquals(200, $response['headers']['status-code']); $this->assertEquals('rolledBack', $response['body']['status']); // Verify no documents were created $documents = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/documents", array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertEquals(200, $documents['headers']['status-code']); $this->assertEquals(0, $documents['body']['total']); } }