From 6965b2fb2e776f9a4573f18757be2c9ab910d3b7 Mon Sep 17 00:00:00 2001 From: Dennis Date: Mon, 24 Aug 2026 17:48:13 +0200 Subject: [PATCH 1/2] Add a mocked test suite covering every resource, and turn CI tests back on The test suite could not run in CI: it hit the live API, so it needed a token that fork PRs do not have, and testServerRestart() restarts a real server. The teststep in php_test.yml had been commented out because of it, which meant nothing but PHPStan guarded the SDK. There was also no seam to intercept HTTP. Ploi::setApiToken() builds the Guzzle client internally and $guzzle is private, so no resource could be tested offline. Ploi::setHandler() now accepts a Guzzle handler, which is also useful in its own right for retry or logging middleware. Tests plug a MockHandler into it. Because the client is still assembled by setApiToken(), the base URI, headers and http_errors setting under test are the ones users get. Tests are split into two suites: tests/Unit mocked, no network, runs by default and in CI tests/Integration the existing live-API tests, opt-in via composer test:integration, still needs tests/.env Every one of the 36 resource classes now has a unit test asserting the URL, the HTTP verb and the JSON body of each of its methods, plus the RequiresId paths. HistoryTest never needed the network and moved to the unit suite. 288 tests, 782 assertions, no network. --- .github/workflows/php_test.yml | 6 +- CLAUDE.md | 20 +- README.md | 32 ++ composer.json | 4 +- phpunit.xml | 11 +- src/Ploi/Ploi.php | 38 ++- tests/{Ploi => Integration}/PloiTest.php | 7 +- .../Resources/AliasTest.php | 6 +- .../Resources/ServerTest.php | 8 +- .../Resources/SiteTest.php | 8 +- .../Resources/SshKeyTest.php | 8 +- .../TestCase.php} | 22 +- tests/Ploi/Traits/HistoryTest.php | 52 --- tests/Unit/Http/ResponseTest.php | 57 ++++ tests/Unit/PloiTest.php | 125 +++++++ tests/Unit/Resources/AliasTest.php | 39 +++ tests/Unit/Resources/AppTest.php | 59 ++++ tests/Unit/Resources/AuthUserTest.php | 71 ++++ tests/Unit/Resources/CertificateTest.php | 74 +++++ tests/Unit/Resources/CronjobTest.php | 66 ++++ tests/Unit/Resources/DaemonTest.php | 109 ++++++ tests/Unit/Resources/DatabaseBackupTest.php | 110 +++++++ tests/Unit/Resources/DatabaseTest.php | 119 +++++++ tests/Unit/Resources/DatabaseUserTest.php | 75 +++++ tests/Unit/Resources/DeploymentTest.php | 48 +++ tests/Unit/Resources/EnvironmentTest.php | 28 ++ tests/Unit/Resources/FastCgiTest.php | 37 +++ tests/Unit/Resources/FileBackupTest.php | 101 ++++++ tests/Unit/Resources/IncidentTest.php | 58 ++++ tests/Unit/Resources/InsightTest.php | 88 +++++ tests/Unit/Resources/LoadBalancerTest.php | 28 ++ tests/Unit/Resources/MonitorsTest.php | 45 +++ tests/Unit/Resources/NetworkRuleTest.php | 78 +++++ .../Unit/Resources/NginxConfigurationTest.php | 30 ++ tests/Unit/Resources/OpcacheTest.php | 40 +++ tests/Unit/Resources/ProjectTest.php | 104 ++++++ tests/Unit/Resources/QueueTest.php | 113 +++++++ tests/Unit/Resources/RedirectTest.php | 74 +++++ tests/Unit/Resources/RepositoryTest.php | 50 +++ tests/Unit/Resources/ResourceTest.php | 95 ++++++ tests/Unit/Resources/RobotTest.php | 28 ++ tests/Unit/Resources/ScriptTest.php | 85 +++++ tests/Unit/Resources/ServerTest.php | 309 ++++++++++++++++++ tests/Unit/Resources/ServiceTest.php | 44 +++ tests/Unit/Resources/SiteTest.php | 299 +++++++++++++++++ tests/Unit/Resources/SshKeyTest.php | 74 +++++ tests/Unit/Resources/StatusPageTest.php | 34 ++ tests/Unit/Resources/SynchronizeTest.php | 20 ++ tests/Unit/Resources/SystemUserTest.php | 72 ++++ tests/Unit/Resources/TenantTest.php | 69 ++++ tests/Unit/Resources/UserTest.php | 46 +++ .../Unit/Resources/WebserverTemplateTest.php | 28 ++ tests/Unit/TestCase.php | 126 +++++++ tests/Unit/Traits/HistoryTest.php | 55 ++++ 54 files changed, 3332 insertions(+), 100 deletions(-) rename tests/{Ploi => Integration}/PloiTest.php (94%) rename tests/{Ploi => Integration}/Resources/AliasTest.php (91%) rename tests/{Ploi => Integration}/Resources/ServerTest.php (98%) rename tests/{Ploi => Integration}/Resources/SiteTest.php (97%) rename tests/{Ploi => Integration}/Resources/SshKeyTest.php (96%) rename tests/{BaseTest.php => Integration/TestCase.php} (64%) delete mode 100644 tests/Ploi/Traits/HistoryTest.php create mode 100644 tests/Unit/Http/ResponseTest.php create mode 100644 tests/Unit/PloiTest.php create mode 100644 tests/Unit/Resources/AliasTest.php create mode 100644 tests/Unit/Resources/AppTest.php create mode 100644 tests/Unit/Resources/AuthUserTest.php create mode 100644 tests/Unit/Resources/CertificateTest.php create mode 100644 tests/Unit/Resources/CronjobTest.php create mode 100644 tests/Unit/Resources/DaemonTest.php create mode 100644 tests/Unit/Resources/DatabaseBackupTest.php create mode 100644 tests/Unit/Resources/DatabaseTest.php create mode 100644 tests/Unit/Resources/DatabaseUserTest.php create mode 100644 tests/Unit/Resources/DeploymentTest.php create mode 100644 tests/Unit/Resources/EnvironmentTest.php create mode 100644 tests/Unit/Resources/FastCgiTest.php create mode 100644 tests/Unit/Resources/FileBackupTest.php create mode 100644 tests/Unit/Resources/IncidentTest.php create mode 100644 tests/Unit/Resources/InsightTest.php create mode 100644 tests/Unit/Resources/LoadBalancerTest.php create mode 100644 tests/Unit/Resources/MonitorsTest.php create mode 100644 tests/Unit/Resources/NetworkRuleTest.php create mode 100644 tests/Unit/Resources/NginxConfigurationTest.php create mode 100644 tests/Unit/Resources/OpcacheTest.php create mode 100644 tests/Unit/Resources/ProjectTest.php create mode 100644 tests/Unit/Resources/QueueTest.php create mode 100644 tests/Unit/Resources/RedirectTest.php create mode 100644 tests/Unit/Resources/RepositoryTest.php create mode 100644 tests/Unit/Resources/ResourceTest.php create mode 100644 tests/Unit/Resources/RobotTest.php create mode 100644 tests/Unit/Resources/ScriptTest.php create mode 100644 tests/Unit/Resources/ServerTest.php create mode 100644 tests/Unit/Resources/ServiceTest.php create mode 100644 tests/Unit/Resources/SiteTest.php create mode 100644 tests/Unit/Resources/SshKeyTest.php create mode 100644 tests/Unit/Resources/StatusPageTest.php create mode 100644 tests/Unit/Resources/SynchronizeTest.php create mode 100644 tests/Unit/Resources/SystemUserTest.php create mode 100644 tests/Unit/Resources/TenantTest.php create mode 100644 tests/Unit/Resources/UserTest.php create mode 100644 tests/Unit/Resources/WebserverTemplateTest.php create mode 100644 tests/Unit/TestCase.php create mode 100644 tests/Unit/Traits/HistoryTest.php diff --git a/.github/workflows/php_test.yml b/.github/workflows/php_test.yml index fed69dc..4c37db9 100644 --- a/.github/workflows/php_test.yml +++ b/.github/workflows/php_test.yml @@ -32,7 +32,5 @@ jobs: - name: Run linter run: vendor/bin/phpstan analyse -c phpstan.neon - #- name: Run test suite - # env: - # API_TOKEN: ${{ secrets.TEST_API_KEY }} - # run: composer test + - name: Run test suite + run: composer test diff --git a/CLAUDE.md b/CLAUDE.md index 601e550..e869157 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,15 +12,21 @@ PHP SDK for the Ploi.io server management API. Wraps the REST API with a fluent, # Install dependencies composer install -# Run tests (requires tests/.env with API_TOKEN - see tests/.env.sample) +# Run the mocked unit suite (no network, no credentials) - this is what CI runs composer test -# or: vendor/bin/phpunit tests +# or: vendor/bin/phpunit --testsuite unit + +# Run the live-API suite (requires tests/.env with API_TOKEN - see tests/.env.sample) +composer test:integration + +# Run both +composer test:all # Run a single test file -vendor/bin/phpunit tests/Ploi/Resources/ServerTest.php +vendor/bin/phpunit tests/Unit/Resources/ServerTest.php # Run a single test method -vendor/bin/phpunit --filter testGetAllServers tests/Ploi/Resources/ServerTest.php +vendor/bin/phpunit --filter testListsServers # Code standards (PSR-2) composer standards @@ -58,7 +64,11 @@ Ploi → [Project, Script, StatusPage, User, WebserverTemplate, FileBackup] ## Testing -Tests extend `Tests\BaseTest` which loads `tests/.env` via phpdotenv and initializes a `Ploi` client with a real API token. Tests hit the live API - there are no mocks. +Two suites, split by whether they touch the network. + +**`tests/Unit/`** - the default suite and the one CI runs. Tests extend `Tests\Unit\TestCase`, which builds a real `Ploi` client but injects a Guzzle `MockHandler` through `Ploi::setHandler()`. Because the client itself is still built by production code, the base URI, headers and `http_errors` setting under test are the ones users get. Helpers: `queue()` to push a JSON response, `assertRequest($method, $path, $body)` to assert the verb, full URI and decoded body of a recorded request. + +**`tests/Integration/`** - hits the live API. Tests extend `Tests\Integration\TestCase`, which loads `tests/.env` via phpdotenv and needs a real API token. Excluded from the default suite; these create, restart and delete real resources. ## Code Style diff --git a/README.md b/README.md index a9f3534..e3bee02 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,15 @@ $ploi = new \Ploi\Ploi(); $ploi->setApiToken($token); ``` +If you need to hook into the HTTP layer, pass your own Guzzle handler. This is how you add middleware such as retries or logging, and how the test suite plugs in a mock handler: + +```php +$stack = \GuzzleHttp\HandlerStack::create(); +$stack->push($yourMiddleware); + +$ploi->setHandler($stack); +``` + ### Responses When calling a resource, it will return a `Ploi\Http\Response` object containing decoded JSON as well as the original response from the Guzzle client. @@ -817,3 +826,26 @@ $ploi->webserverTemplates()->perPage($amountPerPage)->page($pageNumber); // Get webserver template $ploi->webserverTemplates(123)->get(); ``` + +## Testing + +There are two suites. + +The **unit** suite is what runs by default and in CI. It uses a Guzzle mock handler, so it never touches the network and needs no credentials: + +```bash +composer test +``` + +The **integration** suite talks to the live Ploi API and needs `tests/.env` with a valid token (see `tests/.env.sample`). It creates, restarts and deletes real resources, so point it at an account you are happy to have it act on: + +```bash +cp tests/.env.sample tests/.env # then fill in API_TOKEN +composer test:integration +``` + +To run both: + +```bash +composer test:all +``` diff --git a/composer.json b/composer.json index 7b88ac8..5514560 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,8 @@ }, "scripts": { "standards": "./vendor/bin/phpcs --standard=PSR12 --colors src", - "test": "./vendor/bin/phpunit tests" + "test": "./vendor/bin/phpunit --testsuite unit", + "test:integration": "./vendor/bin/phpunit --testsuite integration", + "test:all": "./vendor/bin/phpunit --testsuite unit,integration" } } diff --git a/phpunit.xml b/phpunit.xml index e9cbb19..5d49baf 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,6 +10,7 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" + defaultTestSuite="unit" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"> @@ -17,8 +18,14 @@ - - tests + + + tests/Unit + + + + + tests/Integration diff --git a/src/Ploi/Ploi.php b/src/Ploi/Ploi.php index 9f18e99..f2760d8 100644 --- a/src/Ploi/Ploi.php +++ b/src/Ploi/Ploi.php @@ -50,6 +50,13 @@ class Ploi */ private $apiToken; + /** + * Optional Guzzle handler, used to plug in middleware or a mock handler + * + * @var callable|null + */ + private $handler; + /** * Ploi constructor. * @@ -71,8 +78,7 @@ public function setApiToken($token): self // Set the token $this->apiToken = $token; - // Generate a new Guzzle client - $this->guzzle = new Client([ + $config = [ 'base_uri' => $this->url, 'http_errors' => false, 'headers' => [ @@ -80,7 +86,33 @@ public function setApiToken($token): self 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], - ]); + ]; + + if ($this->handler) { + $config['handler'] = $this->handler; + } + + // Generate a new Guzzle client + $this->guzzle = new Client($config); + + return $this; + } + + /** + * Sets a custom Guzzle handler, for example to add middleware or to + * plug in a mock handler in tests. Passing null restores the default. + * + * @param callable|null $handler + * @return self + */ + public function setHandler(?callable $handler): self + { + $this->handler = $handler; + + // Rebuild the client so it picks up the handler + if ($this->apiToken !== null) { + $this->setApiToken($this->apiToken); + } return $this; } diff --git a/tests/Ploi/PloiTest.php b/tests/Integration/PloiTest.php similarity index 94% rename from tests/Ploi/PloiTest.php rename to tests/Integration/PloiTest.php index 0b79f8a..ebeea74 100644 --- a/tests/Ploi/PloiTest.php +++ b/tests/Integration/PloiTest.php @@ -1,18 +1,17 @@ load(); $dotenv->required('API_TOKEN')->notEmpty(); parent::setUpBeforeClass(); } - - /** - * Base test to make sure it's running - */ - public function testTrue() - { - $this->assertTrue(true); - } } diff --git a/tests/Ploi/Traits/HistoryTest.php b/tests/Ploi/Traits/HistoryTest.php deleted file mode 100644 index b984b1f..0000000 --- a/tests/Ploi/Traits/HistoryTest.php +++ /dev/null @@ -1,52 +0,0 @@ -resource = $this->getPloi()->server(); - } - - public function testGetHistory() - { - $this->assertIsArray($this->resource->getHistory()); - } - - public function testAddHistory() - { - $newHistory ='Adding to the history'; - $this->resource->addHistory($newHistory); - - $this->assertContains($newHistory, $this->resource->getHistory()); - } - - public function testSetHistory() - { - $newHistory = [ - 'New History' - ]; - - // Set the history - $this->resource->setHistory($newHistory); - - $this->assertEquals(1, count($this->resource->getHistory())); - $this->assertEquals($newHistory, $this->resource->getHistory()); - } -} diff --git a/tests/Unit/Http/ResponseTest.php b/tests/Unit/Http/ResponseTest.php new file mode 100644 index 0000000..f244bd4 --- /dev/null +++ b/tests/Unit/Http/ResponseTest.php @@ -0,0 +1,57 @@ +queue(['data' => ['id' => 1]]); + + $response = $this->ploi->makeAPICall('servers/1'); + + $this->assertInstanceOf(Response::class, $response); + $this->assertSame(200, $response->getResponse()->getStatusCode()); + } + + public function testDecodesTheJsonBody(): void + { + $this->queue(['data' => ['id' => 1, 'name' => 'web-01']]); + + $json = $this->ploi->makeAPICall('servers/1')->getJson(); + + $this->assertInstanceOf(stdClass::class, $json); + $this->assertSame('web-01', $json->data->name); + } + + public function testGetDataUnwrapsTheDataKey(): void + { + $this->queue(['data' => ['id' => 1]]); + + $this->assertSame(1, $this->ploi->makeAPICall('servers/1')->getData()->id); + } + + public function testGetDataFallsBackToTheWholeBodyWithoutADataKey(): void + { + $this->queue(['id' => 1]); + + $this->assertSame(1, $this->ploi->makeAPICall('servers/1')->getData()->id); + } + + public function testToArrayHoldsBothTheJsonAndTheResponse(): void + { + $this->queue(['data' => ['id' => 1]]); + + $array = $this->ploi->makeAPICall('servers/1')->toArray(); + + $this->assertArrayHasKey('json', $array); + $this->assertArrayHasKey('response', $array); + $this->assertSame(1, $array['json']->data->id); + } +} diff --git a/tests/Unit/PloiTest.php b/tests/Unit/PloiTest.php new file mode 100644 index 0000000..1321449 --- /dev/null +++ b/tests/Unit/PloiTest.php @@ -0,0 +1,125 @@ +queue(); + + $this->ploi->makeAPICall('servers'); + + $request = $this->request(); + + $this->assertSame('Bearer ' . self::TOKEN, $request->getHeaderLine('Authorization')); + $this->assertSame('application/json', $request->getHeaderLine('Accept')); + $this->assertSame('application/json', $request->getHeaderLine('Content-Type')); + } + + public function testResolvesAgainstTheApiBaseUri(): void + { + $this->queue(); + + $this->ploi->makeAPICall('servers/1/sites'); + + $this->assertRequest('get', 'servers/1/sites'); + } + + public function testCanSetAndGetTheApiToken(): void + { + $this->assertSame(self::TOKEN, $this->ploi->getApiToken()); + + $this->ploi->setApiToken('another-token'); + + $this->assertSame('another-token', $this->ploi->getApiToken()); + } + + public function testKeepsTheHandlerWhenTheTokenIsReplaced(): void + { + $this->queue(); + + $this->ploi->setApiToken('another-token'); + $this->ploi->makeAPICall('servers'); + + $this->assertSame('Bearer another-token', $this->request()->getHeaderLine('Authorization')); + } + + public function testRejectsAnUnsupportedHttpMethod(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid method type'); + + $this->ploi->makeAPICall('servers', 'put'); + } + + public function testAcceptsTheSupportedHttpMethods(): void + { + $this->queueMany(4); + + foreach (['get', 'post', 'patch', 'delete'] as $index => $method) { + $this->ploi->makeAPICall('servers', $method); + + $this->assertSame(strtoupper($method), $this->request($index)->getMethod()); + } + } + + /** + * @return array}> + */ + public function statusCodeProvider(): array + { + return [ + '401 unauthenticated' => [401, Unauthenticated::class], + '404 not found' => [404, NotFound::class], + '405 not allowed' => [405, NotAllowed::class], + '422 not valid' => [422, NotValid::class], + '429 too many attempts' => [429, TooManyAttempts::class], + '500 internal server' => [500, InternalServerError::class], + '503 under maintenance' => [503, PerformingMaintenance::class], + ]; + } + + /** + * @dataProvider statusCodeProvider + * + * @param class-string $exception + */ + public function testMapsStatusCodesToExceptions(int $status, string $exception): void + { + $this->queue(['message' => 'nope'], $status); + + $this->expectException($exception); + + $this->ploi->makeAPICall('servers'); + } + + public function testReturnsAResponseForASuccessfulCall(): void + { + $this->queue(['data' => ['id' => 42, 'name' => 'web-01']]); + + $response = $this->ploi->makeAPICall('servers/42'); + + $this->assertSame(42, $response->getData()->id); + $this->assertSame('web-01', $response->getData()->name); + $this->assertSame(200, $response->getResponse()->getStatusCode()); + $this->assertArrayHasKey('json', $response->toArray()); + } + + public function testHandlerCanBeRemoved(): void + { + $this->assertInstanceOf(Ploi::class, $this->ploi->setHandler(null)); + } +} diff --git a/tests/Unit/Resources/AliasTest.php b/tests/Unit/Resources/AliasTest.php new file mode 100644 index 0000000..0daab30 --- /dev/null +++ b/tests/Unit/Resources/AliasTest.php @@ -0,0 +1,39 @@ +queue(); + + $this->ploi->servers(1)->sites(2)->alias()->get(); + + $this->assertRequest('get', 'servers/1/sites/2/aliases'); + } + + public function testCreatesAliases(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->alias()->create(['www.example.com', 'example.nl']); + + $this->assertRequest('post', 'servers/1/sites/2/aliases', [ + 'aliases' => ['www.example.com', 'example.nl'], + ]); + } + + public function testDeletesAnAlias(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->alias()->delete('www.example.com'); + + $this->assertRequest('delete', 'servers/1/sites/2/aliases/www.example.com'); + } +} diff --git a/tests/Unit/Resources/AppTest.php b/tests/Unit/Resources/AppTest.php new file mode 100644 index 0000000..0b6bb02 --- /dev/null +++ b/tests/Unit/Resources/AppTest.php @@ -0,0 +1,59 @@ +queue(); + + $this->ploi->servers(1)->sites(2)->app()->get(); + + $this->assertRequest('get', 'servers/1/sites/2'); + } + + public function testInstallsAnApp(): void + { + $this->queue(['data' => ['id' => 3, 'type' => 'wordpress']]); + + $app = $this->ploi->servers(1)->sites(2)->app(); + $data = $app->install(); + + $this->assertRequest('post', 'servers/1/sites/2/wordpress', ['create_database' => false]); + $this->assertSame('wordpress', $data->type); + $this->assertSame(3, $app->getId()); + } + + public function testInstallsAnAppWithADatabase(): void + { + $this->queue(['data' => ['id' => 3]]); + + $this->ploi->servers(1)->sites(2)->app()->install('matomo', ['create_database' => true]); + + $this->assertRequest('post', 'servers/1/sites/2/matomo', ['create_database' => true]); + } + + public function testInstallReturnsTheDecodedErrorWhenTheApiRejectsIt(): void + { + $this->queue(['message' => 'Site already has an app'], 422); + + $result = $this->ploi->servers(1)->sites(2)->app()->install(); + + $this->assertSame('Site already has an app', $result->message); + } + + public function testUninstallsAnApp(): void + { + $this->queue(); + + $result = $this->ploi->servers(1)->sites(2)->app()->uninstall('wordpress'); + + $this->assertRequest('delete', 'servers/1/sites/2/wordpress'); + $this->assertTrue($result); + } +} diff --git a/tests/Unit/Resources/AuthUserTest.php b/tests/Unit/Resources/AuthUserTest.php new file mode 100644 index 0000000..0e6eead --- /dev/null +++ b/tests/Unit/Resources/AuthUserTest.php @@ -0,0 +1,71 @@ +queue(); + + $this->ploi->servers(1)->sites(2)->authUser()->get(); + + $this->assertRequest('get', 'servers/1/sites/2/auth-users?page=1'); + } + + public function testGetsASingleAuthUser(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->authUser(3)->get(); + + $this->assertRequest('get', 'servers/1/sites/2/auth-users/3'); + } + + public function testCreatesAnAuthUser(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->authUser()->create('admin', 'secret'); + + $this->assertRequest('post', 'servers/1/sites/2/auth-users', [ + 'name' => 'admin', + 'password' => 'secret', + 'path' => null, + ]); + } + + public function testCreatesAnAuthUserForAPath(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->authUser()->create('admin', 'secret', '/staging'); + + $this->assertRequest('post', 'servers/1/sites/2/auth-users', [ + 'name' => 'admin', + 'password' => 'secret', + 'path' => '/staging', + ]); + } + + public function testDeletesAnAuthUser(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->authUser(3)->delete(); + + $this->assertRequest('delete', 'servers/1/sites/2/auth-users/3'); + } + + public function testDeleteRequiresAnId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers(1)->sites(2)->authUser()->delete(); + } +} diff --git a/tests/Unit/Resources/CertificateTest.php b/tests/Unit/Resources/CertificateTest.php new file mode 100644 index 0000000..bd1781d --- /dev/null +++ b/tests/Unit/Resources/CertificateTest.php @@ -0,0 +1,74 @@ +queue(); + + $this->ploi->servers(1)->sites(2)->certificates()->get(); + + $this->assertRequest('get', 'servers/1/sites/2/certificates?page=1'); + } + + public function testGetsASingleCertificate(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->certificates(3)->get(); + + $this->assertRequest('get', 'servers/1/sites/2/certificates/3'); + } + + public function testCreatesACertificate(): void + { + $this->queue(['data' => ['id' => 3]]); + + $certificate = $this->ploi->servers(1)->sites(2)->certificates(); + $certificate->create('example.com'); + + $this->assertRequest('post', 'servers/1/sites/2/certificates', [ + 'certificate' => 'example.com', + 'type' => 'letsencrypt', + 'force' => false, + ]); + + $this->assertSame(3, $certificate->getId()); + } + + public function testForcesACustomCertificate(): void + { + $this->queue(['data' => ['id' => 3]]); + + $this->ploi->servers(1)->sites(2)->certificates()->create('example.com', 'custom', true); + + $this->assertRequest('post', 'servers/1/sites/2/certificates', [ + 'certificate' => 'example.com', + 'type' => 'custom', + 'force' => true, + ]); + } + + public function testDeletesACertificate(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->certificates(3)->delete(); + + $this->assertRequest('delete', 'servers/1/sites/2/certificates/3'); + } + + public function testDeleteRequiresAnId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers(1)->sites(2)->certificates()->delete(); + } +} diff --git a/tests/Unit/Resources/CronjobTest.php b/tests/Unit/Resources/CronjobTest.php new file mode 100644 index 0000000..cd8c4fa --- /dev/null +++ b/tests/Unit/Resources/CronjobTest.php @@ -0,0 +1,66 @@ +queue(); + + $this->ploi->servers(1)->cronjobs()->get(); + + $this->assertRequest('get', 'servers/1/crontabs?page=1'); + } + + public function testGetsASingleCronjob(): void + { + $this->queue(); + + $this->ploi->servers(1)->cronjobs(2)->get(); + + $this->assertRequest('get', 'servers/1/crontabs/2'); + } + + public function testCreatesACronjob(): void + { + $this->queue(['data' => ['id' => 2]]); + + $cronjob = $this->ploi->servers(1)->cronjobs(); + $cronjob->create('php artisan schedule:run', '* * * * *'); + + $this->assertRequest('post', 'servers/1/crontabs', [ + 'command' => 'php artisan schedule:run', + 'frequency' => '* * * * *', + 'user' => 'ploi', + ]); + + $this->assertSame(2, $cronjob->getId()); + } + + public function testCreatesACronjobForAnotherUser(): void + { + $this->queue(['data' => ['id' => 2]]); + + $this->ploi->servers(1)->cronjobs()->create('backup.sh', '0 3 * * *', 'deployer'); + + $this->assertRequest('post', 'servers/1/crontabs', [ + 'command' => 'backup.sh', + 'frequency' => '0 3 * * *', + 'user' => 'deployer', + ]); + } + + public function testDeletesACronjob(): void + { + $this->queue(); + + $this->ploi->servers(1)->cronjobs(2)->delete(); + + $this->assertRequest('delete', 'servers/1/crontabs/2'); + } +} diff --git a/tests/Unit/Resources/DaemonTest.php b/tests/Unit/Resources/DaemonTest.php new file mode 100644 index 0000000..9be3de1 --- /dev/null +++ b/tests/Unit/Resources/DaemonTest.php @@ -0,0 +1,109 @@ +queue(); + + $this->ploi->servers(1)->daemons()->get(); + + $this->assertRequest('get', 'servers/1/daemons?page=1'); + } + + public function testGetsASingleDaemon(): void + { + $this->queue(); + + $this->ploi->servers(1)->daemons(2)->get(); + + $this->assertRequest('get', 'servers/1/daemons/2'); + } + + public function testCreatesADaemon(): void + { + $this->queue(['data' => ['id' => 2]]); + + $daemon = $this->ploi->servers(1)->daemons(); + $daemon->create('php artisan horizon', 'ploi', 2); + + $this->assertRequest('post', 'servers/1/daemons', [ + 'command' => 'php artisan horizon', + 'system_user' => 'ploi', + 'processes' => 2, + 'directory' => null, + ]); + + $this->assertSame(2, $daemon->getId()); + } + + public function testCreatesADaemonInADirectory(): void + { + $this->queue(['data' => ['id' => 2]]); + + $this->ploi->servers(1)->daemons()->create('php artisan horizon', 'deployer', 1, '/home/deployer/app'); + + $this->assertRequest('post', 'servers/1/daemons', [ + 'command' => 'php artisan horizon', + 'system_user' => 'deployer', + 'processes' => 1, + 'directory' => '/home/deployer/app', + ]); + } + + public function testRestartsADaemon(): void + { + $this->queue(); + + $this->ploi->servers(1)->daemons(2)->restart(); + + $this->assertRequest('post', 'servers/1/daemons/2/restart'); + } + + public function testPausesADaemon(): void + { + $this->queue(); + + $this->ploi->servers(1)->daemons(2)->pause(); + + $this->assertRequest('post', 'servers/1/daemons/2/toggle-pause'); + } + + public function testDeletesADaemon(): void + { + $this->queue(); + + $this->ploi->servers(1)->daemons(2)->delete(); + + $this->assertRequest('delete', 'servers/1/daemons/2'); + } + + /** + * @return array + */ + public function requiresIdProvider(): array + { + return [ + 'restart' => ['restart'], + 'pause' => ['pause'], + 'delete' => ['delete'], + ]; + } + + /** + * @dataProvider requiresIdProvider + */ + public function testMethodsThatNeedADaemonIdThrowWithoutOne(string $method): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers(1)->daemons()->{$method}(); + } +} diff --git a/tests/Unit/Resources/DatabaseBackupTest.php b/tests/Unit/Resources/DatabaseBackupTest.php new file mode 100644 index 0000000..d8f737b --- /dev/null +++ b/tests/Unit/Resources/DatabaseBackupTest.php @@ -0,0 +1,110 @@ +queue(); + + $this->ploi->servers(1)->databases(2)->backups()->get(); + + $this->assertRequest('get', 'backups/database?page=1'); + } + + public function testGetsASingleDatabaseBackup(): void + { + $this->queue(); + + $this->ploi->servers(1)->databases(2)->backups(3)->get(); + + $this->assertRequest('get', 'backups/database/3'); + } + + public function testCreatesABackupForTheParentDatabase(): void + { + $this->queue(['data' => ['id' => 3]]); + + $backup = $this->ploi->servers(1)->databases(2)->backups(); + $backup->create(24, 5); + + $this->assertRequest('post', 'backups/database', [ + 'backup_configuration' => 5, + 'server' => 1, + 'databases' => [2], + 'interval' => 24, + 'table_exclusions' => null, + 'locations' => null, + 'path' => null, + 'keep_backup_amount' => null, + 'custom_name' => null, + 'password' => null, + 'deleteOnFail' => null, + ]); + + $this->assertSame(3, $backup->getId()); + } + + public function testCreatesABackupWithExplicitDatabases(): void + { + $this->queue(['data' => ['id' => 3]]); + + $this->ploi->servers(1)->databases(2)->backups()->create( + 12, + 5, + [7, 8], + 'logs', + 'local', + '/backups', + 10, + 'nightly', + 'secret', + true + ); + + $this->assertRequest('post', 'backups/database', [ + 'backup_configuration' => 5, + 'server' => 1, + 'databases' => [7, 8], + 'interval' => 12, + 'table_exclusions' => 'logs', + 'locations' => 'local', + 'path' => '/backups', + 'keep_backup_amount' => 10, + 'custom_name' => 'nightly', + 'password' => 'secret', + 'deleteOnFail' => true, + ]); + } + + public function testTogglesABackup(): void + { + $this->queue(); + + $this->ploi->servers(1)->databases(2)->backups(3)->toggle(); + + $this->assertRequest('patch', 'backups/database/3/toggle'); + } + + public function testDeletesABackup(): void + { + $this->queue(); + + $this->ploi->servers(1)->databases(2)->backups(3)->delete(); + + $this->assertRequest('delete', 'backups/database/3'); + } + + public function testDeleteRequiresAnId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers(1)->databases(2)->backups()->delete(); + } +} diff --git a/tests/Unit/Resources/DatabaseTest.php b/tests/Unit/Resources/DatabaseTest.php new file mode 100644 index 0000000..a6b3f27 --- /dev/null +++ b/tests/Unit/Resources/DatabaseTest.php @@ -0,0 +1,119 @@ +queue(); + + $this->ploi->servers(1)->databases()->get(); + + $this->assertRequest('get', 'servers/1/databases?page=1'); + } + + public function testGetsASingleDatabase(): void + { + $this->queue(); + + $this->ploi->servers(1)->databases(2)->get(); + + $this->assertRequest('get', 'servers/1/databases/2'); + } + + public function testCreatesADatabase(): void + { + $this->queue(['data' => ['id' => 3]]); + + $database = $this->ploi->servers(1)->databases(); + $database->create('shop', 'shop_user', 'secret'); + + $this->assertRequest('post', 'servers/1/databases', [ + 'name' => 'shop', + 'user' => 'shop_user', + 'password' => 'secret', + 'description' => null, + 'site_id' => null, + ]); + + $this->assertSame(3, $database->getId()); + } + + public function testCreatesADatabaseWithADescriptionAndSite(): void + { + $this->queue(['data' => ['id' => 3]]); + + $this->ploi->servers(1)->databases()->create('shop', 'shop_user', 'secret', 'Webshop', 8); + + $this->assertRequest('post', 'servers/1/databases', [ + 'name' => 'shop', + 'user' => 'shop_user', + 'password' => 'secret', + 'description' => 'Webshop', + 'site_id' => 8, + ]); + } + + public function testDeletesADatabase(): void + { + $this->queue(); + + $this->ploi->servers(1)->databases(2)->delete(); + + $this->assertRequest('delete', 'servers/1/databases/2'); + } + + public function testAcknowledgesADatabase(): void + { + $this->queue(); + + $this->ploi->servers(1)->databases()->acknowledge('shop'); + + $this->assertRequest('post', 'servers/1/databases/acknowledge', ['name' => 'shop']); + } + + public function testForgetsADatabase(): void + { + $this->queue(); + + $this->ploi->servers(1)->databases(2)->forget(); + + $this->assertRequest('delete', 'servers/1/databases/2/forget'); + } + + public function testDuplicatesADatabase(): void + { + $this->queue(); + + $this->ploi->servers(1)->databases(2)->duplicate('shop_copy', 'copy_user', 'secret'); + + $this->assertRequest('post', 'servers/1/databases/2/duplicate', [ + 'name' => 'shop_copy', + 'user' => 'copy_user', + 'password' => 'secret', + ]); + } + + public function testDuplicateRequiresAnId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers(1)->databases()->duplicate('shop_copy'); + } + + public function testExposesBackupsAndUsers(): void + { + $database = $this->ploi->servers(1)->databases(2); + + $this->assertInstanceOf(DatabaseBackup::class, $database->backups()); + $this->assertInstanceOf(DatabaseUser::class, $database->users()); + } +} diff --git a/tests/Unit/Resources/DatabaseUserTest.php b/tests/Unit/Resources/DatabaseUserTest.php new file mode 100644 index 0000000..d686353 --- /dev/null +++ b/tests/Unit/Resources/DatabaseUserTest.php @@ -0,0 +1,75 @@ +queue(); + + $this->ploi->servers(1)->databases(2)->users()->get(); + + $this->assertRequest('get', 'servers/1/databases/2/users?page=1'); + } + + public function testGetsASingleDatabaseUser(): void + { + $this->queue(); + + $this->ploi->servers(1)->databases(2)->users(3)->get(); + + $this->assertRequest('get', 'servers/1/databases/2/users/3'); + } + + public function testCreatesADatabaseUser(): void + { + $this->queue(); + + $this->ploi->servers(1)->databases(2)->users()->create('reader', 'secret'); + + $this->assertRequest('post', 'servers/1/databases/2/users', [ + 'user' => 'reader', + 'password' => 'secret', + 'remote' => false, + 'remote_ip' => '%', + 'readonly' => false, + ]); + } + + public function testCreatesARemoteReadonlyDatabaseUser(): void + { + $this->queue(); + + $this->ploi->servers(1)->databases(2)->users()->create('reader', 'secret', true, '1.2.3.4', true); + + $this->assertRequest('post', 'servers/1/databases/2/users', [ + 'user' => 'reader', + 'password' => 'secret', + 'remote' => true, + 'remote_ip' => '1.2.3.4', + 'readonly' => true, + ]); + } + + public function testDeletesADatabaseUser(): void + { + $this->queue(); + + $this->ploi->servers(1)->databases(2)->users(3)->delete(); + + $this->assertRequest('delete', 'servers/1/databases/2/users/3'); + } + + public function testDeleteRequiresAnId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers(1)->databases(2)->users()->delete(); + } +} diff --git a/tests/Unit/Resources/DeploymentTest.php b/tests/Unit/Resources/DeploymentTest.php new file mode 100644 index 0000000..cb71f2b --- /dev/null +++ b/tests/Unit/Resources/DeploymentTest.php @@ -0,0 +1,48 @@ +queue(); + + $this->ploi->servers(1)->sites(2)->deployment()->deploy(); + + $this->assertRequest('post', 'servers/1/sites/2/deploy'); + } + + public function testDeploysToProduction(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->deployment()->deployToProduction(); + + $this->assertRequest('post', 'servers/1/sites/2/deploy-to-production'); + } + + public function testGetsTheDeployScript(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->deployment()->deployScript(); + + $this->assertRequest('get', 'servers/1/sites/2/deploy/script'); + } + + public function testUpdatesTheDeployScript(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->deployment()->updateDeployScript('php artisan migrate'); + + $this->assertRequest('patch', 'servers/1/sites/2/deploy/script', [ + 'deploy_script' => 'php artisan migrate', + ]); + } +} diff --git a/tests/Unit/Resources/EnvironmentTest.php b/tests/Unit/Resources/EnvironmentTest.php new file mode 100644 index 0000000..8b246ad --- /dev/null +++ b/tests/Unit/Resources/EnvironmentTest.php @@ -0,0 +1,28 @@ +queue(); + + $this->ploi->servers(1)->sites(2)->environment()->get(); + + $this->assertRequest('get', 'servers/1/sites/2/env'); + } + + public function testUpdatesTheEnvironmentFile(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->environment()->update('APP_ENV=production'); + + $this->assertRequest('patch', 'servers/1/sites/2/env', ['content' => 'APP_ENV=production']); + } +} diff --git a/tests/Unit/Resources/FastCgiTest.php b/tests/Unit/Resources/FastCgiTest.php new file mode 100644 index 0000000..e0ca8bb --- /dev/null +++ b/tests/Unit/Resources/FastCgiTest.php @@ -0,0 +1,37 @@ +queue(); + + $this->ploi->servers(1)->sites(2)->fastCgi()->enable(); + + $this->assertRequest('post', 'servers/1/sites/2/fastcgi-cache/enable'); + } + + public function testDisablesFastCgiCache(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->fastCgi()->disable(); + + $this->assertRequest('delete', 'servers/1/sites/2/fastcgi-cache/disable'); + } + + public function testFlushesFastCgiCache(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->fastCgi()->flush(); + + $this->assertRequest('post', 'servers/1/sites/2/fastcgi-cache/flush'); + } +} diff --git a/tests/Unit/Resources/FileBackupTest.php b/tests/Unit/Resources/FileBackupTest.php new file mode 100644 index 0000000..2909b5b --- /dev/null +++ b/tests/Unit/Resources/FileBackupTest.php @@ -0,0 +1,101 @@ +queue(); + + $this->ploi->fileBackups()->get(); + + $this->assertRequest('get', 'backups/file?page=1'); + } + + public function testGetsASingleFileBackup(): void + { + $this->queue(); + + $this->ploi->fileBackups(1)->get(); + + $this->assertRequest('get', 'backups/file/1'); + } + + public function testCreatesAFileBackup(): void + { + $this->queue(); + + $this->ploi->fileBackups()->create(5, 1, [2], 24, ['/home/ploi']); + + $this->assertRequest('post', 'backups/file', [ + 'backup_configuration' => 5, + 'server' => 1, + 'sites' => [2], + 'interval' => 24, + 'path' => ['/home/ploi'], + 'locations' => null, + 'keep_backup_amount' => null, + 'custom_name' => null, + 'password' => null, + 'deleteOnFail' => null, + ]); + } + + public function testCreatesAFileBackupWithEveryOption(): void + { + $this->queue(); + + $this->ploi->fileBackups()->create(5, 1, [2], 12, ['/var/www'], 'local', 7, 'nightly', 'secret', true); + + $this->assertRequest('post', 'backups/file', [ + 'backup_configuration' => 5, + 'server' => 1, + 'sites' => [2], + 'interval' => 12, + 'path' => ['/var/www'], + 'locations' => 'local', + 'keep_backup_amount' => 7, + 'custom_name' => 'nightly', + 'password' => 'secret', + 'deleteOnFail' => true, + ]); + } + + public function testRunsAFileBackup(): void + { + $this->queue(); + + $this->ploi->fileBackups(1)->run(); + + $this->assertRequest('post', 'backups/file/1/run'); + } + + public function testDeletesAFileBackup(): void + { + $this->queue(); + + $this->ploi->fileBackups(1)->delete(); + + $this->assertRequest('delete', 'backups/file/1'); + } + + public function testRunAndDeleteRequireAnId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->fileBackups()->run(); + } + + public function testFileBackupsIsAnAliasForFileBackup(): void + { + $this->assertInstanceOf(FileBackup::class, $this->ploi->fileBackup()); + $this->assertInstanceOf(FileBackup::class, $this->ploi->fileBackups()); + } +} diff --git a/tests/Unit/Resources/IncidentTest.php b/tests/Unit/Resources/IncidentTest.php new file mode 100644 index 0000000..6b5bc0c --- /dev/null +++ b/tests/Unit/Resources/IncidentTest.php @@ -0,0 +1,58 @@ +queue(); + + $this->ploi->statusPage(1)->incident()->get(); + + $this->assertRequest('get', 'status-pages/1/incidents?page=1'); + } + + public function testGetsASingleIncident(): void + { + $this->queue(); + + $this->ploi->statusPage(1)->incident(2)->get(); + + $this->assertRequest('get', 'status-pages/1/incidents/2'); + } + + public function testCreatesAnIncident(): void + { + $this->queue(); + + $this->ploi->statusPage(1)->incident()->create('Database down', 'Investigating', 'critical'); + + $this->assertRequest('post', 'status-pages/1/incidents', [ + 'title' => 'Database down', + 'description' => 'Investigating', + 'severity' => 'critical', + ]); + } + + public function testDeletesAnIncident(): void + { + $this->queue(); + + $this->ploi->statusPage(1)->incident(2)->delete(); + + $this->assertRequest('delete', 'status-pages/1/incidents/2'); + } + + public function testDeleteRequiresAnId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->statusPage(1)->incident()->delete(); + } +} diff --git a/tests/Unit/Resources/InsightTest.php b/tests/Unit/Resources/InsightTest.php new file mode 100644 index 0000000..49ab72f --- /dev/null +++ b/tests/Unit/Resources/InsightTest.php @@ -0,0 +1,88 @@ +queue(); + + $this->ploi->servers(1)->insights()->get(); + + $this->assertRequest('get', 'servers/1/insights?page=1'); + } + + public function testGetsASingleInsight(): void + { + $this->queue(); + + $this->ploi->servers(1)->insights(2)->get(); + + $this->assertRequest('get', 'servers/1/insights/2'); + } + + public function testGetsInsightDetail(): void + { + $this->queue(); + + $this->ploi->servers(1)->insights(2)->detail(); + + $this->assertRequest('get', 'servers/1/insights/2/detail'); + } + + public function testAutomaticallyFixesAnInsight(): void + { + $this->queue(); + + $this->ploi->servers(1)->insights(2)->automaticallyFix(); + + $this->assertRequest('post', 'servers/1/insights/2/automatically-fix'); + } + + public function testIgnoresAnInsight(): void + { + $this->queue(); + + $this->ploi->servers(1)->insights(2)->ignore(); + + $this->assertRequest('post', 'servers/1/insights/2/ignore'); + } + + public function testDeletesAnInsight(): void + { + $this->queue(); + + $this->ploi->servers(1)->insights(2)->delete(); + + $this->assertRequest('delete', 'servers/1/insights/2'); + } + + /** + * @return array + */ + public function requiresIdProvider(): array + { + return [ + 'detail' => ['detail'], + 'automaticallyFix' => ['automaticallyFix'], + 'ignore' => ['ignore'], + 'delete' => ['delete'], + ]; + } + + /** + * @dataProvider requiresIdProvider + */ + public function testMethodsThatNeedAnInsightIdThrowWithoutOne(string $method): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers(1)->insights()->{$method}(); + } +} diff --git a/tests/Unit/Resources/LoadBalancerTest.php b/tests/Unit/Resources/LoadBalancerTest.php new file mode 100644 index 0000000..4647237 --- /dev/null +++ b/tests/Unit/Resources/LoadBalancerTest.php @@ -0,0 +1,28 @@ +queue(); + + $this->ploi->servers(1)->loadBalancer()->requestCertificate('example.com'); + + $this->assertRequest('post', 'servers/1/load-balancer/example.com/request-certificate'); + } + + public function testRevokesACertificate(): void + { + $this->queue(); + + $this->ploi->servers(1)->loadBalancer()->revokeCertificate('example.com'); + + $this->assertRequest('delete', 'servers/1/load-balancer/example.com/revoke-certificate'); + } +} diff --git a/tests/Unit/Resources/MonitorsTest.php b/tests/Unit/Resources/MonitorsTest.php new file mode 100644 index 0000000..7e3654e --- /dev/null +++ b/tests/Unit/Resources/MonitorsTest.php @@ -0,0 +1,45 @@ +queue(); + + $this->ploi->servers(1)->sites(2)->monitors()->get(); + + $this->assertRequest('get', 'servers/1/sites/2/monitors?page=1'); + } + + public function testGetsASingleMonitor(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->monitors(3)->get(); + + $this->assertRequest('get', 'servers/1/sites/2/monitors/3'); + } + + public function testGetsUptimeResponses(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->monitors(3)->uptimeResponses(); + + $this->assertRequest('get', 'servers/1/sites/2/monitors/3/uptime-responses'); + } + + public function testUptimeResponsesRequiresAnId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers(1)->sites(2)->monitors()->uptimeResponses(); + } +} diff --git a/tests/Unit/Resources/NetworkRuleTest.php b/tests/Unit/Resources/NetworkRuleTest.php new file mode 100644 index 0000000..738b824 --- /dev/null +++ b/tests/Unit/Resources/NetworkRuleTest.php @@ -0,0 +1,78 @@ +queue(); + + $this->ploi->servers(1)->networkRules()->get(); + + $this->assertRequest('get', 'servers/1/network-rules?page=1'); + } + + public function testGetsASingleNetworkRule(): void + { + $this->queue(); + + $this->ploi->servers(1)->networkRules(2)->get(); + + $this->assertRequest('get', 'servers/1/network-rules/2'); + } + + public function testCreatesANetworkRule(): void + { + $this->queue(['data' => ['id' => 2]]); + + $rule = $this->ploi->servers(1)->networkRules(); + $rule->create('http', 80); + + $this->assertRequest('post', 'servers/1/network-rules', [ + 'name' => 'http', + 'port' => 80, + 'type' => 'tcp', + 'rule_type' => 'allow', + 'from_ip_address' => null, + ]); + + $this->assertSame(2, $rule->getId()); + } + + public function testCreatesADenyRuleForASingleIp(): void + { + $this->queue(['data' => ['id' => 2]]); + + $this->ploi->servers(1)->networkRules()->create('block', 22, 'udp', '1.2.3.4', 'deny'); + + $this->assertRequest('post', 'servers/1/network-rules', [ + 'name' => 'block', + 'port' => 22, + 'type' => 'udp', + 'rule_type' => 'deny', + 'from_ip_address' => '1.2.3.4', + ]); + } + + public function testDeletesANetworkRule(): void + { + $this->queue(); + + $this->ploi->servers(1)->networkRules(2)->delete(); + + $this->assertRequest('delete', 'servers/1/network-rules/2'); + } + + public function testDeleteRequiresAnId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers(1)->networkRules()->delete(); + } +} diff --git a/tests/Unit/Resources/NginxConfigurationTest.php b/tests/Unit/Resources/NginxConfigurationTest.php new file mode 100644 index 0000000..06b6227 --- /dev/null +++ b/tests/Unit/Resources/NginxConfigurationTest.php @@ -0,0 +1,30 @@ +queue(); + + $this->ploi->servers(1)->sites(2)->nginxConfiguration()->get(); + + $this->assertRequest('get', 'servers/1/sites/2/nginx-configuration'); + } + + public function testUpdatesTheNginxConfiguration(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->nginxConfiguration()->update('server { listen 80; }'); + + $this->assertRequest('patch', 'servers/1/sites/2/nginx-configuration', [ + 'content' => 'server { listen 80; }', + ]); + } +} diff --git a/tests/Unit/Resources/OpcacheTest.php b/tests/Unit/Resources/OpcacheTest.php new file mode 100644 index 0000000..acf0aa4 --- /dev/null +++ b/tests/Unit/Resources/OpcacheTest.php @@ -0,0 +1,40 @@ +queue(); + + $this->ploi->servers(1)->opcache()->refresh(); + + $this->assertRequest('post', 'servers/1/refresh-opcache'); + } + + public function testEnablesOpcache(): void + { + $this->queue(); + + $this->ploi->servers(1)->opcache()->enable(); + + $this->assertRequest('post', 'servers/1/enable-opcache'); + } + + /** + * Note this posts, where the deprecated Server::disableOpcache() deletes. + */ + public function testDisablesOpcache(): void + { + $this->queue(); + + $this->ploi->servers(1)->opcache()->disable(); + + $this->assertRequest('post', 'servers/1/disable-opcache'); + } +} diff --git a/tests/Unit/Resources/ProjectTest.php b/tests/Unit/Resources/ProjectTest.php new file mode 100644 index 0000000..0cdaca5 --- /dev/null +++ b/tests/Unit/Resources/ProjectTest.php @@ -0,0 +1,104 @@ +queue(); + + $this->ploi->projects()->get(); + + $this->assertRequest('get', 'projects?page=1'); + } + + public function testGetsASingleProject(): void + { + $this->queue(); + + $this->ploi->projects(1)->get(); + + $this->assertRequest('get', 'projects/1'); + } + + public function testCreatesAProject(): void + { + $this->queue(['data' => ['id' => 4]]); + + $project = $this->ploi->projects(); + $project->create('Webshop', [1], [2]); + + $this->assertRequest('post', 'projects', [ + 'title' => 'Webshop', + 'servers' => [1], + 'sites' => [2], + ]); + + $this->assertSame(4, $project->getId()); + } + + public function testCreateAcceptsExtraOptions(): void + { + $this->queue(['data' => ['id' => 4]]); + + $this->ploi->projects()->create('Webshop', [], [], ['description' => 'Our shop']); + + $this->assertRequest('post', 'projects', [ + 'title' => 'Webshop', + 'servers' => [], + 'sites' => [], + 'description' => 'Our shop', + ]); + } + + public function testUpdatesAProject(): void + { + $this->queue(); + + $this->ploi->projects(1)->update('Renamed', [1], [2]); + + $this->assertRequest('patch', 'projects/1', [ + 'title' => 'Renamed', + 'servers' => [1], + 'sites' => [2], + ]); + } + + public function testDeletesAProject(): void + { + $this->queue(); + + $this->ploi->projects(1)->delete(); + + $this->assertRequest('delete', 'projects/1'); + } + + public function testSearchesProjects(): void + { + $this->queue(); + + $this->ploi->projects()->search('shop'); + + $this->assertRequest('get', 'projects?search=shop'); + } + + public function testUpdateAndDeleteRequireAnId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->projects()->update('Renamed'); + } + + public function testProjectsIsAnAliasForProject(): void + { + $this->assertInstanceOf(Project::class, $this->ploi->project()); + $this->assertInstanceOf(Project::class, $this->ploi->projects()); + } +} diff --git a/tests/Unit/Resources/QueueTest.php b/tests/Unit/Resources/QueueTest.php new file mode 100644 index 0000000..9015c2a --- /dev/null +++ b/tests/Unit/Resources/QueueTest.php @@ -0,0 +1,113 @@ +queue(); + + $this->ploi->servers(1)->sites(2)->queues()->get(); + + $this->assertRequest('get', 'servers/1/sites/2/queues?page=1'); + } + + public function testGetsASingleQueue(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->queues(3)->get(); + + $this->assertRequest('get', 'servers/1/sites/2/queues/3'); + } + + public function testCreatesAQueueWithDefaults(): void + { + $this->queue(['data' => ['id' => 3]]); + + $queue = $this->ploi->servers(1)->sites(2)->queues(); + $queue->create(); + + $this->assertRequest('post', 'servers/1/sites/2/queues', [ + 'connection' => 'database', + 'queue' => 'default', + 'maximum_seconds' => 60, + 'sleep' => 30, + 'processes' => 1, + 'maximum_tries' => 1, + ]); + + $this->assertSame(3, $queue->getId()); + } + + public function testCreatesAQueueWithEveryOption(): void + { + $this->queue(['data' => ['id' => 3]]); + + $this->ploi->servers(1)->sites(2)->queues()->create('redis', 'emails', 120, 5, 4, 3); + + $this->assertRequest('post', 'servers/1/sites/2/queues', [ + 'connection' => 'redis', + 'queue' => 'emails', + 'maximum_seconds' => 120, + 'sleep' => 5, + 'processes' => 4, + 'maximum_tries' => 3, + ]); + } + + public function testRestartsAQueue(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->queues(3)->restart(); + + $this->assertRequest('post', 'servers/1/sites/2/queues/3/restart'); + } + + public function testPausesAQueue(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->queues(3)->pause(); + + $this->assertRequest('post', 'servers/1/sites/2/queues/3/toggle-pause'); + } + + public function testDeletesAQueue(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->queues(3)->delete(); + + $this->assertRequest('delete', 'servers/1/sites/2/queues/3'); + } + + /** + * @return array + */ + public function requiresIdProvider(): array + { + return [ + 'restart' => ['restart'], + 'pause' => ['pause'], + 'delete' => ['delete'], + ]; + } + + /** + * @dataProvider requiresIdProvider + */ + public function testMethodsThatNeedAQueueIdThrowWithoutOne(string $method): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers(1)->sites(2)->queues()->{$method}(); + } +} diff --git a/tests/Unit/Resources/RedirectTest.php b/tests/Unit/Resources/RedirectTest.php new file mode 100644 index 0000000..fbdbd41 --- /dev/null +++ b/tests/Unit/Resources/RedirectTest.php @@ -0,0 +1,74 @@ +queue(); + + $this->ploi->servers(1)->sites(2)->redirects()->get(); + + $this->assertRequest('get', 'servers/1/sites/2/redirects?page=1'); + } + + public function testGetsASingleRedirect(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->redirects(3)->get(); + + $this->assertRequest('get', 'servers/1/sites/2/redirects/3'); + } + + public function testCreatesARedirect(): void + { + $this->queue(['data' => ['id' => 3]]); + + $redirect = $this->ploi->servers(1)->sites(2)->redirects(); + $redirect->create('/old', '/new'); + + $this->assertRequest('post', 'servers/1/sites/2/redirects', [ + 'redirect_from' => '/old', + 'redirect_to' => '/new', + 'type' => 'redirect', + ]); + + $this->assertSame(3, $redirect->getId()); + } + + public function testCreatesAPermanentRedirect(): void + { + $this->queue(['data' => ['id' => 3]]); + + $this->ploi->servers(1)->sites(2)->redirects()->create('/old', '/new', 'permanent'); + + $this->assertRequest('post', 'servers/1/sites/2/redirects', [ + 'redirect_from' => '/old', + 'redirect_to' => '/new', + 'type' => 'permanent', + ]); + } + + public function testDeletesARedirect(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->redirects(3)->delete(); + + $this->assertRequest('delete', 'servers/1/sites/2/redirects/3'); + } + + public function testDeleteRequiresAnId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers(1)->sites(2)->redirects()->delete(); + } +} diff --git a/tests/Unit/Resources/RepositoryTest.php b/tests/Unit/Resources/RepositoryTest.php new file mode 100644 index 0000000..59e7847 --- /dev/null +++ b/tests/Unit/Resources/RepositoryTest.php @@ -0,0 +1,50 @@ +queue(); + + $this->ploi->servers(1)->sites(2)->repository()->get(); + + $this->assertRequest('get', 'servers/1/sites/2/repository'); + } + + public function testInstallsARepository(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->repository()->install('github', 'main', 'ploi/ploi-php-sdk'); + + $this->assertRequest('post', 'servers/1/sites/2/repository', [ + 'provider' => 'github', + 'branch' => 'main', + 'name' => 'ploi/ploi-php-sdk', + ]); + } + + public function testDeletesTheRepository(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->repository()->delete(); + + $this->assertRequest('delete', 'servers/1/sites/2/repository'); + } + + public function testTogglesQuickDeploy(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->repository()->toggleQuickDeploy(); + + $this->assertRequest('post', 'servers/1/sites/2/repository/quick-deploy'); + } +} diff --git a/tests/Unit/Resources/ResourceTest.php b/tests/Unit/Resources/ResourceTest.php new file mode 100644 index 0000000..81ba077 --- /dev/null +++ b/tests/Unit/Resources/ResourceTest.php @@ -0,0 +1,95 @@ +assertSame(3, $this->ploi->servers(3)->getId()); + $this->assertNull($this->ploi->servers()->getId()); + } + + public function testSetIdAcceptsNullToClearIt(): void + { + $server = $this->ploi->servers(3); + + $this->assertNull($server->setId()->getId()); + } + + public function testSetIdOrFailUsesTheGivenId(): void + { + $server = $this->ploi->servers(); + + $this->assertSame(5, $server->setIdOrFail(5)->getId()); + } + + public function testSetIdOrFailKeepsAnAlreadySetId(): void + { + $server = $this->ploi->servers(5); + + $this->assertSame(5, $server->setIdOrFail()->getId()); + } + + public function testSetIdOrFailThrowsWithoutAnId(): void + { + $this->expectException(RequiresId::class); + $this->expectExceptionMessage(Server::class); + + $this->ploi->servers()->setIdOrFail(); + } + + public function testExposesThePloiInstance(): void + { + $this->assertInstanceOf(Ploi::class, $this->ploi->servers()->getPloi()); + } + + public function testChildResourcesKeepAReferenceToTheirParents(): void + { + $site = $this->ploi->servers(1)->sites(2); + + $this->assertInstanceOf(Server::class, $site->getServer()); + $this->assertSame(1, $site->getServer()->getId()); + + $certificate = $site->certificates(3); + + $this->assertInstanceOf(Site::class, $certificate->getSite()); + $this->assertSame(2, $certificate->getSite()->getId()); + } + + public function testDatabaseChildrenKeepAReferenceToTheDatabase(): void + { + $user = $this->ploi->servers(1)->databases(2)->users(3); + + $this->assertInstanceOf(Database::class, $user->getDatabase()); + $this->assertSame(2, $user->getDatabase()->getId()); + } + + public function testEndpointCanBeReadAndWritten(): void + { + $server = $this->ploi->servers(); + + $this->assertSame('servers', $server->getEndpoint()); + $this->assertSame('something-else', $server->setEndpoint('something-else')->getEndpoint()); + } + + public function testActionCanBeReadAndWritten(): void + { + $server = $this->ploi->servers(); + + $this->assertNull($server->getAction()); + $this->assertSame('forget', $server->setAction('forget')->getAction()); + } +} diff --git a/tests/Unit/Resources/RobotTest.php b/tests/Unit/Resources/RobotTest.php new file mode 100644 index 0000000..75922f7 --- /dev/null +++ b/tests/Unit/Resources/RobotTest.php @@ -0,0 +1,28 @@ +queue(); + + $this->ploi->servers(1)->sites(2)->robots()->allow(); + + $this->assertRequest('patch', 'servers/1/sites/2', ['disable_robots' => false]); + } + + public function testBlocksRobots(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->robots()->block(); + + $this->assertRequest('patch', 'servers/1/sites/2', ['disable_robots' => true]); + } +} diff --git a/tests/Unit/Resources/ScriptTest.php b/tests/Unit/Resources/ScriptTest.php new file mode 100644 index 0000000..dc4a782 --- /dev/null +++ b/tests/Unit/Resources/ScriptTest.php @@ -0,0 +1,85 @@ +queue(); + + $this->ploi->scripts()->get(); + + $this->assertRequest('get', 'scripts?page=1'); + } + + public function testGetsASingleScript(): void + { + $this->queue(); + + $this->ploi->scripts(1)->get(); + + $this->assertRequest('get', 'scripts/1'); + } + + public function testCreatesAScript(): void + { + $this->queue(['data' => ['id' => 4]]); + + $script = $this->ploi->scripts(); + $script->create('Restart nginx', 'root', 'service nginx restart'); + + $this->assertRequest('post', 'scripts', [ + 'label' => 'Restart nginx', + 'user' => 'root', + 'content' => 'service nginx restart', + ]); + + $this->assertSame(4, $script->getId()); + } + + public function testDeletesAScript(): void + { + $this->queue(); + + $this->ploi->scripts(1)->delete(); + + $this->assertRequest('delete', 'scripts/1'); + } + + public function testRunsAScriptOnServers(): void + { + $this->queue(); + + $this->ploi->scripts(1)->run(null, [7, 8]); + + $this->assertRequest('post', 'scripts/1/run', ['servers' => [7, 8]]); + } + + public function testRunRequiresAScriptId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->scripts()->run(null, [7]); + } + + public function testRunRequiresServerIds(): void + { + $this->expectException(RequiresId::class); + $this->expectExceptionMessage('Server IDs are required'); + + $this->ploi->scripts(1)->run(); + } + + public function testDeleteRequiresAnId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->scripts()->delete(); + } +} diff --git a/tests/Unit/Resources/ServerTest.php b/tests/Unit/Resources/ServerTest.php new file mode 100644 index 0000000..5b7622a --- /dev/null +++ b/tests/Unit/Resources/ServerTest.php @@ -0,0 +1,309 @@ +queue(); + + $this->ploi->servers()->get(); + + $this->assertRequest('get', 'servers?page=1'); + } + + public function testPaginatesServers(): void + { + $this->queue(); + + $this->ploi->servers()->perPage(5)->page(2); + + $this->assertRequest('get', 'servers?page=2&per_page=5'); + } + + public function testGetsASingleServer(): void + { + $this->queue(); + + $this->ploi->servers(1)->get(); + + $this->assertRequest('get', 'servers/1'); + } + + public function testGetsASingleServerByArgument(): void + { + $this->queue(); + + $this->ploi->servers()->get(1); + + $this->assertRequest('get', 'servers/1'); + } + + public function testBuildsUrlCorrectly(): void + { + $server = $this->ploi->server(); + + $this->assertSame('servers', $server->buildEndpoint()); + $this->assertSame('servers/custom', $server->buildEndpoint('custom')); + + $server->setId(1); + + $this->assertSame('servers/1', $server->buildEndpoint()); + $this->assertSame('servers/1/endpoint', $server->buildEndpoint('endpoint')); + $this->assertSame('servers/1/endpoint', $server->buildEndpoint('/endpoint')); + + $server->setId(); + + $this->assertSame('servers/different-endpoint', $server->buildEndpoint('/different-endpoint')); + } + + public function testCreatesAServer(): void + { + $this->queue(['data' => ['id' => 7]]); + + $server = $this->ploi->servers(); + $server->create('web-01', 3, 'ams3', 's-1vcpu-1gb'); + + $this->assertRequest('post', 'servers', [ + 'name' => 'web-01', + 'plan' => 's-1vcpu-1gb', + 'region' => 'ams3', + 'credential' => 3, + 'type' => 'server', + 'database_type' => 'mysql', + 'webserver_type' => 'nginx', + 'php_version' => '7.4', + ]); + + $this->assertSame(7, $server->getId()); + } + + public function testCreateAcceptsOverrides(): void + { + $this->queue(['data' => ['id' => 7]]); + + $this->ploi->servers()->create('web-01', 3, 'ams3', 's-1vcpu-1gb', [ + 'php_version' => '8.3', + 'database_type' => 'postgresql', + ]); + + $body = json_decode((string) $this->request()->getBody(), true); + + $this->assertSame('8.3', $body['php_version']); + $this->assertSame('postgresql', $body['database_type']); + } + + public function testCreatesACustomServer(): void + { + // createCustom() reads the id off the root of the response, not off data + $this->queue(['id' => 9]); + + $server = $this->ploi->servers(); + $server->createCustom('1.2.3.4', ['php_version' => '8.3']); + + $this->assertRequest('post', 'servers/custom', [ + 'ip' => '1.2.3.4', + 'type' => 'server', + 'database_type' => 'mysql', + 'php_version' => '8.3', + ]); + + $this->assertSame(9, $server->getId()); + } + + public function testStartsInstallationById(): void + { + $this->queue(); + + $this->ploi->servers(1)->startInstallation(); + + $this->assertRequest('post', 'servers/custom/1/start'); + } + + public function testStartsInstallationByUrl(): void + { + $this->queue(); + + $this->ploi->servers()->startInstallation('servers/custom/abc/start'); + + $this->assertRequest('post', 'servers/custom/abc/start'); + } + + public function testStartInstallationRequiresAnIdOrUrl(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers()->startInstallation(); + } + + public function testDeletesAServer(): void + { + $this->queue(); + + $this->ploi->servers(1)->delete(); + + $this->assertRequest('delete', 'servers/1'); + } + + public function testGetsLogs(): void + { + $this->queue(); + + $this->ploi->servers(1)->logs(); + + $this->assertRequest('get', 'servers/1/logs'); + } + + public function testGetsMonitoring(): void + { + $this->queue(); + + $this->ploi->servers(1)->monitoring(); + + $this->assertRequest('get', 'servers/1/monitor'); + } + + public function testRestartsAServer(): void + { + $this->queue(); + + $this->ploi->servers(1)->restart(); + + $this->assertRequest('post', 'servers/1/restart'); + } + + public function testGetsPhpVersions(): void + { + $this->queue(); + + $this->ploi->servers(1)->phpVersions(); + + $this->assertRequest('get', 'servers/1/php/versions'); + } + + public function testInstallsAPhpVersion(): void + { + $this->queue(); + + $this->ploi->servers(1)->installPhpVersion('8.3'); + + $this->assertRequest('post', 'servers/1/php/install', ['version' => '8.3']); + } + + public function testSwitchesThePhpCliVersion(): void + { + $this->queue(); + + $this->ploi->servers(1)->switchPhpCliVersion('8.3'); + + $this->assertRequest('post', 'servers/1/php/cli-version', ['version' => '8.3']); + } + + public function testRefreshesOpcacheThroughTheDeprecatedMethod(): void + { + $this->queue(); + + $this->ploi->servers(1)->refreshOpcache(); + + $this->assertRequest('post', 'servers/1/refresh-opcache'); + } + + public function testEnablesOpcacheThroughTheDeprecatedMethod(): void + { + $this->queue(); + + $this->ploi->servers(1)->enableOpcache(); + + $this->assertRequest('post', 'servers/1/enable-opcache'); + } + + public function testDisablesOpcacheThroughTheDeprecatedMethod(): void + { + $this->queue(); + + $this->ploi->servers(1)->disableOpcache(); + + $this->assertRequest('delete', 'servers/1/disable-opcache'); + } + + public function testRunsAOneOffScript(): void + { + $this->queue(); + + $this->ploi->servers(1)->runOneOffScript('npm install -g pm2'); + + $this->assertRequest('post', 'servers/1/scripts/run', ['content' => 'npm install -g pm2']); + } + + public function testRunsAOneOffScriptAsAUser(): void + { + $this->queue(); + + $this->ploi->servers(1)->runOneOffScript('whoami', 'deployer'); + + $this->assertRequest('post', 'servers/1/scripts/run', [ + 'content' => 'whoami', + 'user' => 'deployer', + ]); + } + + public function testGetsAScriptExecution(): void + { + $this->queue(); + + $this->ploi->servers(1)->scriptExecution('3f4c9e9a-8b4e-4f0e-9d3b-2f6f2c1a7d42'); + + $this->assertRequest('get', 'servers/1/scripts/run/3f4c9e9a-8b4e-4f0e-9d3b-2f6f2c1a7d42'); + } + + public function testSearchesServers(): void + { + $this->queue(); + + $this->ploi->servers()->search('web'); + + $this->assertRequest('get', 'servers?search=web'); + } + + /** + * @return array}> + */ + public function requiresIdProvider(): array + { + return [ + 'delete' => ['delete', []], + 'logs' => ['logs', []], + 'monitoring' => ['monitoring', []], + 'restart' => ['restart', []], + 'phpVersions' => ['phpVersions', []], + 'installPhpVersion' => ['installPhpVersion', ['8.3']], + 'runOneOffScript' => ['runOneOffScript', ['whoami']], + 'scriptExecution' => ['scriptExecution', ['uuid']], + ]; + } + + /** + * @dataProvider requiresIdProvider + * + * @param array $arguments + */ + public function testMethodsThatNeedAServerIdThrowWithoutOne(string $method, array $arguments): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers()->{$method}(...$arguments); + } + + public function testServersIsAnAliasForServer(): void + { + $this->assertInstanceOf(Server::class, $this->ploi->server()); + $this->assertInstanceOf(Server::class, $this->ploi->servers()); + } +} diff --git a/tests/Unit/Resources/ServiceTest.php b/tests/Unit/Resources/ServiceTest.php new file mode 100644 index 0000000..eac5cf4 --- /dev/null +++ b/tests/Unit/Resources/ServiceTest.php @@ -0,0 +1,44 @@ +queue(); + + $this->ploi->servers(1)->services('nginx')->restart(); + + $this->assertRequest('post', 'servers/1/services/nginx/restart'); + } + + public function testRestartsAServiceGivenAtCallTime(): void + { + $this->queue(); + + $this->ploi->servers(1)->services()->restart('mysql'); + + $this->assertRequest('post', 'servers/1/services/mysql/restart'); + } + + public function testRestartRequiresAServiceName(): void + { + $this->expectException(RequiresServiceName::class); + + $this->ploi->servers(1)->services()->restart(); + } + + public function testRecordsTheServiceNameInTheHistory(): void + { + $service = $this->ploi->servers(1)->services('nginx'); + + $this->assertContains('Resource service name set to nginx', $service->getHistory()); + $this->assertSame('nginx', $service->getServiceName()); + } +} diff --git a/tests/Unit/Resources/SiteTest.php b/tests/Unit/Resources/SiteTest.php new file mode 100644 index 0000000..534a15f --- /dev/null +++ b/tests/Unit/Resources/SiteTest.php @@ -0,0 +1,299 @@ +queue(); + + $this->ploi->servers(1)->sites()->get(); + + $this->assertRequest('get', 'servers/1/sites?page=1'); + } + + public function testPaginatesSites(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites()->perPage(10)->page(3); + + $this->assertRequest('get', 'servers/1/sites?page=3&per_page=10'); + } + + public function testGetsASingleSite(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->get(); + + $this->assertRequest('get', 'servers/1/sites/2'); + } + + public function testCreatesASite(): void + { + $this->queue(['data' => ['id' => 4]]); + + $site = $this->ploi->servers(1)->sites(); + $site->create('example.com'); + + $this->assertRequest('post', 'servers/1/sites', [ + 'root_domain' => 'example.com', + 'web_directory' => '/public', + 'project_root' => null, + 'system_user' => null, + 'webserver_template' => null, + 'project_type' => null, + 'webhook_url' => null, + ]); + + $this->assertSame(4, $site->getId()); + } + + public function testCreatesASiteWithEveryOption(): void + { + $this->queue(['data' => ['id' => 4]]); + + $this->ploi->servers(1)->sites()->create( + 'example.com', + '/dist', + '/app', + 'deployer', + 8, + 'laravel', + 'https://hooks.example.com/ploi' + ); + + $this->assertRequest('post', 'servers/1/sites', [ + 'root_domain' => 'example.com', + 'web_directory' => '/dist', + 'project_root' => '/app', + 'system_user' => 'deployer', + 'webserver_template' => 8, + 'project_type' => 'laravel', + 'webhook_url' => 'https://hooks.example.com/ploi', + ]); + } + + public function testUpdatesASite(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->update('new.example.com'); + + $this->assertRequest('patch', 'servers/1/sites/2', ['root_domain' => 'new.example.com']); + } + + public function testDeletesASite(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->delete(); + + $this->assertRequest('delete', 'servers/1/sites/2'); + } + + public function testGetsSiteLogs(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->logs(); + + $this->assertRequest('get', 'servers/1/sites/2/log'); + } + + public function testSetsThePhpVersion(): void + { + $this->queue(['data' => ['id' => 2]]); + + $this->ploi->servers(1)->sites(2)->phpVersion('8.3'); + + $this->assertRequest('post', 'servers/1/sites/2/php-version', ['php_version' => '8.3']); + } + + public function testTestsTheDomain(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->testDomain(); + + $this->assertRequest('get', 'servers/1/sites/2/test-domain'); + } + + public function testEnablesTheTestDomain(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->enableTestDomain(); + + $this->assertRequest('post', 'servers/1/sites/2/test-domain'); + } + + public function testDisablesTheTestDomain(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->disableTestDomain(); + + $this->assertRequest('delete', 'servers/1/sites/2/test-domain'); + } + + public function testSuspendsASite(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->suspend(); + + $this->assertRequest('post', 'servers/1/sites/2/suspend'); + $this->assertNoBody(); + } + + public function testSuspendsASiteWithAReason(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->suspend(null, 'unpaid'); + + $this->assertRequest('post', 'servers/1/sites/2/suspend', ['reason' => 'unpaid']); + } + + public function testResumesASite(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->resume(); + + $this->assertRequest('post', 'servers/1/sites/2/resume'); + } + + public function testGetsHorizonStatistics(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->horizonStatistics(); + + $this->assertRequest('get', 'servers/1/sites/2/laravel/horizon/stats'); + } + + public function testGetsHorizonStatisticsForAType(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->horizonStatistics('workload'); + + $this->assertRequest('get', 'servers/1/sites/2/laravel/horizon/workload'); + } + + public function testClonesASite(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->clone(5, 'clone.example.com'); + + $this->assertRequest('post', 'servers/1/sites/2/clone', [ + 'clone_to_server' => 5, + 'domain' => 'clone.example.com', + ]); + } + + public function testResetsPermissions(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->resetPermissions(); + + $this->assertRequest('post', 'servers/1/sites/2/permission-reset'); + } + + public function testSearchesSites(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites()->search('example'); + + $this->assertRequest('get', 'servers/1/sites?search=example'); + } + + /** + * @return array}> + */ + public function requiresIdProvider(): array + { + return [ + 'update' => ['update', ['example.com']], + 'logs' => ['logs', []], + 'testDomain' => ['testDomain', []], + 'enableTestDomain' => ['enableTestDomain', []], + 'disableTestDomain' => ['disableTestDomain', []], + 'suspend' => ['suspend', []], + 'resume' => ['resume', []], + 'clone' => ['clone', [5]], + 'resetPermissions' => ['resetPermissions', []], + ]; + } + + /** + * @dataProvider requiresIdProvider + * + * @param array $arguments + */ + public function testMethodsThatNeedASiteIdThrowWithoutOne(string $method, array $arguments): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers(1)->sites()->{$method}(...$arguments); + } + + /** + * @return array + */ + public function childResourceProvider(): array + { + return [ + 'redirects' => ['redirects', Redirect::class], + 'certificates' => ['certificates', Certificate::class], + 'repository' => ['repository', Repository::class], + 'queues' => ['queues', Queue::class], + 'deployment' => ['deployment', Deployment::class], + 'app' => ['app', App::class], + 'environment' => ['environment', Environment::class], + 'alias' => ['alias', Alias::class], + 'fastCgi' => ['fastCgi', FastCgi::class], + 'authUser' => ['authUser', AuthUser::class], + 'robots' => ['robots', Robot::class], + 'tenants' => ['tenants', Tenant::class], + 'monitors' => ['monitors', Monitors::class], + 'nginxConfiguration' => ['nginxConfiguration', NginxConfiguration::class], + ]; + } + + /** + * @dataProvider childResourceProvider + * + * @param class-string $expected + */ + public function testExposesItsChildResources(string $method, string $expected): void + { + $this->assertInstanceOf($expected, $this->ploi->servers(1)->sites(2)->{$method}()); + } +} diff --git a/tests/Unit/Resources/SshKeyTest.php b/tests/Unit/Resources/SshKeyTest.php new file mode 100644 index 0000000..332dddb --- /dev/null +++ b/tests/Unit/Resources/SshKeyTest.php @@ -0,0 +1,74 @@ +queue(); + + $this->ploi->servers(1)->sshKeys()->get(); + + $this->assertRequest('get', 'servers/1/ssh-keys?page=1'); + } + + public function testGetsASingleSshKey(): void + { + $this->queue(); + + $this->ploi->servers(1)->sshKeys(2)->get(); + + $this->assertRequest('get', 'servers/1/ssh-keys/2'); + } + + public function testCreatesAnSshKey(): void + { + $this->queue(['data' => ['id' => 2]]); + + $key = $this->ploi->servers(1)->sshKeys(); + $key->create('laptop', 'ssh-ed25519 AAAA'); + + $this->assertRequest('post', 'servers/1/ssh-keys', [ + 'name' => 'laptop', + 'key' => 'ssh-ed25519 AAAA', + 'system_user' => null, + ]); + + $this->assertSame(2, $key->getId()); + } + + public function testCreatesAnSshKeyForASystemUser(): void + { + $this->queue(['data' => ['id' => 2]]); + + $this->ploi->servers(1)->sshKeys()->create('laptop', 'ssh-ed25519 AAAA', 'deployer'); + + $this->assertRequest('post', 'servers/1/ssh-keys', [ + 'name' => 'laptop', + 'key' => 'ssh-ed25519 AAAA', + 'system_user' => 'deployer', + ]); + } + + public function testDeletesAnSshKey(): void + { + $this->queue(); + + $this->ploi->servers(1)->sshKeys(2)->delete(); + + $this->assertRequest('delete', 'servers/1/ssh-keys/2'); + } + + public function testDeleteRequiresAnId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers(1)->sshKeys()->delete(); + } +} diff --git a/tests/Unit/Resources/StatusPageTest.php b/tests/Unit/Resources/StatusPageTest.php new file mode 100644 index 0000000..56bef5e --- /dev/null +++ b/tests/Unit/Resources/StatusPageTest.php @@ -0,0 +1,34 @@ +queue(); + + $this->ploi->statusPage()->get(); + + $this->assertRequest('get', 'status-pages?page=1'); + } + + public function testGetsASingleStatusPage(): void + { + $this->queue(); + + $this->ploi->statusPage(1)->get(); + + $this->assertRequest('get', 'status-pages/1'); + } + + public function testExposesIncidents(): void + { + $this->assertInstanceOf(Incident::class, $this->ploi->statusPage(1)->incident()); + } +} diff --git a/tests/Unit/Resources/SynchronizeTest.php b/tests/Unit/Resources/SynchronizeTest.php new file mode 100644 index 0000000..bca1191 --- /dev/null +++ b/tests/Unit/Resources/SynchronizeTest.php @@ -0,0 +1,20 @@ +queue(); + + (new Synchronize($this->ploi))->servers(); + + $this->assertRequest('get', 'synchronize/servers'); + } +} diff --git a/tests/Unit/Resources/SystemUserTest.php b/tests/Unit/Resources/SystemUserTest.php new file mode 100644 index 0000000..cfe11d0 --- /dev/null +++ b/tests/Unit/Resources/SystemUserTest.php @@ -0,0 +1,72 @@ +queue(); + + $this->ploi->servers(1)->systemUsers()->get(); + + $this->assertRequest('get', 'servers/1/system-users?page=1'); + } + + public function testGetsASingleSystemUser(): void + { + $this->queue(); + + $this->ploi->servers(1)->systemUsers(2)->get(); + + $this->assertRequest('get', 'servers/1/system-users/2'); + } + + public function testCreatesASystemUser(): void + { + $this->queue(['data' => ['id' => 2]]); + + $user = $this->ploi->servers(1)->systemUsers(); + $user->create('deployer'); + + $this->assertRequest('post', 'servers/1/system-users', [ + 'name' => 'deployer', + 'sudo' => false, + ]); + + $this->assertSame(2, $user->getId()); + } + + public function testCreatesASudoSystemUser(): void + { + $this->queue(['data' => ['id' => 2]]); + + $this->ploi->servers(1)->systemUsers()->create('deployer', true); + + $this->assertRequest('post', 'servers/1/system-users', [ + 'name' => 'deployer', + 'sudo' => true, + ]); + } + + public function testDeletesASystemUser(): void + { + $this->queue(); + + $this->ploi->servers(1)->systemUsers(2)->delete(); + + $this->assertRequest('delete', 'servers/1/system-users/2'); + } + + public function testDeleteRequiresAnId(): void + { + $this->expectException(RequiresId::class); + + $this->ploi->servers(1)->systemUsers()->delete(); + } +} diff --git a/tests/Unit/Resources/TenantTest.php b/tests/Unit/Resources/TenantTest.php new file mode 100644 index 0000000..4ea4219 --- /dev/null +++ b/tests/Unit/Resources/TenantTest.php @@ -0,0 +1,69 @@ +queue(); + + $this->ploi->servers(1)->sites(2)->tenants()->get(); + + $this->assertRequest('get', 'servers/1/sites/2/tenants'); + } + + public function testCreatesTenants(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->tenants()->create(['one.example.com', 'two.example.com']); + + $this->assertRequest('post', 'servers/1/sites/2/tenants', [ + 'tenants' => ['one.example.com', 'two.example.com'], + ]); + } + + public function testDeletesATenant(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->tenants()->delete('one.example.com'); + + $this->assertRequest('delete', 'servers/1/sites/2/tenants/one.example.com'); + } + + public function testRequestsACertificateForATenant(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->tenants()->requestCertificate( + 'one.example.com', + 'https://hooks.example.com/ploi', + 'one.example.com,www.one.example.com' + ); + + $this->assertRequest('post', 'servers/1/sites/2/tenants/one.example.com/request-certificate', [ + 'webhook' => 'https://hooks.example.com/ploi', + 'domains' => 'one.example.com,www.one.example.com', + ]); + } + + public function testRevokesACertificateForATenant(): void + { + $this->queue(); + + $this->ploi->servers(1)->sites(2)->tenants()->revokeCertificate( + 'one.example.com', + 'https://hooks.example.com/ploi' + ); + + $this->assertRequest('post', 'servers/1/sites/2/tenants/one.example.com/revoke-certificate', [ + 'webhook' => 'https://hooks.example.com/ploi', + ]); + } +} diff --git a/tests/Unit/Resources/UserTest.php b/tests/Unit/Resources/UserTest.php new file mode 100644 index 0000000..8b1d5b7 --- /dev/null +++ b/tests/Unit/Resources/UserTest.php @@ -0,0 +1,46 @@ +queue(); + + $this->ploi->user()->get(); + + $this->assertRequest('get', 'user'); + } + + public function testGetsUserStatistics(): void + { + $this->queue(); + + $this->ploi->user()->statistics(); + + $this->assertRequest('get', 'user/statistics'); + } + + public function testListsServerProviders(): void + { + $this->queue(); + + $this->ploi->user()->serverProviders(); + + $this->assertRequest('get', 'user/server-providers'); + } + + public function testGetsASingleServerProvider(): void + { + $this->queue(); + + $this->ploi->user()->serverProviders(3); + + $this->assertRequest('get', 'user/server-providers/3'); + } +} diff --git a/tests/Unit/Resources/WebserverTemplateTest.php b/tests/Unit/Resources/WebserverTemplateTest.php new file mode 100644 index 0000000..b33cbba --- /dev/null +++ b/tests/Unit/Resources/WebserverTemplateTest.php @@ -0,0 +1,28 @@ +queue(); + + $this->ploi->webserverTemplates()->get(); + + $this->assertRequest('get', 'webserver-templates?page=1'); + } + + public function testGetsASingleWebserverTemplate(): void + { + $this->queue(); + + $this->ploi->webserverTemplates(1)->get(); + + $this->assertRequest('get', 'webserver-templates/1'); + } +} diff --git a/tests/Unit/TestCase.php b/tests/Unit/TestCase.php new file mode 100644 index 0000000..68c3ddf --- /dev/null +++ b/tests/Unit/TestCase.php @@ -0,0 +1,126 @@ +> + */ + private $transactions = []; + + protected function setUp(): void + { + parent::setUp(); + + $this->transactions = []; + $this->mockHandler = new MockHandler(); + + $stack = HandlerStack::create($this->mockHandler); + $stack->push(Middleware::history($this->transactions)); + + $this->ploi = (new Ploi(self::TOKEN))->setHandler($stack); + } + + /** + * Queues a JSON response. The default body is enough for the create() + * methods, which read the new id straight off the response. + * + * @param array $data + */ + protected function queue(array $data = ['data' => ['id' => 1]], int $status = 200): self + { + return $this->queueRaw((string) json_encode($data), $status); + } + + protected function queueRaw(string $body, int $status = 200): self + { + $this->mockHandler->append( + new Response($status, ['Content-Type' => 'application/json'], $body) + ); + + return $this; + } + + /** + * Queues the same default response $count times, for chains that make + * more than one call. + */ + protected function queueMany(int $count): self + { + for ($i = 0; $i < $count; $i++) { + $this->queue(); + } + + return $this; + } + + protected function request(int $index = 0): RequestInterface + { + $this->assertArrayHasKey($index, $this->transactions, "No request was made at index {$index}"); + + /** @var RequestInterface $request */ + $request = $this->transactions[$index]['request']; + + return $request; + } + + /** + * Asserts the verb, the full URI and optionally the decoded JSON body of a request. + * + * @param array|null $body + */ + protected function assertRequest(string $method, string $path, ?array $body = null, int $index = 0): void + { + $request = $this->request($index); + + $this->assertSame(strtoupper($method), $request->getMethod()); + $this->assertSame(self::BASE_URI . ltrim($path, '/'), (string) $request->getUri()); + + if ($body !== null) { + $this->assertSame($body, json_decode((string) $request->getBody(), true)); + } + } + + protected function assertRequestCount(int $count): void + { + $this->assertCount($count, $this->transactions); + } + + protected function assertNoBody(int $index = 0): void + { + $this->assertSame('', (string) $this->request($index)->getBody()); + } +} diff --git a/tests/Unit/Traits/HistoryTest.php b/tests/Unit/Traits/HistoryTest.php new file mode 100644 index 0000000..0f183bc --- /dev/null +++ b/tests/Unit/Traits/HistoryTest.php @@ -0,0 +1,55 @@ +resource = $this->ploi->server(); + } + + public function testGetHistory(): void + { + $this->assertIsArray($this->resource->getHistory()); + } + + public function testAddHistory(): void + { + $newHistory = 'Adding to the history'; + + $this->resource->addHistory($newHistory); + + $this->assertContains($newHistory, $this->resource->getHistory()); + } + + public function testSetHistory(): void + { + $newHistory = ['New History']; + + $this->resource->setHistory($newHistory); + + $this->assertCount(1, $this->resource->getHistory()); + $this->assertSame($newHistory, $this->resource->getHistory()); + } + + public function testSettingAnIdIsRecorded(): void + { + $this->resource->setHistory([]); + $this->resource->setId(9); + + $this->assertContains('Resource ID set to 9', $this->resource->getHistory()); + } +} From f0027c472dc617a7ded1be8b8cf160362f1473b4 Mon Sep 17 00:00:00 2001 From: Dennis Date: Mon, 24 Aug 2026 18:59:08 +0200 Subject: [PATCH 2/2] Run PHPStan over the tests too, and fix what it found Adds tests/ to the PHPStan paths so the test suite is held to the same level 5 as src. The nine errors that surfaced: Resource::setId() and setIdOrFail() were declared ": self", which resolves to Resource rather than the called class, so chaining off them lost the concrete type. A "@return static" docblock fixes it at the source, which also helps anyone running static analysis against the SDK. Two of the findings were real bugs in the live tests: SiteTest::testCreateExampleDotCom() dropped the result of its own recursive call and then fell off the end of a method declared to return stdClass, so the @depends chain got null after cleaning up a leftover site. Its $foundSite flag was also dead - never set true, so the break was unreachable. SshKeyTest::testDeleteSshKey() wrapped its only assertion in "if (!empty($sshKey))" on a parameter typed stdClass, which can never be empty. Harmless, but it hid that the guard did nothing. The rest were assertions that could not fail: assertIsArray() on Response::toArray() and on getHistory(), both of which are declared to return arrays. Replaced with assertions that check something. --- phpstan.neon | 1 + src/Ploi/Resources/Resource.php | 6 ++++++ tests/Integration/Resources/ServerTest.php | 6 +++--- tests/Integration/Resources/SiteTest.php | 10 ++++------ tests/Integration/Resources/SshKeyTest.php | 7 +++---- tests/Unit/Traits/HistoryTest.php | 3 ++- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 077ee5a..b35f3d4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,3 +2,4 @@ parameters: level: 5 paths: - src + - tests diff --git a/src/Ploi/Resources/Resource.php b/src/Ploi/Resources/Resource.php index 7700890..e342164 100644 --- a/src/Ploi/Resources/Resource.php +++ b/src/Ploi/Resources/Resource.php @@ -31,6 +31,9 @@ public function __construct(?Ploi $ploi = null, ?int $id = null) } } + /** + * @return static + */ public function setId(?int $id = null): self { $this->id = $id; @@ -40,6 +43,9 @@ public function setId(?int $id = null): self return $this; } + /** + * @return static + */ public function setIdOrFail(?int $id = null): self { if ($id) { diff --git a/tests/Integration/Resources/ServerTest.php b/tests/Integration/Resources/ServerTest.php index 36f47b1..f7c025a 100644 --- a/tests/Integration/Resources/ServerTest.php +++ b/tests/Integration/Resources/ServerTest.php @@ -59,7 +59,7 @@ public function testGetAllServers() $this->assertInstanceOf(\stdClass::class, $servers->getJson()); // Test the array response - $this->assertIsArray($servers->toArray()); + $this->assertSame(['json', 'response'], array_keys($servers->toArray())); // Test to make sure that the data is an array $this->assertIsArray($servers->getJson()->data); @@ -84,8 +84,8 @@ public function testGetPaginatedServers() $this->assertInstanceOf(\stdClass::class, $serversPage2->getJson()); // Test the array response - $this->assertIsArray($serversPage1->toArray()); - $this->assertIsArray($serversPage2->toArray()); + $this->assertSame(['json', 'response'], array_keys($serversPage1->toArray())); + $this->assertSame(['json', 'response'], array_keys($serversPage2->toArray())); // Test responses contain paginated result $this->assertEquals(1, $serversPage1->getJson()->meta->current_page); diff --git a/tests/Integration/Resources/SiteTest.php b/tests/Integration/Resources/SiteTest.php index 5b4f7d6..a269829 100644 --- a/tests/Integration/Resources/SiteTest.php +++ b/tests/Integration/Resources/SiteTest.php @@ -102,18 +102,16 @@ public function testCreateExampleDotCom(): stdClass $this->assertInstanceOf(NotValid::class, $e); $allSites = $this->server->sites()->get(); - $foundSite = false; - foreach ($allSites->getJson()->data as $site) { - if ($foundSite) { - break; - } + foreach ($allSites->getJson()->data as $site) { if ($site->domain === 'example.com') { $this->server->sites($site->id)->delete(); - $this->testCreateExampleDotCom(); + return $this->testCreateExampleDotCom(); } } + + $this->fail('Could not create example.com and found no existing site to remove'); } } diff --git a/tests/Integration/Resources/SshKeyTest.php b/tests/Integration/Resources/SshKeyTest.php index 890ef40..943227e 100644 --- a/tests/Integration/Resources/SshKeyTest.php +++ b/tests/Integration/Resources/SshKeyTest.php @@ -98,10 +98,9 @@ public function testCreateSshKey(): stdClass */ public function testDeleteSshKey(stdClass $sshKey) { - if (!empty($sshKey)) { - $deleted = $this->server->sshKeys($sshKey->id)->delete(); - $this->assertTrue($deleted->getResponse()->getStatusCode() === 200); - } + $deleted = $this->server->sshKeys($sshKey->id)->delete(); + + $this->assertTrue($deleted->getResponse()->getStatusCode() === 200); } public function testDeleteInvalidSshKey() diff --git a/tests/Unit/Traits/HistoryTest.php b/tests/Unit/Traits/HistoryTest.php index 0f183bc..14cd126 100644 --- a/tests/Unit/Traits/HistoryTest.php +++ b/tests/Unit/Traits/HistoryTest.php @@ -23,7 +23,8 @@ protected function setUp(): void public function testGetHistory(): void { - $this->assertIsArray($this->resource->getHistory()); + // Constructing a resource already records the Ploi instance being set + $this->assertNotEmpty($this->resource->getHistory()); } public function testAddHistory(): void