diff --git a/keystoneclient/base.py b/keystoneclient/base.py index 646394e57..77e6108fa 100644 --- a/keystoneclient/base.py +++ b/keystoneclient/base.py @@ -18,6 +18,8 @@ Base utilities to build API operation managers and objects on top of. """ +import urllib + from keystoneclient import exceptions @@ -76,20 +78,25 @@ def _list(self, url, response_key, obj_class=None, body=None): def _get(self, url, response_key): resp, body = self.api.get(url) - return self.resource_class(self, body[response_key]) + return self.resource_class(self, body[response_key], loaded=True) + + def _head(self, url): + resp, body = self.api.head(url) + return resp.status == 204 def _create(self, url, body, response_key, return_raw=False): resp, body = self.api.post(url, body=body) if return_raw: return body[response_key] - return self.resource_class(self, body[response_key]) + return self.resource_class(self, body[response_key], loaded=True) def _delete(self, url): resp, body = self.api.delete(url) - def _update(self, url, body, response_key=None, method="PUT"): + def _update(self, url, body=None, response_key=None, method="PUT"): methods = {"PUT": self.api.put, - "POST": self.api.post} + "POST": self.api.post, + "PATCH": self.api.patch} try: if body is not None: resp, body = methods[method](url, body=body) @@ -100,7 +107,7 @@ def _update(self, url, body, response_key=None, method="PUT"): % method) # PUT requests may not return a body if body: - return self.resource_class(self, body[response_key]) + return self.resource_class(self, body[response_key], loaded=True) class ManagerWithFind(Manager): @@ -142,6 +149,115 @@ def findall(self, **kwargs): return found +class CrudManager(Manager): + """Base manager class for manipulating Keystone entities. + + Children of this class are expected to define a `collection_key` and `key`. + + - `collection_key`: Usually a plural noun by convention (e.g. `entities`); + used to refer collections in both URL's (e.g. `/v3/entities`) and JSON + objects containing a list of member resources (e.g. `{'entities': [{}, + {}, {}]}`). + - `key`: Usually a singular noun by convention (e.g. `entity`); used to + refer to an individual member of the collection. + + """ + collection_key = None + key = None + + def build_url(self, base_url=None, **kwargs): + """Builds a resource URL for the given kwargs. + + Given an example collection where `collection_key = 'entities'` and + `key = 'entity'`, the following URL's could be generated. + + By default, the URL will represent a collection of entities, e.g.:: + + /entities + + If kwargs contains an `entity_id`, then the URL will represent a + specific member, e.g.:: + + /entities/{entity_id} + + If a `base_url` is provided, the generated URL will be appended to it. + + """ + url = base_url if base_url is not None else '' + + url += '/%s' % self.collection_key + + # do we have a specific entity? + entity_id = kwargs.get('%s_id' % self.key) + if entity_id is not None: + url += '/%s' % entity_id + + return url + + def _filter_kwargs(self, kwargs): + # drop null values + for key, ref in kwargs.copy().iteritems(): + if ref is None: + kwargs.pop(key) + else: + id_value = getid(ref) + if id_value != ref: + kwargs.pop(key) + kwargs['%s_id' % key] = id_value + return kwargs + + def create(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._create( + self.build_url(**kwargs), + {self.key: kwargs}, + self.key) + + def get(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._get( + self.build_url(**kwargs), + self.key) + + def head(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + return self._head(self.build_url(**kwargs)) + + def list(self, base_url=None, **kwargs): + kwargs = self._filter_kwargs(kwargs) + + return self._list( + '%(base_url)s%(query)s' % { + 'base_url': self.build_url(base_url=base_url, **kwargs), + 'query': '?%s' % urllib.urlencode(kwargs) if kwargs else '', + }, + self.collection_key) + + def put(self, base_url=None, **kwargs): + kwargs = self._filter_kwargs(kwargs) + + return self._update( + self.build_url(base_url=base_url, **kwargs), + method='PUT') + + def update(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + params = kwargs.copy() + params.pop('%s_id' % self.key) + + return self._update( + self.build_url(**kwargs), + {self.key: params}, + self.key, + method='PATCH') + + def delete(self, **kwargs): + kwargs = self._filter_kwargs(kwargs) + + return self._delete( + self.build_url(**kwargs)) + + class Resource(object): """ A resource represents a particular instance of an object (tenant, user, @@ -188,6 +304,9 @@ def get(self): if new: self._add_details(new._info) + def delete(self): + return self.manager.delete(self) + def __eq__(self, other): if not isinstance(other, self.__class__): return False diff --git a/keystoneclient/client.py b/keystoneclient/client.py index c61f89165..c146ff0aa 100644 --- a/keystoneclient/client.py +++ b/keystoneclient/client.py @@ -108,6 +108,9 @@ def http_log_resp(self, resp, body): if self.debug_log: _logger.debug("RESP: %s\nRESP BODY: %s\n", resp, body) + def serialize(self, entity): + return json.dumps(entity) + def request(self, url, method, **kwargs): """ Send an http request with the specified characteristics. @@ -123,7 +126,7 @@ def request(self, url, method, **kwargs): self.original_ip, self.USER_AGENT) if 'body' in kwargs: request_kwargs['headers']['Content-Type'] = 'application/json' - request_kwargs['body'] = json.dumps(kwargs['body']) + request_kwargs['body'] = self.serialize(kwargs['body']) self.http_log_req((url, method,), request_kwargs) resp, body = super(HTTPClient, self).request(url, @@ -180,11 +183,17 @@ def _cs_request(self, url, method, **kwargs): def get(self, url, **kwargs): return self._cs_request(url, 'GET', **kwargs) + def head(self, url, **kwargs): + return self._cs_request(url, 'HEAD', **kwargs) + def post(self, url, **kwargs): return self._cs_request(url, 'POST', **kwargs) def put(self, url, **kwargs): return self._cs_request(url, 'PUT', **kwargs) + def patch(self, url, **kwargs): + return self._cs_request(url, 'PATCH', **kwargs) + def delete(self, url, **kwargs): return self._cs_request(url, 'DELETE', **kwargs) diff --git a/keystoneclient/exceptions.py b/keystoneclient/exceptions.py index 802d3966d..031787927 100644 --- a/keystoneclient/exceptions.py +++ b/keystoneclient/exceptions.py @@ -9,6 +9,10 @@ class CommandError(Exception): pass +class ValidationError(Exception): + pass + + class AuthorizationFailure(Exception): pass diff --git a/keystoneclient/v3/__init__.py b/keystoneclient/v3/__init__.py new file mode 100644 index 000000000..feb2536b8 --- /dev/null +++ b/keystoneclient/v3/__init__.py @@ -0,0 +1 @@ +from keystoneclient.v3.client import Client diff --git a/keystoneclient/v3/client.py b/keystoneclient/v3/client.py new file mode 100644 index 000000000..51672a738 --- /dev/null +++ b/keystoneclient/v3/client.py @@ -0,0 +1,85 @@ +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import json +import logging + +from keystoneclient.v2_0 import client +from keystoneclient.v3 import credentials +from keystoneclient.v3 import endpoints +from keystoneclient.v3 import domains +from keystoneclient.v3 import policies +from keystoneclient.v3 import projects +from keystoneclient.v3 import roles +from keystoneclient.v3 import services +from keystoneclient.v3 import users + + +_logger = logging.getLogger(__name__) + + +class Client(client.Client): + """Client for the OpenStack Identity API v3. + + :param string username: Username for authentication. (optional) + :param string password: Password for authentication. (optional) + :param string token: Token for authentication. (optional) + :param string tenant_name: Tenant id. (optional) + :param string tenant_id: Tenant name. (optional) + :param string auth_url: Keystone service endpoint for authorization. + :param string region_name: Name of a region to select when choosing an + endpoint from the service catalog. + :param string endpoint: A user-supplied endpoint URL for the keystone + service. Lazy-authentication is possible for API + service calls if endpoint is set at + instantiation.(optional) + :param integer timeout: Allows customization of the timeout for client + http requests. (optional) + + Example:: + + >>> from keystoneclient.v3 import client + >>> keystone = client.Client(username=USER, + password=PASS, + tenant_name=TENANT_NAME, + auth_url=KEYSTONE_URL) + >>> keystone.tenants.list() + ... + >>> user = keystone.users.get(USER_ID) + >>> user.delete() + + """ + + def __init__(self, endpoint=None, **kwargs): + """ Initialize a new client for the Keystone v2.0 API. """ + super(Client, self).__init__(endpoint=endpoint, **kwargs) + + self.credentials = credentials.CredentialManager(self) + self.endpoints = endpoints.EndpointManager(self) + self.domains = domains.DomainManager(self) + self.policies = policies.PolicyManager(self) + self.projects = projects.ProjectManager(self) + self.roles = roles.RoleManager(self) + self.services = services.ServiceManager(self) + self.users = users.UserManager(self) + + # NOTE(gabriel): If we have a pre-defined endpoint then we can + # get away with lazy auth. Otherwise auth immediately. + if endpoint: + self.management_url = endpoint + else: + self.authenticate() + + def serialize(self, entity): + return json.dumps(entity, sort_keys=True) diff --git a/keystoneclient/v3/credentials.py b/keystoneclient/v3/credentials.py new file mode 100644 index 000000000..264c367e7 --- /dev/null +++ b/keystoneclient/v3/credentials.py @@ -0,0 +1,57 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Credential(base.Resource): + """Represents an Identity credential. + + Attributes: + * id: a uuid that identifies the credential + + """ + pass + + +class CredentialManager(base.CrudManager): + """Manager class for manipulating Identity credentials.""" + resource_class = Credential + collection_key = 'credentials' + key = 'credential' + + def create(self, user, type, data, project=None): + return super(CredentialManager, self).create( + user_id=base.getid(user), + type=type, + data=data, + project_id=base.getid(project)) + + def get(self, credential): + return super(CredentialManager, self).get( + credential_id=base.getid(credential)) + + def update(self, credential, user, type=None, data=None, project=None): + return super(CredentialManager, self).update( + credential_id=base.getid(credential), + user_id=base.getid(user), + type=type, + data=data, + project_id=base.getid(project)) + + def delete(self, credential): + return super(CredentialManager, self).delete( + credential_id=base.getid(credential)) diff --git a/keystoneclient/v3/domains.py b/keystoneclient/v3/domains.py new file mode 100644 index 000000000..2d27db225 --- /dev/null +++ b/keystoneclient/v3/domains.py @@ -0,0 +1,55 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Domain(base.Resource): + """Represents an Identity domain. + + Attributes: + * id: a uuid that identifies the domain + + """ + pass + + +class DomainManager(base.CrudManager): + """Manager class for manipulating Identity domains.""" + resource_class = Domain + collection_key = 'domains' + key = 'domain' + + def create(self, name, description=None, enabled=True): + return super(DomainManager, self).create( + name=name, + description=description, + enabled=enabled) + + def get(self, domain): + return super(DomainManager, self).get( + domain_id=base.getid(domain)) + + def update(self, domain, name=None, description=None, enabled=None): + return super(DomainManager, self).update( + domain_id=base.getid(domain), + name=name, + description=description, + enabled=enabled) + + def delete(self, domain): + return super(DomainManager, self).delete( + domain_id=base.getid(domain)) diff --git a/keystoneclient/v3/endpoints.py b/keystoneclient/v3/endpoints.py new file mode 100644 index 000000000..c13313d83 --- /dev/null +++ b/keystoneclient/v3/endpoints.py @@ -0,0 +1,86 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +VALID_INTERFACES = ['public', 'admin', 'internal'] + + +class Endpoint(base.Resource): + """Represents an Identity endpoint. + + Attributes: + * id: a uuid that identifies the endpoint + * interface: 'public', 'admin' or 'internal' network interface + * region: geographic location of the endpoint + * service_id: service to which the endpoint belongs + * url: fully qualified service endpoint + * enabled: determines whether the endpoint appears in the catalog + + """ + pass + + +class EndpointManager(base.CrudManager): + """Manager class for manipulating Identity endpoints.""" + resource_class = Endpoint + collection_key = 'endpoints' + key = 'endpoint' + + def _validate_interface(self, interface): + if interface is not None and interface not in VALID_INTERFACES: + msg = '"interface" must be one of: %s' + msg = msg % ', '.join(VALID_INTERFACES) + raise Exception(msg) + + def create(self, service, url, name=None, interface=None, region=None, + enabled=True): + self._validate_interface(interface) + return super(EndpointManager, self).create( + service_id=base.getid(service), + interface=interface, + url=url, + region=region, + enabled=enabled) + + def get(self, endpoint): + return super(EndpointManager, self).get( + endpoint_id=base.getid(endpoint)) + + def list(self, service=None, name=None, interface=None, region=None, + enabled=None): + self._validate_interface(interface) + return super(EndpointManager, self).list( + service_id=base.getid(service), + interface=interface, + region=region, + enabled=enabled) + + def update(self, endpoint, service=None, url=None, name=None, + interface=None, region=None, enabled=None): + self._validate_interface(interface) + return super(EndpointManager, self).update( + endpoint_id=base.getid(endpoint), + service_id=base.getid(service), + interface=interface, + url=url, + region=region, + enabled=enabled) + + def delete(self, endpoint): + return super(EndpointManager, self).delete( + endpoint_id=base.getid(endpoint)) diff --git a/keystoneclient/v3/policies.py b/keystoneclient/v3/policies.py new file mode 100644 index 000000000..6f3f6a872 --- /dev/null +++ b/keystoneclient/v3/policies.py @@ -0,0 +1,77 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Policy(base.Resource): + """Represents an Identity policy. + + Attributes: + * id: a uuid that identifies the policy + * endpoint_id: references the endpoint the policy applies to + * blob: a policy document (blob) + * type: the mime type of the policy blob + + """ + def update(self, endpoint=None, blob=None, type=None): + kwargs = { + 'endpoint_id': (base.getid(endpoint) + if endpoint is not None + else self.endpoint_id), + 'blob': blob if blob is not None else self.blob, + 'type': type if type is not None else self.type, + } + + try: + retval = self.manager.update(self.id, **kwargs) + self = retval + except Exception: + retval = None + + return retval + + +class PolicyManager(base.CrudManager): + """Manager class for manipulating Identity policies.""" + resource_class = Policy + collection_key = 'policies' + key = 'policy' + + def create(self, endpoint, blob, type='application/json'): + return super(PolicyManager, self).create( + endpoint_id=base.getid(endpoint), + blob=blob, + type=type) + + def get(self, policy): + return super(PolicyManager, self).get( + policy_id=base.getid(policy)) + + def list(self, endpoint=None): + return super(PolicyManager, self).list( + endpoint_id=base.getid(endpoint)) + + def update(self, entity, endpoint=None, blob=None, type=None): + return super(PolicyManager, self).update( + policy_id=base.getid(entity), + endpoint_id=base.getid(endpoint), + blob=blob, + type=type) + + def delete(self, policy): + return super(PolicyManager, self).delete( + policy_id=base.getid(policy)) diff --git a/keystoneclient/v3/projects.py b/keystoneclient/v3/projects.py new file mode 100644 index 000000000..bcbc4fd57 --- /dev/null +++ b/keystoneclient/v3/projects.py @@ -0,0 +1,82 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Project(base.Resource): + """Represents an Identity project. + + Attributes: + * id: a uuid that identifies the project + * name: project name + * description: project description + * enabled: boolean to indicate if project is enabled + + """ + def update(self, name=None, description=None, enabled=None): + kwargs = { + 'name': name if name is not None else self.name, + 'description': (description + if description is not None + else self.description), + 'enabled': enabled if enabled is not None else self.enabled, + } + + try: + retval = self.manager.update(self.id, **kwargs) + self = retval + except Exception: + retval = None + + return retval + + +class ProjectManager(base.CrudManager): + """Manager class for manipulating Identity projects.""" + resource_class = Project + collection_key = 'projects' + key = 'project' + + def create(self, name, domain, description=None, enabled=True): + return super(ProjectManager, self).create( + domain_id=base.getid(domain), + name=name, + description=description, + enabled=enabled) + + def list(self, domain=None, user=None): + base_url = '/users/%s' % base.getid(user) if user else None + return super(ProjectManager, self).list( + base_url=base_url, + domain_id=base.getid(domain)) + + def get(self, project): + return super(ProjectManager, self).get( + project_id=base.getid(project)) + + def update(self, project, name=None, domain=None, description=None, + enabled=None): + return super(ProjectManager, self).update( + project_id=base.getid(project), + domain_id=base.getid(domain), + name=name, + description=description, + enabled=enabled) + + def delete(self, project): + return super(ProjectManager, self).delete( + project_id=base.getid(project)) diff --git a/keystoneclient/v3/roles.py b/keystoneclient/v3/roles.py new file mode 100644 index 000000000..690602575 --- /dev/null +++ b/keystoneclient/v3/roles.py @@ -0,0 +1,110 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base +from keystoneclient import exceptions + + +class Role(base.Resource): + """Represents an Identity role. + + Attributes: + * id: a uuid that identifies the role + * name: user-facing identifier + + """ + pass + + +class RoleManager(base.CrudManager): + """Manager class for manipulating Identity roles.""" + resource_class = Role + collection_key = 'roles' + key = 'role' + + def _role_grants_base_url(self, user, domain, project): + params = {'user_id': base.getid(user)} + + if domain: + params['domain_id'] = base.getid(domain) + base_url = '/domains/%(domain_id)s/users/%(user_id)s' + elif project: + params['project_id'] = base.getid(project) + base_url = '/projects/%(project_id)s/users/%(user_id)s' + + return base_url % params + + def _require_domain_or_project(self, domain, project): + if (domain and project) or (not domain and not project): + msg = 'Specify either a domain or project, not both' + raise exceptions.ValidationError(msg) + + def create(self, name): + return super(RoleManager, self).create( + name=name) + + def get(self, role): + return super(RoleManager, self).get( + role_id=base.getid(role)) + + def list(self, user=None, domain=None, project=None): + """Lists roles and role grants. + + If no arguments are provided, all roles in the system will be listed. + + If a user is specified, you must also specify either a domain or + project to list role grants on that pair. + """ + + if user: + self._require_domain_or_project(domain, project) + return super(RoleManager, self).list( + base_url=self._role_grants_base_url(user, domain, project)) + + return super(RoleManager, self).list() + + def update(self, role, name=None): + return super(RoleManager, self).update( + role_id=base.getid(role), + name=name) + + def delete(self, role): + return super(RoleManager, self).delete( + role_id=base.getid(role)) + + def grant(self, role, user, domain=None, project=None): + """Grants a role to a user on either a domain or project.""" + self._require_domain_or_project(domain, project) + + return super(RoleManager, self).put( + base_url=self._role_grants_base_url(user, domain, project), + role_id=base.getid(role)) + + def check(self, role, user, domain=None, project=None): + """Grants a role to a user on either a domain or project.""" + self._require_domain_or_project(domain, project) + + return super(RoleManager, self).head( + base_url=self._role_grants_base_url(user, domain, project), + role_id=base.getid(role)) + + def revoke(self, role, user, domain=None, project=None): + """Revokes a role from a user on either a domain or project.""" + self._require_domain_or_project(domain, project) + + return super(RoleManager, self).delete( + base_url=self._role_grants_base_url(user, domain, project), + role_id=base.getid(role)) diff --git a/keystoneclient/v3/services.py b/keystoneclient/v3/services.py new file mode 100644 index 000000000..d2134d43d --- /dev/null +++ b/keystoneclient/v3/services.py @@ -0,0 +1,60 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class Service(base.Resource): + """Represents an Identity service. + + Attributes: + * id: a uuid that identifies the service + * name: user-facing name of the service (e.g. Keystone) + * type: 'compute', 'identity', etc + * enabled: determines whether the service appears in the catalog + + """ + pass + + +class ServiceManager(base.CrudManager): + """Manager class for manipulating Identity services.""" + resource_class = Service + collection_key = 'services' + key = 'service' + + def create(self, name, type, enabled=True, **kwargs): + return super(ServiceManager, self).create( + name=name, + type=type, + enabled=enabled, + **kwargs) + + def get(self, service): + return super(ServiceManager, self).get( + service_id=base.getid(service)) + + def update(self, service, name=None, type=None, enabled=None, **kwargs): + return super(ServiceManager, self).update( + service_id=base.getid(service), + name=name, + type=type, + enabled=enabled, + **kwargs) + + def delete(self, service): + return super(ServiceManager, self).delete( + service_id=base.getid(service)) diff --git a/keystoneclient/v3/users.py b/keystoneclient/v3/users.py new file mode 100644 index 000000000..a8c1c4b38 --- /dev/null +++ b/keystoneclient/v3/users.py @@ -0,0 +1,70 @@ +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Nebula, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from keystoneclient import base + + +class User(base.Resource): + """Represents an Identity user. + + Attributes: + * id: a uuid that identifies the user + + """ + pass + + +class UserManager(base.CrudManager): + """Manager class for manipulating Identity users.""" + resource_class = User + collection_key = 'users' + key = 'user' + + def create(self, name, domain=None, project=None, password=None, + email=None, description=None, enabled=True): + return super(UserManager, self).create( + name=name, + domain_id=base.getid(domain), + project_id=base.getid(project), + password=password, + email=email, + description=description, + enabled=enabled) + + def list(self, project=None, domain=None): + return super(UserManager, self).list( + domain_id=base.getid(domain), + project_id=base.getid(project)) + + def get(self, user): + return super(UserManager, self).get( + user_id=base.getid(user)) + + def update(self, user, name=None, domain=None, project=None, password=None, + email=None, description=None, enabled=None): + return super(UserManager, self).update( + user_id=base.getid(user), + name=name, + domain_id=base.getid(domain), + project_id=base.getid(project), + password=password, + email=email, + description=description, + enabled=enabled) + + def delete(self, user): + return super(UserManager, self).delete( + user_id=base.getid(user)) diff --git a/tests/v3/__init__.py b/tests/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/v3/test_credentials.py b/tests/v3/test_credentials.py new file mode 100644 index 000000000..180f680dd --- /dev/null +++ b/tests/v3/test_credentials.py @@ -0,0 +1,22 @@ +import uuid + +from keystoneclient.v3 import credentials +from tests.v3 import utils + + +class CredentialTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(CredentialTests, self).setUp() + self.additionalSetUp() + self.key = 'credential' + self.collection_key = 'credentials' + self.model = credentials.Credential + self.manager = self.client.credentials + + def new_ref(self, **kwargs): + kwargs = super(CredentialTests, self).new_ref(**kwargs) + kwargs.setdefault('data', uuid.uuid4().hex) + kwargs.setdefault('project_id', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('user_id', uuid.uuid4().hex) + return kwargs diff --git a/tests/v3/test_domains.py b/tests/v3/test_domains.py new file mode 100644 index 000000000..8cf0ea075 --- /dev/null +++ b/tests/v3/test_domains.py @@ -0,0 +1,20 @@ +import uuid + +from keystoneclient.v3 import domains +from tests.v3 import utils + + +class DomainTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(DomainTests, self).setUp() + self.additionalSetUp() + self.key = 'domain' + self.collection_key = 'domains' + self.model = domains.Domain + self.manager = self.client.domains + + def new_ref(self, **kwargs): + kwargs = super(DomainTests, self).new_ref(**kwargs) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs diff --git a/tests/v3/test_endpoints.py b/tests/v3/test_endpoints.py new file mode 100644 index 000000000..d428b9148 --- /dev/null +++ b/tests/v3/test_endpoints.py @@ -0,0 +1,78 @@ +import uuid + +from keystoneclient.v3 import endpoints +from tests.v3 import utils + + +class EndpointTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(EndpointTests, self).setUp() + self.additionalSetUp() + self.key = 'endpoint' + self.collection_key = 'endpoints' + self.model = endpoints.Endpoint + self.manager = self.client.endpoints + + def new_ref(self, **kwargs): + kwargs = super(EndpointTests, self).new_ref(**kwargs) + kwargs.setdefault('interface', 'public') + kwargs.setdefault('region', uuid.uuid4().hex) + kwargs.setdefault('service_id', uuid.uuid4().hex) + kwargs.setdefault('url', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + return kwargs + + def test_create_public_interface(self): + ref = self.new_ref(interface='public') + self.test_create(ref) + + def test_create_admin_interface(self): + ref = self.new_ref(interface='admin') + self.test_create(ref) + + def test_create_internal_interface(self): + ref = self.new_ref(interface='internal') + self.test_create(ref) + + def test_create_invalid_interface(self): + ref = self.new_ref(interface=uuid.uuid4().hex) + with self.assertRaises(Exception): + self.manager.create(**utils.parameterize(ref)) + + def test_update_public_interface(self): + ref = self.new_ref(interface='public') + self.test_update(ref) + + def test_update_admin_interface(self): + ref = self.new_ref(interface='admin') + self.test_update(ref) + + def test_update_internal_interface(self): + ref = self.new_ref(interface='internal') + self.test_update(ref) + + def test_update_invalid_interface(self): + ref = self.new_ref(interface=uuid.uuid4().hex) + with self.assertRaises(Exception): + self.manager.update(**utils.parameterize(ref)) + + def test_list_public_interface(self): + interface = 'public' + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + self.test_list(expected_path=expected_path, interface=interface) + + def test_list_admin_interface(self): + interface = 'admin' + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + self.test_list(expected_path=expected_path, interface=interface) + + def test_list_internal_interface(self): + interface = 'admin' + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + self.test_list(expected_path=expected_path, interface=interface) + + def test_list_invalid_interface(self): + interface = uuid.uuid4().hex + expected_path = 'v3/%s?interface=%s' % (self.collection_key, interface) + with self.assertRaises(Exception): + self.manager.list(expected_path=expected_path, interface=interface) diff --git a/tests/v3/test_policies.py b/tests/v3/test_policies.py new file mode 100644 index 000000000..fd3c74ee7 --- /dev/null +++ b/tests/v3/test_policies.py @@ -0,0 +1,21 @@ +import uuid + +from keystoneclient.v3 import policies +from tests.v3 import utils + + +class PolicyTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(PolicyTests, self).setUp() + self.additionalSetUp() + self.key = 'policy' + self.collection_key = 'policies' + self.model = policies.Policy + self.manager = self.client.policies + + def new_ref(self, **kwargs): + kwargs = super(PolicyTests, self).new_ref(**kwargs) + kwargs.setdefault('endpoint_id', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('blob', uuid.uuid4().hex) + return kwargs diff --git a/tests/v3/test_projects.py b/tests/v3/test_projects.py new file mode 100644 index 000000000..8a4ef4090 --- /dev/null +++ b/tests/v3/test_projects.py @@ -0,0 +1,69 @@ +import httplib2 +import urlparse +import uuid + +from keystoneclient.v3 import projects +from tests.v3 import utils + + +class ProjectTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(ProjectTests, self).setUp() + self.additionalSetUp() + self.key = 'project' + self.collection_key = 'projects' + self.model = projects.Project + self.manager = self.client.projects + + def new_ref(self, **kwargs): + kwargs = super(ProjectTests, self).new_ref(**kwargs) + kwargs.setdefault('domain_id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + def test_list_projects_for_user(self): + ref_list = [self.new_ref(), self.new_ref()] + + user_id = uuid.uuid4().hex + resp = httplib2.Response({ + 'status': 200, + 'body': self.serialize(ref_list), + }) + + method = 'GET' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/users/%s/%s' % (user_id, self.collection_key)), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + returned_list = self.manager.list(user=user_id) + self.assertTrue(len(returned_list)) + [self.assertTrue(isinstance(r, self.model)) for r in returned_list] + + def test_list_projects_for_domain(self): + ref_list = [self.new_ref(), self.new_ref()] + + domain_id = uuid.uuid4().hex + resp = httplib2.Response({ + 'status': 200, + 'body': self.serialize(ref_list), + }) + + method = 'GET' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/%s?domain_id=%s' % (self.collection_key, domain_id)), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + returned_list = self.manager.list(domain=domain_id) + self.assertTrue(len(returned_list)) + [self.assertTrue(isinstance(r, self.model)) for r in returned_list] diff --git a/tests/v3/test_roles.py b/tests/v3/test_roles.py new file mode 100644 index 000000000..e3fe35314 --- /dev/null +++ b/tests/v3/test_roles.py @@ -0,0 +1,252 @@ +import httplib2 +import urlparse +import uuid + +from keystoneclient import exceptions +from keystoneclient.v3 import roles +from tests.v3 import utils + + +class RoleTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(RoleTests, self).setUp() + self.additionalSetUp() + self.key = 'role' + self.collection_key = 'roles' + self.model = roles.Role + self.manager = self.client.roles + + def new_ref(self, **kwargs): + kwargs = super(RoleTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + return kwargs + + def test_domain_role_grant(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + resp = httplib2.Response({ + 'status': 201, + 'body': '', + }) + + method = 'PUT' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/domains/%s/users/%s/%s/%s' % ( + domain_id, user_id, self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.grant(role=ref['id'], domain=domain_id, user=user_id) + + def test_domain_role_list(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + resp = httplib2.Response({ + 'status': 200, + 'body': self.serialize(ref_list), + }) + + method = 'GET' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/domains/%s/users/%s/%s' % ( + domain_id, user_id, self.collection_key)), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.list(domain=domain_id, user=user_id) + + def test_domain_role_check(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + resp = httplib2.Response({ + 'status': 200, + 'body': '', + }) + + method = 'HEAD' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/domains/%s/users/%s/%s/%s' % ( + domain_id, user_id, self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.check(role=ref['id'], domain=domain_id, user=user_id) + + def test_domain_role_revoke(self): + user_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + resp = httplib2.Response({ + 'status': 204, + 'body': '', + }) + + method = 'DELETE' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/domains/%s/users/%s/%s/%s' % ( + domain_id, user_id, self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.revoke(role=ref['id'], domain=domain_id, user=user_id) + + def test_project_role_grant(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + resp = httplib2.Response({ + 'status': 201, + 'body': '', + }) + + method = 'PUT' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/projects/%s/users/%s/%s/%s' % ( + project_id, user_id, self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.grant(role=ref['id'], project=project_id, user=user_id) + + def test_project_role_list(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref_list = [self.new_ref(), self.new_ref()] + resp = httplib2.Response({ + 'status': 200, + 'body': self.serialize(ref_list), + }) + + method = 'GET' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/projects/%s/users/%s/%s' % ( + project_id, user_id, self.collection_key)), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.list(project=project_id, user=user_id) + + def test_project_role_check(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + resp = httplib2.Response({ + 'status': 200, + 'body': '', + }) + + method = 'HEAD' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/projects/%s/users/%s/%s/%s' % ( + project_id, user_id, self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.check(role=ref['id'], project=project_id, user=user_id) + + def test_project_role_revoke(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + ref = self.new_ref() + resp = httplib2.Response({ + 'status': 204, + 'body': '', + }) + + method = 'DELETE' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/projects/%s/users/%s/%s/%s' % ( + project_id, user_id, self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.revoke(role=ref['id'], project=project_id, user=user_id) + + def test_domain_project_role_grant_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.grant, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_list_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + + self.assertRaises( + exceptions.ValidationError, + self.manager.list, + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_check_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.check, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) + + def test_domain_project_role_revoke_fails(self): + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = self.new_ref() + + self.assertRaises( + exceptions.ValidationError, + self.manager.revoke, + role=ref['id'], + domain=domain_id, + project=project_id, + user=user_id) diff --git a/tests/v3/test_services.py b/tests/v3/test_services.py new file mode 100644 index 000000000..545e84e1a --- /dev/null +++ b/tests/v3/test_services.py @@ -0,0 +1,21 @@ +import uuid + +from keystoneclient.v3 import services +from tests.v3 import utils + + +class ServiceTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(ServiceTests, self).setUp() + self.additionalSetUp() + self.key = 'service' + self.collection_key = 'services' + self.model = services.Service + self.manager = self.client.services + + def new_ref(self, **kwargs): + kwargs = super(ServiceTests, self).new_ref(**kwargs) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('type', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + return kwargs diff --git a/tests/v3/test_users.py b/tests/v3/test_users.py new file mode 100644 index 000000000..ee9f9d875 --- /dev/null +++ b/tests/v3/test_users.py @@ -0,0 +1,23 @@ +import uuid + +from keystoneclient.v3 import users +from tests.v3 import utils + + +class UserTests(utils.TestCase, utils.CrudTests): + def setUp(self): + super(UserTests, self).setUp() + self.additionalSetUp() + self.key = 'user' + self.collection_key = 'users' + self.model = users.User + self.manager = self.client.users + + def new_ref(self, **kwargs): + kwargs = super(UserTests, self).new_ref(**kwargs) + kwargs.setdefault('description', uuid.uuid4().hex) + kwargs.setdefault('domain_id', uuid.uuid4().hex) + kwargs.setdefault('enabled', True) + kwargs.setdefault('name', uuid.uuid4().hex) + kwargs.setdefault('project_id', uuid.uuid4().hex) + return kwargs diff --git a/tests/v3/utils.py b/tests/v3/utils.py new file mode 100644 index 000000000..d45a07cba --- /dev/null +++ b/tests/v3/utils.py @@ -0,0 +1,227 @@ +import json +import uuid +import time +import urlparse + +import httplib2 +import mox +import unittest2 as unittest + +from keystoneclient.v3 import client + + +def parameterize(ref): + """Rewrites attributes to match the kwarg naming convention in client. + + >>> paramterize({'project_id': 0}) + {'project': 0} + + """ + params = ref.copy() + for key in ref: + if key[-3:] == '_id': + params.setdefault(key[:-3], params.pop(key)) + return params + + +class TestCase(unittest.TestCase): + TEST_TENANT_NAME = 'aTenant' + TEST_TOKEN = 'aToken' + TEST_USER = 'test' + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v3') + + def setUp(self): + super(TestCase, self).setUp() + self.mox = mox.Mox() + self._original_time = time.time + time.time = lambda: 1234 + httplib2.Http.request = self.mox.CreateMockAnything() + self.client = client.Client(username=self.TEST_USER, + token=self.TEST_TOKEN, + tenant_name=self.TEST_TENANT_NAME, + auth_url=self.TEST_URL, + endpoint=self.TEST_URL) + + def tearDown(self): + time.time = self._original_time + super(TestCase, self).tearDown() + self.mox.UnsetStubs() + self.mox.VerifyAll() + + +class UnauthenticatedTestCase(unittest.TestCase): + """ Class used as base for unauthenticated calls """ + TEST_ROOT_URL = 'http://127.0.0.1:5000/' + TEST_URL = '%s%s' % (TEST_ROOT_URL, 'v3') + TEST_ROOT_ADMIN_URL = 'http://127.0.0.1:35357/' + TEST_ADMIN_URL = '%s%s' % (TEST_ROOT_ADMIN_URL, 'v3') + + def setUp(self): + super(UnauthenticatedTestCase, self).setUp() + self.mox = mox.Mox() + self._original_time = time.time + time.time = lambda: 1234 + httplib2.Http.request = self.mox.CreateMockAnything() + + def tearDown(self): + time.time = self._original_time + super(UnauthenticatedTestCase, self).tearDown() + self.mox.UnsetStubs() + self.mox.VerifyAll() + + +class CrudTests(object): + key = None + collection_key = None + model = None + manager = None + + def new_ref(self, **kwargs): + kwargs.setdefault('id', uuid.uuid4().hex) + return kwargs + + def additionalSetUp(self): + self.headers = { + 'GET': { + 'X-Auth-Token': 'aToken', + 'User-Agent': 'python-keystoneclient', + } + } + + self.headers['HEAD'] = self.headers['GET'].copy() + self.headers['DELETE'] = self.headers['GET'].copy() + self.headers['PUT'] = self.headers['GET'].copy() + self.headers['POST'] = self.headers['GET'].copy() + self.headers['POST']['Content-Type'] = 'application/json' + self.headers['PATCH'] = self.headers['POST'].copy() + + def serialize(self, entity): + if isinstance(entity, dict): + return json.dumps({self.key: entity}, sort_keys=True) + if isinstance(entity, list): + return json.dumps({self.collection_key: entity}, sort_keys=True) + raise NotImplementedError('Are you sure you want to serialize that?') + + def test_create(self, ref=None): + ref = ref or self.new_ref() + resp = httplib2.Response({ + 'status': 201, + 'body': self.serialize(ref), + }) + + method = 'POST' + req_ref = ref.copy() + req_ref.pop('id') + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/%s' % self.collection_key), + method, + body=self.serialize(req_ref), + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + returned = self.manager.create(**parameterize(req_ref)) + self.assertTrue(isinstance(returned, self.model)) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + + def test_get(self, ref=None): + ref = ref or self.new_ref() + resp = httplib2.Response({ + 'status': 200, + 'body': self.serialize(ref), + }) + method = 'GET' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/%s/%s' % (self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + returned = self.manager.get(ref['id']) + self.assertTrue(isinstance(returned, self.model)) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + + def test_list(self, ref_list=None, expected_path=None, **filter_kwargs): + ref_list = ref_list or [self.new_ref(), self.new_ref()] + + resp = httplib2.Response({ + 'status': 200, + 'body': self.serialize(ref_list), + }) + + method = 'GET' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + expected_path or 'v3/%s' % self.collection_key), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + returned_list = self.manager.list(**filter_kwargs) + self.assertTrue(len(returned_list)) + [self.assertTrue(isinstance(r, self.model)) for r in returned_list] + + def test_update(self, ref=None): + ref = ref or self.new_ref() + req_ref = ref.copy() + del req_ref['id'] + + resp = httplib2.Response({ + 'status': 200, + 'body': self.serialize(ref), + }) + + method = 'PATCH' + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/%s/%s' % (self.collection_key, ref['id'])), + method, + body=self.serialize(req_ref), + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + returned = self.manager.update(ref['id'], **parameterize(req_ref)) + self.assertTrue(isinstance(returned, self.model)) + for attr in ref: + self.assertEqual( + getattr(returned, attr), + ref[attr], + 'Expected different %s' % attr) + + def test_delete(self, ref=None): + ref = ref or self.new_ref() + method = 'DELETE' + resp = httplib2.Response({ + 'status': 204, + 'body': '', + }) + httplib2.Http.request( + urlparse.urljoin( + self.TEST_URL, + 'v3/%s/%s' % (self.collection_key, ref['id'])), + method, + headers=self.headers[method]) \ + .AndReturn((resp, resp['body'])) + self.mox.ReplayAll() + + self.manager.delete(ref['id'])