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

# Conflicts:
#	app/config/errors.php
#	app/config/roles.php
#	app/controllers/api/databases.php
#	composer.json
#	composer.lock
#	src/Appwrite/Extend/Exception.php
#	src/Appwrite/Utopia/Response.php
#	tests/e2e/Services/Databases/Legacy/DatabasesBase.php
This commit is contained in:
Jake Barnby
2025-07-29 22:16:56 +12:00
7242 changed files with 199911 additions and 19200 deletions
+112 -10
View File
@@ -26,9 +26,9 @@ class AbuseTest extends Scope
}
}
public function testAbuseCreateDocument()
public function testAbuseCreateDocumentCollectionsAPI()
{
$data = $this->createCollection();
$data = $this->createCollectionOrTable();
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
$max = 120;
@@ -52,9 +52,9 @@ class AbuseTest extends Scope
}
}
public function testAbuseUpdateDocument()
public function testAbuseUpdateDocumentCollectionsAPI()
{
$data = $this->createCollection();
$data = $this->createCollectionOrTable();
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
$max = 120;
@@ -90,9 +90,9 @@ class AbuseTest extends Scope
}
}
public function testAbuseDeleteDocument()
public function testAbuseDeleteDocumentCollectionsAPI()
{
$data = $this->createCollection();
$data = $this->createCollectionOrTable();
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
$max = 60;
@@ -124,6 +124,104 @@ class AbuseTest extends Scope
}
}
public function testAbuseCreateDocumentTablesAPI()
{
$data = $this->createCollectionOrTable(false);
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
$max = 120;
for ($i = 0; $i <= $max + 1; $i++) {
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/tables/' . $collectionId . '/rows', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'rowId' => ID::unique(),
'data' => [
'title' => 'The Hulk ' . $i,
],
]);
if ($i < $max) {
$this->assertEquals(201, $response['headers']['status-code']);
} else {
$this->assertEquals(429, $response['headers']['status-code']);
}
}
}
public function testAbuseUpdateDocumentTablesAPI()
{
$data = $this->createCollectionOrTable(false);
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
$max = 120;
$row = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/tables/' . $collectionId . '/rows', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'rowId' => ID::unique(),
'data' => [
'title' => 'The Hulk',
],
]);
$rowId = $row['body']['$id'];
for ($i = 0; $i <= $max + 1; $i++) {
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/tables/' . $collectionId . '/rows/' . $rowId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'data' => [
'title' => 'The Hulk ' . $i,
],
]);
if ($i < $max) {
$this->assertEquals(200, $response['headers']['status-code']);
} else {
$this->assertEquals(429, $response['headers']['status-code']);
}
}
}
public function testAbuseDeleteDocumentTablesAPI()
{
$data = $this->createCollectionOrTable(false);
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
$max = 60;
for ($i = 0; $i <= $max + 1; $i++) {
$document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/tables/' . $collectionId . '/rows', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'rowId' => ID::unique(),
'data' => [
'title' => 'The Hulk',
],
]);
$documentId = $document['body']['$id'];
$response = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/tables/' . $collectionId . '/rows/' . $documentId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
if ($i < $max) {
$this->assertEquals(204, $response['headers']['status-code']);
} else {
$this->assertEquals(429, $response['headers']['status-code']);
}
}
}
public function testAbuseCreateFile()
{
$data = $this->createBucket();
@@ -211,7 +309,7 @@ class AbuseTest extends Scope
}
}
private function createCollection(): array
private function createCollectionOrTable(bool $isCollection = true): array
{
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
@@ -227,12 +325,16 @@ class AbuseTest extends Scope
$databaseId = $database['body']['$id'];
$movies = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [
$endpoint = $isCollection ? 'collections' : 'tables';
$idParam = $isCollection ? 'collectionId' : 'tableId';
$attributePath = $isCollection ? 'attributes' : 'columns';
$movies = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . "/$endpoint", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'collectionId' => ID::unique(),
$idParam => ID::unique(),
'name' => 'Movies',
'permissions' => [
Permission::read(Role::any()),
@@ -244,7 +346,7 @@ class AbuseTest extends Scope
$collectionId = $movies['body']['$id'];
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . "/$endpoint/" . $collectionId . "/$attributePath/string", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
+1 -1
View File
@@ -60,7 +60,7 @@ class HTTPTest extends Scope
'origin' => 'http://localhost',
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(200, $response['headers']['status-code'], "Simple GET /robots.txt HTTP request failed: " . \json_encode($response));
$this->assertStringContainsString('# robotstxt.org/', $response['body']);
}
+258 -8
View File
@@ -418,7 +418,7 @@ class UsageTest extends Scope
}
/** @depends testStorageStats */
public function testPrepareDatabaseStats(array $data): array
public function testPrepareDatabaseStatsCollectionsAPI(array $data): array
{
$requestsTotal = $data['requestsTotal'];
@@ -583,9 +583,8 @@ class UsageTest extends Scope
]);
}
/** @depends testPrepareDatabaseStats */
public function testDatabaseStats(array $data): array
/** @depends testPrepareDatabaseStatsCollectionsAPI */
public function testDatabaseStatsCollectionsAPI(array $data): array
{
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
@@ -658,7 +657,258 @@ class UsageTest extends Scope
return $data;
}
/** @depends testDatabaseStats */
/** @depends testDatabaseStatsCollectionsAPI */
public function testPrepareDatabaseStatsTablesAPI(array $data): array
{
$rowsTotal = 0;
$tablesTotal = 0;
$databasesTotal = $data['databasesTotal'];
$documentsTotal = $data['documentsTotal'];
$collectionsTotal = $data['collectionsTotal'];
$requestsTotal = $data['requestsTotal'];
for ($i = 0; $i < self::CREATE; $i++) {
$name = uniqid() . ' database';
$response = $this->client->call(
Client::METHOD_POST,
'/databases',
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()),
[
'databaseId' => 'unique()',
'name' => $name,
]
);
$this->assertEquals($name, $response['body']['name']);
$this->assertNotEmpty($response['body']['$id']);
$requestsTotal += 1;
$databasesTotal += 1;
$databaseId = $response['body']['$id'];
if ($i < (self::CREATE / 2)) {
$response = $this->client->call(
Client::METHOD_DELETE,
'/databases/' . $databaseId,
array_merge([
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()),
);
$this->assertEmpty($response['body']);
$databasesTotal -= 1;
$requestsTotal += 1;
}
}
for ($i = 0; $i < self::CREATE; $i++) {
$name = uniqid() . ' table';
$response = $this->client->call(
Client::METHOD_POST,
'/databases/' . $databaseId . '/grids/tables',
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()),
[
'tableId' => 'unique()',
'name' => $name,
'documentSecurity' => false,
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]
);
$this->assertEquals($name, $response['body']['name']);
$this->assertNotEmpty($response['body']['$id']);
$requestsTotal += 1;
$tablesTotal += 1;
$tableId = $response['body']['$id'];
if ($i < (self::CREATE / 2)) {
$response = $this->client->call(
Client::METHOD_DELETE,
'/databases/' . $databaseId . '/grids/tables/' . $tableId,
array_merge([
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()),
);
$this->assertEmpty($response['body']);
$tablesTotal -= 1;
$requestsTotal += 1;
}
}
$response = $this->client->call(
Client::METHOD_POST,
'/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns' . '/string',
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()),
[
'key' => 'name',
'size' => 255,
'required' => true,
]
);
$this->assertEquals('name', $response['body']['key']);
sleep(self::WAIT);
$requestsTotal += 1;
for ($i = 0; $i < self::CREATE; $i++) {
$name = uniqid() . ' table';
$response = $this->client->call(
Client::METHOD_POST,
'/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows',
array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()),
[
'rowId' => 'unique()',
'data' => ['name' => $name]
]
);
$this->assertEquals($name, $response['body']['name']);
$this->assertNotEmpty($response['body']['$id']);
$requestsTotal += 1;
$rowsTotal += 1;
$rowId = $response['body']['$id'];
if ($i < (self::CREATE / 2)) {
$response = $this->client->call(
Client::METHOD_DELETE,
'/databases/' . $databaseId . '/grids/tables/' . $tableId . '/rows/' . $rowId,
array_merge([
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()),
);
$this->assertEmpty($response['body']);
$rowsTotal -= 1;
$requestsTotal += 1;
}
}
return array_merge($data, [
'databaseId' => $databaseId,
'tableId' => $tableId,
'requestsTotal' => $requestsTotal,
'databasesTotal' => $databasesTotal,
'tablesTotal' => $tablesTotal,
'rowsTotal' => $rowsTotal,
// For clarity
'absoluteRowsTotal' => $rowsTotal + $data['documentsTotal'],
'absoluteTablesTotal' => $tablesTotal + $data['collectionsTotal'],
]);
}
/** @depends testPrepareDatabaseStatsTablesAPI */
#[Retry(count: 1)]
public function testDatabaseStatsTablesAPI(array $data): array
{
$tableId = $data['tableId'];
$databaseId = $data['databaseId'];
$requestsTotal = $data['requestsTotal'];
$absoluteRowsTotal = $data['absoluteRowsTotal'];
$absoluteTablesTotal = $data['absoluteTablesTotal'];
$rowsTotal = $data['rowsTotal'];
$tablesTotal = $data['tablesTotal'];
$databasesTotal = $data['databasesTotal'];
sleep(self::WAIT);
$response = $this->client->call(
Client::METHOD_GET,
'/project/usage',
$this->getConsoleHeaders(),
[
'period' => '1d',
'startDate' => self::getToday(),
'endDate' => self::getTomorrow(),
]
);
$this->assertGreaterThanOrEqual(31, count($response['body']));
$this->assertCount(1, $response['body']['requests']);
$this->assertCount(1, $response['body']['network']);
$this->assertEquals($requestsTotal, $response['body']['requests'][array_key_last($response['body']['requests'])]['value']);
$this->validateDates($response['body']['requests']);
$this->assertEquals($databasesTotal, $response['body']['databasesTotal']);
// project level includes all i.e. documents + rows total.
$this->assertEquals($absoluteRowsTotal, $response['body']['rowsTotal']);
$response = $this->client->call(
Client::METHOD_GET,
'/databases/usage?range=30d',
$this->getConsoleHeaders()
);
$this->assertEquals($databasesTotal, $response['body']['databases'][array_key_last($response['body']['databases'])]['value']);
$this->validateDates($response['body']['databases']);
// database level includes all i.e. collections + tables total.
$this->assertEquals($absoluteTablesTotal, $response['body']['tables'][array_key_last($response['body']['tables'])]['value']); // database level
$this->validateDates($response['body']['tables']);
// database level includes all i.e. documents + rows total.
$this->assertEquals($absoluteRowsTotal, $response['body']['rows'][array_key_last($response['body']['rows'])]['value']);
$this->validateDates($response['body']['rows']);
$response = $this->client->call(
Client::METHOD_GET,
'/databases/' . $databaseId . '/usage?range=30d',
$this->getConsoleHeaders()
);
$this->assertEquals($tablesTotal, $response['body']['tables'][array_key_last($response['body']['tables'])]['value']);
$this->validateDates($response['body']['tables']);
$this->assertEquals($rowsTotal, $response['body']['rows'][array_key_last($response['body']['rows'])]['value']);
$this->validateDates($response['body']['rows']);
$response = $this->client->call(
Client::METHOD_GET,
'/databases/' . $databaseId . '/grids/tables/' . $tableId . '/usage?range=30d',
$this->getConsoleHeaders()
);
$this->assertEquals($rowsTotal, $response['body']['rows'][array_key_last($response['body']['rows'])]['value']);
$this->validateDates($response['body']['rows']);
return $data;
}
/** @depends testDatabaseStatsTablesAPI */
public function testPrepareFunctionsStats(array $data): array
{
$executionTime = 0;
@@ -675,7 +925,8 @@ class UsageTest extends Scope
[
'functionId' => 'unique()',
'name' => 'Test',
'runtime' => 'php-8.0',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'vars' => [
'funcKey1' => 'funcValue1',
'funcKey2' => 'funcValue2',
@@ -697,8 +948,7 @@ class UsageTest extends Scope
$this->assertNotEmpty($response['body']['$id']);
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true,
]);
$this->assertNotEmpty($deploymentId);
+18 -1
View File
@@ -70,8 +70,12 @@ trait ProjectCustom
'databases.write',
'collections.read',
'collections.write',
'tables.read',
'tables.write',
'documents.read',
'documents.write',
'rows.read',
'rows.write',
'files.read',
'files.write',
'buckets.read',
@@ -140,7 +144,7 @@ trait ProjectCustom
'teams.*',
'users.*'
],
'url' => 'http://request-catcher:5000/webhook',
'url' => 'http://request-catcher-webhook:5000/',
'security' => false,
]);
@@ -200,4 +204,17 @@ trait ProjectCustom
return $key['body']['secret'];
}
public function updateProjectinvalidateSessionsProperty(bool $value)
{
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . self::$project['$id'] . '/auth/session-invalidation', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
'x-appwrite-project' => 'console',
]), [
'enabled' => $value,
]);
return $response['headers']['status-code'];
}
}
+51 -6
View File
@@ -13,6 +13,9 @@ abstract class Scope extends TestCase
use Retryable;
use Async;
public const REQUEST_TYPE_WEBHOOK = 'webhook';
public const REQUEST_TYPE_SMS = 'sms';
protected ?Client $client = null;
protected string $endpoint = 'http://localhost/v1';
@@ -37,18 +40,55 @@ abstract class Scope extends TestCase
if ($limit === 1) {
return end($emails);
} else {
$lastEmails = array_slice($emails, -1 * $limit);
return $lastEmails;
return array_slice($emails, -1 * $limit);
}
}
return [];
}
protected function assertLastRequest(callable $probe, $timeoutMs = 20_000, $waitMs = 500): array
protected function extractQueryParamsFromEmailLink(string $html): array
{
$this->assertEventually(function () use (&$request, $probe) {
$request = json_decode(file_get_contents('http://request-catcher:5000/__last_request__'), true);
foreach (['/join-us?', '/verification?', '/recovery?'] as $prefix) {
$linkStart = strpos($html, $prefix);
if ($linkStart !== false) {
$hrefStart = strrpos(substr($html, 0, $linkStart), 'href="');
if ($hrefStart === false) {
continue;
}
$hrefStart += 6;
$hrefEnd = strpos($html, '"', $hrefStart);
if ($hrefEnd === false || $hrefStart >= $hrefEnd) {
continue;
}
$link = substr($html, $hrefStart, $hrefEnd - $hrefStart);
$link = strtok($link, '#'); // Remove `#title`
$queryStart = strpos($link, '?');
if ($queryStart === false) {
continue;
}
$queryString = substr($link, $queryStart + 1);
parse_str(html_entity_decode($queryString), $queryParams);
return $queryParams;
}
}
return [];
}
protected function assertLastRequest(callable $probe, string $type, $timeoutMs = 20_000, $waitMs = 500): array
{
$hostname = match ($type) {
'webhook' => 'request-catcher-webhook',
'sms' => 'request-catcher-sms',
default => throw new \Exception('Invalid request catcher type.'),
};
$this->assertEventually(function () use (&$request, $probe, $hostname) {
$request = json_decode(file_get_contents('http://' . $hostname . ':5000/__last_request__'), true);
$request['data'] = json_decode($request['data'], true);
call_user_func($probe, $request);
@@ -57,11 +97,16 @@ abstract class Scope extends TestCase
return $request;
}
/**
* @deprecated Use assertLastRequest instead. Used only historically in webhook tests
*/
protected function getLastRequest(): array
{
$hostname = 'request-catcher-webhook';
sleep(2);
$request = json_decode(file_get_contents('http://request-catcher:5000/__last_request__'), true);
$request = json_decode(file_get_contents('http://' . $hostname . ':5000/__last_request__'), true);
$request['data'] = json_decode($request['data'], true);
return $request;
@@ -170,6 +170,7 @@ trait AccountBase
$userId = $response['body']['userId'];
$lastEmail = $this->getLastEmail();
$this->assertEquals('otpuser@appwrite.io', $lastEmail['to'][0]['address']);
$this->assertEquals('OTP for ' . $this->getProject()['name'] . ' Login', $lastEmail['subject']);
@@ -178,6 +179,7 @@ trait AccountBase
$code = ($matches[0] ?? [])[0] ?? '';
$this->assertNotEmpty($code);
$this->assertStringContainsStringIgnoringCase('Use OTP ' . $code . ' to sign in to '. $this->getProject()['name'] . '. Expires in 15 minutes.', $lastEmail['text']);
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/token', array_merge([
'origin' => 'http://localhost',
@@ -480,6 +480,29 @@ class AccountCustomClientTest extends Scope
$password = $data['password'] ?? '';
$session = $data['session'] ?? '';
for ($i = 0; $i < 5; $i++) {
$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'],
]), [
'email' => $email,
'password' => $password,
]);
$this->assertEquals(201, $response['headers']['status-code']);
sleep(1);
}
$response = $this->client->call(Client::METHOD_GET, '/account/sessions', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$allSessions = array_map(fn ($sessionDetails) => $sessionDetails['$id'], $response['body']['sessions']);
/**
* Test for SUCCESS
*/
@@ -500,17 +523,140 @@ class AccountCustomClientTest extends Scope
$this->assertTrue((new DatetimeValidator())->isValid($response['body']['registration']));
$this->assertEquals($response['body']['email'], $email);
$currentSessionId = $data['sessionId'] ?? '';
$response = $this->client->call(Client::METHOD_GET, '/account/sessions', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, $response['body']['total']);
// checking the current session or not
$this->assertEquals($currentSessionId, $response['body']['sessions'][0]['$id']);
$this->assertTrue($response['body']['sessions'][0]['current']);
// checking for all non active sessions are cleared
foreach ($allSessions as $sessionId) {
if ($currentSessionId === $sessionId) {
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/current', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$this->assertEquals(200, $response['headers']['status-code']);
} else {
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/'.$sessionId, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$this->assertEquals(404, $response['headers']['status-code']);
}
}
$newPassword = 'new-password';
// updating the invalidateSession to false to check sessions are not invalidated
$this->updateProjectinvalidateSessionsProperty(false);
for ($i = 0; $i < 5; $i++) {
$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'],
]), [
'email' => $email,
'password' => $newPassword,
]);
$this->assertEquals(201, $response['headers']['status-code']);
sleep(1);
}
$response = $this->client->call(Client::METHOD_GET, '/account/sessions', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$allSessions = array_map(fn ($sessionDetails) => $sessionDetails['$id'], $response['body']['sessions']);
$response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'password' => $newPassword,
'oldPassword' => $newPassword,
]);
$this->assertEquals(200, $response['headers']['status-code']);
foreach ($allSessions as $sessionId) {
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/'.$sessionId, headers: array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$this->assertEquals(200, $response['headers']['status-code']);
}
// setting invalidateSession to true to check the sessions are cleared or not
$this->updateProjectinvalidateSessionsProperty(true);
$response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]), [
'password' => $newPassword,
'oldPassword' => $newPassword,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/account/sessions', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$allSessions = array_map(fn ($sessionDetails) => $sessionDetails['$id'], $response['body']['sessions']);
foreach ($allSessions as $sessionId) {
if ($currentSessionId !== $sessionId) {
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/'.$sessionId, array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
$this->assertEquals(404, $response['headers']['status-code']);
}
}
$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'],
]), [
'email' => $email,
'password' => 'new-password',
'password' => $newPassword,
]);
$this->assertEquals(201, $response['headers']['status-code']);
/**
* Test for FAILURE
*/
@@ -779,18 +925,20 @@ class AccountCustomClientTest extends Scope
$this->assertEquals($email, $lastEmail['to'][0]['address']);
$this->assertEquals($name, $lastEmail['to'][0]['name']);
$this->assertEquals('Account Verification', $lastEmail['subject']);
$this->assertStringContainsStringIgnoringCase('Verify your email to activate your ' . $this->getProject()['name'] . ' account.', $lastEmail['text']);
$verification = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$expireTime = strpos($lastEmail['text'], 'expire=' . urlencode(DateTime::format(new \DateTime($response['body']['expire']))), 0);
$this->assertNotFalse($expireTime);
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
$verification = $tokens['secret'];
$expectedExpire = DateTime::formatTz($response['body']['expire']);
$this->assertEquals($expectedExpire, $tokens['expire']);
$secretTest = strpos($lastEmail['text'], 'secret=' . $response['body']['secret'], 0);
// Secret check
$this->assertArrayHasKey('secret', $tokens);
$this->assertNotEmpty($tokens['secret']);
$this->assertNotFalse($secretTest);
$userIDTest = strpos($lastEmail['text'], 'userId=' . $response['body']['userId'], 0);
$this->assertNotFalse($userIDTest);
// User ID check
$this->assertArrayHasKey('userId', $tokens);
$this->assertNotEmpty($tokens['userId']);
/**
* Test for FAILURE
@@ -1081,20 +1229,28 @@ class AccountCustomClientTest extends Scope
$this->assertEquals($email, $lastEmail['to'][0]['address']);
$this->assertEquals($name, $lastEmail['to'][0]['name']);
$this->assertEquals('Password Reset', $lastEmail['subject']);
$this->assertStringContainsStringIgnoringCase('Reset your ' . $this->getProject()['name'] . ' password using the link.', $lastEmail['text']);
$recovery = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$expireTime = strpos($lastEmail['text'], 'expire=' . urlencode(DateTime::format(new \DateTime($response['body']['expire']))), 0);
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
$this->assertNotFalse($expireTime);
// Secret check
$this->assertArrayHasKey('secret', $tokens);
$this->assertNotEmpty($tokens['secret']);
$this->assertNotFalse($response['body']['secret']);
$secretTest = strpos($lastEmail['text'], 'secret=' . $response['body']['secret'], 0);
// User ID check
$this->assertArrayHasKey('userId', $tokens);
$this->assertNotEmpty($tokens['userId']);
$this->assertNotFalse($response['body']['userId']);
$this->assertNotFalse($secretTest);
$userIDTest = strpos($lastEmail['text'], 'userId=' . $response['body']['userId'], 0);
$this->assertNotFalse($userIDTest);
// Expire check
$this->assertArrayHasKey('expire', $tokens);
$this->assertNotEmpty($tokens['expire']);
$this->assertEquals(
DateTime::formatTz($response['body']['expire']),
$tokens['expire']
);
/**
* Test for FAILURE
@@ -1132,7 +1288,7 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(404, $response['headers']['status-code']);
$data['recovery'] = $recovery;
$data['recovery'] = $tokens['secret'];
return $data;
}
@@ -1279,6 +1435,7 @@ class AccountCustomClientTest extends Scope
$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'];
@@ -1287,7 +1444,7 @@ class AccountCustomClientTest extends Scope
$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
// Find 6 concurrent digits in email text - OTP
preg_match_all("/\b\d{6}\b/", $lastEmail['text'], $matches);
$code = ($matches[0] ?? [])[0] ?? '';
@@ -2058,14 +2215,13 @@ class AccountCustomClientTest extends Scope
$userId = $response['body']['userId'];
$smsRequest = $this->assertLastRequest(function (array $request) use ($number) {
$this->assertEquals('http://request-catcher:5000/mock-sms', $request['url']);
$this->assertEquals('Appwrite Mock Message Sender', $request['headers']['User-Agent']);
$this->assertEquals('username', $request['headers']['X-Username']);
$this->assertEquals('password', $request['headers']['X-Key']);
$this->assertEquals('POST', $request['method']);
$this->assertEquals('+123456789', $request['data']['from']);
$this->assertEquals($number, $request['data']['to']);
});
}, Scope::REQUEST_TYPE_SMS);
$data['token'] = $smsRequest['data']['message'];
$data['id'] = $userId;
@@ -2409,13 +2565,21 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['$createdAt']);
$this->assertEmpty($response['body']['secret']);
$this->assertTrue((new DatetimeValidator())->isValid($response['body']['expire']));
$smsRequest = $this->assertLastRequest(function ($request) {
$tokenCreatedAt = $response['body']['$createdAt'];
$smsRequest = $this->assertLastRequest(function ($request) use ($tokenCreatedAt) {
$this->assertArrayHasKey('data', $request);
$this->assertArrayHasKey('message', $request['data']);
});
$this->assertArrayHasKey('time', $request);
$this->assertArrayHasKey('message', $request['data'], "Last request missing message: " . \json_encode($request));
// Ensure we are not using token from last sms login
$tokenRecievedAt = $request['time'];
$this->assertGreaterThan($tokenCreatedAt, $tokenRecievedAt);
}, Scope::REQUEST_TYPE_SMS);
/**
* Test for FAILURE
@@ -2531,6 +2695,7 @@ class AccountCustomClientTest extends Scope
$lastEmail = $this->getLastEmail();
$this->assertEquals($email, $lastEmail['to'][0]['address']);
$this->assertEquals($this->getProject()['name'] . ' Login', $lastEmail['subject']);
$this->assertStringContainsStringIgnoringCase('Sign in to '. $this->getProject()['name'] . ' with your secure link. Expires in 1 hour.', $lastEmail['text']);
$this->assertStringNotContainsStringIgnoringCase('security phrase', $lastEmail['text']);
$token = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 64);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,336 @@
<?php
namespace Tests\E2E\Services\Databases\Grids;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
class DatabasesConsoleClientTest extends Scope
{
use ProjectCustom;
use SideConsole;
public function testCreateTable(): array
{
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'databaseId' => ID::unique(),
'name' => 'invalidDocumentDatabase',
]);
$this->assertEquals(201, $database['headers']['status-code']);
$this->assertEquals('invalidDocumentDatabase', $database['body']['name']);
$this->assertTrue($database['body']['enabled']);
$databaseId = $database['body']['$id'];
/**
* Test for SUCCESS
*/
$movies = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'tableId' => ID::unique(),
'name' => 'Movies',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'rowSecurity' => true,
]);
$this->assertEquals(201, $movies['headers']['status-code']);
$this->assertEquals('Movies', $movies['body']['name']);
/**
* Test when database is disabled but can still create tables
*/
$database = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'invalidDocumentDatabase Updated',
'enabled' => false,
]);
$this->assertFalse($database['body']['enabled']);
$tvShows = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'tableId' => ID::unique(),
'name' => 'TvShows',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'rowSecurity' => true,
]);
/**
* Test when table is disabled but can still modify tables
*/
$database = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/grids/tables/' . $movies['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Movies',
'enabled' => false,
]);
$this->assertEquals(201, $tvShows['headers']['status-code']);
$this->assertEquals('TvShows', $tvShows['body']['name']);
return ['moviesId' => $movies['body']['$id'], 'databaseId' => $databaseId, 'tvShowsId' => $tvShows['body']['$id']];
}
/**
* @depends testCreateTable
* @param array $data
* @throws \Exception
*/
public function testListTable(array $data)
{
/**
* Test when database is disabled but can still call list tables
*/
$databaseId = $data['databaseId'];
$tables = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()));
$this->assertEquals(200, $tables['headers']['status-code']);
$this->assertEquals(2, $tables['body']['total']);
}
/**
* @depends testCreateTable
* @param array $data
* @throws \Exception
*/
public function testGetTable(array $data)
{
$databaseId = $data['databaseId'];
$moviesCollectionId = $data['moviesId'];
/**
* Test when database and table are disabled but can still call get table
*/
$table = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $moviesCollectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $table['headers']['status-code']);
$this->assertEquals('Movies', $table['body']['name']);
$this->assertEquals($moviesCollectionId, $table['body']['$id']);
$this->assertFalse($table['body']['enabled']);
}
/**
* @depends testCreateTable
* @param array $data
* @throws \Exception
* @throws \Exception
*/
public function testUpdateTable(array $data)
{
$databaseId = $data['databaseId'];
$moviesCollectionId = $data['moviesId'];
/**
* Test When database and table are disabled but can still call update table
*/
$table = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/grids/tables/' . $moviesCollectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Movies Updated',
'enabled' => false
]);
$this->assertEquals(200, $table['headers']['status-code']);
$this->assertEquals('Movies Updated', $table['body']['name']);
$this->assertEquals($moviesCollectionId, $table['body']['$id']);
$this->assertFalse($table['body']['enabled']);
}
/**
* @depends testCreateTable
* @param array $data
* @throws \Exception
* @throws \Exception
*/
public function testDeleteTable(array $data)
{
$databaseId = $data['databaseId'];
$tvShowsId = $data['tvShowsId'];
/**
* Test when database and table are disabled but can still call delete table
*/
$response = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/tables/' . $tvShowsId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $response['headers']['status-code']);
$this->assertEquals("", $response['body']);
}
/**
* @depends testCreateTable
*/
public function testGetDatabaseUsage(array $data)
{
$databaseId = $data['databaseId'];
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '32h'
]);
$this->assertEquals(400, $response['headers']['status-code']);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(15, $response['body']);
$this->assertEquals('24h', $response['body']['range']);
$this->assertIsNumeric($response['body']['rowsTotal']);
$this->assertIsNumeric($response['body']['tablesTotal']);
$this->assertIsArray($response['body']['tables']);
$this->assertIsArray($response['body']['rows']);
}
/**
* @depends testCreateTable
*/
public function testGetTableUsage(array $data)
{
$databaseId = $data['databaseId'];
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $data['moviesId'] . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '32h'
]);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/randomCollectionId/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals(404, $response['headers']['status-code']);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $data['moviesId'] . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(3, count($response['body']));
$this->assertEquals('24h', $response['body']['range']);
$this->assertIsNumeric($response['body']['rowsTotal']);
$this->assertIsArray($response['body']['rows']);
}
/**
* @depends testCreateTable
* @throws \Utopia\Database\Exception\Query
*/
public function testGetTableLogs(array $data)
{
$databaseId = $data['databaseId'];
/**
* Test for SUCCESS
*/
$logs = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::limit(1)->toString()]
]);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::offset(1)->toString()]
]);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $data['moviesId'] . '/logs', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [Query::offset(1)->toString(), Query::limit(1)->toString()]
]);
$this->assertEquals(200, $logs['headers']['status-code']);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
}
}
@@ -0,0 +1,893 @@
<?php
namespace Tests\E2E\Services\Databases\Grids;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
class DatabasesCustomClientTest extends Scope
{
use DatabasesBase;
use ProjectCustom;
use SideClient;
public function testAllowedPermissions(): void
{
/**
* Test for SUCCESS
*/
$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'
]);
$databaseId = $database['body']['$id'];
// Collection aliases write to create, update, delete
$movies = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'Movies',
'rowSecurity' => true,
'permissions' => [
Permission::write(Role::user($this->getUser()['$id'])),
],
]);
$moviesId = $movies['body']['$id'];
$this->assertContains(Permission::create(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
$this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
$this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $movies['body']['$permissions']);
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $moviesId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'title',
'size' => 256,
'required' => true,
]);
sleep(1);
$this->assertEquals(202, $response['headers']['status-code']);
// Document aliases write to update, delete
$row1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $moviesId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'rowId' => ID::unique(),
'data' => [
'title' => 'Captain America',
],
'permissions' => [
Permission::write(Role::user($this->getUser()['$id'])),
]
]);
$this->assertNotContains(Permission::create(Role::user($this->getUser()['$id'])), $row1['body']['$permissions']);
$this->assertContains(Permission::update(Role::user($this->getUser()['$id'])), $row1['body']['$permissions']);
$this->assertContains(Permission::delete(Role::user($this->getUser()['$id'])), $row1['body']['$permissions']);
/**
* Test for FAILURE
*/
// Document does not allow create permission
$row2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $moviesId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'rowId' => ID::unique(),
'data' => [
'title' => 'Captain America',
],
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
]
]);
$this->assertEquals(400, $row2['headers']['status-code']);
}
public function testUpdateWithoutPermission(): array
{
// If document has been created by server and client tried to update it without adjusting permissions, permission validation should be skipped
// As a part of preparation, we get ID of currently logged-in user
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$userId = $response['body']['$id'];
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::custom('permissionCheckDatabase'),
'name' => 'Test Database',
]);
$this->assertEquals(201, $database['headers']['status-code']);
$this->assertEquals('Test Database', $database['body']['name']);
$databaseId = $database['body']['$id'];
// Create collection
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::custom('permissionCheck'),
'name' => 'permissionCheck',
'permissions' => [],
'rowSecurity' => true,
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Add attribute to collection
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/permissionCheck/columns/string', array_merge([
'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, $response['headers']['status-code']);
// Wait for database worker to finish creating attributes
sleep(2);
// Creating document by server, give read permission to our user + some other user
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/permissionCheck/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => ID::custom('permissionCheckDocument'),
'data' => [
'name' => 'AppwriteBeginner',
],
'permissions' => [
Permission::read(Role::user(ID::custom('user2'))),
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
]);
$this->assertEquals(201, $response['headers']['status-code']);
// Update document
// This is the point of this test. We should be allowed to do this action, and it should not fail on permission check
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/permissionCheck/rows/permissionCheckDocument', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'name' => 'AppwriteExpert',
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
// Get name of the document, should be the new one
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/permissionCheck/rows/permissionCheckDocument', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals("AppwriteExpert", $response['body']['name']);
// Cleanup to prevent collision with other tests
// Delete collection
$response = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/tables/permissionCheck', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(204, $response['headers']['status-code']);
// Wait for database worker to finish deleting collection
sleep(2);
// Make sure collection has been deleted
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/permissionCheck', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(404, $response['headers']['status-code']);
return [];
}
public function testUpdateTwoWayRelationship(): void
{
$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'
]);
$databaseId = $database['body']['$id'];
// Creating collection 1
$table1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'level1',
'rowSecurity' => false,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
]);
// Creating collection 2
$table2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'level2',
'rowSecurity' => false,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
]);
\sleep(2);
// Creating two way relationship between collection 1 and collection 2 from collection 1
$relation = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/columns/relationship', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'relatedTableId' => $table2['body']['$id'],
'type' => 'oneToMany',
'twoWay' => true,
'onDelete' => 'cascade',
'key' => $table2['body']['$id'],
'twoWayKey' => $table1['body']['$id']
]);
\sleep(3);
// Update relation from collection 2 to on delete restrict
$this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $table2['body']['$id'] . '/columns/' . $table1['body']['$id'] . '/relationship', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'onDelete' => 'restrict',
]);
// Fetching attributes after updating relation to compare
$table1Attributes = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$table1RelationAttribute = $table1Attributes['body']['columns'][0];
$this->assertEquals($relation['body']['side'], $table1RelationAttribute['side']);
$this->assertEquals($relation['body']['twoWayKey'], $table1RelationAttribute['twoWayKey']);
$this->assertEquals($relation['body']['relatedTable'], $table1RelationAttribute['relatedTable']);
$this->assertEquals('restrict', $table1RelationAttribute['onDelete']);
}
public function testRelationshipSameTwoWayKey(): void
{
$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' => 'Same two way key'
]);
$databaseId = $database['body']['$id'];
$table1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'c1',
'rowSecurity' => false,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
]);
$table2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'c2',
'rowSecurity' => false,
'permissions' => [
Permission::create(Role::user($this->getUser()['$id'])),
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id'])),
]
]);
\sleep(2);
$relation = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/columns/relationship', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'relatedTableId' => $table2['body']['$id'],
'type' => Database::RELATION_ONE_TO_ONE,
'twoWay' => false,
'onDelete' => 'cascade',
'key' => 'attr1',
'twoWayKey' => 'same_key'
]);
\sleep(2);
$this->assertEquals(202, $relation['headers']['status-code']);
$this->assertEquals('same_key', $relation['body']['twoWayKey']);
$relation = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/columns/relationship', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'relatedTableId' => $table2['body']['$id'],
'type' => Database::RELATION_ONE_TO_MANY,
'twoWay' => false,
'onDelete' => 'cascade',
'key' => 'attr2',
'twoWayKey' => 'same_key'
]);
\sleep(2);
$this->assertEquals(409, $relation['body']['code']);
$this->assertEquals('Attribute with the requested key already exists. Attribute keys must be unique, try again with a different key.', $relation['body']['message']);
// twoWayKey is null TwoWayKey is default
$relation = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/columns/relationship', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'relatedTableId' => $table2['body']['$id'],
'type' => Database::RELATION_ONE_TO_MANY,
'twoWay' => false,
'onDelete' => 'cascade',
'key' => 'attr3',
]);
\sleep(2);
$this->assertEquals(202, $relation['headers']['status-code']);
$this->assertArrayHasKey('twoWayKey', $relation['body']);
// twoWayKey is null, TwoWayKey is default, second POST
$relation = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/columns/relationship', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'relatedTableId' => $table2['body']['$id'],
'type' => Database::RELATION_ONE_TO_MANY,
'twoWay' => false,
'onDelete' => 'cascade',
'key' => 'attr4',
]);
\sleep(2);
$this->assertEquals('Attribute with the requested key already exists. Attribute keys must be unique, try again with a different key.', $relation['body']['message']);
$this->assertEquals(409, $relation['body']['code']);
// RelationshipManyToMany
$relation = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/columns/relationship', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'relatedTableId' => $table2['body']['$id'],
'type' => Database::RELATION_MANY_TO_MANY,
'twoWay' => true,
'onDelete' => 'setNull',
'key' => 'songs',
'twoWayKey' => 'playlist',
]);
\sleep(2);
$this->assertEquals(202, $relation['headers']['status-code']);
$this->assertArrayHasKey('twoWayKey', $relation['body']);
// Second RelationshipManyToMany on Same collections
$relation = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/columns/relationship', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'relatedTableId' => $table2['body']['$id'],
'type' => Database::RELATION_MANY_TO_MANY,
'twoWay' => true,
'onDelete' => 'setNull',
'key' => 'songs2',
'twoWayKey' => 'playlist2',
]);
\sleep(2);
$this->assertEquals(409, $relation['body']['code']);
$this->assertEquals('Creating more than one "manyToMany" relationship on the same table is currently not permitted.', $relation['body']['message']);
}
public function testUpdateWithoutRelationPermission(): void
{
$userId = $this->getUser()['$id'];
$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' => ID::unique(),
]);
$databaseId = $database['body']['$id'];
// Creating collection 1
$table1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::custom('collection1'),
'name' => ID::custom('collection1'),
'rowSecurity' => false,
'permissions' => [
Permission::create(Role::user($userId)),
Permission::read(Role::user($userId)),
Permission::delete(Role::user($userId)),
]
]);
// Creating collection 2
$table2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::custom('collection2'),
'name' => ID::custom('collection2'),
'rowSecurity' => false,
'permissions' => [
Permission::read(Role::user($userId)),
]
]);
$table3 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::custom('collection3'),
'name' => ID::custom('collection3'),
'rowSecurity' => false,
'permissions' => [
Permission::create(Role::user($userId)),
Permission::read(Role::user($userId)),
Permission::delete(Role::user($userId)),
]
]);
$table4 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::custom('collection4'),
'name' => ID::custom('collection4'),
'rowSecurity' => false,
'permissions' => [
Permission::read(Role::user($userId)),
]
]);
$table5 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::custom('collection5'),
'name' => ID::custom('collection5'),
'rowSecurity' => false,
'permissions' => [
Permission::create(Role::user($userId)),
Permission::read(Role::user($userId)),
Permission::delete(Role::user($userId)),
]
]);
// Creating one to one relationship from collection 1 to colletion 2
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/columns/relationship', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'relatedTableId' => $table2['body']['$id'],
'type' => 'oneToOne',
'twoWay' => false,
'onDelete' => 'setNull',
'key' => $table2['body']['$id']
]);
// Creating one to one relationship from collection 2 to colletion 3
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table2['body']['$id'] . '/columns/relationship', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'relatedTableId' => $table3['body']['$id'],
'type' => 'oneToOne',
'twoWay' => false,
'onDelete' => 'setNull',
'key' => $table3['body']['$id']
]);
// Creating one to one relationship from collection 3 to colletion 4
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table3['body']['$id'] . '/columns/relationship', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'relatedTableId' => $table4['body']['$id'],
'type' => 'oneToOne',
'twoWay' => false,
'onDelete' => 'setNull',
'key' => $table4['body']['$id']
]);
// Creating one to one relationship from collection 4 to colletion 5
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table4['body']['$id'] . '/columns/relationship', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'relatedTableId' => $table5['body']['$id'],
'type' => 'oneToOne',
'twoWay' => false,
'onDelete' => 'setNull',
'key' => $table5['body']['$id']
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => "Title",
'size' => 100,
'required' => false,
'array' => false,
'default' => null,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table2['body']['$id'] . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => "Rating",
'size' => 100,
'required' => false,
'array' => false,
'default' => null,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table3['body']['$id'] . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => "Rating",
'size' => 100,
'required' => false,
'array' => false,
'default' => null,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table4['body']['$id'] . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => "Rating",
'size' => 100,
'required' => false,
'array' => false,
'default' => null,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table5['body']['$id'] . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => "Rating",
'size' => 100,
'required' => false,
'array' => false,
'default' => null,
]);
\sleep(2);
// Creating parent document with a child reference to test the permissions
$parentDocument = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => ID::custom($table1['body']['$id']),
'data' => [
'Title' => 'Captain America',
$table2['body']['$id'] => [
'$id' => ID::custom($table2['body']['$id']),
'Rating' => '10',
$table3['body']['$id'] => [
'$id' => ID::custom($table3['body']['$id']),
'Rating' => '10',
$table4['body']['$id'] => [
'$id' => ID::custom($table4['body']['$id']),
'Rating' => '10',
$table5['body']['$id'] => [
'$id' => ID::custom($table5['body']['$id']),
'Rating' => '10'
]
]
]
]
]
]);
$this->assertEquals(201, $parentDocument['headers']['status-code']);
// This is the point of the test. We should not need any authorization permission to update the document with same data.
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/rows/' . $table1['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'rowId' => ID::custom($table1['body']['$id']),
'data' => [
'Title' => 'Captain America',
$table2['body']['$id'] => [
'$id' => $table2['body']['$id'],
'Rating' => '10',
$table3['body']['$id'] => [
'$id' => $table3['body']['$id'],
'Rating' => '10',
$table4['body']['$id'] => [
'$id' => $table4['body']['$id'],
'Rating' => '10',
$table5['body']['$id'] => [
'$id' => $table5['body']['$id'],
'Rating' => '10'
]
]
]
]
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($parentDocument['body'], $response['body']);
// Giving update permission of collection 3 to user.
$this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/grids/tables/collection3', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::custom('collection3'),
'name' => ID::custom('collection3'),
'rowSecurity' => false,
'permissions' => [
Permission::create(Role::user($userId)),
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
]
]);
// This is the point of this test. We should be allowed to do this action, and it should not fail on permission check
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/rows/' . $table1['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'Title' => 'Captain America',
$table2['body']['$id'] => [
'$id' => ID::custom($table2['body']['$id']),
'Rating' => '10',
$table3['body']['$id'] => [
'$id' => ID::custom($table3['body']['$id']),
'Rating' => '11',
$table4['body']['$id'] => [
'$id' => ID::custom($table4['body']['$id']),
'Rating' => '10',
$table5['body']['$id'] => [
'$id' => ID::custom($table5['body']['$id']),
'Rating' => '11'
]
]
]
]
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(11, $response['body'][$table2['body']['$id']]['collection3']['Rating']);
// We should not be allowed to update the document as we do not have permission for collection 2.
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/rows/' . $table1['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'Title' => 'Captain America',
$table2['body']['$id'] => [
'$id' => ID::custom($table2['body']['$id']),
'Rating' => '11',
$table3['body']['$id'] => null,
]
]
]);
$this->assertEquals(401, $response['headers']['status-code']);
// We should not be allowed to update the document as we do not have permission for collection 2.
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $table2['body']['$id'] . '/rows/' . $table2['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'Rating' => '11',
]
]);
$this->assertEquals(401, $response['headers']['status-code']);
// Removing update permission from collection 3.
$this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/grids/tables/collection3', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::custom('collection3'),
'name' => ID::custom('collection3'),
'rowSecurity' => false,
'permissions' => [
Permission::create(Role::user($userId)),
Permission::read(Role::user($userId)),
Permission::delete(Role::user($userId)),
]
]);
// Giving update permission to collection 2.
$this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/grids/tables/collection2', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::custom('collection2'),
'name' => ID::custom('collection2'),
'rowSecurity' => false,
'permissions' => [
Permission::create(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::read(Role::user($userId)),
Permission::delete(Role::user($userId)),
]
]);
// Creating collection 3 new document
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table3['body']['$id'] . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'rowId' => ID::custom('collection3Doc1'),
'data' => [
'Rating' => '20'
]
]);
$this->assertEquals(201, $response['headers']['status-code']);
// We should be allowed to link a new document from collection 3 to collection 2.
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/rows/' . $table1['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'Title' => 'Captain America',
$table2['body']['$id'] => [
'$id' => ID::custom($table2['body']['$id']),
$table3['body']['$id'] => 'collection3Doc1',
]
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
// We should be allowed to link and create a new document from collection 3 to collection 2.
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $table1['body']['$id'] . '/rows/' . $table1['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'Title' => 'Captain America',
$table2['body']['$id'] => [
'$id' => ID::custom($table2['body']['$id']),
$table3['body']['$id'] => [
'$id' => ID::custom('collection3Doc2')
],
]
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,278 @@
<?php
namespace Tests\E2E\Services\Databases\Grids;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\Authorization;
class DatabasesPermissionsGuestTest extends Scope
{
use ProjectCustom;
use SideClient;
use DatabasesPermissionsScope;
public function createTable(): array
{
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'InvalidRowDatabase',
]);
$this->assertEquals(201, $database['headers']['status-code']);
$this->assertEquals('InvalidRowDatabase', $database['body']['name']);
$databaseId = $database['body']['$id'];
$publicMovies = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', $this->getServerHeader(), [
'tableId' => ID::unique(),
'name' => 'Movies',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$privateMovies = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', $this->getServerHeader(), [
'tableId' => ID::unique(),
'name' => 'Movies',
'permissions' => [],
'rowSecurity' => true,
]);
$publicTable = ['id' => $publicMovies['body']['$id']];
$privateTable = ['id' => $privateMovies['body']['$id']];
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $publicTable['id'] . '/columns/string', $this->getServerHeader(), [
'key' => 'title',
'size' => 256,
'required' => true,
]);
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $privateTable['id'] . '/columns/string', $this->getServerHeader(), [
'key' => 'title',
'size' => 256,
'required' => true,
]);
sleep(2);
return [
'databaseId' => $databaseId,
'publicTableId' => $publicTable['id'],
'privateTableId' => $privateTable['id'],
];
}
public function permissionsProvider(): array
{
return [
[[Permission::read(Role::any())]],
[[Permission::read(Role::users())]],
[[Permission::update(Role::any()), Permission::delete(Role::any())]],
[[Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())]],
[[Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())]],
[[Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())]],
];
}
/**
* @dataProvider permissionsProvider
*/
public function testReadRows($permissions)
{
$data = $this->createTable();
$publicTableId = $data['publicTableId'];
$privateTableId = $data['privateTableId'];
$databaseId = $data['databaseId'];
$publicResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $publicTableId . '/rows', $this->getServerHeader(), [
'rowId' => ID::unique(),
'data' => [
'title' => 'Lorem',
],
'permissions' => $permissions,
]);
$privateResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $privateTableId . '/rows', $this->getServerHeader(), [
'rowId' => ID::unique(),
'data' => [
'title' => 'Lorem',
],
'permissions' => $permissions,
]);
$this->assertEquals(201, $publicResponse['headers']['status-code']);
$this->assertEquals(201, $privateResponse['headers']['status-code']);
$roles = Authorization::getRoles();
Authorization::cleanRoles();
$publicRows = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $publicTableId . '/rows', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$privateRows = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $privateTableId . '/rows', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertEquals(1, $publicRows['body']['total']);
$this->assertEquals($permissions, $publicRows['body']['rows'][0]['$permissions']);
if (\in_array(Permission::read(Role::any()), $permissions)) {
$this->assertEquals(1, $privateRows['body']['total']);
$this->assertEquals($permissions, $privateRows['body']['rows'][0]['$permissions']);
} else {
$this->assertEquals(0, $privateRows['body']['total']);
}
foreach ($roles as $role) {
Authorization::setRole($role);
}
}
public function testWriteRow()
{
$data = $this->createTable();
$publicTableId = $data['publicTableId'];
$privateTableId = $data['privateTableId'];
$databaseId = $data['databaseId'];
$roles = Authorization::getRoles();
Authorization::cleanRoles();
$publicResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $publicTableId . '/rows', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'rowId' => ID::unique(),
'data' => [
'title' => 'Lorem',
]
]);
$publicRowId = $publicResponse['body']['$id'];
$this->assertEquals(201, $publicResponse['headers']['status-code']);
$privateResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $privateTableId . '/rows', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'rowId' => ID::unique(),
'data' => [
'title' => 'Lorem',
],
]);
$this->assertEquals(401, $privateResponse['headers']['status-code']);
// Create a row in private collection with API key so we can test that update and delete are also not allowed
$privateResponse = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $privateTableId . '/rows', $this->getServerHeader(), [
'rowId' => ID::unique(),
'data' => [
'title' => 'Lorem',
],
]);
$this->assertEquals(201, $privateResponse['headers']['status-code']);
$privateRowId = $privateResponse['body']['$id'];
$publicRow = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $publicTableId . '/rows/' . $publicRowId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'data' => [
'title' => 'Thor: Ragnarok',
],
]);
$this->assertEquals(200, $publicRow['headers']['status-code']);
$this->assertEquals('Thor: Ragnarok', $publicRow['body']['title']);
$privateRow = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $privateTableId . '/rows/' . $privateRowId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'data' => [
'title' => 'Thor: Ragnarok',
],
]);
$this->assertEquals(401, $privateRow['headers']['status-code']);
$publicRow = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/tables/' . $publicTableId . '/rows/' . $publicRowId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertEquals(204, $publicRow['headers']['status-code']);
$privateRow = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/tables/' . $privateTableId . '/rows/' . $privateRowId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
]);
$this->assertEquals(401, $privateRow['headers']['status-code']);
foreach ($roles as $role) {
Authorization::setRole($role);
}
}
public function testWriteRowWithPermissions()
{
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'GuestPermissionsWrite',
]);
$this->assertEquals(201, $database['headers']['status-code']);
$this->assertEquals('GuestPermissionsWrite', $database['body']['name']);
$databaseId = $database['body']['$id'];
$movies = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', $this->getServerHeader(), [
'tableId' => ID::unique(),
'name' => 'Movies',
'permissions' => [
Permission::create(Role::any()),
],
'rowSecurity' => true
]);
$moviesId = $movies['body']['$id'];
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $moviesId . '/columns/string', $this->getServerHeader(), [
'key' => 'title',
'size' => 256,
'required' => true,
]);
sleep(1);
$row = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $moviesId . '/rows', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'rowId' => ID::unique(),
'data' => [
'title' => 'Thor: Ragnarok',
],
'permissions' => [
Permission::read(Role::any()),
]
]);
$this->assertEquals(201, $row['headers']['status-code']);
$this->assertEquals('Thor: Ragnarok', $row['body']['title']);
}
}
@@ -0,0 +1,271 @@
<?php
namespace Tests\E2E\Services\Databases\Grids;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
class DatabasesPermissionsMemberTest extends Scope
{
use ProjectCustom;
use SideClient;
use DatabasesPermissionsScope;
public array $tables = [];
public function createUsers(): array
{
return [
'user1' => $this->createUser('user1', 'lorem@ipsum.com'),
'user2' => $this->createUser('user2', 'dolor@ipsum.com'),
];
}
public function permissionsProvider(): array
{
return [
[
'permissions' => [Permission::read(Role::any())],
'any' => 1,
'users' => 1,
'doconly' => 1,
],
[
'permissions' => [Permission::read(Role::users())],
'any' => 2,
'users' => 2,
'doconly' => 2,
],
[
'permissions' => [Permission::read(Role::user(ID::custom('random')))],
'any' => 3,
'users' => 3,
'doconly' => 2,
],
[
'permissions' => [Permission::read(Role::user(ID::custom('lorem'))), Permission::update(Role::user('lorem')), Permission::delete(Role::user('lorem'))],
'any' => 4,
'users' => 4,
'doconly' => 2,
],
[
'permissions' => [Permission::read(Role::user(ID::custom('dolor'))), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))],
'any' => 5,
'users' => 5,
'doconly' => 2,
],
[
'permissions' => [Permission::read(Role::user(ID::custom('dolor'))), Permission::read(Role::user('lorem')), Permission::update(Role::user('dolor')), Permission::delete(Role::user('dolor'))],
'any' => 6,
'users' => 6,
'doconly' => 2,
],
[
'permissions' => [Permission::update(Role::any()), Permission::delete(Role::any())],
'any' => 7,
'users' => 7,
'doconly' => 2,
],
[
'permissions' => [Permission::read(Role::any()), Permission::update(Role::any()), Permission::delete(Role::any())],
'any' => 8,
'users' => 8,
'doconly' => 3,
],
[
'permissions' => [Permission::read(Role::any()), Permission::update(Role::users()), Permission::delete(Role::users())],
'any' => 9,
'users' => 9,
'doconly' => 4,
],
[
'permissions' => [Permission::read(Role::user(ID::custom('user1')))],
'any' => 10,
'users' => 10,
'doconly' => 5,
],
[
'permissions' => [Permission::read(Role::user(ID::custom('user1'))), Permission::read(Role::user(ID::custom('user1')))],
'any' => 11,
'users' => 11,
'doconly' => 6,
],
[
'permissions' => [Permission::read(Role::users()), Permission::update(Role::users()), Permission::delete(Role::users())],
'any' => 12,
'users' => 12,
'doconly' => 7,
],
];
}
/**
* Setup database
*
* Data providers lose object state so explicitly pass [$users, $tables] to each iteration
*
* @return array
* @throws \Exception
*/
public function testSetupDatabase(): array
{
$this->createUsers();
$db = $this->client->call(Client::METHOD_POST, '/databases', $this->getServerHeader(), [
'databaseId' => ID::unique(),
'name' => 'Test Database',
]);
$this->assertEquals(201, $db['headers']['status-code']);
$databaseId = $db['body']['$id'];
$public = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', $this->getServerHeader(), [
'tableId' => ID::unique(),
'name' => 'Movies',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'rowSecurity' => true,
]);
$this->assertEquals(201, $public['headers']['status-code']);
$this->tables = ['public' => $public['body']['$id']];
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $this->tables['public'] . '/columns/string', $this->getServerHeader(), [
'key' => 'title',
'size' => 256,
'required' => true,
]);
$this->assertEquals(202, $response['headers']['status-code']);
$private = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', $this->getServerHeader(), [
'tableId' => ID::unique(),
'name' => 'Private Movies',
'permissions' => [
Permission::read(Role::users()),
Permission::create(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
'rowSecurity' => true,
]);
$this->assertEquals(201, $private['headers']['status-code']);
$this->tables['private'] = $private['body']['$id'];
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $this->tables['private'] . '/columns/string', $this->getServerHeader(), [
'key' => 'title',
'size' => 256,
'required' => true,
]);
$this->assertEquals(202, $response['headers']['status-code']);
$doconly = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', $this->getServerHeader(), [
'tableId' => ID::unique(),
'name' => 'Row Only Movies',
'permissions' => [],
'rowSecurity' => true,
]);
$this->assertEquals(201, $private['headers']['status-code']);
$this->tables['doconly'] = $doconly['body']['$id'];
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $this->tables['doconly'] . '/columns/string', $this->getServerHeader(), [
'key' => 'title',
'size' => 256,
'required' => true,
]);
$this->assertEquals(202, $response['headers']['status-code']);
sleep(2);
return [
'users' => $this->users,
'tables' => $this->tables,
'databaseId' => $databaseId
];
}
/**
* Data provider params are passed before test dependencies
* @dataProvider permissionsProvider
* @depends testSetupDatabase
*/
public function testReadRows($permissions, $anyCount, $usersCount, $docOnlyCount, $data)
{
$users = $data['users'];
$tables = $data['tables'];
$databaseId = $data['databaseId'];
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tables['public'] . '/rows', $this->getServerHeader(), [
'rowId' => ID::unique(),
'data' => [
'title' => 'Lorem',
],
'permissions' => $permissions
]);
$this->assertEquals(201, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tables['private'] . '/rows', $this->getServerHeader(), [
'rowId' => ID::unique(),
'data' => [
'title' => 'Lorem',
],
'permissions' => $permissions
]);
$this->assertEquals(201, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tables['doconly'] . '/rows', $this->getServerHeader(), [
'rowId' => ID::unique(),
'data' => [
'title' => 'Lorem',
],
'permissions' => $permissions
]);
$this->assertEquals(201, $response['headers']['status-code']);
/**
* Check "any" permission table
*/
$rows = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tables['public'] . '/rows', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
]);
$this->assertEquals(200, $rows['headers']['status-code']);
$this->assertEquals($anyCount, $rows['body']['total']);
/**
* Check "users" permission table
*/
$rows = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tables['private'] . '/rows', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
]);
$this->assertEquals(200, $rows['headers']['status-code']);
$this->assertEquals($usersCount, $rows['body']['total']);
/**
* Check "user:user1" row only permission table
*/
$rows = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tables['doconly'] . '/rows', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users['user1']['session'],
]);
$this->assertEquals(200, $rows['headers']['status-code']);
$this->assertEquals($docOnlyCount, $rows['body']['total']);
}
}
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases;
namespace Tests\E2E\Services\Databases\Grids;
use Tests\E2E\Client;
@@ -0,0 +1,208 @@
<?php
namespace Tests\E2E\Services\Databases\Grids;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
class DatabasesPermissionsTeamTest extends Scope
{
use ProjectCustom;
use SideClient;
use DatabasesPermissionsScope;
public array $tables = [];
public string $databaseId = 'testpermissiondb';
public function createTeams(): array
{
return [
'team1' => $this->createTeam('team1', 'Team 1'),
'team2' => $this->createTeam('team2', 'Team 2'),
];
}
public function createUsers(): array
{
return [
'user1' => $this->createUser('user1', 'lorem@ipsum.com'),
'user2' => $this->createUser('user2', 'dolor@ipsum.com'),
'user3' => $this->createUser('user3', 'sit@ipsum.com'),
];
}
public function createTables($teams)
{
$db = $this->client->call(Client::METHOD_POST, '/databases', $this->getServerHeader(), [
'databaseId' => $this->databaseId,
'name' => 'Test Database',
]);
$this->assertEquals(201, $db['headers']['status-code']);
$table1 = $this->client->call(Client::METHOD_POST, '/databases/' . $this->databaseId . '/grids/tables', $this->getServerHeader(), [
'tableId' => ID::custom('table1'),
'name' => 'Table 1',
'permissions' => [
Permission::read(Role::team($teams['team1']['$id'])),
Permission::create(Role::team($teams['team1']['$id'], 'admin')),
Permission::update(Role::team($teams['team1']['$id'], 'admin')),
Permission::delete(Role::team($teams['team1']['$id'], 'admin')),
],
]);
$this->tables['table1'] = $table1['body']['$id'];
$this->client->call(Client::METHOD_POST, '/databases/' . $this->databaseId . '/grids/tables/' . $this->tables['table1'] . '/columns/string', $this->getServerHeader(), [
'key' => 'title',
'size' => 256,
'required' => true,
]);
$table2 = $this->client->call(Client::METHOD_POST, '/databases/' . $this->databaseId . '/grids/tables', $this->getServerHeader(), [
'tableId' => ID::custom('table2'),
'name' => 'Table 2',
'permissions' => [
Permission::read(Role::team($teams['team2']['$id'])),
Permission::create(Role::team($teams['team2']['$id'], 'owner')),
Permission::update(Role::team($teams['team2']['$id'], 'owner')),
Permission::delete(Role::team($teams['team2']['$id'], 'owner')),
]
]);
$this->tables['table2'] = $table2['body']['$id'];
$this->client->call(Client::METHOD_POST, '/databases/' . $this->databaseId . '/grids/tables/' . $this->tables['table2'] . '/columns/string', $this->getServerHeader(), [
'key' => 'title',
'size' => 256,
'required' => true,
]);
sleep(2);
return $this->tables;
}
/*
* $success = can $user read from $table
* [$user, $table, $success]
*/
public function readRowsProvider(): array
{
return [
['user1', 'table1', true],
['user2', 'table1', false],
['user3', 'table1', true],
['user1', 'table2', false],
['user2', 'table2', true],
['user3', 'table2', true],
];
}
/*
* $success = can $user write to $table
* [$user, $table, $success]
*/
public function writeRowsProvider(): array
{
return [
['user1', 'table1', true],
['user2', 'table1', false],
['user3', 'table1', false],
['user1', 'table2', false],
['user2', 'table2', true],
['user3', 'table2', false],
];
}
/**
* Setup database
*
* Data providers lose object state
* so explicitly pass $users to each iteration
* @return array $users
*/
public function testSetupDatabase(): array
{
$this->createUsers();
$this->createTeams();
$this->addToTeam('user1', 'team1', ['admin']);
$this->addToTeam('user2', 'team2', ['owner']);
// user3 in both teams but with no roles
$this->addToTeam('user3', 'team1');
$this->addToTeam('user3', 'team2');
$this->createTables($this->teams);
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $this->databaseId . '/grids/tables/' . $this->tables['table1'] . '/rows', $this->getServerHeader(), [
'rowId' => ID::unique(),
'data' => [
'title' => 'Lorem',
],
]);
$this->assertEquals(201, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $this->databaseId . '/grids/tables/' . $this->tables['table2'] . '/rows', $this->getServerHeader(), [
'rowId' => ID::unique(),
'data' => [
'title' => 'Ipsum',
],
]);
$this->assertEquals(201, $response['headers']['status-code']);
return $this->users;
}
/**
* Data provider params are passed before test dependencies
* @depends testSetupDatabase
* @dataProvider readRowsProvider
*/
public function testReadRows($user, $table, $success, $users)
{
$rows = $this->client->call(Client::METHOD_GET, '/databases/' . $this->databaseId . '/grids/tables/' . $table . '/rows', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users[$user]['session'],
]);
if ($success) {
$this->assertCount(1, $rows['body']['rows']);
} else {
$this->assertEquals(401, $rows['headers']['status-code']);
}
}
/**
* @depends testSetupDatabase
* @dataProvider writeRowsProvider
*/
public function testWriteRows($user, $table, $success, $users)
{
$rows = $this->client->call(Client::METHOD_POST, '/databases/' . $this->databaseId . '/grids/tables/' . $table . '/rows', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $users[$user]['session'],
], [
'rowId' => ID::unique(),
'data' => [
'title' => 'Ipsum',
],
]);
if ($success) {
$this->assertEquals(201, $rows['headers']['status-code']);
} else {
// 401 if user is a part of team, 404 otherwise
$this->assertContains($rows['headers']['status-code'], [401, 404]);
}
}
}
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases;
namespace Tests\E2E\Services\Databases\Legacy;
use Appwrite\Extend\Exception;
use Tests\E2E\Client;
@@ -87,7 +87,7 @@ trait DatabasesBase
/**
* @depends testCreateCollection
*/
public function testConsoleProject(array $data)
public function testConsoleProject(array $data): void
{
if ($this->getSide() === 'server') {
// Server side can't get past the invalid key check anyway
@@ -1344,7 +1344,8 @@ trait DatabasesBase
'attributes' => ['actors'],
]);
$this->assertEquals(202, $actorsArray['headers']['status-code']);
// Indexes on array attributes are disabled due to MySQL bug
$this->assertEquals(400, $actorsArray['headers']['status-code']);
$twoLevelsArray = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([
'content-type' => 'application/json',
@@ -1357,9 +1358,8 @@ trait DatabasesBase
'orders' => ['DESC', 'DESC'],
]);
$this->assertEquals(202, $twoLevelsArray['headers']['status-code']);
$this->assertEquals('DESC', $twoLevelsArray['body']['orders'][0]);
$this->assertEquals(null, $twoLevelsArray['body']['orders'][1]); // Overwrite by API (array)
// Indexes on array attributes are disabled due to MySQL bug
$this->assertEquals(400, $twoLevelsArray['headers']['status-code']);
$unknown = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([
'content-type' => 'application/json',
@@ -1384,7 +1384,9 @@ trait DatabasesBase
'attributes' => ['integers'], // array attribute
'orders' => ['DESC'], // Check order is removed in API
]);
$this->assertEquals(202, $index1['headers']['status-code']);
// Indexes on array attributes are disabled due to MySQL bug
$this->assertEquals(400, $index1['headers']['status-code']);
$index2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/indexes', array_merge([
'content-type' => 'application/json',
@@ -1395,7 +1397,9 @@ trait DatabasesBase
'type' => 'key',
'attributes' => ['integers'], // array attribute
]);
$this->assertEquals(202, $index2['headers']['status-code']);
// Indexes on array attributes are disabled due to MySQL bug
$this->assertEquals(400, $index2['headers']['status-code']);
/**
* Create Indexes by worker
@@ -1409,22 +1413,32 @@ trait DatabasesBase
]), []);
$this->assertIsArray($movies['body']['indexes']);
$this->assertCount(8, $movies['body']['indexes']);
$this->assertCount(4, $movies['body']['indexes']);
$this->assertEquals($titleIndex['body']['key'], $movies['body']['indexes'][0]['key']);
$this->assertEquals($releaseYearIndex['body']['key'], $movies['body']['indexes'][1]['key']);
$this->assertEquals($releaseWithDate1['body']['key'], $movies['body']['indexes'][2]['key']);
$this->assertEquals($releaseWithDate2['body']['key'], $movies['body']['indexes'][3]['key']);
foreach ($movies['body']['indexes'] as $index) {
$this->assertEquals('available', $index['status']);
}
$this->assertEventually(function () use ($databaseId, $data) {
$movies = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
foreach ($movies['body']['indexes'] as $index) {
$this->assertEquals('available', $index['status']);
}
return true;
}, 60000, 500);
return $data;
}
/**
* @depends testCreateAttributes
*/
* @depends testCreateAttributes
*/
public function testGetIndexByKeyWithLengths(array $data): void
{
$databaseId = $data['databaseId'];
@@ -1453,27 +1467,6 @@ trait DatabasesBase
$this->assertEquals('lengthTestIndex', $index['body']['key']);
$this->assertEquals([128, 200], $index['body']['lengths']);
// Test case for lengths array overriding
// set a length for an array attribute, it should get overriden with Database::ARRAY_INDEX_LENGTH
$create = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/indexes", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'key' => 'lengthOverrideTestIndex',
'type' => 'key',
'attributes' => ['actors'],
'lengths' => [120]
]);
$this->assertEquals(202, $create['headers']['status-code']);
$index = $this->client->call(Client::METHOD_GET, "/databases/{$databaseId}/collections/{$collectionId}/indexes/lengthOverrideTestIndex", [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]);
$this->assertEquals([Database::ARRAY_INDEX_LENGTH], $index['body']['lengths']);
// Test case for count of lengths greater than attributes (should throw 400)
$create = $this->client->call(Client::METHOD_POST, "/databases/{$databaseId}/collections/{$collectionId}/indexes", [
'content-type' => 'application/json',
@@ -1515,8 +1508,8 @@ trait DatabasesBase
$this->assertEquals(400, $create['headers']['status-code']);
}
/**
* @depends testCreateIndexes
*/
* @depends testCreateIndexes
*/
public function testListIndexes(array $data): void
{
$databaseId = $data['databaseId'];
@@ -1985,6 +1978,190 @@ trait DatabasesBase
],
]);
$this->assertEquals(2, $documents['body']['total']);
// test without passing permissions
$document = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Thor: Ragnarok',
'releaseYear' => 2000
]
]);
$this->assertEquals(200, $document['headers']['status-code']);
$this->assertEquals('Thor: Ragnarok', $document['body']['title']);
$document = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(200, $document['headers']['status-code']);
$deleteResponse = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(204, $deleteResponse['headers']['status-code']);
if ($this->getSide() === 'client') {
// Skipped on server side: Creating a document with no permissions results in an empty permissions array, whereas on client side it assigns permissions to the current user
// test without passing permissions
$document = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Thor: Ragnarok',
'releaseYear' => 2000
]
]);
$this->assertEquals(200, $document['headers']['status-code']);
$this->assertEquals('Thor: Ragnarok', $document['body']['title']);
$this->assertCount(3, $document['body']['$permissions']);
$permissionsCreated = $document['body']['$permissions'];
// checking the default created permission
$defaultPermission = [
Permission::read(Role::user($this->getUser()['$id'])),
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id']))
];
// ignoring the order of the permission and checking the permissions
$this->assertEqualsCanonicalizing($defaultPermission, $permissionsCreated);
$document = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()));
$this->assertEquals(200, $document['headers']['status-code']);
// updating the created doc
$document = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Thor: Ragnarok',
'releaseYear' => 2002
]
]);
$this->assertEquals(200, $document['headers']['status-code']);
$this->assertEquals('Thor: Ragnarok', $document['body']['title']);
$this->assertEquals(2002, $document['body']['releaseYear']);
$this->assertCount(3, $document['body']['$permissions']);
$this->assertEquals($permissionsCreated, $document['body']['$permissions']);
// removing the delete permission
$document = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Thor: Ragnarok',
'releaseYear' => 2002
],
'permissions' => [
Permission::update(Role::user($this->getUser()['$id']))
]
]);
$this->assertEquals(200, $document['headers']['status-code']);
$this->assertEquals('Thor: Ragnarok', $document['body']['title']);
$this->assertEquals(2002, $document['body']['releaseYear']);
$this->assertCount(1, $document['body']['$permissions']);
$deleteResponse = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()));
$this->assertEquals(401, $deleteResponse['headers']['status-code']);
// giving the delete permission
$document = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'title' => 'Thor: Ragnarok',
'releaseYear' => 2002
],
'permissions' => [
Permission::update(Role::user($this->getUser()['$id'])),
Permission::delete(Role::user($this->getUser()['$id']))
]
]);
$this->assertEquals(200, $document['headers']['status-code']);
$this->assertEquals('Thor: Ragnarok', $document['body']['title']);
$this->assertEquals(2002, $document['body']['releaseYear']);
$this->assertCount(2, $document['body']['$permissions']);
$deleteResponse = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $data['moviesId'] . '/documents/' . $documentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()));
$this->assertEquals(204, $deleteResponse['headers']['status-code']);
// upsertion for the related document without passing permissions
// data should get added
$newPersonId = ID::unique();
$personNoPerm = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/documents/' . $newPersonId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'library' => [
'$id' => 'library3',
'libraryName' => 'Library 3',
],
],
]);
$this->assertEquals('Library 3', $personNoPerm['body']['library']['libraryName']);
$this->assertCount(3, $personNoPerm['body']['library']['$permissions']);
$this->assertCount(3, $personNoPerm['body']['$permissions']);
$documents = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $person['body']['$id'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => [
Query::select(['fullName', 'library.*'])->toString()
],
]);
$this->assertGreaterThanOrEqual(1, $documents['body']['total']);
$documentsDetails = $documents['body']['documents'];
foreach ($documentsDetails as $doc) {
$this->assertCount(3, $doc['$permissions']);
}
$found = false;
foreach ($documents['body']['documents'] as $doc) {
if (isset($doc['library']['libraryName']) && $doc['library']['libraryName'] === 'Library 3') {
$found = true;
break;
}
}
$this->assertTrue($found, 'Library 3 should be present in the upserted documents.');
// Fetch the related library and assert on its permissions (should be default/inherited)
$library3 = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $library['body']['$id'] . '/documents/library3', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $library3['headers']['status-code']);
$this->assertEquals('Library 3', $library3['body']['libraryName']);
$this->assertArrayHasKey('$permissions', $library3['body']);
$this->assertCount(3, $library3['body']['$permissions']);
$this->assertNotEmpty($library3['body']['$permissions']);
}
}
/**
@@ -2079,7 +2256,6 @@ trait DatabasesBase
return ['documents' => $documents['body']['documents'], 'databaseId' => $databaseId];
}
/**
* @depends testListDocuments
*/
@@ -2830,7 +3006,7 @@ trait DatabasesBase
return $data;
}
public function testInvalidDocumentStructure()
public function testInvalidDocumentStructure(): void
{
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
@@ -3693,7 +3869,7 @@ trait DatabasesBase
$this->assertCount(1, $documentsUser2['body']['documents']);
}
public function testEnforceCollectionPermissions()
public function testEnforceCollectionPermissions(): void
{
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
@@ -4627,7 +4803,11 @@ trait DatabasesBase
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $personCollection . '/documents/' . $person2['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
], $this->getHeaders()), [
'queries' => [
Query::select(['*', 'libraries.*'])->toString()
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertArrayNotHasKey('$collection', $response['body']);
@@ -4637,7 +4817,11 @@ trait DatabasesBase
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $libraryCollection . '/documents/library11', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
], $this->getHeaders()), [
'queries' => [
Query::select(['person_one_to_many.$id'])->toString()
]
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertArrayHasKey('person_one_to_many', $response['body']);
@@ -4787,7 +4971,11 @@ trait DatabasesBase
$album = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $albums['body']['$id'] . '/documents/album1', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
], $this->getHeaders()), [
'queries' => [
Query::select(['*', 'artist.name', 'artist.$permissions'])->toString()
]
]);
$this->assertEquals(200, $album['headers']['status-code']);
$this->assertEquals('album1', $album['body']['$id']);
@@ -4799,7 +4987,11 @@ trait DatabasesBase
$artist = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $artists['body']['$id'] . '/documents/' . $album['body']['artist']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
], $this->getHeaders()), [
'queries' => [
Query::select(['*', 'albums.$id', 'albums.name', 'albums.$permissions'])->toString()
]
]);
$this->assertEquals(200, $artist['headers']['status-code']);
$this->assertEquals('Artist 1', $artist['body']['name']);
@@ -4940,7 +5132,11 @@ trait DatabasesBase
$sport = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $sports['body']['$id'] . '/documents/sport1', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
], $this->getHeaders()), [
'queries' => [
Query::select(['*', 'players.name', 'players.$permissions'])->toString()
]
]);
$this->assertEquals(200, $sport['headers']['status-code']);
$this->assertEquals('sport1', $sport['body']['$id']);
@@ -4954,7 +5150,11 @@ trait DatabasesBase
$player = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $players['body']['$id'] . '/documents/' . $sport['body']['players'][0]['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
], $this->getHeaders()), [
'queries' => [
Query::select(['*', 'sports.$id', 'sports.name', 'sports.$permissions'])->toString()
]
]);
$this->assertEquals(200, $player['headers']['status-code']);
$this->assertEquals('Player 1', $player['body']['name']);
@@ -4982,6 +5182,7 @@ trait DatabasesBase
], $this->getHeaders()), [
'queries' => [
Query::isNotNull('$id')->toString(),
Query::select(['*', 'libraries.*'])->toString(),
Query::startsWith('fullName', 'Stevie')->toString(),
Query::endsWith('fullName', 'Wonder')->toString(),
Query::between('$createdAt', '1975-12-06', '2050-12-01')->toString(),
@@ -5343,7 +5544,7 @@ trait DatabasesBase
], $this->getHeaders()), [
'documentId' => ID::unique(),
'data' => [
'longtext' => file_get_contents(__DIR__ . '/../../../resources/longtext.txt'),
'longtext' => file_get_contents(__DIR__ . '../../../../../resources/longtext.txt'),
],
'permissions' => [
Permission::read(Role::user($this->getUser()['$id'])),
@@ -5571,7 +5772,7 @@ trait DatabasesBase
]), ['min' => 7]);
$this->assertEquals(400, $err['headers']['status-code']);
// Test type error on non-numeric attribut
// Test type error on non-numeric attribute
$typeErr = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId . '/count/decrement', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases;
namespace Tests\E2E\Services\Databases\Legacy;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -224,7 +224,7 @@ class DatabasesConsoleClientTest extends Scope
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(11, count($response['body']));
$this->assertEquals(15, count($response['body']));
$this->assertEquals('24h', $response['body']['range']);
$this->assertIsNumeric($response['body']['documentsTotal']);
$this->assertIsNumeric($response['body']['collectionsTotal']);
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases;
namespace Tests\E2E\Services\Databases\Legacy;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases;
namespace Tests\E2E\Services\Databases\Legacy;
use Appwrite\Extend\Exception as AppwriteException;
use Tests\E2E\Client;
@@ -1470,9 +1470,24 @@ class DatabasesCustomServerTest extends Scope
$this->assertCount(64, $collection['body']['attributes']);
$this->assertCount(0, $collection['body']['indexes']);
foreach ($collection['body']['attributes'] as $attribute) {
$this->assertEquals('available', $attribute['status'], 'attribute: ' . $attribute['key']);
}
$this->assertEventually(function () use ($databaseId, $collectionId) {
$collection = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
foreach ($collection['body']['attributes'] ?? [] as $attribute) {
$this->assertEquals(
'available',
$attribute['status'],
'attribute: ' . $attribute['key']
);
}
return true;
}, 60000, 500);
// Test indexLimit = 64
// MariaDB, MySQL, and MongoDB create 6 indexes per new collection
@@ -3650,7 +3665,7 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals(400, $doc3['headers']['status-code']);
}
public function createRelationshipCollections()
public function createRelationshipCollections(): void
{
// Prepare the database with collections and relationships
$database = $this->client->call(Client::METHOD_POST, '/databases', [
@@ -3699,7 +3714,7 @@ class DatabasesCustomServerTest extends Scope
\sleep(2);
}
public function cleanupRelationshipCollection()
public function cleanupRelationshipCollection(): void
{
$this->client->call(Client::METHOD_DELETE, '/databases/database1', [
'content-type' => 'application/json',
@@ -3779,7 +3794,11 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
]), [
'queries' => [
Query::select(['new_level_2.*'])->toString()
]
]);
$this->assertArrayHasKey('new_level_2', $newDocument['body']);
$this->assertEquals(1, count($newDocument['body']['new_level_2']));
@@ -3889,7 +3908,11 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
]), [
'queries' => [
Query::select(['new_level_2.*'])->toString()
]
]);
$this->assertArrayHasKey('new_level_2', $newDocument['body']);
$this->assertNotEmpty($newDocument['body']['new_level_2']);
@@ -3999,7 +4022,11 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
]), [
'queries' => [
Query::select(['new_level_2.*'])->toString()
]
]);
$this->assertArrayHasKey('new_level_2', $newDocument['body']);
$this->assertNotEmpty($newDocument['body']['new_level_2']);
@@ -4010,7 +4037,11 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
]), [
'queries' => [
Query::select(['*', 'level1.*'])->toString()
]
]);
$this->assertArrayHasKey('level1', $level2Document['body']);
$this->assertNotEmpty($level2Document['body']['level1']);
@@ -4109,7 +4140,11 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
]), [
'queries' => [
Query::select(['new_level_2.*'])->toString()
]
]);
$this->assertArrayHasKey('new_level_2', $newDocument['body']);
$this->assertNotEmpty($newDocument['body']['new_level_2']);
@@ -4120,7 +4155,11 @@ class DatabasesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
]), [
'queries' => [
Query::select(['*', 'level1.*'])->toString()
]
]);
$this->assertArrayHasKey('level1', $level2Document['body']);
$this->assertNotEmpty($level2Document['body']['level1']);
@@ -4465,6 +4504,14 @@ class DatabasesCustomServerTest extends Scope
$createBulkDocuments();
/**
* Wait for database to purge cache...
*
* This test specifically failed on 1.6.x response format,
* could be due to the slow or overworked machine, but being safe here!
*/
sleep(5);
// TEST: Update all documents
$response = $this->client->call(Client::METHOD_PATCH, '/databases/' . $data['databaseId'] . '/collections/' . $data['$id'] . '/documents', array_merge([
'content-type' => 'application/json',
@@ -4483,6 +4530,14 @@ class DatabasesCustomServerTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(10, $response['body']['documents']);
/**
* Wait for database to purge cache...
*
* This test specifically failed on 1.6.x response format,
* could be due to the slow or overworked machine, but being safe here!
*/
sleep(5);
$documents = $this->client->call(Client::METHOD_GET, '/databases/' . $data['databaseId'] . '/collections/' . $data['$id'] . '/documents', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases;
namespace Tests\E2E\Services\Databases\Legacy;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases;
namespace Tests\E2E\Services\Databases\Legacy;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -0,0 +1,87 @@
<?php
namespace Tests\E2E\Services\Databases\Legacy;
use Tests\E2E\Client;
trait DatabasesPermissionsScope
{
public array $users = [];
public array $teams = [];
public function createUser(string $id, string $email, string $password = 'test123!'): array
{
$user = $this->client->call(Client::METHOD_POST, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-dev-key' => $this->getProject()['devKey'] ?? '',
], [
'userId' => $id,
'email' => $email,
'password' => $password
]);
$this->assertEquals(201, $user['headers']['status-code']);
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'email' => $email,
'password' => $password,
]);
$session = $session['cookies']['a_session_' . $this->getProject()['$id']];
$user = [
'$id' => $user['body']['$id'],
'email' => $user['body']['email'],
'session' => $session,
];
$this->users[$id] = $user;
return $user;
}
public function getCreatedUser(string $id): array
{
return $this->users[$id] ?? [];
}
public function createTeam(string $id, string $name): array
{
$team = $this->client->call(Client::METHOD_POST, '/teams', $this->getServerHeader(), [
'teamId' => $id,
'name' => $name
]);
$this->teams[$id] = $team['body'];
return $team['body'];
}
public function addToTeam(string $user, string $team, array $roles = []): array
{
$membership = $this->client->call(Client::METHOD_POST, '/teams/' . $team . '/memberships', $this->getServerHeader(), [
'teamId' => $team,
'email' => $this->getCreatedUser($user)['email'],
'roles' => $roles,
'url' => 'http://localhost:5000/join-us#title'
]);
return [
'user' => $membership['body']['userId'],
'membership' => $membership['body']['$id']
];
}
public function getServerHeader(): array
{
return [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
];
}
}
@@ -1,6 +1,6 @@
<?php
namespace Tests\E2E\Services\Databases;
namespace Tests\E2E\Services\Databases\Legacy;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
@@ -21,8 +21,8 @@ class FunctionsConsoleClientTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'users.*.create',
'users.*.delete',
@@ -39,8 +39,8 @@ class FunctionsConsoleClientTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test Failure',
'execute' => ['some-random-string'],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
]);
$this->assertEquals(400, $function2['headers']['status-code']);
@@ -453,7 +453,7 @@ class FunctionsConsoleClientTest extends Scope
{
$function = $this->createFunction([
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'name' => 'Variable E2E Test',
'entrypoint' => 'index.js',
'logging' => false,
@@ -481,7 +481,7 @@ class FunctionsConsoleClientTest extends Scope
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.js',
'code' => $this->packageFunction('node'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -502,7 +502,7 @@ class FunctionsConsoleClientTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'name' => 'Download Test',
'entrypoint' => 'index.js',
'logging' => false,
@@ -511,7 +511,7 @@ class FunctionsConsoleClientTest extends Scope
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.js',
'code' => $this->packageFunction('node'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -43,8 +43,8 @@ class FunctionsCustomClientTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'users.*.create',
'users.*.delete',
@@ -52,8 +52,7 @@ class FunctionsCustomClientTest extends Scope
'timeout' => 10,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -84,13 +83,12 @@ class FunctionsCustomClientTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::any()->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-fn'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -107,8 +105,8 @@ class FunctionsCustomClientTest extends Scope
$this->assertEquals('Test', $output['APPWRITE_FUNCTION_NAME']);
$this->assertEquals($deploymentId, $output['APPWRITE_FUNCTION_DEPLOYMENT']);
$this->assertEquals('http', $output['APPWRITE_FUNCTION_TRIGGER']);
$this->assertEquals('PHP', $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
$this->assertEquals('8.0', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals('Node.js', $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
$this->assertEquals('22', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals(APP_VERSION_STABLE, $output['APPWRITE_VERSION']);
$this->assertEquals(System::getEnv('_APP_REGION', 'default'), $output['APPWRITE_REGION']);
$this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']);
@@ -142,10 +140,10 @@ class FunctionsCustomClientTest extends Scope
*/
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'name' => 'Test guest execution',
'execute' => [Role::any()->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'vars' => [
'funcKey1' => 'funcValue1',
'funcKey2' => 'funcValue2',
@@ -154,8 +152,7 @@ class FunctionsCustomClientTest extends Scope
'timeout' => 10,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-fn'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -175,8 +172,8 @@ class FunctionsCustomClientTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
@@ -193,15 +190,14 @@ class FunctionsCustomClientTest extends Scope
*/
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'name' => 'Test synchronous execution',
'execute' => [Role::any()->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-fn'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -214,11 +210,11 @@ class FunctionsCustomClientTest extends Scope
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertEquals($functionId, $output['APPWRITE_FUNCTION_ID']);
$this->assertEquals('Test', $output['APPWRITE_FUNCTION_NAME']);
$this->assertEquals('Test synchronous execution', $output['APPWRITE_FUNCTION_NAME']);
$this->assertEquals($deploymentId, $output['APPWRITE_FUNCTION_DEPLOYMENT']);
$this->assertEquals('http', $output['APPWRITE_FUNCTION_TRIGGER']);
$this->assertEquals('PHP', $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
$this->assertEquals('8.0', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals('Node.js', $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
$this->assertEquals('22', $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals(APP_VERSION_STABLE, $output['APPWRITE_VERSION']);
$this->assertEquals(System::getEnv('_APP_REGION', 'default'), $output['APPWRITE_REGION']);
$this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']);
@@ -239,12 +235,12 @@ class FunctionsCustomClientTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::any()->toString()],
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'entrypoint' => 'index.js'
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.js',
'code' => $this->packageFunction('node'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -340,7 +336,7 @@ class FunctionsCustomClientTest extends Scope
'limit' => 5,
'offset' => 2,
'useCases' => ['databases'],
'runtimes' => ['node-16.0']
'runtimes' => ['node-22']
]);
$this->assertEquals(200, $templates['headers']['status-code']);
@@ -352,7 +348,7 @@ class FunctionsCustomClientTest extends Scope
$this->assertContains($template['useCases'][0], ['databases']);
}
$this->assertContains('node-16.0', array_column($templates['body']['templates'][0]['runtimes'], 'name'));
$this->assertContains('node-22', array_column($templates['body']['templates'][0]['runtimes'], 'name'));
/**
* Test for FAILURE
@@ -35,7 +35,7 @@ class FunctionsCustomServerTest extends Scope
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Specs function',
'runtime' => 'php-8.0',
'runtime' => 'node-22',
'specification' => $specifications['body']['specifications'][0]['slug']
]);
$this->assertEquals(201, $function['headers']['status-code']);
@@ -50,7 +50,7 @@ class FunctionsCustomServerTest extends Scope
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Specs function',
'runtime' => 'php-8.0',
'runtime' => 'node-22',
'specification' => 'cheap-please'
]);
$this->assertEquals(400, $function['headers']['status-code']);
@@ -64,8 +64,8 @@ class FunctionsCustomServerTest extends Scope
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'buckets.*.create',
'buckets.*.delete',
@@ -79,7 +79,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(201, $function['headers']['status-code']);
$this->assertNotEmpty($function['body']['$id']);
$this->assertEquals('Test', $function['body']['name']);
$this->assertEquals('php-8.0', $function['body']['runtime']);
$this->assertEquals('node-22', $function['body']['runtime']);
$this->assertEquals(true, $dateValidator->isValid($function['body']['$createdAt']));
$this->assertEquals(true, $dateValidator->isValid($function['body']['$updatedAt']));
$this->assertEquals('', $function['body']['deploymentId']);
@@ -180,7 +180,7 @@ class FunctionsCustomServerTest extends Scope
// Test search runtime
$functions = $this->listFunctions([
'search' => 'php-8.0'
'search' => 'node-22'
]);
$this->assertEquals($functions['headers']['status-code'], 200);
@@ -193,8 +193,8 @@ class FunctionsCustomServerTest extends Scope
$this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test 2',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'buckets.*.create',
'buckets.*.delete',
@@ -286,8 +286,8 @@ class FunctionsCustomServerTest extends Scope
],
'schedule' => '0 0 1 1 *',
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
]);
$dateValidator = new DatetimeValidator();
@@ -322,8 +322,8 @@ class FunctionsCustomServerTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'users.*.create',
'users.*.delete',
@@ -338,8 +338,7 @@ class FunctionsCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey'],
'x-sdk-language' => 'cli',
], [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -363,7 +362,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(200, $starterTemplate['headers']['status-code']);
$phpRuntime = array_values(array_filter($starterTemplate['body']['runtimes'], function ($runtime) {
return $runtime['name'] === 'php-8.0';
return $runtime['name'] === 'node-22';
}))[0];
// If this fails, the template has variables, and this test needs to be updated
@@ -373,7 +372,7 @@ class FunctionsCustomServerTest extends Scope
[
'functionId' => ID::unique(),
'name' => $starterTemplate['body']['name'],
'runtime' => 'php-8.0',
'runtime' => 'node-22',
'execute' => $starterTemplate['body']['permissions'],
'entrypoint' => $phpRuntime['entrypoint'],
'events' => $starterTemplate['body']['events'],
@@ -514,7 +513,7 @@ class FunctionsCustomServerTest extends Scope
$functionId = $data['functionId'];
$deployment = $this->createDeployment($functionId, [
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -522,7 +521,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertNotEmpty($deployment['body']['$id']);
$this->assertEquals('waiting', $deployment['body']['status']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt']));
$this->assertEquals('index.php', $deployment['body']['entrypoint']);
$this->assertEquals('index.js', $deployment['body']['entrypoint']);
$deploymentIdActive = $deployment['body']['$id'] ?? '';
@@ -533,7 +532,7 @@ class FunctionsCustomServerTest extends Scope
}, 50000, 500);
$deployment = $this->createDeployment($functionId, [
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => 'false'
]);
@@ -573,7 +572,7 @@ class FunctionsCustomServerTest extends Scope
$functionId = $data['functionId'];
$deployment = $this->createDeployment($functionId, [
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => 'false'
]);
@@ -582,7 +581,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt']));
$this->assertEquals('index.php', $deployment['body']['entrypoint']);
$this->assertEquals('index.js', $deployment['body']['entrypoint']);
$this->assertEventually(function () use ($functionId, $deploymentId) {
$deployment = $this->getDeployment($functionId, $deploymentId);
@@ -595,12 +594,12 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('canceled', $deployment['body']['status']);
/**
* Build worker still runs the build.
* 30s sleep gives worker enough time to finish build.
* After build finished, it should still be canceled, not ready.
*/
\sleep(30);
// Ensures worker got eventually aware of cancellation and reacted properly
$this->assertEventually(function () use ($functionId, $deploymentId) {
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertStringContainsString('Build has been canceled.', $deployment['body']['buildLogs']);
});
$deployment = $this->getDeployment($functionId, $deploymentId);
@@ -618,7 +617,7 @@ class FunctionsCustomServerTest extends Scope
*/
$functionId = $data['functionId'];
$folder = 'php-large';
$folder = 'large';
$code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz";
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
@@ -633,13 +632,13 @@ class FunctionsCustomServerTest extends Scope
];
$id = '';
while (!feof($handle)) {
$curlFile = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'php-large-fx.tar.gz');
$curlFile = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode(@fread($handle, $chunkSize)), $mimeType, 'large-fx.tar.gz');
$headers['content-range'] = 'bytes ' . ($counter * $chunkSize) . '-' . min(((($counter * $chunkSize) + $chunkSize) - 1), $size - 1) . '/' . $size;
if (!empty($id)) {
$headers['x-appwrite-id'] = $id;
}
$largeTag = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge($headers, $this->getHeaders()), [
'entrypoint' => 'index.php',
'entrypoint' => 'index.js',
'code' => $curlFile,
'activate' => true,
'commands' => 'cp blue.mp4 copy.mp4 && ls -al' // +7MB buildSize
@@ -652,7 +651,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(202, $largeTag['headers']['status-code']);
$this->assertNotEmpty($largeTag['body']['$id']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($largeTag['body']['$createdAt']));
$this->assertEquals('index.php', $largeTag['body']['entrypoint']);
$this->assertEquals('index.js', $largeTag['body']['entrypoint']);
$this->assertGreaterThan(1024 * 1024 * 5, $largeTag['body']['sourceSize']); // ~7MB video file
$this->assertLessThan(1024 * 1024 * 10, $largeTag['body']['sourceSize']); // ~7MB video file
@@ -903,8 +902,8 @@ class FunctionsCustomServerTest extends Scope
$this->assertStringContainsString($data['deploymentId'], $execution['body']['responseBody']);
$this->assertStringContainsString('Test1', $execution['body']['responseBody']);
$this->assertStringContainsString('http', $execution['body']['responseBody']);
$this->assertStringContainsString('PHP', $execution['body']['responseBody']);
$this->assertStringContainsString('8.0', $execution['body']['responseBody']);
$this->assertStringContainsString('Node.js', $execution['body']['responseBody']);
$this->assertStringContainsString('22', $execution['body']['responseBody']);
$this->assertStringContainsString('Global Variable Value', $execution['body']['responseBody']);
// $this->assertStringContainsString('êä', $execution['body']['responseBody']); // tests unknown utf-8 chars
$this->assertNotEmpty($execution['body']['errors']);
@@ -1017,8 +1016,8 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertStringContainsString('Test1', $execution['body']['responseBody']);
$this->assertStringContainsString('http', $execution['body']['responseBody']);
$this->assertStringContainsString('PHP', $execution['body']['responseBody']);
$this->assertStringContainsString('8.0', $execution['body']['responseBody']);
$this->assertStringContainsString('Node.js', $execution['body']['responseBody']);
$this->assertStringContainsString('22', $execution['body']['responseBody']);
// $this->assertStringContainsString('êä', $execution['body']['response']); // tests unknown utf-8 chars
$this->assertLessThan(1.500, $execution['body']['duration']);
@@ -1120,8 +1119,8 @@ class FunctionsCustomServerTest extends Scope
'users.*.update.email',
],
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'specification' => Specification::S_1VCPU_1GB,
]);
@@ -1148,8 +1147,8 @@ class FunctionsCustomServerTest extends Scope
'users.*.update.email',
],
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'specification' => Specification::S_1VCPU_512MB,
]);
@@ -1178,8 +1177,8 @@ class FunctionsCustomServerTest extends Scope
'users.*.update.email',
],
'timeout' => 15,
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'specification' => 's-2vcpu-512mb', // Invalid specification
]);
@@ -1236,9 +1235,9 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test php-8.0',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test timeout execution',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [],
'schedule' => '',
'timeout' => 5, // Should timeout after 5 seconds
@@ -1280,12 +1279,12 @@ class FunctionsCustomServerTest extends Scope
*/
public function provideCustomExecutions(): array
{
// Most disabled to keep tests fast
return [
['folder' => 'php-fn', 'name' => 'php-8.0', 'entrypoint' => 'index.php', 'runtimeName' => 'PHP', 'runtimeVersion' => '8.0'],
['folder' => 'node', 'name' => 'node-18.0', 'entrypoint' => 'index.js', 'runtimeName' => 'Node.js', 'runtimeVersion' => '18.0'],
['folder' => 'python', 'name' => 'python-3.9', 'entrypoint' => 'main.py', 'runtimeName' => 'Python', 'runtimeVersion' => '3.9'],
['folder' => 'ruby', 'name' => 'ruby-3.1', 'entrypoint' => 'main.rb', 'runtimeName' => 'Ruby', 'runtimeVersion' => '3.1'],
// Swift and Dart disabled on purpose, as it's very slow.
// ['folder' => 'php-fn', 'name' => 'php-8.0', 'entrypoint' => 'index.php', 'runtimeName' => 'PHP', 'runtimeVersion' => '8.0'],
['folder' => 'node', 'name' => 'node-22', 'entrypoint' => 'index.js', 'runtimeName' => 'Node.js', 'runtimeVersion' => '22'],
// ['folder' => 'python', 'name' => 'python-3.9', 'entrypoint' => 'main.py', 'runtimeName' => 'Python', 'runtimeVersion' => '3.9'],
// ['folder' => 'ruby', 'name' => 'ruby-3.1', 'entrypoint' => 'main.rb', 'runtimeName' => 'Ruby', 'runtimeVersion' => '3.1'],
// [ 'folder' => 'dart', 'name' => 'dart-2.15', 'entrypoint' => 'main.dart', 'runtimeName' => 'Dart', 'runtimeVersion' => '2.15' ],
// [ 'folder' => 'swift', 'name' => 'swift-5.5', 'entrypoint' => 'index.swift', 'runtimeName' => 'Swift', 'runtimeVersion' => '5.5' ],
];
@@ -1324,27 +1323,14 @@ class FunctionsCustomServerTest extends Scope
]);
$execution = $this->createExecution($functionId, [
'body' => 'foobar',
'async' => 'false'
]);
$output = json_decode($execution['body']['responseBody'], true);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertEquals($functionId, $output['APPWRITE_FUNCTION_ID']);
$this->assertEquals('Test ' . $name, $output['APPWRITE_FUNCTION_NAME']);
$this->assertEquals($deploymentId, $output['APPWRITE_FUNCTION_DEPLOYMENT']);
$this->assertEquals('http', $output['APPWRITE_FUNCTION_TRIGGER']);
$this->assertEquals($runtimeName, $output['APPWRITE_FUNCTION_RUNTIME_NAME']);
$this->assertEquals($runtimeVersion, $output['APPWRITE_FUNCTION_RUNTIME_VERSION']);
$this->assertEquals('', $output['APPWRITE_FUNCTION_EVENT']);
$this->assertEquals('foobar', $output['APPWRITE_FUNCTION_DATA']);
$this->assertEquals('variable', $output['CUSTOM_VARIABLE']);
$this->assertEmpty($output['APPWRITE_FUNCTION_USER_ID']);
$this->assertEmpty($output['APPWRITE_FUNCTION_JWT']);
$this->assertEquals($this->getProject()['$id'], $output['APPWRITE_FUNCTION_PROJECT_ID']);
$this->assertStringContainsString('Amazing Function Log', $execution['body']['logs']);
$this->assertEquals('OK', $execution['body']['responseBody']);
$this->assertEmpty($execution['body']['logs']);
$this->assertEmpty($execution['body']['errors']);
$executionId = $execution['body']['$id'] ?? '';
@@ -1357,7 +1343,8 @@ class FunctionsCustomServerTest extends Scope
$this->assertCount(1, $executions['body']['executions']);
$this->assertEquals($executions['body']['executions'][0]['$id'], $executionId);
$this->assertEquals($executions['body']['executions'][0]['trigger'], 'http');
$this->assertStringContainsString('Amazing Function Log', $executions['body']['executions'][0]['logs']);
$this->assertEquals(200, $executions['body']['executions'][0]['responseStatusCode']);
$this->assertEmpty($executions['body']['executions'][0]['responseBody']);
$this->cleanupFunction($functionId);
}
@@ -1366,15 +1353,14 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Binary executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Binary executions',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'execute' => ['any']
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-binary-response'),
'code' => $this->packageFunction('binary-response'),
'activate' => true
]);
@@ -1415,15 +1401,14 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Binary executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Binary executions',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'execute' => ['any']
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-binary-request'),
'code' => $this->packageFunction('binary-request'),
'activate' => true
]);
@@ -1461,49 +1446,6 @@ class FunctionsCustomServerTest extends Scope
$this->cleanupFunction($functionId);
}
public function testv2Function()
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP V2',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'events' => [],
'timeout' => 15,
]);
$variable = $this->client->call(Client::METHOD_PATCH, '/mock/functions-v2', [
'content-type' => 'application/json',
'origin' => 'http://localhost',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-mode' => 'admin',
], [
'functionId' => $functionId
]);
$this->assertEquals(204, $variable['headers']['status-code']);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-v2'),
'activate' => true
]);
$execution = $this->createExecution($functionId, [
'body' => 'foobar',
'async' => 'false'
]);
$this->assertEquals(201, $execution['headers']['status-code']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$output = json_decode($execution['body']['responseBody'], true);
$this->assertEquals(true, $output['v2Woks']);
$this->cleanupFunction($functionId);
}
public function testGetRuntimes()
{
$runtimes = $this->client->call(Client::METHOD_GET, '/functions/runtimes', array_merge([
@@ -1531,17 +1473,16 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Event executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Event executions',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'users.*.create',
],
'timeout' => 15,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-event'),
'code' => $this->packageFunction('event-handler'),
'activate' => true
]);
@@ -1584,17 +1525,16 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Scopes executions',
'commands' => 'bash setup.sh && composer install',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Scopes executions',
'commands' => 'bash setup.sh && npm install',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'scopes' => ['users.read'],
'timeout' => 15,
]);
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-scopes'),
'code' => $this->packageFunction('dynamic-api-key'),
'activate' => true,
]);
@@ -1643,16 +1583,15 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Cookie executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Cookie executions',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
]);
$this->assertNotEmpty($functionId);
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-cookie'),
'code' => $this->packageFunction('cookies'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId);
@@ -1685,9 +1624,9 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Cookie executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Cookie executions',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'execute' => ['any']
]);
@@ -1695,8 +1634,7 @@ class FunctionsCustomServerTest extends Scope
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-cookie'),
'code' => $this->packageFunction('cookies'),
'activate' => true
]);
@@ -1743,9 +1681,9 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Binary executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Binary executions',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'execute' => ['any']
]);
@@ -1753,8 +1691,7 @@ class FunctionsCustomServerTest extends Scope
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-binary-response'),
'code' => $this->packageFunction('binary-response'),
'activate' => true
]);
@@ -1778,9 +1715,9 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test PHP Binary executions',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'name' => 'Test Binary executions',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'execute' => ['any']
]);
@@ -1788,8 +1725,7 @@ class FunctionsCustomServerTest extends Scope
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php-binary-request'),
'code' => $this->packageFunction('binary-request'),
'activate' => true
]);
@@ -1816,8 +1752,8 @@ class FunctionsCustomServerTest extends Scope
], $this->getHeaders()), [
'functionId' => ID::unique(),
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
]);
@@ -1851,8 +1787,8 @@ class FunctionsCustomServerTest extends Scope
$function1Id = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'execute' => ['any']
]);
@@ -1860,8 +1796,8 @@ class FunctionsCustomServerTest extends Scope
$function2Id = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test2',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'execute' => ['any']
]);
@@ -1892,7 +1828,7 @@ class FunctionsCustomServerTest extends Scope
{
$function = $this->createFunction([
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'name' => 'Logging Test',
'entrypoint' => 'index.js',
'logging' => false,
@@ -1908,7 +1844,7 @@ class FunctionsCustomServerTest extends Scope
$domain = $this->setupFunctionDomain($functionId);
$this->setupDeployment($functionId, [
'code' => $this->packageFunction('node'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -1985,7 +1921,7 @@ class FunctionsCustomServerTest extends Scope
// Check if the function specifications are correctly set in builds
$function = $this->createFunction([
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'name' => 'Specification Test',
'entrypoint' => 'index.js',
'logging' => false,
@@ -2001,7 +1937,7 @@ class FunctionsCustomServerTest extends Scope
$functionId = $functionId = $function['body']['$id'] ?? '';
$deploymentId = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('node'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -2027,7 +1963,7 @@ class FunctionsCustomServerTest extends Scope
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'name' => 'Duplicate Deployment Test',
'entrypoint' => 'index.js',
'commands' => ''
@@ -2035,7 +1971,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertNotEmpty($functionId);
$deploymentId1 = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('node'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId1);
@@ -2045,7 +1981,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertStringContainsString('APPWRITE_FUNCTION_ID', $execution['body']['responseBody']);
$function = $this->updateFunction($functionId, [
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'name' => 'Duplicate Deployment Test',
'entrypoint' => 'index.js',
'commands' => 'rm index.js && mv maintenance.js index.js'
@@ -2089,13 +2025,12 @@ class FunctionsCustomServerTest extends Scope
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'runtime' => 'php-8.0',
'runtime' => 'node-22',
'name' => 'Re-activate Test',
'entrypoint' => 'index.php',
'entrypoint' => 'index.js',
]);
$this->assertNotEmpty($functionId);
$function = $this->getFunction($functionId);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertArrayHasKey('latestDeploymentId', $function['body']);
@@ -2106,7 +2041,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEmpty($function['body']['latestDeploymentStatus']);
$deploymentId1 = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('php-cookie'),
'code' => $this->packageFunction('cookies'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId1);
@@ -2124,7 +2059,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertStringContainsString('cookieValue', $execution['body']['responseBody']);
$deploymentId2 = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId2);
@@ -2192,8 +2127,8 @@ class FunctionsCustomServerTest extends Scope
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test Error Pages',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'commands' => 'cd non-existing-directory',
'execute' => ['any']
@@ -2203,8 +2138,7 @@ class FunctionsCustomServerTest extends Scope
$proxyClient->setEndpoint('http://' . $domain);
$deployment = $this->createDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -2221,8 +2155,7 @@ class FunctionsCustomServerTest extends Scope
// canceled deployment
$deployment = $this->createDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -2250,8 +2183,8 @@ class FunctionsCustomServerTest extends Scope
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test Error Pages',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'commands' => '',
'execute' => ['users']
@@ -2262,7 +2195,7 @@ class FunctionsCustomServerTest extends Scope
$proxyClient->setEndpoint('http://' . $domain);
$deploymentId = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId);
@@ -2284,8 +2217,8 @@ class FunctionsCustomServerTest extends Scope
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test Error Pages',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'commands' => '',
'execute' => ['any']
@@ -2296,7 +2229,7 @@ class FunctionsCustomServerTest extends Scope
$proxyClient->setEndpoint('http://' . $domain);
$deploymentId = $this->setupDeployment($functionId, [
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
$this->assertNotEmpty($deploymentId);
@@ -25,8 +25,8 @@ class FunctionsScheduleTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'users.*.create',
'users.*.delete',
@@ -36,8 +36,7 @@ class FunctionsScheduleTest extends Scope
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -77,14 +76,13 @@ class FunctionsScheduleTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
'logging' => true,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -179,15 +177,14 @@ class FunctionsScheduleTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
'logging' => true,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
+873 -10
View File
@@ -13,12 +13,28 @@ trait Base
public static string $GET_DATABASE = 'get_database';
public static string $UPDATE_DATABASE = 'update_database';
public static string $DELETE_DATABASE = 'delete_database';
// Collections
public static string $CREATE_COLLECTION = 'create_collection';
public static string $GET_COLLECTION = 'get_collection';
public static string $GET_COLLECTIONS = 'list_collections';
public static string $UPDATE_COLLECTION = 'update_collection';
public static string $DELETE_COLLECTION = 'delete_collection';
// Grid databases
public static string $GRIDS_CREATE_DATABASE = 'grids_create_database';
public static string $GRIDS_GET_DATABASES = 'grids_get_databases';
public static string $GRIDS_GET_DATABASE = 'grids_get_database';
public static string $GRIDS_UPDATE_DATABASE = 'grids_update_database';
public static string $GRIDS_DELETE_DATABASE = 'grids_delete_database';
// Grid tables
public static string $CREATE_TABLE = 'create_table';
public static string $GET_TABLE = 'get_table';
public static string $GET_TABLES = 'list_tables';
public static string $UPDATE_TABLE = 'update_table';
public static string $DELETE_TABLE = 'delete_table';
// Attributes
public static string $CREATE_STRING_ATTRIBUTE = 'create_string_attribute';
public static string $CREATE_INTEGER_ATTRIBUTE = 'create_integer_attribute';
@@ -45,18 +61,74 @@ trait Base
public static string $GET_ATTRIBUTES = 'get_attributes';
public static string $GET_ATTRIBUTE = 'get_attribute';
public static string $DELETE_ATTRIBUTE = 'delete_attribute';
// Indexes
public static string $CREATE_INDEX = 'create_index';
public static string $GET_INDEXES = 'get_indexes';
public static string $GET_INDEX = 'get_index';
public static string $DELETE_INDEX = 'delete_index';
// Grids columns
public static string $CREATE_STRING_COLUMN = 'create_string_column';
public static string $CREATE_INTEGER_COLUMN = 'create_integer_column';
public static string $CREATE_FLOAT_COLUMN = 'create_float_column';
public static string $CREATE_BOOLEAN_COLUMN = 'create_boolean_column';
public static string $CREATE_URL_COLUMN = 'create_url_column';
public static string $CREATE_EMAIL_COLUMN = 'create_email_column';
public static string $CREATE_IP_COLUMN = 'create_ip_column';
public static string $CREATE_ENUM_COLUMN = 'create_enum_column';
public static string $CREATE_DATETIME_COLUMN = 'create_datetime_column';
public static string $CREATE_RELATIONSHIP_COLUMN = 'create_relationship_column';
public static string $UPDATE_STRING_COLUMN = 'update_string_column';
public static string $UPDATE_INTEGER_COLUMN = 'update_integer_column';
public static string $UPDATE_FLOAT_COLUMN = 'update_float_column';
public static string $UPDATE_BOOLEAN_COLUMN = 'update_boolean_column';
public static string $UPDATE_URL_COLUMN = 'update_url_column';
public static string $UPDATE_EMAIL_COLUMN = 'update_email_column';
public static string $UPDATE_IP_COLUMN = 'update_ip_column';
public static string $UPDATE_ENUM_COLUMN = 'update_enum_column';
public static string $UPDATE_DATETIME_COLUMN = 'update_datetime_column';
public static string $UPDATE_RELATIONSHIP_COLUMN = 'update_relationship_column';
public static string $GET_COLUMNS = 'get_columns';
public static string $GET_COLUMN = 'get_column';
public static string $DELETE_COLUMN = 'delete_column';
// Attribute Indexes
public static string $CREATE_INDEX = 'create_attribute_index';
public static string $GET_INDEXES = 'get_attribute_indexes';
public static string $GET_INDEX = 'get_attribute_index';
public static string $DELETE_INDEX = 'delete_attribute_index';
// Grid column indexes
public static string $CREATE_COLUMN_INDEX = 'create_column_index';
public static string $GET_COLUMN_INDEXES = 'get_column_indexes';
public static string $GET_COLUMN_INDEX = 'get_column_index';
public static string $DELETE_COLUMN_INDEX = 'delete_column_index';
// Documents
public static string $CREATE_DOCUMENT = 'create_document_rest';
public static string $GET_DOCUMENTS = 'list_documents';
public static string $GET_DOCUMENT = 'get_document';
public static string $UPDATE_DOCUMENT = 'update_document';
public static string $UPSERT_DOCUMENT = 'upsert_document';
public static string $DELETE_DOCUMENT = 'delete_document';
// Documents - Bulk APIs
public static string $CREATE_DOCUMENTS = 'create_documents_rest';
public static string $UPDATE_DOCUMENTS = 'update_documents';
public static string $UPSERT_DOCUMENTS = 'upsert_documents';
public static string $DELETE_DOCUMENTS = 'delete_documents';
// Grid rows
public static string $CREATE_ROW = 'create_row_rest';
public static string $GET_ROWS = 'list_rows';
public static string $GET_ROW = 'get_row';
public static string $UPDATE_ROW = 'update_row';
public static string $UPSERT_ROW = 'upsert_row';
public static string $DELETE_ROW = 'delete_row';
// Grid rows - Bulk APIs
public static string $CREATE_ROWS = 'create_rows_rest';
public static string $UPDATE_ROWS = 'update_rows';
public static string $UPSERT_ROWS = 'upsert_rows';
public static string $DELETE_ROWS = 'delete_rows';
// Custom Entities
public static string $CREATE_CUSTOM_ENTITY = 'create_custom_entity';
public static string $GET_CUSTOM_ENTITIES = 'get_custom_entities';
@@ -255,9 +327,10 @@ trait Base
public static string $UPDATE_PUSH_NOTIFICATION = 'update_push_notification';
// Complex queries
public static string $COMPLEX_QUERY = 'complex_query';
public static string $COMPLEX_QUERY_TABLE = 'complex_query_table';
public static string $COMPLEX_QUERY_COLLECTION = 'complex_query_collection';
// Fragments
// Attribute Fragments
public static string $FRAGMENT_ATTRIBUTES = '
fragment attributeProperties on Attributes {
... on AttributeString {
@@ -332,6 +405,81 @@ trait Base
}
';
// Column Fragments
public static string $FRAGMENT_COLUMNS = '
fragment columnProperties on Columns {
... on ColumnString {
key
required
array
status
default
size
}
... on ColumnInteger {
key
required
array
status
intDefault: default
intMin: min
intMax: max
}
... on ColumnFloat {
key
required
array
status
floatDefault: default
floatMin: min
floatMax: max
}
... on ColumnBoolean {
key
required
array
status
boolDefault: default
}
... on ColumnUrl {
key
required
array
status
default
}
... on ColumnEmail {
key
required
array
status
default
}
... on ColumnIp {
key
required
array
status
default
}
... on ColumnEnum {
key
required
array
status
default
elements
}
... on ColumnDatetime {
key
required
array
status
default
}
}
';
public static string $FRAGMENT_HASH_OPTIONS = '
fragment options on HashOptions {
... on AlgoArgon2 {
@@ -393,6 +541,43 @@ trait Base
status
}
}';
case self::$GRIDS_CREATE_DATABASE:
return 'mutation gridsCreateDatabase($databaseId: String!, $name: String!) {
gridsCreateDatabase(databaseId: $databaseId, name: $name) {
_id
name
}
}';
case self::$GRIDS_GET_DATABASES:
return 'query gridsListDatabases {
gridsListDatabases {
total
databases {
_id
name
}
}
}';
case self::$GRIDS_GET_DATABASE:
return 'query gridsGetDatabase($databaseId: String!) {
gridsGetDatabase(databaseId: $databaseId) {
_id
name
}
}';
case self::$GRIDS_UPDATE_DATABASE:
return 'mutation gridsUpdateDatabase($databaseId: String!, $name: String!) {
gridsUpdateDatabase(databaseId: $databaseId, name: $name) {
_id
name
}
}';
case self::$GRIDS_DELETE_DATABASE:
return 'mutation gridsDeleteDatabase($databaseId: String!) {
gridsDeleteDatabase(databaseId: $databaseId) {
status
}
}';
case self::$GET_COLLECTION:
return 'query getCollection($databaseId: String!, $collectionId: String!) {
databasesGetCollection(databaseId: $databaseId, collectionId: $collectionId) {
@@ -438,6 +623,51 @@ trait Base
status
}
}';
case self::$GET_TABLE:
return 'query gridsGetTable($databaseId: String!, $tableId: String!) {
gridsGetTable(databaseId: $databaseId, tableId: $tableId) {
_id
_permissions
rowSecurity
name
}
}';
case self::$GET_TABLES:
return 'query gridsListTables($databaseId: String!) {
gridsListTables(databaseId: $databaseId) {
total
tables {
_id
_permissions
rowSecurity
name
}
}
}';
case self::$CREATE_TABLE:
return 'mutation gridsCreateTable($databaseId: String!, $tableId: String!, $name: String!, $rowSecurity: Boolean!, $permissions: [String!]!) {
gridsCreateTable(databaseId: $databaseId, tableId: $tableId, name: $name, rowSecurity: $rowSecurity, permissions: $permissions) {
_id
_permissions
rowSecurity
name
}
}';
case self::$UPDATE_TABLE:
return 'mutation gridsUpdateTable($databaseId: String!, $tableId: String!, $name: String!, $rowSecurity: Boolean!, $permissions: [String!], $enabled: Boolean) {
gridsUpdateTable(databaseId: $databaseId, tableId: $tableId, name: $name, rowSecurity: $rowSecurity, permissions: $permissions, enabled: $enabled) {
_id
_permissions
rowSecurity
name
}
}';
case self::$DELETE_TABLE:
return 'mutation gridsDeleteTable($databaseId: String!, $tableId: String!) {
gridsDeleteTable(databaseId: $databaseId, tableId: $tableId) {
status
}
}';
case self::$CREATE_STRING_ATTRIBUTE:
return 'mutation createStringAttribute($databaseId: String!, $collectionId: String!, $key: String!, $size: Int!, $required: Boolean!, $default: String, $array: Boolean){
databasesCreateStringAttribute(databaseId: $databaseId, collectionId: $collectionId, key: $key, size: $size, required: $required, default: $default, array: $array) {
@@ -614,6 +844,182 @@ trait Base
onDelete
}
}';
case self::$CREATE_STRING_COLUMN:
return 'mutation createStringColumn($databaseId: String!, $tableId: String!, $key: String!, $size: Int!, $required: Boolean!, $default: String, $array: Boolean){
gridsCreateStringColumn(databaseId: $databaseId, tableId: $tableId, key: $key, size: $size, required: $required, default: $default, array: $array) {
key
required
default
array
}
}';
case self::$CREATE_INTEGER_COLUMN:
return 'mutation createIntegerColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $min: Int, $max: Int, $default: Int, $array: Boolean){
gridsCreateIntegerColumn(databaseId: $databaseId, tableId: $tableId, key: $key, min: $min, max: $max, required: $required, default: $default, array: $array) {
key
required
min
max
default
array
}
}';
case self::$CREATE_FLOAT_COLUMN:
return 'mutation createFloatColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $min: Float, $max: Float, $default: Float, $array: Boolean){
gridsCreateFloatColumn(databaseId: $databaseId, tableId: $tableId, key: $key, min: $min, max: $max, required: $required, default: $default, array: $array) {
key
required
min
max
default
array
}
}';
case self::$CREATE_BOOLEAN_COLUMN:
return 'mutation createBooleanColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $default: Boolean, $array: Boolean){
gridsCreateBooleanColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, default: $default, array: $array) {
key
required
default
array
}
}';
case self::$CREATE_URL_COLUMN:
return 'mutation createUrlColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $default: String, $array: Boolean){
gridsCreateUrlColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, default: $default, array: $array) {
key
required
default
array
}
}';
case self::$CREATE_EMAIL_COLUMN:
return 'mutation createEmailColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $default: String, $array: Boolean){
gridsCreateEmailColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, default: $default, array: $array) {
key
required
default
array
}
}';
case self::$CREATE_IP_COLUMN:
return 'mutation createIpColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $default: String, $array: Boolean){
gridsCreateIpColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, default: $default, array: $array) {
key
required
default
array
}
}';
case self::$CREATE_ENUM_COLUMN:
return 'mutation createEnumColumn($databaseId: String!, $tableId: String!, $key: String!, $elements: [String!]!, $required: Boolean!, $default: String, $array: Boolean){
gridsCreateEnumColumn(databaseId: $databaseId, tableId: $tableId, key: $key, elements: $elements, required: $required, default: $default, array: $array) {
key
elements
required
default
array
}
}';
case self::$CREATE_DATETIME_COLUMN:
return 'mutation createDatetimeColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $default: String, $array: Boolean){
gridsCreateDatetimeColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, default: $default, array: $array) {
key
required
default
array
}
}';
case self::$CREATE_RELATIONSHIP_COLUMN:
return 'mutation createRelationshipColumn($databaseId: String!, $tableId: String!, $relatedTableId: String!, $type: String!, $twoWay: Boolean, $key: String, $twoWayKey: String, $onDelete: String){
gridsCreateRelationshipColumn(databaseId: $databaseId, tableId: $tableId, relatedTableId: $relatedTableId, type: $type, twoWay: $twoWay, key: $key, twoWayKey: $twoWayKey, onDelete: $onDelete) {
relatedTable
relationType
twoWay
key
twoWayKey
onDelete
}
}';
case self::$UPDATE_STRING_COLUMN:
return 'mutation updateStringColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $default: String){
gridsUpdateStringColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, default: $default) {
required
default
}
}';
case self::$UPDATE_INTEGER_COLUMN:
return 'mutation updateIntegerColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $min: Int!, $max: Int!, $default: Int){
gridsUpdateIntegerColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, min: $min, max: $max, default: $default) {
required
min
max
default
}
}';
case self::$UPDATE_FLOAT_COLUMN:
return 'mutation updateFloatColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $min: Float!, $max: Float!, $default: Float){
gridsUpdateFloatColumn(databaseId: $databaseId, tableId: $tableId, key: $key, min: $min, max: $max, required: $required, default: $default) {
required
min
max
default
}
}';
case self::$UPDATE_BOOLEAN_COLUMN:
return 'mutation updateBooleanColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $default: Boolean){
gridsUpdateBooleanColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, default: $default) {
required
default
}
}';
case self::$UPDATE_URL_COLUMN:
return 'mutation updateUrlColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $default: String){
gridsUpdateUrlColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, default: $default) {
required
default
}
}';
case self::$UPDATE_EMAIL_COLUMN:
return 'mutation updateEmailColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $default: String){
gridsUpdateEmailColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, default: $default) {
required
default
}
}';
case self::$UPDATE_IP_COLUMN:
return 'mutation updateIpColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $default: String){
gridsUpdateIpColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, default: $default) {
required
default
}
}';
case self::$UPDATE_ENUM_COLUMN:
return 'mutation updateEnumColumn($databaseId: String!, $tableId: String!, $key: String!, $elements: [String!]!, $required: Boolean!, $default: String){
gridsUpdateEnumColumn(databaseId: $databaseId, tableId: $tableId, key: $key, elements: $elements, required: $required, default: $default) {
elements
required
default
}
}';
case self::$UPDATE_DATETIME_COLUMN:
return 'mutation updateDatetimeColumn($databaseId: String!, $tableId: String!, $key: String!, $required: Boolean!, $default: String){
gridsUpdateDatetimeColumn(databaseId: $databaseId, tableId: $tableId, key: $key, required: $required, default: $default) {
required
default
}
}';
case self::$UPDATE_RELATIONSHIP_COLUMN:
return 'mutation updateRelationshipColumn($databaseId: String!, $tableId: String!, $key: String!, $onDelete: String){
gridsUpdateRelationshipColumn(databaseId: $databaseId, tableId: $tableId, key: $key, onDelete: $onDelete) {
relatedTable
relationType
twoWay
key
twoWayKey
onDelete
}
}';
case self::$CREATE_INDEX:
return 'mutation createIndex($databaseId: String!, $collectionId: String!, $key: String!, $type: String!, $attributes: [String!]!, $orders: [String!]){
databasesCreateIndex(databaseId: $databaseId, collectionId: $collectionId, key: $key, type: $type, attributes: $attributes, orders: $orders) {
@@ -647,6 +1053,39 @@ trait Base
status
}
}';
case self::$CREATE_COLUMN_INDEX:
return 'mutation createIndex($databaseId: String!, $tableId: String!, $key: String!, $type: String!, $columns: [String!]!, $orders: [String!]){
gridsCreateIndex(databaseId: $databaseId, tableId: $tableId, key: $key, type: $type, columns: $columns, orders: $orders) {
key
type
status
}
}';
case self::$GET_COLUMN_INDEXES:
return 'query listIndexes($databaseId: String!, $tableId: String!) {
gridsListIndexes(databaseId: $databaseId, tableId: $tableId) {
total
indexes {
key
type
status
}
}
}';
case self::$GET_COLUMN_INDEX:
return 'query getIndex($databaseId: String!, $tableId: String!, $key: String!) {
gridsGetIndex(databaseId: $databaseId, tableId: $tableId, key: $key) {
key
type
status
}
}';
case self::$DELETE_COLUMN_INDEX:
return 'mutation deleteIndex($databaseId: String!, $tableId: String!, $key: String!) {
gridsDeleteIndex(databaseId: $databaseId, tableId: $tableId, key: $key) {
status
}
}';
case self::$GET_ATTRIBUTES:
return 'query listAttributes($databaseId: String!, $collectionId: String!) {
databasesListAttributes(databaseId: $databaseId, collectionId: $collectionId) {
@@ -668,6 +1107,28 @@ trait Base
status
}
}';
case self::$GET_COLUMNS:
return 'query listColumns($databaseId: String!, $tableId: String!) {
gridsListColumns(databaseId: $databaseId, tableId: $tableId) {
total
columns {
...columnProperties
}
}
}' . PHP_EOL . self::$FRAGMENT_COLUMNS;
case self::$GET_COLUMN:
return 'query getColumn($databaseId: String!, $tableId: String!, $key: String!) {
gridsGetColumn(databaseId: $databaseId, tableId: $tableId, key: $key) {
...columnProperties
}
}' . PHP_EOL . self::$FRAGMENT_COLUMNS;
case self::$DELETE_COLUMN:
return 'mutation deleteColumn($databaseId: String!, $tableId: String!, $key: String!) {
gridsDeleteColumn(databaseId: $databaseId, tableId: $tableId, key: $key) {
status
}
}';
case self::$GET_DOCUMENT:
return 'query getDocument($databaseId: String!, $collectionId: String!, $documentId: String!) {
databasesGetDocument(databaseId: $databaseId, collectionId: $collectionId, documentId: $documentId) {
@@ -697,6 +1158,58 @@ trait Base
_permissions
}
}';
case self::$CREATE_DOCUMENTS:
return 'mutation createDocuments($databaseId: String!, $collectionId: String!, $documents: [Json!]!) {
databasesCreateDocuments(databaseId: $databaseId, collectionId: $collectionId, documents: $documents) {
documents {
_id
_collectionId
_permissions
data
}
}
}';
case self::$CREATE_ROWS:
return 'mutation createRows($databaseId: String!, $tableId: String!, $rows: [Json!]!) {
gridsCreateRows(databaseId: $databaseId, tableId: $tableId, rows: $rows) {
rows {
_id
_tableId
_permissions
data
}
}
}';
case self::$GET_ROW:
return 'query getRow($databaseId: String!, $tableId: String!, $rowId: String!) {
gridsGetRow(databaseId: $databaseId, tableId: $tableId, rowId: $rowId) {
_id
_tableId
_permissions
data
}
}';
case self::$GET_ROWS:
return 'query listRows($databaseId: String!, $tableId: String!, $queries: [String!] = []) {
gridsListRows(databaseId: $databaseId, tableId: $tableId, queries: $queries) {
total
rows {
_id
_databaseId
_tableId
_permissions
data
}
}
}';
case self::$CREATE_ROW:
return 'mutation createRow($databaseId: String!, $tableId: String!, $rowId: String!, $data: Json!, $permissions: [String!]) {
gridsCreateRow(databaseId: $databaseId, tableId: $tableId, rowId: $rowId, data: $data, permissions: $permissions) {
_id
_tableId
_permissions
}
}';
case self::$CREATE_CUSTOM_ENTITY:
return 'mutation createActor($name: String!, $age: Int!, $alive: Boolean!, $salary: Float, $email: String!, $role: String!, $dob: String!, $ip: String, $url: String){
actorsCreate(name: $name, age: $age, alive: $alive, salary: $salary, email: $email, role: $role, dob: $dob, ip: $ip, url: $url) {
@@ -764,13 +1277,120 @@ trait Base
data
}
}';
case self::$UPSERT_DOCUMENT:
return 'mutation upsertDocument($databaseId: String!, $collectionId: String!, $documentId: String!, $data: Json!, $permissions: [String!] = []) {
databasesUpsertDocument(databaseId: $databaseId, collectionId: $collectionId, documentId: $documentId, data: $data, permissions: $permissions) {
_id
_databaseId
_collectionId
data
}
}';
case self::$UPDATE_DOCUMENTS:
return 'mutation updateDocuments($databaseId: String!, $collectionId: String!, $data: Json!, $queries: [String!]) {
databasesUpdateDocuments(databaseId: $databaseId, collectionId: $collectionId, data: $data, queries: $queries) {
total
documents {
_id
_databaseId
_collectionId
_permissions
data
}
}
}';
case self::$UPSERT_DOCUMENTS:
return 'mutation upsertDocuments($databaseId: String!, $collectionId: String!, $documents: [Json!]!) {
databasesUpsertDocuments(databaseId: $databaseId, collectionId: $collectionId, documents: $documents) {
total
documents {
_id
_databaseId
_collectionId
_permissions
data
}
}
}';
case self::$DELETE_DOCUMENTS:
return 'mutation deleteDocuments($databaseId: String!, $collectionId: String!, $queries: [String!] = []) {
databasesDeleteDocuments(databaseId: $databaseId, collectionId: $collectionId, queries: $queries) {
total
documents {
_id
_databaseId
_collectionId
data
}
}
}';
case self::$DELETE_DOCUMENT:
return 'mutation deleteDocument($databaseId: String!, $collectionId: String!, $documentId: String!){
databasesDeleteDocument(databaseId: $databaseId, collectionId: $collectionId, documentId: $documentId) {
status
}
}';
case self::$UPDATE_ROW:
return 'mutation updateRow($databaseId: String!, $tableId: String!, $rowId: String!, $data: Json!, $permissions: [String!]) {
gridsUpdateRow(databaseId: $databaseId, tableId: $tableId, rowId: $rowId, data: $data, permissions: $permissions) {
_id
_tableId
data
}
}';
case self::$UPSERT_ROW:
return 'mutation upsertRow($databaseId: String!, $tableId: String!, $rowId: String!, $data: Json!, $permissions: [String!] = []) {
gridsUpsertRow(databaseId: $databaseId, tableId: $tableId, rowId: $rowId, data: $data, permissions: $permissions) {
_id
_databaseId
_tableId
data
}
}';
case self::$DELETE_ROW:
return 'mutation deleteRow($databaseId: String!, $tableId: String!, $rowId: String!) {
gridsDeleteRow(databaseId: $databaseId, tableId: $tableId, rowId: $rowId) {
status
}
}';
case self::$UPDATE_ROWS:
return 'mutation updateRows($databaseId: String!, $tableId: String!, $data: Json!, $queries: [String!]) {
gridsUpdateRows(databaseId: $databaseId, tableId: $tableId, data: $data, queries: $queries) {
total
rows {
_id
_databaseId
_tableId
_permissions
data
}
}
}';
case self::$UPSERT_ROWS:
return 'mutation upsertRows($databaseId: String!, $tableId: String!, $rows: [Json!]!) {
gridsUpsertRows(databaseId: $databaseId, tableId: $tableId, rows: $rows) {
total
rows {
_id
_databaseId
_tableId
_permissions
data
}
}
}';
case self::$DELETE_ROWS:
return 'mutation deleteRows($databaseId: String!, $tableId: String!, $queries: [String!] = []) {
gridsDeleteRows(databaseId: $databaseId, tableId: $tableId, queries: $queries) {
total
rows {
_id
_databaseId
_tableId
data
}
}
}';
case self::$GET_USER:
return 'query getUser($userId : String!) {
usersGet(userId : $userId) {
@@ -2245,7 +2865,7 @@ trait Base
status
}
}';
case self::$COMPLEX_QUERY:
case self::$COMPLEX_QUERY_COLLECTION:
return 'mutation complex($databaseId: String!, $databaseName: String!, $collectionId: String!, $collectionName: String!, $documentSecurity: Boolean!, $collectionPermissions: [String!]!) {
databasesCreate(databaseId: $databaseId, name: $databaseName) {
_id
@@ -2256,7 +2876,7 @@ trait Base
_createdAt
_updatedAt
_permissions
_databaseId
databaseId
name
documentSecurity
attributes {
@@ -2488,6 +3108,249 @@ trait Base
}
}
}' . PHP_EOL . self::$FRAGMENT_ATTRIBUTES;
case self::$COMPLEX_QUERY_TABLE:
return 'mutation complex($databaseId: String!, $databaseName: String!, $tableId: String!, $tableName: String!, $rowSecurity: Boolean!, $tablePermissions: [String!]!) {
databasesCreate(databaseId: $databaseId, name: $databaseName) {
_id
name
}
gridsCreate(databaseId: $databaseId, tableId: $tableId, name: $tableName, rowSecurity: $rowSecurity, permissions: $tablePermissions) {
_id
_createdAt
_updatedAt
_permissions
databaseId
name
rowSecurity
columns {
...columnProperties
}
indexes {
key
type
status
}
}
gridsCreateStringColumn(databaseId: $databaseId, tableId: $tableId, key: "name", size: 255, required: true) {
key
type
status
size
required
default
array
}
gridsCreateIntegerColumn(databaseId: $databaseId, tableId: $tableId, key: "age", min: 0, max: 150, required: true) {
key
type
status
required
min
max
default
array
}
gridsCreateBooleanColumn(databaseId: $databaseId, tableId: $tableId, key: "alive", required: false, default: true) {
key
type
status
required
default
array
}
user1: usersCreate(userId: "unique()", email: "test1@appwrite.io", password: "password", name: "Tester 1") {
_id
_createdAt
_updatedAt
name
phone
email
status
registration
passwordUpdate
emailVerification
phoneVerification
prefs {
data
}
}
user2: usersCreate(userId: "unique()", email: "test2@appwrite.io", password: "password", name: "Tester 2") {
_id
_createdAt
_updatedAt
name
phone
email
status
registration
passwordUpdate
emailVerification
phoneVerification
prefs {
data
}
}
user3: usersCreate(userId: "unique()", email: "test3@appwrite.io", password: "password", name: "Tester 3") {
_id
_createdAt
_updatedAt
name
phone
email
status
registration
passwordUpdate
emailVerification
phoneVerification
prefs {
data
}
}
user4: usersCreate(userId: "unique()", email: "test4@appwrite.io", password: "password", name: "Tester 4") {
_id
_createdAt
_updatedAt
name
phone
email
status
registration
passwordUpdate
emailVerification
phoneVerification
prefs {
data
}
}
user5: usersCreate(userId: "unique()", email: "test5@appwrite.io", password: "password", name: "Tester 5") {
_id
_createdAt
_updatedAt
name
phone
email
status
registration
passwordUpdate
emailVerification
phoneVerification
prefs {
data
}
}
user6: usersCreate(userId: "unique()", email: "test6@appwrite.io", password: "password", name: "Tester 6") {
_id
_createdAt
_updatedAt
name
phone
email
status
registration
passwordUpdate
emailVerification
phoneVerification
prefs {
data
}
}
user7: usersCreate(userId: "unique()", email: "test7@appwrite.io", password: "password", name: "Tester 7") {
_id
_createdAt
_updatedAt
name
phone
email
status
registration
passwordUpdate
emailVerification
phoneVerification
prefs {
data
}
}
user8: usersCreate(userId: "unique()", email: "test8@appwrite.io", password: "password", name: "Tester 8") {
_id
_createdAt
_updatedAt
name
phone
email
status
registration
passwordUpdate
emailVerification
phoneVerification
prefs {
data
}
}
user9: usersCreate(userId: "unique()", email: "test9@appwrite.io", password: "password", name: "Tester 9") {
_id
_createdAt
_updatedAt
name
phone
email
status
registration
passwordUpdate
emailVerification
phoneVerification
prefs {
data
}
}
user10: usersCreate(userId: "unique()", email: "test10@appwrite.io", password: "password", name: "Tester 10") {
_id
_createdAt
_updatedAt
name
phone
email
status
registration
passwordUpdate
emailVerification
phoneVerification
prefs {
data
}
}
user11: usersCreate(userId: "unique()", email: "test11@appwrite.io", password: "password", name: "Tester 11") {
_id
_createdAt
_updatedAt
name
phone
email
status
registration
passwordUpdate
emailVerification
phoneVerification
prefs {
data
}
}
user12: usersCreate(userId: "unique()", email: "test12@appwrite.io", password: "password", name: "Tester 5") {
_id
_createdAt
_updatedAt
name
phone
email
status
registration
passwordUpdate
emailVerification
phoneVerification
prefs {
data
}
}
}' . PHP_EOL . self::$FRAGMENT_COLUMNS;
}
throw new \InvalidArgumentException('Invalid query type');
@@ -26,8 +26,8 @@ class FunctionsClientTest extends Scope
'variables' => [
'functionId' => ID::unique(),
'name' => 'Test Function',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'execute' => [Role::any()->toString()],
]
];
@@ -96,7 +96,7 @@ class FunctionsClientTest extends Scope
'map' => \json_encode([
'code' => ["variables.code"]
]),
'code' => $this->packageFunction('php')
'code' => $this->packageFunction('basic')
];
$deployment = $this->client->call(Client::METHOD_POST, '/graphql', [
@@ -26,8 +26,8 @@ class FunctionsServerTest extends Scope
'variables' => [
'functionId' => ID::unique(),
'name' => 'Test Function',
'entrypoint' => 'index.php',
'runtime' => 'php-8.0',
'entrypoint' => 'index.js',
'runtime' => 'node-22',
'execute' => [Role::any()->toString()],
]
];
@@ -95,7 +95,7 @@ class FunctionsServerTest extends Scope
'map' => \json_encode([
'code' => ["variables.code"]
]),
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
];
$deployment = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
@@ -0,0 +1,185 @@
<?php
namespace Tests\E2E\Services\GraphQL\Grids;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Tests\E2E\Services\GraphQL\Base;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\System\System;
class AbuseTest extends Scope
{
use ProjectCustom;
use SideServer;
use Base;
protected function setUp(): void
{
parent::setUp();
if (System::getEnv('_APP_OPTIONS_ABUSE') === 'disabled') {
$this->markTestSkipped('Abuse is not enabled.');
}
}
public function testRateLimitEnforced()
{
$data = $this->createTable();
$databaseId = $data['databaseId'];
$tableId = $data['tableId'];
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_ROW);
$max = 120;
for ($i = 0; $i <= $max + 1; $i++) {
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $databaseId,
'tableId' => $tableId,
'rowId' => ID::unique(),
'data' => [
'name' => 'John Doe',
],
],
];
$response = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $gqlPayload);
if ($i < $max) {
$this->assertArrayNotHasKey('errors', $response['body']);
} else {
$this->assertArrayHasKey('errors', $response['body']);
}
}
}
public function testComplexQueryBlocked()
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$COMPLEX_QUERY_TABLE);
$graphQLPayload = [
'query' => $query,
'variables' => [
'userId' => 'user',
'email' => 'user@appwrite.io',
'password' => 'password',
'databaseId' => 'database',
'databaseName' => 'database',
'tableId' => 'table',
'tableName' => 'table',
'tablePermissions' => [
Permission::read(Role::users()),
Permission::create(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
'rowSecurity' => false,
],
];
$response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$max = System::getEnv('_APP_GRAPHQL_MAX_QUERY_COMPLEXITY', 250);
$this->assertEquals('Max query complexity should be ' . $max . ' but got 259.', $response['body']['errors'][0]['message']);
}
public function testTooManyQueriesBlocked()
{
$projectId = $this->getProject()['$id'];
$maxQueries = System::getEnv('_APP_GRAPHQL_MAX_QUERIES', 10);
$query = [];
for ($i = 0; $i <= $maxQueries + 1; $i++) {
$query[] = ['query' => $this->getQuery(self::$LIST_COUNTRIES)];
}
$response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $query);
$this->assertEquals('Too many queries.', $response['body']['message']);
}
private function createTable(): array
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_DATABASE);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => 'actors',
'name' => 'AbuseDatabase',
]
];
$response = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $gqlPayload);
$databaseId = $response['body']['data']['databasesCreate']['_id'];
$query = $this->getQuery(self::$CREATE_TABLE);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $databaseId,
'tableId' => 'actors',
'name' => 'Actors',
'rowSecurity' => false,
'permissions' => [
Permission::read(Role::any()),
Permission::write(Role::any()),
],
]
];
$response = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $gqlPayload);
$tableId = $response['body']['data']['gridsCreateTable']['_id'];
$query = $this->getQuery(self::$CREATE_STRING_COLUMN);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $databaseId,
'tableId' => $tableId,
'key' => 'name',
'size' => 256,
'required' => true,
]
];
$this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $gqlPayload);
sleep(2);
return [
'databaseId' => $databaseId,
'tableId' => $tableId,
];
}
}
@@ -0,0 +1,253 @@
<?php
namespace Tests\E2E\Services\GraphQL\Grids;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Tests\E2E\Services\GraphQL\Base;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
class AuthTest extends Scope
{
use ProjectCustom;
use SideClient;
use Base;
private array $account1;
private array $account2;
private string $token1;
private string $token2;
private array $database;
private array $table;
public function setUp(): void
{
parent::setUp();
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_ACCOUNT);
$email1 = 'test' . \rand() . '@test.com';
$email2 = 'test' . \rand() . '@test.com';
// Create account 1
$graphQLPayload = [
'query' => $query,
'variables' => [
'userId' => ID::unique(),
'name' => 'User Name',
'email' => $email1,
'password' => 'password',
],
];
$this->account1 = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $graphQLPayload);
// Create account 2
$graphQLPayload['variables']['userId'] = ID::unique();
$graphQLPayload['variables']['email'] = $email2;
$this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $graphQLPayload);
// Create session 1
$query = $this->getQuery(self::$CREATE_ACCOUNT_SESSION);
$graphQLPayload = [
'query' => $query,
'variables' => [
'email' => $email1,
'password' => 'password',
]
];
$session1 = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $graphQLPayload);
$this->token1 = $session1['cookies']['a_session_' . $projectId];
// Create session 2
$graphQLPayload['variables']['email'] = $email2;
$session2 = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $graphQLPayload);
$this->token2 = $session2['cookies']['a_session_' . $projectId];
// Create database
$query = $this->getQuery(self::$CREATE_DATABASE);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => ID::unique(),
'name' => 'Actors',
]
];
$this->database = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $gqlPayload);
// Create table
$query = $this->getQuery(self::$CREATE_TABLE);
$userId = $this->account1['body']['data']['accountCreate']['_id'];
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $this->database['body']['data']['databasesCreate']['_id'],
'tableId' => ID::unique(),
'name' => 'Actors',
'rowSecurity' => true,
'permissions' => [
Permission::create(Role::user($userId))
]
]
];
$this->table = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $gqlPayload);
// Create string attribute
$query = $this->getQuery(self::$CREATE_STRING_COLUMN);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $this->database['body']['data']['databasesCreate']['_id'],
'tableId' => $this->table['body']['data']['gridsCreateTable']['_id'],
'key' => 'name',
'size' => 256,
'required' => true,
]
];
$this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $gqlPayload);
sleep(1);
}
public function testInvalidAuth()
{
$projectId = $this->getProject()['$id'];
// Create row as account 1
$query = $this->getQuery(self::$CREATE_ROW);
$userId = $this->account1['body']['data']['accountCreate']['_id'];
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $this->database['body']['data']['databasesCreate']['_id'],
'tableId' => $this->table['body']['data']['gridsCreateTable']['_id'],
'rowId' => ID::unique(),
'data' => [
'name' => 'John Doe',
],
'permissions' => [
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
]
]
];
$row = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => 'a_session_' . $projectId . '=' . $this->token1,
], $gqlPayload);
// Try to read as account 1
$query = $this->getQuery(self::$GET_ROW);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $this->database['body']['data']['databasesCreate']['_id'],
'tableId' => $this->table['body']['data']['gridsCreateTable']['_id'],
'rowId' => $row['body']['data']['gridsCreateRow']['_id'],
]
];
$row = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => 'a_session_' . $projectId . '=' . $this->token1,
], $gqlPayload);
$this->assertIsArray($row['body']['data']['gridsGetRow']);
$this->assertArrayNotHasKey('errors', $row['body']);
// Try to read as account 2
$row = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => 'a_session_' . $projectId . '=' . $this->token2,
], $gqlPayload);
$this->assertArrayHasKey('errors', $row['body']);
$this->assertEquals('Row with the requested ID could not be found.', $row['body']['errors'][0]['message']);
}
public function testValidAuth()
{
$projectId = $this->getProject()['$id'];
// Create row as account 1
$query = $this->getQuery(self::$CREATE_ROW);
$userId = $this->account1['body']['data']['accountCreate']['_id'];
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $this->database['body']['data']['databasesCreate']['_id'],
'tableId' => $this->table['body']['data']['gridsCreateTable']['_id'],
'rowId' => ID::unique(),
'data' => [
'name' => 'John Doe',
],
'permissions' => [
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
],
]
];
$row = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => 'a_session_' . $projectId . '=' . $this->token1,
], $gqlPayload);
// Try to delete as account 1
$query = $this->getQuery(self::$DELETE_ROW);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $this->database['body']['data']['databasesCreate']['_id'],
'tableId' => $this->table['body']['data']['gridsCreateTable']['_id'],
'rowId' => $row['body']['data']['gridsCreateRow']['_id'],
]
];
$row = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'cookie' => 'a_session_' . $projectId . '=' . $this->token1,
], $gqlPayload);
$this->assertIsNotArray($row['body']);
$this->assertEquals(204, $row['headers']['status-code']);
}
}
@@ -0,0 +1,589 @@
<?php
namespace Tests\E2E\Services\GraphQL\Grids;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Tests\E2E\Services\GraphQL\Base;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
class DatabaseClientTest extends Scope
{
use ProjectCustom;
use SideClient;
use Base;
public function testCreateDatabase(): array
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$GRIDS_CREATE_DATABASE);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => ID::unique(),
'name' => 'Actors',
]
];
$database = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $gqlPayload);
$this->assertIsArray($database['body']['data']);
$this->assertArrayNotHasKey('errors', $database['body']);
$database = $database['body']['data']['gridsCreateDatabase'];
$this->assertEquals('Actors', $database['name']);
return $database;
}
/**
* @depends testCreateDatabase
*/
public function testCreateTable($database): array
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_TABLE);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $database['_id'],
'tableId' => 'actors',
'name' => 'Actors',
'rowSecurity' => false,
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::users()),
Permission::update(Role::users()),
Permission::delete(Role::users()),
],
]
];
$table = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $gqlPayload);
$this->assertIsArray($table['body']['data']);
$this->assertArrayNotHasKey('errors', $table['body']);
$table = $table['body']['data']['gridsCreateTable'];
$this->assertEquals('Actors', $table['name']);
return [
'table' => $table,
'database' => $database,
];
}
/**
* @depends testCreateTable
*/
public function testCreateStringColumn($data): array
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_STRING_COLUMN);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'tableId' => $data['table']['_id'],
'key' => 'name',
'size' => 256,
'required' => true,
]
];
$column = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $gqlPayload);
$this->assertArrayNotHasKey('errors', $column['body']);
$this->assertIsArray($column['body']['data']);
$this->assertIsArray($column['body']['data']['gridsCreateStringColumn']);
return $data;
}
/**
* @depends testCreateTable
*/
public function testCreateIntegerColumn($data): array
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_INTEGER_COLUMN);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'tableId' => $data['table']['_id'],
'key' => 'age',
'min' => 18,
'max' => 150,
'required' => true,
]
];
$column = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $this->getProject()['apiKey'],
], $gqlPayload);
$this->assertArrayNotHasKey('errors', $column['body']);
$this->assertIsArray($column['body']['data']);
$this->assertIsArray($column['body']['data']['gridsCreateIntegerColumn']);
return $data;
}
/**
* @depends testCreateStringColumn
* @depends testCreateIntegerColumn
*/
public function testCreateRow($data): array
{
sleep(1);
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_ROW);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'tableId' => $data['table']['_id'],
'rowId' => ID::unique(),
'data' => [
'name' => 'John Doe',
'age' => 35,
],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]
];
$row = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertArrayNotHasKey('errors', $row['body']);
$this->assertIsArray($row['body']['data']);
$row = $row['body']['data']['gridsCreateRow'];
$this->assertIsArray($row);
return [
'database' => $data['database'],
'table' => $data['table'],
'row' => $row,
];
}
/**
* @depends testCreateTable
* @throws \Exception
*/
public function testGetRows($data): void
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$GET_ROWS);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'tableId' => $data['table']['_id'],
]
];
$rows = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertArrayNotHasKey('errors', $rows['body']);
$this->assertIsArray($rows['body']['data']);
$this->assertIsArray($rows['body']['data']['gridsListRows']);
}
/**
* @depends testCreateRow
* @throws \Exception
*/
public function testGetDocument($data): void
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$GET_ROW);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'tableId' => $data['table']['_id'],
'rowId' => $data['row']['_id'],
]
];
$row = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertArrayNotHasKey('errors', $row['body']);
$this->assertIsArray($row['body']['data']);
$this->assertIsArray($row['body']['data']['gridsGetRow']);
}
/**
* @depends testCreateRow
* @throws \Exception
*/
public function testUpdateRow($data): void
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$UPDATE_ROW);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'tableId' => $data['table']['_id'],
'rowId' => $data['row']['_id'],
'data' => [
'name' => 'New Row Name',
],
]
];
$row = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertArrayNotHasKey('errors', $row['body']);
$this->assertIsArray($row['body']['data']);
$row = $row['body']['data']['gridsUpdateRow'];
$this->assertIsArray($row);
$this->assertStringContainsString('New Row Name', $row['data']);
}
/**
* @depends testCreateRow
* @throws \Exception
*/
public function testDeleteRow($data): void
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$DELETE_ROW);
$gqlPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['database']['_id'],
'tableId' => $data['table']['_id'],
'rowId' => $data['row']['_id'],
]
];
$row = $this->client->call(Client::METHOD_POST, '/graphql', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
$this->assertIsNotArray($row['body']);
$this->assertEquals(204, $row['headers']['status-code']);
}
/**
* @throws \Exception
*/
public function testBulkCreate(): array
{
$project = $this->getProject();
$projectId = $project['$id'];
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $project['apiKey'],
];
// Step 1: Create database
$query = $this->getQuery(self::$GRIDS_CREATE_DATABASE);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => 'bulk',
'name' => 'Bulk',
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$databaseId = $res['body']['data']['gridsCreateDatabase']['_id'];
// Step 2: Create table
$query = $this->getQuery(self::$CREATE_TABLE);
$payload['query'] = $query;
$payload['variables'] = [
'databaseId' => $databaseId,
'tableId' => 'operations',
'name' => 'Operations',
'rowSecurity' => false,
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$tableId = $res['body']['data']['gridsCreateTable']['_id'];
// Step 3: Create column
$query = $this->getQuery(self::$CREATE_STRING_COLUMN);
$payload['query'] = $query;
$payload['variables'] = [
'databaseId' => $databaseId,
'tableId' => $tableId,
'key' => 'name',
'size' => 256,
'required' => true,
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
sleep(1);
// Step 4: Create rows
$query = $this->getQuery(self::$CREATE_ROWS);
$rows = [];
for ($i = 1; $i <= 10; $i++) {
$rows[] = ['$id' => 'row' . $i, 'name' => 'Row #' . $i];
}
$payload['query'] = $query;
$payload['variables'] = [
'databaseId' => $databaseId,
'tableId' => $tableId,
'rows' => $rows,
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertCount(10, $res['body']['data']['gridsCreateRows']['rows']);
return compact('databaseId', 'tableId', 'projectId');
}
/**
* @depends testBulkCreate
*/
public function testBulkUpdate(array $data): array
{
$userId = $this->getUser()['$id'];
$permissions = [
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
];
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
// Step 1: Bulk update rows
$query = $this->getQuery(self::$UPDATE_ROWS);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'tableId' => $data['tableId'],
'data' => [
'name' => 'Rows Updated',
'$permissions' => $permissions,
],
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertCount(10, $res['body']['data']['gridsUpdateRows']['rows']);
// Step 2: Fetch and validate updated rows
$query = $this->getQuery(self::$GET_ROWS);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'tableId' => $data['tableId'],
'queries' => [Query::equal('name', ['Rows Updated'])->toString()],
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertEquals(200, $res['headers']['status-code']);
$fetched = $res['body']['data']['gridsListRows'];
$this->assertEquals(10, $fetched['total']);
foreach ($fetched['rows'] as $row) {
$this->assertEquals($permissions, $row['_permissions']);
$this->assertEquals($data['tableId'], $row['_tableId']);
$this->assertEquals($data['databaseId'], $row['_databaseId']);
$this->assertEquals('Rows Updated', json_decode($row['data'], true)['name']);
}
return $data;
}
/**
* @depends testBulkCreate
*/
public function testBulkUpsert(array $data): array
{
$userId = $this->getUser()['$id'];
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
$permissions = [
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
];
// Step 1: Mutate row 10 and add row 11
$query = $this->getQuery(self::$UPSERT_ROWS);
$upsertPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'tableId' => $data['tableId'],
'rows' => [
[
'$id' => 'row10',
'name' => 'Row #1000',
],
[
'name' => 'Row #11',
],
],
],
];
$response = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $upsertPayload);
$this->assertArrayNotHasKey('errors', $response['body']);
$rows = $response['body']['data']['gridsUpsertRows']['rows'];
$this->assertCount(2, $rows);
$rowMap = [];
foreach ($rows as $row) {
$decoded = json_decode($row['data'], true);
$rowMap[$decoded['name']] = $decoded;
}
$this->assertArrayHasKey('Row #1000', $rowMap);
$this->assertArrayHasKey('Row #11', $rowMap);
// Step 2: Fetch all rows and confirm count is now 11
$query = $this->getQuery(self::$GET_ROWS);
$fetchPayload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'tableId' => $data['tableId'],
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $fetchPayload);
$this->assertEquals(200, $res['headers']['status-code']);
$fetched = $res['body']['data']['gridsListRows'];
$this->assertEquals(11, $fetched['total']);
// Step 3: Upsert row with new permissions using `tablesUpsertRow`
$query = $this->getQuery(self::$UPSERT_ROW);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'tableId' => $data['tableId'],
'rowId' => 'row10',
'data' => ['name' => 'Row #10 Patched'],
'permissions' => $permissions,
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$updated = $res['body']['data']['gridsUpsertRow'];
$this->assertEquals('Row #10 Patched', json_decode($updated['data'], true)['name']);
$this->assertEquals($data['databaseId'], $updated['_databaseId']);
$this->assertEquals($data['tableId'], $updated['_tableId']);
return $data;
}
/**
* @depends testBulkUpsert
*/
public function testBulkDelete(array $data): array
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
// Step 1: Perform bulk delete
$query = $this->getQuery(self::$DELETE_ROWS);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'tableId' => $data['tableId'],
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$deleted = $res['body']['data']['gridsDeleteRows']['rows'];
$this->assertIsArray($deleted);
$this->assertCount(11, $deleted);
// Step 2: Confirm deletion via refetch
$query = $this->getQuery(self::$GET_ROWS);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'tableId' => $data['tableId'],
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertEquals(200, $res['headers']['status-code']);
$this->assertEquals(0, $res['body']['data']['gridsListRows']['total']);
return $data;
}
}
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,12 @@
<?php
namespace Tests\E2E\Services\GraphQL;
namespace Tests\E2E\Services\GraphQL\Legacy;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Tests\E2E\Services\GraphQL\Base;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
@@ -64,7 +65,7 @@ class AbuseTest extends Scope
public function testComplexQueryBlocked()
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$COMPLEX_QUERY);
$query = $this->getQuery(self::$COMPLEX_QUERY_COLLECTION);
$graphQLPayload = [
'query' => $query,
'variables' => [
@@ -113,7 +114,7 @@ class AbuseTest extends Scope
$this->assertEquals('Too many queries.', $response['body']['message']);
}
private function createCollection()
private function createCollection(): array
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_DATABASE);
@@ -1,11 +1,12 @@
<?php
namespace Tests\E2E\Services\GraphQL;
namespace Tests\E2E\Services\GraphQL\Legacy;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Tests\E2E\Services\GraphQL\Base;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
@@ -1,11 +1,12 @@
<?php
namespace Tests\E2E\Services\GraphQL;
namespace Tests\E2E\Services\GraphQL\Legacy;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Tests\E2E\Services\GraphQL\Base;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
@@ -302,4 +303,180 @@ class DatabaseClientTest extends Scope
$this->assertIsNotArray($document['body']);
$this->assertEquals(204, $document['headers']['status-code']);
}
/**
* @throws \Exception
*/
public function testBulkCreateDocuments(): array
{
$project = $this->getProject();
$projectId = $project['$id'];
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $project['apiKey'],
];
// Step 1: Create database
$query = $this->getQuery(self::$CREATE_DATABASE);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => 'bulk',
'name' => 'Bulk',
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$databaseId = $res['body']['data']['databasesCreate']['_id'];
// Step 2: Create collection
$query = $this->getQuery(self::$CREATE_COLLECTION);
$payload['query'] = $query;
$payload['variables'] = [
'databaseId' => $databaseId,
'collectionId' => 'operations',
'name' => 'Operations',
'documentSecurity' => false,
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$collectionId = $res['body']['data']['databasesCreateCollection']['_id'];
// Step 3: Create attribute
$query = $this->getQuery(self::$CREATE_STRING_ATTRIBUTE);
$payload['query'] = $query;
$payload['variables'] = [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'key' => 'name',
'size' => 256,
'required' => true,
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
sleep(1);
// Step 4: Create documents
$query = $this->getQuery(self::$CREATE_DOCUMENTS);
$documents = [];
for ($i = 1; $i <= 10; $i++) {
$documents[] = ['$id' => 'doc' . $i, 'name' => 'Doc #' . $i];
}
$payload['query'] = $query;
$payload['variables'] = [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'documents' => $documents,
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertCount(10, $res['body']['data']['databasesCreateDocuments']['documents']);
return [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'projectId' => $projectId,
];
}
/**
* @depends testBulkCreateDocuments
*/
public function testBulkUpdateDocuments(array $data): array
{
$userId = $this->getUser()['$id'];
$permissions = [
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
];
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
$query = $this->getQuery(self::$UPDATE_DOCUMENTS);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'collectionId' => $data['collectionId'],
'data' => [
'name' => 'Docs Updated',
'$permissions' => $permissions,
],
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertCount(10, $res['body']['data']['databasesUpdateDocuments']['documents']);
return $data;
}
/**
* @depends testBulkUpdateDocuments
*/
public function testBulkUpsertDocuments(array $data): array
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
// Upsert: Update one, insert one
$query = $this->getQuery(self::$UPSERT_DOCUMENTS);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'collectionId' => $data['collectionId'],
'documents' => [
['$id' => 'doc10', 'name' => 'Doc #1000'],
['name' => 'Doc #11'],
],
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertCount(2, $res['body']['data']['databasesUpsertDocuments']['documents']);
return $data;
}
/**
* @depends testBulkUpsertDocuments
*/
public function testBulkDeleteDocuments(array $data): array
{
$headers = [
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
$query = $this->getQuery(self::$DELETE_DOCUMENTS);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'collectionId' => $data['collectionId'],
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertCount(11, $res['body']['data']['databasesDeleteDocuments']['documents']);
return $data;
}
}
@@ -1,12 +1,13 @@
<?php
namespace Tests\E2E\Services\GraphQL;
namespace Tests\E2E\Services\GraphQL\Legacy;
use Exception;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Tests\E2E\Services\GraphQL\Base;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
@@ -133,6 +134,7 @@ class DatabaseServerTest extends Scope
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $gqlPayload);
// TODO: @itznotabug - check for `encrypt` attribute in string column's response body as well!
$this->assertArrayNotHasKey('errors', $attribute['body']);
$this->assertIsArray($attribute['body']['data']);
$this->assertIsArray($attribute['body']['data']['databasesCreateStringAttribute']);
@@ -1463,4 +1465,176 @@ class DatabaseServerTest extends Scope
$this->assertIsNotArray($database['body']);
$this->assertEquals(204, $database['headers']['status-code']);
}
/**
* @throws Exception
*/
public function testBulkCreateDocuments(): array
{
$project = $this->getProject();
$projectId = $project['$id'];
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders());
// Step 1: Create database
$query = $this->getQuery(self::$CREATE_DATABASE);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => 'bulk',
'name' => 'Bulk',
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$databaseId = $res['body']['data']['databasesCreate']['_id'];
// Step 2: Create collection
$query = $this->getQuery(self::$CREATE_COLLECTION);
$payload['query'] = $query;
$payload['variables'] = [
'databaseId' => $databaseId,
'collectionId' => 'operations',
'name' => 'Operations',
'documentSecurity' => false,
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$collectionId = $res['body']['data']['databasesCreateCollection']['_id'];
// Step 3: Create attribute
$query = $this->getQuery(self::$CREATE_STRING_ATTRIBUTE);
$payload['query'] = $query;
$payload['variables'] = [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'key' => 'name',
'size' => 256,
'required' => true,
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
sleep(1);
// Step 4: Create documents
$query = $this->getQuery(self::$CREATE_DOCUMENTS);
$documents = [];
for ($i = 1; $i <= 10; $i++) {
$documents[] = ['$id' => 'doc' . $i, 'name' => 'Doc #' . $i];
}
$payload['query'] = $query;
$payload['variables'] = [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'documents' => $documents,
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertCount(10, $res['body']['data']['databasesCreateDocuments']['documents']);
return [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'projectId' => $projectId,
];
}
/**
* @depends testBulkCreateDocuments
*/
public function testBulkUpdateDocuments(array $data): array
{
$userId = $this->getUser()['$id'];
$permissions = [
Permission::read(Role::user($userId)),
Permission::update(Role::user($userId)),
Permission::delete(Role::user($userId)),
];
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
], $this->getHeaders());
$query = $this->getQuery(self::$UPDATE_DOCUMENTS);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'collectionId' => $data['collectionId'],
'data' => [
'name' => 'Docs Updated',
'$permissions' => $permissions,
],
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertCount(10, $res['body']['data']['databasesUpdateDocuments']['documents']);
return $data;
}
/**
* @depends testBulkUpdateDocuments
*/
public function testBulkUpsertDocuments(array $data): array
{
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
], $this->getHeaders());
// Upsert: Update one, insert one
$query = $this->getQuery(self::$UPSERT_DOCUMENTS);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'collectionId' => $data['collectionId'],
'documents' => [
['$id' => 'doc10', 'name' => 'Doc #1000'],
['name' => 'Doc #11'],
],
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertCount(2, $res['body']['data']['databasesUpsertDocuments']['documents']);
return $data;
}
/**
* @depends testBulkUpsertDocuments
*/
public function testBulkDeleteDocuments(array $data): array
{
$headers = array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $data['projectId'],
], $this->getHeaders());
$query = $this->getQuery(self::$DELETE_DOCUMENTS);
$payload = [
'query' => $query,
'variables' => [
'databaseId' => $data['databaseId'],
'collectionId' => $data['collectionId'],
],
];
$res = $this->client->call(Client::METHOD_POST, '/graphql', $headers, $payload);
$this->assertArrayNotHasKey('errors', $res['body']);
$this->assertCount(11, $res['body']['data']['databasesDeleteDocuments']['documents']);
return $data;
}
}
@@ -13,7 +13,6 @@ use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Migration\Resource;
use Utopia\Migration\Sources\Appwrite;
use Utopia\Migration\Sources\CSV;
trait MigrationsBase
{
@@ -436,25 +435,25 @@ trait MigrationsBase
/**
* @depends testAppwriteMigrationDatabase
*/
public function testAppwriteMigrationDatabasesCollection(array $data): array
public function testAppwriteMigrationDatabasesTable(array $data): array
{
$databaseId = $data['databaseId'];
$collection = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', [
$table = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'collectionId' => ID::unique(),
'name' => 'Test Collection',
'tableId' => ID::unique(),
'name' => 'Test Table',
]);
$this->assertEquals(201, $collection['headers']['status-code']);
$this->assertEquals(201, $table['headers']['status-code']);
$collectionId = $collection['body']['$id'];
$tableId = $table['body']['$id'];
// Create Attribute
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', [
// Create Column
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/string', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
@@ -467,9 +466,9 @@ trait MigrationsBase
$this->assertEquals(202, $response['headers']['status-code']);
// Wait for attribute to be ready
$this->assertEventually(function () use ($databaseId, $collectionId) {
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/name', [
// Wait for column to be ready
$this->assertEventually(function () use ($databaseId, $tableId) {
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/name', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
@@ -482,8 +481,8 @@ trait MigrationsBase
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_DATABASE,
Resource::TYPE_COLLECTION,
Resource::TYPE_ATTRIBUTE,
Resource::TYPE_TABLE,
Resource::TYPE_COLUMN,
],
'endpoint' => 'http://localhost/v1',
'projectId' => $this->getProject()['$id'],
@@ -491,9 +490,9 @@ trait MigrationsBase
]);
$this->assertEquals('completed', $result['status']);
$this->assertEquals([Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_ATTRIBUTE], $result['resources']);
$this->assertEquals([Resource::TYPE_DATABASE, Resource::TYPE_TABLE, Resource::TYPE_COLUMN], $result['resources']);
foreach ([Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_ATTRIBUTE] as $resource) {
foreach ([Resource::TYPE_DATABASE, Resource::TYPE_TABLE, Resource::TYPE_COLUMN] as $resource) {
$this->assertArrayHasKey($resource, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][$resource]['error']);
$this->assertEquals(0, $result['statusCounters'][$resource]['pending']);
@@ -502,7 +501,7 @@ trait MigrationsBase
$this->assertEquals(0, $result['statusCounters'][$resource]['warning']);
}
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId, [
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
@@ -511,10 +510,10 @@ trait MigrationsBase
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertEquals($collectionId, $response['body']['$id']);
$this->assertEquals('Test Collection', $response['body']['name']);
$this->assertEquals($tableId, $response['body']['$id']);
$this->assertEquals('Test Table', $response['body']['name']);
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/name', [
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/name', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
@@ -536,41 +535,41 @@ trait MigrationsBase
return [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'tableId' => $tableId,
];
}
/**
* @depends testAppwriteMigrationDatabasesCollection
* @depends testAppwriteMigrationDatabasesTable
*/
public function testAppwriteMigrationDatabasesDocument(array $data): void
public function testAppwriteMigrationDatabasesRow(array $data): void
{
$table = $data['tableId'];
$databaseId = $data['databaseId'];
$collectionId = $data['collectionId'];
$document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents', [
$row = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $table . '/rows', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'documentId' => ID::unique(),
'rowId' => ID::unique(),
'data' => [
'name' => 'Test Document',
'name' => 'Test Row',
]
]);
$this->assertEquals(201, $document['headers']['status-code']);
$this->assertNotEmpty($document['body']);
$this->assertNotEmpty($document['body']['$id']);
$this->assertEquals(201, $row['headers']['status-code']);
$this->assertNotEmpty($row['body']);
$this->assertNotEmpty($row['body']['$id']);
$documentId = $document['body']['$id'];
$rowId = $row['body']['$id'];
$result = $this->performMigrationSync([
'resources' => [
Resource::TYPE_DATABASE,
Resource::TYPE_COLLECTION,
Resource::TYPE_ATTRIBUTE,
Resource::TYPE_DOCUMENT,
Resource::TYPE_TABLE,
Resource::TYPE_COLUMN,
Resource::TYPE_ROW,
],
'endpoint' => 'http://localhost/v1',
'projectId' => $this->getProject()['$id'],
@@ -586,10 +585,10 @@ trait MigrationsBase
]);
$this->assertEquals('completed', $result['status']);
$this->assertEquals([Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_ATTRIBUTE, Resource::TYPE_DOCUMENT], $result['resources']);
$this->assertEquals([Resource::TYPE_DATABASE, Resource::TYPE_TABLE, Resource::TYPE_COLUMN, Resource::TYPE_ROW], $result['resources']);
//TODO: Add TYPE_DOCUMENT to the migration status counters once pending issue is resolved
foreach ([Resource::TYPE_DATABASE, Resource::TYPE_COLLECTION, Resource::TYPE_ATTRIBUTE] as $resource) {
// TODO: Add TYPE_ROW to the migration status counters once pending issue is resolved
foreach ([Resource::TYPE_DATABASE, Resource::TYPE_TABLE, Resource::TYPE_COLUMN] as $resource) {
$this->assertArrayHasKey($resource, $result['statusCounters']);
$this->assertEquals(0, $result['statusCounters'][$resource]['error']);
$this->assertEquals(0, $result['statusCounters'][$resource]['pending']);
@@ -598,7 +597,7 @@ trait MigrationsBase
$this->assertEquals(0, $result['statusCounters'][$resource]['warning']);
}
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, [
$response = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/grids/tables/' . $table . '/rows/' . $rowId, [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getDestinationProject()['$id'],
'x-appwrite-key' => $this->getDestinationProject()['apiKey'],
@@ -607,8 +606,8 @@ trait MigrationsBase
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']);
$this->assertEquals($documentId, $response['body']['$id']);
$this->assertEquals('Test Document', $response['body']['name']);
$this->assertEquals($rowId, $response['body']['$id']);
$this->assertEquals('Test Row', $response['body']['name']);
// Cleanup
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId, [
@@ -805,13 +804,12 @@ trait MigrationsBase
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php'
'runtime' => 'node-22',
'entrypoint' => 'index.js'
]);
$deploymentId = $this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -855,8 +853,8 @@ trait MigrationsBase
$this->assertEquals($functionId, $response['body']['$id']);
$this->assertEquals('Test', $response['body']['name']);
$this->assertEquals('php-8.0', $response['body']['runtime']);
$this->assertEquals('index.php', $response['body']['entrypoint']);
$this->assertEquals('node-22', $response['body']['runtime']);
$this->assertEquals('index.js', $response['body']['entrypoint']);
$this->assertEventually(function () use ($functionId) {
@@ -920,23 +918,23 @@ trait MigrationsBase
$databaseId = $response['body']['$id'];
// make a collection
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
// make a table
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'Test collection',
'collectionId' => ID::unique(),
'name' => 'Test table',
'tableId' => ID::unique(),
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertEquals($response['body']['name'], 'Test collection');
$this->assertEquals($response['body']['name'], 'Test table');
$collectionId = $response['body']['$id'];
$tableId = $response['body']['$id'];
// make attributes
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/string', array_merge([
// make columns
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
@@ -952,7 +950,7 @@ trait MigrationsBase
$this->assertEquals($response['body']['size'], 256);
$this->assertEquals($response['body']['required'], true);
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collectionId . '/attributes/integer', array_merge([
$response = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $tableId . '/columns/integer', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
@@ -1026,16 +1024,16 @@ trait MigrationsBase
$fileIds[$label] = $response['body']['$id'];
}
// missing attribute, fail in worker.
// missing column, fail in worker.
$missingColumn = $this->performCsvMigration(
[
'fileId' => $fileIds['missing-column'],
'bucketId' => $bucketIds['missing-column'],
'resourceId' => $databaseId . ':' . $collectionId,
'resourceId' => $databaseId . ':' . $tableId,
]
);
$this->assertEventually(function () use ($missingColumn, $databaseId, $collectionId) {
$this->assertEventually(function () use ($missingColumn, $databaseId, $tableId) {
$migrationId = $missingColumn['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@@ -1047,11 +1045,15 @@ trait MigrationsBase
$this->assertEquals('failed', $migration['body']['status']);
$this->assertEquals('CSV', $migration['body']['source']);
$this->assertEquals('Appwrite', $migration['body']['destination']);
$this->assertContains(Resource::TYPE_DOCUMENT, $migration['body']['resources']);
$this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']);
$this->assertEmpty($migration['body']['statusCounters']);
$errorJson = $migration['body']['errors'][0];
$errorData = json_decode($errorJson, true);
$this->assertThat(
implode("\n", $migration['body']['errors']),
$this->stringContains("CSV header mismatch. Missing attribute: 'age'")
$errorData['message'],
$this->stringContains("CSV header mismatch. Missing column: 'age'")
);
}, 60000, 500);
@@ -1060,11 +1062,11 @@ trait MigrationsBase
[
'fileId' => $fileIds['missing-row'],
'bucketId' => $bucketIds['missing-row'],
'resourceId' => $databaseId . ':' . $collectionId,
'resourceId' => $databaseId . ':' . $tableId,
]
);
$this->assertEventually(function () use ($missingColumn, $databaseId, $collectionId) {
$this->assertEventually(function () use ($missingColumn, $databaseId, $tableId) {
$migrationId = $missingColumn['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@@ -1076,10 +1078,14 @@ trait MigrationsBase
$this->assertEquals('failed', $migration['body']['status']);
$this->assertEquals('CSV', $migration['body']['source']);
$this->assertEquals('Appwrite', $migration['body']['destination']);
$this->assertContains(Resource::TYPE_DOCUMENT, $migration['body']['resources']);
$this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']);
$this->assertEmpty($migration['body']['statusCounters']);
$errorJson = $migration['body']['errors'][0];
$errorData = json_decode($errorJson, true);
$this->assertThat(
implode("\n", $migration['body']['errors']),
$errorData['message'],
$this->stringContains('CSV row does not match the number of header columns')
);
}, 60000, 500);
@@ -1089,11 +1095,11 @@ trait MigrationsBase
[
'fileId' => $fileIds['irrelevant-column'],
'bucketId' => $bucketIds['irrelevant-column'],
'resourceId' => $databaseId . ':' . $collectionId,
'resourceId' => $databaseId . ':' . $tableId,
]
);
$this->assertEventually(function () use ($irrelevantColumn, $databaseId, $collectionId) {
$this->assertEventually(function () use ($irrelevantColumn, $databaseId, $tableId) {
$migrationId = $irrelevantColumn['body']['$id'];
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
@@ -1105,11 +1111,15 @@ trait MigrationsBase
$this->assertEquals('failed', $migration['body']['status']);
$this->assertEquals('CSV', $migration['body']['source']);
$this->assertEquals('Appwrite', $migration['body']['destination']);
$this->assertContains(Resource::TYPE_DOCUMENT, $migration['body']['resources']);
$this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']);
$this->assertEmpty($migration['body']['statusCounters']);
$errorJson = $migration['body']['errors'][0];
$errorData = json_decode($errorJson, true);
$this->assertThat(
implode("\n", $migration['body']['errors']),
$this->stringContains("CSV header mismatch. Unexpected attribute: 'email'")
$errorData['message'],
$this->stringContains("CSV header mismatch. Unexpected column: 'email'")
);
}, 60000, 500);
@@ -1119,7 +1129,7 @@ trait MigrationsBase
'endpoint' => 'http://localhost/v1',
'fileId' => $fileIds['default'],
'bucketId' => $bucketIds['default'],
'resourceId' => $databaseId . ':' . $collectionId,
'resourceId' => $databaseId . ':' . $tableId,
]
);
@@ -1127,11 +1137,11 @@ trait MigrationsBase
$this->assertEquals('CSV', $migration['body']['source']);
$this->assertEquals('pending', $migration['body']['status']);
$this->assertEquals('Appwrite', $migration['body']['destination']);
$this->assertContains(Resource::TYPE_DOCUMENT, $migration['body']['resources']);
$this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']);
return [
'databaseId' => $databaseId,
'collectionId' => $collectionId,
'tableId' => $tableId,
'migrationId' => $migration['body']['$id'],
];
}
@@ -1141,14 +1151,14 @@ trait MigrationsBase
*/
public function testImportSuccessful(array $response): void
{
$tableId = $response['tableId'];
$databaseId = $response['databaseId'];
$collectionId = $response['collectionId'];
$migrationId = $response['migrationId'];
$documentsCountInCSV = 100;
$rowsCountInCSV = 100;
// get migration stats
$this->assertEventually(function () use ($migrationId, $databaseId, $collectionId, $documentsCountInCSV) {
$this->assertEventually(function () use ($migrationId, $databaseId, $tableId, $rowsCountInCSV) {
$migration = $this->client->call(Client::METHOD_GET, '/migrations/'.$migrationId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -1159,13 +1169,13 @@ trait MigrationsBase
$this->assertEquals('completed', $migration['body']['status']);
$this->assertEquals('CSV', $migration['body']['source']);
$this->assertEquals('Appwrite', $migration['body']['destination']);
$this->assertContains(Resource::TYPE_DOCUMENT, $migration['body']['resources']);
$this->assertArrayHasKey(Resource::TYPE_DOCUMENT, $migration['body']['statusCounters']);
$this->assertEquals($documentsCountInCSV, $migration['body']['statusCounters'][Resource::TYPE_DOCUMENT]['success']);
}, 60000, 500);
$this->assertContains(Resource::TYPE_ROW, $migration['body']['resources']);
$this->assertArrayHasKey(Resource::TYPE_ROW, $migration['body']['statusCounters']);
$this->assertEquals($rowsCountInCSV, $migration['body']['statusCounters'][Resource::TYPE_ROW]['success']);
}, 1000, 500);
// get documents count
$documents = $this->client->call(Client::METHOD_GET, '/databases/'.$databaseId.'/collections/'.$collectionId.'/documents', array_merge([
// get rows count
$rows = $this->client->call(Client::METHOD_GET, '/databases/'.$databaseId.'/grids/tables/'.$tableId.'/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
@@ -1175,10 +1185,10 @@ trait MigrationsBase
]
]);
$this->assertEquals(200, $documents['headers']['status-code']);
$this->assertIsArray($documents['body']['documents']);
$this->assertIsNumeric($documents['body']['total']);
$this->assertEquals($documentsCountInCSV, $documents['body']['total']);
$this->assertEquals(200, $rows['headers']['status-code']);
$this->assertIsArray($rows['body']['rows']);
$this->assertIsNumeric($rows['body']['total']);
$this->assertEquals($rowsCountInCSV, $rows['body']['total']);
}
private function performCsvMigration(array $body): array
@@ -483,7 +483,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertIsArray($response['body']['requests']);
$this->assertIsArray($response['body']['network']);
$this->assertIsNumeric($response['body']['executionsTotal']);
$this->assertIsNumeric($response['body']['documentsTotal']);
$this->assertIsNumeric($response['body']['rowsTotal']);
$this->assertIsNumeric($response['body']['databasesTotal']);
$this->assertIsNumeric($response['body']['bucketsTotal']);
$this->assertIsNumeric($response['body']['usersTotal']);
@@ -951,6 +951,55 @@ class ProjectsConsoleClientTest extends Scope
return ['projectId' => $projectId];
}
/** @depends testCreateProject */
public function testUpdateProjectInvalidateSessions($data): array
{
$id = $data['projectId'];
// Check defaults
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertTrue($response['body']['authInvalidateSessions']);
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/session-invalidation', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'enabled' => false,
]);
$this->assertFalse($response['body']['authInvalidateSessions']);
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertFalse($response['body']['authInvalidateSessions']);
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/session-invalidation', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'enabled' => true,
]);
$this->assertTrue($response['body']['authInvalidateSessions']);
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertTrue($response['body']['authInvalidateSessions']);
return $data;
}
/**
* @depends testCreateProject
*/
@@ -2942,7 +2991,7 @@ class ProjectsConsoleClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Key Test Update',
'scopes' => ['users.read', 'users.write', 'collections.read'],
'scopes' => ['users.read', 'users.write', 'collections.read', 'tables.read'],
'expire' => DateTime::addSeconds(new \DateTime(), 360),
]);
@@ -2953,6 +3002,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains('users.read', $response['body']['scopes']);
$this->assertContains('users.write', $response['body']['scopes']);
$this->assertContains('collections.read', $response['body']['scopes']);
$this->assertContains('tables.read', $response['body']['scopes']);
$this->assertCount(3, $response['body']['scopes']);
$this->assertArrayHasKey('sdks', $response['body']);
$this->assertEmpty($response['body']['sdks']);
@@ -2971,6 +3021,7 @@ class ProjectsConsoleClientTest extends Scope
$this->assertContains('users.read', $response['body']['scopes']);
$this->assertContains('users.write', $response['body']['scopes']);
$this->assertContains('collections.read', $response['body']['scopes']);
$this->assertContains('tables.read', $response['body']['scopes']);
$this->assertCount(3, $response['body']['scopes']);
$this->assertArrayHasKey('sdks', $response['body']);
$this->assertEmpty($response['body']['sdks']);
@@ -4637,7 +4688,6 @@ class ProjectsConsoleClientTest extends Scope
'failure' => 'https://example.com'
]);
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertStringContainsString('Invalid `success` param: URL host must be one of: localhost, appwrite.io, *.appwrite.io', $response['body']);
/** Test oauth2 with devKey and now get oauth2 is disabled */
$response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/' . $provider, [
@@ -4660,7 +4710,6 @@ class ProjectsConsoleClientTest extends Scope
'url' => 'https://example.com',
]);
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertEquals('Invalid `url` param: URL host must be one of: localhost, appwrite.io, *.appwrite.io', $response['body']['message']);
/** Test hostname in Magic URL with devKey */
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/magic-url', [
@@ -30,8 +30,6 @@ class ProjectsCustomServerTest extends Scope
'domain' => $testId . '-api.appwrite.test',
]);
\var_dump($response);
$this->assertEquals(201, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/proxy/rules/api', $headers, [
+2 -2
View File
@@ -227,7 +227,7 @@ trait ProxyBase
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'functionId' => ID::unique(),
'runtime' => 'node-18.0',
'runtime' => 'node-22',
'name' => 'Proxy Function',
'entrypoint' => 'index.js',
'commands' => '',
@@ -244,7 +244,7 @@ trait ProxyBase
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), [
'code' => $this->packageFunction('node'),
'code' => $this->packageFunction('basic'),
'activate' => 'true'
]);
+5 -19
View File
@@ -16,13 +16,6 @@ trait RealtimeBase
$projectId = $this->getProject()['$id'];
}
$headers = array_merge(
[
"Origin" => "appwrite.test",
],
$headers
);
$query = [
"project" => $projectId,
"channels" => $channels,
@@ -42,14 +35,14 @@ trait RealtimeBase
/**
* Test for SUCCESS
*/
$client = $this->getWebsocket(["documents"]);
$client = $this->getWebsocket(["rows"]);
$this->assertNotEmpty($client->receive());
$client->close();
}
public function testConnectionFailureMissingChannels(): void
{
$client = $this->getWebsocket();
$client = $this->getWebsocket([]);
$payload = json_decode($client->receive(), true);
$this->assertArrayHasKey("type", $payload);
@@ -58,20 +51,13 @@ trait RealtimeBase
$this->assertEquals(1008, $payload["data"]["code"]);
$this->assertEquals("Missing channels", $payload["data"]["message"]);
\usleep(250000); // 250ms
$this->expectException(ConnectionException::class); // Check if server disconnnected client
$this->expectException(ConnectionException::class); // Check if server disconnected client
$client->close();
}
public function testConnectionFailureUnknownProject(): void
{
$client = new WebSocketClient(
"ws://appwrite-traefik/v1/realtime?project=123",
[
"headers" => [
"Origin" => "appwrite.test",
],
]
);
$client = $this->getWebsocket(projectId: '123');
$payload = json_decode($client->receive(), true);
$this->assertArrayHasKey("type", $payload);
@@ -83,7 +69,7 @@ trait RealtimeBase
$payload["data"]["message"]
);
\usleep(250000); // 250ms
$this->expectException(ConnectionException::class); // Check if server disconnnected client
$this->expectException(ConnectionException::class); // Check if server disconnected client
$client->close();
}
}
@@ -123,9 +123,8 @@ class RealtimeConsoleClientTest extends Scope
$client->close();
}
public function testAttributes(): array
public function testAttributesCollectionsAPI(): array
{
$user = $this->getUser();
$projectId = 'console';
$client = $this->getWebsocket(['console'], [
@@ -184,13 +183,13 @@ class RealtimeConsoleClientTest extends Scope
]);
$projectId = $this->getProject()['$id'];
$attributeKey = $name['body']['key'];
$this->assertEquals($name['headers']['status-code'], 202);
$this->assertEquals($name['body']['key'], 'name');
$this->assertEquals($name['body']['type'], 'string');
$this->assertEquals($name['body']['size'], 256);
$this->assertEquals($name['body']['required'], true);
$this->assertEquals(202, $name['headers']['status-code']);
$this->assertEquals('name', $name['body']['key']);
$this->assertEquals('string', $name['body']['type']);
$this->assertEquals(256, $name['body']['size']);
$this->assertTrue($name['body']['required']);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
@@ -235,15 +234,128 @@ class RealtimeConsoleClientTest extends Scope
$client->close();
$data = ['actorsId' => $actorsId, 'databaseId' => $databaseId];
return ['actorsId' => $actorsId, 'databaseId' => $databaseId];
}
return $data;
public function testAttributesTablesAPI(): array
{
$projectId = 'console';
$client = $this->getWebsocket(['console'], [
'origin' => 'http://localhost',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
], $projectId);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('connected', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertCount(1, $response['data']['channels']);
$this->assertContains('console', $response['data']['channels']);
$this->assertNotEmpty($response['data']['user']);
/**
* Create database
*/
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'databaseId' => ID::unique(),
'name' => 'Actors DB',
]);
$databaseId = $database['body']['$id'];
/**
* Test Attributes
*/
$actors = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'tableId' => ID::unique(),
'name' => 'Actors',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$actorsId = $actors['body']['$id'];
$name = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $actorsId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'key' => 'name',
'size' => 256,
'required' => true,
]);
$projectId = $this->getProject()['$id'];
$this->assertEquals(202, $name['headers']['status-code']);
$this->assertEquals('name', $name['body']['key']);
$this->assertEquals('string', $name['body']['type']);
$this->assertEquals(256, $name['body']['size']);
$this->assertTrue($name['body']['required']);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(2, $response['data']['channels']);
$this->assertContains('console', $response['data']['channels']);
$this->assertContains("projects.{$projectId}", $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.columns.*.create", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.columns.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.columns.*.create", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.columns.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}", $response['data']['events']);
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals('processing', $response['data']['payload']['status']);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(2, $response['data']['channels']);
$this->assertContains('console', $response['data']['channels']);
$this->assertContains("projects.{$projectId}", $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.columns.*.update", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.columns.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.columns.*.update", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.columns.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}", $response['data']['events']);
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals('available', $response['data']['payload']['status']);
$client->close();
return ['actorsId' => $actorsId, 'databaseId' => $databaseId];
}
/**
* @depends testAttributes
* @depends testAttributesCollectionsAPI
*/
public function testIndexes(array $data)
public function testIndexesCollectionAPI(array $data)
{
$projectId = 'console';
$actorsId = $data['actorsId'];
@@ -277,10 +389,9 @@ class RealtimeConsoleClientTest extends Scope
],
]);
$this->assertEquals($index['headers']['status-code'], 202);
$this->assertEquals(202, $index['headers']['status-code']);
$projectId = $this->getProject()['$id'];
$indexKey = $index['body']['key'];
$response = json_decode($client->receive(), true);
@@ -326,9 +437,93 @@ class RealtimeConsoleClientTest extends Scope
}
/**
* @depends testIndexes
* @depends testAttributesTablesAPI
*/
public function testDeleteIndex(array $data)
public function testIndexesTablesAPI(array $data)
{
$projectId = 'console';
$actorsId = $data['actorsId'];
$databaseId = $data['databaseId'];
$client = $this->getWebsocket(['console'], [
'origin' => 'http://localhost',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
], $projectId);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('connected', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertCount(1, $response['data']['channels']);
$this->assertContains('console', $response['data']['channels']);
$this->assertNotEmpty($response['data']['user']);
/**
* Test Indexes
*/
$index = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $actorsId . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'key' => 'key_name',
'type' => 'key',
'columns' => [
'name',
],
]);
$this->assertEquals(202, $index['headers']['status-code']);
$projectId = $this->getProject()['$id'];
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(2, $response['data']['channels']);
$this->assertContains('console', $response['data']['channels']);
$this->assertContains("projects.{$projectId}", $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.indexes.*.create", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.indexes.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.indexes.*.create", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.indexes.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals('processing', $response['data']['payload']['status']);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(2, $response['data']['channels']);
$this->assertContains('console', $response['data']['channels']);
$this->assertContains("projects.{$projectId}", $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.indexes.*.update", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.indexes.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.indexes.*.update", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.indexes.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals('available', $response['data']['payload']['status']);
$client->close();
return $data;
}
/**
* @depends testIndexesCollectionAPI
*/
public function testDeleteIndexCollectionsAPI(array $data)
{
$actorsId = $data['actorsId'];
$projectId = 'console';
@@ -360,7 +555,7 @@ class RealtimeConsoleClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($attribute['headers']['status-code'], 204);
$this->assertEquals(204, $attribute['headers']['status-code']);
$response = json_decode($client->receive(), true);
@@ -382,6 +577,7 @@ class RealtimeConsoleClientTest extends Scope
/** Delete index generates two events. One from the API and one from the database worker */
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
@@ -404,12 +600,91 @@ class RealtimeConsoleClientTest extends Scope
}
/**
* @depends testDeleteIndex
* @depends testIndexesTablesAPI
*/
public function testDeleteAttribute(array $data)
public function testDeleteIndexTablesAPI(array $data)
{
$actorsId = $data['actorsId'];
$projectId = 'console';
$actorsId = $data['actorsId'];
$databaseId = $data['databaseId'];
$client = $this->getWebsocket(['console'], [
'origin' => 'http://localhost',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
], $projectId);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('connected', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertCount(1, $response['data']['channels']);
$this->assertContains('console', $response['data']['channels']);
$this->assertNotEmpty($response['data']['user']);
$projectId = $this->getProject()['$id'];
/**
* Test Delete Index
*/
$indexKey = 'key_name';
$attribute = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/tables/' . $actorsId . '/indexes/' . $indexKey, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $attribute['headers']['status-code']);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(2, $response['data']['channels']);
$this->assertContains('console', $response['data']['channels']);
$this->assertContains("projects.{$projectId}", $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.indexes.*.update", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.indexes.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.indexes.*.update", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.indexes.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
/** Delete index generates two events. One from the API and one from the database worker */
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(2, $response['data']['channels']);
$this->assertContains('console', $response['data']['channels']);
$this->assertContains("projects.{$projectId}", $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.indexes.*.delete", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.indexes.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.indexes.*.delete", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.indexes.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$client->close();
return $data;
}
/**
* @depends testDeleteIndexCollectionsAPI
*/
public function testDeleteAttributeCollectionsAPI(array $data)
{
$projectId = 'console';
$actorsId = $data['actorsId'];
$databaseId = $data['databaseId'];
$client = $this->getWebsocket(['console'], [
@@ -438,7 +713,7 @@ class RealtimeConsoleClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($attribute['headers']['status-code'], 204);
$this->assertEquals(204, $attribute['headers']['status-code']);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
@@ -478,6 +753,81 @@ class RealtimeConsoleClientTest extends Scope
$client->close();
}
/**
* @depends testDeleteIndexTablesAPI
*/
public function testDeleteAttributeTablesAPI(array $data)
{
$projectId = 'console';
$actorsId = $data['actorsId'];
$databaseId = $data['databaseId'];
$client = $this->getWebsocket(['console'], [
'origin' => 'http://localhost',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
], $projectId);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('connected', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertCount(1, $response['data']['channels']);
$this->assertContains('console', $response['data']['channels']);
$this->assertNotEmpty($response['data']['user']);
$attributeKey = 'name';
$projectId = $this->getProject()['$id'];
/**
* Test Delete Attribute
*/
$attribute = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/tables/' . $data['actorsId'] . '/columns/' . $attributeKey, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $attribute['headers']['status-code']);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(2, $response['data']['channels']);
$this->assertContains('console', $response['data']['channels']);
$this->assertContains("projects.{$projectId}", $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.columns.*.update", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.columns.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.columns.*.update", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.columns.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$response = json_decode($client->receive(), true);
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(2, $response['data']['channels']);
$this->assertContains('console', $response['data']['channels']);
$this->assertContains("projects.{$projectId}", $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.columns.*.delete", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}.columns.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.{$actorsId}", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.columns.*.delete", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*.columns.*", $response['data']['events']);
$this->assertContains("databases.{$databaseId}.tables.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$client->close();
}
public function testPing()
{
$client = $this->getWebsocket(['console'], [
@@ -524,8 +874,8 @@ class RealtimeConsoleClientTest extends Scope
], $this->getHeaders()), [
'functionId' => ID::unique(),
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'events' => [
'users.*.create',
'users.*.delete',
@@ -564,8 +914,7 @@ class RealtimeConsoleClientTest extends Scope
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'code' => $this->packageFunction('basic'),
'activate' => true
]);
@@ -607,10 +956,8 @@ class RealtimeConsoleClientTest extends Scope
$this->assertContains("projects.{$projectId}", $response['data']['channels']);
$this->assertArrayHasKey('buildLogs', $response['data']['payload']);
if (!empty($response['data']['payload']['buildEndedAt'])) {
$this->assertNotEmpty($response['data']['payload']['buildEndedAt']);
if (!empty($response['data']['payload']['buildSize'])) {
$this->assertNotEmpty($response['data']['payload']['buildStartedAt']);
$this->assertNotEmpty($response['data']['payload']['buildDuration']);
$this->assertNotEmpty($response['data']['payload']['buildPath']);
$this->assertNotEmpty($response['data']['payload']['buildSize']);
$this->assertNotEmpty($response['data']['payload']['totalSize']);
@@ -618,7 +965,7 @@ class RealtimeConsoleClientTest extends Scope
break;
}
// Ignore comparasion for first payload
// Ignore comparison for first payload
if ($previousBuildLogs !== null) {
$this->assertNotEquals($previousBuildLogs, $response['data']['payload']['buildLogs']);
}
@@ -634,6 +981,13 @@ class RealtimeConsoleClientTest extends Scope
$this->assertContains("projects.{$projectId}", $response['data']['channels']);
$this->assertEquals("ready", $response['data']['payload']['status']);
$response = json_decode($client->receive(), true);
$this->assertContains("functions.{$functionId}.deployments.{$deploymentId}.update", $response['data']['events']);
$this->assertContains('console', $response['data']['channels']);
$this->assertContains("projects.{$projectId}", $response['data']['channels']);
$this->assertNotEmpty($response['data']['payload']['buildDuration']);
$this->assertNotEmpty($response['data']['payload']['buildEndedAt']);
$client->close();
}
}
@@ -81,11 +81,17 @@ class RealtimeCustomClientTest extends Scope
'files',
'files.1',
'collections',
'tables',
'collections.1.documents',
'collections.2.documents',
'tables.1.rows',
'tables.2.rows',
'documents',
'rows',
'collections.1.documents.1',
'collections.2.documents.2',
'tables.1.rows.1',
'tables.2.rows.2',
], $headers);
$response = json_decode($client->receive(), true);
@@ -95,17 +101,22 @@ class RealtimeCustomClientTest extends Scope
$this->assertEquals('connected', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertNotEmpty($response['data']['user']);
$this->assertCount(10, $response['data']['channels']);
$this->assertCount(16, $response['data']['channels']);
$this->assertContains('account', $response['data']['channels']);
$this->assertContains('account.' . $userId, $response['data']['channels']);
$this->assertContains('files', $response['data']['channels']);
$this->assertContains('files.1', $response['data']['channels']);
$this->assertContains('collections', $response['data']['channels']);
$this->assertContains('tables', $response['data']['channels']);
$this->assertContains('collections.1.documents', $response['data']['channels']);
$this->assertContains('collections.2.documents', $response['data']['channels']);
$this->assertContains('tables.1.rows', $response['data']['channels']);
$this->assertContains('tables.2.rows', $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains('collections.1.documents.1', $response['data']['channels']);
$this->assertContains('collections.2.documents.2', $response['data']['channels']);
$this->assertContains('tables.1.rows.1', $response['data']['channels']);
$this->assertContains('tables.2.rows.2', $response['data']['channels']);
$this->assertEquals($userId, $response['data']['user']['$id']);
$client->close();
@@ -415,7 +426,8 @@ class RealtimeCustomClientTest extends Scope
$this->assertContains("users.*", $response['data']['events']);
$lastEmail = $this->getLastEmail();
$verification = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
$verification = $tokens['secret'];
/**
* Test Account Verification Complete
@@ -612,7 +624,8 @@ class RealtimeCustomClientTest extends Scope
$response = json_decode($client->receive(), true);
$lastEmail = $this->getLastEmail();
$recovery = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
$recovery = $tokens['secret'];
$this->assertArrayHasKey('type', $response);
$this->assertArrayHasKey('data', $response);
@@ -734,11 +747,11 @@ class RealtimeCustomClientTest extends Scope
'required' => true,
]);
$this->assertEquals($name['headers']['status-code'], 202);
$this->assertEquals($name['body']['key'], 'name');
$this->assertEquals($name['body']['type'], 'string');
$this->assertEquals($name['body']['size'], 256);
$this->assertEquals($name['body']['required'], true);
$this->assertEquals(202, $name['headers']['status-code']);
$this->assertEquals('name', $name['body']['key']);
$this->assertEquals('string', $name['body']['type']);
$this->assertEquals(256, $name['body']['size']);
$this->assertTrue($name['body']['required']);
sleep(2);
@@ -768,7 +781,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']);
$this->assertCount(6, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains('databases.' . $databaseId . '.collections.' . $actorsId . '.documents.' . $documentId, $response['data']['channels']);
$this->assertContains('databases.' . $databaseId . '.collections.' . $actorsId . '.documents', $response['data']['channels']);
@@ -785,7 +798,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertContains("databases.{$databaseId}", $response['data']['events']);
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals($response['data']['payload']['name'], 'Chris Evans');
$this->assertEquals('Chris Evans', $response['data']['payload']['name']);
/**
* Test Document Update
@@ -812,7 +825,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']);
$this->assertCount(6, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
@@ -830,7 +843,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals($response['data']['payload']['name'], 'Chris Evans 2');
$this->assertEquals('Chris Evans 2', $response['data']['payload']['name']);
/**
* Test Document Delete
@@ -866,7 +879,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']);
$this->assertCount(6, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
@@ -883,7 +896,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertContains("databases.{$databaseId}", $response['data']['events']);
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals($response['data']['payload']['name'], 'Bradley Cooper');
$this->assertEquals('Bradley Cooper', $response['data']['payload']['name']);
$client->close();
}
@@ -955,11 +968,11 @@ class RealtimeCustomClientTest extends Scope
'required' => true,
]);
$this->assertEquals($name['headers']['status-code'], 202);
$this->assertEquals($name['body']['key'], 'name');
$this->assertEquals($name['body']['type'], 'string');
$this->assertEquals($name['body']['size'], 256);
$this->assertEquals($name['body']['required'], true);
$this->assertEquals(202, $name['headers']['status-code']);
$this->assertEquals('name', $name['body']['key']);
$this->assertEquals('string', $name['body']['type']);
$this->assertEquals(256, $name['body']['size']);
$this->assertTrue($name['body']['required']);
sleep(2);
@@ -986,7 +999,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']);
$this->assertCount(6, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
@@ -1003,7 +1016,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertContains("databases.{$databaseId}", $response['data']['events']);
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals($response['data']['payload']['name'], 'Chris Evans');
$this->assertEquals('Chris Evans', $response['data']['payload']['name']);
/**
* Test Document Update
@@ -1025,7 +1038,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']);
$this->assertCount(6, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
@@ -1043,7 +1056,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals($response['data']['payload']['name'], 'Chris Evans 2');
$this->assertEquals('Chris Evans 2', $response['data']['payload']['name']);
/**
* Test Document Delete
@@ -1075,7 +1088,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertEquals('event', $response['type']);
$this->assertNotEmpty($response['data']);
$this->assertArrayHasKey('timestamp', $response['data']);
$this->assertCount(3, $response['data']['channels']);
$this->assertCount(6, $response['data']['channels']);
$this->assertContains('documents', $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents.{$documentId}", $response['data']['channels']);
$this->assertContains("databases.{$databaseId}.collections.{$actorsId}.documents", $response['data']['channels']);
@@ -1092,7 +1105,7 @@ class RealtimeCustomClientTest extends Scope
$this->assertContains("databases.{$databaseId}", $response['data']['events']);
$this->assertContains("databases.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals($response['data']['payload']['name'], 'Bradley Cooper');
$this->assertEquals('Bradley Cooper', $response['data']['payload']['name']);
$client->close();
}
@@ -1283,16 +1296,16 @@ class RealtimeCustomClientTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
], [
'functionId' => ID::unique(),
'name' => 'Test',
'name' => 'Test timeout execution',
'execute' => ['users'],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
$functionId = $function['body']['$id'] ?? '';
$this->assertEquals($function['headers']['status-code'], 201);
$this->assertEquals(201, $function['headers']['status-code']);
$this->assertNotEmpty($function['body']['$id']);
$deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([
@@ -1300,14 +1313,13 @@ class RealtimeCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('timeout'),
'activate' => true
]);
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEquals($deployment['headers']['status-code'], 202);
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
// Poll until deployment is built
@@ -1467,7 +1479,7 @@ class RealtimeCustomClientTest extends Scope
'name' => 'Manchester'
]);
$this->assertEquals($team['headers']['status-code'], 200);
$this->assertEquals(200, $team['headers']['status-code']);
$this->assertNotEmpty($team['body']['$id']);
$response = json_decode($client->receive(), true);
@@ -1499,9 +1511,9 @@ class RealtimeCustomClientTest extends Scope
]
]);
$this->assertEquals($team['headers']['status-code'], 200);
$this->assertEquals($team['body']['funcKey1'], 'funcValue1');
$this->assertEquals($team['body']['funcKey2'], 'funcValue2');
$this->assertEquals(200, $team['headers']['status-code']);
$this->assertEquals('funcValue1', $team['body']['funcKey1']);
$this->assertEquals('funcValue2', $team['body']['funcKey2']);
$response = json_decode($client->receive(), true);
@@ -1520,8 +1532,8 @@ class RealtimeCustomClientTest extends Scope
$this->assertContains("teams.*.update", $response['data']['events']);
$this->assertContains("teams.*", $response['data']['events']);
$this->assertNotEmpty($response['data']['payload']);
$this->assertEquals($response['data']['payload']['funcKey1'], 'funcValue1');
$this->assertEquals($response['data']['payload']['funcKey2'], 'funcValue2');
$this->assertEquals('funcValue1', $response['data']['payload']['funcKey1']);
$this->assertEquals('funcValue2', $response['data']['payload']['funcKey2']);
$client->close();
@@ -14,6 +14,9 @@ class SitesConsoleClientTest extends Scope
use SideConsole;
use SitesBase;
/**
* @group screenshots
*/
public function testSiteScreenshot(): void
{
$siteId = $this->setupSite([
@@ -89,12 +92,51 @@ class SitesConsoleClientTest extends Scope
$this->assertNotEquals($screenshotDarkHash, $screenshotHash);
$screenshotId = $deployment['body']['screenshotLight'];
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console");
$this->assertEquals(404, $file['headers']['status-code']);
$screenshotId = $deployment['body']['screenshotDark'];
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/view?project=console");
$this->assertEquals(404, $file['headers']['status-code']);
// Verify previews
$screenshotId = $deployment['body']['screenshotLight'];
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/preview?project=console", array_merge($this->getHeaders(), [
'x-appwrite-mode' => 'default' // NOT ADMIN!
]));
$this->assertEquals(200, $file['headers']['status-code']);
$this->assertNotEmpty(200, $file['body']);
$this->assertGreaterThan(1, $file['headers']['content-length']);
$this->assertEquals('image/png', $file['headers']['content-type']);
$screenshotHash = \md5($file['body']);
$this->assertNotEmpty($screenshotHash);
$screenshotId = $deployment['body']['screenshotDark'];
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/preview?project=console", array_merge($this->getHeaders(), [
'x-appwrite-mode' => 'default' // NOT ADMIN!
]));
$this->assertEquals(200, $file['headers']['status-code']);
$this->assertNotEmpty(200, $file['body']);
$this->assertGreaterThan(1, $file['headers']['content-length']);
$this->assertEquals('image/png', $file['headers']['content-type']);
$screenshotDarkHash = \md5($file['body']);
$this->assertNotEmpty($screenshotDarkHash);
$this->assertNotEquals($screenshotDarkHash, $screenshotHash);
$screenshotId = $deployment['body']['screenshotLight'];
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/preview?project=console");
$this->assertEquals(404, $file['headers']['status-code']);
$screenshotId = $deployment['body']['screenshotDark'];
$file = $this->client->call(Client::METHOD_GET, "/storage/buckets/screenshots/files/$screenshotId/preview?project=console");
$this->assertEquals(404, $file['headers']['status-code']);
$this->cleanupSite($siteId);
}
}
@@ -6,6 +6,7 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\System\System;
class SitesCustomClientTest extends Scope
{
@@ -121,6 +122,7 @@ class SitesCustomClientTest extends Scope
* Test for SUCCESS
*/
$template = $this->getTemplate('starter-for-react');
$hostname = System::getEnv('_APP_DOMAIN') ?: '';
$this->assertEquals(200, $template['headers']['status-code']);
$this->assertIsArray($template['body']);
$this->assertEquals('starter-for-react', $template['body']['key']);
@@ -129,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://localhost/images/sites/templates/starter-for-react-dark.png', $template['body']['screenshotDark']);
$this->assertEquals('http://localhost/images/sites/templates/starter-for-react-light.png', $template['body']['screenshotLight']);
$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']);
/**
* Test for FAILURE
@@ -937,12 +937,12 @@ class SitesCustomServerTest extends Scope
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('canceled', $deployment['body']['status']);
/**
* Build worker still runs the build.
* 30s sleep gives worker enough time to finish build.
* After build finished, it should still be canceled, not ready.
*/
\sleep(30);
// Ensures worker got eventually aware of cancellation and reacted properly
$this->assertEventually(function () use ($siteId, $deploymentId) {
$deployment = $this->getDeployment($siteId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertStringContainsString('Build has been canceled.', $deployment['body']['buildLogs']);
});
$deployment = $this->getDeployment($siteId, $deploymentId);
+21 -15
View File
@@ -223,8 +223,10 @@ trait TeamsBaseClient
$this->assertEquals($email, $lastEmail['to'][0]['address']);
$this->assertEquals($name, $lastEmail['to'][0]['name']);
$this->assertEquals('Invitation to ' . $teamName . ' Team at ' . $this->getProject()['name'], $lastEmail['subject']);
$this->assertEquals($response['body']['teamId'], substr($lastEmail['text'], strpos($lastEmail['text'], '&teamId=', 0) + 8, 20));
$this->assertEquals($teamName, substr($lastEmail['text'], strpos($lastEmail['text'], '&teamName=', 0) + 10, 7));
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
$this->assertEquals($teamName, $tokens['teamName']);
$this->assertEquals($response['body']['teamId'], $tokens['teamId']);
/**
* Test with UserId
@@ -288,8 +290,10 @@ trait TeamsBaseClient
$this->assertEquals($secondEmail, $lastEmail['to'][0]['address']);
$this->assertEquals($secondName, $lastEmail['to'][0]['name']);
$this->assertEquals('Invitation to ' . $teamName . ' Team at ' . $this->getProject()['name'], $lastEmail['subject']);
$this->assertEquals($response['body']['teamId'], substr($lastEmail['text'], strpos($lastEmail['text'], '&teamId=', 0) + 8, 20));
$this->assertEquals($teamName, substr($lastEmail['text'], strpos($lastEmail['text'], '&teamName=', 0) + 10, 7));
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
$this->assertEquals($teamName, $tokens['teamName']);
$this->assertEquals($response['body']['teamId'], $tokens['teamId']);
// test for resending invitation
$response = $this->client->call(Client::METHOD_POST, '/teams/' . $teamUid . '/memberships', array_merge([
@@ -305,9 +309,10 @@ trait TeamsBaseClient
$this->assertEquals(201, $response['headers']['status-code']);
$lastEmail = $this->getLastEmail();
$membershipUid = substr($lastEmail['text'], strpos($lastEmail['text'], '?membershipId=', 0) + 14, 20);
$userUid = substr($lastEmail['text'], strpos($lastEmail['text'], '&userId=', 0) + 8, 20);
$secret = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
$membershipUid = $tokens['membershipId'];
$userUid = $tokens['userId'];
$secret = $tokens['secret'];
/**
* Test for FAILURE
@@ -338,11 +343,11 @@ trait TeamsBaseClient
$this->assertEquals(400, $response['headers']['status-code']);
return [
'teamUid' => $teamUid,
'teamName' => $teamName,
'secret' => $secret,
'membershipUid' => $membershipUid,
'userUid' => $userUid,
'teamUid' => $tokens['teamId'],
'teamName' => $tokens['teamName'],
'secret' => $tokens['secret'],
'membershipUid' => $tokens['membershipId'],
'userUid' => $tokens['userId'],
'email' => $email,
'name' => $name
];
@@ -600,10 +605,11 @@ trait TeamsBaseClient
$this->assertEquals(201, $response['headers']['status-code']);
$lastEmail = $this->getLastEmail();
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html']);
$secret = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
$membershipUid = substr($lastEmail['text'], strpos($lastEmail['text'], '?membershipId=', 0) + 14, 20);
$userUid = substr($lastEmail['text'], strpos($lastEmail['text'], '&userId=', 0) + 8, 20);
$secret = $tokens['secret'];
$membershipUid = $tokens['membershipId'];
$userUid = $tokens['userId'];
$response = $this->client->call(Client::METHOD_PATCH, '/teams/' . $teamUid . '/memberships/' . $membershipUid . '/status', [
'origin' => 'http://localhost',
@@ -6,6 +6,7 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\CLI\Console;
class TeamsCustomClientTest extends Scope
{
@@ -152,11 +153,14 @@ class TeamsCustomClientTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$email = $this->getLastEmail();
Console::log(json_encode([
'testTeamsInviteHTMLInjection' => $email
], JSON_PRETTY_PRINT));
$encoded = 'http://localhost:5000/join-us\&quot;&gt;&lt;/a&gt;&lt;h1&gt;INJECTED&lt;/h1&gt;?';
$this->assertStringNotContainsString('<h1>INJECTED</h1>', $email['html']);
$this->assertStringContainsString($encoded, $email['html']);
$this->assertStringContainsString($encoded, $email['text']);
$response = $this->client->call(Client::METHOD_DELETE, '/teams/' . $teamUid . '/memberships/'.$response['body']['$id'], array_merge([
'content-type' => 'application/json',
+86
View File
@@ -5,6 +5,8 @@ namespace Tests\E2E\Services\Tokens;
use CURLFile;
use Tests\E2E\Client;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
trait TokensBase
{
@@ -275,4 +277,88 @@ trait TokensBase
$this->assertEquals($image->getImageHeight(), $original->getImageHeight());
$this->assertEquals('PNG', $image->getImageFormat());
}
public function testFileAccessWithFileSecurity(): void
{
$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'],
],
[
'name' => 'Test Bucket',
'bucketId' => ID::unique(),
'fileSecurity' => true,
'allowedFileExtensions' => ['jpg', 'png', 'jfif'],
]
);
$this->assertEquals(201, $bucket['headers']['status-code']);
$this->assertNotEmpty($bucket['body']['$id']);
$bucketId = $bucket['body']['$id'];
$file = $this->client->call(
Client::METHOD_POST,
'/storage/buckets/' . $bucketId . '/files',
[
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
],
[
'fileId' => ID::unique(),
'permissions' => [ Permission::read(Role::label('devrel')) ],
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
]
);
$fileId = $file['body']['$id'];
$token = $this->client->call(
Client::METHOD_POST,
'/tokens/buckets/' . $bucketId . '/files/' . $fileId,
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]
);
$jwtToken = $token['body']['secret'];
$endpoints = ['preview', 'view', 'download'];
foreach ($endpoints as $endpoint) {
$response = $this->client->call(
Client::METHOD_GET,
"/storage/buckets/$bucketId/files/$fileId/$endpoint",
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
],
[
'token' => $jwtToken
]
);
$this->assertNotEmpty($response['body']);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('image/png', $response['headers']['content-type']);
if ($endpoint === 'download') {
$image = new \Imagick();
$image->readImageBlob($response['body']);
$original = new \Imagick(__DIR__ . '/../../../resources/logo.png');
$this->assertEquals($original->getImageWidth(), $image->getImageWidth());
$this->assertEquals($original->getImageHeight(), $image->getImageHeight());
$this->assertEquals('PNG', $image->getImageFormat());
}
}
}
}
+11 -2
View File
@@ -1117,7 +1117,7 @@ trait UsersBase
]);
$this->assertEquals(401, $session['headers']['status-code']);
$this->updateProjectinvalidateSessionsProperty(true);
$user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/password', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -1129,6 +1129,15 @@ trait UsersBase
$this->assertNotEmpty($user['body']['$id']);
$this->assertNotEmpty($user['body']['password']);
$sessions = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/sessions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($sessions['headers']['status-code'], 200);
$this->assertIsArray($sessions['body']);
$this->assertEmpty($sessions['body']['sessions']);
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@@ -1138,7 +1147,7 @@ trait UsersBase
]);
$this->assertEquals($session['headers']['status-code'], 201);
$this->updateProjectinvalidateSessionsProperty(false);
return $data;
}
+355 -2
View File
@@ -36,7 +36,7 @@ trait WebhooksBase
return base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
}
// Collection APIs
public function testCreateCollection(): array
{
/**
@@ -389,6 +389,356 @@ trait WebhooksBase
return $data;
}
// Table APIs
public function testCreateTable(): array
{
/**
* Create database
*/
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'databaseId' => ID::unique(),
'name' => 'Actors DB',
]);
$databaseId = $database['body']['$id'];
/**
* Test for SUCCESS
*/
$actors = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'Actors',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'rowSecurity' => true,
]);
$actorsId = $actors['body']['$id'];
$this->assertEquals($actors['headers']['status-code'], 201);
$this->assertNotEmpty($actors['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['name'], 'Actors');
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertCount(4, $webhook['data']['$permissions']);
return array_merge(['actorsId' => $actorsId, 'databaseId' => $databaseId]);
}
/**
* @depends testCreateTable
*/
public function testCreateColumns(array $data): array
{
$actorsId = $data['actorsId'];
$databaseId = $data['databaseId'];
$firstName = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $actorsId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'firstName',
'size' => 256,
'required' => true,
]);
$lastName = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $actorsId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'lastName',
'size' => 256,
'required' => true,
]);
$extra = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $actorsId . '/columns/string', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'extra',
'size' => 64,
'required' => false,
]);
$this->assertEquals($firstName['headers']['status-code'], 202);
$this->assertEquals($firstName['body']['key'], 'firstName');
$this->assertEquals($lastName['headers']['status-code'], 202);
$this->assertEquals($lastName['body']['key'], 'lastName');
$this->assertEquals($extra['headers']['status-code'], 202);
$this->assertEquals($extra['body']['key'], 'extra');
// wait for database worker to kick in
sleep(10);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.columns.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.columns.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.columns.*", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.columns.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertNotEmpty($webhook['data']['key']);
$this->assertEquals($webhook['data']['key'], 'extra');
$removed = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/tables/' . $data['actorsId'] . '/columns/' . $extra['body']['key'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(204, $removed['headers']['status-code']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
// $this->assertEquals($webhook['method'], 'DELETE');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.columns.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.columns.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.columns.*", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.columns.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertNotEmpty($webhook['data']['key']);
$this->assertEquals($webhook['data']['key'], 'extra');
return $data;
}
/**
* @depends testCreateColumns
*/
public function testCreateRow(array $data): array
{
$actorsId = $data['actorsId'];
$databaseId = $data['databaseId'];
/**
* Test for SUCCESS
*/
$row = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $actorsId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'rowId' => ID::unique(),
'data' => [
'firstName' => 'Chris',
'lastName' => 'Evans',
],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$documentId = $row['body']['$id'];
$this->assertEquals($row['headers']['status-code'], 201);
$this->assertNotEmpty($row['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$documentId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$documentId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$documentId}.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['firstName'], 'Chris');
$this->assertEquals($webhook['data']['lastName'], 'Evans');
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertCount(3, $webhook['data']['$permissions']);
$data['rowId'] = $row['body']['$id'];
return $data;
}
/**
* @depends testCreateRow
*/
public function testUpdateRow(array $data): array
{
$actorsId = $data['actorsId'];
$databaseId = $data['databaseId'];
/**
* Test for SUCCESS
*/
$document = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/grids/tables/' . $actorsId . '/rows/' . $data['rowId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'data' => [
'firstName' => 'Chris1',
'lastName' => 'Evans2',
],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$rowId = $document['body']['$id'];
$this->assertEquals($document['headers']['status-code'], 200);
$this->assertNotEmpty($document['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$rowId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$rowId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['firstName'], 'Chris1');
$this->assertEquals($webhook['data']['lastName'], 'Evans2');
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertCount(3, $webhook['data']['$permissions']);
return $data;
}
/**
* @depends testCreateTable
*/
#[Retry(count: 1)]
public function testDeleteRow(array $data): array
{
$actorsId = $data['actorsId'];
$databaseId = $data['databaseId'];
/**
* Test for SUCCESS
*/
$row = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $actorsId . '/rows', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'rowId' => ID::unique(),
'data' => [
'firstName' => 'Bradly',
'lastName' => 'Cooper',
],
'permissions' => [
Permission::read(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
]);
$rowId = $row['body']['$id'];
$this->assertEquals($row['headers']['status-code'], 201);
$this->assertNotEmpty($row['body']['$id']);
$row = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/tables/' . $actorsId . '/rows/' . $row['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($row['headers']['status-code'], 204);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.rows.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$rowId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.*.rows.{$rowId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.*.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.rows.{$rowId}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['firstName'], 'Bradly');
$this->assertEquals($webhook['data']['lastName'], 'Cooper');
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertCount(3, $webhook['data']['$permissions']);
return $data;
}
public function testCreateStorageBucket(): array
{
@@ -921,7 +1271,10 @@ trait WebhooksBase
$lastEmail = $this->getLastEmail();
$secret = substr($lastEmail['text'], strpos($lastEmail['text'], '&secret=', 0) + 8, 256);
// `$isAppUser` — no email expected;
$tokens = $this->extractQueryParamsFromEmailLink($lastEmail['html'] ?? '');
$secret = $tokens['secret'] ?? '';
$membershipId = $team['body']['$id'];
$webhook = $this->getLastRequest();
@@ -894,9 +894,9 @@ class WebhooksCustomClientTest extends Scope
$url = $webhook['url'];
$signatureExpected = base64_encode(hash_hmac('sha1', $url . $payload, $signatureKey, true));
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
$this->assertStringContainsString('teams.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('teams.*.memberships.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('teams.*.memberships.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -919,8 +919,8 @@ class WebhooksCustomClientTest extends Scope
$this->assertNotEmpty($webhook['data']['userId']);
$this->assertNotEmpty($webhook['data']['teamId']);
$this->assertCount(2, $webhook['data']['roles']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['joined']));
$this->assertEquals(true, $webhook['data']['confirm']);
$this->assertTrue((new DatetimeValidator())->isValid($webhook['data']['joined']));
$this->assertTrue($webhook['data']['confirm']);
/**
* Test for FAILURE
@@ -19,6 +19,7 @@ class WebhooksCustomServerTest extends Scope
use ProjectCustom;
use SideServer;
// Collection APIs
/**
* @depends testCreateAttributes
*/
@@ -39,15 +40,15 @@ class WebhooksCustomServerTest extends Scope
'documentSecurity' => true,
]);
$this->assertEquals($actors['headers']['status-code'], 200);
$this->assertEquals(200, $actors['headers']['status-code']);
$this->assertNotEmpty($actors['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
$this->assertStringContainsString('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.collections.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.collections.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -55,9 +56,9 @@ class WebhooksCustomServerTest extends Scope
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
$this->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['name'], 'Actors1');
$this->assertEquals('Actors1', $webhook['data']['name']);
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertCount(4, $webhook['data']['$permissions']);
@@ -84,8 +85,8 @@ class WebhooksCustomServerTest extends Scope
]);
$indexKey = $index['body']['key'];
$this->assertEquals($index['headers']['status-code'], 202);
$this->assertEquals($index['body']['key'], 'fullname');
$this->assertEquals(202, $index['headers']['status-code']);
$this->assertEquals('fullname', $index['body']['key']);
// wait for database worker to create index
sleep(5);
@@ -93,9 +94,9 @@ class WebhooksCustomServerTest extends Scope
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
$this->assertStringContainsString('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.collections.*.indexes.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.collections.*.indexes.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -105,7 +106,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
$this->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''));
// Remove index
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $data['actorsId'] . '/indexes/' . $index['body']['key'], array_merge([
@@ -119,8 +120,8 @@ class WebhooksCustomServerTest extends Scope
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
// $this->assertEquals($webhook['method'], 'DELETE');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
$this->assertStringContainsString('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.collections.*.indexes.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.collections.*.indexes.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -130,7 +131,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
$this->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''));
return $data;
}
@@ -172,7 +173,7 @@ class WebhooksCustomServerTest extends Scope
$id = $actors['body']['$id'];
$this->assertEquals($actors['headers']['status-code'], 201);
$this->assertEquals(201, $actors['headers']['status-code']);
$this->assertNotEmpty($actors['body']['$id']);
$actors = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/collections/' . $actors['body']['$id'], array_merge([
@@ -181,14 +182,14 @@ class WebhooksCustomServerTest extends Scope
'x-appwrite-key' => $this->getProject()['apiKey']
]), []);
$this->assertEquals($actors['headers']['status-code'], 204);
$this->assertEquals(204, $actors['headers']['status-code']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
$this->assertStringContainsString('databases.' . $databaseId . '.collections.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.collections.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.collections.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -196,9 +197,195 @@ class WebhooksCustomServerTest extends Scope
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), true);
$this->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['name'], 'Demo');
$this->assertEquals('Demo', $webhook['data']['name']);
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertCount(4, $webhook['data']['$permissions']);
return [];
}
// Table APIs
/**
* @depends testCreateColumns
*/
public function testUpdateTable($data): array
{
$id = $data['actorsId'];
$databaseId = $data['databaseId'];
/**
* Test for SUCCESS
*/
$actors = $this->client->call(Client::METHOD_PUT, '/databases/' . $databaseId . '/grids/tables/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'name' => 'Actors1',
'rowSecurity' => true,
]);
$this->assertEquals(200, $actors['headers']['status-code']);
$this->assertNotEmpty($actors['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$id}.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEmpty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '');
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals('Actors1', $webhook['data']['name']);
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertCount(4, $webhook['data']['$permissions']);
return array_merge(['actorsId' => $actors['body']['$id']]);
}
/**
* @depends testCreateColumns
*/
public function testCreateDeleteColumnIndexes($data): array
{
$actorsId = $data['actorsId'];
$databaseId = $data['databaseId'];
$index = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables/' . $data['actorsId'] . '/indexes', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'key' => 'fullname',
'type' => 'key',
'columns' => ['lastName', 'firstName'],
'orders' => ['ASC', 'ASC'],
]);
$this->assertEquals(202, $index['headers']['status-code']);
$this->assertEquals('fullname', $index['body']['key']);
// wait for database worker to create index
sleep(5);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.indexes.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.indexes.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.indexes.*", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.indexes.*.create", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''));
// Remove index
$this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/tables/' . $data['actorsId'] . '/indexes/' . $index['body']['key'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
// // wait for database worker to remove index
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
// $this->assertEquals($webhook['method'], 'DELETE');
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.indexes.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.indexes.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.indexes.*", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$actorsId}.indexes.*.update", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertTrue(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''));
return $data;
}
public function testDeleteTable(): array
{
/**
* Create database
*/
$database = $this->client->call(Client::METHOD_POST, '/databases', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
], $this->getHeaders()), [
'databaseId' => ID::unique(),
'name' => 'Actors DB',
]);
$databaseId = $database['body']['$id'];
/**
* Test for SUCCESS
*/
$actors = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/grids/tables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'tableId' => ID::unique(),
'name' => 'Demo',
'permissions' => [
Permission::read(Role::any()),
Permission::create(Role::any()),
Permission::update(Role::any()),
Permission::delete(Role::any()),
],
'rowSecurity' => true,
]);
$id = $actors['body']['$id'];
$this->assertEquals(201, $actors['headers']['status-code']);
$this->assertNotEmpty($actors['body']['$id']);
$actors = $this->client->call(Client::METHOD_DELETE, '/databases/' . $databaseId . '/grids/tables/' . $actors['body']['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]));
$this->assertEquals(204, $actors['headers']['status-code']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('databases.' . $databaseId . '.tables.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("databases.{$databaseId}.tables.{$id}.delete", $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEmpty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? '');
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals('Demo', $webhook['data']['name']);
$this->assertIsArray($webhook['data']['$permissions']);
$this->assertCount(4, $webhook['data']['$permissions']);
@@ -224,7 +411,7 @@ class WebhooksCustomServerTest extends Scope
'name' => $name,
]);
$this->assertEquals($user['headers']['status-code'], 201);
$this->assertEquals(201, $user['headers']['status-code']);
$this->assertNotEmpty($user['body']['$id']);
$id = $user['body']['$id'];
@@ -232,9 +419,9 @@ class WebhooksCustomServerTest extends Scope
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
$this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('users.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -245,11 +432,11 @@ class WebhooksCustomServerTest extends Scope
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['name'], $name);
$this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['registration']));
$this->assertEquals($webhook['data']['status'], true);
$this->assertTrue((new DatetimeValidator())->isValid($webhook['data']['registration']));
$this->assertTrue($webhook['data']['status']);
$this->assertEquals($webhook['data']['email'], $email);
$this->assertEquals($webhook['data']['emailVerification'], false);
$this->assertEquals($webhook['data']['prefs'], []);
$this->assertFalse($webhook['data']['emailVerification']);
$this->assertEquals([], $webhook['data']['prefs']);
/**
* Test for FAILURE
@@ -274,15 +461,15 @@ class WebhooksCustomServerTest extends Scope
'prefs' => ['a' => 'b']
]);
$this->assertEquals($user['headers']['status-code'], 200);
$this->assertEquals($user['body']['a'], 'b');
$this->assertEquals(200, $user['headers']['status-code']);
$this->assertEquals('b', $user['body']['a']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
$this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('users.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('users.*.update.prefs', $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -293,7 +480,7 @@ class WebhooksCustomServerTest extends Scope
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
$this->assertEquals($webhook['data']['a'], 'b');
$this->assertEquals('b', $webhook['data']['a']);
return $data;
}
@@ -315,15 +502,15 @@ class WebhooksCustomServerTest extends Scope
'status' => false,
]);
$this->assertEquals($user['headers']['status-code'], 200);
$this->assertEquals(200, $user['headers']['status-code']);
$this->assertNotEmpty($user['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
$this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('users.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('users.*.update.status', $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -336,11 +523,11 @@ class WebhooksCustomServerTest extends Scope
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['name'], $data['name']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['registration']));
$this->assertEquals($webhook['data']['status'], false);
$this->assertTrue((new DatetimeValidator())->isValid($webhook['data']['registration']));
$this->assertFalse($webhook['data']['status']);
$this->assertEquals($webhook['data']['email'], $data['email']);
$this->assertEquals($webhook['data']['emailVerification'], false);
$this->assertEquals($webhook['data']['prefs']['a'], 'b');
$this->assertFalse($webhook['data']['emailVerification']);
$this->assertEquals('b', $webhook['data']['prefs']['a']);
return $data;
}
@@ -360,14 +547,14 @@ class WebhooksCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($user['headers']['status-code'], 204);
$this->assertEquals(204, $user['headers']['status-code']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
$this->assertStringContainsString('users.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString('users.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']);
$this->assertStringContainsString("users.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -378,11 +565,11 @@ class WebhooksCustomServerTest extends Scope
$this->assertEquals(empty($webhook['headers']['X-Appwrite-Webhook-User-Id'] ?? ''), ('server' === $this->getSide()));
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertEquals($webhook['data']['name'], $data['name']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['registration']));
$this->assertEquals($webhook['data']['status'], false);
$this->assertTrue((new DatetimeValidator())->isValid($webhook['data']['registration']));
$this->assertFalse($webhook['data']['status']);
$this->assertEquals($webhook['data']['email'], $data['email']);
$this->assertEquals($webhook['data']['emailVerification'], false);
$this->assertEquals($webhook['data']['prefs']['a'], 'b');
$this->assertFalse($webhook['data']['emailVerification']);
$this->assertEquals('b', $webhook['data']['prefs']['a']);
return $data;
}
@@ -399,22 +586,22 @@ class WebhooksCustomServerTest extends Scope
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::any()->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 10,
]);
$id = $function['body']['$id'] ?? '';
$this->assertEquals($function['headers']['status-code'], 201);
$this->assertEquals(201, $function['headers']['status-code']);
$this->assertNotEmpty($function['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Signature'], $signatureExpected);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Id'] ?? '', $this->getProject()['webhookId']);
$this->assertEquals($webhook['headers']['X-Appwrite-Webhook-Project-Id'] ?? '', $this->getProject()['$id']);
@@ -439,15 +626,15 @@ class WebhooksCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'name' => 'Test',
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'execute' => [Role::any()->toString()],
'vars' => [
'key1' => 'value1',
]
]);
$this->assertEquals($function['headers']['status-code'], 200);
$this->assertEquals(200, $function['headers']['status-code']);
$this->assertEquals($function['body']['$id'], $data['functionId']);
// Create variable
@@ -464,9 +651,9 @@ class WebhooksCustomServerTest extends Scope
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
// $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
// $this->assertStringContainsString('functions.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
// $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -496,7 +683,7 @@ class WebhooksCustomServerTest extends Scope
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'entrypoint' => 'index.php',
'entrypoint' => 'index.js',
'code' => new CURLFile($code, 'application/x-gzip', \basename($code)),
'activate' => true
]);
@@ -504,15 +691,15 @@ class WebhooksCustomServerTest extends Scope
$functionId = $data['functionId'] ?? '';
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEquals($deployment['headers']['status-code'], 202);
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
// $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
// $this->assertStringContainsString('functions.*.deployments.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
// $this->assertStringContainsString("functions.*.deployments.{$deploymentId}", $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -544,7 +731,7 @@ class WebhooksCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), []);
$this->assertEquals($response['headers']['status-code'], 200);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
// Wait for deployment to be built.
@@ -553,9 +740,9 @@ class WebhooksCustomServerTest extends Scope
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
// $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
// $this->assertStringContainsString('functions.*.deployments.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
// $this->assertStringContainsString('functions.*.deployments.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -596,14 +783,14 @@ class WebhooksCustomServerTest extends Scope
$executionId = $execution['body']['$id'] ?? '';
$this->assertEquals($execution['headers']['status-code'], 202);
$this->assertEquals(202, $execution['headers']['status-code']);
$this->assertNotEmpty($execution['body']['$id']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
// $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
// $this->assertStringContainsString('functions.*.executions.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
// $this->assertStringContainsString('functions.*.executions.*.create', $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -624,9 +811,9 @@ class WebhooksCustomServerTest extends Scope
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
// $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
// $this->assertStringContainsString('functions.*.executions.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
// $this->assertStringContainsString('functions.*.executions.*.update', $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -663,15 +850,15 @@ class WebhooksCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($deployment['headers']['status-code'], 204);
$this->assertEquals(204, $deployment['headers']['status-code']);
$this->assertEmpty($deployment['body']);
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
// $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
// $this->assertStringContainsString('functions.*.deployments.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
// $this->assertStringContainsString('functions.*.deployments.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']);
@@ -714,9 +901,9 @@ class WebhooksCustomServerTest extends Scope
$webhook = $this->getLastRequest();
$signatureExpected = self::getWebhookSignature($webhook, $this->getProject()['signatureKey']);
$this->assertEquals($webhook['method'], 'POST');
$this->assertEquals($webhook['headers']['Content-Type'], 'application/json');
$this->assertEquals($webhook['headers']['User-Agent'], 'Appwrite-Server vdev. Please report abuse at security@appwrite.io');
$this->assertEquals('POST', $webhook['method']);
$this->assertEquals('application/json', $webhook['headers']['Content-Type']);
$this->assertEquals('Appwrite-Server vdev. Please report abuse at security@appwrite.io', $webhook['headers']['User-Agent']);
// $this->assertStringContainsString('functions.*', $webhook['headers']['X-Appwrite-Webhook-Events']);
// $this->assertStringContainsString('functions.*.delete', $webhook['headers']['X-Appwrite-Webhook-Events']);
// $this->assertStringContainsString("functions.{$id}", $webhook['headers']['X-Appwrite-Webhook-Events']);
+46
View File
@@ -0,0 +1,46 @@
module.exports = async(context) => {
context.log('log-works');
context.error('error-log-works');
if(context.req.headers['x-appwrite-user-jwt']) {
context.log('jwt-is-valid');
} else {
context.log('jwt-is-invalid');
}
if(context.req.path === '/custom-response') {
const code = +(context.req.query['code'] || '200');
const body = context.req.query['body'] || '';
return context.res.send(body, code);
}
context.log('body-is-' + (context.req.body ?? ''));
context.log('custom-header-is-' + (context.req.headers['x-custom-header'] ?? ''));
context.log('method-is-' + (context.req.method ?? '').toLowerCase());
context.log('path-is-' + (context.req.path ?? ''));
context.log('user-is-' + (context.req.headers['x-appwrite-user-id'] ?? ''));
const statusCode = context.req.query['code'] || '200';
return context.res.json({
'APPWRITE_FUNCTION_ID' : process.env.APPWRITE_FUNCTION_ID ?? '',
'APPWRITE_FUNCTION_NAME' : process.env.APPWRITE_FUNCTION_NAME ?? '',
'APPWRITE_FUNCTION_DEPLOYMENT' : process.env.APPWRITE_FUNCTION_DEPLOYMENT ?? '',
'APPWRITE_FUNCTION_TRIGGER' : context.req.headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_NAME' : process.env.APPWRITE_FUNCTION_RUNTIME_NAME,
'APPWRITE_FUNCTION_RUNTIME_VERSION' : process.env.APPWRITE_FUNCTION_RUNTIME_VERSION,
'APPWRITE_VERSION' : process.env.APPWRITE_VERSION ?? '',
'APPWRITE_REGION' : process.env.APPWRITE_REGION ?? '',
'UNICODE_TEST' : "êä",
'GLOBAL_VARIABLE' : process.env.GLOBAL_VARIABLE ?? '',
'APPWRITE_FUNCTION_EVENT' : context.req.headers['x-appwrite-event'] ?? '',
'APPWRITE_FUNCTION_EVENT_DATA' : context.req.bodyRaw ?? '',
'APPWRITE_FUNCTION_DATA' : context.req.bodyRaw ?? '',
'APPWRITE_FUNCTION_USER_ID' : context.req.headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' : context.req.headers['x-appwrite-user-jwt'] ?? '',
'APPWRITE_FUNCTION_PROJECT_ID' : process.env.APPWRITE_FUNCTION_PROJECT_ID,
'APPWRITE_FUNCTION_MEMORY' : process.env.APPWRITE_FUNCTION_MEMORY,
'APPWRITE_FUNCTION_CPUS' : process.env.APPWRITE_FUNCTION_CPUS,
'CUSTOM_VARIABLE' : process.env.CUSTOM_VARIABLE
}, +statusCode);
}
@@ -0,0 +1,6 @@
const crypto = require('crypto')
module.exports = async(context) => {
const hash = crypto.createHash('md5').update(context.req.bodyBinary).digest("hex")
return context.res.send(hash);
};
@@ -0,0 +1,4 @@
module.exports = async(context) => {
const bytes = Buffer.from(Uint8Array.from([0, 10, 255]));
return context.res.binary(bytes);
};
@@ -0,0 +1,3 @@
module.exports = async(context) => {
return context.res.send(context.req.headers['cookie'] ?? '');
};
-21
View File
@@ -1,21 +0,0 @@
import 'dart:io' show Platform;
Future<dynamic> main(final context) async {
context.log('Amazing Function Log');
response.json({
'APPWRITE_FUNCTION_ID' : Platform.environment['APPWRITE_FUNCTION_ID'] ?? '',
'APPWRITE_FUNCTION_NAME' : Platform.environment['APPWRITE_FUNCTION_NAME'] ?? '',
'APPWRITE_FUNCTION_DEPLOYMENT' : Platform.environment['APPWRITE_FUNCTION_DEPLOYMENT'] ?? '',
'APPWRITE_FUNCTION_TRIGGER' : context.req.headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_NAME' : Platform.environment['APPWRITE_FUNCTION_RUNTIME_NAME'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' : Platform.environment['APPWRITE_FUNCTION_RUNTIME_VERSION'] ?? '',
'APPWRITE_FUNCTION_EVENT' : context.req.headers['x-appwrite-event'] ?? '',
'APPWRITE_FUNCTION_EVENT_DATA' : context.req.bodyRaw ?? '',
'APPWRITE_FUNCTION_DATA' : context.req.bodyRaw ?? '',
'APPWRITE_FUNCTION_USER_ID' : context.req.headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' : context.req.headers['x-appwrite-user-jwt'] ?? '',
'APPWRITE_FUNCTION_PROJECT_ID' : Platform.environment['APPWRITE_FUNCTION_PROJECT_ID'] ?? '',
'CUSTOM_VARIABLE' : request.variables['CUSTOM_VARIABLE']
});
}
@@ -0,0 +1,15 @@
const sdk = require('node-appwrite');
module.exports = async(context) => {
const client = new sdk.Client();
client.setEndpoint(process.env.APPWRITE_FUNCTION_API_ENDPOINT);
client.setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID);
client.setKey(context.req.headers['x-appwrite-key']);
const users = new sdk.Users(client);
const response = await users.list();
context.log(JSON.stringify(response));
return context.res.json(response);
};
@@ -0,0 +1,31 @@
{
"name": "dynamic-api-key",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dynamic-api-key",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"node-appwrite": "^17.0.0"
}
},
"node_modules/node-appwrite": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-17.0.0.tgz",
"integrity": "sha512-5Moi5ENPnoAfU1/6CZP9K2NTuB6Nm3dSyhokno+24RDuP7czjXCdwzfeyjmyHieggbrLkN89AYSOv9W1XkCL9w==",
"license": "BSD-3-Clause",
"dependencies": {
"node-fetch-native-with-agent": "1.7.2"
}
},
"node_modules/node-fetch-native-with-agent": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/node-fetch-native-with-agent/-/node-fetch-native-with-agent-1.7.2.tgz",
"integrity": "sha512-5MaOOCuJEvcckoz7/tjdx1M6OusOY6Xc5f459IaruGStWnKzlI1qpNgaAwmn4LmFYcsSlj+jBMk84wmmRxfk5g==",
"license": "MIT"
}
}
}
@@ -0,0 +1,15 @@
{
"name": "dynamic-api-key",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"node-appwrite": "^17.0.0"
}
}
@@ -1,6 +1,7 @@
ENDPOINT="$APPWRITE_FUNCTION_API_ENDPOINT/users"
PROJECT_ID="$APPWRITE_FUNCTION_PROJECT_ID"
API_KEY="$APPWRITE_FUNCTION_API_KEY"
apk add curl
curl -v -X GET $ENDPOINT -H "x-appwrite-project: $PROJECT_ID" -H "x-appwrite-key: $API_KEY"
@@ -0,0 +1,5 @@
module.exports = async(context) => {
context.log(context.req.body.$id);
context.log(context.req.body.name);
return context.res.empty();
};
+3
View File
@@ -0,0 +1,3 @@
module.exports = async(context) => {
return context.res.empty();
};
+1 -19
View File
@@ -1,21 +1,3 @@
module.exports = async(context) => {
context.log('Amazing Function Log');
return context.res.json({
'APPWRITE_FUNCTION_ID' : process.env.APPWRITE_FUNCTION_ID ?? '',
'APPWRITE_FUNCTION_NAME' : process.env.APPWRITE_FUNCTION_NAME ?? '',
'APPWRITE_FUNCTION_DEPLOYMENT' : process.env.APPWRITE_FUNCTION_DEPLOYMENT ?? '',
'APPWRITE_FUNCTION_TRIGGER' : context.req.headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_NAME' : process.env.APPWRITE_FUNCTION_RUNTIME_NAME,
'APPWRITE_FUNCTION_RUNTIME_VERSION' : process.env.APPWRITE_FUNCTION_RUNTIME_VERSION,
'APPWRITE_FUNCTION_EVENT' : context.req.headers['x-appwrite-event'] ?? '',
'APPWRITE_FUNCTION_EVENT_DATA' : context.req.bodyRaw ?? '',
'APPWRITE_FUNCTION_DATA' : context.req.bodyRaw ?? '',
'APPWRITE_FUNCTION_USER_ID' : context.req.headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' : context.req.headers['x-appwrite-user-jwt'] ?? '',
'APPWRITE_FUNCTION_PROJECT_ID' : process.env.APPWRITE_FUNCTION_PROJECT_ID,
'APPWRITE_FUNCTION_MEMORY' : process.env.APPWRITE_FUNCTION_MEMORY,
'APPWRITE_FUNCTION_CPUS' : process.env.APPWRITE_FUNCTION_CPUS,
'CUSTOM_VARIABLE' : process.env.CUSTOM_VARIABLE
});
return context.res.send('OK');
}
@@ -1,6 +0,0 @@
<?php
return function ($context) {
$hash = md5($context->req->bodyBinary);
return $context->res->send($hash);
};
@@ -1,6 +0,0 @@
<?php
return function ($context) {
$bytes = pack('C*', ...[0, 10, 255]);
return $context->res->binary($bytes);
};
@@ -1,5 +0,0 @@
<?php
return function ($context) {
return $context->res->send($context->req->headers['cookie'] ?? '');
};
@@ -1,8 +0,0 @@
<?php
return function ($context) {
$context->log($context->req->body['$id']);
$context->log($context->req->body['name']);
return $context->res->empty();
};
@@ -1,23 +0,0 @@
<?php
return function ($context) {
$context->log('Amazing Function Log');
return $context->res->json([
'APPWRITE_FUNCTION_ID' => \getenv('APPWRITE_FUNCTION_ID') ?: '',
'APPWRITE_FUNCTION_NAME' => \getenv('APPWRITE_FUNCTION_NAME') ?: '',
'APPWRITE_FUNCTION_DEPLOYMENT' => \getenv('APPWRITE_FUNCTION_DEPLOYMENT') ?: '',
'APPWRITE_FUNCTION_TRIGGER' => $context->req->headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_NAME' => \getenv('APPWRITE_FUNCTION_RUNTIME_NAME') ?: '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '',
'APPWRITE_VERSION' => \getenv('APPWRITE_VERSION') ?: '',
'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '',
'APPWRITE_FUNCTION_EVENT' => $context->req->headers['x-appwrite-event'] ?? '',
'APPWRITE_FUNCTION_EVENT_DATA' => $context->req->bodyRaw ?? '',
'APPWRITE_FUNCTION_DATA' => $context->req->bodyRaw ?? '',
'APPWRITE_FUNCTION_USER_ID' => $context->req->headers['x-appwrite-user-id'] ?? '',
'APPWRITE_FUNCTION_JWT' => $context->req->headers['x-appwrite-user-jwt'] ?? '',
'APPWRITE_FUNCTION_PROJECT_ID' => \getenv('APPWRITE_FUNCTION_PROJECT_ID') ?: '',
'CUSTOM_VARIABLE' => \getenv('CUSTOM_VARIABLE') ?: '',
]);
};
@@ -1,18 +0,0 @@
{
"name": "appwrite/cloud-function-demo",
"description": "Demo cloud function script",
"type": "library",
"license": "BSD-3-Clause",
"authors": [
{
"name": "Team Appwrite",
"email": "team@appwrite.io"
}
],
"require": {
"php": ">=7.4.0",
"ext-curl": "*",
"ext-json": "*",
"appwrite/appwrite": "1.1.*"
}
}
-64
View File
@@ -1,64 +0,0 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "afdff6a172e6c44aee11f1562175f81a",
"packages": [
{
"name": "appwrite/appwrite",
"version": "1.1.2",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-for-php.git",
"reference": "98b327d3fd18a72f4582019916afd735a0e9e0e7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-for-php/zipball/98b327d3fd18a72f4582019916afd735a0e9e0e7",
"reference": "98b327d3fd18a72f4582019916afd735a0e9e0e7",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"php": ">=7.1.0"
},
"require-dev": {
"phpunit/phpunit": "3.7.35"
},
"type": "library",
"autoload": {
"psr-4": {
"Appwrite\\": "src/Appwrite"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "Appwrite is an open-source backend as a service server that abstract and simplify complex and repetitive development tasks",
"support": {
"email": "team@localhost.test",
"issues": "https://github.com/appwrite/sdk-for-php/issues",
"source": "https://github.com/appwrite/sdk-for-php/tree/1.1.2",
"url": "https://appwrite.io/support"
},
"time": "2020-08-15T18:24:32+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=7.4.0",
"ext-curl": "*",
"ext-json": "*"
},
"platform-dev": [],
"plugin-api-version": "2.0.0"
}
@@ -1,16 +0,0 @@
<?php
return function ($context) {
return $context->res->json([
'APPWRITE_FUNCTION_ID' => \getenv('APPWRITE_FUNCTION_ID') ?: '',
'APPWRITE_FUNCTION_NAME' => \getenv('APPWRITE_FUNCTION_NAME') ?: '',
'APPWRITE_FUNCTION_DEPLOYMENT' => \getenv('APPWRITE_FUNCTION_DEPLOYMENT') ?: '',
'APPWRITE_FUNCTION_TRIGGER' => $context->req->headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_NAME' => \getenv('APPWRITE_FUNCTION_RUNTIME_NAME') ?: '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '',
'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '',
'APPWRITE_FUNCTION_CPUS' => \getenv('APPWRITE_FUNCTION_CPUS') ?: '',
'APPWRITE_FUNCTION_MEMORY' => \getenv('APPWRITE_FUNCTION_MEMORY') ?: '',
'UNICODE_TEST' => "êä"
]);
};
@@ -1,18 +0,0 @@
{
"name": "appwrite/php-scopes",
"description": "PHP scopes test script",
"type": "library",
"license": "BSD-3-Clause",
"authors": [
{
"name": "Team Appwrite",
"email": "team@appwrite.io"
}
],
"require": {
"php": ">=7.4.0",
"ext-curl": "*",
"ext-json": "*",
"appwrite/appwrite": "11.0.*"
}
}
@@ -1,18 +0,0 @@
<?php
require 'vendor/autoload.php';
use Appwrite\Client;
use Appwrite\Services\Users;
return function ($context) {
$client = new Client();
$client
->setEndpoint(getenv('APPWRITE_FUNCTION_API_ENDPOINT'))
->setProject(getenv('APPWRITE_FUNCTION_PROJECT_ID'))
->setKey($context->req->headers['x-appwrite-key']);
$users = new Users($client);
$response = $users->list();
$context->log($response);
return $context->res->json($response);
};
@@ -1,7 +0,0 @@
<?php
return function ($req, $res) {
$res->json([
'v2Woks' => true
]);
};
-39
View File
@@ -1,39 +0,0 @@
<?php
return function ($context) {
if ($context->req->path === '/custom-response') {
$code = (int) ($context->req->query['code'] ?? '200');
$body = $context->req->query['body'] ?? '';
return $context->res->send($body, $code);
}
$context->log('body-is-' . ($context->req->body ?? ''));
$context->log('custom-header-is-' . ($context->req->headers['x-custom-header'] ?? ''));
$context->log('method-is-' . \strtolower($context->req->method ?? ''));
$context->log('path-is-' . ($context->req->path ?? ''));
$context->log('user-is-' . $context->req->headers['x-appwrite-user-id'] ?? '');
if (empty($context->req->headers['x-appwrite-user-jwt'] ?? '')) {
$context->log('jwt-is-invalid');
} else {
$context->log('jwt-is-valid');
}
$context->error('error-log-works');
$statusCode = $context->req->query['code'] ?? '200';
return $context->res->json([
'APPWRITE_FUNCTION_ID' => \getenv('APPWRITE_FUNCTION_ID') ?: '',
'APPWRITE_FUNCTION_NAME' => \getenv('APPWRITE_FUNCTION_NAME') ?: '',
'APPWRITE_FUNCTION_DEPLOYMENT' => \getenv('APPWRITE_FUNCTION_DEPLOYMENT') ?: '',
'APPWRITE_FUNCTION_TRIGGER' => $context->req->headers['x-appwrite-trigger'] ?? '',
'APPWRITE_FUNCTION_RUNTIME_NAME' => \getenv('APPWRITE_FUNCTION_RUNTIME_NAME') ?: '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => \getenv('APPWRITE_FUNCTION_RUNTIME_VERSION') ?: '',
'APPWRITE_REGION' => \getenv('APPWRITE_REGION') ?: '',
'UNICODE_TEST' => "êä",
'GLOBAL_VARIABLE' => \getenv('GLOBAL_VARIABLE') ?: '',
'APPWRITE_FUNCTION_CPUS' => \getenv('APPWRITE_FUNCTION_CPUS') ?: '',
'APPWRITE_FUNCTION_MEMORY' => \getenv('APPWRITE_FUNCTION_MEMORY') ?: '',
], \intval($statusCode));
};
-21
View File
@@ -1,21 +0,0 @@
import json
import os
def main(context):
context.log('Amazing Function Log')
return context.res.json({
'APPWRITE_FUNCTION_ID' : os.environ.get('APPWRITE_FUNCTION_ID',''),
'APPWRITE_FUNCTION_NAME' : os.environ.get('APPWRITE_FUNCTION_NAME',''),
'APPWRITE_FUNCTION_DEPLOYMENT' : os.environ.get('APPWRITE_FUNCTION_DEPLOYMENT',''),
'APPWRITE_FUNCTION_TRIGGER' : context.req.headers.get('x-appwrite-trigger', ''),
'APPWRITE_FUNCTION_RUNTIME_NAME' : os.environ.get('APPWRITE_FUNCTION_RUNTIME_NAME',''),
'APPWRITE_FUNCTION_RUNTIME_VERSION' : os.environ.get('APPWRITE_FUNCTION_RUNTIME_VERSION',''),
'APPWRITE_FUNCTION_EVENT' : context.req.headers.get('x-appwrite-event', ''),
'APPWRITE_FUNCTION_EVENT_DATA' : context.req.body_raw,
'APPWRITE_FUNCTION_DATA' : context.req.body_raw,
'APPWRITE_FUNCTION_USER_ID' : context.req.headers.get('x-appwrite-user-id', ''),
'APPWRITE_FUNCTION_JWT' : context.req.headers.get('x-appwrite-user-jwt', ''),
'APPWRITE_FUNCTION_PROJECT_ID' : os.environ.get('APPWRITE_FUNCTION_PROJECT_ID',''),
'CUSTOM_VARIABLE' : os.environ.get('CUSTOM_VARIABLE',''),
})
-19
View File
@@ -1,19 +0,0 @@
def main(context)
context.log('Amazing Function Log')
return context.res.json({
'APPWRITE_FUNCTION_ID' => ENV['APPWRITE_FUNCTION_ID'] || '',
'APPWRITE_FUNCTION_NAME' => ENV['APPWRITE_FUNCTION_NAME'] || '',
'APPWRITE_FUNCTION_DEPLOYMENT' => ENV['APPWRITE_FUNCTION_DEPLOYMENT'] || '',
'APPWRITE_FUNCTION_TRIGGER' => context.req.headers['x-appwrite-trigger'] || '',
'APPWRITE_FUNCTION_RUNTIME_NAME' => ENV['APPWRITE_FUNCTION_RUNTIME_NAME'] || '',
'APPWRITE_FUNCTION_RUNTIME_VERSION' => ENV['APPWRITE_FUNCTION_RUNTIME_VERSION'] || '',
'APPWRITE_FUNCTION_EVENT' => context.req.headers['x-appwrite-event'] || '',
'APPWRITE_FUNCTION_EVENT_DATA' => context.req.body_raw || '',
'APPWRITE_FUNCTION_DATA' => context.req.body_raw || '',
'APPWRITE_FUNCTION_USER_ID' => context.req.headers['x-appwrite-user-id'] || '',
'APPWRITE_FUNCTION_JWT' => context.req.headers['x-appwrite-user-jwt'] || '',
'APPWRITE_FUNCTION_PROJECT_ID' => ENV['APPWRITE_FUNCTION_PROJECT_ID'] || '',
'CUSTOM_VARIABLE' => ENV['CUSTOM_VARIABLE'] || ''
})
end
@@ -1,17 +0,0 @@
func main(req: RequestValue, res: RequestResponse) throws -> RequestResponse {
return res.json(data: [
"APPWRITE_FUNCTION_ID": req.variables["APPWRITE_FUNCTION_ID"],
"APPWRITE_FUNCTION_NAME": req.variables["APPWRITE_FUNCTION_NAME"],
"APPWRITE_FUNCTION_DEPLOYMENT": req.variables["APPWRITE_FUNCTION_DEPLOYMENT"],
"APPWRITE_FUNCTION_TRIGGER": req.variables["APPWRITE_FUNCTION_TRIGGER"],
"APPWRITE_FUNCTION_RUNTIME_NAME": req.variables["APPWRITE_FUNCTION_RUNTIME_NAME"],
"APPWRITE_FUNCTION_RUNTIME_VERSION": req.variables["APPWRITE_FUNCTION_RUNTIME_VERSION"],
"APPWRITE_FUNCTION_EVENT": req.variables["APPWRITE_FUNCTION_EVENT"],
"APPWRITE_FUNCTION_EVENT_DATA": req.variables["APPWRITE_FUNCTION_EVENT_DATA"],
"APPWRITE_FUNCTION_DATA": req.variables["APPWRITE_FUNCTION_DATA"],
"APPWRITE_FUNCTION_USER_ID": req.variables["APPWRITE_FUNCTION_USER_ID"],
"APPWRITE_FUNCTION_JWT": req.variables["APPWRITE_FUNCTION_JWT"],
"APPWRITE_FUNCTION_PROJECT_ID": req.variables["APPWRITE_FUNCTION_PROJECT_ID"],
"CUSTOM_VARIABLE": req.variables["CUSTOM_VARIABLE"]
])
}
@@ -0,0 +1,4 @@
module.exports = async(context) => {
await new Promise(resolve => setTimeout(resolve, 1000 * 60));
return context.res.send('OK');
};
@@ -1,6 +0,0 @@
<?php
return function ($context) {
sleep(60);
return $context->res->send('OK');
};
+39 -39
View File
@@ -93,57 +93,57 @@ class EventTest extends TestCase
$this->assertContains('users.*.update', $event);
$this->assertContains('users.*', $event);
$event = Event::generateEvents('collections.[collectionId].documents.[documentId].create', [
'collectionId' => 'chapters',
'documentId' => 'prolog',
$event = Event::generateEvents('tables.[tableId].rows.[rowId].create', [
'tableId' => 'chapters',
'rowId' => 'prolog',
]);
$this->assertCount(10, $event);
$this->assertContains('collections.chapters.documents.prolog.create', $event);
$this->assertContains('collections.chapters.documents.prolog', $event);
$this->assertContains('collections.chapters.documents.*.create', $event);
$this->assertContains('collections.chapters.documents.*', $event);
$this->assertContains('collections.chapters', $event);
$this->assertContains('collections.*.documents.prolog.create', $event);
$this->assertContains('collections.*.documents.prolog', $event);
$this->assertContains('collections.*.documents.*.create', $event);
$this->assertContains('collections.*.documents.*', $event);
$this->assertContains('collections.*', $event);
$this->assertContains('tables.chapters.rows.prolog.create', $event);
$this->assertContains('tables.chapters.rows.prolog', $event);
$this->assertContains('tables.chapters.rows.*.create', $event);
$this->assertContains('tables.chapters.rows.*', $event);
$this->assertContains('tables.chapters', $event);
$this->assertContains('tables.*.rows.prolog.create', $event);
$this->assertContains('tables.*.rows.prolog', $event);
$this->assertContains('tables.*.rows.*.create', $event);
$this->assertContains('tables.*.rows.*', $event);
$this->assertContains('tables.*', $event);
$event = Event::generateEvents('databases.[databaseId].collections.[collectionId].documents.[documentId].create', [
$event = Event::generateEvents('databases.[databaseId].tables.[tableId].rows.[rowId].create', [
'databaseId' => 'chaptersDB',
'collectionId' => 'chapters',
'documentId' => 'prolog',
'tableId' => 'chapters',
'rowId' => 'prolog',
]);
$this->assertCount(22, $event);
$this->assertContains('databases.chaptersDB.collections.chapters.documents.prolog.create', $event);
$this->assertContains('databases.chaptersDB.collections.chapters.documents.prolog', $event);
$this->assertContains('databases.chaptersDB.collections.chapters.documents.*.create', $event);
$this->assertContains('databases.chaptersDB.collections.chapters.documents.*', $event);
$this->assertContains('databases.chaptersDB.collections.chapters', $event);
$this->assertContains('databases.chaptersDB.collections.*.documents.prolog.create', $event);
$this->assertContains('databases.chaptersDB.collections.*.documents.prolog', $event);
$this->assertContains('databases.chaptersDB.collections.*', $event);
$this->assertContains('databases.chaptersDB.tables.chapters.rows.prolog.create', $event);
$this->assertContains('databases.chaptersDB.tables.chapters.rows.prolog', $event);
$this->assertContains('databases.chaptersDB.tables.chapters.rows.*.create', $event);
$this->assertContains('databases.chaptersDB.tables.chapters.rows.*', $event);
$this->assertContains('databases.chaptersDB.tables.chapters', $event);
$this->assertContains('databases.chaptersDB.tables.*.rows.prolog.create', $event);
$this->assertContains('databases.chaptersDB.tables.*.rows.prolog', $event);
$this->assertContains('databases.chaptersDB.tables.*', $event);
$this->assertContains('databases.chaptersDB', $event);
$this->assertContains('databases.*.collections.chapters.documents.prolog.create', $event);
$this->assertContains('databases.*.collections.chapters.documents.prolog', $event);
$this->assertContains('databases.*.collections.chapters', $event);
$this->assertContains('databases.*.collections.*.documents.*.create', $event);
$this->assertContains('databases.*.collections.*.documents.*', $event);
$this->assertContains('databases.*.collections.*', $event);
$this->assertContains('databases.*.tables.chapters.rows.prolog.create', $event);
$this->assertContains('databases.*.tables.chapters.rows.prolog', $event);
$this->assertContains('databases.*.tables.chapters', $event);
$this->assertContains('databases.*.tables.*.rows.*.create', $event);
$this->assertContains('databases.*.tables.*.rows.*', $event);
$this->assertContains('databases.*.tables.*', $event);
$this->assertContains('databases.*', $event);
$this->assertContains('databases.*.collections.*.documents.prolog', $event);
$this->assertContains('databases.*.collections.*.documents.prolog.create', $event);
$this->assertContains('databases.*.collections.chapters.documents.*', $event);
$this->assertContains('databases.*.collections.chapters.documents.*.create', $event);
$this->assertContains('databases.chaptersDB.collections.*.documents.*', $event);
$this->assertContains('databases.chaptersDB.collections.*.documents.*.create', $event);
$this->assertContains('databases.*.tables.*.rows.prolog', $event);
$this->assertContains('databases.*.tables.*.rows.prolog.create', $event);
$this->assertContains('databases.*.tables.chapters.rows.*', $event);
$this->assertContains('databases.*.tables.chapters.rows.*.create', $event);
$this->assertContains('databases.chaptersDB.tables.*.rows.*', $event);
$this->assertContains('databases.chaptersDB.tables.*.rows.*.create', $event);
try {
$event = Event::generateEvents('collections.[collectionId].documents.[documentId].create', [
'collectionId' => 'chapters'
$event = Event::generateEvents('tables.[tableId].rows.[rowId].create', [
'tableId' => 'chapters'
]);
$this->fail();
} catch (\Throwable $th) {
@@ -151,7 +151,7 @@ class EventTest extends TestCase
}
try {
$event = Event::generateEvents('collections.[collectionId].documents.[documentId].create');
$event = Event::generateEvents('tables.[tableId].rows.[rowId].create');
$this->fail();
} catch (\Throwable $th) {
$this->assertInstanceOf(InvalidArgumentException::class, $th, 'An invalid exception was thrown');
+1 -1
View File
@@ -7,7 +7,7 @@ use Utopia\Queue\Queue;
class MockPublisher implements Publisher
{
private $events = [];
private array $events = [];
public function enqueue(Queue $queue, array $payload): bool
{
@@ -29,27 +29,27 @@ class EventValidatorTest extends TestCase
$this->assertTrue($this->object->isValid('users.*.update.email'));
$this->assertTrue($this->object->isValid('users.*.update'));
$this->assertTrue($this->object->isValid('users.*'));
$this->assertTrue($this->object->isValid('databases.books.collections.chapters.documents.prolog.create'));
$this->assertTrue($this->object->isValid('databases.books.collections.chapters.documents.prolog'));
$this->assertTrue($this->object->isValid('databases.books.collections.chapters.documents.*.create'));
$this->assertTrue($this->object->isValid('databases.books.collections.chapters.documents.*'));
$this->assertTrue($this->object->isValid('databases.books.collections.*.documents.prolog.create'));
$this->assertTrue($this->object->isValid('databases.books.collections.*.documents.prolog'));
$this->assertTrue($this->object->isValid('databases.books.collections.*.documents.*.create'));
$this->assertTrue($this->object->isValid('databases.books.collections.*.documents.*'));
$this->assertTrue($this->object->isValid('databases.*.collections.chapters.documents.prolog.create'));
$this->assertTrue($this->object->isValid('databases.*.collections.chapters.documents.prolog'));
$this->assertTrue($this->object->isValid('databases.*.collections.chapters.documents.*.create'));
$this->assertTrue($this->object->isValid('databases.*.collections.chapters.documents.*'));
$this->assertTrue($this->object->isValid('databases.*.collections.*.documents.prolog.create'));
$this->assertTrue($this->object->isValid('databases.*.collections.*.documents.prolog'));
$this->assertTrue($this->object->isValid('databases.*.collections.*.documents.*.create'));
$this->assertTrue($this->object->isValid('databases.*.collections.*.documents.*'));
$this->assertTrue($this->object->isValid('databases.*.collections.*'));
$this->assertTrue($this->object->isValid('databases.books.tables.chapters.rows.prolog.create'));
$this->assertTrue($this->object->isValid('databases.books.tables.chapters.rows.prolog'));
$this->assertTrue($this->object->isValid('databases.books.tables.chapters.rows.*.create'));
$this->assertTrue($this->object->isValid('databases.books.tables.chapters.rows.*'));
$this->assertTrue($this->object->isValid('databases.books.tables.*.rows.prolog.create'));
$this->assertTrue($this->object->isValid('databases.books.tables.*.rows.prolog'));
$this->assertTrue($this->object->isValid('databases.books.tables.*.rows.*.create'));
$this->assertTrue($this->object->isValid('databases.books.tables.*.rows.*'));
$this->assertTrue($this->object->isValid('databases.*.tables.chapters.rows.prolog.create'));
$this->assertTrue($this->object->isValid('databases.*.tables.chapters.rows.prolog'));
$this->assertTrue($this->object->isValid('databases.*.tables.chapters.rows.*.create'));
$this->assertTrue($this->object->isValid('databases.*.tables.chapters.rows.*'));
$this->assertTrue($this->object->isValid('databases.*.tables.*.rows.prolog.create'));
$this->assertTrue($this->object->isValid('databases.*.tables.*.rows.prolog'));
$this->assertTrue($this->object->isValid('databases.*.tables.*.rows.*.create'));
$this->assertTrue($this->object->isValid('databases.*.tables.*.rows.*'));
$this->assertTrue($this->object->isValid('databases.*.tables.*'));
$this->assertTrue($this->object->isValid('databases.*'));
$this->assertTrue($this->object->isValid('databases.books'));
$this->assertTrue($this->object->isValid('databases.books.collections.chapters'));
$this->assertTrue($this->object->isValid('databases.books.collections.*'));
$this->assertTrue($this->object->isValid('databases.books.tables.chapters'));
$this->assertTrue($this->object->isValid('databases.books.tables.*'));
$this->assertTrue($this->object->isValid('functions.*'));
$this->assertTrue($this->object->isValid('buckets.*'));
$this->assertTrue($this->object->isValid('teams.*'));
@@ -63,9 +63,9 @@ class EventValidatorTest extends TestCase
$this->assertFalse($this->object->isValid(null));
$this->assertFalse($this->object->isValid(''));
$this->assertFalse($this->object->isValid('unknown.*'));
$this->assertFalse($this->object->isValid('collections'));
$this->assertFalse($this->object->isValid('collections.*.unknown'));
$this->assertFalse($this->object->isValid('collections.*.documents.*.unknown'));
$this->assertFalse($this->object->isValid('tables'));
$this->assertFalse($this->object->isValid('tables.*.unknown'));
$this->assertFalse($this->object->isValid('tables.*.rows.*.unknown'));
$this->assertFalse($this->object->isValid('users.torsten.unknown'));
$this->assertFalse($this->object->isValid('users.torsten.delete.email'));
$this->assertFalse($this->object->isValid('teams.*.memberships.*.update.unknown'));
@@ -29,27 +29,27 @@ class FunctionEventValidatorTest extends TestCase
$this->assertTrue($this->object->isValid('users.*.update.email'));
$this->assertTrue($this->object->isValid('users.*.update'));
$this->assertTrue($this->object->isValid('users.*'));
$this->assertTrue($this->object->isValid('databases.books.collections.chapters.documents.prolog.create'));
$this->assertTrue($this->object->isValid('databases.books.collections.chapters.documents.prolog'));
$this->assertTrue($this->object->isValid('databases.books.collections.chapters.documents.*.create'));
$this->assertTrue($this->object->isValid('databases.books.collections.chapters.documents.*'));
$this->assertTrue($this->object->isValid('databases.books.collections.*.documents.prolog.create'));
$this->assertTrue($this->object->isValid('databases.books.collections.*.documents.prolog'));
$this->assertTrue($this->object->isValid('databases.books.collections.*.documents.*.create'));
$this->assertTrue($this->object->isValid('databases.books.collections.*.documents.*'));
$this->assertTrue($this->object->isValid('databases.*.collections.chapters.documents.prolog.create'));
$this->assertTrue($this->object->isValid('databases.*.collections.chapters.documents.prolog'));
$this->assertTrue($this->object->isValid('databases.*.collections.chapters.documents.*.create'));
$this->assertTrue($this->object->isValid('databases.*.collections.chapters.documents.*'));
$this->assertTrue($this->object->isValid('databases.*.collections.*.documents.prolog.create'));
$this->assertTrue($this->object->isValid('databases.*.collections.*.documents.prolog'));
$this->assertTrue($this->object->isValid('databases.*.collections.*.documents.*.create'));
$this->assertTrue($this->object->isValid('databases.*.collections.*.documents.*'));
$this->assertTrue($this->object->isValid('databases.*.collections.*'));
$this->assertTrue($this->object->isValid('databases.books.tables.chapters.rows.prolog.create'));
$this->assertTrue($this->object->isValid('databases.books.tables.chapters.rows.prolog'));
$this->assertTrue($this->object->isValid('databases.books.tables.chapters.rows.*.create'));
$this->assertTrue($this->object->isValid('databases.books.tables.chapters.rows.*'));
$this->assertTrue($this->object->isValid('databases.books.tables.*.rows.prolog.create'));
$this->assertTrue($this->object->isValid('databases.books.tables.*.rows.prolog'));
$this->assertTrue($this->object->isValid('databases.books.tables.*.rows.*.create'));
$this->assertTrue($this->object->isValid('databases.books.tables.*.rows.*'));
$this->assertTrue($this->object->isValid('databases.*.tables.chapters.rows.prolog.create'));
$this->assertTrue($this->object->isValid('databases.*.tables.chapters.rows.prolog'));
$this->assertTrue($this->object->isValid('databases.*.tables.chapters.rows.*.create'));
$this->assertTrue($this->object->isValid('databases.*.tables.chapters.rows.*'));
$this->assertTrue($this->object->isValid('databases.*.tables.*.rows.prolog.create'));
$this->assertTrue($this->object->isValid('databases.*.tables.*.rows.prolog'));
$this->assertTrue($this->object->isValid('databases.*.tables.*.rows.*.create'));
$this->assertTrue($this->object->isValid('databases.*.tables.*.rows.*'));
$this->assertTrue($this->object->isValid('databases.*.tables.*'));
$this->assertTrue($this->object->isValid('databases.*'));
$this->assertTrue($this->object->isValid('databases.books'));
$this->assertTrue($this->object->isValid('databases.books.collections.chapters'));
$this->assertTrue($this->object->isValid('databases.books.collections.*'));
$this->assertTrue($this->object->isValid('databases.books.tables.chapters'));
$this->assertTrue($this->object->isValid('databases.books.tables.*'));
$this->assertTrue($this->object->isValid('buckets.*'));
$this->assertTrue($this->object->isValid('teams.*'));
$this->assertTrue($this->object->isValid('users.*'));
@@ -62,9 +62,9 @@ class FunctionEventValidatorTest extends TestCase
$this->assertFalse($this->object->isValid(null));
$this->assertFalse($this->object->isValid(''));
$this->assertFalse($this->object->isValid('unknown.*'));
$this->assertFalse($this->object->isValid('collections'));
$this->assertFalse($this->object->isValid('collections.*.unknown'));
$this->assertFalse($this->object->isValid('collections.*.documents.*.unknown'));
$this->assertFalse($this->object->isValid('tables'));
$this->assertFalse($this->object->isValid('tables.*.unknown'));
$this->assertFalse($this->object->isValid('tables.*.rows.*.unknown'));
$this->assertFalse($this->object->isValid('users.torsten.unknown'));
$this->assertFalse($this->object->isValid('users.torsten.delete.email'));
$this->assertFalse($this->object->isValid('teams.*.memberships.*.update.unknown'));
+2 -2
View File
@@ -22,8 +22,8 @@ class BuilderTest extends TestCase
*/
public function testCreateTypeMapping()
{
$model = $this->response->getModel(Response::MODEL_COLLECTION);
$model = $this->response->getModel(Response::MODEL_TABLE);
$type = Mapper::model(\ucfirst($model->getType()));
$this->assertEquals('Collection', $type->name);
$this->assertEquals('Table', $type->name);
}
}
+1
View File
@@ -41,6 +41,7 @@ abstract class MigrationTest extends TestCase
foreach (Migration::$versions as $class) {
$this->assertTrue(class_exists('Appwrite\\Migration\\Version\\' . $class));
}
// Test if current version exists
// Only test official releases - skip if latest is release candidate
if (!(\str_contains(APP_VERSION_STABLE, 'RC'))) {
+67 -29
View File
@@ -2,6 +2,7 @@
namespace Tests\Unit\Network\Validators;
use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Origin;
use PHPUnit\Framework\TestCase;
use Utopia\Database\Helpers\ID;
@@ -14,62 +15,99 @@ class OriginTest extends TestCase
[
'$collection' => ID::custom('platforms'),
'name' => 'Production',
'type' => Origin::CLIENT_TYPE_WEB,
'type' => Platform::TYPE_WEB,
'hostname' => 'appwrite.io',
],
[
'$collection' => ID::custom('platforms'),
'name' => 'Development',
'type' => Origin::CLIENT_TYPE_WEB,
'type' => Platform::TYPE_WEB,
'hostname' => 'appwrite.test',
],
[
'$collection' => ID::custom('platforms'),
'name' => 'Localhost',
'type' => Origin::CLIENT_TYPE_WEB,
'type' => Platform::TYPE_WEB,
'hostname' => 'localhost',
],
[
'$collection' => ID::custom('platforms'),
'name' => 'Flutter',
'type' => Origin::CLIENT_TYPE_FLUTTER_WEB,
'type' => Platform::TYPE_FLUTTER_WEB,
'hostname' => 'appwrite.flutter',
],
[
'$collection' => ID::custom('platforms'),
'name' => 'Expo',
'type' => Platform::TYPE_SCHEME,
'key' => 'exp',
],
[
'$collection' => ID::custom('platforms'),
'name' => 'Appwrite Callback',
'type' => Platform::TYPE_SCHEME,
'key' => 'appwrite-callback-123',
],
]);
$this->assertEquals($validator->isValid('https://localhost'), true);
$this->assertEquals($validator->isValid('http://localhost'), true);
$this->assertEquals($validator->isValid('http://localhost:80'), true);
$this->assertEquals(false, $validator->isValid(''));
$this->assertEquals(false, $validator->isValid('/'));
$this->assertEquals($validator->isValid('https://appwrite.io'), true);
$this->assertEquals($validator->isValid('http://appwrite.io'), true);
$this->assertEquals($validator->isValid('http://appwrite.io:80'), true);
$this->assertEquals(true, $validator->isValid('https://localhost'));
$this->assertEquals(true, $validator->isValid('http://localhost'));
$this->assertEquals(true, $validator->isValid('http://localhost:80'));
$this->assertEquals($validator->isValid('https://appwrite.test'), true);
$this->assertEquals($validator->isValid('http://appwrite.test'), true);
$this->assertEquals($validator->isValid('http://appwrite.test:80'), true);
$this->assertEquals(true, $validator->isValid('https://appwrite.io'));
$this->assertEquals(true, $validator->isValid('http://appwrite.io'));
$this->assertEquals(true, $validator->isValid('http://appwrite.io:80'));
$this->assertEquals($validator->isValid('https://appwrite.flutter'), true);
$this->assertEquals($validator->isValid('http://appwrite.flutter'), true);
$this->assertEquals($validator->isValid('http://appwrite.flutter:80'), true);
$this->assertEquals(true, $validator->isValid('https://appwrite.test'));
$this->assertEquals(true, $validator->isValid('http://appwrite.test'));
$this->assertEquals(true, $validator->isValid('http://appwrite.test:80'));
$this->assertEquals($validator->isValid('https://example.com'), false);
$this->assertEquals($validator->isValid('http://example.com'), false);
$this->assertEquals($validator->isValid('http://example.com:80'), false);
$this->assertEquals(true, $validator->isValid('https://appwrite.flutter'));
$this->assertEquals(true, $validator->isValid('http://appwrite.flutter'));
$this->assertEquals(true, $validator->isValid('http://appwrite.flutter:80'));
$this->assertEquals($validator->isValid('appwrite-ios://com.company.appname'), false);
$this->assertEquals($validator->getDescription(), 'Invalid Origin. Register your new client (com.company.appname) as a new iOS platform on your project console dashboard');
$this->assertEquals(false, $validator->isValid('https://example.com'));
$this->assertEquals(false, $validator->isValid('http://example.com'));
$this->assertEquals(false, $validator->isValid('http://example.com:80'));
$this->assertEquals($validator->isValid('appwrite-android://com.company.appname'), false);
$this->assertEquals($validator->getDescription(), 'Invalid Origin. Register your new client (com.company.appname) as a new Android platform on your project console dashboard');
$this->assertEquals(true, $validator->isValid('exp://'));
$this->assertEquals(true, $validator->isValid('exp:///'));
$this->assertEquals(true, $validator->isValid('exp://index'));
$this->assertEquals($validator->isValid('appwrite-macos://com.company.appname'), false);
$this->assertEquals($validator->getDescription(), 'Invalid Origin. Register your new client (com.company.appname) as a new macOS platform on your project console dashboard');
$this->assertEquals(true, $validator->isValid('appwrite-callback-123://'));
$this->assertEquals(false, $validator->isValid('appwrite-callback-456://'));
$this->assertEquals($validator->isValid('appwrite-linux://com.company.appname'), false);
$this->assertEquals($validator->getDescription(), 'Invalid Origin. Register your new client (com.company.appname) as a new Linux platform on your project console dashboard');
$this->assertEquals(false, $validator->isValid('appwrite-ios://com.company.appname'));
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new iOS platform on your project console dashboard', $validator->getDescription());
$this->assertEquals($validator->isValid('appwrite-windows://com.company.appname'), false);
$this->assertEquals($validator->getDescription(), 'Invalid Origin. Register your new client (com.company.appname) as a new Windows platform on your project console dashboard');
$this->assertEquals(false, $validator->isValid('appwrite-android://com.company.appname'));
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new Android platform on your project console dashboard', $validator->getDescription());
$this->assertEquals(false, $validator->isValid('appwrite-macos://com.company.appname'));
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new macOS platform on your project console dashboard', $validator->getDescription());
$this->assertEquals(false, $validator->isValid('appwrite-linux://com.company.appname'));
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new Linux platform on your project console dashboard', $validator->getDescription());
$this->assertEquals(false, $validator->isValid('appwrite-windows://com.company.appname'));
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new Windows platform on your project console dashboard', $validator->getDescription());
$this->assertEquals(false, $validator->isValid('chrome-extension://com.company.appname'));
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new Web (Chrome Extension) platform on your project console dashboard', $validator->getDescription());
$this->assertEquals(false, $validator->isValid('moz-extension://com.company.appname'));
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new Web (Firefox Extension) platform on your project console dashboard', $validator->getDescription());
$this->assertEquals(false, $validator->isValid('safari-web-extension://com.company.appname'));
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new Web (Safari Extension) platform on your project console dashboard', $validator->getDescription());
$this->assertEquals(false, $validator->isValid('ms-browser-extension://com.company.appname'));
$this->assertEquals('Invalid Origin. Register your new client (com.company.appname) as a new Web (Edge Extension) platform on your project console dashboard', $validator->getDescription());
$this->assertEquals(false, $validator->isValid('random-scheme://localhost'));
$this->assertEquals('Invalid Scheme. The scheme used (random-scheme) in the Origin (random-scheme://localhost) is not supported. If you are using a custom scheme, please change it to `appwrite-callback-<PROJECT_ID>`', $validator->getDescription());
}
}
+22
View File
@@ -26,6 +26,15 @@ class URLTest extends TestCase
$this->assertEquals(null, $url['port']);
$this->assertEquals('', $url['path']);
$this->assertEquals('', $url['query']);
$url = URL::parse('appwrite-callback-project://');
$this->assertIsArray($url);
$this->assertEquals('appwrite-callback-project', $url['scheme']);
$this->assertEquals('', $url['host']);
$this->assertEquals(null, $url['port']);
$this->assertEquals('', $url['path']);
$this->assertEquals('', $url['query']);
}
public function testUnparse(): void
@@ -86,6 +95,19 @@ class URLTest extends TestCase
$this->assertIsString($url);
$this->assertEquals('https://eldad:fux@appwrite.io/#bottom', $url);
$url = URL::unparse([
'scheme' => 'https',
'user' => '',
'pass' => '',
'host' => 'appwrite.io',
'port' => null,
'path' => '',
'fragment' => '',
]);
$this->assertIsString($url);
$this->assertEquals('https://appwrite.io/#', $url);
}
public function testParseQuery(): void