diff --git a/src/OpenStack/Bootstrap.php b/src/OpenStack/Bootstrap.php index b18752d..c876510 100644 --- a/src/OpenStack/Bootstrap.php +++ b/src/OpenStack/Bootstrap.php @@ -23,6 +23,8 @@ namespace OpenStack; use OpenStack\Identity\v2\IdentityService; +use OpenStack\ObjectStore\v1\Resource\StreamWrapper; +use OpenStack\ObjectStore\v1\Resource\StreamWrapperFS; /** * Bootstrapping services. @@ -50,7 +52,7 @@ use OpenStack\Identity\v2\IdentityService; * '\OpenStack\Common\Transport\GuzzleClient', + * 'transport' => 'OpenStack\Common\Transport\Guzzle\GuzzleAdapter', * // Set the HTTP max wait time to 500 seconds. * 'transport.timeout' => 500, * ); @@ -81,9 +83,11 @@ use OpenStack\Identity\v2\IdentityService; */ class Bootstrap { + const VERSION = '0.0.1'; + public static $config = array( // The transport implementation. By default, we use the Guzzle Client - 'transport' => '\OpenStack\Common\Transport\GuzzleClient', + 'transport' => 'OpenStack\Common\Transport\Guzzle\GuzzleAdapter', ); /** @@ -119,17 +123,29 @@ class Bootstrap */ public static function useStreamWrappers() { - $swift = stream_wrapper_register( - \OpenStack\ObjectStore\v1\ObjectStorage\StreamWrapper::DEFAULT_SCHEME, - '\OpenStack\ObjectStore\v1\ObjectStorage\StreamWrapper' + self::enableStreamWrapper( + StreamWrapper::DEFAULT_SCHEME, + 'OpenStack\ObjectStore\v1\Resource\StreamWrapper' ); - - $swiftfs = stream_wrapper_register( - \OpenStack\ObjectStore\v1\ObjectStorage\StreamWrapperFS::DEFAULT_SCHEME, - '\OpenStack\ObjectStore\v1\ObjectStorage\StreamWrapperFS' + self::enableStreamWrapper( + StreamWrapperFS::DEFAULT_SCHEME, + 'OpenStack\ObjectStore\v1\Resource\StreamWrapperFS' ); + } - return ($swift && $swiftfs); + /** + * Register a stream wrapper according to its scheme and class + * + * @param $scheme Stream wrapper's scheme + * @param $class The class that contains stream wrapper functionality + */ + private static function enableStreamWrapper($scheme, $class) + { + if (in_array($scheme, stream_get_wrappers())) { + stream_wrapper_unregister($scheme); + } + + stream_wrapper_register($scheme, $class); } /** @@ -144,7 +160,7 @@ class Bootstrap * Common configuration directives: * * - 'transport': The namespaced classname for the transport that - * should be used. Example: \OpenStack\Common\Transport\GuzzleClient + * should be used. Example: \OpenStack\Common\Transport\Guzzle\GuzzleAdapter * - 'transport.debug': The integer 1 for enabling debug, 0 for * disabling. Enabling will turn on verbose debugging output * for any transport that supports it. @@ -276,10 +292,10 @@ class Bootstrap $options['proxy'] = $proxy; } - $klass = self::config('transport'); - self::$transport = new $klass($options); + $class = self::config('transport'); + self::$transport = $class::create($options); } return self::$transport; } -} +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/AbstractClient.php b/src/OpenStack/Common/Transport/AbstractClient.php new file mode 100644 index 0000000..2a7198b --- /dev/null +++ b/src/OpenStack/Common/Transport/AbstractClient.php @@ -0,0 +1,52 @@ +send($this->createRequest('GET', $uri, null, $options)); + } + + public function head($uri, array $options = []) + { + return $this->send($this->createRequest('HEAD', $uri, null, $options)); + } + + public function post($uri, $body = null, array $options = []) + { + return $this->send($this->createRequest('POST', $uri, $body, $options)); + } + + public function put($uri, $body = null, array $options = []) + { + return $this->send($this->createRequest('PUT', $uri, $body, $options)); + } + + public function delete($uri, array $options = []) + { + return $this->send($this->createRequest('DELETE', $uri, null, $options)); + } +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/ClientInterface.php b/src/OpenStack/Common/Transport/ClientInterface.php index cd2203e..de18314 100644 --- a/src/OpenStack/Common/Transport/ClientInterface.php +++ b/src/OpenStack/Common/Transport/ClientInterface.php @@ -14,9 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. ============================================================================ */ -/** - * This file contains the interface for transporters. - */ namespace OpenStack\Common\Transport; @@ -24,7 +21,7 @@ namespace OpenStack\Common\Transport; * Describes a transport client. * * Transport clients are responsible for moving data from the remote cloud to - * the local host. Transport clinets are responsible only for the transport + * the local host. Transport clients are responsible only for the transport * protocol, not for the payloads. * * The current OpenStack services implementation is oriented toward @@ -34,91 +31,106 @@ namespace OpenStack\Common\Transport; */ interface ClientInterface { - const HTTP_USER_AGENT = 'OpenStack-PHP/1.0'; + /** + * Create a new Request object. To send, use the {see send()} method. + * + * @param string $method HTTP method + * @param string|array|\OpenStack\Common\Transport\Url $uri URL the request will send to + * @param string|resource $body Entity body being sent + * @param array $options Configuration options, such as headers + * + * @return \OpenStack\Common\Transport\RequestInterface + */ + public function createRequest($method, + $uri = null, + $body = null, + array $options = []); /** - * Setup for the HTTP Client. + * Sends a request. * - * @param array $options Options for the HTTP Client including: - * - headers (array) A key/value mapping of default headers for each request. - * - proxy (string) A proxy specified as a URI. - * - debug (bool) True if debug output should be displayed. - * - timeout (int) The timeout, in seconds, a request should wait before - * timing out. - * - ssl_verify (bool|string) True, the default, verifies the SSL certificate, - * false disables verification, and a string is the path to a CA to verify - * against. + * @param \OpenStack\Common\Transport\RequestInterface $request Request to execute + * + * @return \OpenStack\Common\Transport\ResponseInterface */ - public function __construct(array $options = []); + public function send(RequestInterface $request); /** - * Perform a request. + * Execute a GET request. * - * Invoking this method causes a single request to be relayed over the - * transporter. The transporter MUST be capable of handling multiple - * invocations of a doRequest() call. + * @param string|array|\OpenStack\Common\Transport\Url $uri URL the request will send to + * @param array $options Configuration options, such as headers * - * @param string $uri The target URI. - * @param string $method The method to be sent. - * @param array $headers An array of name/value header pairs. - * @param string $body The string containing the request body. - * - * @return \OpenStack\Common\Transport\ResponseInterface The response. The response - * is implicit rather than explicit. The interface is based on a draft for - * messages from PHP FIG. Individual implementing libraries will have their - * own reference to interfaces. For example, see Guzzle. - * - * @throws \OpenStack\Common\Transport\Exception\ForbiddenException - * @throws \OpenStack\Common\Transport\Exception\UnauthorizedException - * @throws \OpenStack\Common\Transport\Exception\FileNotFoundException - * @throws \OpenStack\Common\Transport\Exception\MethodNotAllowedException - * @throws \OpenStack\Common\Transport\Exception\ConflictException - * @throws \OpenStack\Common\Transport\Exception\LengthRequiredException - * @throws \OpenStack\Common\Transport\Exception\UnprocessableEntityException - * @throws \OpenStack\Common\Transport\Exception\ServerException - * @throws \OpenStack\Common\Exception + * @return \OpenStack\Common\Transport\ResponseInterface */ - public function doRequest($uri, $method = 'GET', array $headers = [], $body = ''); + public function get($uri, array $options = []); /** - * Perform a request, but use a resource to read the body. + * Execute a HEAD request. * - * This is a special version of the doRequest() function. - * It handles a very spefic case where... + * @param string|array|\OpenStack\Common\Transport\Url $uri URL the request will send to + * @param array $options Configuration options, such as headers * - * - The HTTP verb requires a body (viz. PUT, POST) - * - The body is in a resource, not a string - * - * Examples of appropriate cases for this variant: - * - * - Uploading large files. - * - Streaming data out of a stream and into an HTTP request. - * - Minimizing memory usage ($content strings are big). - * - * Note that all parameters are required. - * - * @param string $uri The target URI. - * @param string $method The method to be sent. - * @param array $headers An array of name/value header pairs. - * @param mixed $resource The string with a file path or a stream URL; or a - * file object resource. If it is a string, then it will be opened with the - * default context. So if you need a special context, you should open the - * file elsewhere and pass the resource in here. - * - * @return \OpenStack\Common\Transport\ResponseInterface The response. The response - * is implicit rather than explicit. The interface is based on a draft for - * messages from PHP FIG. Individual implementing libraries will have their - * own reference to interfaces. For example, see Guzzle. - * - * @throws \OpenStack\Common\Transport\Exception\ForbiddenException - * @throws \OpenStack\Common\Transport\Exception\UnauthorizedException - * @throws \OpenStack\Common\Transport\Exception\FileNotFoundException - * @throws \OpenStack\Common\Transport\Exception\MethodNotAllowedException - * @throws \OpenStack\Common\Transport\Exception\ConflictException - * @throws \OpenStack\Common\Transport\Exception\LengthRequiredException - * @throws \OpenStack\Common\Transport\Exception\UnprocessableEntityException - * @throws \OpenStack\Common\Transport\Exception\ServerException - * @throws \OpenStack\Common\Exception + * @return \OpenStack\Common\Transport\ResponseInterface */ - public function doRequestWithResource($uri, $method, array $headers = [], $resource); -} + public function head($uri, array $options = []); + + /** + * Execute a POST request. + * + * @param string|array|\OpenStack\Common\Transport\Url $uri URL the request will send to + * @param mixed $body Entity body being sent + * @param array $options Configuration options, such as headers + * + * @return \OpenStack\Common\Transport\ResponseInterface + */ + public function post($uri, $body, array $options = []); + + /** + * Execute a PUT request. + * + * @param string|array|\OpenStack\Common\Transport\Url $uri URL the request will send to + * @param mixed $body Entity body being sent + * @param array $options Configuration options, such as headers + * + * @return \OpenStack\Common\Transport\ResponseInterface + */ + public function put($uri, $body, array $options = []); + + /** + * Execute a DELETE request. + * + * @param string|array|\OpenStack\Common\Transport\Url $uri URL the request will send to + * @param array $options Configuration options, such as headers + * + * @return \OpenStack\Common\Transport\ResponseInterface + */ + public function delete($uri, array $options = []); + + /** + * Sets a particular configuration option, depending on how the client + * implements it. It could, for example, alter cURL configuration or a + * default header. + * + * @param string $key The key being updated + * @param mixed $value The value being set + */ + public function setOption($key, $value); + + /** + * Returns the value of a particular configuration option. If the options + * is not set, NULL is returned. + * + * @param string $key The option name + * + * @return mixed|null + */ + public function getOption($key); + + /** + * Returns the base URL that the client points towards. + * + * @return string + */ + public function getBaseUrl(); +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/Exception/ConflictException.php b/src/OpenStack/Common/Transport/Exception/ConflictException.php index a6d8e36..5d4dbf8 100644 --- a/src/OpenStack/Common/Transport/Exception/ConflictException.php +++ b/src/OpenStack/Common/Transport/Exception/ConflictException.php @@ -16,11 +16,14 @@ ============================================================================ */ namespace OpenStack\Common\Transport\Exception; + /** - * Represents an HTTP 409 error. + * Exception that represents a 409 Conflict HTTP error. * - * During DELETE requests, this occurs when a remote resource cannot be - * deleted because the resource is not empty or deleteable. (viz. - * containers). + * This class is thrown when a request could not be completed due to a conflict + * with the current state of the API resource. For example, when a remote + * container cannot be deleted because it is not empty. */ -class ConflictException extends \OpenStack\Common\Exception {} +class ConflictException extends RequestException +{ +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/Exception/FileNotFoundException.php b/src/OpenStack/Common/Transport/Exception/FileNotFoundException.php deleted file mode 100644 index 0f7e194..0000000 --- a/src/OpenStack/Common/Transport/Exception/FileNotFoundException.php +++ /dev/null @@ -1,22 +0,0 @@ -getStatusCode()); + + $this->request = $request; + $this->response = $response; + } + + /** + * Factory method that creates an appropriate Exception object based on the + * Response's status code. The message is constructed here also. + * + * @param \OpenStack\Common\Transport\RequestInterface $request The failed request + * @param \OpenStack\Common\Transport\ResponseInterface $response The API's response + * @return self + */ + public static function create(RequestInterface $request, ResponseInterface $response) + { + $label = 'A HTTP error occurred'; + + $status = $response->getStatusCode(); + + $exceptions = [ + 401 => 'UnauthorizedException', + 403 => 'ForbiddenException', + 404 => 'ResourceNotFoundException', + 405 => 'MethodNotAllowedException', + 409 => 'ConflictException', + 411 => 'LengthRequiredException', + 422 => 'UnprocessableEntityException', + 500 => 'ServerException' + ]; + + $message = sprintf( + "%s\n[Status] %s (%s)\n[URL] %s\n[Message] %s\n", $label, + (string) $request->getUrl(), + $status, $response->getReasonPhrase(), + (string) $response->getBody() + ); + + // Find custom exception class or use default + $exceptionClass = isset($exceptions[$status]) + ? sprintf("%s\\%s", __NAMESPACE__, $exceptions[$status]) + : __CLASS__; + + return new $exceptionClass($message, $request, $response); + } + + /** + * Returns the server response. + * + * @return \OpenStack\Common\Transport\ResponseInterface + */ + public function getResponse() + { + return $this->response; + } + + /** + * Returns the request that caused error. + * + * @return \OpenStack\Common\Transport\RequestInterface + */ + public function getRequest() + { + return $this->request; + } +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/Exception/AuthorizationException.php b/src/OpenStack/Common/Transport/Exception/ResourceNotFoundException.php similarity index 80% rename from src/OpenStack/Common/Transport/Exception/AuthorizationException.php rename to src/OpenStack/Common/Transport/Exception/ResourceNotFoundException.php index 81bf7ad..15f7559 100644 --- a/src/OpenStack/Common/Transport/Exception/AuthorizationException.php +++ b/src/OpenStack/Common/Transport/Exception/ResourceNotFoundException.php @@ -14,13 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. ============================================================================ */ -/** - * The authorization exception. - */ + namespace OpenStack\Common\Transport\Exception; + /** - * Thrown when an access constraint is not met. + * Exception that represents a 404 Not Found HTTP error. * - * Represents an HTTP 401 or 403 exception. + * This class is thrown when a server has not found any resource matching the + * Request's URI. */ -class AuthorizationException extends \OpenStack\Common\Exception {} +class ResourceNotFoundException extends RequestException +{ +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/Exception/ServerException.php b/src/OpenStack/Common/Transport/Exception/ServerException.php index 848ab6c..cb7ff38 100644 --- a/src/OpenStack/Common/Transport/Exception/ServerException.php +++ b/src/OpenStack/Common/Transport/Exception/ServerException.php @@ -16,7 +16,14 @@ ============================================================================ */ namespace OpenStack\Common\Transport\Exception; + /** - * Represents an HTTP 500 error. + * Exception that represents a 500 Internal Server Error. + * + * This class is thrown when a server encounters an unexpected condition which + * prevents it from fulfilling the request. Sometimes this error is used as a + * generic catch-all by an OpenStack API. */ -class ServerException extends \OpenStack\Common\Exception {} +class ServerException extends RequestException +{ +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/Exception/UnauthorizedException.php b/src/OpenStack/Common/Transport/Exception/UnauthorizedException.php index f1306e3..d18c128 100644 --- a/src/OpenStack/Common/Transport/Exception/UnauthorizedException.php +++ b/src/OpenStack/Common/Transport/Exception/UnauthorizedException.php @@ -18,9 +18,13 @@ * The authorization exception. */ namespace OpenStack\Common\Transport\Exception; + /** - * Thrown when authorization fails. + * Exception that represents a 401 Unauthorized HTTP error. * - * Represents an HTTP 401 exception. + * This class is thrown when a server indicates that authorization has been + * refused for a set of credentials. */ -class UnauthorizedException extends AuthorizationException {} +class UnauthorizedException extends RequestException +{ +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/Exception/UnprocessableEntityException.php b/src/OpenStack/Common/Transport/Exception/UnprocessableEntityException.php index ecd5c87..90e2f9c 100644 --- a/src/OpenStack/Common/Transport/Exception/UnprocessableEntityException.php +++ b/src/OpenStack/Common/Transport/Exception/UnprocessableEntityException.php @@ -16,10 +16,13 @@ ============================================================================ */ namespace OpenStack\Common\Transport\Exception; + /** - * Represents an HTTP 422 error. + * Exception that represents a 422 Unprocessable Entity HTTP error. * - * This often represents a case where a checksum or hash did not match - * the generated checksum on the remote end. + * This class is thrown when a request was well-formed but was unable to be + * processed due to semantic errors. */ -class UnprocessableEntityException extends \OpenStack\Common\Exception {} +class UnprocessableEntityException extends RequestException +{ +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/Guzzle/GuzzleAdapter.php b/src/OpenStack/Common/Transport/Guzzle/GuzzleAdapter.php new file mode 100644 index 0000000..3e1f976 --- /dev/null +++ b/src/OpenStack/Common/Transport/Guzzle/GuzzleAdapter.php @@ -0,0 +1,157 @@ + false, + 'subscribers' => [new HttpError()], + 'headers' => ['User-Agent' => self::getDefaultUserAgent()] + ]; + + // Inject client and pass in options for adapter + return new self(new Client($options)); + } + + /** + * Instantiate a new Adapter which wraps a Guzzle client. + * + * @param \GuzzleHttp\ClientInterface $guzzle The Client being wrapped + */ + public function __construct(GuzzleClientInterface $guzzle) + { + $this->client = $guzzle; + } + + public function createRequest($method, $uri = null, $body = null, array $options = []) + { + $headers = isset($options['headers']) ? $options['headers'] : []; + + $request = $this->client->createRequest($method, $uri, [ + 'headers' => $headers, + 'body' => $body, + ]); + + return new RequestAdapter($request); + } + + /** + * @inheritDoc + * @param \OpenStack\Common\Transport\RequestInterface $adapter + * @return \OpenStack\Common\Transport\ResponseInterface + * @throws \OpenStack\Common\Transport\Exception\RequestException + * @throws \GuzzleHttp\Exception\RequestException + */ + public function send(RequestInterface $adapter) + { + try { + $guzzleResponse = $this->client->send($adapter->getMessage()); + return new ResponseAdapter($guzzleResponse); + } catch (GuzzleRequestException $e) { + // In order to satisfy {@see GuzzleHttp\ClientInterface}, Guzzle + // wraps all exceptions in its own RequestException class. This is + // not useful for our end-users, so we need to make sure our own + // versions are returned (Guzzle buffers them). + $previous = $e->getPrevious(); + if ($previous instanceof Exception\RequestException) { + throw $previous; + } + throw $e; + } + } + + /** + * Guzzle handles options using the defaults/ prefix. So if a key is passed + * in to be set, or got, that contains this prefix - assume that its a + * Guzzle option, not an adapter one. + * + * @inheritDoc + */ + public function setOption($key, $value) + { + $this->client->setDefaultOption($key, $value); + } + + /** + * Guzzle handles options using the defaults/ prefix. So if a key is passed + * in to be set, or got, that contains this prefix - assume that its a + * Guzzle option, not an adapter one. + * + * @inheritDoc + */ + public function getOption($key) + { + if ($key == 'base_url') { + return $this->getBaseUrl(); + } else { + return $this->client->getDefaultOption($key); + } + } + + public function getBaseUrl() + { + return $this->client->getBaseUrl(); + } + + /** + * Prepends the SDK's version number to the standard Guzzle string. + * + * @return string + */ + public static function getDefaultUserAgent() + { + return sprintf("OpenStack/%f %s", Bootstrap::VERSION, Client::getDefaultUserAgent()); + } +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/Guzzle/HttpError.php b/src/OpenStack/Common/Transport/Guzzle/HttpError.php new file mode 100644 index 0000000..686261c --- /dev/null +++ b/src/OpenStack/Common/Transport/Guzzle/HttpError.php @@ -0,0 +1,56 @@ + ['onComplete', RequestEvents::VERIFY_RESPONSE]]; + } + + /** + * When a request completes, this method is executed. Because this class + * checks for HTTP errors and handles them, this method checks the HTTP + * status code and invokes {@see RequestException} if necessary. + * + * @param CompleteEvent $event + * @throws \OpenStack\Common\Transport\Exception\RequestException + */ + public function onComplete(CompleteEvent $event) + { + $status = (int) $event->getResponse()->getStatusCode(); + + // Has an error occurred (4xx or 5xx status)? + if ($status >= 400 && $status <= 505) { + $request = new RequestAdapter($event->getRequest()); + $response = new ResponseAdapter($event->getResponse()); + throw RequestException::create($request, $response); + } + } +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/Guzzle/MessageAdapter.php b/src/OpenStack/Common/Transport/Guzzle/MessageAdapter.php new file mode 100644 index 0000000..3d7f8ee --- /dev/null +++ b/src/OpenStack/Common/Transport/Guzzle/MessageAdapter.php @@ -0,0 +1,117 @@ +setMessage($guzzleMessage); + } + + /** + * This sets the Guzzle object being wrapped. + * + * @param \GuzzleHttp\Message\MessageInterface $guzzleMessage The object being wrapped. + */ + public function setMessage(GuzzleMessageInterface $guzzleMessage) + { + $this->message = $guzzleMessage; + } + + /** + * @return \GuzzleHttp\Message\MessageInterface + */ + public function getMessage() + { + return $this->message; + } + + public function getProtocolVersion() + { + return $this->message->getProtocolVersion(); + } + + public function getBody() + { + return $this->message->getBody(); + } + + public function setBody(/* StreamInterface */ $body = null) + { + $this->message->setBody($body); + } + + public function getHeaders() + { + return $this->message->getHeaders(); + } + + public function hasHeader($header) + { + return $this->message->hasHeader($header); + } + + public function getHeader($header, $asArray = false) + { + return $this->message->getHeader($header, $asArray); + } + + public function setHeader($header, $value) + { + $this->message->setHeader($header, $value); + } + + public function setHeaders(array $headers) + { + $this->message->setHeaders($headers); + } + + public function addHeader($header, $value) + { + $this->message->addHeader($header, $value); + } + + public function addHeaders(array $headers) + { + $this->message->addHeaders($headers); + } + + public function removeHeader($header) + { + $this->message->removeHeader($header); + } +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/Guzzle/RequestAdapter.php b/src/OpenStack/Common/Transport/Guzzle/RequestAdapter.php new file mode 100644 index 0000000..737c4ce --- /dev/null +++ b/src/OpenStack/Common/Transport/Guzzle/RequestAdapter.php @@ -0,0 +1,54 @@ +setMessage($guzzleRequest); + } + + public function getMethod() + { + return $this->message->getMethod(); + } + + public function setMethod($method) + { + $this->message->setMethod($method); + } + + public function getUrl() + { + return $this->message->getUrl(); + } + + public function setUrl($url) + { + $this->message->setUrl($url); + } +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/Guzzle/ResponseAdapter.php b/src/OpenStack/Common/Transport/Guzzle/ResponseAdapter.php new file mode 100644 index 0000000..70f3662 --- /dev/null +++ b/src/OpenStack/Common/Transport/Guzzle/ResponseAdapter.php @@ -0,0 +1,49 @@ +setMessage($guzzleResponse); + } + + public function getStatusCode() + { + return $this->message->getStatusCode(); + } + + public function getReasonPhrase() + { + return $this->message->getReasonPhrase(); + } + + public function json() + { + return $this->message->json(); + } +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/GuzzleClient.php b/src/OpenStack/Common/Transport/GuzzleClient.php deleted file mode 100644 index b73616d..0000000 --- a/src/OpenStack/Common/Transport/GuzzleClient.php +++ /dev/null @@ -1,227 +0,0 @@ -options = $options; - - $this->client = $this->setup($options); - } - - /** - * Setup is a protected method to setup the client. - * - * The functionality would typically be in the constructor. It was broken out - * to be used by the constructor and serialization process. - * - * @param array $options The options as passed to the constructor. - * @return mixed The Guzzle based client. - */ - protected function setup(array $options = []) - { - // If no client has been passed in we create one. This is the default case. - if (!isset($options['client']) || is_string($options['client'])) { - $defaultOptions = ['defaults' => []]; - if (isset($options['headers'])) { - $defaultOptions['defaults']['headers'] = $options['headers']; - } - if (isset($options['proxy'])) { - $defaultOptions['defaults']['proxy'] = $options['proxy']; - } - if (isset($options['debug'])) { - $defaultOptions['defaults']['debug'] = $options['debug']; - } - if (isset($options['ssl'])) { - $defaultOptions['defaults']['verify'] = $options['ssl_verify']; - } - if (isset($options['timeout'])) { - $defaultOptions['defaults']['timeout'] = $options['timeout']; - } - - // Add a user agent if not already specificed. - if (!isset($defaultOptions['defaults']['headers']['User-Agent'])) { - $defaultOptions['defaults']['headers']['User-Agent'] = self::HTTP_USER_AGENT . self::HTTP_USER_AGENT_SUFFIX; - } - - $clientClass = '\GuzzleHttp\Client'; - if (isset($options['client']) && is_string($options['client'])) { - $clientClass = $options['client']; - } - - $options['client'] = new $clientClass($defaultOptions); - } - - return $options['client']; - } - - /** - * {@inheritdoc} - */ - public function doRequest($uri, $method = 'GET', array $headers = [], $body = '') - { - $options = [ - 'headers' => $headers, - 'body' => $body, - ]; - - // We use our own exceptions for errors to provide a common exception - // interface to applications implementing the SDK. - try { - $response = $this->client->send($this->client->createRequest($method, $uri, $options)); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->handleException($e); - } catch (\GuzzleHttp\Exception\ServerException $e) { - $this->handleException($e); - } catch (\GuzzleHttp\Exception\RequestException $e) { - $this->handleException($e); - } - - return $response; - } - - /** - * {@inheritdoc} - */ - public function doRequestWithResource($uri, $method, array $headers = [], $resource) - { - // Guzzle messes with the resource in such a manner that it can no longer be - // used by something else after the fact. So, we clone the content into - // temporary stream. - $tmp = $out = fopen('php://temp', 'wb+'); - stream_copy_to_stream($resource, $tmp); - - $options = [ - 'headers' => $headers, - 'body' => $tmp, - ]; - - // We use our own exceptions for errors to provide a common exception - // interface to applications implementing the SDK. - try { - $response = $this->client->send($this->client->createRequest($method, $uri, $options)); - } catch (\GuzzleHttp\Exception\ClientException $e) { - $this->handleException($e); - } catch (\GuzzleHttp\Exception\ServerException $e) { - $this->handleException($e); - } catch (\GuzzleHttp\Exception\RequestException $e) { - $this->handleException($e); - } - - return $response; - } - - /** - * Handle errors on a response. - * - * @param mixed The Guzzle exception. - * - * @return \OpenStack\Common\Transport\ResponseInterface The response. - * - * @throws \OpenStack\Common\Transport\Exception\ForbiddenException - * @throws \OpenStack\Common\Transport\Exception\UnauthorizedException - * @throws \OpenStack\Common\Transport\Exception\FileNotFoundException - * @throws \OpenStack\Common\Transport\Exception\MethodNotAllowedException - * @throws \OpenStack\Common\Transport\Exception\ConflictException - * @throws \OpenStack\Common\Transport\Exception\LengthRequiredException - * @throws \OpenStack\Common\Transport\Exception\UnprocessableEntityException - * @throws \OpenStack\Common\Transport\Exception\ServerException - * @throws \OpenStack\Common\Exception - */ - protected function handleException($exception) - { - $response = $exception->getResponse(); - $request = $exception->getRequest(); - - if (!is_null($response)) { - $code = $response->getStatusCode(); - - switch ($code) { - case '403': - throw new \OpenStack\Common\Transport\Exception\ForbiddenException($response->getReasonPhrase()); - case '401': - throw new \OpenStack\Common\Transport\Exception\UnauthorizedException($response->getReasonPhrase()); - case '404': - throw new \OpenStack\Common\Transport\Exception\FileNotFoundException($response->getReasonPhrase() . " ({$response->getEffectiveUrl()})"); - case '405': - throw new \OpenStack\Common\Transport\Exception\MethodNotAllowedException($response->getReasonPhrase() . " ({$request->getMethod()} {$response->getEffectiveUrl()})"); - case '409': - throw new \OpenStack\Common\Transport\Exception\ConflictException($response->getReasonPhrase()); - case '412': - throw new \OpenStack\Common\Transport\Exception\LengthRequiredException($response->getReasonPhrase()); - case '422': - throw new \OpenStack\Common\Transport\Exception\UnprocessableEntityException($response->getReasonPhrase()); - case '500': - throw new \OpenStack\Common\Transport\Exception\ServerException($response->getReasonPhrase()); - default: - throw new \OpenStack\Common\Exception($response->getReasonPhrase()); - } - } - // The exception was one other than a HTTP error. For example, a HTTP layer - // timeout occurred. - else { - throw new \OpenStack\Common\Exception($exception->getMessage()); - } - - return $response; - } - - public function serialize() - { - $data = ['options' => $this->options]; - - return serialize($data); - } - - public function unserialize($data) - { - $vals = unserialize($data); - $this->options = $vals['options']; - $this->client = $this->setup($vals['options']); - } -} diff --git a/src/OpenStack/Common/Transport/MessageInterface.php b/src/OpenStack/Common/Transport/MessageInterface.php new file mode 100644 index 0000000..8a688cd --- /dev/null +++ b/src/OpenStack/Common/Transport/MessageInterface.php @@ -0,0 +1,159 @@ +getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * @return array Returns an associative array of the message's headers. + */ + public function getHeaders(); + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $header Case-insensitive header name. + * + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader($header); + + /** + * Retrieve a header by the given case-insensitive name. + * + * By default, this method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. Because some header should not be concatenated together using a + * comma, this method provides a Boolean argument that can be used to + * retrieve the associated header values as an array of strings. + * + * @param string $header Case-insensitive header name. + * @param bool $asArray Set to true to retrieve the header value as an + * array of strings. + * + * @return array|string + */ + public function getHeader($header, $asArray = false); + + /** + * Sets a header, replacing any existing values of any headers with the + * same case-insensitive name. + * + * The header values MUST be a string or an array of strings. + * + * @param string $header Header name + * @param string|array $value Header value(s) + * + * @return self Returns the message. + */ + public function setHeader($header, $value); + + /** + * Sets headers, replacing any headers that have already been set on the + * message. + * + * The array keys MUST be a string. The array values must be either a + * string or an array of strings. + * + * @param array $headers Headers to set. + * + * @return self Returns the message. + */ + public function setHeaders(array $headers); + + /** + * Appends a header value to any existing values associated with the + * given header name. + * + * @param string $header Header name to add + * @param string $value Value of the header + * + * @return self + */ + public function addHeader($header, $value); + + /** + * Merges in an associative array of headers. + * + * Each array key MUST be a string representing the case-insensitive name + * of a header. Each value MUST be either a string or an array of strings. + * For each value, the value is appended to any existing header of the same + * name, or, if a header does not already exist by the given name, then the + * header is added. + * + * @param array $headers Associative array of headers to add to the message + * + * @return self + */ + public function addHeaders(array $headers); + + /** + * Remove a specific header by case-insensitive name. + * + * @param string $header HTTP header to remove + * + * @return self + */ + public function removeHeader($header); +} \ No newline at end of file diff --git a/src/OpenStack/Common/Transport/RequestInterface.php b/src/OpenStack/Common/Transport/RequestInterface.php new file mode 100644 index 0000000..bba5e9e --- /dev/null +++ b/src/OpenStack/Common/Transport/RequestInterface.php @@ -0,0 +1,65 @@ +getHeaders() as $name => $values) { - * echo $name . ": " . implode(", ", $values); - * } - * - * @return array Returns an associative array of the message's headers. - */ - public function getHeaders(); - - /** - * Checks if a header exists by the given case-insensitive name. - * - * @param string $header Case-insensitive header name. - * - * @return bool Returns true if any header names match the given header - * name using a case-insensitive string comparison. Returns false if - * no matching header name is found in the message. - */ - public function hasHeader($header); - - /** - * Retrieve a header by the given case-insensitive name. - * - * By default, this method returns all of the header values of the given - * case-insensitive header name as a string concatenated together using - * a comma. Because some header should not be concatenated together using a - * comma, this method provides a Boolean argument that can be used to - * retrieve the associated header values as an array of strings. - * - * @param string $header Case-insensitive header name. - * @param bool $asArray Set to true to retrieve the header value as an - * array of strings. - * - * @return array|string - */ - public function getHeader($header, $asArray = false); - - /** - * Sets a header, replacing any existing values of any headers with the - * same case-insensitive name. - * - * The header values MUST be a string or an array of strings. - * - * @param string $header Header name - * @param string|array $value Header value(s) - * - * @return self Returns the message. - */ - public function setHeader($header, $value); - - /** - * Sets headers, replacing any headers that have already been set on the - * message. - * - * The array keys MUST be a string. The array values must be either a - * string or an array of strings. - * - * @param array $headers Headers to set. - * - * @return self Returns the message. - */ - public function setHeaders(array $headers); - - /** - * Appends a header value to any existing values associated with the - * given header name. - * - * @param string $header Header name to add - * @param string $value Value of the header - * - * @return self - */ - public function addHeader($header, $value); - - /** - * Merges in an associative array of headers. - * - * Each array key MUST be a string representing the case-insensitive name - * of a header. Each value MUST be either a string or an array of strings. - * For each value, the value is appended to any existing header of the same - * name, or, if a header does not already exist by the given name, then the - * header is added. - * - * @param array $headers Associative array of headers to add to the message - * - * @return self - */ - public function addHeaders(array $headers); - - /** - * Remove a specific header by case-insensitive name. - * - * @param string $header HTTP header to remove - * - * @return self - */ - public function removeHeader($header); -} +} \ No newline at end of file diff --git a/src/OpenStack/Identity/v2/IdentityService.php b/src/OpenStack/Identity/v2/IdentityService.php index d60ad91..ea5d013 100644 --- a/src/OpenStack/Identity/v2/IdentityService.php +++ b/src/OpenStack/Identity/v2/IdentityService.php @@ -19,8 +19,8 @@ */ namespace OpenStack\Identity\v2; - -use OpenStack\Common\Transport\GuzzleClient; +use OpenStack\Common\Transport\ClientInterface; +use OpenStack\Common\Transport\Guzzle\GuzzleAdapter; /** * IdentityService provides authentication and authorization. @@ -203,7 +203,7 @@ class IdentityService * * @param \OpenStack\Common\Transport\ClientInterface $client An optional HTTP client to use when making the requests. */ - public function __construct($url, \OpenStack\Common\Transport\ClientInterface $client = null) + public function __construct($url, ClientInterface $client = null) { $parts = parse_url($url); @@ -215,7 +215,7 @@ class IdentityService // Guzzle is the default client to use. if (is_null($client)) { - $this->client = new GuzzleClient(); + $this->client = GuzzleAdapter::create(); } else { $this->client = $client; } @@ -278,13 +278,13 @@ class IdentityService $body = json_encode($envelope); - $headers = array( - 'Content-Type' => 'application/json', - 'Accept' => self::ACCEPT_TYPE, + $headers = [ + 'Content-Type' => 'application/json', + 'Accept' => self::ACCEPT_TYPE, 'Content-Length' => strlen($body), - ); + ]; - $response = $this->client->doRequest($url, 'POST', $headers, $body); + $response = $this->client->post($url, $body, ['headers' => $headers]); $this->handleResponse($response); @@ -327,7 +327,7 @@ class IdentityService 'passwordCredentials' => array( 'username' => $username, 'password' => $password, - ), + ) ); // If a tenant ID is provided, added it to the auth array. @@ -599,18 +599,14 @@ class IdentityService $token = $this->token(); } - $headers = array( + $headers = [ 'X-Auth-Token' => $token, - 'Accept' => 'application/json', - //'Content-Type' => 'application/json', - ); + 'Accept' => 'application/json' + ]; - $response = $this->client->doRequest($url, 'GET', $headers); - - $json = $response->json(); - - return $json['tenants']; + $response = $this->client->get($url, ['headers' => $headers]); + return $response->json()['tenants']; } /** @@ -644,25 +640,24 @@ class IdentityService public function rescopeUsingTenantId($tenantId) { $url = $this->url() . '/tokens'; - $token = $this->token(); - $data = array( - 'auth' => array( + + $body = json_encode([ + 'auth' => [ 'tenantId' => $tenantId, - 'token' => array( - 'id' => $token, - ), - ), - ); - $body = json_encode($data); + 'token' => [ + 'id' => $this->token(), + ] + ] + ]); - $headers = array( - 'Accept' => self::ACCEPT_TYPE, - 'Content-Type' => 'application/json', - 'Content-Length' => strlen($body), - //'X-Auth-Token' => $token, - ); + $headers = [ + 'Accept' => self::ACCEPT_TYPE, + 'Content-Type' => 'application/json', + 'Content-Length' => strlen($body) + ]; + + $response = $this->client->post($url, $body, ['headers' => $headers]); - $response = $this->client->doRequest($url, 'POST', $headers, $body); $this->handleResponse($response); return $this->token(); @@ -699,25 +694,24 @@ class IdentityService public function rescopeUsingTenantName($tenantName) { $url = $this->url() . '/tokens'; - $token = $this->token(); - $data = array( - 'auth' => array( + + $body = json_encode([ + 'auth' => [ 'tenantName' => $tenantName, - 'token' => array( - 'id' => $token, - ), - ), - ); - $body = json_encode($data); + 'token' => [ + 'id' => $this->token() + ] + ] + ]); - $headers = array( - 'Accept' => self::ACCEPT_TYPE, - 'Content-Type' => 'application/json', - 'Content-Length' => strlen($body), - //'X-Auth-Token' => $token, - ); + $headers = [ + 'Accept' => self::ACCEPT_TYPE, + 'Content-Type' => 'application/json', + 'Content-Length' => strlen($body) + ]; + + $response = $this->client->post($url, $body, ['headers' => $headers]); - $response = $this->client->doRequest($url, 'POST', $headers, $body); $this->handleResponse($response); return $this->token(); diff --git a/src/OpenStack/ObjectStore/v1/ObjectStorage.php b/src/OpenStack/ObjectStore/v1/ObjectStorage.php index 0b7540e..3687e66 100644 --- a/src/OpenStack/ObjectStore/v1/ObjectStorage.php +++ b/src/OpenStack/ObjectStore/v1/ObjectStorage.php @@ -25,9 +25,14 @@ namespace OpenStack\ObjectStore\v1; +use OpenStack\Common\Exception; +use OpenStack\Common\Transport\ClientInterface; +use OpenStack\Common\Transport\Exception\ConflictException; +use OpenStack\Common\Transport\Exception\ResourceNotFoundException; +use OpenStack\Common\Transport\Guzzle\GuzzleAdapter; +use OpenStack\ObjectStore\v1\Exception\ContainerNotEmptyException; use OpenStack\ObjectStore\v1\Resource\Container; use OpenStack\ObjectStore\v1\Resource\ACL; -use OpenStack\Common\Transport\GuzzleClient; /** * Access to ObjectStorage (Swift). @@ -126,9 +131,7 @@ class ObjectStorage if ($catalog[$i]['type'] == self::SERVICE_TYPE) { foreach ($catalog[$i]['endpoints'] as $endpoint) { if (isset($endpoint['publicURL']) && $endpoint['region'] == $region) { - $os = new ObjectStorage($authToken, $endpoint['publicURL'], $client); - - return $os; + return new ObjectStorage($authToken, $endpoint['publicURL'], $client); } } } @@ -150,14 +153,14 @@ class ObjectStorage * after authentication. * @param \OpenStack\Common\Transport\ClientInterface $client The HTTP client */ - public function __construct($authToken, $url, \OpenStack\Common\Transport\ClientInterface $client = null) + public function __construct($authToken, $url, ClientInterface $client = null) { $this->token = $authToken; $this->url = $url; // Guzzle is the default client to use. if (is_null($client)) { - $this->client = new GuzzleClient(); + $this->client = GuzzleAdapter::create(); } else { $this->client = $client; } @@ -226,7 +229,9 @@ class ObjectStorage $url .= sprintf('&marker=%d', $marker); } - $containers = $this->get($url); + $headers = ['X-Auth-Token' => $this->token]; + $response = $this->client->get($url, ['headers' => $headers]); + $containers = $response->json(); $containerList = array(); foreach ($containers as $container) { @@ -246,23 +251,24 @@ class ObjectStorage * * @return \OpenStack\ObjectStore\v1\Resource\Container A container. * - * @throws \OpenStack\Common\Transport\Exception\FileNotFoundException if the named container is not + * @throws \OpenStack\Common\Transport\Exception\ResourceNotFoundException if the named container is not * found on the remote server. */ public function container($name) { $url = $this->url() . '/' . rawurlencode($name); - $data = $this->req($url, 'HEAD', false); - $status = $data->getStatusCode(); + $headers = ['X-Auth-Token' => $this->token()]; + $response = $this->client->head($url, ['headers' => $headers]); + + $status = $response->getStatusCode(); + if ($status == 204) { - $container = Container::newFromResponse($name, $data, $this->token(), $this->url()); - - return $container; + return Container::newFromResponse($name, $response, $this->token(), $this->url()); } // If we get here, it's not a 404 and it's not a 204. - throw new \OpenStack\Common\Exception("Unknown status: $status"); + throw new Exception(sprintf("Unknown status: %d", $status)); } /** @@ -282,7 +288,7 @@ class ObjectStorage { try { $container = $this->container($name); - } catch (\OpenStack\Common\Transport\Exception\FileNotFoundException $fnfe) { + } catch (ResourceNotFoundException $e) { return false; } @@ -351,9 +357,7 @@ class ObjectStorage public function createContainer($name, ACL $acl = null, $metadata = array()) { $url = $this->url() . '/' . rawurlencode($name); - $headers = array( - 'X-Auth-Token' => $this->token(), - ); + $headers = ['X-Auth-Token' => $this->token()]; if (!empty($metadata)) { $prefix = Container::CONTAINER_METADATA_HEADER_PREFIX; @@ -365,8 +369,7 @@ class ObjectStorage $headers += $acl->headers(); } - $data = $this->client->doRequest($url, 'PUT', $headers); - //syslog(LOG_WARNING, print_r($data, true)); + $data = $this->client->put($url, null, ['headers' => $headers]); $status = $data->getStatusCode(); @@ -374,10 +377,9 @@ class ObjectStorage return true; } elseif ($status == 202) { return false; - } - // According to the OpenStack docs, there are no other return codes. - else { - throw new \OpenStack\Common\Exception('Server returned unexpected code: ' . $status); + } else { + // According to the OpenStack docs, there are no other return codes. + throw new Exception('Server returned unexpected code: ' . $status); } } @@ -442,14 +444,18 @@ class ObjectStorage $url = $this->url() . '/' . rawurlencode($name); try { - $data = $this->req($url, 'DELETE', false); - } catch (\OpenStack\Common\Transport\Exception\FileNotFoundException $e) { + $headers = ['X-Auth-Token' => $this->token()]; + $data = $this->client->delete($url, ['headers' => $headers]); + } catch (ResourceNotFoundException $e) { return false; - } - // XXX: I'm not terribly sure about this. Why not just throw the - // ConflictException? - catch (\OpenStack\Common\Transport\Exception\ConflictException $e) { - throw new Exception\ContainerNotEmptyException("Non-empty container cannot be deleted."); + } catch (ConflictException $e) { + // XXX: I'm not terribly sure about this. Why not just throw the + // ConflictException? + throw new ContainerNotEmptyException( + "Non-empty container cannot be deleted", + $e->getRequest(), + $e->getResponse() + ); } $status = $data->getStatusCode(); @@ -457,11 +463,9 @@ class ObjectStorage // 204 indicates that the container has been deleted. if ($status == 204) { return true; - } - // OpenStacks documentation doesn't suggest any other return - // codes. - else { - throw new \OpenStack\Common\Exception('Server returned unexpected code: ' . $status); + } else { + // OpenStacks documentation doesn't suggest any other return codes. + throw new Exception('Server returned unexpected code: ' . $status); } } @@ -483,43 +487,13 @@ class ObjectStorage */ public function accountInfo() { - $url = $this->url(); - $data = $this->req($url, 'HEAD', false); + $headers = ['X-Auth-Token' => $this->token()]; + $response = $this->client->head($this->url(), ['headers' => $headers]); - $results = array( - 'bytes' => $data->getHeader('X-Account-Bytes-Used', 0), - 'containers' => $data->getHeader('X-Account-Container-Count', 0), - 'objects' => $data->getHeader('X-Account-Container-Count', 0), - ); - - return $results; - } - - /** - * Do a GET on Swift. - * - * This is a convenience method that handles the - * most common case of Swift requests. - */ - protected function get($url, $jsonDecode = true) - { - return $this->req($url, 'GET', $jsonDecode); - } - - /** - * Internal request issuing command. - */ - protected function req($url, $method = 'GET', $jsonDecode = true, $body = '') - { - $headers = array( - 'X-Auth-Token' => $this->token(), - ); - - $res = $this->client->doRequest($url, $method, $headers, $body); - if (!$jsonDecode) { - return $res; - } - - return $res->json(); + return [ + 'bytes' => $response->getHeader('X-Account-Bytes-Used', 0), + 'containers' => $response->getHeader('X-Account-Container-Count', 0), + 'objects' => $response->getHeader('X-Account-Container-Count', 0) + ]; } } diff --git a/src/OpenStack/ObjectStore/v1/Resource/Container.php b/src/OpenStack/ObjectStore/v1/Resource/Container.php index 48da850..4a83802 100644 --- a/src/OpenStack/ObjectStore/v1/Resource/Container.php +++ b/src/OpenStack/ObjectStore/v1/Resource/Container.php @@ -20,7 +20,10 @@ namespace OpenStack\ObjectStore\v1\Resource; -use OpenStack\Common\Transport\GuzzleClient; +use OpenStack\Common\Exception; +use OpenStack\Common\Transport\ClientInterface; +use OpenStack\Common\Transport\Exception\ResourceNotFoundException; +use OpenStack\Common\Transport\Guzzle\GuzzleAdapter; /** * A container in an ObjectStorage. @@ -77,8 +80,7 @@ class Container implements \Countable, \IteratorAggregate //protected $properties = array(); protected $name = null; - // These were both changed from 0 to null to allow - // lazy loading. + // These were both changed from 0 to null to allow lazy loading. protected $count = null; protected $bytes = null; @@ -209,7 +211,7 @@ class Container implements \Countable, \IteratorAggregate * * @return \OpenStack\ObjectStore\v1\Resource\Container A new container object. */ - public static function newFromJSON($jsonArray, $token, $url, \OpenStack\Common\Transport\ClientInterface $client = null) + public static function newFromJSON($jsonArray, $token, $url, ClientInterface $client = null) { $container = new Container($jsonArray['name'], null, null, $client); @@ -249,7 +251,7 @@ class Container implements \Countable, \IteratorAggregate * * @return \OpenStack\ObjectStore\v1\Resource\Container The Container object, initialized and ready for use. */ - public static function newFromResponse($name, $response, $token, $url, \OpenStack\Common\Transport\ClientInterface $client = null) + public static function newFromResponse($name, $response, $token, $url, ClientInterface $client = null) { $container = new Container($name, null, null, $client); $container->bytes = $response->getHeader('X-Container-Bytes-Used', 0); @@ -310,7 +312,7 @@ class Container implements \Countable, \IteratorAggregate * @param string $token The auth token. * @param \OpenStack\Common\Transport\ClientInterface $client A HTTP transport client. */ - public function __construct($name , $url = null, $token = null, \OpenStack\Common\Transport\ClientInterface $client = null) + public function __construct($name , $url = null, $token = null, ClientInterface $client = null) { $this->name = $name; $this->url = $url; @@ -318,7 +320,7 @@ class Container implements \Countable, \IteratorAggregate // Guzzle is the default client to use. if (is_null($client)) { - $this->client = new GuzzleClient(); + $this->client = GuzzleAdapter::create(); } else { $this->client = $client; } @@ -453,10 +455,10 @@ class Container implements \Countable, \IteratorAggregate public function save(Object $obj, $file = null) { if (empty($this->token)) { - throw new \OpenStack\Common\Exception('Container does not have an auth token.'); + throw new Exception('Container does not have an auth token.'); } if (empty($this->url)) { - throw new \OpenStack\Common\Exception('Container does not have a URL to send data.'); + throw new Exception('Container does not have a URL to send data.'); } //$url = $this->url . '/' . rawurlencode($obj->name()); @@ -507,12 +509,11 @@ class Container implements \Countable, \IteratorAggregate } else { $headers['Content-Length'] = $obj->contentLength(); } - $response = $this->client->doRequest($url, 'PUT', $headers, $obj->content()); + $response = $this->client->put($url, $obj->content(), ['headers' => $headers]); } else { // Rewind the file. rewind($file); - // XXX: What do we do about Content-Length header? //$headers['Transfer-Encoding'] = 'chunked'; $stat = fstat($file); @@ -527,12 +528,11 @@ class Container implements \Countable, \IteratorAggregate // Not sure if this is necessary: rewind($file); - $response = $this->client->doRequestWithResource($url, 'PUT', $headers, $file); - + $response = $this->client->put($url, $file, ['headers' => $headers]); } if ($response->getStatusCode() != 201) { - throw new \OpenStack\Common\Exception('An unknown error occurred while saving: ' . $response->status()); + throw new Exception('An unknown error occurred while saving: ' . $response->status()); } return true; @@ -553,32 +553,33 @@ class Container implements \Countable, \IteratorAggregate * * @return boolean true if the metadata was updated. * - * @throws \OpenStack\Common\Transport\Exception\FileNotFoundException if the object does not already + * @throws \OpenStack\Common\Transport\Exception\ResourceNotFoundException if the object does not already * exist on the object storage. */ public function updateMetadata(Object $obj) { - //$url = $this->url . '/' . rawurlencode($obj->name()); $url = self::objectUrl($this->url, $obj->name()); - $headers = array(); + $headers = ['X-Auth-Token' => $this->token]; // See if we have any metadata. We post this even if there // is no metadata. - $md = $obj->metadata(); - if (!empty($md)) { - $headers = self::generateMetadataHeaders($md, Container::METADATA_HEADER_PREFIX); + $metadata = $obj->metadata(); + if (!empty($metadata)) { + $headers += self::generateMetadataHeaders($metadata, Container::METADATA_HEADER_PREFIX); } - $headers['X-Auth-Token'] = $this->token; // In spite of the documentation's claim to the contrary, // content type IS reset during this operation. $headers['Content-Type'] = $obj->contentType(); // The POST verb is for updating headers. - $response = $this->client->doRequest($url, 'POST', $headers, $obj->content()); + + $response = $this->client->post($url, $obj->content(), ['headers' => $headers]); if ($response->getStatusCode() != 202) { - throw new \OpenStack\Common\Exception('An unknown error occurred while saving: ' . $response->status()); + throw new Exception(sprintf( + "An unknown error occurred while saving: %d", $response->status() + )); } return true; @@ -609,11 +610,10 @@ class Container implements \Countable, \IteratorAggregate */ public function copy(Object $obj, $newName, $container = null) { - //$sourceUrl = $obj->url(); // This doesn't work with Object; only with RemoteObject. $sourceUrl = self::objectUrl($this->url, $obj->name()); if (empty($newName)) { - throw new \OpenStack\Common\Exception("An object name is required to copy the object."); + throw new Exception("An object name is required to copy the object."); } // Figure out what container we store in. @@ -623,16 +623,18 @@ class Container implements \Countable, \IteratorAggregate $container = rawurlencode($container); $destUrl = self::objectUrl('/' . $container, $newName); - $headers = array( + $headers = [ 'X-Auth-Token' => $this->token, - 'Destination' => $destUrl, + 'Destination' => $destUrl, 'Content-Type' => $obj->contentType(), + ]; + + $response = $this->client->send( + $this->client->createRequest('COPY', $sourceUrl, null, ['headers' => $headers]) ); - $response = $this->client->doRequest($sourceUrl, 'COPY', $headers); - if ($response->getStatusCode() != 201) { - throw new \OpenStack\Common\Exception("An unknown condition occurred during copy. " . $response->getStatusCode()); + throw new Exception("An unknown condition occurred during copy. " . $response->getStatusCode()); } return true; @@ -664,15 +666,12 @@ class Container implements \Countable, \IteratorAggregate public function object($name) { $url = self::objectUrl($this->url, $name); - $headers = array(); + $headers = ['X-Auth-Token' => $this->token]; - // Auth token. - $headers['X-Auth-Token'] = $this->token; - - $response = $this->client->doRequest($url, 'GET', $headers); + $response = $this->client->get($url, ['headers' => $headers]); if ($response->getStatusCode() != 200) { - throw new \OpenStack\Common\Exception('An unknown error occurred while saving: ' . $response->status()); + throw new Exception('An unknown error occurred while saving: ' . $response->status()); } $remoteObject = RemoteObject::newFromHeaders($name, self::reformatHeaders($response->getHeaders()), $this->token, $url, $this->client); @@ -712,21 +711,17 @@ class Container implements \Countable, \IteratorAggregate public function proxyObject($name) { $url = self::objectUrl($this->url, $name); - $headers = array( - 'X-Auth-Token' => $this->token, - ); + $headers = ['X-Auth-Token' => $this->token]; - $response = $this->client->doRequest($url, 'HEAD', $headers); + $response = $this->client->head($url, ['headers' => $headers]); if ($response->getStatusCode() != 200) { - throw new \OpenStack\Common\Exception('An unknown error occurred while saving: ' . $response->status()); + throw new Exception('An unknown error occurred while saving: ' . $response->status()); } $headers = self::reformatHeaders($response->getHeaders()); - $obj = RemoteObject::newFromHeaders($name, $headers, $this->token, $url, $this->client); - - return $obj; + return RemoteObject::newFromHeaders($name, $headers, $this->token, $url, $this->client); } /** @@ -758,9 +753,7 @@ class Container implements \Countable, \IteratorAggregate */ public function objects($limit = null, $marker = null) { - $params = array(); - - return $this->objectQuery($params, $limit, $marker); + return $this->objectQuery([], $limit, $marker); } /** @@ -818,10 +811,10 @@ class Container implements \Countable, \IteratorAggregate */ public function objectsWithPrefix($prefix, $delimiter = '/', $limit = null, $marker = null) { - $params = array( - 'prefix' => $prefix, - 'delimiter' => $delimiter, - ); + $params = [ + 'prefix' => $prefix, + 'delimiter' => $delimiter + ]; return $this->objectQuery($params, $limit, $marker); } @@ -862,10 +855,10 @@ class Container implements \Countable, \IteratorAggregate */ public function objectsByPath($path, $delimiter = '/', $limit = null, $marker = null) { - $params = array( - 'path' => $path, + $params = [ + 'path' => $path, 'delimiter' => $delimiter, - ); + ]; return $this->objectQuery($params, $limit, $marker); } @@ -920,18 +913,17 @@ class Container implements \Countable, \IteratorAggregate */ protected function loadExtraData() { - // If URL and token are empty, we are dealing with - // a local item that has not been saved, and was not - // created with Container::createContainer(). We treat - // this as an error condition. + // If URL and token are empty, we are dealing with a local item that + // has not been saved, and was not created with Container::createContainer(). + // We treat this as an error condition. if (empty($this->url) || empty($this->token)) { - throw new \OpenStack\Common\Exception('Remote data cannot be fetched. A Token and endpoint URL are required.'); + throw new Exception('Remote data cannot be fetched. A Token and endpoint URL are required.'); } + // Do a GET on $url to fetch headers. - $headers = array( - 'X-Auth-Token' => $this->token, - ); - $response = $this->client->doRequest($this->url, 'GET', $headers); + $headers = ['X-Auth-Token' => $this->token]; + $response = $this->client->get($this->url, ['headers' => $headers]); + $headers = self::reformatHeaders($response->getHeaders()); // Get ACL. $this->acl = ACL::newFromHeaders($headers); @@ -967,16 +959,14 @@ class Container implements \Countable, \IteratorAggregate $query = str_replace('%2F', '/', $query); $url = $this->url . '?' . $query; - $headers = array( - 'X-Auth-Token' => $this->token, - ); + $headers = ['X-Auth-Token' => $this->token]; - $response = $this->client->doRequest($url, 'GET', $headers); + $response = $this->client->get($url, ['headers' => $headers]); // The only codes that should be returned are 200 and the ones - // already thrown by doRequest. + // already thrown by GET. if ($response->getStatusCode() != 200) { - throw new \OpenStack\Common\Exception('An unknown exception occurred while processing the request.'); + throw new Exception('An unknown exception occurred while processing the request.'); } $json = $response->json(); @@ -987,7 +977,7 @@ class Container implements \Countable, \IteratorAggregate if (!empty($item['subdir'])) { $list[] = new Subdir($item['subdir'], $params['delimiter']); } elseif (empty($item['name'])) { - throw new \OpenStack\Common\Exception('Unexpected entity returned.'); + throw new Exception('Unexpected entity returned.'); } else { //$url = $this->url . '/' . rawurlencode($item['name']); $url = self::objectUrl($this->url, $item['name']); @@ -1044,13 +1034,15 @@ class Container implements \Countable, \IteratorAggregate ); try { - $response = $this->client->doRequest($url, 'DELETE', $headers); - } catch (\OpenStack\Common\Transport\Exception\FileNotFoundException $fnfe) { + $response = $this->client->delete($url, ['headers' => $headers]); + } catch (ResourceNotFoundException $e) { return false; } if ($response->getStatusCode() != 204) { - throw new \OpenStack\Common\Exception("An unknown exception occured while deleting $name."); + throw new Exception(sprintf( + "An unknown exception occured while deleting %s", $name + )); } return true; @@ -1090,5 +1082,4 @@ class Container implements \Countable, \IteratorAggregate return $newHeaders; } - } diff --git a/src/OpenStack/ObjectStore/v1/Resource/RemoteObject.php b/src/OpenStack/ObjectStore/v1/Resource/RemoteObject.php index 2ea8830..5d5a05d 100644 --- a/src/OpenStack/ObjectStore/v1/Resource/RemoteObject.php +++ b/src/OpenStack/ObjectStore/v1/Resource/RemoteObject.php @@ -20,7 +20,8 @@ namespace OpenStack\ObjectStore\v1\Resource; -use OpenStack\Common\Transport\GuzzleClient; +use OpenStack\Common\Transport\ClientInterface; +use OpenStack\Common\Transport\Guzzle\GuzzleAdapter; use OpenStack\ObjectStore\v1\Exception; /** @@ -76,7 +77,7 @@ class RemoteObject extends Object * @param $url The URL to the object on the remote server * @param \OpenStack\Common\Transport\ClientInterface $client A HTTP transport client. */ - public static function newFromJSON($data, $token, $url, \OpenStack\Common\Transport\ClientInterface $client = null) + public static function newFromJSON($data, $token, $url, ClientInterface $client = null) { $object = new RemoteObject($data['name']); $object->setContentType($data['content_type']); @@ -92,7 +93,7 @@ class RemoteObject extends Object // back in JSON? if (is_null($client)) { - $client = new GuzzleClient(); + $client = GuzzleAdapter::create(); } $object->setClient($client); @@ -115,7 +116,7 @@ class RemoteObject extends Object * * @return \OpenStack\ObjectStore\v1\Resource\RemoteObject A new RemoteObject. */ - public static function newFromHeaders($name, $headers, $token, $url, \OpenStack\Common\Transport\ClientInterface $client = null) + public static function newFromHeaders($name, $headers, $token, $url, ClientInterface $client = null) { $object = new RemoteObject($name); @@ -152,7 +153,7 @@ class RemoteObject extends Object $object->url = $url; if (is_null($client)) { - $client = new GuzzleClient(); + $client = GuzzleAdapter::create(); } $object->setClient($client); @@ -164,7 +165,7 @@ class RemoteObject extends Object * * @param OpenStackTransportClientInterface $client The HTTP Client */ - public function setClient(\OpenStack\Common\Transport\ClientInterface $client) + public function setClient(ClientInterface $client) { $this->client = $client; } @@ -348,7 +349,7 @@ class RemoteObject extends Object * * @return string The contents of the file as a string. * - * @throws \OpenStack\Common\Transport\Exception\FileNotFoundException when the requested content cannot be + * @throws \OpenStack\Common\Transport\Exception\ResourceNotFoundException when the requested content cannot be * located on the remote server. * @throws \OpenStack\Common\Exception when an unknown exception (usually an * abnormal network condition) occurs. @@ -370,7 +371,7 @@ class RemoteObject extends Object // Should fix that. $check = md5($content); if ($this->isVerifyingContent() && $check != $this->etag()) { - throw new ContentVerificationException("Checksum $check does not match Etag " . $this->etag()); + throw new Exception\ContentVerificationException("Checksum $check does not match Etag " . $this->etag()); } // If we are caching, set the content locally when we retrieve @@ -624,11 +625,11 @@ class RemoteObject extends Object { $method = $fetchContent ? 'GET' : 'HEAD'; - $headers = array( - 'X-Auth-Token' => $this->token, - ); + $headers = ['X-Auth-Token' => $this->token]; - $response = $this->client->doRequest($this->url, $method, $headers); + $response = $this->client->send( + $this->client->createRequest($method, $this->url, null, ['headers' => $headers]) + ); if ($response->getStatusCode() != 200) { throw new \OpenStack\Common\Exception('An unknown exception occurred during transmission.'); @@ -666,4 +667,4 @@ class RemoteObject extends Object return $this; } -} +} \ No newline at end of file diff --git a/src/OpenStack/ObjectStore/v1/Resource/StreamWrapper.php b/src/OpenStack/ObjectStore/v1/Resource/StreamWrapper.php index 4f8666a..0c93de3 100644 --- a/src/OpenStack/ObjectStore/v1/Resource/StreamWrapper.php +++ b/src/OpenStack/ObjectStore/v1/Resource/StreamWrapper.php @@ -21,7 +21,9 @@ namespace OpenStack\ObjectStore\v1\Resource; use \OpenStack\Bootstrap; +use OpenStack\Common\Transport\Exception\ResourceNotFoundException; use \OpenStack\ObjectStore\v1\ObjectStorage; +use OpenStack\Common\Exception; /** * Provides stream wrapping for Swift. @@ -604,6 +606,7 @@ class StreamWrapper // Force-clear the memory hogs. unset($this->obj); + fclose($this->objStream); } @@ -793,10 +796,9 @@ class StreamWrapper // server roundtrip? try { $this->container = $this->store->container($containerName); - } catch (\OpenStack\Common\Transport\Exception\FileNotFoundException $e) { - trigger_error('Container not found.', E_USER_WARNING); - - return false; + } catch (ResourceNotFoundException $e) { + trigger_error('Container not found.', E_USER_WARNING); + return false; } try { @@ -838,16 +840,15 @@ class StreamWrapper if ($this->isAppending) { fseek($this->objStream, -1, SEEK_END); } - } - - // If a 404 is thrown, we need to determine whether - // or not a new file should be created. - catch (\OpenStack\Common\Transport\Exception\FileNotFoundException $nf) { + } catch (ResourceNotFoundException $nf) { + // If a 404 is thrown, we need to determine whether + // or not a new file should be created. // For many modes, we just go ahead and create. if ($this->createIfNotFound) { $this->obj = new Object($objectName); $this->objStream = fopen('php://temp', 'rb+'); + $this->isDirty = true; } else { //if ($this->triggerErrors) { @@ -856,9 +857,8 @@ class StreamWrapper return false; } - } - // All other exceptions are fatal. - catch (\OpenStack\Common\Exception $e) { + } catch (Exception $e) { + // All other exceptions are fatal. //if ($this->triggerErrors) { trigger_error('Failed to fetch object: ' . $e->getMessage(), E_USER_WARNING); //} diff --git a/tests/AuthTest.php b/tests/AuthTest.php index 459c676..06392ea 100644 --- a/tests/AuthTest.php +++ b/tests/AuthTest.php @@ -27,7 +27,7 @@ use \OpenStack\ObjectStore\v1\ObjectStorage; use \OpenStack\Identity\v2\IdentityService; $config = array( - 'transport' => '\OpenStack\Common\Transport\GuzzleClient', + 'transport' => 'OpenStack\Common\Transport\Guzzle\GuzzleAdapter', 'transport.timeout' => 240, //'transport.debug' => 1, 'transport.ssl.verify' => 0, diff --git a/tests/Tests/Common/Transport/AbstractClientTest.php b/tests/Tests/Common/Transport/AbstractClientTest.php new file mode 100644 index 0000000..c441f73 --- /dev/null +++ b/tests/Tests/Common/Transport/AbstractClientTest.php @@ -0,0 +1,93 @@ + 'bar']; + private $body = 'baz'; + + public function setUp() + { + $this->request = $this->getMockBuilder('OpenStack\Common\Transport\RequestInterface') + ->disableOriginalConstructor() + ->getMock(); + + $this->client = $this->getMockForAbstractClass('OpenStack\Common\Transport\AbstractClient'); + + $this->client->expects($this->once()) + ->method('send') + ->with($this->request); + } + + public function testGet() + { + $this->client->expects($this->once()) + ->method('createRequest') + ->with('GET', self::URI, null, $this->options) + ->will($this->returnValue($this->request)); + + $this->client->get(self::URI, $this->options); + } + + public function testHead() + { + $this->client->expects($this->once()) + ->method('createRequest') + ->with('HEAD', self::URI, null, $this->options) + ->will($this->returnValue($this->request)); + + $this->client->head(self::URI, $this->options); + } + + public function testPost() + { + $this->client->expects($this->once()) + ->method('createRequest') + ->with('POST', self::URI, $this->body, $this->options) + ->will($this->returnValue($this->request)); + + $this->client->post(self::URI, $this->body, $this->options); + } + + public function testPut() + { + $this->client->expects($this->once()) + ->method('createRequest') + ->with('PUT', self::URI, $this->body, $this->options) + ->will($this->returnValue($this->request)); + + $this->client->put(self::URI, $this->body, $this->options); + } + + public function testDelete() + { + $this->client->expects($this->once()) + ->method('createRequest') + ->with('DELETE', self::URI, null, $this->options) + ->will($this->returnValue($this->request)); + + $this->client->delete(self::URI, $this->options); + } +} \ No newline at end of file diff --git a/tests/Tests/Common/Transport/Guzzle/GuzzleAdapterTest.php b/tests/Tests/Common/Transport/Guzzle/GuzzleAdapterTest.php new file mode 100644 index 0000000..a22dd7b --- /dev/null +++ b/tests/Tests/Common/Transport/Guzzle/GuzzleAdapterTest.php @@ -0,0 +1,90 @@ +mockClient = $this->getMockBuilder('GuzzleHttp\Client') + ->disableOriginalConstructor() + ->getMock(); + + $this->adapter = new GuzzleAdapter($this->mockClient); + } + + public function testFactoryReturnsInstance() + { + $this->assertInstanceOf( + 'OpenStack\Common\Transport\Guzzle\GuzzleAdapter', + $this->adapter + ); + } + + public function testFactoryMethod() + { + $this->assertInstanceOf( + 'OpenStack\Common\Transport\Guzzle\GuzzleAdapter', + GuzzleAdapter::create() + ); + } + + public function testCreateRequestCallsClientAndReturnsAdapter() + { + $this->mockClient + ->expects($this->once()) + ->method('createRequest') + ->with('GET') + ->will($this->returnValue( + $this->getMock('GuzzleHttp\Message\RequestInterface') + )); + + $adapter = (new GuzzleAdapter($this->mockClient))->createRequest('GET'); + $this->assertInstanceOf('OpenStack\Common\Transport\Guzzle\RequestAdapter', $adapter); + $this->assertInstanceOf('GuzzleHttp\Message\RequestInterface', $adapter->getMessage()); + } + + public function testSetOptionCallsClient() + { + $key = 'foo'; + $value = 'bar'; + $this->mockClient->expects($this->once())->method('setDefaultOption')->with($key, $value); + + (new GuzzleAdapter($this->mockClient))->setOption($key, $value); + } + + public function testGetBaseUrlWithOption() + { + $this->mockClient->expects($this->once())->method('getBaseUrl'); + (new GuzzleAdapter($this->mockClient))->getOption('base_url'); + } + + public function testGetOption() + { + $this->mockClient->expects($this->once())->method('getDefaultOption')->with('foo'); + (new GuzzleAdapter($this->mockClient))->getOption('foo'); + } +} \ No newline at end of file diff --git a/tests/Tests/Common/Transport/Guzzle/HttpErrorTest.php b/tests/Tests/Common/Transport/Guzzle/HttpErrorTest.php new file mode 100644 index 0000000..8844ff1 --- /dev/null +++ b/tests/Tests/Common/Transport/Guzzle/HttpErrorTest.php @@ -0,0 +1,127 @@ +assertInstanceOf('OpenStack\Common\Transport\Guzzle\HttpError', $sub); + } + + private function getEvent() + { + return new CompleteEvent(new Transaction(new Client(), new Request('GET', '/'))); + } + + public function testSuccessfulResponsesThrowNothing() + { + $event = $this->getEvent(); + $event->intercept(new Response(200)); + (new HttpError())->onComplete($event); + } + + /** + * @expectedException \OpenStack\Common\Transport\Exception\ConflictException + */ + public function testConflictExceptionRaisedFor409Error() + { + $event = $this->getEvent(); + $event->intercept(new Response(409)); + (new HttpError())->onComplete($event); + } + + /** + * @expectedException \OpenStack\Common\Transport\Exception\ForbiddenException + */ + public function testConflictExceptionRaisedFor403Error() + { + $event = $this->getEvent(); + $event->intercept(new Response(403)); + (new HttpError())->onComplete($event); + } + + /** + * @expectedException \OpenStack\Common\Transport\Exception\LengthRequiredException + */ + public function testConflictExceptionRaisedFor411Error() + { + $event = $this->getEvent(); + $event->intercept(new Response(411)); + (new HttpError())->onComplete($event); + } + + /** + * @expectedException \OpenStack\Common\Transport\Exception\MethodNotAllowedException + */ + public function testConflictExceptionRaisedFor405Error() + { + $event = $this->getEvent(); + $event->intercept(new Response(405)); + (new HttpError())->onComplete($event); + } + + /** + * @expectedException \OpenStack\Common\Transport\Exception\ResourceNotFoundException + */ + public function testConflictExceptionRaisedFor404Error() + { + $event = $this->getEvent(); + $event->intercept(new Response(404)); + (new HttpError())->onComplete($event); + } + + /** + * @expectedException \OpenStack\Common\Transport\Exception\ServerException + */ + public function testConflictExceptionRaisedFor500Error() + { + $event = $this->getEvent(); + $event->intercept(new Response(500)); + (new HttpError())->onComplete($event); + } + + /** + * @expectedException \OpenStack\Common\Transport\Exception\UnauthorizedException + */ + public function testConflictExceptionRaisedFor401Error() + { + $event = $this->getEvent(); + $event->intercept(new Response(401)); + (new HttpError())->onComplete($event); + } + + /** + * @expectedException \OpenStack\Common\Transport\Exception\UnprocessableEntityException + */ + public function testConflictExceptionRaisedFor422Error() + { + $event = $this->getEvent(); + $event->intercept(new Response(422)); + (new HttpError())->onComplete($event); + } +} \ No newline at end of file diff --git a/tests/Tests/Common/Transport/Guzzle/MessageAdapterTest.php b/tests/Tests/Common/Transport/Guzzle/MessageAdapterTest.php new file mode 100644 index 0000000..95edd04 --- /dev/null +++ b/tests/Tests/Common/Transport/Guzzle/MessageAdapterTest.php @@ -0,0 +1,138 @@ +getMockBuilder($class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function setUp() + { + $this->mock = $this->getStub(self::REQUEST_CLASS); + $this->adapter = new MessageAdapter($this->mock); + } + + public function testConstructorSetsMessage() + { + $this->assertInstanceOf(self::REQUEST_CLASS, $this->adapter->getMessage()); + } + + public function testSettingMessage() + { + $this->adapter->setMessage($this->getStub(self::RESPONSE_CLASS)); + $this->assertInstanceOf(self::RESPONSE_CLASS, $this->adapter->getMessage()); + } + + public function testGetProtocol() + { + $this->mock->expects($this->once())->method('getProtocolVersion'); + $this->adapter->setMessage($this->mock); + $this->adapter->getProtocolVersion(); + } + + public function testSetBody() + { + $body = $this->getMock('GuzzleHttp\Stream\StreamInterface'); + $this->mock->expects($this->once())->method('setBody')->with($body); + $this->adapter->setMessage($this->mock); + $this->adapter->setBody($body); + } + + public function testGetBody() + { + $this->mock->expects($this->once())->method('getBody'); + $this->adapter->setMessage($this->mock); + $this->adapter->getBody(); + } + + public function testGetHeaders() + { + $this->mock->expects($this->once())->method('getHeaders'); + $this->adapter->setMessage($this->mock); + $this->adapter->getHeaders(); + } + + public function testHasHeader() + { + $this->mock->expects($this->once())->method('hasHeader')->with('foo'); + $this->adapter->setMessage($this->mock); + $this->adapter->hasHeader('foo'); + } + + public function testSetHeader() + { + $header = 'foo'; + $value = 'bar'; + $this->mock->expects($this->once())->method('setHeader')->with($header, $value); + $this->adapter->setMessage($this->mock); + $this->adapter->setHeader($header, $value); + } + + public function testGetHeader() + { + $this->mock->expects($this->once())->method('getHeader')->with('foo'); + $this->adapter->setMessage($this->mock); + $this->adapter->getHeader('foo'); + } + + public function testSetHeaders() + { + $headers = ['foo' => 'bar']; + $this->mock->expects($this->once())->method('setHeaders')->with($headers); + $this->adapter->setMessage($this->mock); + $this->adapter->setHeaders($headers); + } + + public function testAddHeader() + { + $header = 'foo'; + $value = 'bar'; + $this->mock->expects($this->once())->method('addHeader')->with($header, $value); + $this->adapter->setMessage($this->mock); + $this->adapter->addHeader($header, $value); + } + + public function testAddHeaders() + { + $headers = ['foo' => 'bar']; + $this->mock->expects($this->once())->method('addHeaders')->with($headers); + $this->adapter->setMessage($this->mock); + $this->adapter->addHeaders($headers); + } + + public function testRemoveHeader() + { + $this->mock->expects($this->once())->method('removeHeader')->with('foo'); + $this->adapter->setMessage($this->mock); + $this->adapter->removeHeader('foo'); + } +} \ No newline at end of file diff --git a/tests/Tests/Common/Transport/Guzzle/RequestAdapterTest.php b/tests/Tests/Common/Transport/Guzzle/RequestAdapterTest.php new file mode 100644 index 0000000..fbfe2d3 --- /dev/null +++ b/tests/Tests/Common/Transport/Guzzle/RequestAdapterTest.php @@ -0,0 +1,68 @@ +getMockBuilder($class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function setUp() + { + $this->mock = $this->getStub('GuzzleHttp\Message\Request'); + $this->adapter = new RequestAdapter($this->mock); + } + + public function testGetMethod() + { + $this->mock->expects($this->once())->method('getMethod'); + $this->adapter->setMessage($this->mock); + $this->adapter->getMethod(); + } + + public function testSetMethod() + { + $this->mock->expects($this->once())->method('setMethod')->with('foo'); + $this->adapter->setMessage($this->mock); + $this->adapter->setMethod('foo'); + } + + public function testGetUrl() + { + $this->mock->expects($this->once())->method('getUrl'); + $this->adapter->setMessage($this->mock); + $this->adapter->getUrl(); + } + + public function testSetUrl() + { + $this->mock->expects($this->once())->method('setUrl')->with('foo'); + $this->adapter->setMessage($this->mock); + $this->adapter->setUrl('foo'); + } +} \ No newline at end of file diff --git a/tests/Tests/Common/Transport/Guzzle/ResponseAdapterTest.php b/tests/Tests/Common/Transport/Guzzle/ResponseAdapterTest.php new file mode 100644 index 0000000..c35499e --- /dev/null +++ b/tests/Tests/Common/Transport/Guzzle/ResponseAdapterTest.php @@ -0,0 +1,54 @@ +getMockBuilder($class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function setUp() + { + $this->mock = $this->getStub('GuzzleHttp\Message\Response'); + $this->adapter = new ResponseAdapter($this->mock); + } + + public function testGetStatusCode() + { + $this->mock->expects($this->once())->method('getStatusCode'); + $this->adapter->setMessage($this->mock); + $this->adapter->getStatusCode(); + } + + public function testGetReasonPhrase() + { + $this->mock->expects($this->once())->method('getReasonPhrase'); + $this->adapter->setMessage($this->mock); + $this->adapter->getReasonPhrase(); + } +} \ No newline at end of file diff --git a/tests/Tests/Common/Transport/GuzzleClientTest.php b/tests/Tests/Common/Transport/GuzzleClientTest.php deleted file mode 100644 index 38f597e..0000000 --- a/tests/Tests/Common/Transport/GuzzleClientTest.php +++ /dev/null @@ -1,73 +0,0 @@ -buildClient(); - - $this->assertInstanceOf('\OpenStack\Common\Transport\GuzzleClient', $client); - - $response = $client->doRequest($url, $method); - $this->assertInstanceOf('\GuzzleHttp\Message\Response', $response); - - } - - /** - * @depends testDoRequest - * @expectedException \OpenStack\Common\Transport\Exception\FileNotFoundException - */ - public function testDoRequestException() - { - $url = 'http://www.openstack.org/this-does-no-exist'; - $method = 'GET'; - - $client = $this->buildClient(); - $client->doRequest($url, $method); - } - -} diff --git a/tests/Tests/ObjectStore/v1/ObjectStorageTest.php b/tests/Tests/ObjectStore/v1/ObjectStorageTest.php index 689d4c0..9048700 100644 --- a/tests/Tests/ObjectStore/v1/ObjectStorageTest.php +++ b/tests/Tests/ObjectStore/v1/ObjectStorageTest.php @@ -251,15 +251,10 @@ class ObjectStorageTest extends \OpenStack\Tests\TestCase // we get some data back. $url = $container->url() . '?format=xml'; - // Use CURL to get better debugging: - //$client = \OpenStack\Transport::instance(); - //$response = $client->doRequest($url, 'GET'); - $data = file_get_contents($url); $this->assertNotEmpty($data, $url); $containers = $store->containers(); - //throw new \Exception(print_r($containers, true)); $store->deleteContainer($testCollection); } @@ -276,13 +271,14 @@ class ObjectStorageTest extends \OpenStack\Tests\TestCase } $ret = $store->createContainer($testCollection); - $acl = \OpenStack\ObjectStore\v1\Resource\ACL::makePublic(); + $acl = ACL::makePublic(); $ret = $store->changeContainerACL($testCollection, $acl); $this->assertFalse($ret); $container = $store->container($testCollection); $url = $container->url() . '?format=xml'; + $data = file_get_contents($url); $this->assertNotEmpty($data, $url); diff --git a/tests/Tests/ObjectStore/v1/ACLTest.php b/tests/Tests/ObjectStore/v1/Resource/ACLTest.php similarity index 100% rename from tests/Tests/ObjectStore/v1/ACLTest.php rename to tests/Tests/ObjectStore/v1/Resource/ACLTest.php diff --git a/tests/Tests/ObjectStore/v1/ContainerTest.php b/tests/Tests/ObjectStore/v1/Resource/ContainerTest.php similarity index 81% rename from tests/Tests/ObjectStore/v1/ContainerTest.php rename to tests/Tests/ObjectStore/v1/Resource/ContainerTest.php index 9eeba14..3796771 100644 --- a/tests/Tests/ObjectStore/v1/ContainerTest.php +++ b/tests/Tests/ObjectStore/v1/Resource/ContainerTest.php @@ -19,61 +19,43 @@ */ namespace OpenStack\Tests\ObjectStore\v1\Resource; +use OpenStack\Bootstrap; use \OpenStack\ObjectStore\v1\Resource\Container; use \OpenStack\ObjectStore\v1\Resource\Object; use \OpenStack\ObjectStore\v1\Resource\ACL; +use OpenStack\Tests\TestCase; -class ContainerTest extends \OpenStack\Tests\TestCase +class ContainerTest extends TestCase { const FILENAME = 'unit-test-dummy.txt'; const FILESTR = 'This is a test.'; + const FNAME = 'testSave'; + const FCONTENT = 'This is a test.'; + const FTYPE = 'application/x-monkey-file'; - // The factory functions (newFrom*) are tested in the - // ObjectStorage tests, as they are required there. - // Rather than build a Mock to achieve the same test here, - // we just don't test them again. - - public function testConstructor() + public function testConstructorSetsName() { $container = new Container('foo'); $this->assertEquals('foo', $container->name()); - - // These will now cause the system to try to fetch a remote - // container. - //$this->assertEquals(0, $container->bytes()); - //$this->assertEquals(0, $container->count()); } /** * @expectedException \OpenStack\Common\Exception */ - public function testConstructorFailure() + public function testExceptionIsThrownWhenContainerNotFound() { $container = new Container('foo'); - $this->assertEquals('foo', $container->name()); - - // These will now cause the system to try to fetch a remote - // container. This is a failure condition. - $this->assertEquals(0, $container->bytes()); + $container->bytes(); } public function testCountable() { - // Verify that the interface Countable is properly - // implemented. - - $mockJSON = array('count' => 5, 'bytes' => 128, 'name' => 'foo'); - + // Verify that the interface Countable is properly implemented. + $mockJSON = array('count' => 5, 'bytes' => 128, 'name' => 'foo'); $container = Container::newFromJSON($mockJSON, 'fake', 'fake'); - - $this->assertEquals(5, count($container)); - + $this->assertCount(5, $container); } - const FNAME = 'testSave'; - const FCONTENT = 'This is a test.'; - const FTYPE = 'application/x-monkey-file'; - public function testSave() { // Clean up anything left. @@ -81,13 +63,13 @@ class ContainerTest extends \OpenStack\Tests\TestCase $container = $this->containerFixture(); - $obj = new Object(self::FNAME, self::FCONTENT, self::FTYPE); - $obj->setMetadata(array('foo' => '1234')); + $object = new Object(self::FNAME, self::FCONTENT, self::FTYPE); + $object->setMetadata(array('foo' => '1234')); - $this->assertEquals(self::FCONTENT, $obj->content()); + $this->assertEquals(self::FCONTENT, $object->content()); try { - $ret = $container->save($obj); + $ret = $container->save($object); } catch (\Exception $e) { $this->destroyContainerFixture(); throw $e; @@ -188,7 +170,7 @@ class ContainerTest extends \OpenStack\Tests\TestCase try { $foo = $container->object('no/such'); } catch (\OpenStack\Common\Exception $e) { - $this->assertInstanceOf('\OpenStack\Common\Transport\Exception\FileNotFoundException', $e); + $this->assertInstanceOf('OpenStack\Common\Transport\Exception\ResourceNotFoundException', $e); } } @@ -229,7 +211,6 @@ class ContainerTest extends \OpenStack\Tests\TestCase ++$i; } $this->assertEquals(3, $i); - } /** @@ -286,35 +267,6 @@ class ContainerTest extends \OpenStack\Tests\TestCase $o = array_shift($objects); $this->assertEquals('a/b/' . self::FNAME, $o->name()); - - /* - * The Open Stack documentation is unclear about how best to - * use paths. Experimentation suggests that if you rely on paths - * instead of prefixes, your best bet is to create directory - * markers. - */ - - // Test subdir listings: - // This does not work (by design?) with Path. You have to use prefix - // or else create directory markers. - // $obj1 = new Object('a/aa/aaa/' . self::FNAME, self::FCONTENT, self::FTYPE); - // $container->save($obj1); - // $objects = $container->objectsByPath('a/aaa', '/'); - - // $this->assertEquals(1, count($objects), 'One subdir'); - - // $objects = $container->objectsByPath('a/'); - // throw new \Exception(print_r($objects, true)); - // $this->assertEquals(2, count($objects)); - - // foreach ($objects as $o) { - // if ($o instanceof Object) { - // $this->assertEquals('a/' . self::FNAME, $o->name()); - // } - // else { - // $this->assertEquals('a/b/', $o->path()); - // } - // } } /** @@ -397,7 +349,6 @@ class ContainerTest extends \OpenStack\Tests\TestCase } - /** * @depends testSave */ @@ -412,7 +363,6 @@ class ContainerTest extends \OpenStack\Tests\TestCase $this->destroyContainerFixture(); $this->assertTrue($ret); $this->assertFalse($fail); - } /** @@ -429,7 +379,6 @@ class ContainerTest extends \OpenStack\Tests\TestCase $store->createContainer($cname, ACL::makePublic()); - $store->containers(); $container = $store->container($cname); @@ -439,7 +388,5 @@ class ContainerTest extends \OpenStack\Tests\TestCase $this->assertTrue($acl->isPublic()); $store->deleteContainer($cname); - } - } diff --git a/tests/Tests/ObjectStore/v1/ObjectTest.php b/tests/Tests/ObjectStore/v1/Resource/ObjectTest.php similarity index 100% rename from tests/Tests/ObjectStore/v1/ObjectTest.php rename to tests/Tests/ObjectStore/v1/Resource/ObjectTest.php diff --git a/tests/Tests/ObjectStore/v1/RemoteObjectTest.php b/tests/Tests/ObjectStore/v1/Resource/RemoteObjectTest.php similarity index 100% rename from tests/Tests/ObjectStore/v1/RemoteObjectTest.php rename to tests/Tests/ObjectStore/v1/Resource/RemoteObjectTest.php diff --git a/tests/Tests/ObjectStore/v1/Resource/StreamWrapperFSTest.php b/tests/Tests/ObjectStore/v1/Resource/StreamWrapperFSTest.php new file mode 100644 index 0000000..cfe7b5c --- /dev/null +++ b/tests/Tests/ObjectStore/v1/Resource/StreamWrapperFSTest.php @@ -0,0 +1,353 @@ +context)['swiftfs']; + $this->assertNotEmpty($context['token']); + $this->assertNotEmpty($context['swift_endpoint']); + $this->assertEquals(self::FTYPE, $context['content_type']); + } + + public function testRegister() + { + $this->assertNotEmpty(StreamWrapperFS::DEFAULT_SCHEME); + $this->assertContains(StreamWrapperFS::DEFAULT_SCHEME, stream_get_wrappers()); + } + + public function testOpenFailureWithoutContext() + { + $url = $this->createNewUrl('non_existent_container/foo.txt'); + $this->assertFalse(@fopen($url, 'r')); + } + + public function testResourceType() + { + $this->assertInternalType('resource', $this->resource); + } + + public function testCreatingResourceInWriteMode() + { + $resource = $this->createNewResource($this->createNewUrl(), 'w+'); + $this->assertInternalType('resource', $resource); + fclose($resource); + } + + public function testCreatingResourceInCreateMode() + { + $resource = $this->createNewResource($this->createNewUrl(), 'c+'); + $this->assertInternalType('resource', $resource); + fclose($resource); + } + + public function testTell() + { + // Sould be at the beginning of the buffer. + $this->assertEquals(0, ftell($this->resource)); + } + + public function testWrite() + { + $string = 'To be is to be the value of a bound variable. -- Quine'; + fwrite($this->resource, $string); + $this->assertGreaterThan(0, ftell($this->resource)); + } + + public function testStat() + { + $this->assertEquals(0, fstat($this->resource)['size']); + + fwrite($this->resource, 'foo'); + fflush($this->resource); + $this->assertGreaterThan(0, fstat($this->resource)['size']); + } + + public function testSeek() + { + $text = 'Foo bar'; + fwrite($this->resource, $text); + + fseek($this->resource, 0, SEEK_END); + $pointer = ftell($this->resource); + + $this->assertGreaterThan(0, $pointer); + } + + public function testEof() + { + $this->assertFalse(feof($this->resource)); + + fwrite($this->resource, 'foo'); + rewind($this->resource); + stream_get_contents($this->resource); + + $this->assertTrue(feof($this->resource)); + } + + public function testFlush() + { + $content = str_repeat('foo', 50); + + fwrite($this->resource, $content); + fflush($this->resource); + rewind($this->resource); + + $this->assertEquals($content, stream_get_contents($this->resource)); + } + + public function testStreamGetMetadata() + { + $object = stream_get_meta_data($this->resource)['wrapper_data']->object(); + $this->assertInstanceOf('OpenStack\ObjectStore\v1\Resource\Object', $object); + $this->assertEquals(self::FTYPE, $object->contentType()); + } + + public function testClose() + { + fclose($this->resource); + $this->assertFalse(is_resource($this->resource)); + } + + public function testCast() + { + $read = [$this->resource]; + $write = []; + $except = []; + $this->assertGreaterThan(0, stream_select($read, $write, $except, 0)); + } + + public function testUrlStat() + { + $stat = stat($this->url); + + // Check that the array looks right. + $this->assertCount(26, $stat); + $this->assertEquals(0, $stat[3]); + $this->assertEquals($stat[2], $stat['mode']); + } + + public function testFileExists() + { + $this->assertTrue(file_exists($this->url)); + } + + public function testFileIsReadable() + { + $this->assertTrue(is_readable($this->url)); + } + + public function testFileIsWritable() + { + $this->assertTrue(is_writeable($this->url)); + } + + public function testFileModifyTime() + { + $this->assertGreaterThan(0, filemtime($this->url)); + } + + public function testFileSize() + { + $url = $this->createNewUrl('file_size_test'); + + $resource = $this->createNewResource($url, 'w+'); + fwrite($resource, '!'); + fclose($resource); + + $this->assertEquals(1, filesize($url)); + unlink($url); + } + + public function testPermissions() + { + $perm = fileperms($this->url); + + // Assert that this is a file. Objects are *always* marked as files. + $this->assertEquals(0x8000, $perm & 0x8000); + + // Assert writeable by owner. + $this->assertEquals(0x0080, $perm & 0x0080); + + // Assert not world writable. + $this->assertEquals(0, $perm & 0x0002); + } + + public function testFileGetContents() + { + $url = $this->createNewUrl('get_contents'); + $resource = $this->createNewResource($url, 'w+'); + + fwrite($resource, '!'); + fclose($resource); + + $contents = file_get_contents($url, null, $this->context); + $this->assertEquals('!', $contents); + unlink($url); + } + + public function testCopy() + { + $newUrl = '/tmp/new_file_from_swift.txt'; + copy($this->url, $newUrl, $this->context); + + $this->assertTrue(file_exists($newUrl)); + unlink($newUrl); + } + + public function testUnlink() + { + unlink($this->url, $this->context); + $this->assertFalse(file_exists($this->url)); + } + + public function testSetOption() + { + $this->assertTrue(stream_set_blocking($this->resource, 1)); + + // Returns 0 on success. + $this->assertEquals(0, stream_set_write_buffer($this->resource, 8192)); + + // Cannot set a timeout on a tmp storage: + $this->assertFalse(stream_set_timeout($this->resource, 10)); + } + + public function testRename() + { + $oldUrl = $this->createNewUrl('old'); + $newUrl = $this->createNewUrl('new'); + + $original = $this->createNewResource($oldUrl, 'w+'); + fwrite($original, 'fooooo'); + fclose($original); + + rename($oldUrl, $newUrl, $this->context); + + $this->assertTrue(file_exists($newUrl)); + $this->assertFalse(file_exists($this->url)); + + unlink($newUrl, $this->context); + } + + public function testOpenDir() + { + $baseDirectory = opendir($this->createNewUrl(''), $this->context); + $this->assertInternalType('resource', $baseDirectory); + closedir($baseDirectory); + } + + public function testReadDir() + { + $paths = ['test1.txt', 'foo/test2.txt', 'foo/test3.txt', 'bar/test4.txt']; + + foreach ($paths as $path) { + $file = fopen($this->createNewUrl($path), 'c+', false, $this->context); + fwrite($file, 'Test.'); + fclose($file); + } + + $baseDirectory = opendir($this->createNewUrl(''), $this->context); + + $expectedPaths = ['bar/', 'foo/', 'test1.txt']; + while (false !== ($currentEntry = readdir($baseDirectory))) { + $nextPath = array_shift($expectedPaths); + $this->assertEquals($nextPath, $currentEntry); + } + + $this->assertFalse(readdir($baseDirectory)); + + closedir($baseDirectory); + } + + public function testRewindDir() + { + $baseDirectory = opendir($this->createNewUrl(''), $this->context); + rewinddir($baseDirectory); + + $this->assertEquals('bar/', readdir($baseDirectory)); + + closedir($baseDirectory); + } + + public function testCloseDir() + { + $baseDirectory = opendir($this->createNewUrl(''), $this->context); + closedir($baseDirectory); + $this->assertFalse(is_resource($baseDirectory)); + } + + public function testOpenSubdir() + { + // Opening foo we should find test2.txt and test3.txt. + $url = $this->createNewUrl('foo/'); + $dir = opendir($url, $this->context); + + $this->assertEquals('test2.txt', readdir($dir)); + $this->assertEquals('test3.txt', readdir($dir)); + + $array = scandir($url, -1, $this->context); + $this->assertEquals(2, count($array)); + $this->assertEquals('test3.txt', $array[0]); + } + + public function testIsDir() + { + // Object names are pathy. If objects exist starting with this path we can + // consider the directory to exist. + $url = $this->createNewUrl('baz/'); + $this->assertFalse(is_dir($url)); + + $url = $this->createNewUrl('foo/'); + $this->assertTrue(is_dir($url)); + } + + public function testMkdir() + { + // Object names are pathy. If no object names start with the a path we can + // consider mkdir passed. If object names exist we should fail mkdir. + $url = $this->createNewUrl('baz/'); + $this->assertTrue(mkdir($url, 0700, true, $this->context)); + + // Test the case for an existing directory. + $url = $this->createNewUrl('foo/'); + $this->assertFalse(mkdir($url, 0700, true, $this->context)); + } + + public function testRmdir() + { + // Object names are pathy. If no object names start with the a path we can + // consider rmdir passed. If object names exist we should fail rmdir. + $url = $this->createNewUrl('baz/'); + $this->assertTrue(rmdir($url, $this->context)); + + // Test the case for an existing directory. + $url = $this->createNewUrl('foo/'); + $this->assertFalse(rmdir($url, $this->context)); + } +} \ No newline at end of file diff --git a/tests/Tests/ObjectStore/v1/Resource/StreamWrapperTest.php b/tests/Tests/ObjectStore/v1/Resource/StreamWrapperTest.php new file mode 100644 index 0000000..93fbec8 --- /dev/null +++ b/tests/Tests/ObjectStore/v1/Resource/StreamWrapperTest.php @@ -0,0 +1,312 @@ +context)['swift']; + $this->assertNotEmpty($context['token']); + $this->assertNotEmpty($context['swift_endpoint']); + $this->assertEquals(self::FTYPE, $context['content_type']); + } + + public function testOpenFailureWithoutContext() + { + $url = $this->createNewUrl('non_existent_container/foo.txt'); + $this->assertFalse(@fopen($url, 'r')); + } + + public function testResourceType() + { + $this->assertInternalType('resource', $this->resource); + } + + public function testCreatingResourceInWriteMode() + { + $resource = $this->createNewResource($this->createNewUrl(), 'w+'); + $this->assertInternalType('resource', $resource); + fclose($resource); + } + + public function testCreatingResourceInCreateMode() + { + $resource = $this->createNewResource($this->createNewUrl(), 'c+'); + $this->assertInternalType('resource', $resource); + fclose($resource); + } + + public function testTell() + { + // Sould be at the beginning of the buffer. + $this->assertEquals(0, ftell($this->resource)); + } + + public function testWrite() + { + $string = 'To be is to be the value of a bound variable. -- Quine'; + fwrite($this->resource, $string); + $this->assertGreaterThan(0, ftell($this->resource)); + } + + public function testStat() + { + $this->assertEquals(0, fstat($this->resource)['size']); + + fwrite($this->resource, 'foo'); + fflush($this->resource); + $this->assertGreaterThan(0, fstat($this->resource)['size']); + } + + public function testSeek() + { + $text = 'Foo bar'; + fwrite($this->resource, $text); + + fseek($this->resource, 0, SEEK_END); + $pointer = ftell($this->resource); + + $this->assertGreaterThan(0, $pointer); + } + + public function testEof() + { + $this->assertFalse(feof($this->resource)); + + fwrite($this->resource, 'foo'); + rewind($this->resource); + stream_get_contents($this->resource); + + $this->assertTrue(feof($this->resource)); + } + + public function testFlush() + { + $content = str_repeat('foo', 50); + + fwrite($this->resource, $content); + fflush($this->resource); + rewind($this->resource); + + $this->assertEquals($content, stream_get_contents($this->resource)); + } + + public function testStreamGetMetadata() + { + $object = stream_get_meta_data($this->resource)['wrapper_data']->object(); + $this->assertInstanceOf('OpenStack\ObjectStore\v1\Resource\Object', $object); + $this->assertEquals(self::FTYPE, $object->contentType()); + } + + public function testClose() + { + fclose($this->resource); + $this->assertFalse(is_resource($this->resource)); + } + + public function testCast() + { + $read = [$this->resource]; + $write = []; + $except = []; + $this->assertGreaterThan(0, stream_select($read, $write, $except, 0)); + } + + public function testUrlStat() + { + $stat = stat($this->url); + + // Check that the array looks right. + $this->assertCount(26, $stat); + $this->assertEquals(0, $stat[3]); + $this->assertEquals($stat[2], $stat['mode']); + } + + public function testFileExists() + { + $this->assertTrue(file_exists($this->url)); + } + + public function testFileIsReadable() + { + $this->assertTrue(is_readable($this->url)); + } + + public function testFileIsWritable() + { + $this->assertTrue(is_writeable($this->url)); + } + + public function testFileModifyTime() + { + $this->assertGreaterThan(0, filemtime($this->url)); + } + + public function testFileSize() + { + $url = $this->createNewUrl('file_size_test'); + + $resource = $this->createNewResource($url, 'w+'); + fwrite($resource, '!'); + fclose($resource); + + $this->assertEquals(1, filesize($url)); + unlink($url); + } + + public function testPermissions() + { + $perm = fileperms($this->url); + + // Assert that this is a file. Objects are *always* marked as files. + $this->assertEquals(0x8000, $perm & 0x8000); + + // Assert writeable by owner. + $this->assertEquals(0x0080, $perm & 0x0080); + + // Assert not world writable. + $this->assertEquals(0, $perm & 0x0002); + } + + public function testFileGetContents() + { + $url = $this->createNewUrl('get_contents'); + $resource = $this->createNewResource($url, 'w+'); + + fwrite($resource, '!'); + fclose($resource); + + $contents = file_get_contents($url, null, $this->context); + $this->assertEquals('!', $contents); + unlink($url); + } + + public function testCopy() + { + $newUrl = '/tmp/new_file_from_swift.txt'; + copy($this->url, $newUrl, $this->context); + + $this->assertTrue(file_exists($newUrl)); + unlink($newUrl); + } + + public function testUnlink() + { + unlink($this->url, $this->context); + $this->assertFalse(file_exists($this->url)); + } + + public function testSetOption() + { + $this->assertTrue(stream_set_blocking($this->resource, 1)); + + // Returns 0 on success. + $this->assertEquals(0, stream_set_write_buffer($this->resource, 8192)); + + // Cannot set a timeout on a tmp storage: + $this->assertFalse(stream_set_timeout($this->resource, 10)); + } + + public function testRename() + { + $oldUrl = $this->createNewUrl('old'); + $newUrl = $this->createNewUrl('new'); + + $original = $this->createNewResource($oldUrl, 'w+'); + fwrite($original, 'fooooo'); + fclose($original); + + rename($oldUrl, $newUrl, $this->context); + + $this->assertTrue(file_exists($newUrl)); + $this->assertFalse(file_exists($this->url)); + + unlink($newUrl, $this->context); + } + + public function testOpenDir() + { + $baseDirectory = opendir($this->createNewUrl(''), $this->context); + $this->assertInternalType('resource', $baseDirectory); + closedir($baseDirectory); + } + + public function testReadDir() + { + $paths = ['test1.txt', 'foo/test2.txt', 'foo/test3.txt', 'bar/test4.txt']; + + foreach ($paths as $path) { + $file = fopen($this->createNewUrl($path), 'c+', false, $this->context); + fwrite($file, 'Test.'); + fclose($file); + } + + $baseDirectory = opendir($this->createNewUrl(''), $this->context); + + $expectedPaths = ['bar/', 'foo/', 'test1.txt']; + while (false !== ($currentEntry = readdir($baseDirectory))) { + $nextPath = array_shift($expectedPaths); + $this->assertEquals($nextPath, $currentEntry); + } + + $this->assertFalse(readdir($baseDirectory)); + + closedir($baseDirectory); + } + + public function testRewindDir() + { + $baseDirectory = opendir($this->createNewUrl(''), $this->context); + rewinddir($baseDirectory); + + $this->assertEquals('bar/', readdir($baseDirectory)); + + closedir($baseDirectory); + } + + public function testCloseDir() + { + $baseDirectory = opendir($this->createNewUrl(''), $this->context); + closedir($baseDirectory); + $this->assertFalse(is_resource($baseDirectory)); + } + + public function testOpenSubdir() + { + // Opening foo we should find test2.txt and test3.txt. + $url = $this->createNewUrl('foo/'); + $dir = opendir($url, $this->context); + + $this->assertEquals('test2.txt', readdir($dir)); + $this->assertEquals('test3.txt', readdir($dir)); + + $array = scandir($url, -1, $this->context); + $this->assertEquals(2, count($array)); + $this->assertEquals('test3.txt', $array[0]); + } +} \ No newline at end of file diff --git a/tests/Tests/ObjectStore/v1/Resource/StreamWrapperTestCase.php b/tests/Tests/ObjectStore/v1/Resource/StreamWrapperTestCase.php new file mode 100644 index 0000000..acfbd24 --- /dev/null +++ b/tests/Tests/ObjectStore/v1/Resource/StreamWrapperTestCase.php @@ -0,0 +1,133 @@ +createContainer($containerName); + + try { + self::$container = $service->container($containerName); + } catch (\Exception $e) { + $service->deleteContainer($containerName); + throw $e; + } + + self::$settings += [ + 'username' => self::$settings['openstack.identity.username'], + 'password' => self::$settings['openstack.identity.password'], + 'endpoint' => self::$settings['openstack.identity.url'], + 'tenantid' => self::$settings['openstack.identity.tenantId'], + 'token' => $service->token(), + 'swift_endpoint' => $service->url(), + ]; + Bootstrap::setConfiguration(self::$settings); + } + + public static function tearDownAfterClass() + { + if (!self::$container) { + return; + } + + foreach (self::$container as $object) { + try { + self::$container->delete($object->name()); + } catch (\Exception $e) {} + } + + $service = self::createObjectStoreService(); + $service->deleteContainer(self::$container->name()); + } + + public function setUp() + { + Bootstrap::useStreamWrappers(); + + $this->url = $this->createNewUrl(); + $this->context = $this->createStreamContext(); + $this->resource = $this->createNewResource($this->url); + } + + public function tearDown() + { + if (is_resource($this->resource)) { + fclose($this->resource); + } + + $this->resource = null; + stream_wrapper_unregister(static::SCHEME); + } + + protected function createNewResource($url, $mode = self::DEFAULT_MODE) + { + return fopen($url, $mode, false, $this->context); + } + + protected function createNewUrl($objectName = self::FILE_PATH) + { + return sprintf("%s://%s/%s", + static::SCHEME, + urlencode(self::$settings['openstack.swift.container']), + join('/', array_map('urlencode', explode('/', $objectName))) + ); + } + + private function createStreamContext(array $params = [], $scheme = null) + { + if (!$scheme) { + $scheme = static::SCHEME; + } + + if (!($objectStore = $this->objectStore())) { + throw new \Exception('Object storage service could not be created'); + } + + $params += [ + 'token' => $objectStore->token(), + 'swift_endpoint' => $objectStore->url(), + 'content_type' => self::FTYPE, + 'transport_client' => $this->getTransportClient(), + ]; + + return stream_context_create([ + $scheme => $params + ]); + } +} \ No newline at end of file diff --git a/tests/Tests/ObjectStore/v1/StreamWrapperFSTest.php b/tests/Tests/ObjectStore/v1/StreamWrapperFSTest.php deleted file mode 100644 index 9336a3d..0000000 --- a/tests/Tests/ObjectStore/v1/StreamWrapperFSTest.php +++ /dev/null @@ -1,642 +0,0 @@ -container($cname); - } - // The container was never created. - catch (FileNotFoundException $e) { - return; - } - - foreach ($container as $object) { - try { - $container->delete($object->name()); - } catch (\Exception $e) {} - } - - $store->deleteContainer($cname); - } - - protected function newUrl($objectName) - { - $scheme = StreamWrapperFS::DEFAULT_SCHEME; - $cname = self::$settings['openstack.swift.container']; - $cname = urlencode($cname); - - $objectParts = explode('/', $objectName); - for ($i = 0; $i < count($objectParts); ++$i) { - $objectParts[$i] = urlencode($objectParts[$i]); - } - $objectName = implode('/', $objectParts); - - $url = $scheme . '://' . $cname . '/' . $objectName; - - return $url; - } - - /** - * This assumes auth has already been done. - */ - protected function basicSwiftContext($add = array(), $scheme = null) - { - $cname = self::$settings['openstack.swift.container']; - - if (empty($scheme)) { - $scheme = StreamWrapperFS::DEFAULT_SCHEME; - } - - if (empty(self::$ostore)) { - throw new \Exception('OStore is gone.'); - } - - $params = $add + array( - 'token' => $this->objectStore()->token(), - 'swift_endpoint' => $this->objectStore()->url(), - 'content_type' => self::FTYPE, - 'transport_client' => $this->getTransportClient(), - ); - $cxt = array($scheme => $params); - - return stream_context_create($cxt); - } - - /** - * This performs authentication via context. - */ - protected function authSwiftContext(array $params = [], $scheme = null) - { - if (empty($scheme)) { - $scheme = StreamWrapperFS::DEFAULT_SCHEME; - } - - $params += [ - 'username' => self::$settings['openstack.identity.username'], - 'password' => self::$settings['openstack.identity.password'], - 'endpoint' => self::$settings['openstack.identity.url'], - 'tenantid' => self::$settings['openstack.identity.tenantId'], - 'content_type' => self::FTYPE, - 'transport_client' => $this->getTransportClient(), - ]; - - return stream_context_create([ - $scheme => $params - ]); - } - - /** - * Add additional params to the config. - * - * This allows us to insert credentials into the - * bootstrap config, which in turn allows us to run - * high-level context-less functions like - * file_get_contents(), stat(), and is_file(). - */ - protected function addBootstrapConfig() - { - $opts = array( - 'username' => self::$settings['openstack.identity.username'], - 'password' => self::$settings['openstack.identity.password'], - 'endpoint' => self::$settings['openstack.identity.url'], - 'tenantid' => self::$settings['openstack.identity.tenantId'], - 'token' => $this->objectStore()->token(), - 'swift_endpoint' => $this->objectStore()->url(), - ); - \OpenStack\Bootstrap::setConfiguration($opts); - - } - - // Canary. There are UTF-8 encoding issues in stream wrappers. - public function testStreamContext() - { - // Clear old values. - \OpenStack\Bootstrap::setConfiguration(array( - 'token' => null, - )); - - $cxt = $this->authSwiftContext(); - $array = stream_context_get_options($cxt); - - $opts = $array['swiftfs']; - $endpoint = self::conf('openstack.identity.url'); - - $this->assertEquals($endpoint, $opts['endpoint'], 'A UTF-8 encoding issue.'); - } - - /** - * @depends testStreamContext - */ - public function testRegister() - { - // Canary - $this->assertNotEmpty(StreamWrapperFS::DEFAULT_SCHEME); - - $klass = '\OpenStack\ObjectStore\v1\Resource\StreamWrapperFS'; - stream_wrapper_register(StreamWrapperFS::DEFAULT_SCHEME, $klass); - - $wrappers = stream_get_wrappers(); - - $this->assertContains(StreamWrapperFS::DEFAULT_SCHEME, $wrappers); - } - - /** - * @depends testRegister - */ - public function testOpenFailureWithoutContext() - { - $url = $this->newUrl('foo→/bar.txt'); - $ret = @fopen($url, 'r'); - - $this->assertFalse($ret); - } - - /** - * @depends testRegister - */ - public function testOpen() - { - $cname = self::$settings['openstack.swift.container']; - - // Create a fresh container. - $this->eradicateContainer($cname); - $this->containerFixture(); - - // Simple write test. - $oUrl = $this->newUrl('foo→/test.csv'); - - $res = fopen($oUrl, 'nope', false, $this->authSwiftContext()); - - $this->assertTrue(is_resource($res)); - - $md = stream_get_meta_data($res); - $wrapper = $md['wrapper_data']; - - fclose($res); - - // Now we test the same, but re-using the auth token: - $cxt = $this->basicSwiftContext(array('token' => $wrapper->token())); - - $res = fopen($oUrl, 'nope', false, $cxt); - - $this->assertTrue(is_resource($res)); - - fclose($res); - - } - - /** - * @depends testOpen - */ - public function testOpenFailureWithRead() - { - $url = $this->newUrl(__FUNCTION__); - $res = @fopen($url, 'r', false, $this->basicSwiftContext()); - - $this->assertFalse($res); - - } - - // DO we need to test other modes? - - /** - * @depends testOpen - */ - public function testOpenCreateMode() - { - $url = $this->newUrl(self::FNAME); - $res = fopen($url, 'c+', false, $this->basicSwiftContext()); - $this->assertTrue(is_resource($res)); - //fclose($res); - return $res; - } - - /** - * @depends testOpenCreateMode - */ - public function testTell($res) - { - // Sould be at the beginning of the buffer. - $this->assertEquals(0, ftell($res)); - - return $res; - } - - /** - * @depends testTell - */ - public function testWrite($res) - { - $str = 'To be is to be the value of a bound variable. -- Quine'; - fwrite($res, $str); - $this->assertGreaterThan(0, ftell($res)); - - return $res; - } - - /** - * @depends testWrite - */ - public function testStat($res) - { - $stat = fstat($res); - - $this->assertGreaterThan(0, $stat['size']); - - return $res; - } - - /** - * @depends testStat - */ - public function testSeek($res) - { - $then = ftell($res); - rewind($res); - - $now = ftell($res); - - // $now should be 0 - $this->assertLessThan($then, $now); - $this->assertEquals(0, $now); - - fseek($res, 0, SEEK_END); - $final = ftell($res); - - $this->assertEquals($then, $final); - - return $res; - - } - - /** - * @depends testSeek - */ - public function testEof($res) - { - rewind($res); - - $this->assertEquals(0, ftell($res)); - - $this->assertFalse(feof($res)); - - fseek($res, 0, SEEK_END); - $this->assertGreaterThan(0, ftell($res)); - - $read = fread($res, 8192); - - $this->assertEmpty($read); - - $this->assertTrue(feof($res)); - - return $res; - } - - /** - * @depends testEof - */ - public function testFlush($res) - { - $stat1 = fstat($res); - - fflush($res); - - // Grab a copy of the object. - $url = $this->newUrl(self::FNAME); - $newObj = fopen($url, 'r', false, $this->basicSwiftContext()); - - $stat2 = fstat($newObj); - - $this->assertEquals($stat1['size'], $stat2['size']); - - return $res; - } - - /** - * @depends testFlush - */ - public function testStreamGetMetadata($res) - { - // Grab a copy of the object. - $url = $this->newUrl(self::FNAME); - $newObj = fopen($url, 'r', false, $this->basicSwiftContext()); - - $md = stream_get_meta_data($newObj); - //throw new \Exception(print_r($md, true)); - $obj = $md['wrapper_data']->object(); - - $this->assertInstanceOf('\OpenStack\ObjectStore\v1\Resource\RemoteObject', $obj); - - $this->assertEquals(self::FTYPE, $obj->contentType()); - - } - - /** - * @depends testFlush - */ - public function testClose($res) - { - $this->assertTrue(is_resource($res)); - fwrite($res, '~~~~'); - //throw new \Exception(stream_get_contents($res)); - fflush($res); - - // This is occasionally generating seemingly - // spurious PHP errors about Bootstrap::$config. - fclose($res); - - $url = $this->newUrl(self::FNAME); - $res2 = fopen($url, 'r', false, $this->basicSwiftContext()); - $this->assertTrue(is_resource($res2)); - - $contents = stream_get_contents($res2); - fclose($res2); - $this->assertRegExp('/~{4}$/', $contents); - - } - - /** - * @depends testClose - */ - public function testCast() - { - $url = $this->newUrl(self::FNAME); - $res = fopen($url, 'r', false, $this->basicSwiftContext()); - - $read = array($res); - $write = array(); - $except = array(); - $num_changed = stream_select($read, $write, $except, 0); - $this->assertGreaterThan(0, $num_changed); - } - - /** - * @depends testClose - */ - public function testUrlStat() - { - // Add context to the bootstrap config. - $this->addBootstrapConfig(); - - $url = $this->newUrl(self::FNAME); - - $ret = stat($url); - - // Check that the array looks right. - $this->assertEquals(26, count($ret)); - $this->assertEquals(0, $ret[3]); - $this->assertEquals($ret[2], $ret['mode']); - - $this->assertTrue(file_exists($url)); - $this->assertTrue(is_readable($url)); - $this->assertTrue(is_writeable($url)); - $this->assertFalse(is_link($url)); - $this->assertGreaterThan(0, filemtime($url)); - $this->assertGreaterThan(5, filesize($url)); - - $perm = fileperms($url); - - // Assert that this is a file. Objects are - // *always* marked as files. - $this->assertEquals(0x8000, $perm & 0x8000); - - // Assert writeable by owner. - $this->assertEquals(0x0080, $perm & 0x0080); - - // Assert not world writable. - $this->assertEquals(0, $perm & 0x0002); - - $contents = file_get_contents($url); - $this->assertGreaterThan(5, strlen($contents)); - - $fsCopy = '/tmp/hpcloud-copy-test.txt'; - copy($url, $fsCopy, $this->basicSwiftContext()); - $this->assertTrue(file_exists($fsCopy)); - unlink($fsCopy); - } - - /** - * @depends testFlush - */ - public function testUnlink() - { - $url = $this->newUrl(self::FNAME); - $cxt = $this->basicSwiftContext(); - - $ret = unlink($url, $cxt); - $this->assertTrue($ret); - - $ret2 = unlink($url, $cxt); - $this->assertFalse($ret2); - } - - public function testSetOption() - { - $url = $this->newUrl('fake.foo'); - $fake = fopen($url, 'nope', false, $this->basicSwiftContext()); - - $this->assertTrue(stream_set_blocking($fake, 1)); - - // Returns 0 on success. - $this->assertEquals(0, stream_set_write_buffer($fake, 8192)); - - // Cant set a timeout on a tmp storage: - $this->assertFalse(stream_set_timeout($fake, 10)); - - fclose($fake); - } - - /** - * @depends testUnlink - */ - public function testRename() - { - $url = $this->newUrl('rename.foo'); - $fake = fopen($url, 'w+', false, $this->basicSwiftContext()); - fwrite($fake, 'test'); - fclose($fake); - - $this->assertTrue(file_exists($url)); - - $url2 = $this->newUrl('rename.txt'); - - rename($url, $url2, $this->basicSwiftContext()); - - $this->assertTrue(file_exists($url2)); - $this->assertFalse(file_exists($url)); - - unlink($url2, $this->basicSwiftContext()); - } - - /** - * @depends testUnlink - */ - public function testOpenDir() - { - $urls = array('test1.txt', 'foo/test2.txt', 'foo/test3.txt', 'bar/test4.txt'); - foreach ($urls as $base) { - $url = $this->newUrl($base); - $f = fopen($url, 'c+', false, $this->basicSwiftContext()); - fwrite($f, 'Test.'); - fclose($f); - } - - $dirUrl = $this->newUrl(''); - $dir = opendir($dirUrl, $this->basicSwiftContext()); - - $this->assertTrue(is_resource($dir)); - - return $dir; - - } - - /** - * @depends testOpenDir - */ - public function testReaddir($dir) - { - // Order should be newest to oldest. - $expects = array('bar/', 'foo/', 'test1.txt'); - - $buffer = array(); - while (($entry = readdir($dir)) !== false) { - $should_be = array_shift($expects); - $this->assertEquals($should_be, $entry); - } - $this->assertFalse(readdir($dir)); - - return $dir; - } - /** - * @depends testReaddir - */ - public function testRewindDir($dir) - { - $this->assertFalse(readdir($dir)); - rewinddir($dir); - $this->assertEquals('bar/', readdir($dir)); - - return $dir; - } - - /** - * @depends testRewindDir - */ - public function testCloseDir($dir) - { - $this->assertTrue(is_resource($dir)); - closedir($dir); - - // There is a bug in PHP where a - // resource buffer is not getting cleared. - // So this might return a value even though - // the underlying stream is cleared. - //$this->assertFalse(readdir($dir)); - } - - /** - * @depends testCloseDir - */ - public function testOpenSubdir() - { - // Opening foo we should find test2.txt and test3.txt. - $url = $this->newUrl('foo/'); - $dir = opendir($url, $this->basicSwiftContext()); - - // I don't know why, but these are always returned in - // lexical order. - $this->assertEquals('test2.txt', readdir($dir)); - $this->assertEquals('test3.txt', readdir($dir)); - - $array = scandir($url, -1, $this->basicSwiftContext()); - $this->assertEquals(2, count($array)); - $this->assertEquals('test3.txt', $array[0]); - - } - - /** - * @depends testReaddir - */ - public function testIsdir($dir) - { - // Object names are pathy. If objects exist starting with this path we can - // consider the directory to exist. - $url = $this->newUrl('baz/'); - $this->assertFALSE(is_dir($url)); - - $url = $this->newUrl('foo/'); - $this->assertTRUE(is_dir($url)); - - } - - /** - * @depends testReaddir - */ - public function testMkdir() - { - // Object names are pathy. If no object names start with the a path we can - // consider mkdir passed. If object names exist we should fail mkdir. - $url = $this->newUrl('baz/'); - $this->assertTrue(mkdir($url, 0700, true, $this->basicSwiftContext())); - - // Test the case for an existing directory. - $url = $this->newUrl('foo/'); - $this->assertFalse(mkdir($url, 0700, true, $this->basicSwiftContext())); - } - - /** - * @depends testReaddir - */ - public function testRmdir() - { - // Object names are pathy. If no object names start with the a path we can - // consider rmdir passed. If object names exist we should fail rmdir. - $url = $this->newUrl('baz/'); - $this->assertTrue(rmdir($url, $this->basicSwiftContext())); - - // Test the case for an existing directory. - $url = $this->newUrl('foo/'); - $this->assertFalse(rmdir($url, $this->basicSwiftContext())); - } - -} diff --git a/tests/Tests/ObjectStore/v1/StreamWrapperTest.php b/tests/Tests/ObjectStore/v1/StreamWrapperTest.php deleted file mode 100644 index 6de0160..0000000 --- a/tests/Tests/ObjectStore/v1/StreamWrapperTest.php +++ /dev/null @@ -1,598 +0,0 @@ -container($cname); - } catch (FileNotFoundException $e) { - // The container was never created. - return; - } - - foreach ($container as $object) { - try { - $container->delete($object->name()); - } catch (\Exception $e) {} - } - - $store->deleteContainer($cname); - } - - protected function newUrl($objectName) - { - $scheme = StreamWrapper::DEFAULT_SCHEME; - $cname = self::$settings['openstack.swift.container']; - $cname = urlencode($cname); - - $objectParts = explode('/', $objectName); - for ($i = 0; $i < count($objectParts); ++$i) { - $objectParts[$i] = urlencode($objectParts[$i]); - } - $objectName = implode('/', $objectParts); - - $url = $scheme . '://' . $cname . '/' . $objectName; - - return $url; - } - - /** - * This assumes auth has already been done. - */ - protected function basicSwiftContext($add = array(), $scheme = null) - { - $cname = self::$settings['openstack.swift.container']; - - if (empty($scheme)) { - $scheme = StreamWrapper::DEFAULT_SCHEME; - } - - if (empty(self::$ostore)) { - throw new \Exception('OStore is gone.'); - } - - $params = $add + array( - 'token' => $this->objectStore()->token(), - 'swift_endpoint' => $this->objectStore()->url(), - 'content_type' => self::FTYPE, - 'transport_client' => $this->getTransportClient(), - ); - $cxt = array($scheme => $params); - - return stream_context_create($cxt); - } - - /** - * This performs authentication via context. - */ - protected function authSwiftContext(array $params = [], $scheme = null) - { - if (empty($scheme)) { - $scheme = StreamWrapper::DEFAULT_SCHEME; - } - - $params += [ - 'username' => self::$settings['openstack.identity.username'], - 'password' => self::$settings['openstack.identity.password'], - 'endpoint' => self::$settings['openstack.identity.url'], - 'tenantid' => self::$settings['openstack.identity.tenantId'], - 'content_type' => self::FTYPE, - 'transport_client' => $this->getTransportClient() - ]; - - return stream_context_create([ - $scheme => $params - ]); - } - - /** - * Add additional params to the config. - * - * This allows us to insert credentials into the - * bootstrap config, which in turn allows us to run - * high-level context-less functions like - * file_get_contents(), stat(), and is_file(). - */ - protected function addBootstrapConfig() - { - $opts = array( - 'username' => self::$settings['openstack.identity.username'], - 'password' => self::$settings['openstack.identity.password'], - 'endpoint' => self::$settings['openstack.identity.url'], - 'tenantid' => self::$settings['openstack.identity.tenantId'], - 'token' => $this->objectStore()->token(), - 'swift_endpoint' => $this->objectStore()->url(), - ); - \OpenStack\Bootstrap::setConfiguration($opts); - - } - - // Canary. There are UTF-8 encoding issues in stream wrappers. - public function testStreamContext() - { - // Reset this in case something else left its - // auth token lying around. - \OpenStack\Bootstrap::setConfiguration(array( - 'token' => null, - )); - $cxt = $this->authSwiftContext(); - $array = stream_context_get_options($cxt); - - $opts = $array['swift']; - $endpoint = self::conf('openstack.identity.url'); - - $this->assertEquals($endpoint, $opts['endpoint'], 'A UTF-8 encoding issue.'); - } - - /** - * @depends testStreamContext - */ - public function testRegister() - { - // Canary - $this->assertNotEmpty(StreamWrapper::DEFAULT_SCHEME); - - $klass = '\OpenStack\ObjectStore\v1\Resource\StreamWrapper'; - stream_wrapper_register(StreamWrapper::DEFAULT_SCHEME, $klass); - - $wrappers = stream_get_wrappers(); - - $this->assertContains(StreamWrapper::DEFAULT_SCHEME, $wrappers); - } - - /** - * @depends testRegister - */ - public function testOpenFailureWithoutContext() - { - $cname = self::$settings['openstack.swift.container']; - - // Create a fresh container. - $this->eradicateContainer($cname); - $this->containerFixture(); - - $url = $this->newUrl('foo→/bar.txt'); - $ret = @fopen($url, 'r'); - - $this->assertFalse($ret); - } - - /** - * @depends testRegister - */ - public function testOpen() - { - $cname = self::$settings['openstack.swift.container']; - - // Create a fresh container. - $this->eradicateContainer($cname); - $this->containerFixture(); - - // Simple write test. - $oUrl = $this->newUrl('foo→/test.csv'); - - $res = fopen($oUrl, 'nope', false, $this->authSwiftContext()); - - $this->assertTrue(is_resource($res)); - - $md = stream_get_meta_data($res); - $wrapper = $md['wrapper_data']; - - fclose($res); - - // Now we test the same, but re-using the auth token: - $cxt = $this->basicSwiftContext(array('token' => $wrapper->token())); - $res = fopen($oUrl, 'nope', false, $cxt); - - $this->assertTrue(is_resource($res)); - - fclose($res); - - } - - /** - * @depends testOpen - */ - public function testOpenFailureWithRead() - { - $url = $this->newUrl(__FUNCTION__); - $res = @fopen($url, 'r', false, $this->basicSwiftContext()); - - $this->assertFalse($res); - - } - - // DO we need to test other modes? - - /** - * @depends testOpen - */ - public function testOpenCreateMode() - { - $url = $this->newUrl(self::FNAME); - $res = fopen($url, 'c+', false, $this->basicSwiftContext()); - $this->assertTrue(is_resource($res)); - //fclose($res); - return $res; - } - - /** - * @depends testOpenCreateMode - */ - public function testTell($res) - { - // Sould be at the beginning of the buffer. - $this->assertEquals(0, ftell($res)); - - return $res; - } - - /** - * @depends testTell - */ - public function testWrite($res) - { - $str = 'To be is to be the value of a bound variable. -- Quine'; - fwrite($res, $str); - $this->assertGreaterThan(0, ftell($res)); - - return $res; - } - - /** - * @depends testWrite - */ - public function testStat($res) - { - $stat = fstat($res); - - $this->assertGreaterThan(0, $stat['size']); - - return $res; - } - - /** - * @depends testStat - */ - public function testSeek($res) - { - $then = ftell($res); - rewind($res); - - $now = ftell($res); - - // $now should be 0 - $this->assertLessThan($then, $now); - $this->assertEquals(0, $now); - - fseek($res, 0, SEEK_END); - $final = ftell($res); - - $this->assertEquals($then, $final); - - return $res; - - } - - /** - * @depends testSeek - */ - public function testEof($res) - { - rewind($res); - - $this->assertEquals(0, ftell($res)); - - $this->assertFalse(feof($res)); - - fseek($res, 0, SEEK_END); - $this->assertGreaterThan(0, ftell($res)); - - $read = fread($res, 8192); - - $this->assertEmpty($read); - - $this->assertTrue(feof($res)); - - return $res; - } - - /** - * @depends testEof - */ - public function testFlush($res) - { - $stat1 = fstat($res); - - fflush($res); - - // Grab a copy of the object. - $url = $this->newUrl(self::FNAME); - $newObj = fopen($url, 'r', false, $this->basicSwiftContext()); - - $stat2 = fstat($newObj); - - $this->assertEquals($stat1['size'], $stat2['size']); - - return $res; - } - - /** - * @depends testFlush - */ - public function testStreamGetMetadata($res) - { - // Grab a copy of the object. - $url = $this->newUrl(self::FNAME); - $newObj = fopen($url, 'r', false, $this->basicSwiftContext()); - - $md = stream_get_meta_data($newObj); - //throw new \Exception(print_r($md, true)); - $obj = $md['wrapper_data']->object(); - - $this->assertInstanceOf('\OpenStack\ObjectStore\v1\Resource\RemoteObject', $obj); - - $this->assertEquals(self::FTYPE, $obj->contentType()); - - } - - /** - * @depends testFlush - */ - public function testClose($res) - { - $this->assertTrue(is_resource($res)); - fwrite($res, '~~~~'); - //throw new \Exception(stream_get_contents($res)); - fflush($res); - - // This is occasionally generating seemingly - // spurious PHP errors about Bootstrap::$config. - fclose($res); - - $url = $this->newUrl(self::FNAME); - $res2 = fopen($url, 'r', false, $this->basicSwiftContext()); - $this->assertTrue(is_resource($res2)); - - $contents = stream_get_contents($res2); - fclose($res2); - $this->assertRegExp('/~{4}$/', $contents); - - } - - /** - * @depends testClose - */ - public function testCast() - { - $url = $this->newUrl(self::FNAME); - $res = fopen($url, 'r', false, $this->basicSwiftContext()); - - $read = array($res); - $write = array(); - $except = array(); - $num_changed = stream_select($read, $write, $except, 0); - $this->assertGreaterThan(0, $num_changed); - } - - /** - * @depends testClose - */ - public function testUrlStat() - { - // Add context to the bootstrap config. - $this->addBootstrapConfig(); - - $url = $this->newUrl(self::FNAME); - - $ret = stat($url); - - // Check that the array looks right. - $this->assertEquals(26, count($ret)); - $this->assertEquals(0, $ret[3]); - $this->assertEquals($ret[2], $ret['mode']); - - $this->assertTrue(file_exists($url)); - $this->assertTrue(is_readable($url)); - $this->assertTrue(is_writeable($url)); - $this->assertFalse(is_link($url)); - $this->assertGreaterThan(0, filemtime($url)); - $this->assertGreaterThan(5, filesize($url)); - - $perm = fileperms($url); - - // Assert that this is a file. Objects are - // *always* marked as files. - $this->assertEquals(0x8000, $perm & 0x8000); - - // Assert writeable by owner. - $this->assertEquals(0x0080, $perm & 0x0080); - - // Assert not world writable. - $this->assertEquals(0, $perm & 0x0002); - - $contents = file_get_contents($url); - $this->assertGreaterThan(5, strlen($contents)); - - $fsCopy = '/tmp/hpcloud-copy-test.txt'; - copy($url, $fsCopy, $this->basicSwiftContext()); - $this->assertTrue(file_exists($fsCopy)); - unlink($fsCopy); - } - - /** - * @depends testFlush - */ - public function testUnlink() - { - $url = $this->newUrl(self::FNAME); - $cxt = $this->basicSwiftContext(); - - $ret = unlink($url, $cxt); - $this->assertTrue($ret); - - $ret2 = unlink($url, $cxt); - $this->assertFalse($ret2); - } - - public function testSetOption() - { - $url = $this->newUrl('fake.foo'); - $fake = fopen($url, 'nope', false, $this->basicSwiftContext()); - - $this->assertTrue(stream_set_blocking($fake, 1)); - - // Returns 0 on success. - $this->assertEquals(0, stream_set_write_buffer($fake, 8192)); - - // Cant set a timeout on a tmp storage: - $this->assertFalse(stream_set_timeout($fake, 10)); - - fclose($fake); - } - - /** - * @depends testUnlink - */ - public function testRename() - { - $url = $this->newUrl('rename.foo'); - $fake = fopen($url, 'w+', false, $this->basicSwiftContext()); - fwrite($fake, 'test'); - fclose($fake); - - $this->assertTrue(file_exists($url)); - - $url2 = $this->newUrl('rename.txt'); - - rename($url, $url2, $this->basicSwiftContext()); - - $this->assertTrue(file_exists($url2)); - $this->assertFalse(file_exists($url)); - - unlink($url2, $this->basicSwiftContext()); - } - - /** - * @depends testUnlink - */ - public function testOpenDir() - { - $urls = array('test1.txt', 'foo/test2.txt', 'foo/test3.txt', 'bar/test4.txt'); - foreach ($urls as $base) { - $url = $this->newUrl($base); - $f = fopen($url, 'c+', false, $this->basicSwiftContext()); - fwrite($f, 'Test.'); - fclose($f); - } - - $dirUrl = $this->newUrl(''); - $dir = opendir($dirUrl, $this->basicSwiftContext()); - - $this->assertTrue(is_resource($dir)); - - return $dir; - - } - - /** - * @depends testOpenDir - */ - public function testReaddir($dir) - { - // Order should be newest to oldest. - $expects = array('bar/', 'foo/', 'test1.txt'); - - $buffer = array(); - while (($entry = readdir($dir)) !== false) { - $should_be = array_shift($expects); - $this->assertEquals($should_be, $entry); - } - $this->assertFalse(readdir($dir)); - - return $dir; - } - /** - * @depends testReaddir - */ - public function testRewindDir($dir) - { - $this->assertFalse(readdir($dir)); - rewinddir($dir); - $this->assertEquals('bar/', readdir($dir)); - - return $dir; - } - - /** - * @depends testRewindDir - */ - public function testCloseDir($dir) - { - $this->assertTrue(is_resource($dir)); - closedir($dir); - - // There is a bug in PHP where a - // resource buffer is not getting cleared. - // So this might return a value even though - // the underlying stream is cleared. - //$this->assertFalse(readdir($dir)); - } - - /** - * @depends testCloseDir - */ - public function testOpenSubdir() - { - // Opening foo we should find test2.txt and test3.txt. - $url = $this->newUrl('foo/'); - $dir = opendir($url, $this->basicSwiftContext()); - - // I don't know why, but these are always returned in - // lexical order. - $this->assertEquals('test2.txt', readdir($dir)); - $this->assertEquals('test3.txt', readdir($dir)); - - $array = scandir($url, -1, $this->basicSwiftContext()); - $this->assertEquals(2, count($array)); - $this->assertEquals('test3.txt', $array[0]); - - } -} diff --git a/tests/Tests/TestCase.php b/tests/Tests/TestCase.php index 5836be5..b2444b4 100644 --- a/tests/Tests/TestCase.php +++ b/tests/Tests/TestCase.php @@ -27,17 +27,21 @@ namespace OpenStack\Tests; +use GuzzleHttp\Exception\ClientException; +use OpenStack\Bootstrap; +use OpenStack\Common\Transport\Exception\ResourceNotFoundException; use OpenStack\Identity\v2\IdentityService; use OpenStack\ObjectStore\v1\ObjectStorage; +use OpenStack\Common\Transport\Guzzle\GuzzleAdapter; /** * @ingroup Tests */ -class TestCase extends \PHPUnit_Framework_TestCase +abstract class TestCase extends \PHPUnit_Framework_TestCase { - public static $settings = array(); + public static $settings = []; - public static $ostore = null; + protected $objectStoreService; /** * The IdentityService instance. @@ -48,26 +52,20 @@ class TestCase extends \PHPUnit_Framework_TestCase protected $containerFixture = null; - public static function setUpBeforeClass() + protected static function setConfiguration() { - global $bootstrap_settings; - - if (!isset($bootstrap_settings)) { - $bootstrap_settings = array(); - } - self::$settings = $bootstrap_settings; - - //$this->setTestNamespace('Tests\Units'); if (file_exists('tests/settings.ini')) { - self::$settings += parse_ini_file('tests/settings.ini'); + self::$settings = parse_ini_file('tests/settings.ini'); } else { throw new \Exception('Could not access test/settings.ini'); } - \OpenStack\Autoloader::useAutoloader(); - \OpenStack\Bootstrap::setConfiguration(self::$settings); + Bootstrap::setConfiguration(self::$settings); + } - //parent::__construct($score, $locale, $adapter); + public static function setUpBeforeClass() + { + self::setConfiguration(); } /** @@ -129,11 +127,11 @@ class TestCase extends \PHPUnit_Framework_TestCase protected function objectStore($reset = false) { - if ($reset || empty(self::$ostore)) { - self::$ostore = self::createObjectStoreService(); + if ($reset || !$this->objectStoreService) { + $this->objectStoreService = self::createObjectStoreService(); } - return self::$ostore; + return $this->objectStoreService; } /** @@ -148,15 +146,11 @@ class TestCase extends \PHPUnit_Framework_TestCase try { $store->createContainer($cname); $this->containerFixture = $store->container($cname); - - } - // This is why PHP needs 'finally'. - catch (\Exception $e) { + } catch (\Exception $e) { // Delete the container. $store->deleteContainer($cname); throw $e; } - } return $this->containerFixture; @@ -177,9 +171,8 @@ class TestCase extends \PHPUnit_Framework_TestCase $store = $this->objectStore(); try { $container = $store->container($cname); - } - // The container was never created. - catch (\OpenStack\Common\Transport\Exception\FileNotFoundException $e) { + } catch (ResourceNotFoundException $e) { + // The container was never created. return; } @@ -202,6 +195,7 @@ class TestCase extends \PHPUnit_Framework_TestCase { if (is_null(self::$httpClient)) { $options = []; + if (isset(self::$settings['transport.proxy'])) { $options['proxy'] = self::$settings['transport.proxy']; } @@ -215,7 +209,9 @@ class TestCase extends \PHPUnit_Framework_TestCase $options['timeout'] = self::$settings['transport.timeout']; } - self::$httpClient = new self::$settings['transport']($options); + self::$httpClient = GuzzleAdapter::create([ + 'defaults' => $options + ]); } return self::$httpClient; @@ -235,7 +231,7 @@ class TestCase extends \PHPUnit_Framework_TestCase $container = $store->container($cname); } // The container was never created. - catch (\OpenStack\Common\Transport\Exception\FileNotFoundException $e) { + catch (ResourceNotFoundException $e) { return; } @@ -249,4 +245,4 @@ class TestCase extends \PHPUnit_Framework_TestCase $store->deleteContainer($cname); } -} +} \ No newline at end of file diff --git a/tests/example.settings.ini b/tests/example.settings.ini index 27b87a3..eee250e 100644 --- a/tests/example.settings.ini +++ b/tests/example.settings.ini @@ -27,17 +27,17 @@ openstack.swift.key = abcdef123456 openstack.swift.url = https://region-a.geo-1.identity.hpcloudsvc.com:35357/auth/v1.0/ ; Container used for testing. -openstack.swift.container = "I♡HPCloud" +openstack.swift.container = "test" ; Specified region name to test against. -openstack.swift.region = "region-a.geo-1" +openstack.swift.region = "Region1" ;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Configuration Parameters ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; The HTTP Transport Client to use. -transport = "\OpenStack\Common\Transport\GuzzleClient" +transport = "OpenStack\Common\Transport\Guzzle\GuzzleAdapter" ; If behind a proxy set to the https proxy server communications need ; to pass through.