Merge remote-tracking branch 'origin/1.9.x' into pr-12288

# Conflicts:
#	composer.lock
This commit is contained in:
Chirag Aggarwal
2026-05-13 16:22:26 +05:30
50 changed files with 2257 additions and 65 deletions
+4
View File
@@ -177,6 +177,10 @@ trait ProjectCustom
'project.policies.write',
'templates.read',
'templates.write',
'insights.read',
'insights.write',
'reports.read',
'reports.write',
],
]);
+122
View File
@@ -0,0 +1,122 @@
<?php
namespace Tests\E2E\Services\Advisor;
use Tests\E2E\Client;
use Utopia\Database\Helpers\ID;
trait AdvisorBase
{
protected function serverHeaders(): array
{
return [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
];
}
protected function getReport(string $reportId, ?array $headers = null): array
{
return $this->client->call(Client::METHOD_GET, '/reports/' . $reportId, $headers ?? $this->serverHeaders());
}
protected function listReports(array $params = [], ?array $headers = null): array
{
return $this->client->call(Client::METHOD_GET, '/reports', $headers ?? $this->serverHeaders(), $params);
}
protected function getInsight(string $reportId, string $insightId, ?array $headers = null): array
{
return $this->client->call(Client::METHOD_GET, '/reports/' . $reportId . '/insights/' . $insightId, $headers ?? $this->serverHeaders());
}
protected function listInsights(string $reportId, array $params = [], ?array $headers = null): array
{
return $this->client->call(Client::METHOD_GET, '/reports/' . $reportId . '/insights', $headers ?? $this->serverHeaders(), $params);
}
public function testListReports(): void
{
$list = $this->listReports();
$this->assertSame(200, $list['headers']['status-code']);
$this->assertArrayHasKey('reports', $list['body']);
$this->assertArrayHasKey('total', $list['body']);
$this->assertIsArray($list['body']['reports']);
}
public function testGetReportMissing(): void
{
$missing = $this->getReport(ID::unique());
$this->assertSame(404, $missing['headers']['status-code']);
$this->assertSame('report_not_found', $missing['body']['type']);
}
public function testListInsightsMissingReport(): void
{
$missing = $this->listInsights(ID::unique());
$this->assertSame(404, $missing['headers']['status-code']);
$this->assertSame('report_not_found', $missing['body']['type']);
}
public function testGetInsightMissingReport(): void
{
$missing = $this->getInsight(ID::unique(), ID::unique());
$this->assertSame(404, $missing['headers']['status-code']);
$this->assertSame('report_not_found', $missing['body']['type']);
}
public function testReportsCreateAndUpdateNotExposed(): void
{
$create = $this->client->call(Client::METHOD_POST, '/reports', $this->serverHeaders(), [
'reportId' => ID::unique(),
'type' => 'audit',
'title' => 'Read-only check',
'targetType' => 'sites',
'target' => 'home',
]);
$this->assertSame(404, $create['headers']['status-code']);
$update = $this->client->call(Client::METHOD_PATCH, '/reports/' . ID::unique(), $this->serverHeaders(), [
'title' => 'Read-only check',
]);
$this->assertSame(404, $update['headers']['status-code']);
}
public function testDeleteReportMissing(): void
{
$delete = $this->client->call(Client::METHOD_DELETE, '/reports/' . ID::unique(), $this->serverHeaders());
$this->assertSame(404, $delete['headers']['status-code']);
$this->assertSame('report_not_found', $delete['body']['type']);
}
public function testInsightsCreateUpdateDeleteNotExposed(): void
{
$create = $this->client->call(
Client::METHOD_POST,
'/reports/' . ID::unique() . '/insights',
$this->serverHeaders(),
[]
);
$this->assertSame(404, $create['headers']['status-code']);
$update = $this->client->call(
Client::METHOD_PATCH,
'/reports/' . ID::unique() . '/insights/' . ID::unique(),
$this->serverHeaders(),
['status' => 'dismissed']
);
$this->assertSame(404, $update['headers']['status-code']);
$delete = $this->client->call(
Client::METHOD_DELETE,
'/reports/' . ID::unique() . '/insights/' . ID::unique(),
$this->serverHeaders()
);
$this->assertSame(404, $delete['headers']['status-code']);
}
}
@@ -0,0 +1,58 @@
<?php
namespace Tests\E2E\Services\Advisor;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\Database\Helpers\ID;
class AdvisorCustomServerTest extends Scope
{
use AdvisorBase;
use ProjectCustom;
use SideServer;
public function testReadWithAdvisorScopes(): void
{
$projectId = $this->getProject()['$id'];
$userKey = $this->getNewKey([
// Advisor read APIs are protected by the underlying report/insight resource scopes.
'insights.read',
'reports.read',
]);
$listed = $this->client->call(
Client::METHOD_GET,
'/reports',
[
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $userKey,
]
);
$this->assertSame(200, $listed['headers']['status-code']);
$create = $this->client->call(
Client::METHOD_POST,
'/reports',
[
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
'x-appwrite-key' => $userKey,
],
[
'reportId' => ID::unique(),
'type' => 'audit',
'title' => 'Read-only check',
'targetType' => 'sites',
'target' => 'home',
]
);
$this->assertSame(404, $create['headers']['status-code']);
}
}
+6 -5
View File
@@ -171,8 +171,8 @@ trait ProxyBase
$siteId = $this->setupSite()['siteId'];
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301, 'site', $siteId);
$this->assertNotEmpty($ruleId);
$ruleId301 = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 301, 'site', $siteId);
$this->assertNotEmpty($ruleId301);
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
$this->assertEquals(200, $response['headers']['status-code']);
@@ -187,8 +187,8 @@ trait ProxyBase
$this->assertEquals('https://jsonplaceholder.typicode.com/todos/1', $response['headers']['location']);
$domain = \uniqid() . '-redirect-307.custom.localhost';
$ruleId = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307, 'site', $siteId);
$this->assertNotEmpty($ruleId);
$ruleId307 = $this->setupRedirectRule($domain, 'https://jsonplaceholder.typicode.com/todos/1', 307, 'site', $siteId);
$this->assertNotEmpty($ruleId307);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://appwrite.test');
@@ -209,7 +209,8 @@ trait ProxyBase
$this->assertEquals(200, $rules['headers']['status-code']);
$this->assertEquals(2, $rules['body']['total']);
$this->cleanupRule($ruleId);
$this->cleanupRule($ruleId301);
$this->cleanupRule($ruleId307);
$this->cleanupSite($siteId);
}
+241
View File
@@ -0,0 +1,241 @@
<?php
namespace Tests\Unit\Advisor\Validator;
use Appwrite\Advisor\Validator\CTAs;
use PHPUnit\Framework\TestCase;
class CTAsTest extends TestCase
{
public function testRejectsNonArray(): void
{
$validator = new CTAs();
$this->assertFalse($validator->isValid('not-an-array'));
$this->assertFalse($validator->isValid(42));
$this->assertFalse($validator->isValid(null));
}
public function testAcceptsEmptyArray(): void
{
$validator = new CTAs();
$this->assertTrue($validator->isValid([]));
}
public function testAcceptsCompleteEntry(): void
{
$validator = new CTAs();
$this->assertTrue($validator->isValid([[
'label' => 'Create missing index',
'service' => 'tablesDB',
'method' => 'createIndex',
'params' => [
'databaseId' => 'main',
'tableId' => 'orders',
],
]]));
}
public function testAcceptsEntryWithoutParams(): void
{
$validator = new CTAs();
$this->assertTrue($validator->isValid([[
'label' => 'Create missing index',
'service' => 'tablesDB',
'method' => 'createIndex',
]]));
}
public function testRejectsEntryMissingRequiredKeys(): void
{
$validator = new CTAs();
$this->assertFalse($validator->isValid([['label' => 'x']]));
$this->assertFalse($validator->isValid([['label' => 'x', 'service' => 'tablesDB']]));
$this->assertFalse($validator->isValid([['label' => 'x', 'method' => 'createIndex']]));
}
public function testRejectsEntryWithEmptyStrings(): void
{
$validator = new CTAs();
$this->assertFalse($validator->isValid([[
'label' => '',
'service' => 'tablesDB',
'method' => 'createIndex',
]]));
}
public function testRejectsEntryWithNonStringFields(): void
{
$validator = new CTAs();
$this->assertFalse($validator->isValid([[
'label' => 123,
'service' => 'tablesDB',
'method' => 'createIndex',
]]));
}
public function testRejectsEntryWithScalarParams(): void
{
$validator = new CTAs();
$this->assertFalse($validator->isValid([[
'label' => 'Create missing index',
'service' => 'tablesDB',
'method' => 'createIndex',
'params' => 'not-a-map',
]]));
}
public function testReportsArrayType(): void
{
$validator = new CTAs();
$this->assertTrue($validator->isArray());
$this->assertSame($validator::TYPE_ARRAY, $validator->getType());
}
public function testRejectsMoreThanMaxCount(): void
{
$validator = new CTAs(maxCount: 3);
$entries = [];
for ($i = 0; $i < 4; $i++) {
$entries[] = [
'label' => 'Label ' . $i,
'service' => 'tablesDB',
'method' => 'createIndex',
];
}
$this->assertFalse($validator->isValid($entries));
$this->assertStringContainsString('maximum of 3', $validator->getDescription());
}
public function testAcceptsExactlyMaxCount(): void
{
$validator = new CTAs(maxCount: 3);
$entries = [];
for ($i = 0; $i < 3; $i++) {
$entries[] = [
'label' => 'Label ' . $i,
'service' => 'tablesDB',
'method' => 'createIndex',
];
}
$this->assertTrue($validator->isValid($entries));
}
public function testAcceptsObjectParams(): void
{
$validator = new CTAs();
$entry = [
'label' => 'Create missing index',
'service' => 'tablesDB',
'method' => 'createIndex',
'params' => new \stdClass(),
];
$this->assertTrue($validator->isValid([$entry]));
}
public function testRejectsEntryWithEmptyService(): void
{
$validator = new CTAs();
$this->assertFalse($validator->isValid([[
'label' => 'Create missing index',
'service' => '',
'method' => 'createIndex',
]]));
}
public function testRejectsEntryWithEmptyMethod(): void
{
$validator = new CTAs();
$this->assertFalse($validator->isValid([[
'label' => 'Create missing index',
'service' => 'tablesDB',
'method' => '',
]]));
}
public function testRejectsUnknownService(): void
{
$validator = new CTAs();
$this->assertFalse($validator->isValid([[
'label' => 'Create missing index',
'service' => 'nonExistentService',
'method' => 'createIndex',
]]));
$this->assertStringContainsString('service', $validator->getDescription());
}
public function testRejectsUnknownMethod(): void
{
$validator = new CTAs();
$this->assertFalse($validator->isValid([[
'label' => 'Create missing index',
'service' => 'tablesDB',
'method' => 'nonExistentMethod',
]]));
$this->assertStringContainsString('method', $validator->getDescription());
}
public function testAcceptsCustomAllowedLists(): void
{
$validator = new CTAs(
allowedServices: ['custom'],
allowedMethods: ['doThing'],
);
$this->assertTrue($validator->isValid([[
'label' => 'Custom action',
'service' => 'custom',
'method' => 'doThing',
]]));
$this->assertFalse($validator->isValid([[
'label' => 'Custom action',
'service' => 'tablesDB',
'method' => 'doThing',
]]));
}
public function testDefaultMaxCountIsSixteen(): void
{
$validator = new CTAs();
$this->assertSame(CTAs::MAX_COUNT_DEFAULT, 16);
$entries = [];
for ($i = 0; $i < 16; $i++) {
$entries[] = [
'label' => 'Label ' . $i,
'service' => 'tablesDB',
'method' => 'createIndex',
];
}
$this->assertTrue($validator->isValid($entries));
$entries[] = [
'label' => 'Label 16',
'service' => 'tablesDB',
'method' => 'createIndex',
];
$this->assertFalse($validator->isValid($entries));
}
}