#StackBounty: #php #rest #curl #phpunit Curl-based REST client library (round 2)

Bounty: 100

This is a second iteration on an earlier review – cURL based REST client library

I have done some refactoring to split out the REST HTTP response into it’s own class from what was in the previous review and implement.

You might find it useful to read the library’s README file before performing this review for further background and usage examples, which I have omitted here for brevity and to allow the question to focus on the code.

I am only reviewing the single REST call portion of the library here.

I have also opened a separate review to cover the curl_multi_* based functionality for performing multiple requests in parallel.

RestClient class

<?php

namespace MikeBrantRestClientLib;

/**
 * @desc Class for executing RESTful service calls using a fluent interface.
 */
class RestClient
{
    /**
      * Flag to determine if basic authentication is to be used.
     * 
     * @var boolean
     */
    protected $useBasicAuth = false;

    /**
     * User Name for HTTP Basic Auth
     * 
     * @var string
     */
    protected $basicAuthUsername = null;

    /**
     * Password for HTTP Basic Auth
     *
     * @var string
     */
    protected $basicAuthPassword = null;

    /**
     * Flag to determine if SSL is used
     * 
     * @var boolean
     */
    protected $useSsl = false;

    /**
     * Flag to determine is we are to run in test mode where host's SSL cert is not verified
     * 
     * @var boolean
     */
    protected $useSslTestMode = false;

    /**
     * Integer value representing number of seconds to set for curl timeout option. Defaults to 30 seconds.
     * 
     * @var integer
     */
    protected $timeout = 30;

    /**
     * Variable to store remote host name
     * 
     * @var string
     */
    protected $remoteHost = null;

    /**
     * Variable to hold setting to determine if redirects are followed
     * 
     * @var boolean
     */
    protected $followRedirects = false;

    /**
     * Variable to hold value for maximum number of redirects to follow for cases when redirect are being followed.
     * Default value of 0 will allow for following of unlimited redirects.
     * 
     * @var integer
     */
    protected $maxRedirects = 0;

    /**
     * Variable which can hold a URI base for all actions
     * 
     * @var string
     */
    protected $uriBase = '/';

    /**
     * Stores curl handle
     *
     * @var mixed
     */
    private $curl = null;

    /**
     * Variable to store request URL that is formed before a request is made
     * 
     * @var string
     */
    private $requestUrl = null;

    /**
     * Array containing headers to be used for request
     * 
     * @var array
     */
    private $headers = array();

    /**
     * Variable to store the request header as sent
     * 
     * @var string
     */

    /**
     * Variable to store CurlHttpResponse result from curl call
     * 
     * @var CurlHttpResponse
     */
    private $response = null;

    /**
     * Constructor method. Currently there is no instantiation logic.
     *
     * @return void
     */
    public function __construct() {}

    /**
     * Method to execute GET on server
     * 
     * @param string $action
     * @return CurlHttpResponse
     * @throws InvalidArgumentException
     * @throws Exception
     */
    public function get($action) {
        $this->validateAction($action);
        $this->curlSetup();
        $this->setRequestUrl($action);
        curl_setopt($this->curl, CURLOPT_HTTPGET, true);
        // execute call. Can throw Exception.
        $this->curlExec();

        return $this->response;
    }

    /**
     * Method to exexute POST on server
     * 
     * @param mixed $action
     * @param mixed $data
     * @return CurlHttpResponse
     * @throws InvalidArgumentException
     * @throws Exception
     */
    public function post($action, $data) {
        $this->validateAction($action);
        $this->validateData($data);
        $this->curlSetup();
        $this->setRequestUrl($action);
        $this->setRequestData($data);
        curl_setopt($this->curl, CURLOPT_POST, true);
        // execute call. Can throw Exception.
        $this->curlExec();

        return $this->response;
    }

    /**
     * Method to execute PUT on server
     * 
     * @param string $action
     * @param mixed $data
     * @return CurlHttpResponse
     * @throws InvalidArgumentException
     * @throws Exception
     */
    public function put($action, $data) {
        $this->validateAction($action);
        $this->validateData($data);
        $this->curlSetup();
        $this->setRequestUrl($action);
        $this->setRequestData($data);
        curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'PUT');
        // execute call. Can throw Exception.
        $this->curlExec();

