Files
appwrite/tests/e2e/Scopes/Scope.php
T
Jake BarnbyandClaude Opus 4.6 bf68bb2f71 fix: resolve MongoDB-specific E2E test failures and $sequence type bug
- Fix $sequence always cast to string (was incorrectly cast to int for numeric values) in Document.php and Row.php
- Add supportForMultipleFulltextIndexes, supportForAttributeResizing, supportForSchemas console variables for MongoDB adapter
- Fix testCreateIndexes to use getSupportForMultipleFulltextIndexes() instead of getSupportForRelationships() for fulltext index error branching
- Fix testAttributeRowWidthLimit/testColumnRowWidthLimit to skip on databases without schema support (MongoDB)
- Fix testAttributeUpdateStringResize/testColumnUpdateStringResize to skip on databases without attribute resizing support (MongoDB)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 01:43:28 +13:00

358 lines
10 KiB
PHP

<?php
namespace Tests\E2E\Scopes;
use Appwrite\Tests\Async;
use Appwrite\Tests\Retryable;
use PHPUnit\Framework\TestCase;
use Tests\E2E\Client;
use Utopia\Database\Helpers\ID;
use Utopia\System\System;
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://appwrite/v1';
protected string $webEndpoint = 'http://appwrite.test/v1';
protected function setUp(): void
{
$this->client = new Client();
$this->client->setEndpoint($this->endpoint);
$format = System::getEnv('_APP_E2E_RESPONSE_FORMAT');
if (!empty($format)) {
if (
!\preg_match('/^\d+\.\d+\.\d+$/', $format) ||
!\version_compare($format, APP_VERSION_STABLE, '<=')
) {
throw new \Exception('E2E response format must be ' . APP_VERSION_STABLE . ' or lower.');
}
$this->client->setResponseFormat($format);
}
}
protected function tearDown(): void
{
$this->client = null;
}
/**
* @var array|null Cached console variables
*/
protected static ?array $consoleVariables = null;
/**
* Fetch console variables from the API
*/
protected function getConsoleVariables(): array
{
if (self::$consoleVariables !== null) {
return self::$consoleVariables;
}
$root = $this->getRoot();
$response = $this->client->call(Client::METHOD_GET, '/console/variables', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
'cookie' => 'a_session_console=' . $root['session'],
]);
self::$consoleVariables = $response['body'] ?? [];
return self::$consoleVariables;
}
/**
* Check if the database adapter supports relationships
*/
protected function getSupportForRelationships(): bool
{
return $this->getConsoleVariables()['supportForRelationships'] ?? true;
}
/**
* Check if the database adapter supports operators
*/
protected function getSupportForOperators(): bool
{
return $this->getConsoleVariables()['supportForOperators'] ?? true;
}
/**
* Check if the database adapter supports spatial attributes
*/
protected function getSupportForSpatials(): bool
{
return $this->getConsoleVariables()['supportForSpatials'] ?? true;
}
/**
* Check if the database adapter supports spatial indexes on nullable columns
*/
protected function getSupportForSpatialIndexNull(): bool
{
return $this->getConsoleVariables()['supportForSpatialIndexNull'] ?? false;
}
/**
* Check if the database adapter supports fulltext wildcard search
*/
protected function getSupportForFulltextWildcard(): bool
{
return $this->getConsoleVariables()['supportForFulltextWildcard'] ?? true;
}
/**
* Check if the database adapter supports multiple fulltext indexes per collection
*/
protected function getSupportForMultipleFulltextIndexes(): bool
{
return $this->getConsoleVariables()['supportForMultipleFulltextIndexes'] ?? true;
}
/**
* Check if the database adapter supports resizing attributes
*/
protected function getSupportForAttributeResizing(): bool
{
return $this->getConsoleVariables()['supportForAttributeResizing'] ?? true;
}
/**
* Check if the database adapter supports fixed schemas with row width limits
*/
protected function getSupportForSchemas(): bool
{
return $this->getConsoleVariables()['supportForSchemas'] ?? true;
}
/**
* Get the maximum index length supported by the database adapter
*/
protected function getMaxIndexLength(): int
{
return $this->getConsoleVariables()['maxIndexLength'] ?? 768;
}
protected function getLastEmail(int $limit = 1): array
{
sleep(3);
$emails = json_decode(file_get_contents('http://maildev:1080/email'), true);
if ($emails && is_array($emails)) {
if ($limit === 1) {
return end($emails);
} else {
return array_slice($emails, -1 * $limit);
}
}
return [];
}
protected function extractQueryParamsFromEmailLink(string $html): array
{
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);
}, $timeoutMs, $waitMs);
return $request;
}
protected function assertSamePixels(string $expectedImagePath, string $actualImageBlob): void
{
$expected = new \Imagick($expectedImagePath);
$actual = new \Imagick();
$actual->readImageBlob($actualImageBlob);
foreach ([$expected, $actual] as $image) {
$image->setImageFormat('PNG');
$image->stripImage();
$image->setOption('png:exclude-chunks', 'date,time,iCCP,sRGB,gAMA,cHRM');
}
$this->assertSame($expected->getImageSignature(), $actual->getImageSignature());
}
/**
* @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://' . $hostname . ':5000/__last_request__'), true);
$request['data'] = json_decode($request['data'], true);
return $request;
}
/**
* @return array
*/
abstract public function getHeaders(bool $devKey = true): array;
/**
* @return array
*/
abstract public function getProject(): array;
/**
* @var array
*/
protected static $root = [];
/**
* @return array
*/
public function getRoot(): array
{
if ((self::$root)) {
return self::$root;
}
$email = uniqid() . 'user@localhost.test';
$password = 'password';
$name = 'User Name';
$root = $this->client->call(Client::METHOD_POST, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
], [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
]);
$this->assertEquals(201, $root['headers']['status-code']);
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
], [
'email' => $email,
'password' => $password,
]);
$session = $session['cookies']['a_session_console'];
self::$root = [
'$id' => ID::custom($root['body']['$id']),
'name' => $root['body']['name'],
'email' => $root['body']['email'],
'session' => $session,
];
return self::$root;
}
/**
* @var array
*/
protected static $user = [];
/**
* @return array
*/
public function getUser(bool $fresh = false): array
{
if (!$fresh && isset(self::$user[$this->getProject()['$id']])) {
return self::$user[$this->getProject()['$id']];
}
$email = uniqid() . 'user@localhost.test';
$password = 'password';
$name = 'User Name';
$user = $this->client->call(Client::METHOD_POST, '/account', [
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
]);
$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,
]);
$token = $session['cookies']['a_session_' . $this->getProject()['$id']];
self::$user[$this->getProject()['$id']] = [
'$id' => ID::custom($user['body']['$id']),
'name' => $user['body']['name'],
'email' => $user['body']['email'],
'session' => $token,
'sessionId' => $session['body']['$id'],
];
return self::$user[$this->getProject()['$id']];
}
}