Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/php_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 15 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
```
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ parameters:
level: 5
paths:
- src
- tests
11 changes: 9 additions & 2 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,22 @@
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
defaultTestSuite="unit"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage>
<include>
<directory suffix=".php">src/</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Ploi PHP SDK Test Suite">
<directory>tests</directory>
<!-- Mocked, no network. This is what runs by default and in CI. -->
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>

<!-- Hits the live Ploi API, needs tests/.env with a valid API_TOKEN. -->
<testsuite name="integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>
</phpunit>
38 changes: 35 additions & 3 deletions src/Ploi/Ploi.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -71,16 +78,41 @@ 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' => [
'Authorization' => 'Bearer ' . $this->getApiToken(),
'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;
}
Expand Down
6 changes: 6 additions & 0 deletions src/Ploi/Resources/Resource.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,6 +43,9 @@ public function setId(?int $id = null): self
return $this;
}

/**
* @return static
*/
public function setIdOrFail(?int $id = null): self
{
if ($id) {
Expand Down
7 changes: 3 additions & 4 deletions tests/Ploi/PloiTest.php → tests/Integration/PloiTest.php
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
<?php

namespace Tests\Ploi;
namespace Tests\Integration;

use Exception;
use Tests\BaseTest;
use Ploi\Exceptions\Http\NotFound;
use Ploi\Exceptions\Http\NotAllowed;
use Ploi\Exceptions\Http\Unauthenticated;

/**
* Class PloiTest
* @package Tests\Ploi
* @package Tests\Integration
*/
class PloiTest extends BaseTest
class PloiTest extends TestCase
{
public function testCanGetAPiToken()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<?php


namespace Tests\Ploi\Resources;
namespace Tests\Integration\Resources;


use Tests\BaseTest;
use Tests\Integration\TestCase;
use Ploi\Resources\Site;

class AliasTest extends BaseTest
class AliasTest extends TestCase
{

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
<?php

namespace Tests\Ploi\Resources;
namespace Tests\Integration\Resources;

use Tests\BaseTest;
use Tests\Integration\TestCase;
use Ploi\Http\Response;
use Ploi\Resources\Server;
use Ploi\Exceptions\Resource\RequiresId;

/**
* Class ServerTest
*
* @package Tests\Ploi\Resources
* @package Tests\Integration\Resources
*/
class ServerTest extends BaseTest
class ServerTest extends TestCase
{
public function testInstanceOfServer()
{
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<?php

namespace Tests\Ploi\Resources;
namespace Tests\Integration\Resources;

use Ploi\Exceptions\Resource\RequiresId;
use stdClass;
use Tests\BaseTest;
use Tests\Integration\TestCase;
use Ploi\Http\Response;
use Ploi\Resources\Server;
use Ploi\Exceptions\Http\NotFound;
Expand All @@ -13,9 +13,9 @@
/**
* Class SiteTest
*
* @package Tests\Ploi\Resources
* @package Tests\Integration\Resources
*/
class SiteTest extends BaseTest
class SiteTest extends TestCase
{
/**
* @var Server
Expand Down Expand Up @@ -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');
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
<?php

namespace Tests\Ploi\Resources;
namespace Tests\Integration\Resources;

use stdClass;
use Tests\BaseTest;
use Tests\Integration\TestCase;
use Ploi\Http\Response;
use Ploi\Resources\Server;
use Ploi\Exceptions\Http\NotFound;

/**
* Class SshKeyTest
*
* @package Tests\Ploi\Resources
* @package Tests\Integration\Resources
*/
class SshKeyTest extends BaseTest
class SshKeyTest extends TestCase
{
/**
* @var Server
Expand Down Expand Up @@ -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()
Expand Down
Loading