Skip to content

Commit

Permalink
Merge pull request #461 from recurly/v3-mock-client
Browse files Browse the repository at this point in the history
[V3] Mock Client
  • Loading branch information
douglasmiller authored Feb 21, 2020
2 parents 10b6b61 + 959ccd0 commit eec31fc
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 43 deletions.
59 changes: 16 additions & 43 deletions lib/recurly/base_client.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

abstract class BaseClient
{
use RecurlyTraits;

private $_baseUrl = 'https://v3.recurly.com';
private $_api_key;
protected $http;

/**
* Constructor
Expand All @@ -15,6 +18,7 @@ abstract class BaseClient
public function __construct(string $api_key)
{
$this->_api_key = $api_key;
$this->http = new HttpAdapter;
}

/**
Expand Down Expand Up @@ -56,22 +60,12 @@ private function _getResponse(string $method, string $path, ?array $body = [], ?
{
$request = new \Recurly\Request($method, $path, $body, $params);

$options = array(
'http' => array(
'ignore_errors' => true, // Allows for returning error bodies
'method' => $method,
'header' => $this->_headers(),
'content' => isset($body) && !empty($body) ? json_encode($body) : null
)
);

$context = stream_context_create($options);
$url = $this->_buildPath($path, $params);
$result = file_get_contents($url, false, $context);
list($result, $response_header) = $this->http->execute($method, $url, $body, $this->_headers());

// TODO: The $request should be added to the $response
$response = new \Recurly\Response($result);
$response->setHeaders($http_response_header);
$response->setHeaders($response_header);

return $response;
}
Expand Down Expand Up @@ -114,7 +108,7 @@ public function pagerCount(string $path, ?array $params = []): \Recurly\Response
*/
private function _buildPath(string $path, ?array $params): string
{
if (isset($params)) {
if (isset($params) && !empty($params)) {
return $this->_baseUrl . $path . '?' . http_build_query($params);
} else {
return $this->_baseUrl . $path;
Expand Down Expand Up @@ -142,38 +136,17 @@ protected function interpolatePath(string $path, array $options = []): string
/**
* Generates headers to be sent with the HTTP request
*
* @return string String representation of the HTTP headers
* @return array Array representation of the HTTP headers
*/
private function _headers(): string
private function _headers(): array
{
$php_version = phpversion();
$client_version = \Recurly\Version::CURRENT;
$auth_token = base64_encode("{$this->_api_key}:");
$headers = array(
"User-Agent: Recurly/{$client_version}; php {$php_version}",
"Authorization: Basic {$auth_token}",
"Accept: application/vnd.recurly.{$this->apiVersion()}",
"Content-Type: application/json",
$auth_token = self::encodeApiKey($this->_api_key);
$agent = self::getUserAgent();
return array(
"User-Agent" => $agent,
"Authorization" => "Basic {$auth_token}",
"Accept" => "application/vnd.recurly.{$this->apiVersion()}",
"Content-Type" => "application/json",
);
return join("\r\n", $headers);
}

/**
* Method to override the default Recurly API URL.
* This is primarily for Recurly testing
*
* @param string $url The replacement URL to use
*
* @return void
*/
public function setApiUrl(string $url): void
{
echo "[SECURITY WARNING] setApiUrl is for testing only and not supported in production." . PHP_EOL;
if (getenv("RECURLY_INSECURE") == "true") {
$this->_baseUrl = $url;
} else {
echo "ApiUrl not changed. To change, set the environment variable RECURLY_INSECURE to true" . PHP_EOL;
}
}

}
45 changes: 45 additions & 0 deletions lib/recurly/http_adapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
/**
* This class abstracts away all the PHP-level HTTP
* code. This allows us to easily mock out the HTTP
* calls in BaseClient by injecting a mocked version of
* this adapter.
*/

namespace Recurly;

class HttpAdapter
{
private static $_default_options = [
'ignore_errors' => true
];

/**
* Performs HTTP request
*
* @param string $method HTTP method to use
* @param string $url Fully qualified URL
* @param array $body The request body
* @param array $headers HTTP headers
*
* @return array The API response as a string and the headers as an array
*/
public function execute($method, $url, $body, $headers): array
{
$body = empty($body) ? null : json_encode($body);
$options = array_replace(
self::$_default_options, [
'method' => $method,
'content' => $body,
]
);
$headers_str = "";
foreach ($headers as $k => $v) {
$headers_str .= "$k: $v\r\n";
}
$options['header'] = $headers_str;
$context = stream_context_create(['http' => $options]);
$result = file_get_contents($url, false, $context);
return array($result, $http_response_header);
}
}
22 changes: 22 additions & 0 deletions lib/recurly/recurly_traits.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@

trait RecurlyTraits
{
/**
* Generates User-Agent for API requests
*
* @return string Recurly client User-Agent string
*/
protected static function getUserAgent(): string
{
$php_version = phpversion();
return "Recurly/" . \Recurly\Version::CURRENT . "; php " . $php_version;
}

/**
* Base64 encodes the API key
*
* @param string $key The API key to encode
*
* @return string base64 encoded API key
*/
protected static function encodeApiKey(string $key): string
{
return base64_encode($key);
}

/**
* Capitalizes all the words in the $input.
Expand Down
110 changes: 110 additions & 0 deletions tests/BaseClient_Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

use Recurly\Page;
use Recurly\Resources\TestResource;
use Recurly\BaseClient;
use Recurly\Utils;

final class BaseClientTest extends RecurlyTestCase
{
public function setUp(): void
{
parent::setUp();
$this->client = new MockClient();
}

public function tearDown(): void
{
$this->client->clearScenarios();
}

public function testGetResource200(): void
{
$url = "https://v3.recurly.com/resources/iexist";
$result = '{"id": "iexist", "object": "test_resource"}';
$this->client->addScenario("GET", $url, NULL, $result, "200 OK");

$resource = $this->client->getResource("iexist");
$this->assertEquals($resource->getId(), "iexist");
}

public function testGetResource404(): void
{
$url = "https://v3.recurly.com/resources/idontexist";
$result = "{\"error\":{\"type\":\"not_found\",\"message\":\"Couldn't find Resource with id = idontexist\",\"params\":[{\"param\":\"resource_id\"}]}}";
$this->client->addScenario("GET", $url, NULL, $result, "404 Not Found");

$this->expectException(\Recurly\Errors\NotFound::class);
$this->client->getResource("idontexist");
}

public function testCreateResource201(): void
{
$url = "https://v3.recurly.com/resources/";
$result = '{"id": "created", "object": "test_resource", "name": "valid"}';
$body = [ "name" => "valid" ];
$this->client->addScenario("POST", $url, $body, $result, "201 Created");
$resource = $this->client->createResource([ "name" => "valid" ]);
$this->assertEquals($resource->getId(), "created");
}

public function testCreateResource422(): void
{
$url = "https://v3.recurly.com/resources/";
$result = "{\"error\":{\"type\":\"validation\",\"message\":\"Name is invalid\",\"params\":[{\"param\":\"name\",\"message\":\"is invalid\"}]}}";
$body = [ "name" => "invalid" ];
$this->client->addScenario("POST", $url, $body, $result, "422 Unprocessable Entity");

$this->expectException(\Recurly\Errors\Validation::class);
$resource = $this->client->createResource([ "name" => "invalid" ]);
}

public function testDeleteResource204(): void
{
$url = "https://v3.recurly.com/resources/iexist";
$result = "";
$this->client->addScenario("DELETE", $url, NULL, $result, "204 No Content");
$empty = $this->client->deleteResource("iexist");
}

public function testUpdateResource200(): void
{
$url = "https://v3.recurly.com/resources/iexist";
$result = '{"id": "iexist", "object": "test_resource", "name": "newname"}';
$body = [ "name" => "newname" ];
$this->client->addScenario("PUT", $url, $body, $result, "200 OK");

$resource = $this->client->updateResource("iexist", $body);
$this->assertEquals($resource->getName(), "newname");
}

public function testListResources200(): void
{
$url = "https://v3.recurly.com/resources";
$result = '{ "object": "list", "has_more": false, "next": null, "data": [{"id": "iexist", "object": "test_resource", "name": "newname"}]}';
$this->client->addScenario("GET", $url, NULL, $result, "200 OK");

$resources = $this->client->listResources();
$count = 0;
foreach($resources as $resource) {
$count = $count + 1;
$this->assertEquals($resource->getId(), "iexist");
}
$this->assertEquals($count, 1);
}

public function testListResourcesWithParams200(): void
{
$url = "https://v3.recurly.com/resources?limit=1";
$result = '{ "object": "list", "has_more": false, "next": null, "data": [{"id": "iexist", "object": "test_resource", "name": "newname"}]}';
$this->client->addScenario("GET", $url, NULL, $result, "200 OK");

$resources = $this->client->listResources([ "limit" => 1 ]);
$count = 0;
foreach($resources as $resource) {
$count = $count + 1;
$this->assertEquals($resource->getId(), "iexist");
}
$this->assertEquals($count, 1);
}
}
98 changes: 98 additions & 0 deletions tests/mock_client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

use Recurly\Page;
use Recurly\Resources\TestResource;
use Recurly\BaseClient;
use Recurly\Utils;
use PHPUnit\Framework\MockObject\Generator;
use Recurly\HttpAdapter;

class MockClient extends BaseClient
{
use Recurly\RecurlyTraits;

public function __construct()
{
parent::__construct("apikey");
$this->http = (new Generator())->getMock(HttpAdapter::class);
}

protected function apiVersion(): string
{
return "v2999-01-01";
}

public function listResources(array $options = []): \Recurly\Pager
{
$path = $this->interpolatePath("/resources", []);
return new \Recurly\Pager($this, $path, $options);
}

public function getResource(string $resource_id): TestResource
{
$path = $this->interpolatePath("/resources/{resource_id}", ['resource_id' => $resource_id]);
return $this->makeRequest('GET', $path, null, null);
}

public function createResource(array $body): TestResource
{
$path = $this->interpolatePath("/resources/", []);
return $this->makeRequest('POST', $path, $body, null);
}

public function updateResource(string $resource_id, array $body): TestResource
{
$path = $this->interpolatePath("/resources/{resource_id}", ['resource_id' => $resource_id]);
return $this->makeRequest('PUT', $path, $body, null);
}

public function deleteResource(string $resource_id): \Recurly\EmptyResource
{
$path = $this->interpolatePath("/resources/{resource_id}", ['resource_id' => $resource_id]);
return $this->makeRequest('DELETE', $path, null, null);
}

public function addScenario($method, $url, $body, $result, $status): void
{
$resp_header = self::_generateRespHeader($status);
$this->http->method('execute')->with(
$method,
$url,
$body,
self::_expectedHeaders()
)->willReturn(array($result, $resp_header));
}

public function clearScenarios(): void
{
$this->http = (new Generator())->getMock(HttpAdapter::class);
}

private static function _generateRespHeader($status): array
{
return [
"HTTP/1.1 $status",
"Date: Wed, 19 Feb 2020 17:52:05 GMT",
"Content-Type: application/json; charset=utf-8",
"Recurly-Version: recurly.v2999-01-01",
"X-RateLimit-Limit: 2000",
"X-RateLimit-Remaining: 1996",
"X-RateLimit-Reset: 1582135020",
"X-Request-Id: 567a17af7875e3ba-ATL",
"Server: cloudflare",
"CF-RAY: 567a17af7875e3ba-ATL"
];
}

private static function _expectedHeaders(): array
{
$auth_token = self::encodeApiKey("apikey");
$agent = self::getUserAgent();
return [
"User-Agent" => $agent,
"Authorization" => "Basic {$auth_token}",
"Accept" => "application/vnd.recurly.v2999-01-01",
"Content-Type" => "application/json",
];
}
}
Loading

0 comments on commit eec31fc

Please sign in to comment.