2026-04-09 13:58:28 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Tests\E2E\Services\Project;
|
|
|
|
|
|
|
|
|
|
use Tests\E2E\Client;
|
|
|
|
|
|
|
|
|
|
trait ProtocolsBase
|
|
|
|
|
{
|
|
|
|
|
protected static array $protocols = ['rest', 'graphql', 'websocket'];
|
|
|
|
|
|
|
|
|
|
// Success flow
|
|
|
|
|
|
|
|
|
|
public function testDisableProtocol(): void
|
|
|
|
|
{
|
|
|
|
|
foreach (self::$protocols as $protocol) {
|
|
|
|
|
$response = $this->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);
|
|
|
|
|
|
2026-04-09 14:08:11 +00:00
|
|
|
$response = $this->client->call(Client::METHOD_GET, '/locale/countries', [
|
2026-04-09 13:58:28 +00:00
|
|
|
'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);
|
|
|
|
|
|
2026-04-09 14:08:11 +00:00
|
|
|
$response = $this->client->call(Client::METHOD_GET, '/locale/countries', [
|
2026-04-09 13:58:28 +00:00
|
|
|
'content-type' => 'application/json',
|
|
|
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
2026-04-09 14:08:11 +00:00
|
|
|
]);
|
2026-04-09 13:58:28 +00:00
|
|
|
|
|
|
|
|
$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
|
2026-04-09 14:08:11 +00:00
|
|
|
$response = $this->client->call(Client::METHOD_GET, '/locale/countries', [
|
2026-04-09 13:58:28 +00:00
|
|
|
'content-type' => 'application/json',
|
|
|
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
2026-04-09 14:08:11 +00:00
|
|
|
]);
|
2026-04-09 13:58:28 +00:00
|
|
|
|
|
|
|
|
$this->assertSame(200, $response['headers']['status-code']);
|
|
|
|
|
|
|
|
|
|
// Cleanup
|
|
|
|
|
$this->updateProtocolStatus('graphql', true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 14:57:44 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 13:58:28 +00:00
|
|
|
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);
|
|
|
|
|
|
2026-04-09 14:08:11 +00:00
|
|
|
$this->assertSame(404, $response['headers']['status-code']);
|
2026-04-09 13:58:28 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 11:19:20 +00:00
|
|
|
// Backwards compatibility
|
|
|
|
|
|
|
|
|
|
public function testUpdateProtocolLegacyStatusPath(): void
|
|
|
|
|
{
|
|
|
|
|
$headers = array_merge([
|
|
|
|
|
'content-type' => 'application/json',
|
|
|
|
|
'x-appwrite-project' => $this->getProject()['$id'],
|
|
|
|
|
], $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']);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 13:58:28 +00:00
|
|
|
// 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());
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 11:19:20 +00:00
|
|
|
return $this->client->call(Client::METHOD_PATCH, '/project/protocols/' . $protocolId, $headers, [
|
2026-04-09 13:58:28 +00:00
|
|
|
'enabled' => $enabled,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|