        return $this->response;
    }

    /**
     * Method to execute DELETE on server
     * 
     * @param string $action
     * @return CurlHttpResponse
     * @throws InvalidArgumentException
     * @throws Exception
     */
    public function delete($action) {
        $this->validateAction($action);
        $this->curlSetup();
        $this->setRequestUrl($action);
        curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'DELETE');
        // execute call. Can throw Exception.
        $this->curlExec();

        return $this->response;
    }

    /**
     * Method to execute HEAD on server
     * 
     * @param string $action
     * @return CurlHttpResponse
     * @throws InvalidArgumentException
     * @throws Exception
     */
    public function head($action) {
        $this->validateAction($action);
        $this->curlSetup();
        $this->setRequestUrl($action);
        curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, 'HEAD');
        curl_setopt($this->curl, CURLOPT_NOBODY, true);
        // execute call. Can throw Exception.
        $this->curlExec();

        return $this->response;
    }

    /**
     * Sets host name of remote server
     * 
     * @param string $host
     * @return RestClient
     * @throws InvalidArgumentException
     */
    public function setRemoteHost($host) {
        if(empty($host)) {
            throw new InvalidArgumentException('Host name not provided.');
        } else if(!is_string($host)) {
            throw new InvalidArgumentException('Non-string host name provided.');
        }

        // remove any http(s):// at beginning of host name
        $httpsPattern = '#https://#i';
        $httpPattern = '#http://#i';
        if (1 === preg_match($httpsPattern, $host)) {
            // this needs to be SSL request
            $this->setUseSsl(true);
            $host = str_ireplace('https://', '', $host);
        } else if (1 === preg_match($httpPattern, $host)) {
            $host = str_ireplace('http://', '', $host);
        }

        // remove trailing slash in host name
        $host = rtrim($host, '/');

        // look for common SSL port values in host name to see if SSL is needed
        $portPatterns = array(
            '/:443$/',
            '/:8443$/',
        );
        foreach ($portPatterns as $pattern) {
            if (1 === preg_match($pattern, $host)) {
                $this->setUseSsl(true);
            }
        }

        $this->remoteHost = $host;

        return $this;
    }

    /**
     * Sets URI base for the instance
     * 
     * @param string $uriBase
     * @return RestClient
     * @throws InvalidArgumentException
     */
    public function setUriBase($uriBase) {
        if(empty($uriBase)) {
            throw new InvalidArgumentException('URI base not provided.');
        } else if(!is_string($uriBase)) {
            throw new InvalidArgumentException('Non-string URI base provided.');
        }

        // make sure we always have forward slash at beginning and end of uriBase
        $uriBase = '/' . ltrim($uriBase, '/');
        $uriBase = rtrim($uriBase, '/') . '/';
        $this->uriBase = $uriBase;

        return $this;
    }

    /**
     * Sets whether SSL is to be used
     * 
     * @param boolean $value
     * @return RestClient
     * @throws InvalidArgumentException
     */
    public function setUseSsl($value) {
        if (!is_bool($value)) {
            throw new InvalidArgumentException('Non-boolean value passed as parameter.');
        }
        $this->useSsl = $value;

        return $this;
    }

    /**
     * Sets whether SSL Test Mode is to be used
     * 
     * @param boolean $value
     * @return RestClient
     * @throws InvalidArgumentException
     */
    public function setUseSslTestMode($value) {
        if (!is_bool($value)) {
            throw new InvalidArgumentException('Non-boolean value passed as parameter.');
        }
        $this->useSslTestMode = $value;

        return $this;
    }
    /**
     * Sets basic authentication credentials
     * 
     * @param string $user
     * @param string $password
     * @return RestClient
     * @throws InvalidArgumentException
     */
    public function setBasicAuthCredentials($user, $password) {
        if (empty($user)) {
            throw new InvalidArgumentException('User name not provided when trying to set basic authentication credentials.');
        }
        if (empty($password)) {
            throw new InvalidArgumentException('Password not provided when trying to set basic authentication credentials.');
        }

        $this->useBasicAuth = true;
        $this->basicAuthUsername = $user;
        $this->basicAuthPassword = $password;

        return $this;
    }

    /**
     * Sets HTTP headers from an associative array where key is header name and value is the header value
     * 
     * @param array $headers
     * @return RestClient
     */
    public function setHeaders(array $headers) {
        if(empty($headers)) {
            throw new InvalidArgumentException('Empty array passed when triyng to set headers');
        }
        $this->headers = $headers;

        return $this;  
    }

    /**
     * Sets maximum timeout for curl requests
     * 
     * @param integer $seconds
     * @return RestClient
     * @throws InvalidArgumentException
     */
    public function setTimeout($seconds) {
        if(!is_integer($seconds) || $seconds < 0) {
            throw new InvalidArgumentException('A non-negative integer value must be passed when trying to set timeout');
        }
        $this->timeout = $seconds;

        return $this;
    }

    /**
     * Sets flag on whether to follow 3XX redirects.
     * 
     * @param boolean $follow
     * @return RestClient
     * @throws InvalidArgumentException
     */
    public function setFollowRedirects($follow) {
        if(!is_bool($follow)) {
            throw new InvalidArgumentException('Non-boolean value passed as parameter.');
        }
        $this->followRedirects = $follow;

        return $this;
    }

    /**
     * Sets maximum number of redirects to follow. A value of 0 represents no redirect limit. Also sets followRedirects property to true .
     * 
     * @param integer $redirects
     * @return RestClient
     * @throws InvalidArgumentException
     */
    public function setMaxRedirects($redirects) {
        if(!is_integer($redirects) || $redirects < 0) {
            throw new InvalidArgumentException('A non-negative integer value must be passed when trying to set max redirects.');
        }
        $this->maxRedirects = $redirects;
        $this->setFollowRedirects(true);

        return $this;
    }

    /**
     * Get remote host setting
     * 
     * @return string
     */
    public function getRemoteHost() {
        return $this->remoteHost;
    }

    /**
     * Get URI Base setting
     * 
     * @return string
     */
    public function getUriBase() {
        return $this->uriBase;
    }

    /**
     * Get boolean setting indicating whether SSL is to be used
     * 
     * @return boolean
     */
    public function isUsingSsl() {
        return $this->useSsl;
    }

    /**
     * Get boolean setting indicating whether SSL test mode is enabled
     * 
     * @return boolean
     */
    public function isUsingSslTestMode() {
        return $this->useSslTestMode;
    }

    /**
     * Get timeout setting
     * 
     * @return integer
     */
    public function getTimeout() {
        return $this->timeout;
    }

    /**
     * Get follow redirects setting
     * 
     * @return boolean
     */
    public function isFollowingRedirects() {
        return $this->followRedirects;
    }

    /**
     * Get max redirects setting
     * 
     * @return integer
     */
    public function getMaxRedirects() {
        return $this->maxRedirects;
    }

    /**
     * Method to initialize curl handle in object
     * 
     * @return void
     * @throws Exception
     */
    private function curlSetup() {        
        // reset all request/response properties
        $this->resetRequestResponseProperties();

        // initialize curl. Throws Exception on failure.
        $this->curl = $this->curlInit();
    }

    /**
     * Method to initilize a curl handle
     * 
     * @return resource
     * @throws Exception
     */
    protected function curlInit() {
        // initialize curl
        $curl = curl_init();
        if($curl === false) {
            throw new Exception('curl failed to initialize.');
        }
        // set timeout
        curl_setopt($curl, CURLOPT_TIMEOUT, $this->timeout);

        // set basic HTTP authentication settings
        if (true === $this->useBasicAuth) {
            curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
            curl_setopt($curl, CURLOPT_USERPWD, $this->basicAuthUsername . ':' . $this->basicAuthPassword);
        }

        // set headers
        if (!empty($this->headers)) {
            $headers = array();
            foreach ($this->headers as $key=>$val) {
                $headers[] = $key . ': ' . $val;
            }
            curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
        }

        // if not in production environment, we want to ignore SSL validation
        if (true === $this->useSsl && true === $this->useSslTestMode) {
            curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
        }

        // set option to add request header information to curl_getinfo output
        curl_setopt($curl, CURLINFO_HEADER_OUT, true);

        // set option to return content body
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

        // set redirect options
        if (true === $this->followRedirects) {
            curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
            if ($this->maxRedirects > 0) {
                curl_setopt($curl, CURLOPT_MAXREDIRS, $this->maxRedirects);
            }
        }

        return $curl;
    }

    /**
     * Method to to teardown curl fixtures at end of request
     * 
     * @return void
     */
    private function curlTeardown() {
        $this->curlClose($this->curl);
        $this->curl = null;
    }

    /**
     * Method to close curl handle
     * 
     * @return void
     */
    protected function curlClose($curl) {
        curl_close($curl);
    }

    /**
     * Method to execute curl call
     * 
     * @return void
     * @throws Exception
     */
    private function curlExec() {
        $curlResult = curl_exec($this->curl);
        if($curlResult === false) {
            // our curl call failed for some reason
            $curlError = curl_error($this->curl);
            $this->curlTeardown();
            throw new Exception('curl call failed with message: "' . $curlError. '"');
        }

        // return CurlHttpResponse
        try {
            $this->response = new CurlHttpResponse($curlResult, curl_getinfo($this->curl));
        } catch (InvalidArgumentException $e) {
            throw new Exception(
                'Unable to instantiate CurlHttpResponse. Message: "' . $e->getMessage() . '"',
                $e->getCode(),
                $e
            );
        } finally {
            $this->curlTeardown();
        }
    }

    /**
     * Method to reset all properties specific to a particular request/response sequence.
     * 
     * @return void
     */
    protected function resetRequestResponseProperties() {
        $this->requestUrl = null;
        $this->response = null;
    }

    /**
     * Method to set the url on curl handle based on passed action
     * 
     * @param string $action
     * @return void
     */
    protected function setRequestUrl($action) {
        $url = $this->buildUrl($action);
        $this->requestUrl = $url;
        curl_setopt($this->curl, CURLOPT_URL, $url);
    }

    /**
     * Method to build URL based on class settings and passed action
     * 
     * @param string $action
     * @return string
     */
    protected function buildUrl($action) {
        $url = 'http://';
        if (true === $this->useSsl) {
            $url = 'https://';
        }
        $url = $url . $this->remoteHost . $this->uriBase . $action;
        return $url;
    }

    /**
     * Method to set data to be sent along with POST/PUT requests
     * 
     * @param mixed $data
     * @return void
     */
    protected function setRequestData($data) {
        $this->requestData = $data;
        curl_setopt($this->curl, CURLOPT_POSTFIELDS, $data);
    }

    /**
     * Method to provide common validation for action parameters
     * 
     * @param string $action
     * @return void
     * @throws InvalidArgumentException
     */
    protected function validateAction($action) {
        if(!is_string($action)) {
            throw new InvalidArgumentException('A non-string value was passed for action parameter');
        }
    }

    /**
     * Method to provide common validation for data parameters
     * 
     * @param mixed $data
     * @return void
     * @throws InvalidArgumentException
     */
    protected function validateData($data) {
        if(empty($data)) {
            throw new InvalidArgumentException('An empty value was passed for data parameter');
        }
    }
}

