New Library

This commit is contained in:
EoleDev 2016-03-09 15:36:02 +01:00
parent 5c6f6c97b7
commit c479658f0b
83 changed files with 5788 additions and 0 deletions

View file

@ -0,0 +1,74 @@
<?php
namespace OpenCloud\Test\Common\Api;
use OpenCloud\Common\Api\Operation;
use OpenCloud\Common\Api\Parameter;
use OpenCloud\Test\Fixtures\ComputeV2Api;
class OperationTest extends \PHPUnit_Framework_TestCase
{
private $operation;
public function setUp()
{
$def = (new ComputeV2Api())->postServer();
$this->operation = new Operation($def);
}
public function test_it_reveals_whether_params_are_set_or_not()
{
$this->assertFalse($this->operation->hasParam('foo'));
$this->assertTrue($this->operation->hasParam('name'));
}
public function test_it_gets_params()
{
$this->assertInstanceOf(Parameter::class, $this->operation->getParam('name'));
}
public function test_it_validates_params()
{
$this->assertTrue($this->operation->validate([
'name' => 'foo',
'imageId' => 'bar',
'flavorId' => 'baz',
]));
}
/**
* @expectedException \Exception
*/
public function test_exceptions_are_propagated()
{
$this->assertFalse($this->operation->validate([
'name' => true,
'imageId' => 'bar',
'flavorId' => 'baz',
]));
}
/**
* @expectedException \Exception
*/
public function test_an_exception_is_thrown_when_user_does_not_provide_required_options()
{
$this->operation->validate([]);
}
/**
* @expectedException \Exception
*/
public function test_it_throws_exception_when_user_provides_undefined_options()
{
$userData = ['name' => 'new_server', 'undefined_opt' => 'bah'];
$this->operation->validate($userData);
}
public function test_it_gets_json_key()
{
$this->assertEquals('server', $this->operation->getJsonKey());
}
}

View file

@ -0,0 +1,137 @@
<?php
namespace OpenCloud\Test\Common\Api;
use function GuzzleHttp\Psr7\uri_for;
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Uri;
use OpenCloud\Common\Api\Operator;
use OpenCloud\Common\Resource\AbstractResource;
use OpenCloud\Common\Resource\ResourceInterface;
use OpenCloud\Compute\v2\Models\Server;
use OpenCloud\Test\Fixtures\ComputeV2Api;
use OpenCloud\Test\TestCase;
use Prophecy\Argument;
class OperatorTest extends TestCase
{
private $operator;
private $def;
public function setUp()
{
parent::setUp();
$this->rootFixturesDir = __DIR__;
$this->def = [
'method' => 'GET',
'path' => 'test',
'params' => [],
];
$this->operator = new TestOperator($this->client->reveal(), new ComputeV2Api());
}
public function test_it_returns_operations()
{
$this->assertInstanceOf(
'OpenCloud\Common\Api\Operation',
$this->operator->getOperation($this->def, [])
);
}
public function test_it_sends_a_request_when_operations_are_executed()
{
$this->client->request('GET', 'test', ['headers' => []])->willReturn(new Request('GET', 'test'));
$this->operator->execute($this->def, []);
}
public function test_it_sends_a_request_when_async_operations_are_executed()
{
$this->client->requestAsync('GET', 'test', ['headers' => []])->willReturn(new Promise());
$this->operator->executeAsync($this->def, []);
}
public function test_it_returns_a_model_instance()
{
$this->assertInstanceOf(ResourceInterface::class, $this->operator->model(TestResource::class));
}
public function test_it_populates_models_from_response()
{
$this->assertInstanceOf(ResourceInterface::class, $this->operator->model(TestResource::class, new Response(200)));
}
public function test_it_populates_models_from_arrays()
{
$data = ['flavor' => [], 'image' => []];
$this->assertInstanceOf(ResourceInterface::class, $this->operator->model(TestResource::class, $data));
}
public function test_it_wraps_sequential_ops_in_promise_when_async_is_appended_to_method_name()
{
$promise = $this->operator->createAsync('something');
$this->assertInstanceOf(Promise::class, $promise);
$promise->then(function ($val) {
$this->assertEquals('Created something', $val);
});
$promise->wait();
}
/**
* @expectedException \RuntimeException
*/
public function test_it_throws_exception_when_async_is_called_on_a_non_existent_method()
{
$this->operator->fooAsync();
}
public function test_it_retrieves_base_http_url()
{
$returnedUri = uri_for('http://foo.com');
$this->client->getConfig('base_uri')->shouldBeCalled()->willReturn($returnedUri);
$uri = $this->operator->testBaseUri();
$this->assertInstanceOf(Uri::class, $uri);
$this->assertEquals($returnedUri, $uri);
}
/**
* @expectedException \Exception
*/
public function test_undefined_methods_result_in_error()
{
$this->operator->foo();
}
}
class TestResource extends AbstractResource
{
}
class TestOperator extends Operator
{
public function testBaseUri()
{
return $this->getHttpBaseUrl();
}
public function create($str)
{
return 'Created ' . $str;
}
public function fail()
{
}
}

View file

