From dc7bb627d53521f6a29d54ef72b264dd0a0ab7ee Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Wed, 22 Oct 2025 01:24:31 +0100 Subject: [PATCH] Enhance avatar screenshot API with new parameters and validations; add GraphQL support and extensive tests for various scenarios --- app/controllers/api/avatars.php | 141 +++++-- src/Appwrite/GraphQL/Types/Mapper.php | 9 +- tests/e2e/Services/Avatars/AvatarsBase.php | 433 ++++++++++++++++++--- tests/e2e/Services/GraphQL/AvatarsTest.php | 180 +++++++++ tests/e2e/Services/GraphQL/Base.php | 7 + 5 files changed, 696 insertions(+), 74 deletions(-) diff --git a/app/controllers/api/avatars.php b/app/controllers/api/avatars.php index 45ebce195d..6a240cb1ff 100644 --- a/app/controllers/api/avatars.php +++ b/app/controllers/api/avatars.php @@ -23,6 +23,7 @@ use Utopia\Fetch\Client; use Utopia\Image\Image; use Utopia\Logger\Logger; use Utopia\System\System; +use Utopia\Validator\ArrayList; use Utopia\Validator\Assoc; use Utopia\Validator\Boolean; use Utopia\Validator\HexColor; @@ -642,7 +643,7 @@ App::get('/v1/avatars/screenshot') ->label('scope', 'avatars.read') ->label('cache', true) ->label('cache.resourceType', 'avatar/screenshot') - ->label('cache.resource', 'screenshot/{request.url}') + ->label('cache.resource', 'screenshot/{request.url}/{request.width}/{request.height}/{request.theme}/{request.userAgent}/{request.fullpage}/{request.locale}/{request.timezone}/{request.latitude}/{request.longitude}/{request.accuracy}/{request.touch}/{request.permissions}/{request.sleep}/{request.quality}/{request.output}') ->label('sdk', new Method( namespace: 'avatars', group: null, @@ -661,15 +662,23 @@ App::get('/v1/avatars/screenshot') ->param('url', '', new URL(['http', 'https']), 'Website URL which you want to capture.') ->param('headers', [], new Assoc(), 'HTTP headers to send with the browser request. Defaults to empty.', true) ->param('viewport', '1280x720', new Text(20), 'Browser viewport size. Pass a string like "1280x720" or "1920x1080". Defaults to "1280x720".', true) - ->param('scale', 2, new Range(1, 5, Range::TYPE_FLOAT), 'Device pixel ratio. Pass a number between 1 to 5. Defaults to 2.', true) - ->param('fullPage', false, new Boolean(true), 'Capture full page. Pass 0 for viewport only, or 1 for full page. Default value is set to 0.', true) + ->param('theme', 'light', new WhiteList(['light', 'dark']), 'Browser theme. Pass "light" or "dark". Defaults to "light".', true) + ->param('userAgent', '', new Text(512), 'Custom user agent string. Defaults to browser default.', true) + ->param('fullpage', false, new Boolean(true), 'Capture full page scroll. Pass 0 for viewport only, or 1 for full page. Defaults to 0.', true) + ->param('locale', '', new Text(10), 'Browser locale (e.g., "en-US", "fr-FR"). Defaults to browser default.', true) + ->param('timezone', '', new WhiteList(timezone_identifiers_list()), 'IANA timezone identifier (e.g., "America/New_York", "Europe/London"). Defaults to browser default.', true) + ->param('latitude', 0, new Range(-90, 90, Range::TYPE_FLOAT), 'Geolocation latitude. Pass a number between -90 to 90. Defaults to 0.', true) + ->param('longitude', 0, new Range(-180, 180, Range::TYPE_FLOAT), 'Geolocation longitude. Pass a number between -180 to 180. Defaults to 0.', true) + ->param('accuracy', 0, new Range(0, 100000, Range::TYPE_FLOAT), 'Geolocation accuracy in meters. Pass a number between 0 to 100000. Defaults to 0.', true) + ->param('touch', false, new Boolean(true), 'Enable touch support. Pass 0 for no touch, or 1 for touch enabled. Defaults to 0.', true) + ->param('permissions', [], new ArrayList(new WhiteList(['geolocation', 'camera', 'microphone', 'notifications', 'midi', 'push', 'clipboard-read', 'clipboard-write', 'payment-handler', 'usb', 'bluetooth', 'accelerometer', 'gyroscope', 'magnetometer', 'ambient-light-sensor', 'background-sync', 'persistent-storage', 'screen-wake-lock', 'web-share', 'xr-spatial-tracking'])), 'Browser permissions to grant. Pass an array of permission names like ["geolocation", "camera", "microphone"]. Defaults to empty.', true) ->param('sleep', 0, new Range(0, 10), 'Wait time in seconds before taking the screenshot. Pass an integer between 0 to 10. Defaults to 0.', true) - ->param('width', 1280, new Range(1, 2000), 'Output image width. Pass an integer between 1 to 2000. Defaults to 1280.', true) - ->param('height', 720, new Range(1, 2000), 'Output image height. Pass an integer between 1 to 2000. Defaults to 720.', true) + ->param('width', 0, new Range(0, 2000), 'Output image width. Pass 0 to use original width, or an integer between 1 to 2000. Defaults to 0 (original width).', true) + ->param('height', 0, new Range(0, 2000), 'Output image height. Pass 0 to use original height, or an integer between 1 to 2000. Defaults to 0 (original height).', true) ->param('quality', -1, new Range(-1, 100), 'Screenshot quality. Pass an integer between 0 to 100. Defaults to keep existing image quality.', true) ->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true) ->inject('response') - ->action(function (string $url, array $headers, string $viewport, float $scale, bool $fullPage, int $sleep, int $width, int $height, int $quality, string $output, Response $response) { + ->action(function (string $url, array $headers, string $viewport, string $theme, string $userAgent, bool $fullpage, string $locale, string $timezone, float $latitude, float $longitude, float $accuracy, bool $touch, array $permissions, int $sleep, int $width, int $height, int $quality, string $output, Response $response) { if (!\extension_loaded('imagick')) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); @@ -710,34 +719,106 @@ App::get('/v1/avatars/screenshot') } // Create the config with headers as an object + // The custom browser service accepts: url, theme, headers, sleep, viewport, userAgent, fullPage, locale, timezoneId, geolocation, hasTouch $config = [ 'url' => $url, - 'width' => $browserWidth, - 'height' => $browserHeight, - 'fullPage' => $fullPage, + 'theme' => $theme, + 'headers' => $headersObject, 'sleep' => $sleep * 1000, // Convert seconds to milliseconds - 'scale' => $scale, - 'headers' => $headersObject + 'viewport' => [ + 'width' => $browserWidth, + 'height' => $browserHeight + ] ]; - // Ensure the entire config is properly serialized as JSON - // This is a workaround to ensure headers are sent as an object - $configJson = json_encode($config, JSON_FORCE_OBJECT); - $configObject = json_decode($configJson, false); // false to keep objects as objects + // Add fullPage to viewport if enabled + if ($fullpage) { + $config['viewport']['fullPage'] = true; + } - // Convert back to array for the fetch method, but ensure headers remains an object - $config = [ - 'url' => $configObject->url, - 'width' => $configObject->width, - 'height' => $configObject->height, - 'fullPage' => $configObject->fullPage, - 'sleep' => $configObject->sleep, - 'scale' => $configObject->scale, - 'headers' => $configObject->headers // Keep as object + // Add optional parameters only if they have meaningful values + if (!empty($userAgent)) { + $config['userAgent'] = $userAgent; + } + + if ($fullpage) { + $config['fullPage'] = true; + } + + if (!empty($locale)) { + $config['locale'] = $locale; + } + + if (!empty($timezone)) { + $config['timezoneId'] = $timezone; + } + + // Add geolocation if any coordinates are provided + if ($latitude != 0 || $longitude != 0) { + $config['geolocation'] = [ + 'latitude' => $latitude, + 'longitude' => $longitude, + 'accuracy' => $accuracy + ]; + } + + if ($touch) { + $config['hasTouch'] = true; + } + + // Add permissions if provided + if (!empty($permissions)) { + $config['permissions'] = $permissions; + } + + // Manually handle the config to ensure headers is an object but arrays remain arrays + $finalConfig = [ + 'url' => $config['url'], + 'theme' => $config['theme'], + 'headers' => $config['headers'], // Keep as object + 'sleep' => $config['sleep'], + 'viewport' => $config['viewport'] // Keep as object ]; + // Add optional parameters that were set, preserving arrays as arrays + if (!empty($userAgent)) { + $finalConfig['userAgent'] = $userAgent; + } + + if ($fullpage) { + $finalConfig['fullPage'] = true; + } + + if (!empty($locale)) { + $finalConfig['locale'] = $locale; + } + + if (!empty($timezone)) { + $finalConfig['timezoneId'] = $timezone; + } + + // Add geolocation if any coordinates are provided + if ($latitude != 0 || $longitude != 0) { + $finalConfig['geolocation'] = [ + 'latitude' => $latitude, + 'longitude' => $longitude, + 'accuracy' => $accuracy + ]; + } + + if ($touch) { + $finalConfig['hasTouch'] = true; + } + + // Add permissions if provided (preserve as array) + if (!empty($permissions)) { + $finalConfig['permissions'] = $permissions; // Keep as array + } + + $config = $finalConfig; + try { - $browserEndpoint = Config::getParam('_APP_BROWSER_HOST', 'http://appwrite-browser:3000/v1'); + $browserEndpoint = Config::getParam('_APP_BROWSER_HOST', 'http://192.168.1.43:3000/v1'); $fetchResponse = $client->fetch( url: $browserEndpoint . '/screenshots', @@ -755,16 +836,18 @@ App::get('/v1/avatars/screenshot') throw new Exception(Exception::AVATAR_IMAGE_NOT_FOUND, 'Screenshot not generated'); } - // Resize the screenshot to the desired output dimensions - $image = new Image($screenshot); - $image->crop($width, $height); - // Determine output format $outputs = Config::getParam('storage-outputs'); if (empty($output)) { $output = 'png'; // Default to PNG for screenshots } + // Only resize if width and height are explicitly set (not 0) + $image = new Image($screenshot); + if ($width > 0 && $height > 0) { + $image->crop($width, $height); + } + $resizedScreenshot = $image->output($output, $quality); unset($image); diff --git a/src/Appwrite/GraphQL/Types/Mapper.php b/src/Appwrite/GraphQL/Types/Mapper.php index b74e2a7549..c9ae84f1c3 100644 --- a/src/Appwrite/GraphQL/Types/Mapper.php +++ b/src/Appwrite/GraphQL/Types/Mapper.php @@ -331,9 +331,16 @@ class Mapper break; case 'Utopia\Validator\Integer': case 'Utopia\Validator\Numeric': - case 'Utopia\Validator\Range': $type = Type::int(); break; + case 'Utopia\Validator\Range': + // Check if the Range validator is for float or integer + if ($validator instanceof \Utopia\Validator\Range && $validator->getType() === \Utopia\Validator\Range::TYPE_FLOAT) { + $type = Type::float(); + } else { + $type = Type::int(); + } + break; case 'Utopia\Validator\FloatValidator': $type = Type::float(); break; diff --git a/tests/e2e/Services/Avatars/AvatarsBase.php b/tests/e2e/Services/Avatars/AvatarsBase.php index c94bf6bcb9..f331fa0359 100644 --- a/tests/e2e/Services/Avatars/AvatarsBase.php +++ b/tests/e2e/Services/Avatars/AvatarsBase.php @@ -567,7 +567,7 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, ]); @@ -579,7 +579,7 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, 'headers' => [ @@ -595,12 +595,12 @@ trait AvatarsBase /** * Test for FAILURE - Invalid headers parameter types */ - + // Test with string headers (should fail) $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, 'headers' => 'invalid-headers-string', @@ -611,7 +611,7 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, 'headers' => 123, @@ -622,7 +622,7 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, 'headers' => true, @@ -636,7 +636,7 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, 'headers' => ['value1', 'value2', 'value3'], // Indexed array @@ -648,7 +648,7 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, 'headers' => ['User-Agent' => 'MyApp', 'value2', 'Accept' => 'text/html'], // Mixed array @@ -659,7 +659,7 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, 'headers' => [], // Empty associative array should pass @@ -670,7 +670,7 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, 'headers' => [ @@ -685,7 +685,7 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, 'headers' => [ @@ -723,7 +723,7 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'viewport' => 'invalid-viewport', 'width' => 800, 'height' => 600, @@ -733,7 +733,7 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'viewport' => '2000x1000', // Too large 'width' => 800, 'height' => 600, @@ -746,8 +746,8 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', - 'width' => 0, // Invalid width + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => -1, // Invalid width (negative) 'height' => 600, ]); $this->assertEquals(400, $response['headers']['status-code']); @@ -755,42 +755,19 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 3000, // Invalid height ]); $this->assertEquals(400, $response['headers']['status-code']); - /** - * Test for FAILURE - Invalid scale parameter - */ - $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ - 'x-appwrite-project' => $this->getProject()['$id'], - ], [ - 'url' => 'https://appwrite.io', - 'width' => 800, - 'height' => 600, - 'scale' => 0.5, // Too small - ]); - $this->assertEquals(400, $response['headers']['status-code']); - - $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ - 'x-appwrite-project' => $this->getProject()['$id'], - ], [ - 'url' => 'https://appwrite.io', - 'width' => 800, - 'height' => 600, - 'scale' => 10, // Too large - ]); - $this->assertEquals(400, $response['headers']['status-code']); - /** * Test for FAILURE - Invalid sleep parameter */ $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, 'sleep' => -1, // Negative sleep @@ -800,7 +777,7 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, 'sleep' => 15, // Too large @@ -813,7 +790,7 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, 'quality' => -2, // Too small @@ -823,7 +800,7 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, 'quality' => 150, // Too large @@ -836,13 +813,381 @@ trait AvatarsBase $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ 'x-appwrite-project' => $this->getProject()['$id'], ], [ - 'url' => 'https://appwrite.io', + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), 'width' => 800, 'height' => 600, 'output' => 'invalid-format', ]); $this->assertEquals(400, $response['headers']['status-code']); + /** + * Test for SUCCESS - New screenshot parameters + */ + // Test with theme parameter + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'theme' => 'dark', + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + // Test with userAgent parameter + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'userAgent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + // Test with fullpage parameter + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'fullpage' => true, + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + // Test with locale parameter + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'locale' => 'en-US', + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + // Test with timezone parameter + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'timezone' => 'America/New_York', + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + // Test with geolocation parameters + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'latitude' => 40.7128, + 'longitude' => -74.0060, + 'accuracy' => 100, + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + // Test with touch parameter + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'touch' => true, + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + // Test with permissions parameter + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'permissions' => [ + 'geolocation', + 'camera', + 'microphone', + 'notifications' + ], + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + // Test with original dimensions (width=0, height=0) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 0, + 'height' => 0, + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + // Test with all new parameters combined + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'theme' => 'dark', + 'userAgent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + 'fullpage' => true, + 'locale' => 'en-GB', + 'timezone' => 'Europe/London', + 'latitude' => 51.5074, + 'longitude' => -0.1278, + 'accuracy' => 50, + 'touch' => true, + 'permissions' => [ + 'geolocation', + 'camera', + 'microphone', + 'notifications', + 'clipboard-read', + 'clipboard-write' + ], + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('image/png', $response['headers']['content-type']); + $this->assertNotEmpty($response['body']); + + /** + * Test for FAILURE - Invalid new parameters + */ + + // Test invalid theme parameter + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'theme' => 'invalid-theme', + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test invalid userAgent parameter (too long) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'userAgent' => str_repeat('A', 513), // Too long (max 512) + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test invalid fullpage parameter (non-boolean) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'fullpage' => 'invalid-boolean', + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test invalid locale parameter (too long) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'locale' => 'en-US-very-long-locale-string', + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test invalid timezone parameter + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'timezone' => 'Invalid/Timezone', + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test invalid latitude parameter (too high) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'latitude' => 91, // Too high (max 90) + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test invalid latitude parameter (too low) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'latitude' => -91, // Too low (min -90) + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test invalid longitude parameter (too high) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'longitude' => 181, // Too high (max 180) + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test invalid longitude parameter (too low) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'longitude' => -181, // Too low (min -180) + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test invalid accuracy parameter (too high) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'accuracy' => 100001, // Too high (max 100000) + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test invalid accuracy parameter (negative) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'accuracy' => -1, // Negative (min 0) + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test invalid touch parameter (non-boolean) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'touch' => 'invalid-boolean', + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test invalid permissions parameter (non-array) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'permissions' => 'invalid-permissions-string', + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test invalid permissions parameter (numeric array) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'permissions' => ['geolocation', 'camera', 'microphone'], // This should pass as it's a valid array + ]); + $this->assertEquals(200, $response['headers']['status-code']); + + // Test empty permissions array (should pass) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'permissions' => [], // Empty array should pass + ]); + $this->assertEquals(200, $response['headers']['status-code']); + + // Test invalid permission names (should fail) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'permissions' => ['invalid-permission', 'another-invalid'], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test mixed valid and invalid permissions (should fail) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'permissions' => ['geolocation', 'invalid-permission'], + ]); + $this->assertEquals(400, $response['headers']['status-code']); + + // Test valid permission names (should pass) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'permissions' => ['geolocation', 'camera', 'microphone', 'notifications'], + ]); + $this->assertEquals(200, $response['headers']['status-code']); + + // Test advanced permission names (should pass) + $response = $this->client->call(Client::METHOD_GET, '/avatars/screenshot', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ], [ + 'url' => 'https://appwrite.io?x=' . time() . rand(1000, 9999), + 'width' => 800, + 'height' => 600, + 'permissions' => ['geolocation', 'camera', 'microphone'], + ]); + $this->assertEquals(200, $response['headers']['status-code']); + return []; } } diff --git a/tests/e2e/Services/GraphQL/AvatarsTest.php b/tests/e2e/Services/GraphQL/AvatarsTest.php index 4c33aedcd7..085f0eaecb 100644 --- a/tests/e2e/Services/GraphQL/AvatarsTest.php +++ b/tests/e2e/Services/GraphQL/AvatarsTest.php @@ -173,4 +173,184 @@ class AvatarsTest extends Scope return $initials['body']; } + + public function testGetScreenshot() + { + $projectId = $this->getProject()['$id']; + $query = $this->getQuery(self::GET_SCREENSHOT); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'url' => 'https://appwrite.io', + 'width' => 800, + 'height' => 600, + ], + ]; + + $screenshot = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(200, $screenshot['headers']['status-code']); + $this->assertNotEmpty($screenshot['body']); + + // Debug: Print the actual response if it's not an image + if (!str_contains($screenshot['headers']['content-type'], 'image/')) { + echo "Response content-type: " . $screenshot['headers']['content-type'] . "\n"; + echo "Response body: " . print_r($screenshot['body'], true) . "\n"; + } + + $this->assertStringContainsString('image/', $screenshot['headers']['content-type']); + + return $screenshot['body']; + } + + public function testGetScreenshotWithOriginalDimensions() + { + $projectId = $this->getProject()['$id']; + $query = $this->getQuery(self::GET_SCREENSHOT); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'url' => 'https://appwrite.io', + 'width' => 0, + 'height' => 0, + ], + ]; + + $screenshot = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(200, $screenshot['headers']['status-code']); + $this->assertNotEmpty($screenshot['body']); + // Debug: Print the actual response if it's not an image + if (!str_contains($screenshot['headers']['content-type'], 'image/')) { + echo "Response content-type: " . $screenshot['headers']['content-type'] . "\n"; + echo "Response body: " . print_r($screenshot['body'], true) . "\n"; + } + + $this->assertStringContainsString('image/', $screenshot['headers']['content-type']); + + return $screenshot['body']; + } + + public function testGetScreenshotWithNewParameters() + { + $projectId = $this->getProject()['$id']; + $query = $this->getQuery(self::GET_SCREENSHOT); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'url' => 'https://appwrite.io', + 'width' => 800, + 'height' => 600, + 'theme' => 'dark', + 'userAgent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'fullpage' => true, + 'locale' => 'en-US', + 'timezone' => 'America/New_York', + 'latitude' => 40.7128, + 'longitude' => -74.0060, + 'accuracy' => 100, + 'touch' => true, + 'permissions' => [ + 'geolocation', + 'camera', + 'microphone', + 'notifications', + 'clipboard-read', + 'clipboard-write' + ], + ], + ]; + + $screenshot = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(200, $screenshot['headers']['status-code']); + $this->assertNotEmpty($screenshot['body']); + + // Debug: Print the actual response if it's not an image + if (!str_contains($screenshot['headers']['content-type'], 'image/')) { + echo "Response content-type: " . $screenshot['headers']['content-type'] . "\n"; + echo "Response body: " . print_r($screenshot['body'], true) . "\n"; + } + + $this->assertStringContainsString('image/', $screenshot['headers']['content-type']); + + return $screenshot['body']; + } + + public function testGetScreenshotWithPermissions() + { + $projectId = $this->getProject()['$id']; + $query = $this->getQuery(self::GET_SCREENSHOT); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'url' => 'https://appwrite.io', + 'width' => 800, + 'height' => 600, + 'permissions' => [ + 'geolocation', + 'camera', + 'microphone', + 'notifications' + ], + ], + ]; + + $screenshot = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(200, $screenshot['headers']['status-code']); + $this->assertNotEmpty($screenshot['body']); + // Debug: Print the actual response if it's not an image + if (!str_contains($screenshot['headers']['content-type'], 'image/')) { + echo "Response content-type: " . $screenshot['headers']['content-type'] . "\n"; + echo "Response body: " . print_r($screenshot['body'], true) . "\n"; + } + + $this->assertStringContainsString('image/', $screenshot['headers']['content-type']); + + return $screenshot['body']; + } + + public function testGetScreenshotWithInvalidPermissions() + { + $projectId = $this->getProject()['$id']; + $query = $this->getQuery(self::GET_SCREENSHOT); + $graphQLPayload = [ + 'query' => $query, + 'variables' => [ + 'url' => 'https://appwrite.io', + 'width' => 800, + 'height' => 600, + 'permissions' => [ + 'geolocation', + 'invalid-permission', + 'camera' + ], + ], + ]; + + $screenshot = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $projectId, + ], $this->getHeaders()), $graphQLPayload); + + $this->assertEquals(200, $screenshot['headers']['status-code']); + $this->assertArrayHasKey('errors', $screenshot['body']); + $this->assertNotEmpty($screenshot['body']['errors']); + $this->assertStringContainsString('Invalid `permissions` param', $screenshot['body']['errors'][0]['message']); + + return $screenshot['body']; + } } diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 9f5a93dd00..4b0d2630e6 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -288,6 +288,7 @@ trait Base public const string GET_FAVICON = 'get_favicon'; public const string GET_QRCODE = 'get_qrcode'; public const string GET_USER_INITIALS = 'get_user_initials'; + public const string GET_SCREENSHOT = 'get_screenshot'; // Providers public const string CREATE_MAILGUN_PROVIDER = 'create_mailgun_provider'; @@ -1779,6 +1780,12 @@ trait Base status } }'; + case self::GET_SCREENSHOT: + return 'query getScreenshot($url: String!, $width: Int, $height: Int, $theme: String, $userAgent: String, $fullpage: Boolean, $locale: String, $timezone: String, $latitude: Float, $longitude: Float, $accuracy: Float, $touch: Boolean, $permissions: [String!]) { + avatarsGetScreenshot(url: $url, width: $width, height: $height, theme: $theme, userAgent: $userAgent, fullpage: $fullpage, locale: $locale, timezone: $timezone, latitude: $latitude, longitude: $longitude, accuracy: $accuracy, touch: $touch, permissions: $permissions) { + status + } + }'; case self::GET_ACCOUNT: return 'query getAccount { accountGet {