From 4e35307501163c2ed3f63c05b2fe107fb4e14aaa Mon Sep 17 00:00:00 2001 From: Nikolai Kristiansen Date: Fri, 18 Oct 2013 02:03:07 +0200 Subject: [PATCH] Use Member as the Django User model, auth fixes --- .travis.yml | 2 +- dusken/admin.py | 2 - dusken/api/member.py | 10 +- dusken/authentication.py | 43 ++- dusken/authorization.py | 22 ++ dusken/fixtures/member.json | 43 +-- dusken/fixtures/users.json | 1 - dusken/models.py | 23 +- dusken/settings-sample.py | 6 +- dusken/tests/group.py | 4 +- dusken/tests/member.py | 51 ++- dusken/tests/membership.py | 14 +- dusken/tests/support/__init__.py | 0 dusken/tests/support/test.py | 516 ------------------------------- dusken/urls.py | 1 - utils/tests.py | 18 +- 16 files changed, 117 insertions(+), 639 deletions(-) create mode 100644 dusken/authorization.py delete mode 100644 dusken/fixtures/users.json delete mode 100644 dusken/tests/support/__init__.py delete mode 100644 dusken/tests/support/test.py diff --git a/.travis.yml b/.travis.yml index 5db529c..2406e53 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ install: before_script: - psql -c 'create database dusken_test;' -U postgres script: - python manage.py test --settings="dusken.settings-sample" + python manage.py test dusken --settings="dusken.settings-sample" notifications: irc: "ircnet.eversible.com#edb" diff --git a/dusken/admin.py b/dusken/admin.py index d1e4435..58e07da 100644 --- a/dusken/admin.py +++ b/dusken/admin.py @@ -1,5 +1,4 @@ from django.contrib import admin -from django.contrib.auth.models import User from dusken.models import * class MemberAdmin(admin.ModelAdmin): @@ -28,7 +27,6 @@ class MemberAdmin(admin.ModelAdmin): return self.readonly_fields admin.site.unregister(django.contrib.auth.models.Group) -admin.site.unregister(User) admin.site.register(Address) admin.site.register(Country) diff --git a/dusken/api/member.py b/dusken/api/member.py index 1242856..698e6db 100644 --- a/dusken/api/member.py +++ b/dusken/api/member.py @@ -3,12 +3,12 @@ from django.contrib.auth.models import User from django.conf.urls import url from django.shortcuts import get_object_or_404 from tastypie import fields -from dusken.authentication import ApiKeyAuthentication -from tastypie.authorization import DjangoAuthorization from tastypie.exceptions import ImmediateHttpResponse from tastypie.http import HttpForbidden, HttpNoContent, HttpResponse, HttpAccepted from tastypie.resources import ModelResource, ALL from dusken.models import * +from dusken.authorization import MyDjangoAuthorization +from dusken.authentication import MyApiKeyAuthentication from dusken.api.groupsbymember import GroupsByMemberResource @@ -23,8 +23,8 @@ class MemberResource(ModelResource): resource_name = 'member' list_allowed_methods = [ 'get', 'post' ] detail_allowed_methods = [ 'get', 'patch', 'delete' ] - authentication = ApiKeyAuthentication() - authorization = DjangoAuthorization() + authentication = MyApiKeyAuthentication() + authorization = MyDjangoAuthorization() excludes = [ 'date_joined', 'password', 'is_active', 'is_staff', 'is_superuser', 'last_login' ] filtering = { 'first_name' : [ 'exact' ], @@ -72,7 +72,7 @@ class MemberResource(ModelResource): """ Catches POST and PATCH requests and intercepts data. """ - update_existing_user = bundle.obj.user_ptr_id is not None + update_existing_user = bundle.obj.id is not None if update_existing_user: # If the user already exists, return an error when attempting to change the username. diff --git a/dusken/authentication.py b/dusken/authentication.py index df57e7c..7d18d6b 100644 --- a/dusken/authentication.py +++ b/dusken/authentication.py @@ -1,19 +1,42 @@ -from tastypie.authentication import ApiKeyAuthentication as tastypieApiKeyAuth +from tastypie.authentication import ApiKeyAuthentication +from tastypie.http import HttpUnauthorized -class ApiKeyAuthentication(tastypieApiKeyAuth): +class MyApiKeyAuthentication(ApiKeyAuthentication): """ - Override with our own ApiKey model. + Override method below to use our custom User model """ - def get_key(self, user, api_key): + + def is_authenticated(self, request, **kwargs): """ - Attempts to find the API key for the user. Uses ``ApiKey`` by default - but can be overridden. + Finds the user and checks their API key. + + Should return either ``True`` if allowed, ``False`` if not or an + ``HttpResponse`` if you need something custom. """ - from dusken.models import ApiKey + # Ninjafix to get our custom model working + from django.contrib.auth import get_user_model + User = get_user_model() + username_field = User.USERNAME_FIELD try: - ApiKey.objects.get(user=user, key=api_key) - except ApiKey.DoesNotExist: + username, api_key = self.extract_credentials(request) + except ValueError: return self._unauthorized() - return True + if not username or not api_key: + return self._unauthorized() + + try: + lookup_kwargs = {username_field: username} + user = User.objects.get(**lookup_kwargs) + except (User.DoesNotExist, User.MultipleObjectsReturned): + return self._unauthorized() + + if not self.check_active(user): + return False + + key_auth_check = self.get_key(user, api_key) + if key_auth_check and not isinstance(key_auth_check, HttpUnauthorized): + request.user = user + + return key_auth_check diff --git a/dusken/authorization.py b/dusken/authorization.py new file mode 100644 index 0000000..7192caa --- /dev/null +++ b/dusken/authorization.py @@ -0,0 +1,22 @@ +from tastypie.authorization import DjangoAuthorization +from tastypie.exceptions import Unauthorized + +class MyDjangoAuthorization(DjangoAuthorization): + + def update_detail(self, object_list, bundle): + klass = self.base_checks(bundle.request, bundle.obj.__class__) + + if klass is False: + raise Unauthorized("You are not allowed to access that resource.") + + # Skip permission check if the request is made by the object owner + # Note: Standard DjangoAuthorization does not have this check + if bundle.obj == bundle.request.user: + return True + + permission = '%s.change_%s' % (klass._meta.app_label, klass._meta.module_name) + + if not bundle.request.user.has_perm(permission): + raise Unauthorized("You are not allowed to access that resource.") + + return True diff --git a/dusken/fixtures/member.json b/dusken/fixtures/member.json index e665b9d..faac92c 100644 --- a/dusken/fixtures/member.json +++ b/dusken/fixtures/member.json @@ -1,26 +1,11 @@ [ { - "pk": 2, - "model": "auth.user", - "fields": { - "username": "", - "first_name": "", - "last_name": "", - "is_active": true, - "is_superuser": false, - "is_staff": false, - "last_login": "2013-10-17T10:59:18.493Z", - "groups": [], - "user_permissions": [], - "password": "", - "email": "", - "date_joined": "2013-10-17T10:59:18.493Z" - } - }, - { - "pk": 2, + "pk": 1, "model": "dusken.member", "fields": { + "username": "robert", + "first_name": "Robert", + "last_name": "Kolner", "phone_number": null, "updated": "2013-09-11T18:45:06.354Z", "groups": [], @@ -28,7 +13,25 @@ "place_of_study": [], "date_of_birth": null, "legacy_id": null, - "address": null, + "address": 1, + "user_permissions": [] + } + }, + { + "pk": 2, + "model": "dusken.member", + "fields": { + "username": "testesen", + "first_name": "Test", + "last_name": "Testesen", + "phone_number": null, + "updated": "2013-09-11T18:45:06.354Z", + "groups": [], + "created": "2013-09-11T18:45:06.354Z", + "place_of_study": [], + "date_of_birth": null, + "legacy_id": null, + "address": 2, "user_permissions": [] } } diff --git a/dusken/fixtures/users.json b/dusken/fixtures/users.json deleted file mode 100644 index a98ee86..0000000 --- a/dusken/fixtures/users.json +++ /dev/null @@ -1 +0,0 @@ -[{"pk": 1, "model": "auth.user", "fields": {"username": "mark", "first_name": "", "last_name": "", "is_active": true, "is_superuser": true, "is_staff": true, "last_login": "2013-09-11T16:46:09.869Z", "groups": [], "user_permissions": [], "password": "pbkdf2_sha256$10000$8NUVcwCrxbai$5gri/57kESD/t8ZhpZxueVGQcPCW8l3PSCjSuxDp4AI=", "email": "mark@thedutchies.com", "date_joined": "2013-08-21T19:53:07.653Z"}}, {"pk": 2, "model": "auth.user", "fields": {"username": "markpo", "first_name": "", "last_name": "", "is_active": true, "is_superuser": false, "is_staff": false, "last_login": "2013-09-11T18:45:06.339Z", "groups": [], "user_permissions": [], "password": "", "email": "", "date_joined": "2013-09-11T18:44:52Z"}}] \ No newline at end of file diff --git a/dusken/models.py b/dusken/models.py index 1a87ab9..a5403c9 100644 --- a/dusken/models.py +++ b/dusken/models.py @@ -1,16 +1,6 @@ import django from django.db import models -import tastypie -from tastypie.models import ApiKey as tastypieApiKey -import uuid -import hmac - -# TODO override ApiKeyAuthentication -try: - from hashlib import sha1 -except ImportError: - import sha - sha1 = sha.sha +from tastypie.models import create_api_key class BaseModel(models.Model): created = models.DateTimeField(auto_now_add=True) @@ -18,18 +8,7 @@ class BaseModel(models.Model): class Meta: abstract = True - -class ApiKey(tastypieApiKey): - user = models.OneToOneField('dusken.Member', related_name='api_key') - -def create_api_key(sender, **kwargs): - """ - A signal for hooking up automatic ``ApiKey`` creation. - """ - if kwargs.get('created') is True: - ApiKey.objects.create(user=kwargs.get('instance')) - class Member(django.contrib.auth.models.AbstractUser): def __unicode__(self): if len(self.first_name) + len(self.last_name) > 0: diff --git a/dusken/settings-sample.py b/dusken/settings-sample.py index 08dcbeb..45e821a 100644 --- a/dusken/settings-sample.py +++ b/dusken/settings-sample.py @@ -122,8 +122,6 @@ INSTALLED_APPS = ( 'south' ) -# Profile of the User class used for authentication. -AUTH_PROFILE_MODULE = 'dusken.Member' # A sample logging configuration. The only tangible logging # performed by this configuration is to send an email to @@ -155,4 +153,6 @@ LOGGING = { } TASTYPIE_DEFAULT_FORMATS = ['json'] -TASTYPIE_ABSTRACT_APIKEY = True + +# Our own User class used for authentication. +AUTH_USER_MODEL = 'dusken.Member' diff --git a/dusken/tests/group.py b/dusken/tests/group.py index 65f36a4..e8fcb57 100644 --- a/dusken/tests/group.py +++ b/dusken/tests/group.py @@ -2,7 +2,7 @@ import logging import utils.tests from dusken.models import Group -from support.test import ResourceTestCase +from tastypie.test import ResourceTestCase class GroupTest(ResourceTestCase): @@ -19,4 +19,4 @@ class GroupTest(ResourceTestCase): def test_delete_not_allowed(self): resp = self.api_client.delete(self.groups_url) self.assertHttpMethodNotAllowed(resp) - \ No newline at end of file + diff --git a/dusken/tests/member.py b/dusken/tests/member.py index 76b495f..ed2f8f9 100644 --- a/dusken/tests/member.py +++ b/dusken/tests/member.py @@ -1,10 +1,14 @@ import logging +from tastypie.models import ApiKey -from dusken.models import Address, Country, Member, ApiKey -from support.test import ResourceTestCase +from dusken.models import Address, Country, Member +from tastypie.test import ResourceTestCase from utils.tests import test_fixtures_member class MemberTestBase(ResourceTestCase): + + # Ref: https://docs.djangoproject.com/en/dev/topics/testing/overview/#django.test.TransactionTestCase.fixtures + fixtures = test_fixtures_member() def setUp(self): super(MemberTestBase, self).setUp() @@ -12,17 +16,9 @@ class MemberTestBase(ResourceTestCase): # TODO use fixtures for this # Get the preloaded member, which will be used for # comparison with fetched object. - self.member = Member(username='robert', password='pass', email='robert.kolner@gmail.com', phone_number=90567268) - self.member.is_staff = True - self.member.is_superuser = True - self.member.save() - - self.member2 = Member(username='test', password='pass', email='test@test.com') - self.member2.save() - - # create credentials for both members: + self.member = Member.objects.get(pk=2) + ## create credentials self.creds = self.create_apikey(username=self.member.username, api_key=ApiKey.objects.get(user=self.member).key) - self.creds2 = self.create_apikey(username=self.member2.username, api_key=ApiKey.objects.get(user=self.member2).key) # URIs to the member api self.all_members_url = '/api/v1/member/' @@ -65,7 +61,7 @@ class MemberTest(MemberTestBase): member = self.deserialize(resp) self.assertValidMemberData(member) - def test_get_member_unauthorized(self): + def test_get_all_members_unauthorized(self): self.assertHttpUnauthorized(self.api_client.get(self.all_members_url, format='json')) def test_get_all_members(self): @@ -192,18 +188,9 @@ class MemberTest(MemberTestBase): """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" class MemberAddressTest(MemberTestBase): - # Ref: https://docs.djangoproject.com/en/dev/topics/testing/overview/#django.test.TransactionTestCase.fixtures - fixtures = test_fixtures_member() - def setUp(self): super(MemberAddressTest, self).setUp() - self.user_id = self.member.id - - member = Member.objects.get(id=self.user_id) - member.address = Address.objects.all()[0] - member.save() - def test_member_address_update_street(self): data = { "address": { @@ -212,13 +199,13 @@ class MemberAddressTest(MemberTestBase): } response = self.api_client.patch( - self.member_url.format(self.user_id), + self.member_url.format(self.member.id), format='json', data=data, authentication=self.creds) self.assertHttpAccepted(response) - self.assertEquals('somestreet 2', Member.objects.get(id=self.user_id).address.street_address) + self.assertEquals('somestreet 2', Member.objects.get(id=self.member.id).address.street_address) def test_member_address_update_city(self): data = { @@ -228,15 +215,13 @@ class MemberAddressTest(MemberTestBase): } response = self.api_client.patch( - self.member_url.format(self.user_id), + self.member_url.format(self.member.id), format='json', data=data, authentication=self.creds) - import pdb - pdb.set_trace() self.assertHttpAccepted(response) - self.assertEquals('Amsterdam', Member.objects.get(id=self.user_id).address.city) + self.assertEquals('Amsterdam', Member.objects.get(id=self.member.id).address.city) def test_member_address_update_postcode(self): data = { @@ -246,13 +231,13 @@ class MemberAddressTest(MemberTestBase): } response = self.api_client.patch( - self.member_url.format(self.user_id), + self.member_url.format(self.member.id), format='json', data=data, authentication=self.creds) self.assertHttpAccepted(response) - self.assertEquals('0977RT', Member.objects.get(id=self.user_id).address.postal_code) + self.assertEquals('0977RT', Member.objects.get(id=self.member.id).address.postal_code) def test_member_address_update_country(self): data = { @@ -262,7 +247,7 @@ class MemberAddressTest(MemberTestBase): } response = self.api_client.patch( - self.member_url.format(self.user_id), + self.member_url.format(self.member.id), format='json', data=data, authentication=self.creds) @@ -270,7 +255,7 @@ class MemberAddressTest(MemberTestBase): self.assertHttpAccepted(response) self.assertEquals( Country.objects.get(name="Netherlands"), - Member.objects.get(id=self.user_id).address.country) + Member.objects.get(id=self.member.id).address.country) def test_member_adress_update_unknown_country(self): data = { @@ -280,7 +265,7 @@ class MemberAddressTest(MemberTestBase): } response = self.api_client.patch( - self.member_url.format(self.user_id), + self.member_url.format(self.member.id), format='json', data=data, authentication=self.creds) diff --git a/dusken/tests/membership.py b/dusken/tests/membership.py index 3bc62d7..1d18cec 100644 --- a/dusken/tests/membership.py +++ b/dusken/tests/membership.py @@ -1,9 +1,10 @@ import logging import django +from tastypie.models import ApiKey -from dusken.models import Address, Country, Member, ApiKey -from support.test import ResourceTestCase -from utils.tests import load_test_fixtures_for_membership, test_fixtures_membership +from dusken.models import Address, Country, Member +from tastypie.test import ResourceTestCase +from utils.tests import test_fixtures_membership class MembershipTest(ResourceTestCase): @@ -12,13 +13,12 @@ class MembershipTest(ResourceTestCase): fixtures = test_fixtures_membership() - def setUp(self): - #load_test_fixtures_for_membership() # FIXME use fixtures above + self.member = Member.objects.get(pk=2) self.creds = self.create_apikey( - username=Member.objects.get(id=2).username, - api_key=ApiKey.objects.get(user=Member.objects.get(id=2)).key) + username=self.member.username, + api_key=ApiKey.objects.get(user=self.member).key) super(MembershipTest, self).setUp() diff --git a/dusken/tests/support/__init__.py b/dusken/tests/support/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/dusken/tests/support/test.py b/dusken/tests/support/test.py deleted file mode 100644 index f050e3c..0000000 --- a/dusken/tests/support/test.py +++ /dev/null @@ -1,516 +0,0 @@ -import time -from urlparse import urlparse -from django.conf import settings -from django.test import TestCase -from django.test.client import FakePayload, Client -from tastypie.serializers import Serializer - -# This file was taken from: -# https://github.com/toastdriven/django-tastypie/blob/master/tastypie/test.py -# ...to act as a replacement before official 0.9.12 comes out. Remove this -# file once it does. - -class TestApiClient(object): - def __init__(self, serializer=None): - """ - Sets up a fresh ``TestApiClient`` instance. - - If you are employing a custom serializer, you can pass the class to the - ``serializer=`` kwarg. - """ - self.client = Client() - self.serializer = serializer - - if not self.serializer: - self.serializer = Serializer() - - def get_content_type(self, short_format): - """ - Given a short name (such as ``json`` or ``xml``), returns the full content-type - for it (``application/json`` or ``application/xml`` in this case). - """ - return self.serializer.content_types.get(short_format, 'json') - - def get(self, uri, format='json', data=None, authentication=None, **kwargs): - """ - Performs a simulated ``GET`` request to the provided URI. - - Optionally accepts a ``data`` kwarg, which in the case of ``GET``, lets you - send along ``GET`` parameters. This is useful when testing filtering or other - things that read off the ``GET`` params. Example:: - - from tastypie.test import TestApiClient - client = TestApiClient() - - response = client.get('/api/v1/entry/1/', data={'format': 'json', 'title__startswith': 'a', 'limit': 20, 'offset': 60}) - - Optionally accepts an ``authentication`` kwarg, which should be an HTTP header - with the correct authentication data already setup. - - All other ``**kwargs`` passed in get passed through to the Django - ``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client - for details. - """ - content_type = self.get_content_type(format) - kwargs['HTTP_ACCEPT'] = content_type - - # GET & DELETE are the only times we don't serialize the data. - if data is not None: - kwargs['data'] = data - - if authentication is not None: - kwargs['HTTP_AUTHORIZATION'] = authentication - - return self.client.get(uri, **kwargs) - - def post(self, uri, format='json', data=None, authentication=None, **kwargs): - """ - Performs a simulated ``POST`` request to the provided URI. - - Optionally accepts a ``data`` kwarg. **Unlike** ``GET``, in ``POST`` the - ``data`` gets serialized & sent as the body instead of becoming part of the URI. - Example:: - - from tastypie.test import TestApiClient - client = TestApiClient() - - response = client.post('/api/v1/entry/', data={ - 'created': '2012-05-01T20:02:36', - 'slug': 'another-post', - 'title': 'Another Post', - 'user': '/api/v1/user/1/', - }) - - Optionally accepts an ``authentication`` kwarg, which should be an HTTP header - with the correct authentication data already setup. - - All other ``**kwargs`` passed in get passed through to the Django - ``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client - for details. - """ - content_type = self.get_content_type(format) - kwargs['content_type'] = content_type - - if data is not None: - kwargs['data'] = self.serializer.serialize(data, format=content_type) - - if authentication is not None: - kwargs['HTTP_AUTHORIZATION'] = authentication - - return self.client.post(uri, **kwargs) - - def put(self, uri, format='json', data=None, authentication=None, **kwargs): - """ - Performs a simulated ``PUT`` request to the provided URI. - - Optionally accepts a ``data`` kwarg. **Unlike** ``GET``, in ``PUT`` the - ``data`` gets serialized & sent as the body instead of becoming part of the URI. - Example:: - - from tastypie.test import TestApiClient - client = TestApiClient() - - response = client.put('/api/v1/entry/1/', data={ - 'created': '2012-05-01T20:02:36', - 'slug': 'another-post', - 'title': 'Another Post', - 'user': '/api/v1/user/1/', - }) - - Optionally accepts an ``authentication`` kwarg, which should be an HTTP header - with the correct authentication data already setup. - - All other ``**kwargs`` passed in get passed through to the Django - ``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client - for details. - """ - content_type = self.get_content_type(format) - kwargs['content_type'] = content_type - - if data is not None: - kwargs['data'] = self.serializer.serialize(data, format=content_type) - - if authentication is not None: - kwargs['HTTP_AUTHORIZATION'] = authentication - - return self.client.put(uri, **kwargs) - - def patch(self, uri, format='json', data=None, authentication=None, **kwargs): - """ - Performs a simulated ``PATCH`` request to the provided URI. - - Optionally accepts a ``data`` kwarg. **Unlike** ``GET``, in ``PATCH`` the - ``data`` gets serialized & sent as the body instead of becoming part of the URI. - Example:: - - from tastypie.test import TestApiClient - client = TestApiClient() - - response = client.patch('/api/v1/entry/1/', data={ - 'created': '2012-05-01T20:02:36', - 'slug': 'another-post', - 'title': 'Another Post', - 'user': '/api/v1/user/1/', - }) - - Optionally accepts an ``authentication`` kwarg, which should be an HTTP header - with the correct authentication data already setup. - - All other ``**kwargs`` passed in get passed through to the Django - ``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client - for details. - """ - content_type = self.get_content_type(format) - kwargs['content_type'] = content_type - - if data is not None: - kwargs['data'] = self.serializer.serialize(data, format=content_type) - - if authentication is not None: - kwargs['HTTP_AUTHORIZATION'] = authentication - - # This hurts because Django doesn't support PATCH natively. - parsed = urlparse(uri) - r = { - 'CONTENT_LENGTH': len(kwargs['data']), - 'CONTENT_TYPE': content_type, - 'PATH_INFO': self.client._get_path(parsed), - 'QUERY_STRING': parsed[4], - 'REQUEST_METHOD': 'PATCH', - 'wsgi.input': FakePayload(kwargs['data']), - } - r.update(kwargs) - return self.client.request(**r) - - def delete(self, uri, format='json', data=None, authentication=None, **kwargs): - """ - Performs a simulated ``DELETE`` request to the provided URI. - - Optionally accepts a ``data`` kwarg, which in the case of ``DELETE``, lets you - send along ``DELETE`` parameters. This is useful when testing filtering or other - things that read off the ``DELETE`` params. Example:: - - from tastypie.test import TestApiClient - client = TestApiClient() - - response = client.delete('/api/v1/entry/1/', data={'format': 'json'}) - - Optionally accepts an ``authentication`` kwarg, which should be an HTTP header - with the correct authentication data already setup. - - All other ``**kwargs`` passed in get passed through to the Django - ``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client - for details. - """ - content_type = self.get_content_type(format) - kwargs['content_type'] = content_type - - # GET & DELETE are the only times we don't serialize the data. - if data is not None: - kwargs['data'] = data - - if authentication is not None: - kwargs['HTTP_AUTHORIZATION'] = authentication - - return self.client.delete(uri, **kwargs) - - -class ResourceTestCase(TestCase): - """ - A useful base class for the start of testing Tastypie APIs. - """ - def setUp(self): - super(ResourceTestCase, self).setUp() - self.serializer = Serializer() - self.api_client = TestApiClient() - - def get_credentials(self): - """ - A convenience method for the user as a way to shorten up the - often repetitious calls to create the same authentication. - - Raises ``NotImplementedError`` by default. - - Usage:: - - class MyResourceTestCase(ResourceTestCase): - def get_credentials(self): - return self.create_basic('daniel', 'pass') - - # Then the usual tests... - - """ - raise NotImplementedError("You must return the class for your Resource to test.") - - def create_basic(self, username, password): - """ - Creates & returns the HTTP ``Authorization`` header for use with BASIC - Auth. - """ - import base64 - return 'Basic %s' % base64.b64encode(':'.join([username, password])) - - def create_apikey(self, username, api_key): - """ - Creates & returns the HTTP ``Authorization`` header for use with - ``ApiKeyAuthentication``. - """ - return 'ApiKey %s:%s' % (username, api_key) - - def create_digest(self, username, api_key, method, uri): - """ - Creates & returns the HTTP ``Authorization`` header for use with Digest - Auth. - """ - from tastypie.authentication import hmac, sha1, uuid, python_digest - - new_uuid = uuid.uuid4() - opaque = hmac.new(str(new_uuid), digestmod=sha1).hexdigest() - return python_digest.build_authorization_request( - username, - method.upper(), - uri, - 1, # nonce_count - digest_challenge=python_digest.build_digest_challenge(time.time(), getattr(settings, 'SECRET_KEY', ''), 'django-tastypie', opaque, False), - password=api_key - ) - - def create_oauth(self, user): - """ - Creates & returns the HTTP ``Authorization`` header for use with Oauth. - """ - from oauth_provider.models import Consumer, Token, Resource - - # Necessary setup for ``oauth_provider``. - resource, _ = Resource.objects.get_or_create(url='test', defaults={ - 'name': 'Test Resource' - }) - consumer, _ = Consumer.objects.get_or_create(key='123', defaults={ - 'name': 'Test', - 'description': 'Testing...' - }) - token, _ = Token.objects.get_or_create(key='foo', token_type=Token.ACCESS, defaults={ - 'consumer': consumer, - 'resource': resource, - 'secret': '', - 'user': user, - }) - - # Then generate the header. - oauth_data = { - 'oauth_consumer_key': '123', - 'oauth_nonce': 'abc', - 'oauth_signature': '&', - 'oauth_signature_method': 'PLAINTEXT', - 'oauth_timestamp': str(int(time.time())), - 'oauth_token': 'foo', - } - return 'OAuth %s' % ','.join([key+'='+value for key, value in oauth_data.items()]) - - def assertHttpOK(self, resp): - """ - Ensures the response is returning a HTTP 200. - """ - return self.assertEqual(resp.status_code, 200) - - def assertHttpCreated(self, resp): - """ - Ensures the response is returning a HTTP 201. - """ - return self.assertEqual(resp.status_code, 201) - - def assertHttpAccepted(self, resp): - """ - Ensures the response is returning either a HTTP 202 or a HTTP 204. - """ - return self.assertIn(resp.status_code, [202, 204]) - - def assertHttpMultipleChoices(self, resp): - """ - Ensures the response is returning a HTTP 300. - """ - return self.assertEqual(resp.status_code, 300) - - def assertHttpSeeOther(self, resp): - """ - Ensures the response is returning a HTTP 303. - """ - return self.assertEqual(resp.status_code, 303) - - def assertHttpNotModified(self, resp): - """ - Ensures the response is returning a HTTP 304. - """ - return self.assertEqual(resp.status_code, 304) - - def assertHttpBadRequest(self, resp): - """ - Ensures the response is returning a HTTP 400. - """ - return self.assertEqual(resp.status_code, 400) - - def assertHttpUnauthorized(self, resp): - """ - Ensures the response is returning a HTTP 401. - """ - return self.assertEqual(resp.status_code, 401) - - def assertHttpForbidden(self, resp): - """ - Ensures the response is returning a HTTP 403. - """ - return self.assertEqual(resp.status_code, 403) - - def assertHttpNotFound(self, resp): - """ - Ensures the response is returning a HTTP 404. - """ - return self.assertEqual(resp.status_code, 404) - - def assertHttpMethodNotAllowed(self, resp): - """ - Ensures the response is returning a HTTP 405. - """ - return self.assertEqual(resp.status_code, 405) - - def assertHttpConflict(self, resp): - """ - Ensures the response is returning a HTTP 409. - """ - return self.assertEqual(resp.status_code, 409) - - def assertHttpGone(self, resp): - """ - Ensures the response is returning a HTTP 410. - """ - return self.assertEqual(resp.status_code, 410) - - def assertHttpTooManyRequests(self, resp): - """ - Ensures the response is returning a HTTP 429. - """ - return self.assertEqual(resp.status_code, 429) - - def assertHttpApplicationError(self, resp): - """ - Ensures the response is returning a HTTP 500. - """ - return self.assertEqual(resp.status_code, 500) - - def assertHttpNotImplemented(self, resp): - """ - Ensures the response is returning a HTTP 501. - """ - return self.assertEqual(resp.status_code, 501) - - def assertValidJSON(self, data): - """ - Given the provided ``data`` as a string, ensures that it is valid JSON & - can be loaded properly. - """ - # Just try the load. If it throws an exception, the test case will fail. - self.serializer.from_json(data) - - def assertValidXML(self, data): - """ - Given the provided ``data`` as a string, ensures that it is valid XML & - can be loaded properly. - """ - # Just try the load. If it throws an exception, the test case will fail. - self.serializer.from_xml(data) - - def assertValidYAML(self, data): - """ - Given the provided ``data`` as a string, ensures that it is valid YAML & - can be loaded properly. - """ - # Just try the load. If it throws an exception, the test case will fail. - self.serializer.from_yaml(data) - - def assertValidPlist(self, data): - """ - Given the provided ``data`` as a string, ensures that it is valid - binary plist & can be loaded properly. - """ - # Just try the load. If it throws an exception, the test case will fail. - self.serializer.from_plist(data) - - def assertValidJSONResponse(self, resp): - """ - Given a ``HttpResponse`` coming back from using the ``client``, assert that - you get back: - - * An HTTP 200 - * The correct content-type (``application/json``) - * The content is valid JSON - """ - self.assertHttpOK(resp) - self.assertTrue(resp['Content-Type'].startswith('application/json')) - self.assertValidJSON(resp.content) - - def assertValidXMLResponse(self, resp): - """ - Given a ``HttpResponse`` coming back from using the ``client``, assert that - you get back: - - * An HTTP 200 - * The correct content-type (``application/xml``) - * The content is valid XML - """ - self.assertHttpOK(resp) - self.assertTrue(resp['Content-Type'].startswith('application/xml')) - self.assertValidXML(resp.content) - - def assertValidYAMLResponse(self, resp): - """ - Given a ``HttpResponse`` coming back from using the ``client``, assert that - you get back: - - * An HTTP 200 - * The correct content-type (``text/yaml``) - * The content is valid YAML - """ - self.assertHttpOK(resp) - self.assertTrue(resp['Content-Type'].startswith('text/yaml')) - self.assertValidYAML(resp.content) - - def assertValidPlistResponse(self, resp): - """ - Given a ``HttpResponse`` coming back from using the ``client``, assert that - you get back: - - * An HTTP 200 - * The correct content-type (``application/x-plist``) - * The content is valid binary plist data - """ - self.assertHttpOK(resp) - self.assertTrue(resp['Content-Type'].startswith('application/x-plist')) - self.assertValidPlist(resp.content) - - def deserialize(self, resp): - """ - Given a ``HttpResponse`` coming back from using the ``client``, this method - checks the ``Content-Type`` header & attempts to deserialize the data based on - that. - - It returns a Python datastructure (typically a ``dict``) of the serialized data. - """ - return self.serializer.deserialize(resp.content, format=resp['Content-Type']) - - def serialize(self, data, format='application/json'): - """ - Given a Python datastructure (typically a ``dict``) & a desired content-type, - this method will return a serialized string of that data. - """ - return self.serializer.serialize(data, format=format) - - def assertKeys(self, data, expected): - """ - This method ensures that the keys of the ``data`` match up to the keys of - ``expected``. - - It covers the (extremely) common case where you want to make sure the keys of - a response match up to what is expected. This is typically less fragile than - testing the full structure, which can be prone to data changes. - """ - self.assertEqual(sorted(data.keys()), sorted(expected)) diff --git a/dusken/urls.py b/dusken/urls.py index 3d92cac..37d1bd2 100644 --- a/dusken/urls.py +++ b/dusken/urls.py @@ -16,7 +16,6 @@ v1_api.register(GroupResource()) v1_api.register(InstitutionResource()) urlpatterns = patterns('', - url(r'^admin/doc/', include('django.contrib.admindocs.urls')), url(r'^admin/', include(admin.site.urls)), url(r'^api/', include(v1_api.urls)), ) diff --git a/utils/tests.py b/utils/tests.py index 1daf52b..bf0d404 100644 --- a/utils/tests.py +++ b/utils/tests.py @@ -29,31 +29,17 @@ def test_fixtures(): os.path.join('dusken', 'fixtures', 'institution.json'), ] -def load_test_fixtures(): - ''' Explicitly load all defined fixtures. This can be useful when - you only want to load the fixtures for some testcases, but not - for all tests in a test class.''' - for f in test_fixtures(): - call_command('loaddata', f, verbosity=0) - def test_fixtures_member(): ''' Returns a list with the locations of all defined fixtures. ''' - return [os.path.join('dusken', 'fixtures', 'country.json'), + return [os.path.join('dusken', 'fixtures', 'member.json'), + os.path.join('dusken', 'fixtures', 'country.json'), os.path.join('dusken', 'fixtures', 'address.json'), os.path.join('dusken', 'fixtures', 'institution.json'), ] def test_fixtures_membership(): return test_fixtures() + [ - os.path.join('dusken', 'fixtures', 'users.json'), - os.path.join('dusken', 'fixtures', 'apikey.json'), os.path.join('dusken', 'fixtures', 'member.json'), os.path.join('dusken', 'fixtures', 'membershiptype.json'), os.path.join('dusken', 'fixtures', 'membership.json'), ] - -def load_test_fixtures_for_membership(): - django.db.models.signals.post_save.disconnect(create_api_key, sender=Member) - print test_fixtures_membership() - for f in test_fixtures_membership(): - call_command('loaddata', f, verbosity=0)