diff --git a/app/config/collections.php b/app/config/collections.php index 69750e9427..bd9a4550c8 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -4530,6 +4530,28 @@ $consoleCollections = array_merge([ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('pingCount'), + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => false, + 'default' => 0, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => ID::custom('pingedAt'), + 'type' => Database::VAR_DATETIME, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => ['datetime'], + ] ], 'indexes' => [ [ diff --git a/app/controllers/general.php b/app/controllers/general.php index 04554a940e..bd49872436 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -25,6 +25,7 @@ use Utopia\App; use Utopia\CLI\Console; use Utopia\Config\Config; use Utopia\Database\Database; +use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; @@ -1048,6 +1049,38 @@ App::get('/.well-known/acme-challenge/*') include_once __DIR__ . '/shared/api.php'; include_once __DIR__ . '/shared/api/auth.php'; +App::get('/v1/ping') + ->groups(['api', 'general']) + ->desc('Test the connection between the Appwrite and the SDK.') + ->label('scope', 'global') + ->label('event', 'projects.[projectId].ping') + ->inject('response') + ->inject('project') + ->inject('dbForConsole') + ->inject('queueForEvents') + ->action(function (Response $response, Document $project, Database $dbForConsole, Event $queueForEvents) { + if ($project->isEmpty()) { + throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND); + } + + $pingCount = $project->getAttribute('pingCount', 0) + 1; + $pingedAt = DateTime::now(); + + $project + ->setAttribute('pingCount', $pingCount) + ->setAttribute('pingedAt', $pingedAt); + + Authorization::skip(function () use ($dbForConsole, $project) { + $dbForConsole->updateDocument('projects', $project->getId(), $project); + }); + + $queueForEvents + ->setParam('projectId', $project->getId()) + ->setPayload($response->output($project, Response::MODEL_PROJECT)); + + $response->text('Pong!'); + }); + App::wildcard() ->groups(['api']) ->label('scope', 'global') diff --git a/src/Appwrite/Messaging/Adapter/Realtime.php b/src/Appwrite/Messaging/Adapter/Realtime.php index d0d4a7c725..c437d4d487 100644 --- a/src/Appwrite/Messaging/Adapter/Realtime.php +++ b/src/Appwrite/Messaging/Adapter/Realtime.php @@ -270,6 +270,12 @@ class Realtime extends Adapter $projectId = 'console'; $roles = [Role::team($project->getAttribute('teamId'))->toString()]; break; + case 'projects': + $channels[] = 'console'; + $channels[] = 'projects.' . $parts[1]; + $projectId = 'console'; + $roles = [Role::team($project->getAttribute('teamId'))->toString()]; + break; case 'teams': if ($parts[2] === 'memberships') { $permissionsChanged = $parts[4] ?? false; diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index 80214aaa73..e1d0105587 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -234,6 +234,18 @@ class Project extends Model 'default' => '', 'example' => 'tls', ]) + ->addRule('pingCount', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Number of times the ping was received for this project.', + 'default' => 0, + 'example' => 1, + ]) + ->addRule('pingedAt', [ + 'type' => self::TYPE_DATETIME, + 'description' => 'Last ping datetime in ISO 8601 format.', + 'default' => '', + 'example' => self::TYPE_DATETIME_EXAMPLE, + ]) ; $services = Config::getParam('services', []); diff --git a/tests/e2e/General/PingTest.php b/tests/e2e/General/PingTest.php new file mode 100644 index 0000000000..e41bac6736 --- /dev/null +++ b/tests/e2e/General/PingTest.php @@ -0,0 +1,55 @@ +client->call(Client::METHOD_GET, '/ping', [ + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Pong!', $response['body']); + + // With user session + $response = $this->client->call(Client::METHOD_GET, '/ping', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Pong!', $response['body']); + + // With API key + $response = $this->client->call(Client::METHOD_GET, '/ping', [ + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('Pong!', $response['body']); + + /** + * Test for FAILURE + */ + // Fake project ID + $response = $this->client->call(Client::METHOD_GET, '/ping', \array_merge([ + 'x-appwrite-project' => 'fake-project-id', + ], $this->getHeaders())); + + $this->assertEquals(404, $response['headers']['status-code']); + $this->assertNotContains('Pong!', $response['body']); + } +} diff --git a/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php b/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php index 60c96c6e19..0155d251f2 100644 --- a/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php +++ b/tests/e2e/Services/Realtime/RealtimeConsoleClientTest.php @@ -478,6 +478,44 @@ class RealtimeConsoleClientTest extends Scope $client->close(); } + public function testPing() + { + $client = $this->getWebsocket(['console'], [ + 'origin' => 'http://localhost', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + ], 'console'); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('type', $response); + $this->assertEquals('connected', $response['type']); + + $pong = $this->client->call(Client::METHOD_GET, '/ping', [ + 'origin' => 'http://localhost', + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals(200, $pong['headers']['status-code']); + $this->assertEquals('Pong!', $pong['body']); + + $response = json_decode($client->receive(), true); + + $this->assertArrayHasKey('type', $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.{$this->getProject()['$id']}", $response['data']['channels']); + $this->assertContains("projects.{$this->getProject()['$id']}.ping", $response['data']['events']); + $this->assertNotEmpty($response['data']['payload']); + $this->assertArrayHasKey('pingCount', $response['data']['payload']); + $this->assertArrayHasKey('pingedAt', $response['data']['payload']); + $this->assertEquals(1, $response['data']['payload']['pingCount']); + + $client->close(); + } + public function testCreateDeployment() { $response1 = $this->client->call(Client::METHOD_POST, '/functions', array_merge([