Merge remote-tracking branch 'origin/1.8.x' into feat-mongodb

# Conflicts:
#	app/controllers/api/account.php
#	app/controllers/api/messaging.php
#	app/controllers/api/projects.php
#	app/controllers/api/storage.php
#	app/controllers/api/teams.php
#	app/controllers/api/users.php
#	composer.lock
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Boolean/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Datetime/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Email/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Enum/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Float/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/IP/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Integer/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Line/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Point/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Polygon/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Create.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/Relationship/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/String/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Attributes/URL/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Delete.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Bulk/Upsert.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Create.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Delete.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Get.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Upsert.php
#	src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/XList.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Boolean/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Datetime/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Email/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Enum/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Float/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/IP/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Integer/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Line/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Point/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Polygon/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Create.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/Relationship/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/String/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Columns/URL/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Delete.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Bulk/Upsert.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Decrement.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Column/Increment.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Create.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Delete.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Get.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Update.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/Upsert.php
#	src/Appwrite/Platform/Modules/Databases/Http/TablesDB/Tables/Rows/XList.php
#	src/Appwrite/Platform/Modules/Functions/Http/Deployments/Create.php
#	src/Appwrite/Platform/Modules/Sites/Http/Deployments/Create.php
#	src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Buckets/Files/Create.php
#	src/Appwrite/Platform/Modules/Tokens/Http/Tokens/Update.php
This commit is contained in:
Jake Barnby
2025-11-20 17:46:19 +13:00
1138 changed files with 36942 additions and 6863 deletions
+1
View File
@@ -28,6 +28,7 @@ class UsageTest extends Scope
FunctionsBase::createVariable insteadof SitesBase;
FunctionsBase::getVariable insteadof SitesBase;
FunctionsBase::listVariables insteadof SitesBase;
FunctionsBase::helperGetLatestCommit insteadof SitesBase;
FunctionsBase::updateVariable insteadof SitesBase;
FunctionsBase::deleteVariable insteadof SitesBase;
FunctionsBase::getDeployment insteadof SitesBase;
@@ -41,6 +41,11 @@ trait AccountBase
$this->assertNotEmpty($response['body']['accessedAt']);
$this->assertArrayHasKey('targets', $response['body']);
$this->assertEquals($email, $response['body']['targets'][0]['identifier']);
$this->assertArrayNotHasKey('emailCanonical', $response['body']);
$this->assertArrayNotHasKey('emailIsFree', $response['body']);
$this->assertArrayNotHasKey('emailIsDisposable', $response['body']);
$this->assertArrayNotHasKey('emailIsCorporate', $response['body']);
$this->assertArrayNotHasKey('emailIsCanonical', $response['body']);
/**
* Test for FAILURE
@@ -88,4 +88,112 @@ class AccountConsoleClientTest extends Scope
$this->assertEquals($response['headers']['status-code'], 204);
}
public function testSessionAlert(): void
{
$email = uniqid() . 'session-alert@appwrite.io';
$password = 'password123';
$name = 'Session Alert Tester';
// Create a new account
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-dev-key' => $this->getProject()['devKey'] ?? ''
]), [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Create first session for the new account
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'user-agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
]), [
'email' => $email,
'password' => $password,
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Create second session for the new account
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'user-agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
]), [
'email' => $email,
'password' => $password,
]);
// Check the alert email
$lastEmail = $this->getLastEmail();
$this->assertEquals($email, $lastEmail['to'][0]['address']);
$this->assertStringContainsString('Security alert: new session', $lastEmail['subject']);
$this->assertStringContainsString($response['body']['ip'], $lastEmail['text']); // IP Address
$this->assertStringContainsString('Unknown', $lastEmail['text']); // Country
$this->assertStringContainsString($response['body']['clientName'], $lastEmail['text']); // Client name
$this->assertStringContainsStringIgnoringCase('Appwrite logo', $lastEmail['html']);
// Verify no alert sent in OTP login
$response = $this->client->call(Client::METHOD_POST, '/account/tokens/email', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => ID::unique(),
'email' => 'otpuser2@appwrite.io'
]);
$this->assertEquals($response['headers']['status-code'], 201);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['$createdAt']);
$this->assertNotEmpty($response['body']['userId']);
$this->assertNotEmpty($response['body']['expire']);
$this->assertEmpty($response['body']['secret']);
$this->assertEmpty($response['body']['phrase']);
$this->assertStringContainsStringIgnoringCase('New login detected on '. $this->getProject()['name'], $lastEmail['text']);
$userId = $response['body']['userId'];
$lastEmail = $this->getLastEmail();
$this->assertEquals('otpuser2@appwrite.io', $lastEmail['to'][0]['address']);
$this->assertEquals('OTP for ' . $this->getProject()['name'] . ' Login', $lastEmail['subject']);
// Find 6 concurrent digits in email text - OTP
preg_match_all("/\b\d{6}\b/", $lastEmail['text'], $matches);
$code = ($matches[0] ?? [])[0] ?? '';
$this->assertNotEmpty($code);
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/token', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]), [
'userId' => $userId,
'secret' => $code
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertEquals($userId, $response['body']['userId']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['expire']);
$this->assertEmpty($response['body']['secret']);
$lastEmailId = $lastEmail['id'];
$lastEmail = $this->getLastEmail();
$this->assertEquals($lastEmailId, $lastEmail['id']);
}
}
@@ -389,6 +389,26 @@ class AccountCustomClientTest extends Scope
$this->assertIsNumeric($responseLimitOffset['body']['total']);
$this->assertEquals($response['body']['logs'][1], $responseLimitOffset['body']['logs'][0]);
/**
* Test for total=false
*/
$logsWithIncludeTotalFalse = $this->client->call(Client::METHOD_GET, '/account/logs', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'total' => false
]);
$this->assertEquals(200, $logsWithIncludeTotalFalse['headers']['status-code']);
$this->assertIsArray($logsWithIncludeTotalFalse['body']);
$this->assertIsArray($logsWithIncludeTotalFalse['body']['logs']);
$this->assertIsInt($logsWithIncludeTotalFalse['body']['total']);
$this->assertEquals(0, $logsWithIncludeTotalFalse['body']['total']);
$this->assertGreaterThan(0, count($logsWithIncludeTotalFalse['body']['logs']));
/**
* Test for FAILURE
*/
@@ -1348,10 +1368,7 @@ class AccountCustomClientTest extends Scope
return $data;
}
/**
* @depends testCreateAccountSession
*/
public function testSessionAlert($data): void
public function testSessionAlert(): void
{
$email = uniqid() . 'session-alert@appwrite.io';
$password = 'password123';
@@ -1417,6 +1434,7 @@ class AccountCustomClientTest extends Scope
$this->assertStringContainsString($response['body']['ip'], $lastEmail['text']); // IP Address
$this->assertStringContainsString('Unknown', $lastEmail['text']); // Country
$this->assertStringContainsString($response['body']['clientName'], $lastEmail['text']); // Client name
$this->assertStringNotContainsStringIgnoringCase('Appwrite logo', $lastEmail['html']);
// Verify no alert sent in OTP login
$response = $this->client->call(Client::METHOD_POST, '/account/tokens/email', array_merge([
+735
View File
@@ -558,4 +558,739 @@ trait AvatarsBase
$this->assertEquals('PNG', $image->getImageFormat());
$this->assertEquals(strlen(\file_get_contents(__DIR__ . '/../../../resources/initials.png')), strlen($response['body']));
}
public function testGetScreenshot(): array
{
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
$this->assertGreaterThan(100000, strlen($response['body']));
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'headers' => [
'User-Agent' => 'Mozilla/5.0 (compatible; AppwriteBot/1.0)',
'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
/**
* Test for FAILURE - Invalid headers parameter types
*/
// Test with string headers (should fail)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'headers' => 'invalid-headers-string',
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test with numeric headers (should fail)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'headers' => 123,
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test with boolean headers (should fail)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'headers' => true,
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test with null headers - framework converts null to empty array, so this passes
// Skipping this test as null is converted to [] by the framework before validation
// Test with regular array (indexed array) - should fail
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'headers' => ['value1', 'value2', 'value3'], // Indexed array
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test with mixed array (some numeric keys) - Assoc validator allows this
// Mixed arrays are considered associative by the Assoc validator
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'headers' => ['User-Agent' => 'MyApp', 'value2', 'Accept' => 'text/html'], // Mixed array
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Test with empty array (should pass - empty associative array)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'headers' => [], // Empty associative array should pass
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Test with valid headers object (should pass)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'headers' => [
'User-Agent' => 'MyApp/1.0',
'Accept' => 'text/html,application/xhtml+xml',
'Accept-Language' => 'en-US,en;q=0.9'
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Test with headers containing special characters (should pass)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'headers' => [
'X-Custom-Header' => 'custom-value',
'Authorization' => 'Bearer token123',
'Content-Type' => 'application/json'
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Test with custom viewport width and height
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'viewportWidth' => 1920,
'viewportHeight' => 1080,
'width' => 800,
'height' => 600,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
// Test with minimum valid viewport dimensions
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'viewportWidth' => 1,
'viewportHeight' => 1,
'width' => 800,
'height' => 600,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
// Test with maximum valid viewport dimensions
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'viewportWidth' => 1920,
'viewportHeight' => 1080,
'width' => 800,
'height' => 600,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
/**
* Test for FAILURE - Invalid URL parameter
*/
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'invalid-url',
'width' => 800,
'height' => 600,
]);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'ftp://example.com', // Non-HTTP/HTTPS URL
'width' => 800,
'height' => 600,
]);
$this->assertEquals(400, $response['headers']['status-code']);
/**
* Test for FAILURE - Invalid viewport parameters
*/
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'viewportWidth' => 0, // Too small
'viewportHeight' => 720,
'width' => 800,
'height' => 600,
]);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'viewportWidth' => 2000, // Too large
'viewportHeight' => 720,
'width' => 800,
'height' => 600,
]);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'viewportWidth' => 1280,
'viewportHeight' => 0, // Too small
'width' => 800,
'height' => 600,
]);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'viewportWidth' => 1280,
'viewportHeight' => 2000, // Too large
'width' => 800,
'height' => 600,
]);
$this->assertEquals(400, $response['headers']['status-code']);
/**
* Test for FAILURE - Invalid width/height parameters
*/
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => -1, // Invalid width (negative)
'height' => 600,
]);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 3000, // Invalid height
]);
$this->assertEquals(400, $response['headers']['status-code']);
/**
* Test for FAILURE - Invalid sleep parameter
*/
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'sleep' => -1, // Negative sleep
]);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'sleep' => 15, // Too large
]);
$this->assertEquals(400, $response['headers']['status-code']);
/**
* Test for FAILURE - Invalid quality parameter
*/
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'quality' => -2, // Too small
]);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'quality' => 150, // Too large
]);
$this->assertEquals(400, $response['headers']['status-code']);
/**
* Test for FAILURE - Invalid output parameter
*/
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'output' => 'invalid-format',
]);
$this->assertEquals(400, $response['headers']['status-code']);
/**
* Test for SUCCESS - New screenshot parameters
*/
// Test with theme parameter
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'theme' => 'dark',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
// Test with scale parameter
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'scale' => 2.0,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
// Test with userAgent parameter
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'userAgent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
// Test with fullpage parameter
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'fullpage' => true,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
// Test with locale parameter
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'locale' => 'en-US',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
// Test with timezone parameter
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'timezone' => 'America/New_York',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
// Test with geolocation parameters
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'latitude' => 40.7128,
'longitude' => -74.0060,
'accuracy' => 100,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
// Test with touch parameter
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'touch' => true,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
// Test with permissions parameter
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'permissions' => [
'geolocation',
'camera',
'microphone',
'notifications'
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
// Test with original dimensions (width=0, height=0)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 0,
'height' => 0,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
// Test with all new parameters combined
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'scale' => 1.5,
'theme' => 'dark',
'userAgent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
'fullpage' => true,
'locale' => 'en-GB',
'timezone' => 'Europe/London',
'latitude' => 51.5074,
'longitude' => -0.1278,
'accuracy' => 50,
'touch' => true,
'permissions' => [
'geolocation',
'camera',
'microphone',
'notifications',
'clipboard-read',
'clipboard-write'
],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
$this->assertNotEmpty($response['body']);
/**
* Test for FAILURE - Invalid new parameters
*/
// Test invalid theme parameter
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://test' . time() . '.com',
'width' => 800,
'height' => 600,
'theme' => 'invalid-theme',
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid scale parameter (too small)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://test' . time() . '.com',
'width' => 800,
'height' => 600,
'scale' => 0.05, // Too small (min 0.1)
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid scale parameter (too large)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://test' . time() . '.com',
'width' => 800,
'height' => 600,
'scale' => 5.0, // Too large (max 3.0)
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid userAgent parameter (too long)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'userAgent' => str_repeat('A', 513), // Too long (max 512)
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid fullpage parameter (non-boolean)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'fullpage' => 'invalid-boolean',
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid locale parameter (too long)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'locale' => 'en-US-very-long-locale-string',
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid timezone parameter
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'timezone' => 'Invalid/Timezone',
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid latitude parameter (too high)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'latitude' => 91, // Too high (max 90)
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid latitude parameter (too low)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'latitude' => -91, // Too low (min -90)
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid longitude parameter (too high)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'longitude' => 181, // Too high (max 180)
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid longitude parameter (too low)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'longitude' => -181, // Too low (min -180)
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid accuracy parameter (too high)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'accuracy' => 100001, // Too high (max 100000)
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid accuracy parameter (negative)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'accuracy' => -1, // Negative (min 0)
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid touch parameter (non-boolean)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'touch' => 'invalid-boolean',
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid permissions parameter (non-array)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'permissions' => 'invalid-permissions-string',
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test invalid permissions parameter (numeric array)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'permissions' => ['geolocation', 'camera', 'microphone'], // This should pass as it's a valid array
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Test empty permissions array (should pass)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'permissions' => [], // Empty array should pass
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Test invalid permission names (should fail)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'permissions' => ['invalid-permission', 'another-invalid'],
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test mixed valid and invalid permissions (should fail)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'permissions' => ['geolocation', 'invalid-permission'],
]);
$this->assertEquals(400, $response['headers']['status-code']);
// Test valid permission names (should pass)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'permissions' => ['geolocation', 'camera', 'microphone', 'notifications'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Test advanced permission names (should pass)
$response = $this->client->call(Client::METHOD_GET, '/avatars/screenshots', [
'x-appwrite-project' => $this->getProject()['$id'],
], [
'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999),
'width' => 800,
'height' => 600,
'permissions' => ['geolocation', 'camera', 'microphone'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
return [];
}
}
@@ -10,6 +10,7 @@ use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Operator;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
@@ -986,6 +987,24 @@ trait DatabasesBase
$this->assertEquals(200, $attributes['headers']['status-code']);
$this->assertEquals(12, $attributes['body']['total']);
/**
* Test for SUCCESS with total=false
*/
$attributesWithIncludeTotalFalse = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'total' => false
]);
$this->assertEquals(200, $attributesWithIncludeTotalFalse['headers']['status-code']);
$this->assertIsArray($attributesWithIncludeTotalFalse['body']);
$this->assertIsArray($attributesWithIncludeTotalFalse['body']['attributes']);
$this->assertIsInt($attributesWithIncludeTotalFalse['body']['total']);
$this->assertEquals(0, $attributesWithIncludeTotalFalse['body']['total']);
$this->assertGreaterThan(0, count($attributesWithIncludeTotalFalse['body']['attributes']));
$attributes = $attributes['body']['attributes'];
$this->assertIsArray($attributes);
$this->assertCount(12, $attributes);
@@ -3136,6 +3155,404 @@ trait DatabasesBase
return [];
}
public function testOperators(): void
{
// Create database
$database = $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 for Operators'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection
$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' => 'Operator Tests',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$collectionId = $collection['body']['$id'];
// Create attributes
$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' => 'title',
'size' => 256,
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'releaseYear',
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'duration',
'required' => false,
]);
$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' => 'actors',
'size' => 256,
'required' => false,
'array' => true,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'integers',
'required' => false,
'array' => true,
]);
$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' => 'tagline',
'size' => 512,
'required' => false,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/datetime', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'birthDay',
'required' => false,
]);
// Wait for attributes to be created
sleep(2);
// Create a document to test operators
$document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => ID::unique(),
'data' => [
'title' => 'Operator Test',
'releaseYear' => 2020,
'duration' => 120,
'actors' => ['Actor1', 'Actor2'],
'integers' => [10, 20],
'tagline' => 'Original',
'birthDay' => '2020-01-01 12:00:00',
],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
],
]);
$this->assertEquals(201, $document['headers']['status-code']);
$documentId = $document['body']['$id'];
// Test increment operator on integer
$updated = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'releaseYear' => Operator::increment(5)->toString(),
'duration' => Operator::increment(10)->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(2025, $updated['body']['releaseYear']);
$this->assertEquals(130, $updated['body']['duration']);
// Test decrement operator
$updated = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'releaseYear' => Operator::decrement(3)->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(2022, $updated['body']['releaseYear']);
// Test array append operator
$updated = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'actors' => Operator::arrayAppend(['Actor3'])->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(['Actor1', 'Actor2', 'Actor3'], $updated['body']['actors']);
// Test array prepend operator
$updated = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'actors' => Operator::arrayPrepend(['Actor0'])->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(['Actor0', 'Actor1', 'Actor2', 'Actor3'], $updated['body']['actors']);
// Test string concat operator
$updated = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'tagline' => Operator::stringConcat(' Appended')->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals('Original Appended', $updated['body']['tagline']);
// Test multiple operators in a single update
$updated = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'releaseYear' => Operator::increment(1)->toString(),
'integers' => Operator::arrayAppend([30])->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(2023, $updated['body']['releaseYear']);
$this->assertEquals([10, 20, 30], $updated['body']['integers']);
// Test upsert with operators
$upsertId = ID::unique();
$upserted = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $upsertId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Upsert Test',
'releaseYear' => 2020,
'actors' => [],
'birthDay' => '2020-01-01 12:00:00',
],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
],
]);
$this->assertEquals(200, $upserted['headers']['status-code']);
$upserted = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $upsertId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Upsert Test Updated',
'releaseYear' => Operator::increment(5)->toString(),
'actors' => [],
'birthDay' => '2020-01-01 12:00:00',
],
]);
$this->assertEquals(200, $upserted['headers']['status-code']);
$this->assertEquals(2025, $upserted['body']['releaseYear']);
}
public function testBulkOperators(): void
{
// Create database
$database = $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 for Bulk Operators'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create collection
$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' => 'Bulk Operator Tests',
'documentSecurity' => true,
'permissions' => [
Permission::create(Role::users()),
],
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$collectionId = $collection['body']['$id'];
// Create attributes
$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' => 'title',
'size' => 256,
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'releaseYear',
'required' => true,
]);
$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' => 'actors',
'size' => 256,
'required' => false,
'array' => true,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/datetime', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'birthDay',
'required' => false,
]);
// Wait for attributes to be created
sleep(2);
// Create multiple documents
$document1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => ID::unique(),
'data' => [
'title' => 'Bulk Test 1',
'releaseYear' => 2020,
'actors' => ['Actor1'],
'birthDay' => '2020-01-01 12:00:00',
],
'permissions' => [
Permission::read(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
]);
$document2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'documentId' => ID::unique(),
'data' => [
'title' => 'Bulk Test 2',
'releaseYear' => 2021,
'actors' => ['Actor2'],
'birthDay' => '2020-01-01 12:00:00',
],
'permissions' => [
Permission::read(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
]);
$this->assertEquals(201, $document1['headers']['status-code']);
$this->assertEquals(201, $document2['headers']['status-code']);
// Test bulk update with operators
$bulkUpdate = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'releaseYear' => Operator::increment(10)->toString(),
],
'queries' => [
Query::startsWith('title', 'Bulk Test')->toString(),
],
]);
$this->assertEquals(200, $bulkUpdate['headers']['status-code']);
$this->assertGreaterThanOrEqual(2, $bulkUpdate['body']['total']);
// Verify the updates
$verify1 = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $document1['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $verify1['headers']['status-code']);
$this->assertEquals(2030, $verify1['body']['releaseYear']);
$verify2 = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $document2['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $verify2['headers']['status-code']);
$this->assertEquals(2031, $verify2['body']['releaseYear']);
}
/**
* @depends testCreateDocument
*/
@@ -55,6 +55,23 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals($test1['body']['$id'], $databases['body']['databases'][0]['$id']);
$this->assertEquals($test2['body']['$id'], $databases['body']['databases'][1]['$id']);
/**
* Test for SUCCESS with total=false
*/
$databasesWithIncludeTotalFalse = $this->client->call(Client::METHOD_GET, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'total' => false
]);
$this->assertEquals(200, $databasesWithIncludeTotalFalse['headers']['status-code']);
$this->assertIsArray($databasesWithIncludeTotalFalse['body']);
$this->assertIsArray($databasesWithIncludeTotalFalse['body']['databases']);
$this->assertIsInt($databasesWithIncludeTotalFalse['body']['total']);
$this->assertEquals(0, $databasesWithIncludeTotalFalse['body']['total']);
$this->assertGreaterThan(0, count($databasesWithIncludeTotalFalse['body']['databases']));
$base = array_reverse($databases['body']['databases']);
$databases = $this->client->call(Client::METHOD_GET, '/databases', array_merge([
@@ -395,6 +412,23 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals($test2['body']['$id'], $collections['body']['collections'][1]['$id']);
$this->assertEquals($test1['body']['enabled'], $collections['body']['collections'][0]['enabled']);
/**
* Test for SUCCESS with total=false
*/
$collectionsWithIncludeTotalFalse = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'total' => false
]);
$this->assertEquals(200, $collectionsWithIncludeTotalFalse['headers']['status-code']);
$this->assertIsArray($collectionsWithIncludeTotalFalse['body']);
$this->assertIsArray($collectionsWithIncludeTotalFalse['body']['collections']);
$this->assertIsInt($collectionsWithIncludeTotalFalse['body']['total']);
$this->assertEquals(0, $collectionsWithIncludeTotalFalse['body']['total']);
$this->assertGreaterThan(0, count($collectionsWithIncludeTotalFalse['body']['collections']));
$base = array_reverse($collections['body']['collections']);
$collections = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections', array_merge([
@@ -4580,6 +4614,23 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals(200, $documents['headers']['status-code']);
$this->assertEquals(10, $documents['body']['total']);
/**
* Test for SUCCESS with total=false
*/
$documentsWithIncludeTotalFalse = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['$id'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'total' => false
]);
$this->assertEquals(200, $documentsWithIncludeTotalFalse['headers']['status-code']);
$this->assertIsArray($documentsWithIncludeTotalFalse['body']);
$this->assertIsArray($documentsWithIncludeTotalFalse['body']['documents']);
$this->assertIsInt($documentsWithIncludeTotalFalse['body']['total']);
$this->assertEquals(0, $documentsWithIncludeTotalFalse['body']['total']);
$this->assertGreaterThan(0, count($documentsWithIncludeTotalFalse['body']['documents']));
$returnedDocuments = $response['body']['documents'];
$refetchedDocuments = $documents['body']['documents'];
@@ -10,6 +10,7 @@ use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Operator;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
@@ -3042,6 +3043,404 @@ trait DatabasesBase
return [];
}
public function testOperators(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'Test Database for Operators'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create table
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'tableId' => ID::unique(),
'name' => 'Operator Tests',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$tableId = $table['body']['$id'];
// Create columns
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'title',
'size' => 256,
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'releaseYear',
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'duration',
'required' => false,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'actors',
'size' => 256,
'required' => false,
'array' => true,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'integers',
'required' => false,
'array' => true,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'tagline',
'size' => 512,
'required' => false,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/datetime', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'birthDay',
'required' => false,
]);
// Wait for columns to be created
sleep(2);
// Create a row to test operators
$row = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'rowId' => ID::unique(),
'data' => [
'title' => 'Operator Test',
'releaseYear' => 2020,
'duration' => 120,
'actors' => ['Actor1', 'Actor2'],
'integers' => [10, 20],
'tagline' => 'Original',
'birthDay' => '2020-01-01 12:00:00',
],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
],
]);
$this->assertEquals(201, $row['headers']['status-code']);
$rowId = $row['body']['$id'];
// Test increment operator on integer
$updated = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'releaseYear' => Operator::increment(5)->toString(),
'duration' => Operator::increment(10)->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(2025, $updated['body']['releaseYear']);
$this->assertEquals(130, $updated['body']['duration']);
// Test decrement operator
$updated = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'releaseYear' => Operator::decrement(3)->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(2022, $updated['body']['releaseYear']);
// Test array append operator
$updated = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'actors' => Operator::arrayAppend(['Actor3'])->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(['Actor1', 'Actor2', 'Actor3'], $updated['body']['actors']);
// Test array prepend operator
$updated = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'actors' => Operator::arrayPrepend(['Actor0'])->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(['Actor0', 'Actor1', 'Actor2', 'Actor3'], $updated['body']['actors']);
// Test string concat operator
$updated = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'tagline' => Operator::stringConcat(' Appended')->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals('Original Appended', $updated['body']['tagline']);
// Test multiple operators in a single update
$updated = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'releaseYear' => Operator::increment(1)->toString(),
'integers' => Operator::arrayAppend([30])->toString(),
],
]);
$this->assertEquals(200, $updated['headers']['status-code']);
$this->assertEquals(2023, $updated['body']['releaseYear']);
$this->assertEquals([10, 20, 30], $updated['body']['integers']);
// Test upsert with operators
$upsertId = ID::unique();
$upserted = $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $upsertId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Upsert Test',
'releaseYear' => 2020,
'actors' => [],
'birthDay' => '2020-01-01 12:00:00',
],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
],
]);
$this->assertEquals(200, $upserted['headers']['status-code']);
$upserted = $this->client->call(Client::METHOD_PUT, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $upsertId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Upsert Test Updated',
'releaseYear' => Operator::increment(5)->toString(),
'actors' => [],
'birthDay' => '2020-01-01 12:00:00',
],
]);
$this->assertEquals(200, $upserted['headers']['status-code']);
$this->assertEquals(2025, $upserted['body']['releaseYear']);
}
public function testBulkOperators(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'databaseId' => ID::unique(),
'name' => 'Test Database for Bulk Operators'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create table
$table = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'tableId' => ID::unique(),
'name' => 'Bulk Operator Tests',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::users()),
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$tableId = $table['body']['$id'];
// Create columns
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'title',
'size' => 256,
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/integer', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'releaseYear',
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'actors',
'size' => 256,
'required' => false,
'array' => true,
]);
$this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/columns/datetime', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'birthDay',
'required' => false,
]);
// Wait for columns to be created
sleep(2);
// Create multiple rows
$row1 = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'rowId' => ID::unique(),
'data' => [
'title' => 'Bulk Test 1',
'releaseYear' => 2020,
'actors' => ['Actor1'],
'birthDay' => '2020-01-01 12:00:00',
],
'permissions' => [
Permission::read(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
]);
$row2 = $this->client->call(Client::METHOD_POST, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'rowId' => ID::unique(),
'data' => [
'title' => 'Bulk Test 2',
'releaseYear' => 2021,
'actors' => ['Actor2'],
'birthDay' => '2020-01-01 12:00:00',
],
'permissions' => [
Permission::read(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
]);
$this->assertEquals(201, $row1['headers']['status-code']);
$this->assertEquals(201, $row2['headers']['status-code']);
// Test bulk update with operators
$bulkUpdate = $this->client->call(Client::METHOD_PATCH, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'data' => [
'releaseYear' => Operator::increment(10)->toString(),
],
'queries' => [
Query::startsWith('title', 'Bulk Test')->toString(),
],
]);
$this->assertEquals(200, $bulkUpdate['headers']['status-code']);
$this->assertGreaterThanOrEqual(2, $bulkUpdate['body']['total']);
// Verify the updates
$verify1 = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $row1['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $verify1['headers']['status-code']);
$this->assertEquals(2030, $verify1['body']['releaseYear']);
$verify2 = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $row2['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $verify2['headers']['status-code']);
$this->assertEquals(2031, $verify2['body']['releaseYear']);
}
/**
* @depends testCreateRow
*/
@@ -55,6 +55,23 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals($test1['body']['$id'], $databases['body']['databases'][0]['$id']);
$this->assertEquals($test2['body']['$id'], $databases['body']['databases'][1]['$id']);
/**
* Test for SUCCESS with total=false
*/
$databasesWithIncludeTotalFalse = $this->client->call(Client::METHOD_GET, '/tablesdb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'total' => false
]);
$this->assertEquals(200, $databasesWithIncludeTotalFalse['headers']['status-code']);
$this->assertIsArray($databasesWithIncludeTotalFalse['body']);
$this->assertIsArray($databasesWithIncludeTotalFalse['body']['databases']);
$this->assertIsInt($databasesWithIncludeTotalFalse['body']['total']);
$this->assertEquals(0, $databasesWithIncludeTotalFalse['body']['total']);
$this->assertGreaterThan(0, count($databasesWithIncludeTotalFalse['body']['databases']));
$base = array_reverse($databases['body']['databases']);
$databases = $this->client->call(Client::METHOD_GET, '/tablesdb', array_merge([
@@ -6,6 +6,7 @@ use Tests\E2E\Client;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Operator;
use Utopia\Database\Query;
trait TransactionsBase
@@ -5561,4 +5562,395 @@ trait TransactionsBase
$this->assertEquals('Updated after upsert', $response['body']['name']);
$this->assertEquals(20, $response['body']['counter']);
}
/**
* Test array operators in transactions using updateRow with transactionId
* This tests the fix for operators not being parsed when stored in transaction logs
*/
public function testArrayOperatorsWithUpdateRow(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'ArrayOperatorsTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create table with array column
$table = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'Items',
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$tableId = $table['body']['$id'];
// Create array column
$column = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'items',
'size' => 255,
'required' => false,
'array' => true,
]);
$this->assertEquals(202, $column['headers']['status-code']);
sleep(2); // Wait for column to be created
// Create initial row with some items
$row = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'rowId' => 'test-row',
'data' => [
'items' => ['item1', 'item2', 'item3', 'item4']
]
]);
$this->assertEquals(201, $row['headers']['status-code']);
$this->assertEquals(['item1', 'item2', 'item3', 'item4'], $row['body']['items']);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(201, $transaction['headers']['status-code']);
$transactionId = $transaction['body']['$id'];
// Test arrayRemove operator
$updateResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test-row", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'transactionId' => $transactionId,
'data' => [
'items' => Operator::arrayRemove('item2')->toString()
]
]);
$this->assertEquals(200, $updateResponse['headers']['status-code']);
// Test arrayInsert operator
$updateResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test-row", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'transactionId' => $transactionId,
'data' => [
'items' => Operator::arrayInsert(2, 'newItem')->toString()
]
]);
$this->assertEquals(200, $updateResponse['headers']['status-code']);
// Commit transaction
$commitResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'commit' => true
]);
$this->assertEquals(200, $commitResponse['headers']['status-code']);
// Verify the operations were applied correctly
$row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/test-row", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
// After removing item2: ['item1', 'item3', 'item4']
// After inserting 'newItem' at index 2: ['item1', 'item3', 'newItem', 'item4']
$this->assertEquals(['item1', 'item3', 'newItem', 'item4'], $row['body']['items']);
}
/**
* Test array operators in transactions using createOperations
* This tests the fix for operators not being parsed in bulk operation creation
*/
public function testArrayOperatorsWithCreateOperations(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'ArrayOperatorsBulkTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create table with array column
$table = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'Tags',
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$tableId = $table['body']['$id'];
// Create array column
$column = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'tags',
'size' => 255,
'required' => false,
'array' => true,
]);
$this->assertEquals(202, $column['headers']['status-code']);
sleep(2); // Wait for column to be created
// Create initial row
$row = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'rowId' => 'doc1',
'data' => [
'tags' => ['php', 'javascript', 'python', 'ruby']
]
]);
$this->assertEquals(201, $row['headers']['status-code']);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(201, $transaction['headers']['status-code']);
$transactionId = $transaction['body']['$id'];
// Create operations using bulk createOperations endpoint with array operators
$operations = $this->client->call(Client::METHOD_POST, "/tablesdb/transactions/{$transactionId}/operations", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'operations' => [
[
'action' => 'update',
'databaseId' => $databaseId,
'tableId' => $tableId,
'rowId' => 'doc1',
'data' => [
'tags' => Operator::arrayRemove('javascript')->toString()
]
],
[
'action' => 'update',
'databaseId' => $databaseId,
'tableId' => $tableId,
'rowId' => 'doc1',
'data' => [
'tags' => Operator::arrayAppend(['go', 'rust'])->toString()
]
]
]
]);
$this->assertEquals(201, $operations['headers']['status-code']);
$this->assertEquals(2, $operations['body']['operations']);
// Commit transaction
$commitResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'commit' => true
]);
$this->assertEquals(200, $commitResponse['headers']['status-code']);
// Verify the operations were applied correctly
$row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/doc1", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
// After removing 'javascript': ['php', 'python', 'ruby']
// After appending ['go', 'rust']: ['php', 'python', 'ruby', 'go', 'rust']
$this->assertEquals(['php', 'python', 'ruby', 'go', 'rust'], $row['body']['tags']);
}
/**
* Test multiple array operators in a single transaction
* This tests all common array operators to ensure comprehensive coverage
*/
public function testMultipleArrayOperators(): void
{
// Create database
$database = $this->client->call(Client::METHOD_POST, '/tablesdb', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'MultipleOperatorsTestDB'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create table
$table = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'Arrays',
'permissions' => [
Permission::create(Role::any()),
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $table['headers']['status-code']);
$tableId = $table['body']['$id'];
// Create multiple array columns
$columns = [
['columnId' => 'list1', 'name' => 'List1'],
['columnId' => 'list2', 'name' => 'List2'],
['columnId' => 'list3', 'name' => 'List3'],
];
foreach ($columns as $col) {
$column = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/columns/string", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => $col['columnId'],
'size' => 255,
'required' => false,
'array' => true,
]);
$this->assertEquals(202, $column['headers']['status-code']);
}
sleep(2); // Wait for columns to be created
// Create initial row
$row = $this->client->call(Client::METHOD_POST, "/tablesdb/{$databaseId}/tables/{$tableId}/rows", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'rowId' => 'multi-ops',
'data' => [
'list1' => ['a', 'b', 'c'],
'list2' => ['x', 'y', 'z'],
'list3' => ['1', '2', '3', '4', '5']
]
]);
$this->assertEquals(201, $row['headers']['status-code']);
// Create transaction
$transaction = $this->client->call(Client::METHOD_POST, '/tablesdb/transactions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(201, $transaction['headers']['status-code']);
$transactionId = $transaction['body']['$id'];
// Test arrayPrepend
$this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/multi-ops", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'transactionId' => $transactionId,
'data' => [
'list1' => Operator::arrayPrepend(['z'])->toString()
]
]);
// Test arrayAppend
$this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/multi-ops", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'transactionId' => $transactionId,
'data' => [
'list2' => Operator::arrayAppend(['w'])->toString()
]
]);
// Test arrayRemove
$this->client->call(Client::METHOD_PATCH, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/multi-ops", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'transactionId' => $transactionId,
'data' => [
'list3' => Operator::arrayRemove('3')->toString()
]
]);
// Commit transaction
$commitResponse = $this->client->call(Client::METHOD_PATCH, "/tablesdb/transactions/{$transactionId}", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'commit' => true
]);
$this->assertEquals(200, $commitResponse['headers']['status-code']);
// Verify all operations were applied correctly
$row = $this->client->call(Client::METHOD_GET, "/tablesdb/{$databaseId}/tables/{$tableId}/rows/multi-ops", array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $row['headers']['status-code']);
$this->assertEquals(['z', 'a', 'b', 'c'], $row['body']['list1'], 'arrayPrepend should add element at the beginning');
$this->assertEquals(['x', 'y', 'z', 'w'], $row['body']['list2'], 'arrayAppend should add element at the end');
$this->assertEquals(['1', '2', '4', '5'], $row['body']['list3'], 'arrayRemove should remove the element');
}
}
@@ -271,6 +271,29 @@ trait FunctionsBase
return $template;
}
protected function helperGetLatestCommit(string $owner, string $repository): ?string
{
$ch = curl_init("https://api.github.com/repos/{$owner}/{$repository}/commits/main");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'User-Agent: Appwrite',
'Accept: application/vnd.github.v3+json'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$commitData = json_decode($response, true);
if (isset($commitData['sha'])) {
return $commitData['sha'];
}
}
return null;
}
protected function createExecution(string $functionId, mixed $params = []): mixed
{
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([
@@ -376,6 +376,23 @@ class FunctionsCustomClientTest extends Scope
$this->assertGreaterThan(0, $templates['body']['total']);
$this->assertIsArray($templates['body']['templates']);
/**
* Test for SUCCESS with total=false
*/
$templatesWithIncludeTotalFalse = $this->client->call(Client::METHOD_GET, '/functions/templates', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'total' => false
]);
$this->assertEquals(200, $templatesWithIncludeTotalFalse['headers']['status-code']);
$this->assertIsArray($templatesWithIncludeTotalFalse['body']);
$this->assertIsArray($templatesWithIncludeTotalFalse['body']['templates']);
$this->assertIsInt($templatesWithIncludeTotalFalse['body']['total']);
$this->assertEquals(0, $templatesWithIncludeTotalFalse['body']['total']);
$this->assertGreaterThan(0, count($templatesWithIncludeTotalFalse['body']['templates']));
foreach ($templates['body']['templates'] as $template) {
$this->assertArrayHasKey('name', $template);
$this->assertArrayHasKey('id', $template);
@@ -361,7 +361,7 @@ class FunctionsCustomServerTest extends Scope
$starterTemplate = $this->getTemplate('starter');
$this->assertEquals(200, $starterTemplate['headers']['status-code']);
$phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) {
$runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) {
return $runtime['name'] === 'node-22';
}))[0];
@@ -374,15 +374,15 @@ class FunctionsCustomServerTest extends Scope
'name' => $starterTemplate['body']['name'],
'runtime' => 'node-22',
'execute' => $starterTemplate['body']['permissions'],
'entrypoint' => $phpRuntime['entrypoint'],
'entrypoint' => $runtime['entrypoint'],
'events' => $starterTemplate['body']['events'],
'schedule' => $starterTemplate['body']['cron'],
'timeout' => $starterTemplate['body']['timeout'],
'commands' => $phpRuntime['commands'],
'commands' => $runtime['commands'],
'scopes' => $starterTemplate['body']['scopes'],
'templateRepository' => $starterTemplate['body']['providerRepositoryId'],
'templateOwner' => $starterTemplate['body']['providerOwner'],
'templateRootDirectory' => $phpRuntime['providerRootDirectory'],
'templateRootDirectory' => $runtime['providerRootDirectory'],
'templateVersion' => $starterTemplate['body']['providerVersion'],
]
);
@@ -399,38 +399,51 @@ class FunctionsCustomServerTest extends Scope
'activate' => true,
'repository' => $starterTemplate['body']['providerRepositoryId'],
'owner' => $starterTemplate['body']['providerOwner'],
'rootDirectory' => $phpRuntime['providerRootDirectory'],
'version' => $starterTemplate['body']['providerVersion'],
'rootDirectory' => $runtime['providerRootDirectory'],
'type' => 'tag',
'reference' => $starterTemplate['body']['providerVersion'],
]
);
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$deployment = $this->getDeployment($functionId, $deployment['body']['$id']);
// Wait for deployment to be ready
$deploymentId = $deployment['body']['$id'];
$this->assertEventually(function () use ($functionId, $deploymentId) {
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals('ready', $deployment['body']['status']);
}, 50000, 500);
// Verify deployment sizes
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals(0, $deployment['body']['sourceSize']);
$this->assertEquals(0, $deployment['body']['buildSize']);
$this->assertEquals(0, $deployment['body']['totalSize']);
$this->assertGreaterThan(0, $deployment['body']['sourceSize']);
$this->assertGreaterThan(0, $deployment['body']['buildSize']);
$totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize'];
$this->assertEquals($totalSize, $deployment['body']['totalSize']);
$deployments = $this->listDeployments($functionId);
$this->assertEquals(200, $deployments['headers']['status-code']);
$this->assertEquals(1, $deployments['body']['total']);
/**
* Test for SUCCESS with total=false
*/
$deploymentsWithIncludeTotalFalse = $this->listDeployments($functionId, ['total' => false]);
$this->assertEquals(200, $deploymentsWithIncludeTotalFalse['headers']['status-code']);
$this->assertIsArray($deploymentsWithIncludeTotalFalse['body']);
$this->assertIsArray($deploymentsWithIncludeTotalFalse['body']['deployments']);
$this->assertIsInt($deploymentsWithIncludeTotalFalse['body']['total']);
$this->assertEquals(0, $deploymentsWithIncludeTotalFalse['body']['total']);
$this->assertGreaterThan(0, count($deploymentsWithIncludeTotalFalse['body']['deployments']));
$lastDeployment = $deployments['body']['deployments'][0];
$this->assertNotEmpty($lastDeployment['$id']);
$this->assertEquals(0, $lastDeployment['sourceSize']);
$deploymentId = $lastDeployment['$id'];
$this->assertEventually(function () use ($functionId, $deploymentId) {
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('ready', $deployment['body']['status']);
}, 50000, 1000);
$this->assertGreaterThan(0, $lastDeployment['sourceSize']);
$function = $this->getFunction($functionId);
@@ -499,7 +512,144 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals($deployment['body']['$id'], $function['body']['deploymentId']);
$this->assertEquals($deployment['body']['$createdAt'], $function['body']['deploymentCreatedAt']);
$function = $this->cleanupFunction($functionId);
$this->cleanupFunction($functionId);
}
public function testCreateFunctionAndDeploymentFromTemplateBranch()
{
$starterTemplate = $this->getTemplate('starter');
$this->assertEquals(200, $starterTemplate['headers']['status-code']);
$runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) {
return $runtime['name'] === 'node-22';
}))[0];
// If this fails, the template has variables, and this test needs to be updated
$this->assertEmpty($starterTemplate['body']['variables']);
$function = $this->createFunction(
[
'functionId' => ID::unique(),
'name' => $starterTemplate['body']['name'] . ' - Branch Test',
'runtime' => 'node-22',
'execute' => $starterTemplate['body']['permissions'],
'entrypoint' => $runtime['entrypoint'],
'events' => $starterTemplate['body']['events'],
'schedule' => $starterTemplate['body']['cron'],
'timeout' => $starterTemplate['body']['timeout'],
'commands' => $runtime['commands'],
'scopes' => $starterTemplate['body']['scopes'],
]
);
$this->assertEquals(201, $function['headers']['status-code']);
$this->assertNotEmpty($function['body']['$id']);
$functionId = $function['body']['$id'] ?? '';
// Deploy using branch
$deployment = $this->createTemplateDeployment(
$functionId,
[
'resourceId' => ID::unique(),
'activate' => true,
'repository' => $starterTemplate['body']['providerRepositoryId'],
'owner' => $starterTemplate['body']['providerOwner'],
'rootDirectory' => $runtime['providerRootDirectory'],
'type' => 'branch',
'reference' => 'main',
]
);
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$deploymentId = $deployment['body']['$id'];
$this->assertEventually(function () use ($functionId, $deploymentId) {
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals('ready', $deployment['body']['status']);
}, 50000, 500);
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertGreaterThan(0, $deployment['body']['sourceSize']);
$this->assertGreaterThan(0, $deployment['body']['buildSize']);
$totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize'];
$this->assertEquals($totalSize, $deployment['body']['totalSize']);
$this->cleanupFunction($functionId);
}
public function testCreateFunctionAndDeploymentFromTemplateCommit()
{
$starterTemplate = $this->getTemplate('starter');
$this->assertEquals(200, $starterTemplate['headers']['status-code']);
// Get latest commit using helper function
$latestCommit = $this->helperGetLatestCommit(
$starterTemplate['body']['providerOwner'],
$starterTemplate['body']['providerRepositoryId']
);
$this->assertNotNull($latestCommit);
$runtime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) {
return $runtime['name'] === 'node-22';
}))[0];
// If this fails, the template has variables, and this test needs to be updated
$this->assertEmpty($starterTemplate['body']['variables']);
$function = $this->createFunction(
[
'functionId' => ID::unique(),
'name' => $starterTemplate['body']['name'] . ' - Commit Test',
'runtime' => 'node-22',
'execute' => $starterTemplate['body']['permissions'],
'entrypoint' => $runtime['entrypoint'],
'events' => $starterTemplate['body']['events'],
'schedule' => $starterTemplate['body']['cron'],
'timeout' => $starterTemplate['body']['timeout'],
'commands' => $runtime['commands'],
'scopes' => $starterTemplate['body']['scopes'],
]
);
$this->assertEquals(201, $function['headers']['status-code']);
$this->assertNotEmpty($function['body']['$id']);
$functionId = $function['body']['$id'] ?? '';
// Deploy using commit
$deployment = $this->createTemplateDeployment(
$functionId,
[
'resourceId' => ID::unique(),
'activate' => true,
'repository' => $starterTemplate['body']['providerRepositoryId'],
'owner' => $starterTemplate['body']['providerOwner'],
'rootDirectory' => $runtime['providerRootDirectory'],
'type' => 'commit',
'reference' => $latestCommit,
]
);
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$deploymentId = $deployment['body']['$id'];
$this->assertEventually(function () use ($functionId, $deploymentId) {
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals('ready', $deployment['body']['status']);
}, 50000, 500);
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertGreaterThan(0, $deployment['body']['sourceSize']);
$this->assertGreaterThan(0, $deployment['body']['buildSize']);
$totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize'];
$this->assertEquals($totalSize, $deployment['body']['totalSize']);
$this->cleanupFunction($functionId);
}
/**
@@ -1004,6 +1154,18 @@ class FunctionsCustomServerTest extends Scope
$this->assertCount(1, $executions['body']['executions']);
$this->assertEquals($data['deploymentId'], $executions['body']['executions'][0]['deploymentId']);
/**
* Test for SUCCESS with total=false
*/
$executionsWithIncludeTotalFalse = $this->listExecutions($data['functionId'], ['total' => false]);
$this->assertEquals(200, $executionsWithIncludeTotalFalse['headers']['status-code']);
$this->assertIsArray($executionsWithIncludeTotalFalse['body']);
$this->assertIsArray($executionsWithIncludeTotalFalse['body']['executions']);
$this->assertIsInt($executionsWithIncludeTotalFalse['body']['total']);
$this->assertEquals(0, $executionsWithIncludeTotalFalse['body']['total']);
$this->assertGreaterThan(0, count($executionsWithIncludeTotalFalse['body']['executions']));
$executions = $this->listExecutions($data['functionId'], [
'queries' => [
Query::equal('deploymentId', [$data['deploymentId']])->toString(),
+210
View File
@@ -173,4 +173,214 @@ class AvatarsTest extends Scope
return $initials['body'];
}
public function testGetScreenshot()
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::GET_SCREENSHOT);
$graphQLPayload = [
'query' => $query,
'variables' => [
'url' => 'https://appwrite.io',
'width' => 800,
'height' => 600,
],
];
$screenshot = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $screenshot['headers']['status-code']);
$this->assertNotEmpty($screenshot['body']);
// Debug: Print the actual response if it's not an image
if (!str_contains($screenshot['headers']['content-type'], 'image/')) {
echo "Response content-type: " . $screenshot['headers']['content-type'] . "\n";
echo "Response body: " . print_r($screenshot['body'], true) . "\n";
}
$this->assertStringContainsString('image/', $screenshot['headers']['content-type']);
return $screenshot['body'];
}
public function testGetScreenshotWithOriginalDimensions()
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::GET_SCREENSHOT);
$graphQLPayload = [
'query' => $query,
'variables' => [
'url' => 'https://appwrite.io',
'width' => 0,
'height' => 0,
],
];
$screenshot = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $screenshot['headers']['status-code']);
$this->assertNotEmpty($screenshot['body']);
// Debug: Print the actual response if it's not an image
if (!str_contains($screenshot['headers']['content-type'], 'image/')) {
echo "Response content-type: " . $screenshot['headers']['content-type'] . "\n";
echo "Response body: " . print_r($screenshot['body'], true) . "\n";
}
$this->assertStringContainsString('image/', $screenshot['headers']['content-type']);
return $screenshot['body'];
}
public function testGetScreenshotWithNewParameters()
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::GET_SCREENSHOT);
$graphQLPayload = [
'query' => $query,
'variables' => [
'url' => 'https://appwrite.io',
'width' => 800,
'height' => 600,
'viewportWidth' => 1920,
'viewportHeight' => 1080,
'scale' => 1.5,
'theme' => 'dark',
'userAgent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'fullpage' => true,
'locale' => 'en-US',
'timezone' => 'America/New_York',
'latitude' => 40.7128,
'longitude' => -74.0060,
'accuracy' => 100,
'touch' => true,
'permissions' => [
'geolocation',
'camera',
'microphone',
'notifications',
'clipboard-read',
'clipboard-write'
],
],
];
$screenshot = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $screenshot['headers']['status-code']);
$this->assertNotEmpty($screenshot['body']);
// Debug: Print the actual response if it's not an image
if (!str_contains($screenshot['headers']['content-type'], 'image/')) {
echo "Response content-type: " . $screenshot['headers']['content-type'] . "\n";
echo "Response body: " . print_r($screenshot['body'], true) . "\n";
}
$this->assertStringContainsString('image/', $screenshot['headers']['content-type']);
return $screenshot['body'];
}
public function testGetScreenshotWithViewportParameters()
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::GET_SCREENSHOT);
$graphQLPayload = [
'query' => $query,
'variables' => [
'url' => 'https://appwrite.io',
'width' => 800,
'height' => 600,
'viewportWidth' => 1920,
'viewportHeight' => 1080,
],
];
$screenshot = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $screenshot['headers']['status-code']);
$this->assertNotEmpty($screenshot['body']);
$this->assertStringContainsString('image/', $screenshot['headers']['content-type']);
return $screenshot['body'];
}
public function testGetScreenshotWithPermissions()
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::GET_SCREENSHOT);
$graphQLPayload = [
'query' => $query,
'variables' => [
'url' => 'https://appwrite.io',
'width' => 800,
'height' => 600,
'permissions' => [
'geolocation',
'camera',
'microphone',
'notifications'
],
],
];
$screenshot = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $screenshot['headers']['status-code']);
$this->assertNotEmpty($screenshot['body']);
// Debug: Print the actual response if it's not an image
if (!str_contains($screenshot['headers']['content-type'], 'image/')) {
echo "Response content-type: " . $screenshot['headers']['content-type'] . "\n";
echo "Response body: " . print_r($screenshot['body'], true) . "\n";
}
$this->assertStringContainsString('image/', $screenshot['headers']['content-type']);
return $screenshot['body'];
}
public function testGetScreenshotWithInvalidPermissions()
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::GET_SCREENSHOT);
$graphQLPayload = [
'query' => $query,
'variables' => [
'url' => 'https://appwrite.io',
'width' => 800,
'height' => 600,
'permissions' => [
'geolocation',
'invalid-permission',
'camera'
],
],
];
$screenshot = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $screenshot['headers']['status-code']);
$this->assertArrayHasKey('errors', $screenshot['body']);
$this->assertNotEmpty($screenshot['body']['errors']);
$this->assertStringContainsString('Invalid `permissions` param', $screenshot['body']['errors'][0]['message']);
return $screenshot['body'];
}
}
+29
View File
@@ -288,10 +288,12 @@ trait Base
public const string GET_FAVICON = 'get_favicon';
public const string GET_QRCODE = 'get_qrcode';
public const string GET_USER_INITIALS = 'get_user_initials';
public const string GET_SCREENSHOT = 'get_screenshot';
// Providers
public const string CREATE_MAILGUN_PROVIDER = 'create_mailgun_provider';
public const string CREATE_SENDGRID_PROVIDER = 'create_sendgrid_provider';
public const string CREATE_RESEND_PROVIDER = 'create_resend_provider';
public const string CREATE_SMTP_PROVIDER = 'create_smtp_provider';
public const string CREATE_TWILIO_PROVIDER = 'create_twilio_provider';
public const string CREATE_TELESIGN_PROVIDER = 'create_telesign_provider';
@@ -304,6 +306,7 @@ trait Base
public const string GET_PROVIDER = 'get_provider';
public const string UPDATE_MAILGUN_PROVIDER = 'update_mailgun_provider';
public const string UPDATE_SENDGRID_PROVIDER = 'update_sendgrid_provider';
public const string UPDATE_RESEND_PROVIDER = 'update_resend_provider';
public const string UPDATE_SMTP_PROVIDER = 'update_smtp_provider';
public const string UPDATE_TWILIO_PROVIDER = 'update_twilio_provider';
public const string UPDATE_TELESIGN_PROVIDER = 'update_telesign_provider';
@@ -1779,6 +1782,12 @@ trait Base
status
}
}';
case self::GET_SCREENSHOT:
return 'query getScreenshot($url: String!, $width: Int, $height: Int, $viewportWidth: Int, $viewportHeight: Int, $scale: Float, $theme: String, $userAgent: String, $fullpage: Boolean, $locale: String, $timezone: String, $latitude: Float, $longitude: Float, $accuracy: Float, $touch: Boolean, $permissions: [String!]) {
avatarsGetScreenshot(url: $url, width: $width, height: $height, viewportWidth: $viewportWidth, viewportHeight: $viewportHeight, scale: $scale, theme: $theme, userAgent: $userAgent, fullpage: $fullpage, locale: $locale, timezone: $timezone, latitude: $latitude, longitude: $longitude, accuracy: $accuracy, touch: $touch, permissions: $permissions) {
status
}
}';
case self::GET_ACCOUNT:
return 'query getAccount {
accountGet {
@@ -2494,6 +2503,16 @@ trait Base
enabled
}
}';
case self::CREATE_RESEND_PROVIDER:
return 'mutation createResendProvider($providerId: String!, $name: String!, $apiKey: String!, $fromName: String!, $fromEmail: String!, $replyToName: String, $replyToEmail: String) {
messagingCreateResendProvider(providerId: $providerId, name: $name, apiKey: $apiKey, fromName: $fromName, fromEmail: $fromEmail, replyToName: $replyToName, replyToEmail: $replyToEmail) {
_id
name
provider
type
enabled
}
}';
case self::CREATE_SMTP_PROVIDER:
return 'mutation createSmtpProvider($providerId: String!, $name: String!, $host: String!, $port: Int!, $username: String!, $password: String!, $encryption: String!, $autoTLS: Boolean! $fromName: String!, $fromEmail: String!, $replyToName: String, $replyToEmail: String) {
messagingCreateSmtpProvider(providerId: $providerId, name: $name, host: $host, port: $port, username: $username, password: $password, encryption: $encryption, autoTLS: $autoTLS, fromName: $fromName, fromEmail: $fromEmail, replyToName: $replyToName, replyToEmail: $replyToEmail) {
@@ -2618,6 +2637,16 @@ trait Base
enabled
}
}';
case self::UPDATE_RESEND_PROVIDER:
return 'mutation messagingUpdateResendProvider($providerId: String!, $name: String!, $apiKey: String!, $enabled: Boolean, $fromName: String, $fromEmail: String, $replyToName: String, $replyToEmail: String) {
messagingUpdateResendProvider(providerId: $providerId, name: $name, apiKey: $apiKey, enabled: $enabled, fromName: $fromName, fromEmail: $fromEmail, replyToName: $replyToName, replyToEmail: $replyToEmail) {
_id
name
provider
type
enabled
}
}';
case self::UPDATE_SMTP_PROVIDER:
return 'mutation updateSmtpProvider($providerId: String!, $name: String!, $host: String!, $port: Int!, $username: String!, $password: String!, $encryption: String!, $autoTLS: Boolean!, $fromName: String, $fromEmail: String, $enabled: Boolean) {
messagingUpdateSmtpProvider(providerId: $providerId, name: $name, host: $host, port: $port, username: $username, password: $password, encryption: $encryption, autoTLS: $autoTLS, fromName: $fromName, fromEmail: $fromEmail, enabled: $enabled) {
+22 -10
View File
@@ -26,6 +26,13 @@ class MessagingTest extends Scope
'fromName' => 'Sender Name',
'fromEmail' => 'sender-email@my-domain.com',
],
'Resend' => [
'providerId' => ID::unique(),
'name' => 'Resend1',
'apiKey' => 'my-apikey',
'fromName' => 'Sender Name',
'fromEmail' => 'sender-email@my-domain.com',
],
'Mailgun' => [
'providerId' => ID::unique(),
'name' => 'Mailgun1',
@@ -123,45 +130,50 @@ class MessagingTest extends Scope
'name' => 'Sengrid2',
'apiKey' => 'my-apikey',
],
'Mailgun' => [
'Resend' => [
'providerId' => $providers[1]['_id'],
'name' => 'Resend2',
'apiKey' => 'my-apikey',
],
'Mailgun' => [
'providerId' => $providers[2]['_id'],
'name' => 'Mailgun2',
'apiKey' => 'my-apikey',
'domain' => 'my-domain',
],
'Twilio' => [
'providerId' => $providers[2]['_id'],
'providerId' => $providers[3]['_id'],
'name' => 'Twilio2',
'accountSid' => 'my-accountSid',
'authToken' => 'my-authToken',
],
'Telesign' => [
'providerId' => $providers[3]['_id'],
'providerId' => $providers[4]['_id'],
'name' => 'Telesign2',
'customerId' => 'my-username',
'apiKey' => 'my-password',
],
'Textmagic' => [
'providerId' => $providers[4]['_id'],
'providerId' => $providers[5]['_id'],
'name' => 'Textmagic2',
'username' => 'my-username',
'apiKey' => 'my-apikey',
],
'Msg91' => [
'providerId' => $providers[5]['_id'],
'providerId' => $providers[6]['_id'],
'name' => 'Ms91-2',
'senderId' => 'my-senderid',
'authKey' => 'my-authkey',
'templateId' => '123456',
],
'Vonage' => [
'providerId' => $providers[6]['_id'],
'providerId' => $providers[7]['_id'],
'name' => 'Vonage2',
'apiKey' => 'my-apikey',
'apiSecret' => 'my-apisecret',
],
'Fcm' => [
'providerId' => $providers[7]['_id'],
'providerId' => $providers[8]['_id'],
'name' => 'FCM2',
'serviceAccountJSON' => [
'type' => 'service_account',
@@ -171,7 +183,7 @@ class MessagingTest extends Scope
]
],
'Apns' => [
'providerId' => $providers[8]['_id'],
'providerId' => $providers[9]['_id'],
'name' => 'APNS2',
'authKey' => 'my-authkey',
'authKeyId' => 'my-authkeyid',
@@ -205,7 +217,7 @@ class MessagingTest extends Scope
], [
'query' => $this->getQuery('update_mailgun_provider'),
'variables' => [
'providerId' => $providers[1]['_id'],
'providerId' => $providers[2]['_id'],
'name' => 'Mailgun2',
'apiKey' => 'my-apikey',
'domain' => 'my-domain',
@@ -213,7 +225,7 @@ class MessagingTest extends Scope
'enabled' => false,
]
]);
$providers[1] = $response['body']['data']['messagingUpdateMailgunProvider'];
$providers[2] = $response['body']['data']['messagingUpdateMailgunProvider'];
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('Mailgun2', $response['body']['data']['messagingUpdateMailgunProvider']['name']);
$this->assertEquals(false, $response['body']['data']['messagingUpdateMailgunProvider']['enabled']);
+32 -3
View File
@@ -25,6 +25,13 @@ trait MessagingBase
'apiKey' => 'my-apikey',
'from' => 'sender-email@my-domain.com',
],
'resend' => [
'providerId' => ID::unique(),
'name' => 'Resend1',
'apiKey' => 'my-apikey',
'fromName' => 'Sender Name',
'fromEmail' => 'sender-email@my-domain.com',
],
'mailgun' => [
'providerId' => ID::unique(),
'name' => 'Mailgun1',
@@ -132,6 +139,10 @@ trait MessagingBase
'name' => 'Sengrid2',
'apiKey' => 'my-apikey',
],
'resend' => [
'name' => 'Resend2',
'apiKey' => 'my-apikey',
],
'mailgun' => [
'name' => 'Mailgun2',
'apiKey' => 'my-apikey',
@@ -210,7 +221,7 @@ trait MessagingBase
$providers[$index] = $response['body'];
}
$response = $this->client->call(Client::METHOD_PATCH, '/messaging/providers/mailgun/' . $providers[1]['$id'], [
$response = $this->client->call(Client::METHOD_PATCH, '/messaging/providers/mailgun/' . $providers[2]['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
@@ -226,7 +237,7 @@ trait MessagingBase
$this->assertEquals('Mailgun2', $response['body']['name']);
$this->assertEquals(false, $response['body']['enabled']);
$providers[1] = $response['body'];
$providers[2] = $response['body'];
return $providers;
}
@@ -269,7 +280,7 @@ trait MessagingBase
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(11, \count($response['body']['providers']));
$this->assertEquals(12, \count($response['body']['providers']));
return $providers;
}
@@ -633,6 +644,24 @@ trait MessagingBase
$this->assertEquals(1, $response['body']['total']);
}
/**
* Test for SUCCESS with total=false
*/
$subscribersWithIncludeTotalFalse = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $data['topicId'] . '/subscribers', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'total' => false
]);
$this->assertEquals(200, $subscribersWithIncludeTotalFalse['headers']['status-code']);
$this->assertIsArray($subscribersWithIncludeTotalFalse['body']);
$this->assertIsArray($subscribersWithIncludeTotalFalse['body']['subscribers']);
$this->assertIsInt($subscribersWithIncludeTotalFalse['body']['total']);
$this->assertEquals(0, $subscribersWithIncludeTotalFalse['body']['total']);
$this->assertGreaterThan(0, count($subscribersWithIncludeTotalFalse['body']['subscribers']));
return $data;
}
@@ -900,7 +900,7 @@ trait MigrationsBase
/**
* Import documents from a CSV file.
*/
public function testCreateCsvMigration(): void
public function testCreateCSVImport(): void
{
// Make a database
$response = $this->client->call(Client::METHOD_POST, '/databases', [
@@ -1194,4 +1194,165 @@ trait MigrationsBase
'x-appwrite-project' => $this->getProject()['$id'],
], $body);
}
/**
* Test CSV export with email notification
*/
public function testCreateCSVExport(): void
{
// Create a database
$database = $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 Export Database'
]);
$this->assertEquals(201, $database['headers']['status-code']);
$databaseId = $database['body']['$id'];
// Create a collection
$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 Export Collection',
'permissions' => []
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$collectionId = $collection['body']['$id'];
// Create a simple attribute like the basic test
$name = $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' => 255,
'required' => true,
]);
$this->assertEquals(202, $name['headers']['status-code']);
// Create a simple attribute like the basic test
$email = $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' => 'email',
'size' => 255,
'required' => false,
]);
$this->assertEquals(202, $email['headers']['status-code']);
\sleep(3);
// Create sample documents
for ($i = 1; $i <= 10; $i++) {
$doc = $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 User ' . $i,
'email' => 'user' . $i . '@appwrite.io'
]
]);
$this->assertEquals(201, $doc['headers']['status-code'], 'Failed to create document ' . $i);
}
// Verify documents were created
$docs = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals(200, $docs['headers']['status-code']);
$this->assertEquals(10, $docs['body']['total'], 'Expected 10 documents but got ' . $docs['body']['total']);
// Perform CSV export with notification enabled (uses internal bucket)
$migration = $this->client->call(Client::METHOD_POST, '/migrations/csv/exports', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'resourceId' => $databaseId . ':' . $collectionId,
'filename' => 'test-export',
'columns' => [],
'delimiter' => ',',
'enclosure' => '"',
'escape' => '\\',
'header' => true,
'notify' => true
]);
$this->assertEquals(202, $migration['headers']['status-code']);
$this->assertNotEmpty($migration['body']['$id']);
$migrationId = $migration['body']['$id'];
$this->assertEventually(function () use ($migrationId) {
$response = $this->client->call(Client::METHOD_GET, '/migrations/' . $migrationId, [
'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('finished', $response['body']['stage']);
$this->assertEquals('completed', $response['body']['status']);
$this->assertEquals('Appwrite', $response['body']['source']);
$this->assertEquals('CSV', $response['body']['destination']);
return true;
}, 30000, 500);
// Check that email was sent with download link
$lastEmail = $this->getLastEmail();
$this->assertNotEmpty($lastEmail);
$this->assertEquals('Your CSV export is ready', $lastEmail['subject']);
$this->assertStringContainsStringIgnoringCase('Your data export has been completed successfully', $lastEmail['text']);
// Extract download URL from email HTML
\preg_match('/href="([^"]*\/storage\/buckets\/[^"]*\/push[^"]*)"/', $lastEmail['html'], $matches);
$this->assertNotEmpty($matches[1], 'Download URL not found in email');
$downloadUrl = html_entity_decode($matches[1]);
// Parse the URL to extract components
$components = \parse_url($downloadUrl);
$this->assertNotEmpty($components);
\parse_str($components['query'] ?? '', $queryParams);
$this->assertArrayHasKey('jwt', $queryParams, 'JWT not found in download URL');
$this->assertNotEmpty($queryParams['jwt']);
$this->assertArrayHasKey('project', $queryParams, 'Project not found in download URL');
$this->assertStringContainsString('/storage/buckets/default/files/', $downloadUrl);
// Test download with JWT
$path = \str_replace('/v1', '', $components['path']);
$downloadWithJwt = $this->client->call(Client::METHOD_GET, $path . '?project=' . $queryParams['project'] . '&jwt=' . $queryParams['jwt']);
$this->assertEquals(200, $downloadWithJwt['headers']['status-code'], 'Failed to download file with JWT');
// Verify the downloaded content is valid CSV
$csvData = $downloadWithJwt['body'];
$this->assertNotEmpty($csvData, 'CSV export should not be empty');
$this->assertStringContainsString('name', $csvData, 'CSV should contain the name column header');
$this->assertStringContainsString('email', $csvData, 'CSV should contain the email column header');
$this->assertStringContainsString('Test User 1', $csvData, 'CSV should contain test data');
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
}
}
+24
View File
@@ -329,9 +329,33 @@ trait SitesBase
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
return $template;
}
protected function helperGetLatestCommit(string $owner, string $repository): ?string
{
$ch = curl_init("https://api.github.com/repos/{$owner}/{$repository}/commits/main");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'User-Agent: Appwrite',
'Accept: application/vnd.github.v3+json'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$commitData = json_decode($response, true);
if (isset($commitData['sha'])) {
return $commitData['sha'];
}
}
return null;
}
protected function deleteSite(string $siteId): mixed
{
$site = $this->client->call(Client::METHOD_DELETE, '/sites/' . $siteId, array_merge([
@@ -131,8 +131,8 @@ class SitesCustomClientTest extends Scope
$this->assertEquals('github', $template['body']['vcsProvider']);
$this->assertEquals('Simple React application integrated with Appwrite SDK.', $template['body']['tagline']);
$this->assertIsArray($template['body']['frameworks']);
$this->assertEquals('http://'. $hostname . '/images/sites/templates/starter-for-react-dark.png', $template['body']['screenshotDark']);
$this->assertEquals('http://' . $hostname . '/images/sites/templates/starter-for-react-light.png', $template['body']['screenshotLight']);
$this->assertStringContainsString('/images/sites/templates/starter-for-react-dark.png', $template['body']['screenshotDark']);
$this->assertStringContainsString('/images/sites/templates/starter-for-react-light.png', $template['body']['screenshotLight']);
/**
* Test for FAILURE
@@ -634,18 +634,20 @@ class SitesCustomServerTest extends Scope
'buildRuntime' => 'node-22',
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site',
'name' => 'Test List Sites',
'outputDirectory' => './',
'providerBranch' => 'main',
'providerRootDirectory' => './',
'siteId' => ID::unique()
]);
$sites = $this->listSites();
$sites = $this->listSites([
'search' => 'Test List Sites',
]);
$this->assertEquals($sites['headers']['status-code'], 200);
$this->assertCount(1, $sites['body']['sites']);
$this->assertEquals($sites['body']['sites'][0]['name'], 'Test Site');
$this->assertEquals($sites['body']['sites'][0]['name'], 'Test List Sites');
// Test pagination limit
$sites = $this->listSites([
@@ -689,7 +691,7 @@ class SitesCustomServerTest extends Scope
// Test search name
$sites = $this->listSites([
'search' => 'Test'
'search' => 'Test List Sites'
]);
$this->assertEquals($sites['headers']['status-code'], 200);
@@ -712,21 +714,23 @@ class SitesCustomServerTest extends Scope
'buildRuntime' => 'node-22',
'fallbackFile' => '',
'framework' => 'other',
'name' => 'Test Site 2',
'name' => 'Test List Sites 2',
'outputDirectory' => './',
'providerBranch' => 'main',
'providerRootDirectory' => './',
'siteId' => ID::unique()
]);
$sites = $this->listSites();
$sites = $this->listSites([
'search' => 'Test List Sites',
]);
$this->assertEquals($sites['headers']['status-code'], 200);
$this->assertEquals($sites['body']['total'], 2);
$this->assertIsArray($sites['body']['sites']);
$this->assertCount(2, $sites['body']['sites']);
$this->assertEquals($sites['body']['sites'][0]['name'], 'Test Site');
$this->assertEquals($sites['body']['sites'][1]['name'], 'Test Site 2');
$this->assertEquals($sites['body']['sites'][0]['name'], 'Test List Sites');
$this->assertEquals($sites['body']['sites'][1]['name'], 'Test List Sites 2');
$sites1 = $this->listSites([
'queries' => [
@@ -736,7 +740,7 @@ class SitesCustomServerTest extends Scope
$this->assertEquals($sites1['headers']['status-code'], 200);
$this->assertCount(1, $sites1['body']['sites']);
$this->assertEquals($sites1['body']['sites'][0]['name'], 'Test Site 2');
$this->assertEquals($sites1['body']['sites'][0]['name'], 'Test List Sites 2');
$sites2 = $this->listSites([
'queries' => [
@@ -746,7 +750,7 @@ class SitesCustomServerTest extends Scope
$this->assertEquals($sites2['headers']['status-code'], 200);
$this->assertCount(1, $sites2['body']['sites']);
$this->assertEquals($sites2['body']['sites'][0]['name'], 'Test Site');
$this->assertEquals($sites2['body']['sites'][0]['name'], 'Test List Sites');
/**
* Test for FAILURE
@@ -1563,7 +1567,157 @@ class SitesCustomServerTest extends Scope
'repository' => $template['providerRepositoryId'],
'owner' => $template['providerOwner'],
'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'],
'version' => $template['providerVersion'],
'type' => 'tag',
'reference' => $template['providerVersion'],
'activate' => true
]);
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$deployment = $this->getDeployment($siteId, $deployment['body']['$id']);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals(0, $deployment['body']['sourceSize']);
$this->assertEquals(0, $deployment['body']['buildSize']);
$this->assertEquals(0, $deployment['body']['totalSize']);
$this->assertEventually(function () use ($siteId) {
$site = $this->getSite($siteId);
$this->assertNotEmpty($site['body']['deploymentId']);
}, 50000, 500);
$domain = $this->setupSiteDomain($siteId);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Astro Blog", $response['body']);
$this->assertStringContainsString("Hello, Astronaut!", $response['body']);
$response = $proxyClient->call(Client::METHOD_GET, '/about');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Astro Blog", $response['body']);
$this->assertStringContainsString("About Me", $response['body']);
$deployment = $this->getDeployment($siteId, $deployment['body']['$id']);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertGreaterThan(0, $deployment['body']['sourceSize']);
$this->assertGreaterThan(0, $deployment['body']['buildSize']);
$totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize'];
$this->assertEquals($totalSize, $deployment['body']['totalSize']);
$this->cleanupSite($siteId);
}
public function testCreateSiteFromTemplateBranch()
{
$template = $this->getTemplate('playground-for-astro');
$this->assertEquals(200, $template['headers']['status-code']);
$template = $template['body'];
$siteId = $this->setupSite([
'siteId' => ID::unique(),
'name' => 'Astro Blog - Branch Test',
'framework' => $template['frameworks'][0]['key'],
'adapter' => $template['frameworks'][0]['adapter'],
'buildRuntime' => $template['frameworks'][0]['buildRuntime'],
'outputDirectory' => $template['frameworks'][0]['outputDirectory'],
'buildCommand' => $template['frameworks'][0]['buildCommand'],
'installCommand' => $template['frameworks'][0]['installCommand'],
'fallbackFile' => $template['frameworks'][0]['fallbackFile'],
]);
$this->assertNotEmpty($siteId);
// Deploy using branch
$deployment = $this->createTemplateDeployment($siteId, [
'repository' => $template['providerRepositoryId'],
'owner' => $template['providerOwner'],
'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'],
'type' => 'branch',
'reference' => 'main',
'activate' => true
]);
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$deployment = $this->getDeployment($siteId, $deployment['body']['$id']);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals(0, $deployment['body']['sourceSize']);
$this->assertEquals(0, $deployment['body']['buildSize']);
$this->assertEquals(0, $deployment['body']['totalSize']);
$this->assertEventually(function () use ($siteId) {
$site = $this->getSite($siteId);
$this->assertNotEmpty($site['body']['deploymentId']);
}, 50000, 500);
$domain = $this->setupSiteDomain($siteId);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Astro Blog", $response['body']);
$this->assertStringContainsString("Hello, Astronaut!", $response['body']);
$response = $proxyClient->call(Client::METHOD_GET, '/about');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertStringContainsString("Astro Blog", $response['body']);
$this->assertStringContainsString("About Me", $response['body']);
$deployment = $this->getDeployment($siteId, $deployment['body']['$id']);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertGreaterThan(0, $deployment['body']['sourceSize']);
$this->assertGreaterThan(0, $deployment['body']['buildSize']);
$totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize'];
$this->assertEquals($totalSize, $deployment['body']['totalSize']);
$this->cleanupSite($siteId);
}
public function testCreateSiteFromTemplateCommit()
{
$template = $this->getTemplate('playground-for-astro');
$this->assertEquals(200, $template['headers']['status-code']);
// Get latest commit using helper function
$latestCommit = $this->helperGetLatestCommit(
$template['body']['providerOwner'],
$template['body']['providerRepositoryId']
);
$this->assertNotNull($latestCommit);
$template = $template['body'];
$siteId = $this->setupSite([
'siteId' => ID::unique(),
'name' => 'Astro Blog - Commit Test',
'framework' => $template['frameworks'][0]['key'],
'adapter' => $template['frameworks'][0]['adapter'],
'buildRuntime' => $template['frameworks'][0]['buildRuntime'],
'outputDirectory' => $template['frameworks'][0]['outputDirectory'],
'buildCommand' => $template['frameworks'][0]['buildCommand'],
'installCommand' => $template['frameworks'][0]['installCommand'],
'fallbackFile' => $template['frameworks'][0]['fallbackFile'],
]);
$this->assertNotEmpty($siteId);
// Deploy using commit
$deployment = $this->createTemplateDeployment($siteId, [
'repository' => $template['providerRepositoryId'],
'owner' => $template['providerOwner'],
'rootDirectory' => $template['frameworks'][0]['providerRootDirectory'],
'type' => 'commit',
'reference' => $latestCommit,
'activate' => true
]);
+77 -3
View File
@@ -30,7 +30,7 @@ trait StorageBase
'name' => 'Test Bucket',
'fileSecurity' => true,
'maximumFileSize' => 2000000, //2MB
'allowedFileExtensions' => ['jpg', 'png', 'jfif'],
'allowedFileExtensions' => ['jpg', 'png', 'jfif', 'webp'],
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
@@ -263,7 +263,39 @@ trait StorageBase
$this->assertEquals(400, $res['headers']['status-code']);
$this->assertEquals(Exception::STORAGE_INVALID_APPWRITE_ID, $res['body']['type']);
return ['bucketId' => $bucketId, 'fileId' => $file['body']['$id'], 'largeFileId' => $largeFile['body']['$id'], 'largeBucketId' => $bucket2['body']['$id']];
/**
* Test for SUCCESS - Upload and view webp image
*/
$webpFile = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/image.webp'), 'image/webp', 'image.webp'),
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$this->assertEquals(201, $webpFile['headers']['status-code']);
$this->assertNotEmpty($webpFile['body']['$id']);
$this->assertEquals('image.webp', $webpFile['body']['name']);
$this->assertEquals('image/webp', $webpFile['body']['mimeType']);
$webpFileId = $webpFile['body']['$id'];
// View webp file
$webpView = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $webpFileId . '/view', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $webpView['headers']['status-code']);
$this->assertEquals('image/webp', $webpView['headers']['content-type']);
$this->assertNotEmpty($webpView['body']);
return ['bucketId' => $bucketId, 'fileId' => $file['body']['$id'], 'largeFileId' => $largeFile['body']['$id'], 'largeBucketId' => $bucket2['body']['$id'], 'webpFileId' => $webpFileId];
}
public function testCreateBucketFileZstdCompression(): array
@@ -379,6 +411,23 @@ trait StorageBase
$this->assertGreaterThan(0, $files['body']['total']);
$this->assertGreaterThan(0, count($files['body']['files']));
/**
* Test for SUCCESS with total=false
*/
$filesWithIncludeTotalFalse = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $data['bucketId'] . '/files', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'total' => false
]);
$this->assertEquals(200, $filesWithIncludeTotalFalse['headers']['status-code']);
$this->assertIsArray($filesWithIncludeTotalFalse['body']);
$this->assertIsArray($filesWithIncludeTotalFalse['body']['files']);
$this->assertIsInt($filesWithIncludeTotalFalse['body']['total']);
$this->assertEquals(0, $filesWithIncludeTotalFalse['body']['total']);
$this->assertGreaterThan(0, count($filesWithIncludeTotalFalse['body']['files']));
$files = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $data['bucketId'] . '/files', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -399,7 +448,7 @@ trait StorageBase
],
]);
$this->assertEquals(200, $files['headers']['status-code']);
$this->assertEquals(0, count($files['body']['files']));
$this->assertEquals(1, count($files['body']['files']));
$files = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $data['bucketId'] . '/files', array_merge([
'content-type' => 'application/json',
@@ -852,6 +901,31 @@ trait StorageBase
return $data;
}
/**
* @depends testCreateBucketFile
*/
public function testFilePreview(array $data): array
{
$bucketId = $data['bucketId'];
$fileId = $data['fileId'];
// Preview PNG as webp
$preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $fileId . '/preview', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'width' => 300,
'height' => 300,
'output' => 'webp',
]);
$this->assertEquals(200, $preview['headers']['status-code']);
$this->assertEquals('image/webp', $preview['headers']['content-type']);
$this->assertNotEmpty($preview['body']);
return $data;
}
/**
* @depends testUpdateBucketFile
*/
@@ -2,6 +2,7 @@
namespace Tests\E2E\Services\Storage;
use CURLFile;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
@@ -107,4 +108,56 @@ class StorageConsoleClientTest extends Scope
$this->assertIsArray($response['body']['imageTransformations']);
$this->assertIsNumeric($response['body']['imageTransformationsTotal']);
}
public function testCreateBucketTransformationsDisabledConsole(): void
{
// Create a bucket with default settings
$bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'bucketId' => ID::unique(),
'name' => 'Test Console Bucket Transformations Disabled',
]);
$this->assertEquals(201, $bucket['headers']['status-code']);
// Create a file in the bucket
$file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucket['body']['$id'] . '/files', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'fileId' => ID::unique(),
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'transformations.png'),
]);
$this->assertEquals(201, $file['headers']['status-code']);
// Try to get the file preview
$preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucket['body']['$id'] . '/files/' . $file['body']['$id'] . '/preview', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $preview['headers']['status-code']);
// Update the bucket to disable transformations
$bucket = $this->client->call(Client::METHOD_PUT, '/storage/buckets/' . $bucket['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Test Bucket Transformations Disabled',
'transformations' => false,
]);
// Try to get the file preview again
$preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucket['body']['$id'] . '/files/' . $file['body']['$id'] . '/preview', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $preview['headers']['status-code']); // Returns 200 since image transformations are not counted for console requests
// Delete the bucket
$response = $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucket['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $response['headers']['status-code']);
}
}
@@ -1386,4 +1386,65 @@ class StorageCustomClientTest extends Scope
$this->assertStringContainsString('users', $file['body']['message']);
$this->assertStringContainsString('user:' . $this->getUser()['$id'], $file['body']['message']);
}
public function testCreateBucketTransformationsDisabled(): void
{
// Create a bucket with default settings
$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 Transformations Disabled',
'permissions' => [
Permission::read(Role::any())
],
]);
$this->assertEquals(201, $bucket['headers']['status-code']);
// Create a file in the bucket
$file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucket['body']['$id'] . '/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', 'transformations.png'),
]);
$this->assertEquals(201, $file['headers']['status-code']);
// Try to get the file preview
$preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucket['body']['$id'] . '/files/' . $file['body']['$id'] . '/preview', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertEquals(200, $preview['headers']['status-code']);
// Update the bucket to disable transformations
$bucket = $this->client->call(Client::METHOD_PUT, '/storage/buckets/' . $bucket['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'name' => 'Test Bucket Transformations Disabled',
'transformations' => false,
]);
// Try to get the file preview again
$preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucket['body']['$id'] . '/files/' . $file['body']['$id'] . '/preview', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertEquals(403, $preview['headers']['status-code']);
$this->assertStringContainsString('Image transformations are disabled for the requested bucket.', $preview['body']['message']);
// Delete the bucket
$response = $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'],
]);
$this->assertEquals(204, $response['headers']['status-code']);
}
}
+17
View File
@@ -322,6 +322,23 @@ trait TeamsBase
$this->assertEquals(400, $response['headers']['status-code']);
/**
* Test for SUCCESS with total=false
*/
$teamsWithIncludeTotalFalse = $this->client->call(Client::METHOD_GET, '/teams', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'total' => false
]);
$this->assertEquals(200, $teamsWithIncludeTotalFalse['headers']['status-code']);
$this->assertIsArray($teamsWithIncludeTotalFalse['body']);
$this->assertIsArray($teamsWithIncludeTotalFalse['body']['teams']);
$this->assertIsInt($teamsWithIncludeTotalFalse['body']['total']);
$this->assertEquals(0, $teamsWithIncludeTotalFalse['body']['total']);
$this->assertGreaterThan(0, count($teamsWithIncludeTotalFalse['body']['teams']));
return [];
}
@@ -9,7 +9,6 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\Database\DateTime;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
@@ -63,10 +62,23 @@ class TokensConsoleClientTest extends Scope
$fileId = $file['body']['$id'];
// Failure case: Expire date is in the past
$token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()));
], $this->getHeaders()), [
'expire' => '2022-11-02',
]);
$this->assertEquals(400, $token['headers']['status-code']);
$this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']);
// Success case: No expire date
$token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'expire' => null,
]);
$this->assertEquals(201, $token['headers']['status-code']);
$this->assertEquals('files', $token['body']['resourceType']);
@@ -107,8 +119,19 @@ class TokensConsoleClientTest extends Scope
{
$tokenId = $data['tokenId'];
// Failure case: Expire date is in the past
$token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'expire' => '2022-11-02',
]);
$this->assertEquals(400, $token['headers']['status-code']);
$this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']);
// Finite expiry
$expiry = DateTime::addSeconds(new \DateTime(), 3600);
$expiry = date('Y-m-d', strtotime("tomorrow"));
$token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
@@ -7,7 +7,6 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\Database\DateTime;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
@@ -61,6 +60,17 @@ class TokensCustomServerTest extends Scope
$fileId = $file['body']['$id'];
// Failure case: Expire date is in the past
$token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'expire' => '2022-11-02',
]);
$this->assertEquals(400, $token['headers']['status-code']);
$this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']);
// Success case: No expire date
$token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
@@ -83,8 +93,19 @@ class TokensCustomServerTest extends Scope
{
$tokenId = $data['tokenId'];
// Finite expiry
$expiry = DateTime::addSeconds(new \DateTime(), 3600);
// Failure case: Expire date is in the past
$token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'expire' => '2022-11-02',
]);
$this->assertEquals(400, $token['headers']['status-code']);
$this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']);
// Success case: Finite expiry
$expiry = date('Y-m-d', strtotime("tomorrow"));
$token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -94,9 +115,10 @@ class TokensCustomServerTest extends Scope
]);
$dateValidator = new DatetimeValidator();
$this->assertEquals(200, $token['headers']['status-code']);
$this->assertTrue($dateValidator->isValid($token['body']['expire']));
// Infinite expiry
// Success case: Infinite expiry
$token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
+17
View File
@@ -802,6 +802,23 @@ trait UsersBase
$this->assertIsInt($users['body']['total']);
$this->assertGreaterThan(0, $users['body']['total']);
/**
* Test for SUCCESS with total=false
*/
$usersWithIncludeTotalFalse = $this->client->call(Client::METHOD_GET, '/users', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'total' => false
]);
$this->assertEquals(200, $usersWithIncludeTotalFalse['headers']['status-code']);
$this->assertIsArray($usersWithIncludeTotalFalse['body']);
$this->assertIsArray($usersWithIncludeTotalFalse['body']['users']);
$this->assertIsInt($usersWithIncludeTotalFalse['body']['total']);
$this->assertEquals(0, $usersWithIncludeTotalFalse['body']['total']);
$this->assertGreaterThan(0, count($usersWithIncludeTotalFalse['body']['users']));
/**
* Test for FAILURE
*/
Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