RestClient Unit Tests

<?php

namespace MikeBrantRestClientLib;

use PHPUnitFrameworkTestCase;

/**
 * Mock for curl_init global function
 * 
 * @return mixed
 */
function curl_init() {
    if (!is_null(RestClientTest::$curlInitResponse)) {
        return RestClientTest::$curlInitResponse;
    }
    return curl_init();
}

/**
 * Mock for curl_exec global function
 * 
 * @param resource curl handle
 * @return mixed
 */
function curl_exec($curl) {
    if (!is_null(RestClientTest::$curlExecResponse)) {
        return RestClientTest::$curlExecResponse;
    }
    return curl_exec($curl);
}

/**
 * Mock for curl_error global function
 * 
 * @param resource curl handle
 * @return mixed
 */
function curl_error($curl) {
    if (!is_null(RestClientTest::$curlErrorResponse)) {
        return RestClientTest::$curlErrorResponse;
    }
    return curl_error($curl);
}

/**
 * This is hacky workaround for avoiding double definition of this global method override
 * when running full test suite on this library.
 */
if(!function_exists('MikeBrantRestClientLibcurl_getinfo')) {

    /**
     * Mock for curl_getinfo function
     * 
     * @param resource curl handle
     * @return mixed
     */
    function curl_getinfo($curl) {
        $backtrace = debug_backtrace();
        $testClass = $backtrace[1]['class'] . 'Test';
        if (!is_null($testClass::$curlGetinfoResponse)) {
            return $testClass::$curlGetinfoResponse;
        }
        return curl_getinfo($curl);
    }
}

