← Back to team overview

launchpad-reviewers team mailing list archive

[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,