diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1c84dad
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/.idea
+*.DS_Store
+/vendor
+composer.lock
+tmp
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..4e4671f
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,15 @@
+language: php
+
+php:
+ - 5.5
+ - 5.6
+
+install:
+ - composer self-update
+ - composer install
+
+script:
+ - vendor/bin/phpunit --coverage-clover=coverage.xml
+
+after_success:
+ - bash <(curl -s https://codecov.io/bash)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8081ebd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,89 @@
+# Classy PHP SDK [![Build Status](https://travis-ci.org/classy-org/classy-php-sdk.png?branch=master)](https://travis-ci.org/classy-org/classy-php-sdk) [![codecov.io](https://codecov.io/github/classy-org/classy-php-sdk/coverage.svg?branch=master)](https://codecov.io/github/classy-org/classy-php-sdk?branch=master)
+
+This repository contains a php HTTP client library to let your php App interact with Classy's API.
+
+## Installation
+
+The Classy PHP SDK can be installed with [Composer](https://getcomposer.org/):
+
+```sh
+composer require classy-org/classy-php-sdk
+```
+
+Be sure you included composer autoloader in your app:
+
+```php
+require_once '/path/to/your/project/vendor/autoload.php';
+```
+
+## Usage
+
+### Basic example
+
+```php
+$client = new \Classy\Client([
+ 'client_id' => 'your_client_id',
+ 'client_secret' => 'your_client_secret',
+ 'version' => '2.0' // version of the API to be used
+]);
+
+$session = $client->newAppSession();
+
+// Get information regarding the campaign #1234
+$campaign = $client->get('/campaigns/1234', $session);
+
+// Access the campaign goal: $campaign->goal
+
+// Unpublish the campaign
+$client->post('/campaign/1234/deactivate', $session);
+```
+
+### Sessions handling
+
+Sessions have an expiration date. It is possible to refresh a session:
+
+```php
+if ($session->expired()) {
+ $client->refresh($session)
+}
+
+// $session->expired() is now false.
+```
+
+Sessions are serializable, they can be saved an reused to reduce the amount of API calls:
+
+```php
+$client = new \Classy\Client([
+ 'client_id' => 'your_client_id',
+ 'client_secret' => 'your_client_secret',
+ 'version' => '2.0' // version of the API to be used
+]);
+
+// Retrieve the session from a file
+$session = unserialize(file_get_contents("path/to/a/cache/file"));
+
+// ... work with the API...
+
+// Save the session for later
+file_put_contents("path/to/a/cache/file", serialize($session));
+```
+
+### Errors handling
+
+This client can throw two types of Exceptions:
+
+* Classy\Exceptions\SDKException when the SDK is misused
+* Classy\Exceptions\APIResponseException when the API is not returning an OK response
+
+```php
+try {
+ $response = $client->get('/endpoint', $session);
+} catch (\Classy\Exceptions\APIResponseException $e) {
+ // Get the HTTP response code
+ $code = $e->getCode();
+ // Get the response content
+ $content = $e->getResponseData();
+ // Get the response headers
+ $headers = $e->getResponseHeaders();
+}
+```
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..3a13453
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,22 @@
+{
+ "name": "classy-org/classy-php-sdk",
+ "type": "library",
+ "description": "Php HTTP client library for Classy API",
+ "require": {
+ "guzzlehttp/guzzle": "^6.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.0",
+ "mockery/mockery": "^0.9.4"
+ },
+ "autoload": {
+ "psr-4": {
+ "Classy\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Classy\\Tests\\": "tests"
+ }
+ }
+}
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..9e0577e
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,13 @@
+
+
+ * The string representation of the object. + *
+ * @return void + * @since 5.1.0 + */ + public function unserialize($serialized) + { + $values = unserialize($serialized); + foreach ($values as $key => $value) { + $this->$key = $value; + } + } +} diff --git a/tests/ClientTest.php b/tests/ClientTest.php new file mode 100644 index 0000000..b6af83b --- /dev/null +++ b/tests/ClientTest.php @@ -0,0 +1,356 @@ + '2.0'], "client_id is missing"], + [['version' => '2.0', 'client_id' => '123'], "client_secret is missing"], + ]; + + } + + /** + * @dataProvider constructProvider + * @covers Classy\Client::__construct + */ + public function testConstructFailure($inputs, $error) + { + try { + new Client($inputs); + $this->fail("Exception expected"); + } catch (SDKException $e) { + $this->assertEquals($error, $e->getMessage()); + } + } + + /** + * @covers Classy\Client::__construct + */ + public function testConstructSuccess() + { + $client = new Client([ + 'version' => '2.0', + 'base_uri' => 'https://classy.org', + 'client_id' => '123', + 'client_secret' => '456' + ]); + $this->assertEquals('https://classy.org', $client->getHttpClient()->getConfig('base_uri')->__toString()); + } + + /** + * @covers Classy\Client::newAppSession + * @covers Classy\Session::expired + */ + public function testNewAppSession() + { + $this->guzzleMock->shouldReceive('request') + ->once() + ->with('POST', '/oauth2/auth', Mockery::on(function($args) { + return $args['form_params'] === [ + 'grant_type' => 'client_credentials', + 'client_id' => '123', + 'client_secret' => '456', + ]; + })) + ->andReturn(new Response(200, [], json_encode([ + "access_token" => 'access_token', + "expires_in" => 3600 + ]))); + + $session = $this->client->newAppSession(); + $this->assertInstanceOf(Session::class, $session); + $this->assertEquals("access_token", $session->getAccessToken()); + $this->assertFalse($session->expired()); + } + + /** + * @covers Classy\Client::newMemberSessionFromCode + */ + public function testNewMemberSessionFromCode() + { + $this->guzzleMock->shouldReceive('request') + ->once() + ->with('POST', '/oauth2/auth', Mockery::on(function($args) { + return $args['form_params'] === [ + 'grant_type' => 'authorization_code', + 'client_id' => '123', + 'client_secret' => '456', + 'code' => '789' + ]; + })) + ->andReturn(new Response(200, [], "{}")); + + $session = $this->client->newMemberSessionFromCode("789"); + $this->assertInstanceOf(Session::class, $session); + } + + /** + * @covers Classy\Client::newMemberSessionFromCredentials + */ + public function testNewMemberSessionFromCredentials() + { + $this->guzzleMock->shouldReceive('request') + ->once() + ->with('POST', '/oauth2/auth', Mockery::on(function($args) { + return $args['form_params'] === [ + 'grant_type' => 'password', + 'client_id' => '123', + 'client_secret' => '456', + 'username' => 'email@domain.tld', + 'password' => 'pass' + ]; + })) + ->andReturn(new Response(200, [], "{}")); + + $session = $this->client->newMemberSessionFromCredentials("email@domain.tld", "pass"); + $this->assertInstanceOf(Session::class, $session); + } + + /** + * @covers Classy\Client::newMemberSessionFromRefreshToken + */ + public function testNewMemberSessionFromRefreshToken() + { + $this->guzzleMock->shouldReceive('request') + ->once() + ->with('POST', '/oauth2/auth', Mockery::on(function($args) { + return $args['form_params'] === [ + 'grant_type' => 'refresh_token', + 'client_id' => '123', + 'client_secret' => '456', + 'refresh_token' => 'token', + ]; + })) + ->andReturn(new Response(200, [], "{}")); + + $session = $this->client->newMemberSessionFromRefreshToken("token"); + $this->assertInstanceOf(Session::class, $session); + } + + /** + * @covers Classy\Client::refresh + */ + public function testRefreshAppToken() + { + $session = new Session([ + 'access_token' => '12345', + 'expires_in' => -100 + ]); + $this->assertTrue($session->expired()); + + $this->guzzleMock->shouldReceive('request') + ->once() + ->with('POST', '/oauth2/auth', Mockery::on(function($args) { + return $args['form_params'] === [ + 'grant_type' => 'client_credentials', + 'client_id' => '123', + 'client_secret' => '456', + ]; + })) + ->andReturn(new Response(200, [], json_encode([ + "access_token" => '56789', + "expires_in" => 3600 + ]))); + + $this->client->refresh($session); + + $this->assertEquals('56789', $session->getAccessToken()); + $this->assertFalse($session->expired()); + } + + /** + * @covers Classy\Client::refresh + */ + public function testRefreshMemberToken() + { + $session = new Session([ + 'access_token' => '12345', + 'refresh_token' => '55555', + 'expires_in' => 3600 + ]); + + $this->guzzleMock->shouldReceive('request') + ->once() + ->with('POST', '/oauth2/auth', Mockery::on(function($args) { + return $args['form_params'] === [ + 'grant_type' => 'refresh_token', + 'client_id' => '123', + 'client_secret' => '456', + 'refresh_token' => '55555' + ]; + })) + ->andReturn(new Response(200, [], json_encode([ + "access_token" => '56789', + "refresh_token" => '6666', + "expires_in" => 3600 + ]))); + + $this->client->refresh($session); + + $this->assertEquals('56789', $session->getAccessToken()); + $this->assertEquals('6666', $session->getRefreshToken()); + $this->assertFalse($session->expired()); + } + + + public function testRESTVerbsProvider() + { + return [ + ['get', null], + ['delete', null], + ['post', ['payload' => 'content']], + ['put', ['payload' => 'content']], + ]; + + } + + /** + * @dataProvider testRESTVerbsProvider + * @covers Classy\Client::get + * @covers Classy\Client::post + * @covers Classy\Client::delete + * @covers Classy\Client::put + */ + public function testRESTVerbs($verb, array $payload = null) + { + $clientMock = Mockery::mock(Client::class . "[request]", [[ + 'client_id' => '123', + 'client_secret' => '456', + 'version' => '2.0' + ]]); + + $expectation = $clientMock->shouldReceive('request')->once(); + if (is_null($payload)) { + $expectation->with(mb_strtoupper($verb), '/2.0/endpoint', null); + $clientMock->$verb('endpoint'); + } else { + $expectation->with(mb_strtoupper($verb), '/2.0/endpoint', null, Mockery::on(function($args) { + return $args['json'] === ['payload' => 'content']; + })); + $clientMock->$verb('endpoint', null, $payload); + } + } + + /** + * @covers Classy\Client::request + */ + public function testRequest() + { + $session = new Session([ + 'access_token' => 'abcdef', + 'expires_in' => '3600' + ]); + + $this->guzzleMock->shouldReceive('request') + ->once() + ->with( + 'POST', + '/3.0/endpoint', + Mockery::on(function($args) { + return $args === [ + 'json' => ['payload' => 'content'], + 'headers' => ['Authorization' => 'Bearer abcdef'] + ]; + })) + ->andReturn(new Response(200, [], "{}")); + + $this->client->request('POST', '/3.0/endpoint', $session, ['json' => ['payload' => 'content']]); + } + + /** + * @covers Classy\Client::request + */ + public function testRequestWithExpiredSession() + { + $session = new Session([ + 'access_token' => 'abcdef', + 'expires_in' => '-1000' + ]); + + $this->guzzleMock->shouldReceive('request') + ->once() + ->with('POST', '/oauth2/auth', Mockery::on(function($args) { + return $args['form_params'] === [ + 'grant_type' => 'client_credentials', + 'client_id' => '123', + 'client_secret' => '456', + ]; + })) + ->andReturn(new Response(200, [], json_encode([ + "access_token" => '56789', + "expires_in" => 3600 + ]))); + + + $this->guzzleMock->shouldReceive('request') + ->once() + ->with( + 'GET', + '/3.0/endpoint', + Mockery::on(function($args) { + return $args === ['headers' => ['Authorization' => 'Bearer 56789']]; + })) + ->andReturn(new Response(200, [], "{}")); + + $this->client->request('GET', '/3.0/endpoint', $session); + $this->assertFalse($session->expired()); + } + + /** + * @covers Classy\Client::applyVersion + */ + public function testApplyVersion() + { + $testSets = [ + ['2.0', 'endpoint'], + ['2.0 /', '/endpoint'], + [' /2.0/ ', '/endpoint'], + ['/2.0 ', '/ endpoint'], + [' /2.0 ', '/endpoint '], + ['/2.0/ ', '/endpoint / '], + ]; + $method = new ReflectionMethod(Client::class, 'applyVersion'); + $method->setAccessible(true); + + foreach ($testSets as $inputs) { + $result = $method->invoke($this->client, $inputs[0], $inputs[1]); + $this->assertEquals('/2.0/endpoint', $result); + } + } + + /** + * @covers Classy\Client::request + * @covers Classy\Exceptions\APIResponseException + */ + public function testErrorHandling() + { + $client = new Client([ + 'version' => '2.0', + 'client_id' => 'aze', + 'client_secret' => 'aze' + ]); + try { + $client->newAppSession(); + $this->fail('Exception expected'); + } catch (APIResponseException $e) { + $this->assertEquals(400, $e->getCode()); + $this->assertEquals('invalid_request', $e->getResponseData()->error); + $this->assertEquals('application/json; charset=utf-8', $e->getResponseHeaders()['Content-Type'][0]); + } + } +} diff --git a/tests/SessionTest.php b/tests/SessionTest.php new file mode 100644 index 0000000..01406ed --- /dev/null +++ b/tests/SessionTest.php @@ -0,0 +1,41 @@ + 'abc', + 'refresh_token' => 'def', + 'expires_in' => 3600 + ]); + $this->assertEquals('abc', $session->getAccessToken()); + $this->assertEquals('def', $session->getRefreshToken()); + } + + /** + * @covers Classy\Session::serialize + * @covers Classy\Session::unserialize + */ + public function testSerialize() + { + $session = new Session([ + 'access_token' => 'abc', + 'refresh_token' => 'def', + 'expires_in' => 3600 + ]); + + $session2 = unserialize(serialize($session)); + $this->assertEquals($session->getAccessToken(), $session2->getAccessToken()); + $this->assertEquals($session->getRefreshToken(), $session2->getRefreshToken()); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..dd79429 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,33 @@ +guzzleMock = Mockery::mock(\GuzzleHttp\Client::class); + $this->client = new Client([ + 'client_id' => '123', + 'client_secret' => '456', + 'version' => '2.0' + ]); + $this->client->setHttpClient($this->guzzleMock); + parent::setUp(); + } + +}