class RestClientTest extends TestCase
{
    public static $curlInitResponse = null;

    public static $curlExecResponse = null;

    public static $curlErrorResponse = null;

    public static $curlGetinfoResponse = null;

    protected $client = null;

    protected $curlExecMockResponse = 'Test Response';

    protected $curlGetinfoMockResponse = array(
        'url' => 'http://google.com/',
        'content_type' => 'text/html; charset=UTF-8',
        'http_code' => 200,
        'header_size' => 321,
        'request_size' => 49,
        'filetime' => -1,
        'ssl_verify_result' => 0,
        'redirect_count' => 0,
        'total_time' => 1.123264,
        'namelookup_time' => 1.045272,
        'connect_time' => 1.070183,
        'pretransfer_time' => 1.071139,
        'size_upload' => 0,
        'size_download' => 219,
        'speed_download' => 194,
        'speed_upload' => 0,
        'download_content_length' => 219,
        'upload_content_length' => -1,
        'starttransfer_time' => 1.122377,
        'redirect_time' => 0,
        'redirect_url' => 'http://www.google.com/',
        'primary_ip' => '216.58.194.142',
        'certinfo' => array(),
        'primary_port' => 80,
        'local_ip' => '192.168.1.74',
        'local_port' => 59733,
        'request_header' => "GET / HTTP/1.1nHost: google.comnAccept: */*",
    );

    protected function setUp() {
        self::$curlInitResponse = null;
        self::$curlExecResponse = null;
        self::$curlErrorResponse = null;
        self::$curlGetinfoResponse = null;
        $this->client = new RestClient();
    }

    protected function tearDown() {
        $this->client = null;
    }

    public function notStringProvider() {
        return array(
            array(null),
            array(new stdClass()),
            array(1),
            array(0),
            array(true),
            array(false),
            array(array())
        );
    }

    public function emptyProvider() {
        return array(
            array(null),
            array(''),
            array(0),
            array(0.0),
            array(false),
            array('0'),
            array(array())
        );
    }

    public function notStringAndEmptyProvider() {
        return array(
            array(null),
            array(''),
            array(new stdClass()),
            array(1),
            array(0),
            array(0.0),
            array('0'),
            array(true),
            array(false),
            array(array())
        );
    }

    public function hostProvider() {
        return array(
            array('somedomain.com', 'somedomain.com', false),
            array('somedomain.com/', 'somedomain.com', false),
            array('https://somedomain.com', 'somedomain.com', true),
            array('http://somedomain.com', 'somedomain.com', false),
            array('somedomain.com:80', 'somedomain.com:80', false),
            array('somedomain.com:443', 'somedomain.com:443', true),
            array('somedomain.com:8443', 'somedomain.com:8443', true)
        );
    }

    public function notBooleanProvider() {
        return array(
            array(null),
            array(''),
            array('string'),
            array('true'),
            array('false'),
            array(1),
            array(0),
            array('1'),
            array('0'),
            array(0.0),
            array(new stdClass()),
            array(array())
        );
    }

