Files
rmyndharis--openwa/sdk/php/tests/ClientTest.php
T
wehub-resource-sync 4ce4204b6c
CI / Lint (push) Failing after 2s
CI / Build (push) Has been skipped
SDK CI / PHP SDK (push) Failing after 1s
Split PHP SDK / PHP SDK tests (push) Failing after 1s
CI / Test (push) Failing after 1s
CI / Dashboard (push) Failing after 0s
SDK CI / JavaScript SDK (push) Failing after 0s
SDK CI / Python SDK (push) Failing after 2s
SDK CI / Java SDK (push) Failing after 1s
Split PHP SDK / Mirror sdk/php -> rmyndharis/openwa-php (push) Has been skipped
CI / Test (PostgreSQL migrations) (push) Failing after 7m47s
CI / Docker Build (push) Has been skipped
chore: import upstream snapshot with attribution
2026-07-13 12:24:08 +08:00

197 lines
7.7 KiB
PHP

<?php
declare(strict_types=1);
namespace OpenWA\Tests;
use OpenWA\Exceptions\OpenWAApiException;
use OpenWA\Exceptions\OpenWAAuthException;
use OpenWA\Exceptions\OpenWANotFoundException;
use OpenWA\Exceptions\OpenWATimeoutException;
use PHPUnit\Framework\TestCase;
class ClientTest extends TestCase
{
public function testRequiresBaseUrlAndApiKey(): void
{
$this->expectException(\OpenWA\Exceptions\OpenWAException::class);
new \OpenWA\Client(['baseUrl' => '', 'apiKey' => 'k']);
}
public function testRequiresApiKey(): void
{
$this->expectException(\OpenWA\Exceptions\OpenWAException::class);
new \OpenWA\Client(['baseUrl' => 'https://x', 'apiKey' => '']);
}
public function testSendsApiKeyHeader(): void
{
$backend = (new MockBackend())->on(200, []);
$client = $backend->makeClient();
$client->sessions->list();
$call = $backend->lastCall();
$this->assertSame('owa_k1_test', $call['headers']['x-api-key'] ?? '');
$this->assertSame('application/json', $call['headers']['content-type'] ?? '');
}
public function testDefaultHeadersApplledUnderAuth(): void
{
$backend = (new MockBackend())->on(200, []);
$client = new \OpenWA\Client([
'baseUrl' => 'https://x',
'apiKey' => 'REAL_KEY',
'httpClient' => $backend->httpClient(),
'defaultHeaders' => ['X-Trace' => 'keep', 'X-API-Key' => 'EVIL'],
]);
$client->sessions->list();
$headers = $backend->lastCall()['headers'];
$this->assertSame('keep', $headers['x-trace'] ?? ''); // custom header forwarded
$this->assertSame('REAL_KEY', $headers['x-api-key'] ?? ''); // auth still wins
}
public function testPathSegmentsAreEncoded(): void
{
$backend = (new MockBackend())->on(200, ['id' => 'x']);
$backend->makeClient()->labels->get('s', 'weird/id#x');
$this->assertStringContainsString('/labels/weird%2Fid%23x', $backend->lastCall()['path']);
$backend2 = (new MockBackend())->on(200, ['id' => 'x']);
$backend2->makeClient()->labels->get('s', 'a@c.us');
$this->assertStringContainsString('/labels/a@c.us', $backend2->lastCall()['path']); // @ preserved
}
public function testRawRequestEscapeHatch(): void
{
$backend = (new MockBackend())->on(200, ['ok' => true]);
$result = $backend->makeClient()->request('GET', '/api/anything', ['a' => 1]);
$this->assertSame(['ok' => true], $result);
$this->assertSame('/api/anything', $backend->lastCall()['path']);
$this->assertStringContainsString('a=1', $backend->lastCall()['query']);
}
public function testBaseUrlPathPrefixIsPreserved(): void
{
// A base URL with a path prefix (e.g. behind a reverse proxy at /v1) must
// be kept; absolute request paths must not drop it.
$backend = (new MockBackend())->on(200, []);
$backend->makeClient('http://localhost:2785/v1')->sessions->list();
$this->assertStringContainsString('/v1/api/sessions', $backend->lastCall()['path']);
}
public function testTreatsUnfollowedRedirectAsError(): void
{
// A redirect must not be followed (which would re-send X-API-Key to the target origin).
// An unfollowed 3xx is not a usable response, so it surfaces as an API error rather than a
// fake success — matching the JS and Python transports. Only one request is made.
$backend = new MockBackend();
$backend->on(302, ['redirected' => true], ['Location' => 'http://evil.example/x']);
$backend->on(200, ['followed' => true]); // only reached if a redirect were followed
$threw = false;
try {
$backend->makeClient()->sessions->list();
} catch (OpenWAApiException $e) {
$threw = true;
}
$this->assertTrue($threw, 'an unfollowed 3xx must surface as an API error');
$this->assertCount(1, $backend->calls()); // the redirect target was never requested
}
public function test204DeleteSucceedsWithNoBody(): void
{
$backend = (new MockBackend())->on(204);
$backend->makeClient()->sessions->delete('x');
$this->assertSame('DELETE', $backend->lastCall()['method']);
}
public function test404MapsToNotFoundException(): void
{
$backend = (new MockBackend())->on(404, [
'statusCode' => 404,
'message' => 'Session not found',
'error' => 'Not Found',
]);
$this->expectException(OpenWANotFoundException::class);
$backend->makeClient()->sessions->get('missing');
}
public function test401MapsToAuthException(): void
{
$backend = (new MockBackend())->on(401, [
'statusCode' => 401,
'message' => 'Unauthorized',
'error' => 'Unauthorized',
]);
$this->expectException(OpenWAAuthException::class);
$backend->makeClient()->sessions->list();
}
public function testExposesAllResources(): void
{
$client = (new MockBackend())->makeClient();
foreach (['sessions', 'messages', 'contacts', 'groups', 'webhooks', 'chats', 'status', 'health'] as $r) {
$this->assertTrue(property_exists($client, $r), "Client should expose resource: {$r}");
}
}
public function testErrorCarriesStatusAndBody(): void
{
$backend = (new MockBackend())->on(404, [
'statusCode' => 404,
'message' => 'Session not found',
'error' => 'Not Found',
]);
try {
$backend->makeClient()->sessions->get('x');
$this->fail('Expected exception');
} catch (OpenWANotFoundException $e) {
$this->assertSame(404, $e->getStatus());
$this->assertSame('Not Found', $e->getErrorKind());
$this->assertIsArray($e->getBody());
}
}
public function testTimeoutWithErrno28MapsToTimeoutException(): void
{
// Regression guard: cURL error 28 (CURLE_OPERATION_TIMEDOUT) must map to
// OpenWATimeoutException, regardless of the message wording.
$timeoutRequest = new \GuzzleHttp\Exception\ConnectException(
'cURL error 28: Operation timed out',
new \GuzzleHttp\Psr7\Request('GET', '/api/sessions'),
null,
['errno' => 28],
);
$mock = new \GuzzleHttp\Handler\MockHandler([$timeoutRequest]);
$httpClient = new \GuzzleHttp\Client(['handler' => \GuzzleHttp\HandlerStack::create($mock)]);
$client = new \OpenWA\Client([
'baseUrl' => 'http://localhost:2785',
'apiKey' => 'k',
'httpClient' => $httpClient,
]);
$this->expectException(OpenWATimeoutException::class);
$client->sessions->list();
}
public function testConnectionRefusedDoesNotMapToTimeoutException(): void
{
// Regression guard: a non-timeout ConnectException (errno 7) must NOT be
// misclassified as a timeout — it must propagate as ConnectException.
$refused = new \GuzzleHttp\Exception\ConnectException(
'cURL error 7: connection refused',
new \GuzzleHttp\Psr7\Request('GET', '/api/sessions'),
null,
['errno' => 7],
);
$mock = new \GuzzleHttp\Handler\MockHandler([$refused]);
$httpClient = new \GuzzleHttp\Client(['handler' => \GuzzleHttp\HandlerStack::create($mock)]);
$client = new \OpenWA\Client([
'baseUrl' => 'http://localhost:2785',
'apiKey' => 'k',
'httpClient' => $httpClient,
]);
$this->expectException(\GuzzleHttp\Exception\ConnectException::class);
$client->sessions->list();
}
}