Merge branch '1.8.x' into refactor-auth-single-instance

This commit is contained in:
Shimon Newman
2025-11-28 16:10:26 +02:00
committed by GitHub
19 changed files with 149 additions and 201 deletions
+18 -6
View File
@@ -5,7 +5,6 @@ use Appwrite\Event\Build;
use Appwrite\Event\Delete;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Redirect;
use Appwrite\Platform\Modules\Compute\Base as ComputeBase;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
@@ -32,7 +31,10 @@ use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Validator\Queries;
use Utopia\Database\Validator\Query\Cursor;
use Utopia\Database\Validator\Query\Limit;
use Utopia\Database\Validator\Query\Offset;
use Utopia\Detector\Detection\Framework\Analog;
use Utopia\Detector\Detection\Framework\Angular;
use Utopia\Detector\Detection\Framework\Astro;
@@ -1036,10 +1038,11 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
->param('installationId', '', new Text(256), 'Installation Id')
->param('type', '', new WhiteList(['runtime', 'framework']), 'Detector type. Must be one of the following: runtime, framework')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('queries', [], new Queries([new Limit(), new Offset()]), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Only supported methods are limit and offset', true)
->inject('gitHub')
->inject('response')
->inject('dbForPlatform')
->action(function (string $installationId, string $type, string $search, GitHub $github, Response $response, Database $dbForPlatform) {
->action(function (string $installationId, string $type, string $search, array $queries, GitHub $github, Response $response, Database $dbForPlatform) {
if (empty($search)) {
$search = "";
}
@@ -1055,11 +1058,20 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID');
$github->initializeVariables($providerInstallationId, $privateKey, $githubAppId);
$page = 1;
$perPage = 4;
$queries = Query::parseQueries($queries);
$limitQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_LIMIT));
$offsetQuery = current(array_filter($queries, fn ($query) => $query->getMethod() === Query::TYPE_OFFSET));
$limit = !empty($limitQuery) ? $limitQuery->getValue() : 4;
$offset = !empty($offsetQuery) ? $offsetQuery->getValue() : 0;
if ($offset % $limit !== 0) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'offset must be a multiple of the limit');
}
$page = ($offset / $limit) + 1;
$owner = $github->getOwnerName($providerInstallationId);
$repos = $github->searchRepositories($owner, $page, $perPage, $search);
['items' => $repos, 'total' => $total] = $github->searchRepositories($owner, $page, $limit, $search);
$repos = \array_map(function ($repo) use ($installation) {
$repo['id'] = \strval($repo['id'] ?? '');
@@ -1231,7 +1243,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories')
$response->dynamic(new Document([
$type === 'framework' ? 'frameworkProviderRepositories' : 'runtimeProviderRepositories' => $repos,
'total' => \count($repos),
'total' => $total,
]), ($type === 'framework') ? Response::MODEL_PROVIDER_REPOSITORY_FRAMEWORK_LIST : Response::MODEL_PROVIDER_REPOSITORY_RUNTIME_LIST);
});
+1 -1
View File
@@ -859,7 +859,7 @@ App::shutdown()
*
* Therefore, we consider this an anonymous request and create a relevant user.
*/
$user = new Document([
$user = new User([
'$id' => '',
'status' => true,
'type' => ACTIVITY_TYPE_GUEST,
+7
View File
@@ -93,6 +93,13 @@ const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
const APP_VCS_GITHUB_URL = 'https://github.com/TeamAppwrite';
const APP_BRANDED_EMAIL_BASE_TEMPLATE = 'email-base-styled';
/**
* JWT for Resource Tokens.
*/
const RESOURCE_TOKEN_ALGORITHM = 'HS256';
const RESOURCE_TOKEN_MAX_AGE = 86400 * 365 * 10; /* 10 years */
const RESOURCE_TOKEN_LEEWAY = 10; // 10 seconds
/**
* Token Expiration times.
*/
+2 -1
View File
@@ -1013,7 +1013,8 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request, A
$tokenJWT = $request->getParam('token');
if (!empty($tokenJWT) && !$project->isEmpty()) { // JWT authentication
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway.
// Use a large but reasonable maxAge to avoid auto-exp when token has no expiry
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), RESOURCE_TOKEN_ALGORITHM, RESOURCE_TOKEN_MAX_AGE, RESOURCE_TOKEN_LEEWAY); // Instantiate with key, algo, maxAge and leeway.
try {
$payload = $jwt->decode($tokenJWT);
+1 -1
View File
@@ -75,7 +75,7 @@
"utopia-php/swoole": "0.8.*",
"utopia-php/system": "0.9.*",
"utopia-php/telemetry": "0.1.*",
"utopia-php/vcs": "0.12.*",
"utopia-php/vcs": "0.13.*",
"utopia-php/websocket": "0.3.*",
"matomo/device-detector": "6.4.*",
"dragonmantank/cron-expression": "3.4.*",
Generated
+6 -6
View File
@@ -5212,16 +5212,16 @@
},
{
"name": "utopia-php/vcs",
"version": "0.12.0",
"version": "0.13.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/vcs.git",
"reference": "28457cf347972c4ec95d3ca77776a4921364a665"
"reference": "c59e21db5ca42014fe2071fec3c2f814efcc86dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/28457cf347972c4ec95d3ca77776a4921364a665",
"reference": "28457cf347972c4ec95d3ca77776a4921364a665",
"url": "https://api.github.com/repos/utopia-php/vcs/zipball/c59e21db5ca42014fe2071fec3c2f814efcc86dd",
"reference": "c59e21db5ca42014fe2071fec3c2f814efcc86dd",
"shasum": ""
},
"require": {
@@ -5255,9 +5255,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/vcs/issues",
"source": "https://github.com/utopia-php/vcs/tree/0.12.0"
"source": "https://github.com/utopia-php/vcs/tree/0.13.0"
},
"time": "2025-10-22T12:58:29+00:00"
"time": "2025-11-28T08:42:31+00:00"
},
{
"name": "utopia-php/websocket",
@@ -13,7 +13,6 @@ use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Swoole\Request;
use Utopia\System\System;
@@ -5,7 +5,6 @@ namespace Appwrite\Platform\Modules\Functions\Http\Deployments;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
@@ -33,7 +32,7 @@ use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Create extends Base
class Create extends Action
{
use HTTP;
@@ -84,7 +83,6 @@ class Create extends Base
->inject('request')
->inject('response')
->inject('dbForProject')
->inject('dbForPlatform')
->inject('queueForEvents')
->inject('project')
->inject('deviceForFunctions')
@@ -104,7 +102,6 @@ class Create extends Base
Request $request,
Response $response,
Database $dbForProject,
Database $dbForPlatform,
Event $queueForEvents,
Document $project,
Device $deviceForFunctions,
@@ -127,7 +127,6 @@ class Create extends Base
project: $project,
installation: $installation,
dbForProject: $dbForProject,
dbForPlatform: $dbForPlatform,
queueForBuilds: $queueForBuilds,
template: $template,
github: $github,
@@ -107,7 +107,6 @@ class Create extends Base
project: $project,
installation: $installation,
dbForProject: $dbForProject,
dbForPlatform: $dbForPlatform,
queueForBuilds: $queueForBuilds,
template: $template,
github: $github,
@@ -307,14 +307,12 @@ class Create extends Base
$template = new Document();
$installation = $dbForPlatform->getDocument('installations', $function->getAttribute('installationId'));
// TODO: Is this still needed? Can this be removed?
$deployment = $this->redeployVcsFunction(
request: $request,
function: $function,
project: $project,
installation: $installation,
dbForProject: $dbForProject,
dbForPlatform: $dbForPlatform,
queueForBuilds: $queueForBuilds,
template: $template,
github: $github,
@@ -5,7 +5,6 @@ namespace Appwrite\Platform\Modules\Sites\Http\Deployments;
use Appwrite\Event\Build;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Modules\Compute\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
@@ -33,7 +32,7 @@ use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Create extends Base
class Create extends Action
{
use HTTP;
@@ -64,7 +64,7 @@ class ResourceToken extends Model
$expire = $document->getAttribute('expire');
// Use a large but reasonable maxAge to avoid auto-exp when we set explicit exp
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400 * 365 * 10, 10); // 10 years
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), RESOURCE_TOKEN_ALGORITHM, RESOURCE_TOKEN_MAX_AGE, RESOURCE_TOKEN_LEEWAY); // 10 years
$payload = [
'tokenId' => $document->getId(),
@@ -73,13 +73,13 @@ class ResourceToken extends Model
'resourceInternalId' => $document->getAttribute('resourceInternalId'),
];
$createdDate = new \DateTime($document->getCreatedAt());
$payload['iat'] = $createdDate->getTimestamp();
// Set explicit expiration in JWT payload if we have an expiry date
if ($expire !== null) {
$expiryDate = new \DateTime($expire);
$payload['exp'] = $expiryDate->getTimestamp();
} else {
// For infinite expiry, set 'iat' to prevent JWT library from auto-adding 'exp'
$payload['iat'] = time();
}
$secret = $jwt->encode($payload);
@@ -432,27 +432,4 @@ trait FunctionsBase
return $specifications;
}
protected function createFunctionRule(string $functionId, string $domain): mixed
{
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/function', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'functionId' => $functionId,
'domain' => $domain,
]);
return $rule;
}
protected function getFunctionRule(string $ruleId): mixed
{
$rule = $this->client->call(Client::METHOD_GET, '/proxy/rules/' . $ruleId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
return $rule;
}
}
@@ -14,7 +14,6 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\System\System;
class FunctionsCustomServerTest extends Scope
{
@@ -393,10 +392,6 @@ class FunctionsCustomServerTest extends Scope
$functionId = $function['body']['$id'] ?? '';
$domain = ID::unique() . '.' . System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$rule = $this->createFunctionRule($functionId, $domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$deployment = $this->createTemplateDeployment(
$functionId,
[
@@ -413,20 +408,6 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$rule = $this->getFunctionRule($rule['body']['$id']);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->assertEquals($deployment['body']['$id'], $rule['body']['deploymentId']);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertStringContainsString("Deployment is still building", $response['body']);
$this->assertStringContainsString("The page will update after the build completes.", $response['body']);
$deployment = $this->getDeployment($functionId, $deployment['body']['$id']);
$this->assertEquals(200, $deployment['headers']['status-code']);
// Wait for deployment to be ready
$deploymentId = $deployment['body']['$id'];
$this->assertEventually(function () use ($functionId, $deploymentId) {
@@ -436,6 +417,7 @@ class FunctionsCustomServerTest extends Scope
// Verify deployment sizes
$deployment = $this->getDeployment($functionId, $deploymentId);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertGreaterThan(0, $deployment['body']['sourceSize']);
$this->assertGreaterThan(0, $deployment['body']['buildSize']);
$totalSize = $deployment['body']['sourceSize'] + $deployment['body']['buildSize'];
@@ -680,10 +662,6 @@ class FunctionsCustomServerTest extends Scope
*/
$functionId = $data['functionId'];
$domain = ID::unique() . '.' . System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$rule = $this->createFunctionRule($functionId, $domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$deployment = $this->createDeployment($functionId, [
'code' => $this->packageFunction('basic'),
'activate' => true
@@ -695,18 +673,6 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt']));
$this->assertEquals('index.js', $deployment['body']['entrypoint']);
$rule = $this->getFunctionRule($rule['body']['$id']);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->assertEquals($deployment['body']['$id'], $rule['body']['deploymentId']);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertStringContainsString("Deployment is still building", $response['body']);
$this->assertStringContainsString('The page will update after the build completes.', $response['body']);
$deploymentIdActive = $deployment['body']['$id'] ?? '';
$this->assertEventually(function () use ($functionId, $deploymentIdActive) {
@@ -2426,12 +2392,6 @@ class FunctionsCustomServerTest extends Scope
$domain = $this->setupFunctionDomain($functionId);
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(404, $response['headers']['status-code']);
$this->assertStringContainsString('No active deployments', $response['body']);
$this->assertStringContainsString('View deployments', $response['body']);
$deployment = $this->createDeployment($functionId, [
'code' => $this->packageFunction('basic'),
'activate' => true
@@ -2444,22 +2404,11 @@ class FunctionsCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id']
]));
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertStringContainsString('Deployment is still building', $response['body']);
$this->assertStringContainsString('The page will update after the build completes.', $response['body']);
$this->assertEquals(404, $response['headers']['status-code']);
$this->assertStringContainsString('No active deployments', $response['body']);
$this->assertStringContainsString('View deployments', $response['body']);
// canceled deployment
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test Error Pages',
'runtime' => 'node-22',
'entrypoint' => 'index.js',
'timeout' => 15,
'commands' => 'cd non-existing-directory',
'execute' => ['any']
]);
$domain = $this->setupFunctionDomain($functionId);
$proxyClient->setEndpoint('http://' . $domain);
$deployment = $this->createDeployment($functionId, [
'code' => $this->packageFunction('basic'),
'activate' => true
@@ -2477,9 +2426,9 @@ class FunctionsCustomServerTest extends Scope
'x-appwrite-project' => $this->getProject()['$id']
]));
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertStringContainsString('Deployment build canceled', $response['body']);
$this->assertStringContainsString('This build was canceled and won\'t be deployed.', $response['body']);
$this->assertEquals(404, $response['headers']['status-code']);
$this->assertStringContainsString('No active deployments', $response['body']);
$this->assertStringContainsString('View deployments', $response['body']);
$this->cleanupFunction($functionId);
}
-23
View File
@@ -474,27 +474,4 @@ trait SitesBase
return $specifications;
}
protected function createSiteRule(string $siteId, string $domain): mixed
{
$rule = $this->client->call(Client::METHOD_POST, '/proxy/rules/site', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'siteId' => $siteId,
'domain' => $domain,
]);
return $rule;
}
protected function getSiteRule(string $ruleId): mixed
{
$rule = $this->client->call(Client::METHOD_GET, '/proxy/rules/' . $ruleId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
return $rule;
}
}
@@ -861,10 +861,6 @@ class SitesCustomServerTest extends Scope
$this->assertNotNull($siteId);
$domain = ID::unique() . '.' . System::getEnv('_APP_DOMAIN_SITES', '');
$rule = $this->createSiteRule($siteId, $domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$deployment = $this->createDeployment($siteId, [
'siteId' => $siteId,
'code' => $this->packageSite('static-single-file'),
@@ -876,18 +872,6 @@ class SitesCustomServerTest extends Scope
$this->assertEquals('waiting', $deployment['body']['status']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($deployment['body']['$createdAt']));
$rule = $this->getSiteRule($rule['body']['$id']);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->assertEquals($deployment['body']['$id'], $rule['body']['deploymentId']);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertStringContainsString("Deployment is still building", $response['body']);
$this->assertStringContainsString('The page will update after the build completes.', $response['body']);
$deploymentIdActive = $deployment['body']['$id'] ?? '';
$this->assertEventually(function () use ($siteId, $deploymentIdActive) {
@@ -1579,10 +1563,6 @@ class SitesCustomServerTest extends Scope
$this->assertNotEmpty($siteId);
$domain = ID::unique() . '.' . System::getEnv('_APP_DOMAIN_SITES', '');
$rule = $this->createSiteRule($siteId, $domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$deployment = $this->createTemplateDeployment($siteId, [
'repository' => $template['providerRepositoryId'],
'owner' => $template['providerOwner'],
@@ -1595,18 +1575,6 @@ class SitesCustomServerTest extends Scope
$this->assertEquals(202, $deployment['headers']['status-code']);
$this->assertNotEmpty($deployment['body']['$id']);
$rule = $this->getSiteRule($rule['body']['$id']);
$this->assertEquals(200, $rule['headers']['status-code']);
$this->assertEquals($deployment['body']['$id'], $rule['body']['deploymentId']);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertStringContainsString("Deployment is still building", $response['body']);
$this->assertStringContainsString('The page will update after the build completes.', $response['body']);
$deployment = $this->getDeployment($siteId, $deployment['body']['$id']);
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals(0, $deployment['body']['sourceSize']);
@@ -1618,6 +1586,10 @@ class SitesCustomServerTest extends Scope
$this->assertNotEmpty($site['body']['deploymentId']);
}, 50000, 500);
$domain = $this->setupSiteDomain($siteId);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(200, $response['headers']['status-code']);
@@ -2612,8 +2584,7 @@ class SitesCustomServerTest extends Scope
}, 100000, 500);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertStringContainsString('Deployment build failed', $response['body']);
$this->assertStringContainsString('This page is empty, activate a deployment to make it live.', $response['body']);
$this->cleanupSite($siteId);
}
@@ -2708,12 +2679,6 @@ class SitesCustomServerTest extends Scope
$this->assertNotEmpty($siteId);
$domain = $this->setupSiteDomain($siteId);
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(404, $response['headers']['status-code']);
$this->assertStringContainsString('No active deployments', $response['body']);
$this->assertStringContainsString('View deployments', $response['body']);
// test canceled deployment error page
$deployment = $this->createDeployment($siteId, [
@@ -2749,6 +2714,13 @@ class SitesCustomServerTest extends Scope
$this->assertStringContainsString("Deployment build canceled", $response['body']);
$this->assertStringContainsString("View deployments", $response['body']);
// check site domain for no active deployments
$proxyClient->setEndpoint('http://' . $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/');
$this->assertEquals(404, $response['headers']['status-code']);
$this->assertStringContainsString('No active deployments', $response['body']);
$this->assertStringContainsString('View deployments', $response['body']);
$deployment = $this->createDeployment($siteId, [
'code' => $this->packageSite('static-single-file'),
'activate' => 'true'
@@ -72,37 +72,45 @@ class TokensConsoleClientTest extends Scope
$this->assertEquals(400, $token['headers']['status-code']);
$this->assertStringContainsString('Value must be valid date in the future', $token['body']['message']);
// Success case: No expire date
$token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'expire' => null,
]);
// Success cases: With & without expiry
$expireList = [null, date('Y-m-d', strtotime("tomorrow"))];
foreach ($expireList as $expire) {
$token = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'expire' => $expire,
]);
$this->assertEquals(201, $token['headers']['status-code']);
$this->assertEquals('files', $token['body']['resourceType']);
$this->assertNotEmpty($token['body']['$id']);
$this->assertNotEmpty($token['body']['secret']);
$this->assertEquals(201, $token['headers']['status-code']);
$this->assertEquals('files', $token['body']['resourceType']);
$this->assertNotEmpty($token['body']['$id']);
$this->assertNotEmpty($token['body']['secret']);
// Verify the generated token JWT contains correct resource information
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400 * 365 * 10, 10); // 10 years maxAge
try {
$payload = $jwt->decode($token['body']['secret']);
$this->assertIsArray($payload, 'JWT payload should decode to an array');
$this->assertArrayHasKey('tokenId', $payload, 'JWT payload should contain tokenId');
$this->assertArrayHasKey('resourceId', $payload, 'JWT payload should contain resourceId');
$this->assertArrayHasKey('resourceType', $payload, 'JWT payload should contain resourceType');
$this->assertArrayHasKey('resourceInternalId', $payload, 'JWT payload should contain resourceInternalId');
// Verify the generated token JWT contains correct resource information
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400 * 365 * 10, 10); // 10 years maxAge
try {
$payload = $jwt->decode($token['body']['secret']);
$this->assertIsArray($payload, 'JWT payload should decode to an array');
$this->assertArrayHasKey('tokenId', $payload, 'JWT payload should contain tokenId');
$this->assertArrayHasKey('resourceId', $payload, 'JWT payload should contain resourceId');
$this->assertArrayHasKey('resourceType', $payload, 'JWT payload should contain resourceType');
$this->assertArrayHasKey('resourceInternalId', $payload, 'JWT payload should contain resourceInternalId');
$this->assertArrayHasKey('iat', $payload, 'JWT payload should contain iat');
$this->assertEquals($token['body']['$id'], $payload['tokenId'], 'JWT tokenId should match token ID');
$this->assertEquals($bucketId . ':' . $fileId, $payload['resourceId'], 'JWT resourceId should match bucketId:fileId format');
$this->assertEquals('files', $payload['resourceType'], 'JWT resourceType should be files');
if (!empty($expire)) {
$this->assertArrayHasKey('exp', $payload, 'JWT payload should contain exp');
} else {
$this->assertArrayNotHasKey('exp', $payload, 'JWT payload should not contain exp field for tokens without expiry');
}
// For newly created tokens without expiry, should not have exp field
$this->assertArrayNotHasKey('exp', $payload, 'JWT payload should not contain exp field for tokens without expiry');
} catch (JWTException $e) {
$this->fail('Failed to decode JWT: ' . $e->getMessage());
$this->assertEquals($token['body']['$id'], $payload['tokenId'], 'JWT tokenId should match token ID');
$this->assertEquals($bucketId . ':' . $fileId, $payload['resourceId'], 'JWT resourceId should match bucketId:fileId format');
$this->assertEquals('files', $payload['resourceType'], 'JWT resourceType should be files');
} catch (JWTException $e) {
$this->fail('Failed to decode JWT: ' . $e->getMessage());
}
}
return [
@@ -218,6 +226,11 @@ class TokensConsoleClientTest extends Scope
$this->assertArrayHasKey('resourceId', $payload, 'JWT payload should contain resourceId');
$this->assertArrayHasKey('resourceType', $payload, 'JWT payload should contain resourceType');
$this->assertArrayHasKey('resourceInternalId', $payload, 'JWT payload should contain resourceInternalId');
$this->assertArrayHasKey('iat', $payload, 'JWT payload should contain iat');
if (!empty($token['expire'])) {
$this->assertArrayHasKey('exp', $payload, 'JWT payload should contain exp');
}
$this->assertEquals($token['$id'], $payload['tokenId'], 'JWT tokenId should match token ID');
$this->assertEquals($data['bucketId'] . ':' . $data['fileId'], $payload['resourceId'], 'JWT resourceId should match bucketId:fileId format');
@@ -10,6 +10,7 @@ use Utopia\Cache\Adapter\None;
use Utopia\Cache\Cache;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Role;
use Utopia\Database\Query;
use Utopia\System\System;
use Utopia\VCS\Adapter\Git\GitHub;
@@ -316,6 +317,43 @@ class VCSConsoleClientTest extends Scope
$this->assertEquals($searchedRepositories['body']['runtimeProviderRepositories'][0]['name'], 'appwrite');
$this->assertEquals($searchedRepositories['body']['runtimeProviderRepositories'][0]['runtime'], 'other');
// with limit and offset
$repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'type' => 'runtime',
'limit' => Query::limit(1)->toString(),
'offset' => Query::offset(0)->toString()
]);
$this->assertSame(200, $repositories['headers']['status-code']);
$this->assertSame(4, $repositories['body']['total']);
$this->assertCount(1, $repositories['body']['runtimeProviderRepositories']);
$this->assertSame('starter-for-svelte', $repositories['body']['runtimeProviderRepositories'][0]['name']);
$repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'type' => 'runtime',
'limit' => Query::limit(2)->toString(),
'offset' => Query::offset(2)->toString()
]);
$this->assertSame(200, $repositories['headers']['status-code']);
$this->assertSame(4, $repositories['body']['total']);
$this->assertCount(2, $repositories['body']['runtimeProviderRepositories']);
$this->assertSame('appwrite', $repositories['body']['runtimeProviderRepositories'][0]['name']);
$this->assertSame('ruby-starter', $repositories['body']['runtimeProviderRepositories'][1]['name']);
$repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'type' => 'runtime',
'limit' => Query::limit(2)->toString(),
'offset' => Query::offset(100)->toString()
]);
$this->assertSame(200, $repositories['headers']['status-code']);
$this->assertSame(4, $repositories['body']['total']);
$this->assertCount(0, $repositories['body']['runtimeProviderRepositories']);
// TODO: If you are about to add another check, rewrite this to @provideScenarios
/**
@@ -338,6 +376,17 @@ class VCSConsoleClientTest extends Scope
$this->assertEquals(400, $repositories['headers']['status-code']);
// invalid offset
$repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'type' => 'runtime',
'limit' => Query::limit(2)->toString(),
'offset' => Query::offset(1)->toString()
]);
$this->assertEquals(400, $repositories['headers']['status-code']);
$this->assertEquals('offset must be a multiple of the limit', $repositories['body']['message']);
$repositories = $this->client->call(Client::METHOD_GET, '/vcs/github/installations/' . $installationId . '/providerRepositories', array_merge([
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [