<?php
namespace Telnyx\HttpClient;
/**
* @internal
* @covers \Telnyx\HttpClient\CurlClient
*/
final class CurlClientTest extends \Telnyx\TestCase
{
/** @var \ReflectionProperty */
private $initialNetworkRetryDelayProperty;
/** @var \ReflectionProperty */
private $maxNetworkRetryDelayProperty;
/** @var float */
private $origInitialNetworkRetryDelay;
/** @var int */
private $origMaxNetworkRetries;
/** @var float */
private $origMaxNetworkRetryDelay;
/** @var \ReflectionMethod */
private $sleepTimeMethod;
/** @var \ReflectionMethod */
private $shouldRetryMethod;
/**
* @before
*/
public function saveOriginalNetworkValues()
{
$this->origMaxNetworkRetries = \Telnyx\Telnyx::getMaxNetworkRetries();
$this->origMaxNetworkRetryDelay = \Telnyx\Telnyx::getMaxNetworkRetryDelay();
$this->origInitialNetworkRetryDelay = \Telnyx\Telnyx::getInitialNetworkRetryDelay();
}
/**
* @before
*/
public function setUpReflectors()
{
$telnyxReflector = new \ReflectionClass('\Telnyx\Telnyx');
$this->maxNetworkRetryDelayProperty = $telnyxReflector->getProperty('maxNetworkRetryDelay');
$this->maxNetworkRetryDelayProperty->setAccessible(true);
$this->initialNetworkRetryDelayProperty = $telnyxReflector->getProperty('initialNetworkRetryDelay');
$this->initialNetworkRetryDelayProperty->setAccessible(true);
$curlClientReflector = new \ReflectionClass('\Telnyx\HttpClient\CurlClient');
$this->shouldRetryMethod = $curlClientReflector->getMethod('shouldRetry');
$this->shouldRetryMethod->setAccessible(true);
$this->sleepTimeMethod = $curlClientReflector->getMethod('sleepTime');
$this->sleepTimeMethod->setAccessible(true);
}
/**
* @after
*/
public function restoreOriginalNetworkValues()
{
\Telnyx\Telnyx::setMaxNetworkRetries($this->origMaxNetworkRetries);
$this->setMaxNetworkRetryDelay($this->origMaxNetworkRetryDelay);
$this->setInitialNetworkRetryDelay($this->origInitialNetworkRetryDelay);
}
private function setMaxNetworkRetryDelay($maxNetworkRetryDelay)
{
$this->maxNetworkRetryDelayProperty->setValue(null, $maxNetworkRetryDelay);
}
private function setInitialNetworkRetryDelay($initialNetworkRetryDelay)
{
$this->initialNetworkRetryDelayProperty->setValue(null, $initialNetworkRetryDelay);
}
private function createFakeRandomGenerator($returnValue = 1.0)
{
$fakeRandomGenerator = $this->createMock('\Telnyx\Util\RandomGenerator');
$fakeRandomGenerator->method('randFloat')->willReturn($returnValue);
return $fakeRandomGenerator;
}
public function testTimeout()
{
$curl = new CurlClient();
static::assertSame(CurlClient::DEFAULT_TIMEOUT, $curl->getTimeout());
static::assertSame(CurlClient::DEFAULT_CONNECT_TIMEOUT, $curl->getConnectTimeout());
// implicitly tests whether we're returning the CurlClient instance
$curl = $curl->setConnectTimeout(1)->setTimeout(10);
static::assertSame(1, $curl->getConnectTimeout());
static::assertSame(10, $curl->getTimeout());
$curl->setTimeout(-1);
$curl->setConnectTimeout(-999);
static::assertSame(0, $curl->getTimeout());
static::assertSame(0, $curl->getConnectTimeout());
}
public function testUserAgentInfo()
{
$curl = new CurlClient();
$uaInfo = $curl->getUserAgentInfo();
static::assertNotNull($uaInfo);
static::assertNotNull($uaInfo['httplib']);
static::assertNotNull($uaInfo['ssllib']);
}
public function testDefaultOptions()
{
// make sure options array loads/saves properly
$optionsArray = [\CURLOPT_PROXY => 'localhost:80'];
$withOptionsArray = new CurlClient($optionsArray);
static::assertSame($withOptionsArray->getDefaultOptions(), $optionsArray);
// make sure closure-based options work properly, including argument passing
$ref = null;
$withClosure = new CurlClient(function ($method, $absUrl, $headers, $params, $hasFile) use (&$ref) {
$ref = \func_get_args();
return [];
});
$withClosure->request('get', 'https://httpbin.org/status/200', [], [], false);
static::assertSame($ref, ['get', 'https://httpbin.org/status/200', [], [], false]);
// this is the last test case that will run, since it'll throw an exception at the end
$withBadClosure = new CurlClient(function () {
return 'thisShouldNotWork';
});
$this->expectException('Telnyx\Exception\UnexpectedValueException');
$this->expectExceptionMessage('Non-array value returned by defaultOptions CurlClient callback');
$withBadClosure->request('get', 'https://httpbin.org/status/200', [], [], false);
}
public function testSslOption()
{
// make sure options array loads/saves properly
$optionsArray = [\CURLOPT_SSLVERSION => \CURL_SSLVERSION_TLSv1];
$withOptionsArray = new CurlClient($optionsArray);
static::assertSame($withOptionsArray->getDefaultOptions(), $optionsArray);
}
public function testShouldRetryOnTimeout()
{
\Telnyx\Telnyx::setMaxNetworkRetries(2);
$curlClient = new CurlClient();
static::assertTrue($this->shouldRetryMethod->invoke($curlClient, \CURLE_OPERATION_TIMEOUTED, 0, [], 0));
}
public function testShouldRetryOnConnectionFailure()
{
\Telnyx\Telnyx::setMaxNetworkRetries(2);
$curlClient = new CurlClient();
static::assertTrue($this->shouldRetryMethod->invoke($curlClient, \CURLE_COULDNT_CONNECT, 0, [], 0));
}
public function testShouldRetryOnConflict()
{
\Telnyx\Telnyx::setMaxNetworkRetries(2);
$curlClient = new CurlClient();
static::assertTrue($this->shouldRetryMethod->invoke($curlClient, 0, 409, [], 0));
}
public function testShouldNotRetryOn429()
{
\Telnyx\Telnyx::setMaxNetworkRetries(2);
$curlClient = new CurlClient();
static::assertFalse($this->shouldRetryMethod->invoke($curlClient, 0, 429, [], 0));
}
public function testShouldRetryOn500()
{
\Telnyx\Telnyx::setMaxNetworkRetries(2);
$curlClient = new CurlClient();
static::assertTrue($this->shouldRetryMethod->invoke($curlClient, 0, 500, [], 0));
}
public function testShouldRetryOn503()
{
\Telnyx\Telnyx::setMaxNetworkRetries(2);
$curlClient = new CurlClient();
static::assertTrue($this->shouldRetryMethod->invoke($curlClient, 0, 503, [], 0));
}
public function testShouldRetryOnStripeShouldRetryTrue()
{
\Telnyx\Telnyx::setMaxNetworkRetries(2);
$curlClient = new CurlClient();
static::assertFalse($this->shouldRetryMethod->invoke($curlClient, 0, 400, [], 0));
static::assertTrue($this->shouldRetryMethod->invoke($curlClient, 0, 400, ['telnyx-should-retry' => 'true'], 0));
}
public function testShouldNotRetryOnStripeShouldRetryFalse()
{
\Telnyx\Telnyx::setMaxNetworkRetries(2);
$curlClient = new CurlClient();
static::assertTrue($this->shouldRetryMethod->invoke($curlClient, 0, 500, [], 0));
static::assertFalse($this->shouldRetryMethod->invoke($curlClient, 0, 500, ['telnyx-should-retry' => 'false'], 0));
}
public function testShouldNotRetryAtMaximumCount()
{
\Telnyx\Telnyx::setMaxNetworkRetries(2);
$curlClient = new CurlClient();
static::assertFalse($this->shouldRetryMethod->invoke($curlClient, 0, 0, [], \Telnyx\Telnyx::getMaxNetworkRetries()));
}
public function testShouldNotRetryOnCertValidationError()
{
\Telnyx\Telnyx::setMaxNetworkRetries(2);
$curlClient = new CurlClient();
static::assertFalse($this->shouldRetryMethod->invoke($curlClient, \CURLE_SSL_PEER_CERTIFICATE, -1, [], 0));
}
public function testSleepTimeShouldGrowExponentially()
{
$this->setMaxNetworkRetryDelay(999.0);
$curlClient = new CurlClient(null, $this->createFakeRandomGenerator());
static::assertSame(
\Telnyx\Telnyx::getInitialNetworkRetryDelay() * 1,
$this->sleepTimeMethod->invoke($curlClient, 1, [])
);
static::assertSame(
\Telnyx\Telnyx::getInitialNetworkRetryDelay() * 2,
$this->sleepTimeMethod->invoke($curlClient, 2, [])
);
static::assertSame(
\Telnyx\Telnyx::getInitialNetworkRetryDelay() * 4,
$this->sleepTimeMethod->invoke($curlClient, 3, [])
);
static::assertSame(
\Telnyx\Telnyx::getInitialNetworkRetryDelay() * 8,
$this->sleepTimeMethod->invoke($curlClient, 4, [])
);
}
public function testSleepTimeShouldEnforceMaxNetworkRetryDelay()
{
$this->setInitialNetworkRetryDelay(1.0);
$this->setMaxNetworkRetryDelay(2);
$curlClient = new CurlClient(null, $this->createFakeRandomGenerator());
static::assertSame(1.0, $this->sleepTimeMethod->invoke($curlClient, 1, []));
static::assertSame(2.0, $this->sleepTimeMethod->invoke($curlClient, 2, []));
static::assertSame(2.0, $this->sleepTimeMethod->invoke($curlClient, 3, []));
static::assertSame(2.0, $this->sleepTimeMethod->invoke($curlClient, 4, []));
}
public function testSleepTimeShouldRespectRetryAfter()
{
$this->setInitialNetworkRetryDelay(1.0);
$this->setMaxNetworkRetryDelay(2.0);
$curlClient = new CurlClient(null, $this->createFakeRandomGenerator());
// Uses max of default and header.
static::assertSame(10.0, $this->sleepTimeMethod->invoke($curlClient, 1, ['retry-after' => '10']));
static::assertSame(2.0, $this->sleepTimeMethod->invoke($curlClient, 2, ['retry-after' => '1']));
// Ignores excessively large values.
static::assertSame(2.0, $this->sleepTimeMethod->invoke($curlClient, 2, ['retry-after' => '100']));
}
public function testSleepTimeShouldAddSomeRandomness()
{
$randomValue = 0.8;
$this->setInitialNetworkRetryDelay(1.0);
$this->setMaxNetworkRetryDelay(8.0);
$curlClient = new CurlClient(null, $this->createFakeRandomGenerator($randomValue));
$baseValue = \Telnyx\Telnyx::getInitialNetworkRetryDelay() * (0.5 * (1 + $randomValue));
// the initial value cannot be smaller than the base,
// so the randomness is ignored
static::assertSame(\Telnyx\Telnyx::getInitialNetworkRetryDelay(), $this->sleepTimeMethod->invoke($curlClient, 1, []));
// after the first one, the randomness is applied
static::assertSame($baseValue * 2, $this->sleepTimeMethod->invoke($curlClient, 2, []));
static::assertSame($baseValue * 4, $this->sleepTimeMethod->invoke($curlClient, 3, []));
static::assertSame($baseValue * 8, $this->sleepTimeMethod->invoke($curlClient, 4, []));
}
public function testResponseHeadersCaseInsensitive()
{
$profiles = \Telnyx\MessagingProfile::all();
$headers = $profiles->getLastResponse()->headers;
static::assertNotNull($headers['request-id']);
static::assertSame($headers['request-id'], $headers['Request-Id']);
}
public function testSetRequestStatusCallback()
{
try {
$called = false;
$curl = new CurlClient();
$curl->setRequestStatusCallback(function ($rbody, $rcode, $rheaders, $errno, $message, $willBeRetried, $numRetries) use (&$called) {
$called = true;
$this->assertInternalType('string', $rbody);
$this->assertSame(200, $rcode);
$this->assertSame(0, $errno);
$this->assertNull($message);
$this->assertFalse($willBeRetried);
$this->assertSame(0, $numRetries);
});
\Telnyx\ApiRequestor::setHttpClient($curl);
\Telnyx\MessagingProfile::all();
static::assertTrue($called);
} finally {
\Telnyx\ApiRequestor::setHttpClient(null);
}
}
public function testInvalidMethod() {
$curl = new CurlClient();
$this->expectException('Telnyx\Exception\UnexpectedValueException');
$this->expectExceptionMessage('Unrecognized method invalidmethod');
$curl->request('invalidmethod', 'https://httpbin.org/status/200', [], [], false);
}
public function testGetSets() {
$curl = new CurlClient();
$curl->setEnablePersistentConnections(false);
static::assertSame(false, $curl->getEnablePersistentConnections());
$curl->setEnablePersistentConnections(true);
static::assertSame(true, $curl->getEnablePersistentConnections());
$curl->setEnableHttp2(false);
static::assertSame(false, $curl->getEnableHttp2());
$curl->setEnableHttp2(true);
static::assertSame(true, $curl->getEnableHttp2());
}
}