+17 -25
View File
@@ -5,30 +5,20 @@ namespace Tests\Unit\Network\Validators;
use Appwrite\Network\Validator\DNS;
use Appwrite\Tests\Retry;
use PHPUnit\Framework\TestCase;
use Utopia\DNS\Message\Record;
/*
DNS Setup (on Appwrite Labs digital ocean team, network tab):
certainly.caa.appwrite.org: CAA 0 issue "certainly.com"
certainly-full.caa.appwrite.org: CAA 128 issuewild "certainly.com;account=123456;validationmethods=dns-01"
letsencrypt.certainly.caa.appwrite.org: CAA 0 issue "letsencrypt.org"
*/
/**
* DNS Setup (on Appwrite Labs digital ocean team, network tab):
*
* certainly.caa.appwrite.org: CAA 0 issue "certainly.com"
* certainly-full.caa.appwrite.org: CAA 128 issuewild "certainly.com;account=123456;validationmethods=dns-01"
* letsencrypt.certainly.caa.appwrite.org: CAA 0 issue "letsencrypt.org"
*/
class DNSTest extends TestCase
{
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function testCNAME(): void
{
$validator = new DNS('appwrite.io', DNS::RECORD_CNAME);
$validator = new DNS('appwrite.io', Record::TYPE_CNAME);
$this->assertEquals($validator->isValid(''), false);
$this->assertEquals($validator->isValid(null), false);
$this->assertEquals($validator->isValid(false), false);
@@ -39,7 +29,7 @@ class DNSTest extends TestCase
public function testA(): void
{
// IPv4 for documentation purposes
$validator = new DNS('203.0.113.1', DNS::RECORD_A);
$validator = new DNS('203.0.113.1', Record::TYPE_A);
$this->assertEquals($validator->isValid(''), false);
$this->assertEquals($validator->isValid(null), false);
$this->assertEquals($validator->isValid(false), false);
@@ -50,7 +40,7 @@ class DNSTest extends TestCase
public function testAAAA(): void
{
// IPv6 for documentation purposes
$validator = new DNS('2001:db8::1', DNS::RECORD_AAAA);
$validator = new DNS('2001:db8::1', Record::TYPE_AAAA);
$this->assertEquals($validator->isValid(''), false);
$this->assertEquals($validator->isValid(null), false);
$this->assertEquals($validator->isValid(false), false);
@@ -61,8 +51,10 @@ class DNSTest extends TestCase
#[Retry(count: 5)]
public function testCAA(): void
{
$certainly = new DNS('certainly.com', DNS::RECORD_CAA, 'ns1.digitalocean.com');
$letsencrypt = new DNS('letsencrypt.org', DNS::RECORD_CAA, 'ns1.digitalocean.com');
$digitalOceanIp = '172.64.52.210'; // ping ns1.digitalocean.com
$certainly = new DNS('certainly.com', Record::TYPE_CAA, $digitalOceanIp);
$letsencrypt = new DNS('letsencrypt.org', Record::TYPE_CAA, $digitalOceanIp);
// No CAA record succeeds on main domain & subdomains for any issuer
$this->assertEquals($certainly->isValid('caa.appwrite.org'), true);
@@ -78,11 +70,11 @@ class DNSTest extends TestCase
$this->assertEquals($letsencrypt->isValid('certainly-full.caa.appwrite.org'), false);
// Custom flags&tag are not allowed if validator includes specific flags&tag
$certainlyFull = new DNS('0 issue "certainly.com"', DNS::RECORD_CAA);
$certainlyFull = new DNS('0 issue "certainly.com"', Record::TYPE_CAA);
$this->assertEquals($certainlyFull->isValid('certainly-full.caa.appwrite.org'), false);
// Custom flags&tag still allows if they match exactly
$certainlyFull = new DNS('128 issuewild "certainly.com;account=123456;validationmethods=dns-01"', DNS::RECORD_CAA);
$certainlyFull = new DNS('128 issuewild "certainly.com;account=123456;validationmethods=dns-01"', Record::TYPE_CAA);
$this->assertEquals($certainlyFull->isValid('certainly-full.caa.appwrite.org'), true);
// Certainly CAA allows Certainly, but not LetsEncrypt; Same for subdomains