    public function uriBaseProvider() {
        return array(
            array('test', '/test/'),
            array('/test', '/test/'),
            array('test/', '/test/'),
            array('/test/', '/test/')
        );
    }

    public function notZeroOrPositiveIntegerProvider() {
        return array(
            array(-1),
            array(null),
            array(''),
            array(new stdClass()),
            array(1.0),
            array('1'),
            array(array())
        );
    }

    public function headersProvider() {
        return array(
            array(
                array(
                    'header1' => 'header1 value',
                    'header2' => 'header2 value'
                )
            )
        );
    }

    public function curlExecExceptionProvider() {
        return array(
            array(false, $this->curlGetinfoMockResponse),
            array('test', array())
        );
    }

    public function buildUrlProvider() {
        return array(
            array(true, 'google.com', 'base', 'action', 'https://google.com/base/action'),
            array(false, 'google.com', 'base', 'action', 'http://google.com/base/action')
        );
    }

    /**
     * @dataProvider notStringProvider
     * @expectedException InvalidArgumentException
     * @covers MikeBrantRestClientLibRestClient::validateAction
     */
    public function testValidateActionThrowsExceptions($action) {
        $this->client->get($action);
    }

    /**
     * @dataProvider emptyProvider
     * @expectedException InvalidArgumentException
     * @covers MikeBrantRestClientLibRestClient::validateData
     */
    public function testValidateDataThrowsExceptions($data) {
        $this->client->post('', $data);
    }

    /**
     * @dataProvider notStringAndEmptyProvider
     * @expectedException InvalidArgumentException
     * @covers MikeBrantRestClientLibRestClient::setRemoteHost
     */
    public function testSetRemoteHostThrowsExceptions($host) {
        $this->client->setRemoteHost($host);
    }

    /**
     * @dataProvider hostProvider
     * @covers MikeBrantRestClientLibRestClient::setRemoteHost
     * @covers MikeBrantRestClientLibRestClient::getRemoteHost
     */
    public function testSetRemoteHost($hostInput, $hostOutput, $useSslSet) {
        $this->client->setRemoteHost($hostInput);
        $this->assertEquals($hostOutput, $this->client->getRemoteHost());
        $this->assertEquals($useSslSet, $this->client->isUsingSsl());
    }

    /**
     * @dataProvider notStringAndEmptyProvider
     * @expectedException InvalidArgumentException
     * @covers MikeBrantRestClientLibRestClient::setUriBase
     */
    public function testSetUriBaseThrowsExceptions($string) {
        $this->client->setUriBase($string);
    }

    /**
     * @dataProvider uriBaseProvider
     * @covers MikeBrantRestClientLibRestClient::setUriBase
     * @covers MikeBrantRestClientLibRestClient::getUriBase
     */
    public function testSetUriBase($stringInput, $stringOutput) {
        $this->client->setUriBase($stringInput);
        $this->assertEquals($stringOutput, $this->client->getUriBase());
    }

    /**
     * @dataProvider notBooleanProvider
     * @expectedException InvalidArgumentException
     * @covers MikeBrantRestClientLibRestClient::setUseSsl
     */
    public function testSetUseSslThrowsExceptions($boolean) {
        $this->client->setUseSsl($boolean);
    }

    /**
     * @covers MikeBrantRestClientLibRestClient::setUseSsl
     * @covers MikeBrantRestClientLibRestClient::isUsingSsl
     */
    public function testSetUseSsl() {
        $this->client->setUseSsl(true);
        $this->assertTrue($this->client->isUsingSsl());
        $this->client->setUseSsl(false);
        $this->assertFalse($this->client->isUsingSsl());
    }

    /**
     * @dataProvider notBooleanProvider
     * @expectedException InvalidArgumentException
     * @covers MikeBrantRestClientLibRestClient::setUseSslTestMode
     */
    public function testSetUseSslTestModeThrowsExceptions($boolean) {
        $this->client->setUseSslTestMode($boolean);
    }

    /**
     * @covers MikeBrantRestClientLibRestClient::setUseSslTestMode
     * @covers MikeBrantRestClientLibRestClient::isUsingSslTestMode
     */
    public function testSetUseSslTestMode() {
        $this->client->setUseSslTestMode(true);
        $this->assertTrue($this->client->isUsingSslTestMode());
        $this->client->setUseSslTestMode(false);
        $this->assertFalse($this->client->isUsingSslTestMode());
    }

    /**
     * @dataProvider emptyProvider
     * @expectedException InvalidArgumentException
     * @covers MikeBrantRestClientLibRestClient::setBasicAuthCredentials
     */
    public function testSetBasicAuthCredentialsThrowsExceptionOnEmptyUser($user) {
        $this->client->setBasicAuthCredentials($user, 'password');
    }

    /**
     * @dataProvider emptyProvider
     * @expectedException InvalidArgumentException
     * @covers MikeBrantRestClientLibRestClient::setBasicAuthCredentials
     */
    public function testSetBasicAuthCredentialsThrowsExceptionOnEmptyPassword($password) {
        $this->client->setBasicAuthCredentials('user', $password);
    }

    /**
     * @covers MikeBrantRestClientLibRestClient::setBasicAuthCredentials
     */
    public function testSetBasicAuthCredentials() {
        $this->client->setBasicAuthCredentials('user', 'password');
        $this->assertAttributeEquals('user', 'basicAuthUsername', $this->client);
        $this->assertAttributeEquals('password', 'basicAuthPassword', $this->client);
        $this->assertAttributeEquals(true, 'useBasicAuth', $this->client);
    }

    /**
     * @expectedException InvalidArgumentException
     * @covers MikeBrantRestClientLibRestClient::setHeaders
     */
    public function testSetHeadersThrowsExceptionOnEmptyArray() {
        $this->client->setHeaders(array());
    }

    /**
     * @dataProvider headersProvider
     * @covers MikeBrantRestClientLibRestClient::setHeaders
     */
    public function testSetHeaders($headers) {
        $this->client->setHeaders($headers);
        $this->assertAttributeEquals($headers, 'headers', $this->client);
    }

    /**
     * @dataProvider notZeroOrPositiveIntegerProvider
     * @expectedException InvalidArgumentException
     * @covers MikeBrantRestClientLibRestClient::setTimeout
     */
    public function testSetTimeoutThrowsExceptions($int) {
        $this->client->setTimeout($int);
    }

    /**
     * @covers MikeBrantRestClientLibRestClient::setTimeout
     * @covers MikeBrantRestClientLibRestClient::getTimeout
     */
    public function testSetTimeout() {
        $this->client->setTimeout(30);
        $this->assertEquals(30, $this->client->getTimeout());
        $this->client->setTimeout(0);
        $this->assertEquals(0, $this->client->getTimeout());
    }

    /**
     * @dataProvider notBooleanProvider
     * @expectedException InvalidArgumentException
     * @covers MikeBrantRestClientLibRestClient::setFollowRedirects
     */
    public function testSetFollowRedirectsThrowsExceptions($boolean) {
        $this->client->setFollowRedirects($boolean);
    }

    /**
     * @covers MikeBrantRestClientLibRestClient::setFollowRedirects
     * @covers MikeBrantRestClientLibRestClient::isFollowingRedirects
     */
    public function testSetFollowRedirects() {
        $this->client->setFollowRedirects(true);
        $this->assertTrue($this->client->isFollowingRedirects());
        $this->client->setFollowRedirects(false);
        $this->assertFalse($this->client->isFollowingRedirects());
    }

    /**
     * @dataProvider notZeroOrPositiveIntegerProvider
     * @expectedException InvalidArgumentException
     * @covers MikeBrantRestClientLibRestClient::setMaxRedirects
     */
    public function testSetMaxRedirectsThrowsExceptions($int) {
        $this->client->setMaxRedirects($int);
    }

    /**
     * @covers MikeBrantRestClientLibRestClient::setMaxRedirects
     * @covers MikeBrantRestClientLibRestClient::getMaxRedirects
     */
    public function testSetMaxRedirects() {
        $this->client->setMaxRedirects(1);
        $this->assertEquals(1, $this->client->getMaxRedirects());
        $this->assertTrue($this->client->isFollowingRedirects());
        $this->client->setMaxRedirects(0);
        $this->assertEquals(0, $this->client->getMaxRedirects());
        $this->assertTrue($this->client->isFollowingRedirects());
    }

    /**
     * @expectedException Exception
     * @covers MikeBrantRestClientLibRestClient::curlInit
     */
    public function testCurlInitThrowsException() {
        self::$curlInitResponse = false;
        $this->client->get('action');
    }

    /**
     * @dataProvider curlExecExceptionProvider
     * @expectedException Exception
     * @covers MikeBrantRestClientLibRestClient::curlExec
     */
    public function testCurlExecThrowsException($response, $getinfo) {
        self::$curlExecResponse = $response;
        self::$curlErrorResponse = 'test error';
        self::$curlGetinfoResponse = $getinfo;
        $this->client->get('action');
    }

    /**
     * @dataProvider buildUrlProvider
     * @covers MikeBrantRestClientLibRestClient::get
     * @covers MikeBrantRestClientLibRestClient::validateAction
     * @covers MikeBrantRestClientLibRestClient::buildUrl
     * @covers MikeBrantRestClientLibRestClient::curlSetup
     * @covers MikeBrantRestClientLibRestClient::resetRequestResponseProperties
     * @covers MikeBrantRestClientLibRestClient::curlInit
     * @covers MikeBrantRestClientLibRestClient::setRequestUrl
     * @covers MikeBrantRestClientLibRestClient::curlExec
     * @covers MikeBrantRestClientLibRestClient::curlTeardown
     * @covers MikeBrantRestClientLibRestClient::curlClose
     */
    public function testGet($useSsl, $host, $uriBase, $action, $expectedUrl) {
        self::$curlExecResponse = $this->curlExecMockResponse;
        self::$curlGetinfoResponse = $this->curlGetinfoMockResponse;
        $this->client->setBasicAuthCredentials('user', 'password')
                     ->setHeaders(array('header' => 'header value'))
                     ->setUseSsl($useSsl)
                     ->setUseSslTestMode(true)
                     ->setFollowRedirects(true)
                     ->setMaxRedirects(1)
                     ->setremoteHost($host)
                     ->setUriBase($uriBase);
        $response = $this->client->get($action);
        $this->assertInstanceOf(CurlHttpResponse::class, $response);
        $this->assertAttributeEquals(null, 'curl', $this->client);
    }

