getProject()['$id']; } $query = [ "project" => $projectId, "channels" => $channels ]; /** * Query param encoding rules: * - $queries === null -> only send channels (no per-channel query params) for backward compatibility. * - $queries === [] -> explicit "select all" subscription: send Query::select(['*']) as a single group. * - non-empty $queries -> treat as a single subscription group for the first channel: * AND logic within the group; OR logic across multiple groups (if we ever add them). * * For now all E2E tests subscribe to a single channel, so we map queries to $channels[0]. * * Slot-based format: channel[slot][]=query1&channel[slot][]=query2 * We need to manually build the query string to ensure the [] format is used. */ // Build base query string $queryParams = [ "project" => $projectId, "channels" => $channels ]; $queryString = http_build_query($queryParams); if ($queries !== null && !empty($channels)) { $channel = $channels[0]; $slot = 0; // All tests use slot 0 for now if ($queries === []) { // Explicit select("*") group - single query in slot 0 $queryValue = \Utopia\Database\Query::select(['*'])->toString(); $queryString .= "&" . urlencode($channel) . "[" . $slot . "][]=" . urlencode($queryValue); } else { // Single subscription group for this channel - multiple queries in slot 0 // Each query should be appended with [] format foreach ($queries as $queryValue) { $queryString .= "&" . urlencode($channel) . "[" . $slot . "][]=" . urlencode($queryValue); } } } return new WebSocketClient( "ws://appwrite.test/v1/realtime?" . $queryString, [ "headers" => $headers, "timeout" => $timeout, ] ); } /** * Build WebSocket client with custom query parameters. * Useful for testing edge cases like project in header only, or project as Query array. * * @param array $queryParams Custom query parameters (e.g., ['channels' => ['project'], 'project' => [...]]) * @param array $headers HTTP headers * @param int $timeout Timeout in seconds (default: 2) * @return WebSocketClient */ private function getWebsocketWithCustomQuery(array $queryParams, array $headers = [], int $timeout = 2): WebSocketClient { $queryString = http_build_query($queryParams); return new WebSocketClient( "ws://appwrite.test/v1/realtime?" . $queryString, [ "headers" => $headers, "timeout" => $timeout, ] ); } public function testConnection(): void { /** * Test for SUCCESS */ $client = $this->getWebsocket(["rows"]); $this->assertNotEmpty($client->receive()); $client->close(); } public function testConnectionSuccessMissingChannels(): void { $client = $this->getWebsocket([]); $payload = json_decode($client->receive(), true); $this->assertArrayHasKey("type", $payload); $this->assertArrayHasKey("data", $payload); $this->assertEquals("connected", $payload["type"]); $client->close(); } public function testConnectionFailureUnknownProject(): void { $client = $this->getWebsocket(projectId: '123'); $payload = json_decode($client->receive(), true); $this->assertArrayHasKey("type", $payload); $this->assertArrayHasKey("data", $payload); $this->assertEquals("error", $payload["type"]); $this->assertEquals(1008, $payload["data"]["code"]); $this->assertEquals( "Missing or unknown project ID", $payload["data"]["message"] ); \usleep(250000); // 250ms $this->expectException(ConnectionException::class); // Check if server disconnected client $client->close(); } public function testConnectionRegionCheck(): void { /** * Test for SUCCESS * A project whose region matches the server region should connect successfully. */ $client = $this->getWebsocket(['documents']); $response = json_decode($client->receive(), true); $this->assertArrayHasKey('type', $response); $this->assertArrayHasKey('data', $response); $this->assertEquals('connected', $response['type']); $this->assertNotEmpty($response['data']); $this->assertArrayHasKey('channels', $response['data']); $this->assertContains('documents', $response['data']['channels']); $client->close(); } }