launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #14489
[Merge] lp:~jtv/maas/1.2-extract-api-support into lp:maas/1.2
Jeroen T. Vermeulen has proposed merging lp:~jtv/maas/1.2-extract-api-support into lp:maas/1.2.
Commit message:
Backport from trunk: extract support code from massively overweight src/maasserver/api.py.
Requested reviews:
MAAS Maintainers (maas-maintainers)
For more details, see:
https://code.launchpad.net/~jtv/maas/1.2-extract-api-support/+merge/135743
The only actual code change is in get_list_from_dict_or_multidict, where trunk uses "getlist" and 1.2 uses "get_optional_list." To minimize surprises I kept the trunk behaviour in the trunk version, and the 1.2 behaviour in the 1.2 version.
Jeroen
--
https://code.launchpad.net/~jtv/maas/1.2-extract-api-support/+merge/135743
Your team MAAS Maintainers is requested to review the proposed merge of lp:~jtv/maas/1.2-extract-api-support into lp:maas/1.2.
=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py 2012-11-22 15:59:37 +0000
+++ src/maasserver/api.py 2012-11-22 17:37:22 +0000
@@ -98,11 +98,7 @@
)
from django.db.utils import DatabaseError
from django.forms.models import model_to_dict
-from django.http import (
- HttpResponse,
- HttpResponseBadRequest,
- QueryDict,
- )
+from django.http import HttpResponse
from django.shortcuts import (
get_object_or_404,
render_to_response,
@@ -110,7 +106,19 @@
from django.template import RequestContext
from docutils import core
from formencode import validators
-from formencode.validators import Invalid
+from maasserver.api_support import (
+ AnonymousOperationsHandler,
+ operation,
+ OperationsHandler,
+ )
+from maasserver.api_utils import (
+ extract_oauth_key,
+ get_list_from_dict_or_multidict,
+ get_mandatory_param,
+ get_oauth_token,
+ get_optional_list,
+ get_overrided_query_dict,
+ )
from maasserver.apidoc import (
describe_resource,
find_api_resources,
@@ -170,269 +178,11 @@
strip_domain,
)
from maasserver.utils.orm import get_one
-from piston.handler import (
- AnonymousBaseHandler,
- BaseHandler,
- HandlerMetaClass,
- )
-from piston.models import Token
-from piston.resource import Resource
from piston.utils import rc
from provisioningserver.enum import POWER_TYPE
from provisioningserver.kernel_opts import KernelParameters
import simplejson as json
-
-class OperationsResource(Resource):
- """A resource supporting operation dispatch.
-
- All requests are passed onto the handler's `dispatch` method. See
- :class:`OperationsHandler`.
- """
-
- crudmap = Resource.callmap
- callmap = dict.fromkeys(crudmap, "dispatch")
-
-
-class RestrictedResource(OperationsResource):
-
- def authenticate(self, request, rm):
- actor, anonymous = super(
- RestrictedResource, self).authenticate(request, rm)
- if not anonymous and not request.user.is_active:
- raise PermissionDenied("User is not allowed access to this API.")
- else:
- return actor, anonymous
-
-
-class AdminRestrictedResource(RestrictedResource):
-
- def authenticate(self, request, rm):
- actor, anonymous = super(
- AdminRestrictedResource, self).authenticate(request, rm)
- if anonymous or not request.user.is_superuser:
- raise PermissionDenied("User is not allowed access to this API.")
- else:
- return actor, anonymous
-
-
-def operation(idempotent, exported_as=None):
- """Decorator to make a method available on the API.
-
- :param idempotent: If this operation is idempotent. Idempotent operations
- are made available via HTTP GET, non-idempotent operations via HTTP
- POST.
- :param exported_as: Optional operation name; defaults to the name of the
- exported method.
- """
- method = "GET" if idempotent else "POST"
-
- def _decorator(func):
- if exported_as is None:
- func.export = method, func.__name__
- else:
- func.export = method, exported_as
- return func
-
- return _decorator
-
-
-class OperationsHandlerType(HandlerMetaClass):
- """Type for handlers that dispatch operations.
-
- Collects all the exported operations, CRUD and custom, into the class's
- `exports` attribute. This is a signature:function mapping, where signature
- is an (http-method, operation-name) tuple. If operation-name is None, it's
- a CRUD method.
-
- The `allowed_methods` attribute is calculated as the union of all HTTP
- methods required for the exported CRUD and custom operations.
- """
-
- def __new__(metaclass, name, bases, namespace):
- cls = super(OperationsHandlerType, metaclass).__new__(
- metaclass, name, bases, namespace)
-
- # Create a signature:function mapping for CRUD operations.
- crud = {
- (http_method, None): getattr(cls, method)
- for http_method, method in OperationsResource.crudmap.items()
- if getattr(cls, method, None) is not None
- }
-
- # Create a signature:function mapping for non-CRUD operations.
- operations = {
- attribute.export: attribute
- for attribute in vars(cls).values()
- if getattr(attribute, "export", None) is not None
- }
-
- # Create the exports mapping.
- exports = {}
- exports.update(crud)
- exports.update(operations)
-
- # Update the class.
- cls.exports = exports
- cls.allowed_methods = frozenset(
- http_method for http_method, name in exports)
-
- return cls
-
-
-class OperationsHandlerMixin:
- """Handler mixin for operations dispatch.
-
- This enabled dispatch to custom functions that piggyback on HTTP methods
- that ordinarily, in Piston, are used for CRUD operations.
-
- This must be used in cooperation with :class:`OperationsResource` and
- :class:`OperationsHandlerType`.
- """
-
- def dispatch(self, request, *args, **kwargs):
- signature = request.method.upper(), request.REQUEST.get("op")
- function = self.exports.get(signature)
- if function is None:
- return HttpResponseBadRequest(
- "Unrecognised signature: %s %s" % signature)
- else:
- return function(self, request, *args, **kwargs)
-
-
-class OperationsHandler(
- OperationsHandlerMixin, BaseHandler):
- """Base handler that supports operation dispatch."""
-
- __metaclass__ = OperationsHandlerType
-
-
-class AnonymousOperationsHandler(
- OperationsHandlerMixin, AnonymousBaseHandler):
- """Anonymous base handler that supports operation dispatch."""
-
- __metaclass__ = OperationsHandlerType
-
-
-def get_mandatory_param(data, key, validator=None):
- """Get the parameter from the provided data dict or raise a ValidationError
- if this parameter is not present.
-
- :param data: The data dict (usually request.data or request.GET where
- request is a django.http.HttpRequest).
- :param data: dict
- :param key: The parameter's key.
- :type key: basestring
- :param validator: An optional validator that will be used to validate the
- retrieved value.
- :type validator: formencode.validators.Validator
- :return: The value of the parameter.
- :raises: ValidationError
- """
- value = data.get(key, None)
- if value is None:
- raise ValidationError("No provided %s!" % key)
- if validator is not None:
- try:
- return validator.to_python(value)
- except Invalid, e:
- raise ValidationError("Invalid %s: %s" % (key, e.msg))
- else:
- return value
-
-
-def get_optional_list(data, key, default=None):
- """Get the list from the provided data dict or return a default value.
- """
- value = data.getlist(key)
- if value == []:
- return default
- else:
- return value
-
-
-def get_list_from_dict_or_multidict(data, key, default=None):
- """Get a list from 'data'.
-
- If data is a MultiDict, then we use 'getlist' if the data is a plain dict,
- then we just use __getitem__.
-
- The rationale is that data POSTed as multipart/form-data gets parsed into a
- MultiDict, but data POSTed as application/json gets parsed into a plain
- dict(key:list).
- """
- getlist = getattr(data, 'getlist', None)
- if getlist is not None:
- return get_optional_list(data, key, default)
- return data.get(key, default)
-
-
-def extract_oauth_key_from_auth_header(auth_data):
- """Extract the oauth key from auth data in HTTP header.
-
- :param auth_data: {string} The HTTP Authorization header.
-
- :return: The oauth key from the header, or None.
- """
- for entry in auth_data.split():
- key_value = entry.split('=', 1)
- if len(key_value) == 2:
- key, value = key_value
- if key == 'oauth_token':
- return value.rstrip(',').strip('"')
- return None
-
-
-def extract_oauth_key(request):
- """Extract the oauth key from a request's headers.
-
- Raises :class:`Unauthorized` if no key is found.
- """
- auth_header = request.META.get('HTTP_AUTHORIZATION')
- if auth_header is None:
- raise Unauthorized("No authorization header received.")
- key = extract_oauth_key_from_auth_header(auth_header)
- if key is None:
- raise Unauthorized("Did not find request's oauth token.")
- return key
-
-
-def get_oauth_token(request):
- """Get the OAuth :class:`piston.models.Token` used for `request`.
-
- Raises :class:`Unauthorized` if no key is found, or if the token is
- unknown.
- """
- try:
- return Token.objects.get(key=extract_oauth_key(request))
- except Token.DoesNotExist:
- raise Unauthorized("Unknown OAuth token.")
-
-
-def get_overrided_query_dict(defaults, data):
- """Returns a QueryDict with the values of 'defaults' overridden by the
- values in 'data'.
-
- :param defaults: The dictionary containing the default values.
- :type defaults: dict
- :param data: The data used to override the defaults.
- :type data: :class:`django.http.QueryDict`
- :return: The updated QueryDict.
- :raises: :class:`django.http.QueryDict`
- """
- # Create a writable query dict.
- new_data = QueryDict('').copy()
- # Missing fields will be taken from the node's current values. This
- # is to circumvent Django's ModelForm (form created from a model)
- # default behaviour that requires all the fields to be defined.
- new_data.update(defaults)
- # We can't use update here because data is a QueryDict and 'update'
- # does not replaces the old values with the new as one would expect.
- for k, v in data.items():
- new_data[k] = v
- return new_data
-
-
# Node's fields exposed on the API.
DISPLAYED_NODE_FIELDS = (
'system_id',
=== added file 'src/maasserver/api_support.py'
--- src/maasserver/api_support.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/api_support.py 2012-11-22 17:37:22 +0000
@@ -0,0 +1,159 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Supporting infrastructure for Piston-based APIs in MAAS."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = [
+ 'AnonymousOperationsHandler',
+ 'operation',
+ 'OperationsHandler',
+ ]
+
+from django.core.exceptions import PermissionDenied
+from django.http import HttpResponseBadRequest
+from piston.handler import (
+ AnonymousBaseHandler,
+ BaseHandler,
+ HandlerMetaClass,
+ )
+from piston.resource import Resource
+
+
+class OperationsResource(Resource):
+ """A resource supporting operation dispatch.
+
+ All requests are passed onto the handler's `dispatch` method. See
+ :class:`OperationsHandler`.
+ """
+
+ crudmap = Resource.callmap
+ callmap = dict.fromkeys(crudmap, "dispatch")
+
+
+class RestrictedResource(OperationsResource):
+ """A resource that's restricted to active users."""
+
+ def authenticate(self, request, rm):
+ actor, anonymous = super(
+ RestrictedResource, self).authenticate(request, rm)
+ if not anonymous and not request.user.is_active:
+ raise PermissionDenied("User is not allowed access to this API.")
+ else:
+ return actor, anonymous
+
+
+class AdminRestrictedResource(RestrictedResource):
+ """A resource that's restricted to administrators."""
+
+ def authenticate(self, request, rm):
+ actor, anonymous = super(
+ AdminRestrictedResource, self).authenticate(request, rm)
+ if anonymous or not request.user.is_superuser:
+ raise PermissionDenied("User is not allowed access to this API.")
+ else:
+ return actor, anonymous
+
+
+def operation(idempotent, exported_as=None):
+ """Decorator to make a method available on the API.
+
+ :param idempotent: If this operation is idempotent. Idempotent operations
+ are made available via HTTP GET, non-idempotent operations via HTTP
+ POST.
+ :param exported_as: Optional operation name; defaults to the name of the
+ exported method.
+ """
+ method = "GET" if idempotent else "POST"
+
+ def _decorator(func):
+ if exported_as is None:
+ func.export = method, func.__name__
+ else:
+ func.export = method, exported_as
+ return func
+
+ return _decorator
+
+
+class OperationsHandlerType(HandlerMetaClass):
+ """Type for handlers that dispatch operations.
+
+ Collects all the exported operations, CRUD and custom, into the class's
+ `exports` attribute. This is a signature:function mapping, where signature
+ is an (http-method, operation-name) tuple. If operation-name is None, it's
+ a CRUD method.
+
+ The `allowed_methods` attribute is calculated as the union of all HTTP
+ methods required for the exported CRUD and custom operations.
+ """
+
+ def __new__(metaclass, name, bases, namespace):
+ cls = super(OperationsHandlerType, metaclass).__new__(
+ metaclass, name, bases, namespace)
+
+ # Create a signature:function mapping for CRUD operations.
+ crud = {
+ (http_method, None): getattr(cls, method)
+ for http_method, method in OperationsResource.crudmap.items()
+ if getattr(cls, method, None) is not None
+ }
+
+ # Create a signature:function mapping for non-CRUD operations.
+ operations = {
+ attribute.export: attribute
+ for attribute in vars(cls).values()
+ if getattr(attribute, "export", None) is not None
+ }
+
+ # Create the exports mapping.
+ exports = {}
+ exports.update(crud)
+ exports.update(operations)
+
+ # Update the class.
+ cls.exports = exports
+ cls.allowed_methods = frozenset(
+ http_method for http_method, name in exports)
+
+ return cls
+
+
+class OperationsHandlerMixin:
+ """Handler mixin for operations dispatch.
+
+ This enabled dispatch to custom functions that piggyback on HTTP methods
+ that ordinarily, in Piston, are used for CRUD operations.
+
+ This must be used in cooperation with :class:`OperationsResource` and
+ :class:`OperationsHandlerType`.
+ """
+
+ def dispatch(self, request, *args, **kwargs):
+ signature = request.method.upper(), request.REQUEST.get("op")
+ function = self.exports.get(signature)
+ if function is None:
+ return HttpResponseBadRequest(
+ "Unrecognised signature: %s %s" % signature)
+ else:
+ return function(self, request, *args, **kwargs)
+
+
+class OperationsHandler(
+ OperationsHandlerMixin, BaseHandler):
+ """Base handler that supports operation dispatch."""
+
+ __metaclass__ = OperationsHandlerType
+
+
+class AnonymousOperationsHandler(
+ OperationsHandlerMixin, AnonymousBaseHandler):
+ """Anonymous base handler that supports operation dispatch."""
+
+ __metaclass__ = OperationsHandlerType
=== added file 'src/maasserver/api_utils.py'
--- src/maasserver/api_utils.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/api_utils.py 2012-11-22 17:37:22 +0000
@@ -0,0 +1,145 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Helpers for Piston-based MAAS APIs."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = [
+ 'extract_oauth_key',
+ 'get_list_from_dict_or_multidict',
+ 'get_mandatory_param',
+ 'get_oauth_token',
+ 'get_optional_list',
+ 'get_overrided_query_dict',
+ ]
+
+from django.core.exceptions import ValidationError
+from django.http import QueryDict
+from formencode.validators import Invalid
+from maasserver.exceptions import Unauthorized
+from piston.models import Token
+
+
+def get_mandatory_param(data, key, validator=None):
+ """Get the parameter from the provided data dict or raise a ValidationError
+ if this parameter is not present.
+
+ :param data: The data dict (usually request.data or request.GET where
+ request is a django.http.HttpRequest).
+ :param data: dict
+ :param key: The parameter's key.
+ :type key: basestring
+ :param validator: An optional validator that will be used to validate the
+ retrieved value.
+ :type validator: formencode.validators.Validator
+ :return: The value of the parameter.
+ :raises: ValidationError
+ """
+ value = data.get(key, None)
+ if value is None:
+ raise ValidationError("No provided %s!" % key)
+ if validator is not None:
+ try:
+ return validator.to_python(value)
+ except Invalid as e:
+ raise ValidationError("Invalid %s: %s" % (key, e.msg))
+ else:
+ return value
+
+
+def get_optional_list(data, key, default=None):
+ """Get the list from the provided data dict or return a default value.
+ """
+ value = data.getlist(key)
+ if value == []:
+ return default
+ else:
+ return value
+
+
+def get_list_from_dict_or_multidict(data, key, default=None):
+ """Get a list from 'data'.
+
+ If data is a MultiDict, then we use 'getlist' if the data is a plain dict,
+ then we just use __getitem__.
+
+ The rationale is that data POSTed as multipart/form-data gets parsed into a
+ MultiDict, but data POSTed as application/json gets parsed into a plain
+ dict(key:list).
+ """
+ getlist = getattr(data, 'getlist', None)
+ if getlist is not None:
+ return get_optional_list(data, key, default)
+ return data.get(key, default)
+
+
+def get_overrided_query_dict(defaults, data):
+ """Returns a QueryDict with the values of 'defaults' overridden by the
+ values in 'data'.
+
+ :param defaults: The dictionary containing the default values.
+ :type defaults: dict
+ :param data: The data used to override the defaults.
+ :type data: :class:`django.http.QueryDict`
+ :return: The updated QueryDict.
+ :raises: :class:`django.http.QueryDict`
+ """
+ # Create a writable query dict.
+ new_data = QueryDict('').copy()
+ # Missing fields will be taken from the node's current values. This
+ # is to circumvent Django's ModelForm (form created from a model)
+ # default behaviour that requires all the fields to be defined.
+ new_data.update(defaults)
+ # We can't use update here because data is a QueryDict and 'update'
+ # does not replaces the old values with the new as one would expect.
+ for k, v in data.items():
+ new_data[k] = v
+ return new_data
+
+
+def extract_oauth_key_from_auth_header(auth_data):
+ """Extract the oauth key from auth data in HTTP header.
+
+ :param auth_data: {string} The HTTP Authorization header.
+
+ :return: The oauth key from the header, or None.
+ """
+ for entry in auth_data.split():
+ key_value = entry.split('=', 1)
+ if len(key_value) == 2:
+ key, value = key_value
+ if key == 'oauth_token':
+ return value.rstrip(',').strip('"')
+ return None
+
+
+def extract_oauth_key(request):
+ """Extract the oauth key from a request's headers.
+
+ Raises :class:`Unauthorized` if no key is found.
+ """
+ auth_header = request.META.get('HTTP_AUTHORIZATION')
+ if auth_header is None:
+ raise Unauthorized("No authorization header received.")
+ key = extract_oauth_key_from_auth_header(auth_header)
+ if key is None:
+ raise Unauthorized("Did not find request's oauth token.")
+ return key
+
+
+def get_oauth_token(request):
+ """Get the OAuth :class:`piston.models.Token` used for `request`.
+
+ Raises :class:`Unauthorized` if no key is found, or if the token is
+ unknown.
+ """
+ try:
+ return Token.objects.get(key=extract_oauth_key(request))
+ except Token.DoesNotExist:
+ raise Unauthorized("Unknown OAuth token.")
=== modified file 'src/maasserver/apidoc.py'
--- src/maasserver/apidoc.py 2012-11-02 09:48:02 +0000
+++ src/maasserver/apidoc.py 2012-11-22 17:37:22 +0000
@@ -25,6 +25,7 @@
RegexURLPattern,
RegexURLResolver,
)
+from maasserver.api_support import OperationsResource
from piston.authentication import NoAuthentication
from piston.doc import generate_doc
from piston.handler import BaseHandler
@@ -115,7 +116,6 @@
restful: Indicates if this is a CRUD/ReSTful action.
"""
- from maasserver.api import OperationsResource
getname = OperationsResource.crudmap.get
for signature, function in handler.exports.items():
http_method, operation = signature
=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py 2012-11-22 15:59:37 +0000
+++ src/maasserver/tests/test_api.py 2012-11-22 17:37:22 +0000
@@ -17,7 +17,6 @@
abstractproperty,
)
from base64 import b64encode
-from collections import namedtuple
from cStringIO import StringIO
from datetime import (
datetime,
@@ -51,12 +50,9 @@
describe,
DISPLAYED_NODEGROUP_FIELDS,
extract_constraints,
- extract_oauth_key,
- extract_oauth_key_from_auth_header,
- get_oauth_token,
- get_overrided_query_dict,
store_node_power_parameters,
)
+from maasserver.api_utils import get_overrided_query_dict
from maasserver.enum import (
ARCHITECTURE,
ARCHITECTURE_CHOICES,
@@ -68,10 +64,7 @@
NODEGROUP_STATUS,
NODEGROUPINTERFACE_MANAGEMENT,
)
-from maasserver.exceptions import (
- MAASAPIBadRequest,
- Unauthorized,
- )
+from maasserver.exceptions import MAASAPIBadRequest
from maasserver.fields import mac_error_msg
from maasserver.forms import DEFAULT_ZONE_NAME
from maasserver.models import (
@@ -170,54 +163,6 @@
class TestModuleHelpers(TestCase):
- def make_fake_request(self, auth_header):
- """Create a very simple fake request, with just an auth header."""
- FakeRequest = namedtuple('FakeRequest', ['META'])
- return FakeRequest(META={'HTTP_AUTHORIZATION': auth_header})
-
- def test_extract_oauth_key_from_auth_header_returns_key(self):
- token = factory.getRandomString(18)
- self.assertEqual(
- token,
- extract_oauth_key_from_auth_header(
- factory.make_oauth_header(oauth_token=token)))
-
- def test_extract_oauth_key_from_auth_header_returns_None_if_missing(self):
- self.assertIs(None, extract_oauth_key_from_auth_header(''))
-
- def test_extract_oauth_key_raises_Unauthorized_if_no_auth_header(self):
- self.assertRaises(
- Unauthorized,
- extract_oauth_key, self.make_fake_request(None))
-
- def test_extract_oauth_key_raises_Unauthorized_if_no_key(self):
- self.assertRaises(
- Unauthorized,
- extract_oauth_key, self.make_fake_request(''))
-
- def test_extract_oauth_key_returns_key(self):
- token = factory.getRandomString(18)
- self.assertEqual(
- token,
- extract_oauth_key(self.make_fake_request(
- factory.make_oauth_header(oauth_token=token))))
-
- def test_get_oauth_token_finds_token(self):
- user = factory.make_user()
- consumer, token = user.get_profile().create_authorisation_token()
- self.assertEqual(
- token,
- get_oauth_token(
- self.make_fake_request(
- factory.make_oauth_header(oauth_token=token.key))))
-
- def test_get_oauth_token_raises_Unauthorized_for_unknown_token(self):
- fake_token = factory.getRandomString(18)
- header = factory.make_oauth_header(oauth_token=fake_token)
- self.assertRaises(
- Unauthorized,
- get_oauth_token, self.make_fake_request(header))
-
def test_extract_constraints_ignores_unknown_parameters(self):
unknown_parameter = "%s=%s" % (
factory.getRandomString(),
=== added file 'src/maasserver/tests/test_api_utils.py'
--- src/maasserver/tests/test_api_utils.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/tests/test_api_utils.py 2012-11-22 17:37:22 +0000
@@ -0,0 +1,95 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for API helpers."""
+
+from __future__ import (
+ absolute_import,
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = []
+
+from collections import namedtuple
+
+from django.http import QueryDict
+from maasserver.api_utils import (
+ extract_oauth_key,
+ extract_oauth_key_from_auth_header,
+ get_oauth_token,
+ get_overrided_query_dict,
+ )
+from maasserver.exceptions import Unauthorized
+from maasserver.testing.factory import factory
+from maasserver.testing.testcase import TestCase
+
+
+class TestGetOverridedQueryDict(TestCase):
+
+ def test_returns_QueryDict(self):
+ defaults = {factory.getRandomString(): factory.getRandomString()}
+ results = get_overrided_query_dict(defaults, QueryDict(''))
+ expected_results = QueryDict('').copy()
+ expected_results.update(defaults)
+ self.assertEqual(expected_results, results)
+
+ def test_data_values_override_defaults(self):
+ key = factory.getRandomString()
+ defaults = {key: factory.getRandomString()}
+ data_value = factory.getRandomString()
+ data = {key: data_value}
+ results = get_overrided_query_dict(defaults, data)
+ self.assertEqual([data_value], results.getlist(key))
+
+
+class TestOAuthHelpers(TestCase):
+
+ def make_fake_request(self, auth_header):
+ """Create a very simple fake request, with just an auth header."""
+ FakeRequest = namedtuple('FakeRequest', ['META'])
+ return FakeRequest(META={'HTTP_AUTHORIZATION': auth_header})
+
+ def test_extract_oauth_key_from_auth_header_returns_key(self):
+ token = factory.getRandomString(18)
+ self.assertEqual(
+ token,
+ extract_oauth_key_from_auth_header(
+ factory.make_oauth_header(oauth_token=token)))
+
+ def test_extract_oauth_key_from_auth_header_returns_None_if_missing(self):
+ self.assertIs(None, extract_oauth_key_from_auth_header(''))
+
+ def test_extract_oauth_key_raises_Unauthorized_if_no_auth_header(self):
+ self.assertRaises(
+ Unauthorized,
+ extract_oauth_key, self.make_fake_request(None))
+
+ def test_extract_oauth_key_raises_Unauthorized_if_no_key(self):
+ self.assertRaises(
+ Unauthorized,
+ extract_oauth_key, self.make_fake_request(''))
+
+ def test_extract_oauth_key_returns_key(self):
+ token = factory.getRandomString(18)
+ self.assertEqual(
+ token,
+ extract_oauth_key(self.make_fake_request(
+ factory.make_oauth_header(oauth_token=token))))
+
+ def test_get_oauth_token_finds_token(self):
+ user = factory.make_user()
+ consumer, token = user.get_profile().create_authorisation_token()
+ self.assertEqual(
+ token,
+ get_oauth_token(
+ self.make_fake_request(
+ factory.make_oauth_header(oauth_token=token.key))))
+
+ def test_get_oauth_token_raises_Unauthorized_for_unknown_token(self):
+ fake_token = factory.getRandomString(18)
+ header = factory.make_oauth_header(oauth_token=fake_token)
+ self.assertRaises(
+ Unauthorized,
+ get_oauth_token, self.make_fake_request(header))
=== modified file 'src/maasserver/tests/test_apidoc.py'
--- src/maasserver/tests/test_apidoc.py 2012-11-19 14:16:43 +0000
+++ src/maasserver/tests/test_apidoc.py 2012-11-22 17:37:22 +0000
@@ -22,7 +22,7 @@
url,
)
from django.core.exceptions import ImproperlyConfigured
-from maasserver.api import (
+from maasserver.api_support import (
operation,
OperationsHandler,
OperationsResource,
=== modified file 'src/maasserver/urls_api.py'
--- src/maasserver/urls_api.py 2012-09-20 15:10:28 +0000
+++ src/maasserver/urls_api.py 2012-11-22 17:37:22 +0000
@@ -18,7 +18,6 @@
)
from maasserver.api import (
AccountHandler,
- AdminRestrictedResource,
api_doc,
BootImagesHandler,
describe,
@@ -32,12 +31,15 @@
NodeMacHandler,
NodeMacsHandler,
NodesHandler,
+ pxeconfig,
TagHandler,
TagsHandler,
- pxeconfig,
+ )
+from maasserver.api_auth import api_auth
+from maasserver.api_support import (
+ AdminRestrictedResource,
RestrictedResource,
)
-from maasserver.api_auth import api_auth
account_handler = RestrictedResource(AccountHandler, authentication=api_auth)
=== modified file 'src/metadataserver/api.py'
--- src/metadataserver/api.py 2012-11-14 14:36:27 +0000
+++ src/metadataserver/api.py 2012-11-22 17:37:22 +0000
@@ -25,12 +25,14 @@
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
-from maasserver.api import (
+from maasserver.api import store_node_power_parameters
+from maasserver.api_support import (
+ operation,
+ OperationsHandler,
+ )
+from maasserver.api_utils import (
extract_oauth_key,
get_mandatory_param,
- operation,
- OperationsHandler,
- store_node_power_parameters,
)
from maasserver.enum import (
NODE_STATUS,
=== modified file 'src/metadataserver/urls.py'
--- src/metadataserver/urls.py 2012-09-26 21:42:32 +0000
+++ src/metadataserver/urls.py 2012-11-22 17:37:22 +0000
@@ -18,8 +18,8 @@
patterns,
url,
)
-from maasserver.api import OperationsResource
from maasserver.api_auth import api_auth
+from maasserver.api_support import OperationsResource
from metadataserver.api import (
AnonMetaDataHandler,
EnlistMetaDataHandler,