    /**
     * @covers MikeBrantRestClientLibRestClient::post
     * @covers MikeBrantRestClientLibRestClient::validateData
     * @covers MikeBrantRestClientLibRestClient::setRequestData
     */
    public function testPost() {
        self::$curlExecResponse = $this->curlExecMockResponse;
        self::$curlGetinfoResponse = $this->curlGetinfoMockResponse;
        $response = $this->client->post('', 'test post data');
        $this->assertInstanceOf(CurlHttpResponse::class, $response);
        $this->assertAttributeEquals(null, 'curl', $this->client);
   }

    /**
     * @covers MikeBrantRestClientLibRestClient::put
     */
    public function testPut() {
        self::$curlExecResponse = $this->curlExecMockResponse;
        self::$curlGetinfoResponse = $this->curlGetinfoMockResponse;
        $response = $this->client->put('', 'test put data');
        $this->assertInstanceOf(CurlHttpResponse::class, $response);
        $this->assertAttributeEquals(null, 'curl', $this->client);
    }

    /**
     * @covers MikeBrantRestClientLibRestClient::delete
     */
    public function testDelete() {
        self::$curlExecResponse = $this->curlExecMockResponse;
        self::$curlGetinfoResponse = $this->curlGetinfoMockResponse;
        $response = $this->client->delete('');
        $this->assertInstanceOf(CurlHttpResponse::class, $response);
        $this->assertAttributeEquals(null, 'curl', $this->client);
    }

    /**
     * @covers MikeBrantRestClientLibRestClient::head
     */
    public function testHead() {
        self::$curlExecResponse = '';
        self::$curlGetinfoResponse = $this->curlGetinfoMockResponse;
        $response = $this->client->head('');
        $this->assertInstanceOf(CurlHttpResponse::class, $response);
        $this->assertAttributeEquals(null, 'curl', $this->client);
    }
}

CurlHttpResponse class

<?php

namespace MikeBrantRestClientLib;

/**
 * @desc Class representing HTTP response as returned from curl call.
 */
class CurlHttpResponse
{
    /**
     * Variable to store response body
     */
    protected $body = null;

    /**
     * Variable to store HTTP repsonse code
     * 
     * @var integer
     */
    protected $httpCode = null;

    /**
     * Variable to store response content type header
     * 
     * @var string
     */
    protected $contentType = null;

    /**
     * Variable to store URL used in request as reported via curl_getinfo().
     * 
     * @var string
     */
    protected $requestUrl = null;

    /**
     * Variable to store header used in request as reported via curl_getinfo().
     * 
     * @var string
     */
    protected $requestHeader = null;

    /**
     * Variable to store curl getinfo array.
     * See documentation at http://php.net/manual/en/function.curl-getinfo.php for expected array format.
     * 
     * @var array
     */
    protected $curlGetinfo = null;

    /**
     * Constructor method.
     * 
     * @param mixed $responseBody Response body as returned from a curl request.
     * @param array $curlGetinto Array returned form curl_getinfo() function call for request.
     * @return void
     * @throws InvalidArgumentException
     */
    public function __construct($responseBody, array $curlGetinfo) {
        $this->validateGetinfoArray($curlGetinfo);
        $this->body = $responseBody;
        $this->httpCode = $curlGetinfo['http_code'];
        $this->contentType = $curlGetinfo['content_type'];
        $this->requestUrl = $curlGetinfo['url'];
        $this->requestHeader = $curlGetinfo['request_header'];
        $this->curlGetinfo = $curlGetinfo;
    }

    /**
     * Returns response body for request
     * 
     * @return mixed
     */
    public function getBody() {
        return $this->body;
    }

    /**
     * Returns HTTP response code for request
     * 
     * @return integer
     */
    public function getHttpCode() {
        return $this->httpCode;
    }

    /**
     * Returns URL used in request as reported via curl_getinfo().
     * 
     * @return string
     */
    public function getRequestUrl() {
        return $this->requestUrl;
    }

    /**
     * Returns header used in request as reported via curl_getinfo().
     * 
     * @return string
     */
    public function getRequestHeader() {
        return $this->requestHeader;
    }

    /**
     * Returns curl getinfo array.
     * See documentation at http://php.net/manual/en/function.curl-getinfo.php for expected array format.
     * 
     * @return array
     */
    public function getCurlGetinfo() {
        return $this->curlGetinfo;
    }

