diff options
| author | termie <github@anarkystic.com> | 2011-11-14 09:57:41 -0800 |
|---|---|---|
| committer | termie <github@anarkystic.com> | 2011-11-14 09:57:41 -0800 |
| commit | 096eb3fa568f3dd14df3703c2d15a499ff4e5712 (patch) | |
| tree | 99fadb8f6d7c09b907b375edee8f8abb091f8369 | |
| parent | adbbe0147e9e726db2dc6f2c2d4e446fa589c5ba (diff) | |
| parent | f2e73bc9b20b26947980067bcf95c9989e37907d (diff) | |
| download | keystone-096eb3fa568f3dd14df3703c2d15a499ff4e5712.tar.gz keystone-096eb3fa568f3dd14df3703c2d15a499ff4e5712.tar.xz keystone-096eb3fa568f3dd14df3703c2d15a499ff4e5712.zip | |
Merge branch 'crud' of github.com:termie/keystonelight into crud
| -rwxr-xr-x | bin/ksl | 1 | ||||
| -rw-r--r-- | keystonelight/client.py | 100 | ||||
| -rw-r--r-- | keystonelight/identity.py | 28 | ||||
| -rw-r--r-- | keystonelight/middleware.py | 94 | ||||
| -rw-r--r-- | keystonelight/service.py | 313 | ||||
| -rw-r--r-- | tests/default.conf | 37 | ||||
| -rw-r--r-- | tests/keystone_compat_diablo.conf | 6 | ||||
| -rw-r--r-- | tests/keystoneclient_compat_master.conf | 6 | ||||
| -rw-r--r-- | tests/test_identity_api.py | 75 |
9 files changed, 462 insertions, 198 deletions
@@ -33,7 +33,6 @@ class LoadData(BaseApp): pass - CMDS = {'loaddata': LoadData, } diff --git a/keystonelight/client.py b/keystonelight/client.py new file mode 100644 index 00000000..25128ba3 --- /dev/null +++ b/keystonelight/client.py @@ -0,0 +1,100 @@ + +"""Client library for KeystoneLight API.""" + +import json + +import httplib2 +import webob + +from keystonelight import service +from keystonelight import wsgi + + +URLMAP = service.URLMAP + + +class Client(object): + def __init__(self, token=None): + self.token = token + + def request(self, method, path, headers, body): + raise NotImplemented + + def get(self, path, headers=None): + return self.request('GET', path=path, headers=headers) + + def post(self, path, headers=None, body=None): + return self.request('POST', path=path, headers=headers, body=body) + + def put(self, path, headers=None, body=None): + return self.request('PUT', path=path, headers=headers, body=body) + + def _build_headers(self, headers=None): + if headers is None: + headers = {} + + if self.token: + headers.setdefault('X-Auth-Token', self.token) + + return headers + + def __getattr__(self, key): + """Lazy way to define a bunch of dynamic urls based on URLMAP. + + Turns something like + + c.authenticate(user_id='foo', password='bar') + + into + + c.request('POST', '/token', body={'user_id': 'foo', 'password': 'bar'}) + + """ + if key not in URLMAP: + raise AttributeError(key) + + method, path = URLMAP[key] + + def _internal(method_=method, path_=path, **kw): + path_ = path_ % kw + params = {'method': method_, + 'path': path_} + if method.lower() in ('put', 'post'): + params['body'] = kw + return self.request(**params) + + setattr(self, key, _internal) + + return getattr(self, key) + + +class HttpClient(Client): + def __init__(self, endpoint=None, token=None): + self.endpoint = endpoint + super(HttpClient, self).__init__(token=token) + + def request(self, method, path, headers=None, body=None): + if type(body) is type({}): + body = json.dumps(body) + headers = self._build_headers(headers) + h = httplib.Http() + resp, content = h.request(path, method=method, headers=headers, body=body) + return webob.Response(content, status=resp.status, headerlist=resp.headers) + + +class TestClient(Client): + def __init__(self, app=None, token=None): + self.app = app + super(TestClient, self).__init__(token=token) + + def request(self, method, path, headers=None, body=None): + if type(body) is type({}): + body = json.dumps(body) + headers = self._build_headers(headers) + req = wsgi.Request.blank(path) + req.method = method + for k, v in headers.iteritems(): + req.headers[k] = v + if body: + req.body = body + return req.get_response(self.app) diff --git a/keystonelight/identity.py b/keystonelight/identity.py index a144e938..1b2a2f30 100644 --- a/keystonelight/identity.py +++ b/keystonelight/identity.py @@ -32,3 +32,31 @@ class Manager(object): def get_extras(self, context, user_id, tenant_id): return self.driver.get_extras(user_id, tenant_id) + + # CRUD operations + def create_user(self, context, user_id, data): + return self.driver.create_user(user_id, data) + + def update_user(self, context, user_id, data): + return self.driver.update_user(user_id, data) + + def delete_user(self, context, user_id): + return self.driver.delete_user(user_id) + + def create_tenant(self, context, tenant_id, data): + return self.driver.create_tenant(tenant_id, data) + + def update_tenant(self, context, tenant_id, data): + return self.driver.update_tenant(tenant_id, data) + + def delete_tenant(self, context, tenant_id): + return self.driver.delete_tenant(tenant_id) + + def create_extras(self, context, user_id, tenant_id, data): + return self.driver.create_extras(user_id, tenant_id, data) + + def update_extras(self, context, user_id, tenant_id, data): + return self.driver.update_extras(user_id, tenant_id, data) + + def delete_extras(self, context, user_id, tenant_id): + return self.driver.delete_extras(user_id, tenant_id) diff --git a/keystonelight/middleware.py b/keystonelight/middleware.py new file mode 100644 index 00000000..29e655bd --- /dev/null +++ b/keystonelight/middleware.py @@ -0,0 +1,94 @@ +import json + +from keystonelight import wsgi + + +# Header used to transmit the auth token +AUTH_TOKEN_HEADER = 'X-Auth-Token' + + +# Environment variable used to pass the request context +CONTEXT_ENV = 'openstack.context' + + +# Environment variable used to pass the request params +PARAMS_ENV = 'openstack.params' + + +class TokenAuthMiddleware(wsgi.Middleware): + def process_request(self, request): + token = request.headers.get(AUTH_TOKEN_HEADER) + context = request.environ.get(CONTEXT_ENV, {}) + context['token_id'] = token + request.environ[CONTEXT_ENV] = context + + +class AdminTokenAuthMiddleware(wsgi.Middleware): + """A trivial filter that checks for a pre-defined admin token. + + Sets 'is_admin' to true in the context, expected to be checked by + methods that are admin-only. + + """ + + def process_request(self, request): + token = request.headers.get(AUTH_TOKEN_HEADER) + context = request.environ.get(CONTEXT_ENV, {}) + context['is_admin'] = (token == self.options['admin_token']) + request.environ[CONTEXT_ENV] = context + + +class PostParamsMiddleware(wsgi.Middleware): + """Middleware to allow method arguments to be passed as POST parameters. + + Filters out the parameters `self`, `context` and anything beginning with + an underscore. + + """ + + def process_request(self, request): + params_parsed = request.params + params = {} + for k, v in params_parsed.iteritems(): + if k in ('self', 'context'): + continue + if k.startswith('_'): + continue + params[k] = v + + request.environ[PARAMS_ENV] = params + + +class JsonBodyMiddleware(wsgi.Middleware): + """Middleware to allow method arguments to be passed as serialized JSON. + + Accepting arguments as JSON is useful for accepting data that may be more + complex than simple primitives. + + In this case we accept it as urlencoded data under the key 'json' as in + json=<urlencoded_json> but this could be extended to accept raw JSON + in the POST body. + + Filters out the parameters `self`, `context` and anything beginning with + an underscore. + + """ + + def process_request(self, request): + #if 'json' not in request.params: + # return + + params_json = request.body + if not params_json: + return + + params_parsed = json.loads(params_json) + params = {} + for k, v in params_parsed.iteritems(): + if k in ('self', 'context'): + continue + if k.startswith('_'): + continue + params[k] = v + + request.environ[PARAMS_ENV] = params diff --git a/keystonelight/service.py b/keystonelight/service.py index 797a3415..682f5151 100644 --- a/keystonelight/service.py +++ b/keystonelight/service.py @@ -1,7 +1,3 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# this is the web service frontend - import json import logging @@ -15,206 +11,151 @@ from keystonelight import utils from keystonelight import wsgi -class BaseApplication(wsgi.Application): - @webob.dec.wsgify - def __call__(self, req): - arg_dict = req.environ['wsgiorg.routing_args'][1] - action = arg_dict['action'] - del arg_dict['action'] - del arg_dict['controller'] - logging.debug('arg_dict: %s', arg_dict) - - context = req.environ.get('openstack.context', {}) - # allow middleware up the stack to override the params - params = {} - if 'openstack.params' in req.environ: - params = req.environ['openstack.params'] - params.update(arg_dict) - - # TODO(termie): do some basic normalization on methods - method = getattr(self, action) - - # NOTE(vish): make sure we have no unicode keys for py2.6. - params = dict([(str(k), v) for (k, v) in params.iteritems()]) - result = method(context, **params) - - if result is None or type(result) is str or type(result) is unicode: - return result - - return json.dumps(result) - - -class TokenAuthMiddleware(wsgi.Middleware): - def process_request(self, request): - token = request.headers.get('X-Auth-Token') - context = request.environ.get('openstack.context', {}) - context['token_id'] = token - request.environ['openstack.context'] = context - - -class AdminTokenAuthMiddleware(wsgi.Middleware): - """A trivial filter that checks for a pre-defined admin token. - - Sets 'is_admin' to true in the context, expected to be checked by - methods that are admin-only. - - """ - def process_request(self, request): - token = request.headers.get('X-Auth-Token') - context = request.environ.get('openstack.context', {}) - context['is_admin'] = (token == self.options['admin_token']) - request.environ['openstack.context'] = context - +HIGH_LEVEL_CALLS = { + 'authenticate': ('POST', '/tokens'), + 'get_tenants': ('GET', '/user/%(user_id)s/tenants'), + 'get_user': ('GET', '/user/%(user_id)s'), + 'get_tenant': ('GET', '/tenant/%(tenant_id)s'), + 'get_tenant_by_name': ('GET', '/tenant_name/%(tenant_name)s'), + 'get_extras': ('GET', '/extras/%(tenant_id)s-%(user_id)s'), + 'get_token': ('GET', '/token/%(token_id)s'), + } + +# NOTE(termie): creates are seperate from updates to allow duplication checks +LOW_LEVEL_CALLS = { + # tokens + 'create_token': ('POST', '/token'), + 'delete_token': ('DELETE', '/token/%(token_id)s'), + # users + 'create_user': ('POST', '/user'), + 'update_user': ('PUT', '/user/%(user_id)s'), + 'delete_user': ('DELETE', '/user/%(user_id)s'), + # tenants + 'create_tenant': ('POST', '/tenant'), + 'update_tenant': ('PUT', '/tenant/%(tenant_id)s'), + 'delete_tenant': ('DELETE', '/tenant/%(tenant_id)s'), + # extras + # NOTE(termie): these separators are probably going to bite us eventually + 'create_extras': ('POST', '/extras'), + 'update_extras': ('PUT', '/extras/%(tenant_id)s-%(user_id)s'), + 'delete_extras': ('DELETE', '/extras/%(tenant_id)s-%(user_id)s'), + } + + +URLMAP = HIGH_LEVEL_CALLS.copy() +URLMAP.update(LOW_LEVEL_CALLS) -class PostParamsMiddleware(wsgi.Middleware): - """Middleware to allow method arguments to be passed as POST parameters. - Filters out the parameters `self`, `context` and anything beginning with - an underscore. - - """ - - def process_request(self, request): - params_parsed = request.params - params = {} - for k, v in params_parsed.iteritems(): - if k in ('self', 'context'): - continue - if k.startswith('_'): - continue - params[k] = v - - request.environ['openstack.params'] = params - - -class JsonBodyMiddleware(wsgi.Middleware): - """Middleware to allow method arguments to be passed as serialized JSON. - - Accepting arguments as JSON is useful for accepting data that may be more - complex than simple primitives. - - In this case we accept it as urlencoded data under the key 'json' as in - json=<urlencoded_json> but this could be extended to accept raw JSON - in the POST body. - - Filters out the parameters `self`, `context` and anything beginning with - an underscore. +class BaseApplication(wsgi.Application): + @webob.dec.wsgify + def __call__(self, req): + arg_dict = req.environ['wsgiorg.routing_args'][1] + action = arg_dict['action'] + del arg_dict['action'] + del arg_dict['controller'] + logging.debug('arg_dict: %s', arg_dict) - """ + context = req.environ.get('openstack.context', {}) + # allow middleware up the stack to override the params + params = {} + if 'openstack.params' in req.environ: + params = req.environ['openstack.params'] + params.update(arg_dict) - def process_request(self, request): - #if 'json' not in request.params: - # return + # TODO(termie): do some basic normalization on methods + method = getattr(self, action) - params_json = request.body - if not params_json: - return + # NOTE(vish): make sure we have no unicode keys for py2.6. + params = dict([(str(k), v) for (k, v) in params.iteritems()]) + result = method(context, **params) - params_parsed = json.loads(params_json) - params = {} - for k, v in params_parsed.iteritems(): - if k in ('self', 'context'): - continue - if k.startswith('_'): - continue - params[k] = v + if result is None or type(result) is str or type(result) is unicode: + return result - request.environ['openstack.params'] = params + return json.dumps(result) class TokenController(BaseApplication): - """Validate and pass through calls to TokenManager.""" + """Validate and pass through calls to TokenManager.""" - def __init__(self, options): - self.token_api = token.Manager(options=options) - self.options = options + def __init__(self, options): + self.token_api = token.Manager(options=options) + self.options = options - def validate_token(self, context, token_id): - token_info = self.token_api.validate_token(context, token_id) - if not token_info: - raise webob.exc.HTTPUnauthorized() - return token_info + def validate_token(self, context, token_id): + token_info = self.token_api.validate_token(context, token_id) + if not token_info: + raise webob.exc.HTTPUnauthorized() + return token_info class IdentityController(BaseApplication): - """Validate and pass calls through to IdentityManager. - - IdentityManager will also pretty much just pass calls through to - a specific driver. - """ - - def __init__(self, options): - self.identity_api = identity.Manager(options=options) - self.token_api = token.Manager(options=options) - self.options = options - - def authenticate(self, context, **kwargs): - tenant, user, extras = self.identity_api.authenticate(context, - **kwargs) - token = self.token_api.create_token(context, - dict(tenant=tenant, - user=user, - extras=extras)) - logging.debug('TOKEN: %s', token) - return token - - def get_tenants(self, context): - token_id = context.get('token') - token = self.token_api.validate_token(context, token_id) - - return self.identity_api.get_tenants(context, - user_id=token['user']) + """Validate and pass calls through to IdentityManager. + + IdentityManager will also pretty much just pass calls through to + a specific driver. + """ + + def __init__(self, options): + self.identity_api = identity.Manager(options=options) + self.token_api = token.Manager(options=options) + self.options = options + + def authenticate(self, context, **kwargs): + user_ref, tenant_ref, extras_ref = self.identity_api.authenticate( + context, **kwargs) + # TODO(termie): strip password from return values + token_ref = self.token_api.create_token(context, + dict(tenant=tenant_ref, + user=user_ref, + extras=extras_ref)) + logging.debug('TOKEN: %s', token_ref) + return token_ref + + def get_tenants(self, context, user_id=None): + token_id = context.get('token_id') + token_ref = self.token_api.get_token(context, token_id) + assert token_ref + assert token_ref['user']['id'] == user_id + tenants_ref = [] + for tenant_id in token_ref['user']['tenants']: + tenants_ref.append(self.identity_api.get_tenant(context, + tenant_id)) + + return tenants_ref class Router(wsgi.Router): - def __init__(self, options): - self.options = options - token_controller = utils.import_object( - options['token_controller'], - options=options) - identity_controller = utils.import_object( - options['identity_controller'], - options=options) - mapper = routes.Mapper() - mapper.connect('/v2.0/tokens', controller=identity_controller, - action='authenticate') - mapper.connect('/v2.0/tokens/{token_id}', controller=token_controller, - action='revoke_token', - conditions=dict(method=['DELETE'])) - mapper.connect("/v2.0/tenants", controller=identity_controller, - action="get_tenants", conditions=dict(method=["GET"])) - super(Router, self).__init__(mapper) - - -class AdminRouter(wsgi.Router): - def __init__(self, options): - self.options = options - token_controller = utils.import_object( - options['token_controller'], - options=options) - identity_controller = utils.import_object( - options['identity_controller'], - options=options) - mapper = routes.Mapper() - - mapper.connect('/v2.0/tokens', controller=identity_controller, - action='authenticate') - mapper.connect('/v2.0/tokens/{token_id}', controller=token_controller, - action='validate_token', - conditions=dict(method=['GET'])) - mapper.connect('/v2.0/tokens/{token_id}', controller=token_controller, - action='revoke_token', - conditions=dict(method=['DELETE'])) - super(AdminRouter, self).__init__(mapper) - - -def identity_app_factory(global_conf, **local_conf): - conf = global_conf.copy() - conf.update(local_conf) - return Router(conf) + def __init__(self, options): + self.options = options + self.identity_controller = IdentityController(options) + self.token_controller = TokenController(options) + + mapper = self._build_map(URLMAP) + super(Router, self).__init__(mapper) + + def _build_map(self, urlmap): + """Build a routes.Mapper based on URLMAP.""" + mapper = routes.Mapper() + for k, v in urlmap.iteritems(): + # NOTE(termie): hack + if 'token' in k: + controller = self.token_controller + else: + controller = self.identity_controller + action = k + method, path = v + path = path.replace('%(', '{').replace(')s', '}') + + mapper.connect(path, + controller=controller, + action=action, + conditions=dict(method=[method])) + + return mapper def app_factory(global_conf, **local_conf): - conf = global_conf.copy() - conf.update(local_conf) - return Router(conf) + conf = global_conf.copy() + conf.update(local_conf) + return Router(conf) diff --git a/tests/default.conf b/tests/default.conf index 68388b27..220a350f 100644 --- a/tests/default.conf +++ b/tests/default.conf @@ -1,23 +1,50 @@ [DEFAULT] -catalog_driver = keystonelight.backends.kvs.KvsCatalog +catalog_driver = keystonelight.backends.templated.TemplatedCatalog identity_driver = keystonelight.backends.kvs.KvsIdentity token_driver = keystonelight.backends.kvs.KvsToken +public_port = 5000 admin_token = ADMIN +# config for TemplatedCatalog, using camelCase because I don't want to do +# translations for keystone compat +catalog.RegionOne.identity.publicURL = http://localhost:$(public_port)s/v2.0 +catalog.RegionOne.identity.adminURL = http://localhost:$(public_port)s/v2.0 +catalog.RegionOne.identity.internalURL = http://localhost:$(public_port)s/v2.0 +catalog.RegionOne.identity.name = 'Identity Service' + +# fake compute service for now to help novaclient tests work +compute_port = 3000 +catalog.RegionOne.compute.publicURL = http://localhost:$(compute_port)s/v1.1/$(tenant_id)s +catalog.RegionOne.compute.adminURL = http://localhost:$(compute_port)s/v1.1/$(tenant_id)s +catalog.RegionOne.compute.internalURL = http://localhost:$(compute_port)s/v1.1/$(tenant_id)s +catalog.RegionOne.compute.name = 'Compute Service' + + [filter:debug] paste.filter_factory = keystonelight.wsgi:Debug.factory [filter:token_auth] -paste.filter_factory = keystonelight.service:TokenAuthMiddleware.factory +paste.filter_factory = keystonelight.middleware:TokenAuthMiddleware.factory [filter:admin_token_auth] -paste.filter_factory = keystonelight.service:AdminTokenAuthMiddleware.factory +paste.filter_factory = keystonelight.middleware:AdminTokenAuthMiddleware.factory [filter:json_body] -paste.filter_factory = keystonelight.service:JsonBodyMiddleware.factory +paste.filter_factory = keystonelight.middleware:JsonBodyMiddleware.factory [app:keystonelight] paste.app_factory = keystonelight.service:app_factory -[pipeline:main] +[app:keystone] +paste.app_factory = keystonelight.keystone_compat:app_factory + +[pipeline:keystone_api] +pipeline = token_auth admin_token_auth json_body debug keystone + +[pipeline:keystonelight_api] pipeline = token_auth admin_token_auth json_body debug keystonelight + +[composite:main] +use = egg:Paste#urlmap +/ = keystonelight_api +/v2.0 = keystone_api diff --git a/tests/keystone_compat_diablo.conf b/tests/keystone_compat_diablo.conf index d9052631..94024df3 100644 --- a/tests/keystone_compat_diablo.conf +++ b/tests/keystone_compat_diablo.conf @@ -9,13 +9,13 @@ admin_token = ADMIN paste.filter_factory = keystonelight.wsgi:Debug.factory [filter:token_auth] -paste.filter_factory = keystonelight.service:TokenAuthMiddleware.factory +paste.filter_factory = keystonelight.middleware:TokenAuthMiddleware.factory [filter:admin_token_auth] -paste.filter_factory = keystonelight.service:AdminTokenAuthMiddleware.factory +paste.filter_factory = keystonelight.middleware:AdminTokenAuthMiddleware.factory [filter:json_body] -paste.filter_factory = keystonelight.service:JsonBodyMiddleware.factory +paste.filter_factory = keystonelight.middleware:JsonBodyMiddleware.factory [app:keystone] paste.app_factory = keystonelight.keystone_compat:app_factory diff --git a/tests/keystoneclient_compat_master.conf b/tests/keystoneclient_compat_master.conf index e006e821..e9861b6b 100644 --- a/tests/keystoneclient_compat_master.conf +++ b/tests/keystoneclient_compat_master.conf @@ -24,13 +24,13 @@ catalog.RegionOne.compute.name = 'Compute Service' paste.filter_factory = keystonelight.wsgi:Debug.factory [filter:token_auth] -paste.filter_factory = keystonelight.service:TokenAuthMiddleware.factory +paste.filter_factory = keystonelight.middleware:TokenAuthMiddleware.factory [filter:admin_token_auth] -paste.filter_factory = keystonelight.service:AdminTokenAuthMiddleware.factory +paste.filter_factory = keystonelight.middleware:AdminTokenAuthMiddleware.factory [filter:json_body] -paste.filter_factory = keystonelight.service:JsonBodyMiddleware.factory +paste.filter_factory = keystonelight.middleware:JsonBodyMiddleware.factory [app:keystone] paste.app_factory = keystonelight.keystone_compat:app_factory diff --git a/tests/test_identity_api.py b/tests/test_identity_api.py new file mode 100644 index 00000000..885d4314 --- /dev/null +++ b/tests/test_identity_api.py @@ -0,0 +1,75 @@ +import json +import uuid + +from keystonelight import client +from keystonelight import models +from keystonelight import test +from keystonelight import utils +from keystonelight.backends import kvs + + +class IdentityApi(test.TestCase): + def setUp(self): + super(IdentityApi, self).setUp() + self.options = self.appconfig('default') + app = self.loadapp('default') + self.app = app + + self.identity_backend = utils.import_object( + self.options['identity_driver'], options=self.options) + self.token_backend = utils.import_object( + self.options['token_driver'], options=self.options) + self.catalog_backend = utils.import_object( + self.options['catalog_driver'], options=self.options) + self._load_fixtures() + + def _load_fixtures(self): + self.tenant_bar = self.identity_backend._create_tenant( + 'bar', + models.Tenant(id='bar', name='BAR')) + self.user_foo = self.identity_backend._create_user( + 'foo', + models.User(id='foo', + name='FOO', + password='foo2', + tenants=[self.tenant_bar['id']])) + self.extras_foobar = self.identity_backend._create_extras( + 'foo', 'bar', + {'extra': 'extra'}) + + def _login(self): + c = client.TestClient(self.app) + post_data = {'user_id': self.user_foo['id'], + 'tenant_id': self.tenant_bar['id'], + 'password': self.user_foo['password']} + resp = c.post('/tokens', body=post_data) + token = json.loads(resp.body) + return token + + def test_authenticate(self): + c = client.TestClient(self.app) + post_data = {'user_id': self.user_foo['id'], + 'tenant_id': self.tenant_bar['id'], + 'password': self.user_foo['password']} + resp = c.authenticate(**post_data) + data = json.loads(resp.body) + self.assertEquals(self.user_foo['id'], data['user']['id']) + self.assertEquals(self.tenant_bar['id'], data['tenant']['id']) + self.assertDictEquals(self.extras_foobar, data['extras']) + + def test_authenticate_no_tenant(self): + c = client.TestClient(self.app) + post_data = {'user_id': self.user_foo['id'], + 'password': self.user_foo['password']} + resp = c.authenticate(**post_data) + data = json.loads(resp.body) + self.assertEquals(self.user_foo['id'], data['user']['id']) + self.assertEquals(None, data['tenant']) + self.assertEquals(None, data['extras']) + + def test_get_tenants(self): + token = self._login() + c = client.TestClient(self.app, token['id']) + resp = c.get_tenants(user_id=self.user_foo['id']) + data = json.loads(resp.body) + self.assertDictEquals(self.tenant_bar, data[0]) |