@ -0,0 +1,203 @@
<?php
namespace OpenCloud\Test\Common\Api;
use OpenCloud\Common\Api\Parameter;
use OpenCloud\Test\Fixtures\ComputeV2Api;
class ParameterTest extends \PHPUnit_Framework_TestCase
{
const PARAMETER_CLASS = 'OpenCloud\Common\Api\Parameter';
private $param;
private $data;
private $api;
public function setUp()
{
$this->api = new ComputeV2Api();
$this->data = $this->api->postServer()['params']['name'] + ['name' => 'name'];
$this->param = new Parameter($this->data);
}
/**
* @expectedException \RuntimeException
*/
public function test_exception_is_thrown_for_invalid_locations()
{
$data = $this->data;
$data['location'] = 'foo';
new Parameter($data);
}
public function test_it_should_provide_access_to_a_name()
{
$this->assertEquals($this->data['name'], $this->param->getName());
}
public function test_it_should_use_sentAs_alias_for_name_if_one_is_set()
{
$data = $this->data + ['sentAs' => 'foo'];
$param = new Parameter($data);
$this->assertEquals($data['sentAs'], $param->getName());
}
public function test_it_indicates_whether_it_is_required_or_not()
{
$this->assertTrue($this->param->isRequired());
}
public function test_it_indicates_its_item_schema()
{
$data = $this->api->postServer()['params']['networks'] + ['name' => 'networks'];
$param = new Parameter($data);
$this->assertInstanceOf(self::PARAMETER_CLASS, $param->getItemSchema());
}
public function test_it_allows_property_retrieval()
{
$definition = $this->api->postServer()['params']['networks']['items'] + ['name' => 'network'];
$param = new Parameter($definition);
$this->assertInstanceOf(self::PARAMETER_CLASS, $param->getProperty('uuid'));
}
public function test_it_indicates_its_path()
{
$path = 'foo.bar.baz';
$param = new Parameter($this->data + ['path' => $path]);
$this->assertEquals($path, $param->getPath());
}
public function test_it_verifies_a_given_location_with_a_boolean()
{
$this->assertFalse($this->param->hasLocation('foo'));
$this->assertTrue($this->param->hasLocation('json'));
}
public function test_it_should_return_true_when_required_attributes_are_provided_and_match_their_definitions()
{
$this->assertTrue($this->param->validate('TestName'));
}
/**
* @expectedException \Exception
*/
public function test_it_throws_exception_when_values_do_not_match_their_definition_types()
{
$data = $this->api->postServer()['params']['networks'] + ['name' => 'networks'];
$param = new Parameter($data);
$param->validate('a_network!'); // should be an array
}
/**
* @expectedException \Exception
*/
public function test_it_throws_exception_when_deeply_nested_values_have_wrong_types()
{
$data = $this->api->postServer()['params']['networks'] + ['name' => 'networks'];
$param = new Parameter($data);
$param->validate(['name' => false]); // value should be a string, not bool
}
public function test_metadata_properties_are_handled_differently()
{
$params = [
'name' => 'metadata',
'type' => 'object',
'properties' => [
'type' => 'string',
],
];
$userValues = ['some' => 'value'];
$param = new Parameter($params);
$this->assertTrue($param->validate($userValues));
}
public function test_it_passes_validation_when_array_values_pass()
{
$params = [
'name' => 'foo',
'type' => 'array',
'items' => ['type' => 'string'],
];
$userVals = ['1', '2', '3'];
$param = new Parameter($params);
$this->assertTrue($param->validate($userVals));
}
/**
* @expectedException \Exception
*/
public function test_an_exception_is_thrown_when_an_undefined_property_is_provided()
{
$params = ['type' => 'object', 'properties' => ['foo' => ['type' => 'string']]];
$userVals = ['bar' => 'baz'];
$param = new Parameter($params);
$param->validate($userVals);
}
public function test_it_passes_validation_when_all_subproperties_pass()
{
$params = ['type' => 'object', 'properties' => ['foo' => ['type' => 'string']]];
$userVals = ['foo' => 'baz'];
$param = new Parameter($params);
$this->assertTrue($param->validate($userVals));
}
public function test_it_sets_name()
{
$this->param->setName('foo');
$this->assertEquals($this->param->getName(), 'foo');
}
public function test_it_gets_property()
{
$property = new Parameter([
'name' => 'metadata',
'properties' => [
'type' => 'string',
'prefix' => 'foo',
],
]);
$prop = $property->getProperty('metadata');
$this->assertInstanceOf(Parameter::class, $prop);
$this->assertEquals('foo', $prop->getPrefix());
}
public function test_it_gets_prefixed_name()
{
$property = new Parameter([
'name' => 'metadata',
'prefix' => 'foo-',
]);
$this->assertEquals('foo-metadata', $property->getPrefixedName());
}
/**
* @expectedException \Exception
*/
public function test_exception_is_thrown_when_value_is_not_in_enum_list()
{
$data = $this->data;
$data['enum'] = ['foo'];
$param = new Parameter($data);
$param->validate('blah');
}
}

View file

@ -0,0 +1,25 @@
<?php
return [
'method' => 'POST',
'path' => 'something',
'params' => [
'name' => [
'type' => 'string',
'location' => 'header',
'sentAs' => 'X-Foo-Name'
],
'age' => [
'type' => 'integer',
'location' => 'header'
],
'metadata' => [
'type' => 'object',
'location' => 'header',
'items' => [
'prefix' => 'X-Meta-'
]
],
'other' => ['type' => 'string'] // should not be a header
],
];

View file

@ -0,0 +1,27 @@
<?php
return [
'method' => 'POST',
'path' => 'something',
'params' => [
'name' => [
'type' => 'string',
'sentAs' => 'server_name',
],
'other' => [
'type' => 'array',
'sentAs' => 'other_params',
'items' => [
'type' => 'string'
]
],
'etc' => [
'type' => 'object',
'sentAs' => 'etcetc',
'properties' => [
'dob' => ['type' => 'string'],
'age' => ['type' => 'integer', 'sentAs' => 'current_age'],
]
],
],
];

View file

@ -0,0 +1,59 @@
<?php
namespace OpenCloud\Test\Common;
use OpenCloud\Common\ArrayAccessTrait;
use OpenCloud\Test\TestCase;
class ArrayAccessTraitTest extends TestCase
{
private $aa;
public function setUp()
{
$this->aa = new ArrayAccess();
}
public function test_offset_is_set()
{
$this->aa->offsetSet('foo', 'bar');
$this->assertEquals(['foo' => 'bar'], $this->aa->getElements());
}
public function test_it_appends_if_no_key_is_set()
{
$this->aa->offsetSet(null, 'bar');
$this->assertEquals(['bar'], $this->aa->getElements());
}
public function test_if_checks_if_offset_exists()
{
$this->aa->offsetSet('bar', 'foo');
$this->assertTrue($this->aa->offsetExists('bar'));
$this->assertFalse($this->aa->offsetExists('baz'));
}
public function test_if_gets_offset()
{
$this->aa->offsetSet('bar', 'foo');
$this->assertEquals('foo', $this->aa->offsetGet('bar'));
$this->assertNull($this->aa->offsetGet('baz'));
}
public function test_it_unsets_offset()
{
$this->aa->offsetSet('bar', 'foo');
$this->aa->offsetUnset('bar');
$this->assertNull($this->aa->offsetGet('bar'));
}
}
class ArrayAccess
{
use ArrayAccessTrait;
public function getElements()
{
return $this->internalState;
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace OpenCloud\Test\Common\Auth;
use GuzzleHttp\Psr7\Request;
use OpenCloud\Common\Auth\AuthHandler;
use OpenCloud\Common\Auth\Token;
use OpenCloud\Test\TestCase;
use Psr\Http\Message\RequestInterface;
class AuthHandlerTest extends TestCase
{
const TOKEN_ID = 'tokenId';
private $generator;
private $handler;
public function setUp()
{
$this->generator = function () {
$token = $this->prophesize(FakeToken::class);
$token->getId()->shouldBeCalled()->willReturn(self::TOKEN_ID);
return $token->reveal();
};
$this->handler = function (RequestInterface $r) {
return $r;
};
$this->handler = new AuthHandler($this->handler, $this->generator);
}
public function test_it_should_bypass_auth_http_requests()
{
// Fake a Keystone request
$request = new Request('POST', 'https://my-openstack.org:5000/v2.0/tokens');
$this->assertEquals($request, call_user_func_array($this->handler, [$request, []]));
}
public function test_it_should_generate_a_new_token_if_the_current_token_is_either_expired_or_not_set()
{
$token = $this->prophesize(Token::class);
// force the mock token to indicate that its expired
$token->getId()->willReturn('');
$token->hasExpired()->willReturn(true);
$request = new Request('GET', '');
$handler = new AuthHandler($this->handler, $this->generator, $token->reveal());
$handler($request, []);
}
}
class FakeToken implements Token {
public function getId() {}
public function hasExpired() {}
}

View file

@ -0,0 +1,16 @@
<?php
namespace OpenCloud\Test\Common;
class DateTime extends \DateTime
{
public static function factory($time)
{
return new static($time);
}
public function toIso8601()
{
return $this->format(self::ISO8601);
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace OpenCloud\Test\Common\Error;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use OpenCloud\Common\Error\BadResponseError;
use OpenCloud\Test\TestCase;
class BadResponseErrorTest extends TestCase
{
private $e;
public function setUp()
{
$this->e = new BadResponseError();
}
public function test_it_gets_request()
{
$r = new Request('GET', '');
$this->e->setRequest($r);
$this->assertEquals($this->e->getRequest(), $r);
}
public function test_it_gets_response()
{
$r = new Response(500);
$this->e->setResponse($r);
$this->assertEquals($this->e->getResponse(), $r);
}
}

View file

@ -0,0 +1,117 @@
<?php
namespace OpenCloud\Test\Common\Error;
use function GuzzleHttp\Psr7\{stream_for,str};
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Psr7\{Request,Response};
use OpenCloud\Common\Error\{BadResponseError,Builder,UserInputError};
class BuilderTest extends \PHPUnit_Framework_TestCase
{
private $builder;
private $client;
public function __construct()
{
$this->client = $this->prophesize(ClientInterface::class);
$this->builder = new Builder($this->client->reveal());
}
public function test_it_injects_client()
{
$this->assertInstanceOf(Builder::class, new Builder($this->client->reveal()));
}
public function test_it_builds_http_errors()
{
$request = new Request('POST', '/servers');
$response = new Response(400, [], stream_for('Invalid parameters'));
$requestStr = trim($this->builder->str($request));
$responseStr = trim($this->builder->str($response));
$errorMessage = <<<EOT
HTTP Error
~~~~~~~~~~
The remote server returned a "400 Bad Request" error for the following transaction:
Request
~~~~~~~
$requestStr
Response
~~~~~~~~
$responseStr
Further information
~~~~~~~~~~~~~~~~~~~
Please ensure that your input values are valid and well-formed. Visit http://docs.php-opencloud.com/en/latest/http-codes for more information about debugging HTTP status codes, or file a support issue on https://github.com/php-opencloud/openstack/issues.
EOT;
$e = new BadResponseError($errorMessage);
$e->setRequest($request);
$e->setResponse($response);
$this->assertEquals($e, $this->builder->httpError($request, $response));
}
public function test_it_builds_user_input_errors()
{
$expected = 'A well-formed string';
$value = ['foo' => true];
$link = 'http://docs.php-opencloud.com/en/latest/index.html';
$errorMessage = <<<EOT
User Input Error
~~~~~~~~~~~~~~~~
A well-formed string was expected, but the following value was passed in:
Array
(
[foo] => 1
)
Please ensure that the value adheres to the expectation above. Visit $link for more information about input arguments. If you run into trouble, please open a support issue on https://github.com/php-opencloud/openstack/issues.
EOT;
$this->client
->request('HEAD', $link)
->shouldBeCalled()
->willReturn(new Response(200));
$e = new UserInputError($errorMessage);
$this->assertEquals($e, $this->builder->userInputError($expected, $value, 'index.html'));
}
public function test_dead_links_are_ignored()
{
$expected = 'A well-formed string';
$value = ['foo' => true];
$errorMessage = <<<EOT
User Input Error
~~~~~~~~~~~~~~~~
A well-formed string was expected, but the following value was passed in:
Array
(
[foo] => 1
)
Please ensure that the value adheres to the expectation above. If you run into trouble, please open a support issue on https://github.com/php-opencloud/openstack/issues.
EOT;
$this->client
->request('HEAD', 'http://docs.php-opencloud.com/en/latest/sdffsda')
->shouldBeCalled()
->willThrow(ClientException::class);
$e = new UserInputError($errorMessage);
$this->assertEquals($e, $this->builder->userInputError($expected, $value, 'sdffsda'));
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace OpenCloud\Test\Common;
use OpenCloud\Common\HydratorStrategyTrait;
use OpenCloud\Test\TestCase;
class HydratorStrategyTraitTest extends TestCase
{
private $fixture;
public function setUp()
{
$this->fixture = new Fixture();
}
public function test_it_hydrates()
{
$data = ['foo' => 1, 'bar' => 2, 'baz' => 3, 'boo' => 4];
$this->fixture->hydrate($data);
$this->assertEquals(1, $this->fixture->foo);
$this->assertEquals(2, $this->fixture->getBar());
$this->assertEquals(3, $this->fixture->getBaz());
}
public function test_it_hydrates_aliases()
{
$this->fixture->hydrate(['FOO!' => 1], ['FOO!' => 'foo']);
$this->assertEquals(1, $this->fixture->foo);
}
}
class Fixture
{
public $foo;
protected $bar;
private $baz;
use HydratorStrategyTrait;
public function getBar()
{
return $this->bar;
}
public function getBaz()
{
return $this->baz;
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace OpenCloud\Test\Common;
use OpenCloud\Common\JsonPath;
class JsonPathTest extends \PHPUnit_Framework_TestCase
{
private $jsonPath;
public function setUp()
{
$this->jsonPath = new JsonPath([]);
}
public function test_it_sets_values_according_to_paths()
{
$this->jsonPath->set('foo.bar.baz', 'VALUE');
$expected = [
'foo' => [
'bar' => [
'baz' => 'VALUE',
]
]
];
$this->assertEquals($expected, $this->jsonPath->getStructure());
}
public function test_it_sets_arrays_according_to_paths()
{
$jsonPath = new JsonPath([
'foo' => [
'bar' => [
'value' => 'VALUE',
]
]
]);
$jsonPath->set('foo.bar.items', ['item_1', 'item_2']);
$expected = [
'foo' => [
'bar' => [
'value' => 'VALUE',
'items' => ['item_1', 'item_2'],
]
]
];
$this->assertEquals($expected, $jsonPath->getStructure());
}
public function test_it_gets_values_according_to_paths()
{
$jsonPath = new JsonPath([
'foo' => [
'bar' => [
'baz' => 'VALUE_1',
'lol' => 'VALUE_2',
]
]
]);
$this->assertEquals('VALUE_1', $jsonPath->get('foo.bar.baz'));
$this->assertEquals('VALUE_2', $jsonPath->get('foo.bar.lol'));
$this->assertNull($jsonPath->get('foo.bar.boo'));
}
}

View file

@ -0,0 +1,231 @@
[
{ "comment": "empty list, empty docs",
"doc": {},
"patch": [],
"expected": {} },
{ "comment": "empty patch list",
"doc": {"foo": 1},
"patch": [],
"expected": {"foo": 1} },
{ "comment": "rearrangements OK?",
"doc": {"foo": 1, "bar": 2},
"patch": [],
"expected": {"bar":2, "foo": 1} },
{ "comment": "rearrangements OK? How about one level down ... array",
"doc": [{"foo": 1, "bar": 2}],
"patch": [],
"expected": [{"bar":2, "foo": 1}] },
{ "comment": "rearrangements OK? How about one level down...",
"doc": {"foo":{"foo": 1, "bar": 2}},
"patch": [],
"expected": {"foo":{"bar":2, "foo": 1}} },
{ "comment": "toplevel array",
"doc": [],
"patch": [{"op": "add", "path": "/0", "value": "foo"}],
"expected": ["foo"] },
{ "comment": "toplevel array, no change",
"doc": ["foo"],
"patch": [],
"expected": ["foo"] },
{ "comment": "toplevel object, numeric string",
"doc": {},
"patch": [{"op": "add", "path": "/foo", "value": "1"}],
"expected": {"foo":"1"} },
{ "comment": "toplevel object, integer",
"doc": {},
"patch": [{"op": "add", "path": "/foo", "value": 1}],
"expected": {"foo":1} },
{ "comment": "Toplevel scalar values OK?",
"doc": "foo",
"patch": [{"op": "replace", "path": "", "value": "bar"}],
"expected": "bar",
"disabled": true },
{ "comment": "Add, / target",
"doc": {},
"patch": [ {"op": "add", "path": "/", "value":1 } ],
"expected": {"":1} },
{ "comment": "Add composite value at top level",
"doc": {"foo": 1},
"patch": [{"op": "add", "path": "/bar", "value": [1, 2]}],
"expected": {"foo": 1, "bar": [1, 2]} },
{ "comment": "Add into composite value",
"doc": {"foo": 1, "baz": [{"qux": "hello"}]},
"patch": [{"op": "add", "path": "/baz/0/foo", "value": "world"}],
"expected": {"foo": 1, "baz": [{"qux": "hello", "foo": "world"}]} },
{ "doc": {"bar": [1, 2]},
"patch": [{"op": "add", "path": "/bar/8", "value": "5"}],
"error": "Out of bounds (upper)" },
{ "doc": {"bar": [1, 2]},
"patch": [{"op": "add", "path": "/bar/-1", "value": "5"}],
"error": "Out of bounds (lower)" },
{ "doc": {"foo": 1},
"patch": [{"op": "add", "path": "/bar", "value": true}],
"expected": {"foo": 1, "bar": true} },
{ "doc": {"foo": 1},
"patch": [{"op": "add", "path": "/bar", "value": false}],
"expected": {"foo": 1, "bar": false} },
{ "doc": {"foo": 1},
"patch": [{"op": "add", "path": "/bar", "value": null}],
"expected": {"foo": 1, "bar": null} },
{ "comment": "0 can be an array index or object element name",
"doc": {"foo": 1},
"patch": [{"op": "add", "path": "/0", "value": "bar"}],
"expected": {"foo": 1, "0": "bar" } },
{ "doc": ["foo"],
"patch": [{"op": "add", "path": "/1", "value": "bar"}],
"expected": ["foo", "bar"] },
{ "doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/1", "value": "bar"}],
"expected": ["foo", "bar", "sil"] },
{ "doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/0", "value": "bar"}],
"expected": ["bar", "foo", "sil"] },
{ "comment": "push item to array via last index + 1",
"doc": ["foo", "sil"],
"patch": [{"op":"add", "path": "/2", "value": "bar"}],
"expected": ["foo", "sil", "bar"] },
{ "comment": "add item to array at index > length should fail",
"doc": ["foo", "sil"],
"patch": [{"op":"add", "path": "/3", "value": "bar"}],
"error": "index is greater than number of items in array" },
{ "doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/bar", "value": 42}],
"error": "Object operation on array target" },
{ "doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/1", "value": ["bar", "baz"]}],
"expected": ["foo", ["bar", "baz"], "sil"],
"comment": "value in array add not flattened" },
{ "doc": {"foo": 1, "bar": [1, 2, 3, 4]},
"patch": [{"op": "remove", "path": "/bar"}],
"expected": {"foo": 1} },
{ "doc": {"foo": 1, "baz": [{"qux": "hello"}]},
"patch": [{"op": "remove", "path": "/baz/0/qux"}],
"expected": {"foo": 1, "baz": [{}]} },
{ "doc": {"foo": 1, "baz": [{"qux": "hello"}]},
"patch": [{"op": "replace", "path": "/foo", "value": [1, 2, 3, 4]}],
"expected": {"foo": [1, 2, 3, 4], "baz": [{"qux": "hello"}]} },
{ "doc": {"foo": [1, 2, 3, 4], "baz": [{"qux": "hello"}]},
"patch": [{"op": "replace", "path": "/baz/0/qux", "value": "world"}],
"expected": {"foo": [1, 2, 3, 4], "baz": [{"qux": "world"}]} },
{ "doc": ["foo"],
"patch": [{"op": "replace", "path": "/0", "value": "bar"}],
"expected": ["bar"] },
{ "doc": [""],
"patch": [{"op": "replace", "path": "/0", "value": 0}],
"expected": [0] },
{ "doc": [""],
"patch": [{"op": "replace", "path": "/0", "value": true}],
"expected": [true] },
{ "doc": [""],
"patch": [{"op": "replace", "path": "/0", "value": false}],
"expected": [false] },
{ "doc": [""],
"patch": [{"op": "replace", "path": "/0", "value": null}],
"expected": [null] },
{ "doc": ["foo", "sil"],
"patch": [{"op": "replace", "path": "/1", "value": ["bar", "baz"]}],
"expected": ["foo", ["bar", "baz"]],
"comment": "value in array replace not flattened" },
{ "comment": "replace whole document",
"disabled": true,
"doc": {"foo": "bar"},
"patch": [{"op": "replace", "path": "", "value": {"baz": "qux"}}],
"expected": {"baz": "qux"} },
{ "doc": {"foo": null},
"patch": [{"op": "replace", "path": "/foo", "value": "truthy"}],
"expected": {"foo": "truthy"},
"comment": "null value should be valid obj property to be replaced with something truthy" },
{ "doc": {"foo": null},
"patch": [{"op": "remove", "path": "/foo"}],
"expected": {},
"comment": "null value should be valid obj property to be removed" },
{ "doc": {"foo": "bar"},
"patch": [{"op": "replace", "path": "/foo", "value": null}],
"expected": {"foo": null},
"comment": "null value should still be valid obj property replace other value" },
{ "comment": "test remove with bad number should fail",
"doc": {"foo": 1, "baz": [{"qux": "hello"}]},
"patch": [{"op": "remove", "path": "/baz/1e0/qux"}],
"error": "remove op shouldn't remove from array with bad number" },
{ "comment": "test remove on array",
"doc": [1, 2, 3, 4],
"patch": [{"op": "remove", "path": "/0"}],
"expected": [2, 3, 4] },
{ "comment": "test repeated removes",
"doc": [1, 2, 3, 4],
"patch": [{ "op": "remove", "path": "/1" },
{ "op": "remove", "path": "/3" }],
"expected": [1, 3] },
{ "comment": "test remove with bad index should fail",
"doc": [1, 2, 3, 4],
"patch": [{"op": "remove", "path": "/1e0"}],
"error": "remove op shouldn't remove from array with bad number" },
{ "comment": "test replace with bad number should fail",
"doc": [""],
"patch": [{"op": "replace", "path": "/1e0", "value": false}],
"error": "replace op shouldn't replace in array with bad number" },
{ "comment": "test add with bad number should fail",
"doc": ["foo", "sil"],
"patch": [{"op": "add", "path": "/1e0", "value": "bar"}],
"error": "add op shouldn't add to array with bad number" },
{ "comment": "missing 'value' parameter to add",
"doc": [ 1 ],
"patch": [ { "op": "add", "path": "/-" } ],
"error": "missing 'value' parameter" },
{ "comment": "missing 'value' parameter to replace",
"doc": [ 1 ],
"patch": [ { "op": "replace", "path": "/0" } ],
"error": "missing 'value' parameter" },
{ "comment": "unrecognized op should fail",
"doc": {"foo": 1},
"patch": [{"op": "spam", "path": "/foo", "value": 1}],
"error": "Unrecognized op 'spam'" }
]

View file

@ -0,0 +1,28 @@
<?php
namespace OpenCloud\Test\Common\JsonSchema;
use OpenCloud\Common\JsonSchema\JsonPatch;
use OpenCloud\Test\TestCase;
class JsonPatchTest extends TestCase
{
public function testAll()
{
$fixtures = json_decode(file_get_contents(__DIR__ . '/Fixtures/jsonPatchTests.json'));
foreach ($fixtures as $fixture) {
if (isset($fixture->disabled) || !isset($fixture->expected)) {
continue;
}
$actual = JsonPatch::diff($fixture->doc, $fixture->expected);
$this->assertEquals(
json_encode($fixture->patch, JSON_UNESCAPED_SLASHES),
json_encode($actual, JSON_UNESCAPED_SLASHES),
isset($fixture->comment) ? sprintf("Failed asserting test: %s\n", $fixture->comment) : ''
);
}
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace OpenCloud\Test\Common\JsonSchema;
use JsonSchema\Validator;
use OpenCloud\Common\JsonSchema\Schema;
use OpenCloud\Test\TestCase;
class SchemaTest extends TestCase
{
/** @var Schema */
private $schema;
/** @var Validator */
private $validator;
private $body;
public function setUp()
{
$this->body = [
'properties' => [
'foo' => (object)[],
'bar' => (object)[],
'baz' => (object)['readOnly' => true],
],
];
$this->validator = $this->prophesize(Validator::class);
$this->schema = new Schema($this->body, $this->validator->reveal());
}
public function test_it_gets_errors()
{
$this->validator->getErrors()
->shouldBeCalled()
->willReturn([]);
$this->assertEquals([], $this->schema->getErrors());
}
public function test_it_gets_error_string()
{
$this->validator->getErrors()
->shouldBeCalled()
->willReturn([['property' => 'foo', 'message' => 'bar']]);
$errorMsg = sprintf("Provided values do not validate. Errors:\n[foo] bar\n");
$this->assertEquals($errorMsg, $this->schema->getErrorString());
}
public function test_it_gets_property_paths()
{
$this->assertEquals(['/foo', '/bar', '/baz'], $this->schema->getPropertyPaths());
}
public function test_it_ignores_readOnly_attrs()
{
$expected = (object)[
'foo' => true,
'bar' => false,
];
$subject = (object)[
'foo' => true,
'bar' => false,
'baz' => true,
];
$this->assertEquals((object)$expected, $this->schema->normalizeObject((object)$subject, []));
}
public function test_it_stocks_aliases()
{
$subject = (object)[
'fooAlias' => true,
'bar' => false,
'other' => true,
];
$expected = (object)[
'foo' => true,
'bar' => false,
];
$this->assertEquals($expected, $this->schema->normalizeObject($subject, ['foo' => 'fooAlias', 'bar' => 'lol']));
}
public function test_it_validates()
{
$this->validator->check([], (object) $this->body)->shouldBeCalled();
$this->schema->validate([]);
}
public function test_it_checks_validity()
{
$this->validator->isValid()->shouldBeCalled();
$this->schema->isValid();
}
}

View file

@ -0,0 +1,161 @@
<?php
namespace OpenCloud\Test\Common\Resource;
use function GuzzleHttp\Psr7\stream_for;
use GuzzleHttp\Psr7\Response;
use OpenCloud\Common\Resource\AbstractResource;
use OpenCloud\Common\Resource\Generator;
use OpenCloud\Test\Fixtures\ComputeV2Api;
use OpenCloud\Test\TestCase;
use Prophecy\Argument;
class AbstractResourceTest extends TestCase
{
private $resource;
public function setUp()
{
parent::setUp();
$this->rootFixturesDir = __DIR__;
$this->resource = new TestResource($this->client->reveal(), new ComputeV2Api());
}
public function test_it_populates_from_response()
{
$response = new Response(200, ['Content-Type' => 'application/json'], stream_for(
json_encode(['foo' => ['bar' => '1']])
));
$this->resource->populateFromResponse($response);
$this->assertEquals('1', $this->resource->bar);
}
public function test_it_populates_datetimes_from_arrays()
{
$dt = new \DateTimeImmutable('2015');
$this->resource->populateFromArray(['created' => '2015']);
$this->assertEquals($this->resource->created, $dt);
}
public function test_it_populates_arrays_from_arrays()
{
$this->resource->populateFromArray(['children' => [$this->resource, $this->resource]]);
$this->assertInstanceOf(TestResource::class, $this->resource->children[0]);
}
public function test_it_gets_attrs()
{
$this->resource->bar = 'foo';
$this->assertEquals(['bar' => 'foo'], $this->resource->getAttrs(['bar']));
}
public function test_it_executes_with_state()
{
$this->resource->id = 'foo';
$this->resource->bar = 'bar';
$expectedJson = ['id' => 'foo', 'bar' => 'bar'];
$this->setupMock('GET', 'foo', $expectedJson, [], new Response(204));
$this->resource->executeWithState((new ComputeV2Api())->test());
}
public function test_it_executes_operations_until_a_204_is_received()
{
$this->client
->request('GET', 'servers', ['headers' => []])
->shouldBeCalled()
->willReturn($this->getFixture('servers-page1'));
$this->client
->request('GET', 'servers', ['query' => ['marker' => '5'], 'headers' => []])
->shouldBeCalled()
->willReturn(new Response(204));
$count = 0;
$api = new ComputeV2Api();
foreach ($this->resource->enumerate($api->getServers()) as $item) {
$count++;
$this->assertInstanceOf(TestResource::class, $item);
}
$this->assertEquals(5, $count);
}
public function test_it_invokes_function_if_provided()
{
$this->client
->request('GET', 'servers', ['headers' => []])
->shouldBeCalled()
->willReturn($this->getFixture('servers-page1'));
$this->client
->request('GET', 'servers', ['query' => ['marker' => '5'], 'headers' => []])
->shouldBeCalled()
->willReturn(new Response(204));
$api = new ComputeV2Api();
$count = 0;
$fn = function () use (&$count) {
$count++;
};
foreach ($this->resource->enumerate($api->getServers(), [], $fn) as $item) {
}
$this->assertEquals(5, $count);
}
public function test_it_halts_when_user_provided_limit_is_reached()
{
$this->client
->request('GET', 'servers', ['query' => ['limit' => 2], 'headers' => []])
->shouldBeCalled()
->willReturn($this->getFixture('servers-page1'));
$count = 0;
$api = new ComputeV2Api();
foreach ($this->resource->enumerate($api->getServers(), ['limit' => 2]) as $item) {
$count++;
}
$this->assertEquals(2, $count);
}
}
class TestResource extends AbstractResource
{
protected $resourceKey = 'foo';
protected $resourcesKey = 'servers';
protected $markerKey = 'id';
/** @var string */
public $bar;
public $id;
/** @var \DateTimeImmutable */
public $created;
/** @var []TestResource */
public $children;
public function getAttrs(array $keys)
{
return parent::getAttrs($keys);
}
}

View file

@ -0,0 +1,6 @@
HTTP/1.1 200 OK
Content-Type: application/json
{
"servers": []
}

View file

@ -0,0 +1,77 @@
HTTP/1.1 200 OK
Content-Type: application/json
{
"servers": [
{
"id": "1",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server1"
},
{
"id": "2",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server2"
},
{
"id": "3",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server3"
},
{
"id": "4",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server4"
},
{
"id": "5",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server5"
}
]
}

View file

@ -0,0 +1,77 @@
HTTP/1.1 200 OK
Content-Type: application/json
{
"servers": [
{
"id": "6",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server6"
},
{
"id": "7",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server7"
},
{
"id": "8",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server8"
},
{
"id": "9",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server9"
},
{
"id": "10",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/616fb98f-46ca-475e-917e-2563e5a8cd19",
"rel": "bookmark"
}
],
"name": "server10"
}
]
}

View file

@ -0,0 +1,164 @@
<?php
namespace OpenCloud\Test\Common\Service;
use GuzzleHttp\ClientInterface;
use OpenCloud\Common\Service\Builder;
use OpenCloud\Identity\v2\Models\Token;
use OpenCloud\Identity\v2\Service as IdentityV2;
use OpenCloud\Identity\v3\Service as IdentityV3;
use OpenCloud\Compute\v2\Service as ComputeV2;
use OpenCloud\Test\Common\Auth\FakeToken;
use OpenCloud\Test\TestCase;
use Prophecy\Argument;
class BuilderTest extends TestCase
{
private $builder;
private $opts;
public function setUp()
{
$this->builder = new Builder([]);
$this->opts = [
'username' => '1',
'password' => '2',
'tenantId' => '3',
'authUrl' => '4',
'region' => '5',
'catalogName' => '6',
'catalogType' => '7',
];
}
/**
* @expectedException \Exception
*/
public function test_it_throws_exception_if_username_is_missing()
{
$this->builder->createService('Compute', 2, []);
}
/**
* @expectedException \Throwable
*/
public function test_it_throws_exception_if_password_is_missing()
{
$this->builder->createService('Compute', 2, ['username' => 1]);
}
/**
* @expectedException \Throwable
*/
public function test_it_throws_exception_if_both_tenantId_and_tenantName_is_missing()
{
$this->builder->createService('Compute', 2, [
'username' => 1, 'password' => 2, 'authUrl' => 4, 'region' => 5, 'catalogName' => 6, 'catalogType' => 7,
]);
}
/**
* @expectedException \Throwable
*/
public function test_it_throws_exception_if_authUrl_is_missing()
{
$this->builder->createService('Compute', 2, ['username' => 1, 'password' => 2, 'tenantId' => 3]);
}
/**
* @expectedException \Throwable
*/
public function test_it_throws_exception_if_region_is_missing()
{
$this->builder->createService('Compute', 2, [
'username' => 1, 'password' => 2, 'tenantId' => 3, 'authUrl' => 4,
]);
}
/**
* @expectedException \Throwable
*/
public function test_it_throws_exception_if_catalogName_is_missing()
{
$this->builder->createService('Compute', 2, [
'username' => 1, 'password' => 2, 'tenantId' => 3, 'authUrl' => 4,
]);
}
/**
* @expectedException \Throwable
*/
public function test_it_throws_exception_if_catalogType_is_missing()
{
$this->builder->createService('Compute', 2, [
'username' => 1, 'password' => 2, 'tenantId' => 3, 'authUrl' => 4, 'region' => 5, 'catalogName' => 6,
]);
}
// public function test_it_builds_services_with_custom_identity_service()
// {
// $this->rootFixturesDir = dirname(dirname(__DIR__)) . '/Identity/v2/';
//
// $token = $this->prophesize(FakeToken::class)->reveal();
// $service = $this->prophesize(IdentityService::class);
// $service->authenticate(Argument::type('array'))->shouldBeCalled()->willReturn([$token, '']);
//
// $this->opts += [
// 'identityService' => $service->reveal(),
// 'catalogName' => 'nova',
// 'catalogType' => 'compute',
// 'region' => 'RegionOne',
// ];
//
// $service = $this->builder->createService('Compute', 2, $this->opts);
// $this->assertInstanceOf(ComputeV2::class, $service);
// }
private function setupHttpClient()
{
$this->rootFixturesDir = dirname(dirname(__DIR__)) . '/Identity/v3/';
$response = $this->getFixture('token-get');
$expectedJson = [
'auth' => [
'identity' => [
'methods' => ['password'],
'password' => ['user' => ['id' => '0ca8f6', 'password' => 'secretsecret']]
]
]
];
$httpClient = $this->prophesize(ClientInterface::class);
$httpClient->request('POST', 'tokens', ['json' => $expectedJson])->shouldBeCalled()->willReturn($response);
return $httpClient;
}
public function it_builds_services_with_default_identity()
{
$httpClient = $this->setupHttpClient();
$options = [
'httpClient' => $httpClient->reveal(),
'catalogName' => 'nova',
'catalogType' => 'compute',
'region' => 'RegionOne',
'user' => [
'id' => '0ca8f6',
'password' => 'secretsecret',
]
];
$service = $this->builder->createService('Compute', 2, $options);
$this->assertInstanceOf(ComputeV2::class, $service);
}
// public function test_it_does_not_authenticate_when_creating_identity_services()
// {
// $this->assertInstanceOf(IdentityV3::class, $this->builder->createService('Identity', 3, [
// 'authUrl' => 'foo.com',
// ]));
// }
}

View file

@ -0,0 +1,15 @@
<?php
namespace OpenCloud\Test\Common\Transport;
use GuzzleHttp\Handler\MockHandler;
use OpenCloud\Common\Transport\HandlerStack;
use OpenCloud\Test\TestCase;
class HandlerStackTest extends TestCase
{
public function test_it_is_created()
{
$this->assertInstanceOf(HandlerStack::class, HandlerStack::create(new MockHandler()));
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace OpenCloud\Test\Common\Transport;
use OpenCloud\Common\Api\Parameter;
use OpenCloud\Common\Transport\JsonSerializer;
class JsonSerializerTest extends \PHPUnit_Framework_TestCase
{
private $serializer;
public function setUp()
{
$this->serializer = new JsonSerializer();
}
public function test_it_embeds_params_according_to_path()
{
$param = $this->prophesize(Parameter::class);
$param->isArray()->shouldBeCalled()->willReturn(false);
$param->isObject()->shouldBeCalled()->willReturn(false);
$param->getName()->shouldBeCalled()->willReturn('username');
$param->getPath()->shouldBeCalled()->willReturn('auth.passwordCredentials');
$userValue = 'fooBar';
$expected = [
'auth' => [
'passwordCredentials' => [
'username' => $userValue,
],
],
];
$actual = $this->serializer->stockJson($param->reveal(), $userValue, []);
$this->assertEquals($expected, $actual);
}
public function test_it_serializes_arrays()
{
$param = $this->prophesize(Parameter::class);
$param->isArray()->shouldBeCalled()->willReturn(true);
$param->getName()->shouldBeCalled()->willReturn('fooBar');
$param->getPath()->shouldBeCalled()->willReturn(false);
$itemSchema = $this->prophesize(Parameter::class);
$itemSchema->isArray()->shouldBeCalled()->willReturn(false);
$itemSchema->isObject()->shouldBeCalled()->willReturn(false);
$itemSchema->getName()->shouldBeCalled()->willReturn(null);
$itemSchema->getPath()->shouldBeCalled()->willReturn(null);
$param->getItemSchema()->shouldBeCalled()->willReturn($itemSchema);
$userValues = ['1', '2', '3'];
$expected = ['fooBar' => $userValues];
$actual = $this->serializer->stockJson($param->reveal(), $userValues, []);
$this->assertEquals($expected, $actual);
}
public function test_it_serializes_objects()
{
$prop = $this->prophesize(Parameter::class);
$prop->isArray()->shouldBeCalled()->willReturn(false);
$prop->isObject()->shouldBeCalled()->willReturn(false);
$prop->getName()->shouldBeCalled()->willReturn('foo');
$prop->getPath()->shouldBeCalled()->willReturn(null);
$param = $this->prophesize(Parameter::class);
$param->isArray()->shouldBeCalled()->willReturn(false);
$param->isObject()->shouldBeCalled()->willReturn(true);
$param->getName()->shouldBeCalled()->willReturn('topLevel');
$param->getPath()->shouldBeCalled()->willReturn(false);
$param->getProperty('foo')->shouldBeCalled()->willReturn($prop);
$expected = ['topLevel' => ['foo' => true]];
$json = $this->serializer->stockJson($param->reveal(), ['foo' => true], []);
$this->assertEquals($expected, $json);
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace unit\Common\Transport;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use OpenCloud\Common\Transport\Middleware;
use OpenCloud\Test\TestCase;
use OpenCloud\Common\Auth\AuthHandler;
class MiddlewareTest extends TestCase
{
/**
* @expectedException \OpenCloud\Common\Error\BadResponseError
*/
public function test_exception_is_thrown_for_4xx_statuses()
{
$middleware = Middleware::httpErrors();
$handler = new MockHandler([new Response(404)]);
$fn = $middleware($handler);
$promise = $fn(new Request('GET', 'http://foo.com'), []);
$this->assertEquals('pending', $promise->getState());
$promise->wait();
$this->assertEquals('rejected', $promise->getState());
}
public function test_responses_are_left_alone_when_status_under_400()
{
$middleware = Middleware::httpErrors();
$response = new Response(204);
$handler = new MockHandler([$response]);
$fn = $middleware($handler);
$promise = $fn(new Request('GET', 'http://foo.com'), []);
$promise->then(function ($val) use ($response) {
$this->assertEquals($val, $response);
});
$promise->wait();
}
public function test_auth_handler_is_returned()
{
$generator = function () {};
$middleware = Middleware::authHandler($generator);
$handler = new MockHandler([new Response(204)]);
$fn = $middleware($handler);
$this->assertInstanceOf(AuthHandler::class, $fn);
}
}

View file

@ -0,0 +1,112 @@
<?php
namespace OpenCloud\Test\Common\Transport;
use OpenCloud\Common\Api\Operation;
use OpenCloud\Common\Api\Parameter;
use OpenCloud\Common\Transport\JsonSerializer;
use OpenCloud\Common\Transport\RequestSerializer;
use OpenCloud\Test\TestCase;
class RequestSerializerTest extends TestCase
{
private $rs;
private $js;
public function setUp()
{
$this->js = $this->prophesize(JsonSerializer::class);
$this->rs = new RequestSerializer($this->js->reveal());
}
public function test_it_ignores_undefined_params()
{
$op = $this->prophesize(Operation::class);
$op->getParam('foo')->shouldBeCalled()->willReturn(null);
$this->assertEquals(['headers' => []], $this->rs->serializeOptions($op->reveal(), ['foo' => 'bar']));
}
public function test_it_serializes_queries()
{
$sch = $this->prophesize(Parameter::class);
$sch->getName()->shouldBeCalled()->willReturn('fooAlias');
$sch->getLocation()->shouldBeCalled()->willReturn('query');
$op = $this->prophesize(Operation::class);
$op->getParam('foo')->shouldBeCalled()->willReturn($sch);
$actual = $this->rs->serializeOptions($op->reveal(), ['foo' => 'bar']);
$expected = ['query' => ['fooAlias' => 'bar'], 'headers' => []];
$this->assertEquals($expected, $actual);
}
public function test_it_serializes_headers()
{
$sch = $this->prophesize(Parameter::class);
$sch->getLocation()->shouldBeCalled()->willReturn('header');
$sch->getName()->shouldBeCalled()->willReturn('fooAlias');
$sch->getPrefixedName()->shouldBeCalled()->willReturn('prefix-fooAlias');
$op = $this->prophesize(Operation::class);
$op->getParam('foo')->shouldBeCalled()->willReturn($sch);
$actual = $this->rs->serializeOptions($op->reveal(), ['foo' => 'bar']);
$expected = ['headers' => ['prefix-fooAlias' => 'bar']];
$this->assertEquals($expected, $actual);
}
public function test_it_serializes_metadata_headers()
{
$itemSch = $this->prophesize(Parameter::class);
$itemSch->getName()->shouldBeCalled()->willReturn('foo');
$itemSch->getPrefixedName()->shouldBeCalled()->willReturn('prefix-foo');
$sch = $this->prophesize(Parameter::class);
$sch->getItemSchema()->shouldBeCalled()->willReturn($itemSch);
$sch->getLocation()->shouldBeCalled()->willReturn('header');
$sch->getName()->shouldBeCalled()->willReturn('metadata');
$op = $this->prophesize(Operation::class);
$op->getParam('metadata')->shouldBeCalled()->willReturn($sch);
$actual = $this->rs->serializeOptions($op->reveal(), ['metadata' => ['foo' => 'bar']]);
$expected = ['headers' => ['prefix-foo' => 'bar']];
$this->assertEquals($expected, $actual);
}
public function test_it_serializes_json()
{
$sch = $this->prophesize(Parameter::class);
$sch->getLocation()->shouldBeCalled()->willReturn('json');
$op = $this->prophesize(Operation::class);
$op->getParam('foo')->shouldBeCalled()->willReturn($sch);
$op->getJsonKey()->shouldBeCalled()->willReturn('jsonKey');
$this->js->stockJson($sch, 'bar', [])->shouldBeCalled()->willReturn(['foo' => 'bar']);
$actual = $this->rs->serializeOptions($op->reveal(), ['foo' => 'bar']);
$expected = ['json' => ['jsonKey' => ['foo' => 'bar']], 'headers' => []];
$this->assertEquals($expected, $actual);
}
public function test_it_serializes_raw_vals()
{
$sch = $this->prophesize(Parameter::class);
$sch->getLocation()->shouldBeCalled()->willReturn('raw');
$op = $this->prophesize(Operation::class);
$op->getParam('foo')->shouldBeCalled()->willReturn($sch);
$actual = $this->rs->serializeOptions($op->reveal(), ['foo' => 'bar']);
$expected = ['body' => 'bar', 'headers' => []];
$this->assertEquals($expected, $actual);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace OpenCloud\Test\Common\Transport;
use GuzzleHttp\Psr7\Response;
use function GuzzleHttp\Psr7\uri_for;
use GuzzleHttp\Psr7\Uri;
use OpenCloud\Common\Transport\Utils;
use OpenCloud\Test\TestCase;
class UtilsTest extends TestCase
{
/**
* @expectedException \InvalidArgumentException
*/
public function test_decoding_malformed_json_throws_error()
{
$response = new Response(200, [], \GuzzleHttp\Psr7\stream_for('{'));
Utils::jsonDecode($response);
}
public function test_it_adds_paths()
{
$uri = Utils::addPaths(uri_for('http://openstack.org/foo'), 'bar', 'baz', '1', '2');
$this->assertInstanceOf(Uri::class, $uri);
$this->assertEquals(uri_for('http://openstack.org/foo/bar/baz/1/2'), $uri);
}
}

View file

@ -0,0 +1,107 @@
<?php
namespace OpenCloud\Test\Fixtures;
use OpenCloud\Common\Api\ApiInterface;
class ComputeV2Api implements ApiInterface
{
private $idParam = ['type' => 'string', 'required' => true, 'location' => 'url'];
public function getImage()
{
return [
'method' => 'GET',
'path' => 'images/{id}',
'params' => [self::$idParam]
];
}
public function postServer()
{
return [
'path' => 'servers',
'method' => 'POST',
'jsonKey' => 'server',
'params' => [
'removeMetadata' => [
'type' => 'object',
'properties' => ['type' => 'string'],
],
'securityGroups' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'name' => ['type' => 'string']
]
],
'sentAs' => 'security_groups',
],
'userData' => ['type' => 'string', 'sentAs' => 'user_data'],
'availabilityZone' => ['type' => 'string', 'sentAs' => 'availability_zone'],
'imageId' => ['type' => 'string', 'required' => true, 'sentAs' => 'imageRef'],
'flavorId' => ['type' => 'string', 'required' => true, 'sentAs' => 'flavorRef'],
'networks' => [
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'uuid' => ['type' => 'string'],
'port' => ['type' => 'string'],
]
]
],
'name' => ['type' => 'string', 'required' => true],
'metadata' => ['type' => 'object', 'location' => 'json'],
'personality' => ['type' => 'string'],
'blockDeviceMapping' => [
'type' => 'array',
'sentAs' => 'block_device_mapping_v2',
'items' => [
'type' => 'object',
'properties' => [
'configDrive' => ['type' => 'string', 'sentAs' => 'config_drive'],
'bootIndex' => ['type' => 'string', 'sentAs' => 'boot_index'],
'deleteOnTermination' => ['type' => 'boolean', 'sentAs' => 'delete_on_termination'],
'guestFormat' => ['type' => 'string', 'sentAs' => 'guest_format'],
'destinationType' => ['type' => 'string', 'sentAs' => 'destination_type'],
'sourceType' => ['type' => 'string', 'sentAs' => 'source_type'],
'deviceName' => ['type' => 'string', 'sentAs' => 'device_name'],
]
],
],
]
];
}
public function test()
{
return [
'method' => 'GET',
'path' => 'foo',
'params' => [
'id' => ['type' => 'string', 'location' => 'json'],
'bar' => ['type' => 'string', 'location' => 'json'],
]
];
}
public function getServers()
{
return [
'method' => 'GET',
'path' => 'servers',
'params' => [
'changesSince' => ['sentAs' => 'changes-since', 'type' => 'string', 'location' => 'query'],
'imageId' => ['sentAs' => 'image', 'type' => 'string', 'location' => 'query'],
'flavorId' => ['sentAs' => 'flavor', 'type' => 'string', 'location' => 'query'],
'name' => ['type' => 'string', 'location' => 'query'],
'marker' => ['type' => 'string', 'location' => 'query'],
'limit' => ['type' => 'integer', 'location' => 'query'],
'status' => ['type' => 'string', 'location' => 'query'],
'host' => ['type' => 'string', 'location' => 'query']
],
];
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace OpenCloud\Test\Fixtures;
use OpenCloud\Common\Api\ApiInterface;
class IdentityV2Api implements ApiInterface
{
public function postToken()
{
return [
'method' => 'POST',
'path' => 'tokens',
'params' => [
'username' => [
'type' => 'string',
'required' => true,
'path' => 'auth.passwordCredentials'
],
'password' => [
'type' => 'string',
'required' => true,
'path' => 'auth.passwordCredentials'
],
'tenantId' => [
'type' => 'string',
'path' => 'auth',
],
'tenantName' => [
'type' => 'string',
'path' => 'auth',
]
],
];
}
}

View file

@ -0,0 +1,75 @@
<?php
namespace OpenCloud\Test\Fixtures;
class IdentityV3Api
{
private function domainParam()
{
return [
'type' => 'object',
'params' => [
'id' => ['type' => 'string'],
'name' => ['type' => 'string']
]
];
}
private function projectParam()
{
return [
'type' => 'object',
'params' => [
'id' => ['type' => 'string'],
'name' => ['type' => 'string'],
'domain' => $this->domainParam(),
]
];
}
public function postTokens()
{
return [
'method' => 'POST',
'path' => 'tokens',
'params' => [
'methods' => [
'type' => 'array',
'path' => 'auth.identity',
'items' => [
'type' => 'string'
]
],
'user' => [
'path' => 'auth.identity.password',
'type' => 'object',
'properties' => [
'id' => [
'type' => 'string',
],
'name' => [
'type' => 'string',
],
'password' => [
'type' => 'string',
],
'domain' => $this->domainParam()
]
],
'tokenId' => [
'type' => 'string',
'path' => 'auth.identity.token',
'sentAs' => 'id',
],
'scope' => [
'type' => 'object',
'path' => 'auth',
'properties' => [
'project' => $this->projectParam(),
'domain' => $this->domainParam()
]
]
]
];
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace OpenCloud\Test;
use function GuzzleHttp\Psr7\stream_for;
use function GuzzleHttp\Psr7\parse_response;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Response;
use Prophecy\Argument;
abstract class TestCase extends \PHPUnit_Framework_TestCase
{
/** @var \Prophecy\Prophecy\ObjectProphecy */
protected $client;
/** @var string */
protected $rootFixturesDir;
protected $api;
protected function setUp()
{
$this->client = $this->prophesize(ClientInterface::class);
}
protected function createResponse($status, array $headers, array $json)
{
return new Response($status, $headers, stream_for(json_encode($json)));
}
protected function getFixture($file)
{
if (!$this->rootFixturesDir) {
throw new \RuntimeException('Root fixtures dir not set');
}
$path = $this->rootFixturesDir . '/Fixtures/' . $file . '.resp';
if (!file_exists($path)) {
throw new \RuntimeException(sprintf("%s does not exist", $path));
}
return parse_response(file_get_contents($path));
}
protected function setupMock($method, $path, $body = null, array $headers = [], $response)
{
$options = ['headers' => $headers];
if (!empty($body)) {
$options[is_array($body) ? 'json' : 'body'] = $body;
}
if (is_string($response)) {
$response = $this->getFixture($response);
}
$this->client
->request($method, $path, $options)
->shouldBeCalled()
->willReturn($response);
}
protected function createFn($receiver, $method, $args)
{
return function () use ($receiver, $method, $args) {
return $receiver->$method($args);
};
}
protected function listTest(callable $call, $urlPath, $modelName = null, $responseFile = null)
{
$modelName = $modelName ?: $urlPath;
$responseFile = $responseFile ?: $urlPath;
$this->setupMock('GET', $urlPath, null, [], $responseFile);
$resources = call_user_func($call);
$this->assertInstanceOf('\Generator', $resources);
$count = 0;
foreach ($resources as $resource) {
$this->assertInstanceOf('OpenStack\Identity\v3\Models\\' . ucfirst($modelName), $resource);
++$count;
}
$this->assertEquals(2, $count);
}
protected function getTest(callable $call, $modelName)
{
$resource = call_user_func($call);
$this->assertInstanceOf('OpenStack\Identity\v3\Models\\' . ucfirst($modelName), $resource);
$this->assertEquals('id', $resource->id);
}
}