    /**
     * Method to perform minimal validation of input array as having keys expected to be returned from
     * curl_getinfo().
     * 
     * @throws InvalidArgumentException
     */
    protected function validateGetinfoArray(array $getinfo) {
        if(empty($getinfo)) {
            throw new InvalidArgumentException('Empty array passed. Valid curl_getinfo() result array expected.');
        }
        if(!isset($getinfo['http_code']) || !is_integer($getinfo['http_code'])) {
            throw new InvalidArgumentException('curl_getinfo() response array expects integer value at http_code key.');
        }
        if(!isset($getinfo['content_type']) || !is_string($getinfo['content_type'])) {
            throw new InvalidArgumentException('curl_getinfo() response array expects string value at content_type key.');
        }
        if(!isset($getinfo['url']) || !is_string($getinfo['url'])) {
            throw new InvalidArgumentException('curl_getinfo() response array expects string value at url key.');
        }
        if(!isset($getinfo['request_header']) || !is_string($getinfo['request_header'])) {
            throw new InvalidArgumentException('curl_getinfo() response array expects string value at request_header key.');
        }
    }
}

CurlHttpResponse Unit Tests

<?php

namespace MikeBrantRestClientLib;

use PHPUnitFrameworkTestCase;

class CurlHttpResponseTest extends TestCase
{
    protected $curlExecMockResponse = 'Test Response';

    protected $curlGetinfoMockResponse = array(
        'url' => 'http://google.com/',
        'content_type' => 'text/html; charset=UTF-8',
        'http_code' => 200,
        'header_size' => 321,
        'request_size' => 49,
        'filetime' => -1,
        'ssl_verify_result' => 0,
        'redirect_count' => 0,
        'total_time' => 1.123264,
        'namelookup_time' => 1.045272,
        'connect_time' => 1.070183,
        'pretransfer_time' => 1.071139,
        'size_upload' => 0,
        'size_download' => 219,
        'speed_download' => 194,
        'speed_upload' => 0,
        'download_content_length' => 219,
        'upload_content_length' => -1,
        'starttransfer_time' => 1.122377,
        'redirect_time' => 0,
        'redirect_url' => 'http://www.google.com/',
        'primary_ip' => '216.58.194.142',
        'certinfo' => array(),
        'primary_port' => 80,
        'local_ip' => '192.168.1.74',
        'local_port' => 59733,
        'request_header' => "GET / HTTP/1.1nHost: google.comnAccept: */*",
    );

    public function invalidGetinfoProvider() {
        return array(
            array(
                array()
            ),
            array(
                array('no keys')
            ),
            array(
                array(
                    'http_code' => 'not integer'
                )
            ),
            array(
                array(
                    'http_code' => 200
                )
            ),
            array(
                array(
                   'http_code' => 200,
                   'content_type' => false
                )
            ),
            array(
                array(
                   'http_code' => 200,
                   'content_type' => 'text/html'
                )
            ),
            array(
                array(
                   'http_code' => 200,
                   'content_type' => 'text/html',
                   'url' => false
                )
            ),
            array(
                array(
                   'http_code' => 200,
                   'content_type' => 'text/html',
                   'url' => 'htttp://somedomain.com'
                )
            ),
            array(
                array(
                   'http_code' => 200,
                   'content_type' => 'text/html',
                   'url' => 'htttp://somedomain.com',
                   'request_header' => false
                )
            )
        );
    }

    /**
     * @dataProvider invalidGetinfoProvider
     * @expectedException InvalidArgumentException
     * @covers MikeBrantRestClientLibCurlHttpResponse::validateGetinfoArray
     */
    public function testValidateGetinfoArrayThrowsExceptions($getinfo) {
        $response = new CurlHttpResponse('test', $getinfo);
    }

    /**
     * @covers MikeBrantRestClientLibCurlHttpResponse::__construct
     * @covers MikeBrantRestClientLibCurlHttpResponse::validateGetinfoArray
     * @covers MikeBrantRestClientLibCurlHttpResponse::getBody
     * @covers MikeBrantRestClientLibCurlHttpResponse::getHttpCode
     * @covers MikeBrantRestClientLibCurlHttpResponse::getRequestUrl
     * @covers MikeBrantRestClientLibCurlHttpResponse::getRequestHeader
     * @covers MikeBrantRestClientLibCurlHttpResponse::getCurlGetinfo
     */
    public function testConstructor() {
        $response = new CurlHttpResponse($this->curlExecMockResponse, $this->curlGetinfoMockResponse);
        $this->assertEquals($this->curlExecMockResponse, $response->getBody());
        $this->assertEquals($this->curlGetinfoMockResponse['http_code'], $response->getHttpCode());
        $this->assertEquals($this->curlGetinfoMockResponse['url'], $response->getRequestUrl());
        $this->assertEquals($this->curlGetinfoMockResponse['request_header'], $response->getRequestHeader());
        $this->assertEquals($this->curlGetinfoMockResponse, $response->getCurlGetinfo());
    }
}


Get this bounty!!!