From 2ab698f9e97a722571150f57280fc427bc53f5af Mon Sep 17 00:00:00 2001 From: Jewei Mak Date: Fri, 21 Aug 2026 10:33:20 +0800 Subject: [PATCH 1/2] fix: use databases include for cluster list --- app/Commands/DatabaseClusterList.php | 2 +- tests/Feature/DatabaseClusterListTest.php | 38 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/DatabaseClusterListTest.php diff --git a/app/Commands/DatabaseClusterList.php b/app/Commands/DatabaseClusterList.php index dae011f..1868b47 100644 --- a/app/Commands/DatabaseClusterList.php +++ b/app/Commands/DatabaseClusterList.php @@ -28,7 +28,7 @@ public function handle() intro('Database Clusters'); $databases = spin( - fn () => $this->client->databaseClusters()->include('schemas')->list(), + fn () => $this->client->databaseClusters()->include('databases')->list(), 'Fetching databases...', ); diff --git a/tests/Feature/DatabaseClusterListTest.php b/tests/Feature/DatabaseClusterListTest.php new file mode 100644 index 0000000..b35af29 --- /dev/null +++ b/tests/Feature/DatabaseClusterListTest.php @@ -0,0 +1,38 @@ +shouldReceive('apiTokens')->andReturn(collect(['test-api-token'])); + $this->app->instance(ConfigRepository::class, $config); +}); + +afterEach(function () { + MockClient::destroyGlobal(); +}); + +it('requests databases when listing database clusters', function () { + MockClient::global([ + GetOrganizationRequest::class => MockResponse::make(organizationResponse(), 200), + ListDatabaseClustersRequest::class => MockResponse::make([ + 'data' => [], + 'included' => [], + 'links' => ['next' => null], + ], 200), + ]); + + $this->artisan('database-cluster:list', [ + '--json' => true, + '--no-interaction' => true, + ])->assertSuccessful(); + + MockClient::global()->assertSent(function ($request) { + return $request instanceof ListDatabaseClustersRequest + && $request->query()->get('include') === 'databases'; + }); +}); From f197c8a252b411d2fb660860ff6fcc3a436d8df5 Mon Sep 17 00:00:00 2001 From: Jewei Mak Date: Fri, 21 Aug 2026 13:43:39 +0800 Subject: [PATCH 2/2] fix: update database API paths Cloud rejects deprecated include=schemas requests and serves HTML for the obsolete plural restore endpoint. --- .../CreateDatabaseRestoreRequest.php | 2 +- app/Commands/DatabaseClusterDelete.php | 2 +- app/Commands/EnvironmentUpdate.php | 2 +- app/Commands/Ship.php | 6 +-- app/Resolvers/DatabaseClusterResolver.php | 4 +- tests/Feature/DatabaseClusterDeleteTest.php | 45 +++++++++++++++++++ tests/Feature/DatabaseRestoreCreateTest.php | 41 +++++++++++++++++ 7 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 tests/Feature/DatabaseClusterDeleteTest.php diff --git a/app/Client/Resources/DatabaseRestores/CreateDatabaseRestoreRequest.php b/app/Client/Resources/DatabaseRestores/CreateDatabaseRestoreRequest.php index c54fd26..a52d7c8 100644 --- a/app/Client/Resources/DatabaseRestores/CreateDatabaseRestoreRequest.php +++ b/app/Client/Resources/DatabaseRestores/CreateDatabaseRestoreRequest.php @@ -24,7 +24,7 @@ public function __construct( public function resolveEndpoint(): string { - return "/databases/clusters/{$this->data->clusterId}/restores"; + return "/databases/clusters/{$this->data->clusterId}/restore"; } protected function defaultBody(): array diff --git a/app/Commands/DatabaseClusterDelete.php b/app/Commands/DatabaseClusterDelete.php index 413e074..37fddb6 100644 --- a/app/Commands/DatabaseClusterDelete.php +++ b/app/Commands/DatabaseClusterDelete.php @@ -26,7 +26,7 @@ public function handle() $database = $this->resolvers()->databaseCluster()->from($this->argument('database')); $schemas = spin( - fn () => $this->client->databaseClusters()->include('schemas')->get($database->id)->schemas, + fn () => $this->client->databaseClusters()->include('databases')->get($database->id)->schemas, 'Fetching database cluster schemas...', ); diff --git a/app/Commands/EnvironmentUpdate.php b/app/Commands/EnvironmentUpdate.php index 2261f80..4f82dac 100644 --- a/app/Commands/EnvironmentUpdate.php +++ b/app/Commands/EnvironmentUpdate.php @@ -145,7 +145,7 @@ protected function defineFields(Environment $environment): void protected function selectDatabase(?string $value, ?string $currentId): string { $clusters = spin( - fn () => $this->client->databaseClusters()->include('schemas')->list()->collect(), + fn () => $this->client->databaseClusters()->include('databases')->list()->collect(), 'Fetching databases...', ); diff --git a/app/Commands/Ship.php b/app/Commands/Ship.php index dd42ca4..4c95653 100644 --- a/app/Commands/Ship.php +++ b/app/Commands/Ship.php @@ -373,7 +373,7 @@ protected function collectOptionsToEnable(Environment $environment): void $cluster = $this->getDatabaseCluster(); if ($cluster) { - $cluster = $this->client->databaseClusters()->include('schemas')->get($cluster->id); + $cluster = $this->client->databaseClusters()->include('databases')->get($cluster->id); $database = $this->getDatabase($cluster); $environmentParams['database_schema_id'] = $database->id; } @@ -587,7 +587,7 @@ protected function provisionDatabaseOpinionated(): ?string if (! $cluster) { $cluster = $this->createDatabaseClusterWithOptions($type->type, $preset, $name, $region); - $cluster = $this->client->databaseClusters()->include('schemas')->get($cluster->id); + $cluster = $this->client->databaseClusters()->include('databases')->get($cluster->id); } return $this->loopUntilValid( @@ -721,7 +721,7 @@ protected function getDatabase(DatabaseCluster $database): ?Database protected function getDatabaseCluster(): ?DatabaseCluster { - $databasesPaginator = $this->client->databaseClusters()->include('schemas')->list(); + $databasesPaginator = $this->client->databaseClusters()->include('databases')->list(); $databases = $databasesPaginator->collect(); if ($databases->isEmpty()) { diff --git a/app/Resolvers/DatabaseClusterResolver.php b/app/Resolvers/DatabaseClusterResolver.php index e7a28ef..655eec9 100644 --- a/app/Resolvers/DatabaseClusterResolver.php +++ b/app/Resolvers/DatabaseClusterResolver.php @@ -34,7 +34,7 @@ public function fromIdentifier(string $identifier): ?DatabaseCluster return $this->resolveFromIdentifier( $identifier, fn () => spin( - fn () => $this->client->databaseClusters()->include('schemas')->get($identifier), + fn () => $this->client->databaseClusters()->include('databases')->get($identifier), 'Fetching database...', ), fn () => $this->fetchAndFind($identifier), @@ -82,7 +82,7 @@ public function fetchAndFind(string $identifier): ?DatabaseCluster protected function fetchAll(): Collection { return collect(spin( - fn () => $this->client->databaseClusters()->include('schemas')->list()->items(), + fn () => $this->client->databaseClusters()->include('databases')->list()->items(), 'Fetching databases...', )); } diff --git a/tests/Feature/DatabaseClusterDeleteTest.php b/tests/Feature/DatabaseClusterDeleteTest.php new file mode 100644 index 0000000..ce51b97 --- /dev/null +++ b/tests/Feature/DatabaseClusterDeleteTest.php @@ -0,0 +1,45 @@ +shouldReceive('apiTokens')->andReturn(collect(['test-api-token'])); + $this->app->instance(ConfigRepository::class, $config); +}); + +afterEach(function () { + MockClient::destroyGlobal(); +}); + +it('requests databases before deleting a cluster', function () { + MockClient::global([ + GetOrganizationRequest::class => MockResponse::make(organizationResponse(), 200), + GetDatabaseClusterRequest::class => MockResponse::make(databaseClusterResponse(), 200), + DeleteDatabaseClusterRequest::class => MockResponse::make([], 204), + ]); + + $this->artisan('database-cluster:delete', [ + 'database' => 'db-123', + '--force' => true, + '--no-interaction' => true, + ])->assertSuccessful(); + + $clusterRequests = collect(MockClient::global()->getRecordedResponses()) + ->map(fn ($response) => $response->getRequest()) + ->filter(fn ($request) => $request instanceof GetDatabaseClusterRequest); + + expect($clusterRequests)->toHaveCount(2); + + $clusterRequests->each( + fn ($request) => expect($request->query()->get('include'))->toBe('databases'), + ); +}); diff --git a/tests/Feature/DatabaseRestoreCreateTest.php b/tests/Feature/DatabaseRestoreCreateTest.php index be1f71d..b257e18 100644 --- a/tests/Feature/DatabaseRestoreCreateTest.php +++ b/tests/Feature/DatabaseRestoreCreateTest.php @@ -1,6 +1,7 @@ 'snap-1', '--no-interaction' => true, ])->assertSuccessful(); + + MockClient::global()->assertSent(function ($request) { + return $request instanceof GetDatabaseClusterRequest + && $request->query()->get('include') === 'databases'; + }); }); it('creates a restore from a point-in-time non-interactively', function () { @@ -53,8 +59,43 @@ function setupDatabaseRestoreMocks(int $createStatus = 200, ?array $createBody = 'cluster' => 'db-123', 'name' => 'my-restore', '--point-in-time' => '2024-01-15T12:00:00Z', + '--json' => true, + '--fields' => 'id,name,status', + '--no-interaction' => true, + ])->assertSuccessful(); + + MockClient::global()->assertSent(function ($request) { + return $request instanceof CreateDatabaseRestoreRequest + && $request->resolveEndpoint() === '/databases/clusters/db-123/restore'; + }); +}); + +it('requests databases when resolving an omitted cluster', function () { + $cluster = databaseClusterResponse(); + + MockClient::global([ + GetOrganizationRequest::class => MockResponse::make(organizationResponse(), 200), + ListDatabaseClustersRequest::class => MockResponse::make([ + 'data' => [$cluster['data']], + 'included' => [], + 'links' => ['next' => null], + ], 200), + CreateDatabaseRestoreRequest::class => MockResponse::make(databaseClusterResponse([ + 'id' => 'db-456', + 'attributes' => ['name' => 'my-restore'], + ]), 200), + ]); + + $this->artisan('database-restore:create', [ + 'name' => 'my-restore', + '--snapshot' => 'snap-1', '--no-interaction' => true, ])->assertSuccessful(); + + MockClient::global()->assertSent(function ($request) { + return $request instanceof ListDatabaseClustersRequest + && $request->query()->get('include') === 'databases'; + }); }); it('fails when neither snapshot nor point-in-time is given non-interactively', function () {