updateProtocolStatus($protocol, false); $this->assertSame(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $this->assertSame(false, $response['body']['protocolStatusFor' . ucfirst($protocol)]); } // Cleanup foreach (self::$protocols as $protocol) { $this->updateProtocolStatus($protocol, true); } } public function testEnableProtocol(): void { // Disable first foreach (self::$protocols as $protocol) { $this->updateProtocolStatus($protocol, false); } // Re-enable foreach (self::$protocols as $protocol) { $response = $this->updateProtocolStatus($protocol, true); $this->assertSame(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $this->assertSame(true, $response['body']['protocolStatusFor' . ucfirst($protocol)]); } } public function testDisableProtocolIdempotent(): void { $first = $this->updateProtocolStatus('rest', false); $this->assertSame(200, $first['headers']['status-code']); $this->assertSame(false, $first['body']['protocolStatusForRest']); $second = $this->updateProtocolStatus('rest', false); $this->assertSame(200, $second['headers']['status-code']); $this->assertSame(false, $second['body']['protocolStatusForRest']); // Cleanup $this->updateProtocolStatus('rest', true); } public function testEnableProtocolIdempotent(): void { $first = $this->updateProtocolStatus('rest', true); $this->assertSame(200, $first['headers']['status-code']); $this->assertSame(true, $first['body']['protocolStatusForRest']); $second = $this->updateProtocolStatus('rest', true); $this->assertSame(200, $second['headers']['status-code']); $this->assertSame(true, $second['body']['protocolStatusForRest']); } public function testDisabledRestBlocksClientRequest(): void { $this->updateProtocolStatus('rest', false); $response = $this->client->call(Client::METHOD_GET, '/locale/countries', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ]); $this->assertSame(403, $response['headers']['status-code']); $this->assertSame('general_api_disabled', $response['body']['type']); // Cleanup $this->updateProtocolStatus('rest', true); } public function testEnabledRestAllowsClientRequest(): void { $this->updateProtocolStatus('rest', false); $this->updateProtocolStatus('rest', true); $response = $this->client->call(Client::METHOD_GET, '/locale/countries', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ]); $this->assertSame(200, $response['headers']['status-code']); } public function testDisabledGraphqlBlocksClientRequest(): void { $this->updateProtocolStatus('graphql', false); $response = $this->client->call(Client::METHOD_POST, '/graphql', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], [ 'query' => '{ localeListCountries { total } }', ]); $this->assertSame(403, $response['headers']['status-code']); $this->assertSame('general_api_disabled', $response['body']['type']); // Cleanup $this->updateProtocolStatus('graphql', true); } public function testDisableOneProtocolDoesNotAffectOther(): void { $this->updateProtocolStatus('graphql', false); // REST should still work $response = $this->client->call(Client::METHOD_GET, '/locale/countries', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ]); $this->assertSame(200, $response['headers']['status-code']); // Cleanup $this->updateProtocolStatus('graphql', true); } public function testDisabledRestBlocksAllServiceEndpoints(): void { $endpoints = [ 'account' => '/account', 'teams' => '/teams', 'databases' => '/databases', 'storage' => '/storage/buckets', 'functions' => '/functions', 'sites' => '/sites', 'locale' => '/locale', 'health' => '/health', 'users' => '/users', 'messaging' => '/messaging/providers', 'migrations' => '/migrations', ]; $this->updateProtocolStatus('rest', false); foreach ($endpoints as $service => $path) { $response = $this->client->call(Client::METHOD_GET, $path, [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ]); $this->assertSame(403, $response['headers']['status-code'], 'Disabled REST protocol should block ' . $service . ' endpoint (got ' . $response['headers']['status-code'] . ')'); $this->assertSame('general_api_disabled', $response['body']['type'], 'Disabled REST protocol should return general_api_disabled for ' . $service); } // Cleanup $this->updateProtocolStatus('rest', true); } public function testReenabledRestAllowsAllServiceEndpoints(): void { $endpoints = [ 'teams' => '/teams', 'databases' => '/databases', 'functions' => '/functions', 'locale' => '/locale', ]; $this->updateProtocolStatus('rest', false); $this->updateProtocolStatus('rest', true); foreach ($endpoints as $service => $path) { $response = $this->client->call(Client::METHOD_GET, $path, array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], $this->getHeaders())); $this->assertNotEquals(403, $response['headers']['status-code'], 'Re-enabled REST protocol should not block ' . $service . ' endpoint'); } } public function testDisabledGraphqlBlocksMutationRequest(): void { $this->updateProtocolStatus('graphql', false); $response = $this->client->call(Client::METHOD_POST, '/graphql', [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ], [ 'query' => 'mutation { teamsCreate(teamId: "unique()", name: "Test") { _id } }', ]); $this->assertSame(403, $response['headers']['status-code']); $this->assertSame('general_api_disabled', $response['body']['type']); // Cleanup $this->updateProtocolStatus('graphql', true); } public function testResponseModel(): void { $response = $this->updateProtocolStatus('rest', false); $this->assertSame(200, $response['headers']['status-code']); $this->assertArrayHasKey('$id', $response['body']); $this->assertArrayHasKey('name', $response['body']); $this->assertArrayHasKey('protocolStatusForRest', $response['body']); $this->assertArrayHasKey('protocolStatusForGraphql', $response['body']); $this->assertArrayHasKey('protocolStatusForWebsocket', $response['body']); // Cleanup $this->updateProtocolStatus('rest', true); } // Failure flow public function testUpdateProtocolWithoutAuthentication(): void { $response = $this->updateProtocolStatus('rest', false, false); $this->assertSame(401, $response['headers']['status-code']); } public function testUpdateProtocolInvalidProtocolId(): void { $response = $this->updateProtocolStatus('invalid', false); $this->assertSame(400, $response['headers']['status-code']); } public function testUpdateProtocolEmptyProtocolId(): void { $response = $this->updateProtocolStatus('', false); $this->assertSame(404, $response['headers']['status-code']); } // Backwards compatibility public function testUpdateProtocolLegacyStatusPath(): void { $headers = array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], 'x-appwrite-response-format' => '1.9.4', ], $this->getHeaders()); // Disable via the legacy `/status` alias $response = $this->client->call(Client::METHOD_PATCH, '/project/protocols/rest/status', $headers, [ 'enabled' => false, ]); $this->assertSame(200, $response['headers']['status-code']); $this->assertNotEmpty($response['body']['$id']); $this->assertSame(false, $response['body']['protocolStatusForRest']); // Re-enable via the legacy `/status` alias $response = $this->client->call(Client::METHOD_PATCH, '/project/protocols/rest/status', $headers, [ 'enabled' => true, ]); $this->assertSame(200, $response['headers']['status-code']); $this->assertSame(true, $response['body']['protocolStatusForRest']); } // Helpers protected function updateProtocolStatus(string $protocolId, bool $enabled, bool $authenticated = true): mixed { $headers = [ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], ]; if ($authenticated) { $headers = array_merge($headers, $this->getHeaders(), [ 'x-appwrite-response-format' => '1.9.4', ]); } return $this->client->call(Client::METHOD_PATCH, '/project/protocols/' . $protocolId, $headers, [ 'enabled' => $enabled, ]); } }