Archived
Use Member as the Django User model, auth fixes
This commit is contained in:
1 parent
42e83727dd
commit
4e35307501
16 files changed
+117
-639
No files matched your search
+1
-1
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
+33
-10
@@ -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
|
||||
@@ -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
|
||||
+23
-20
@@ -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": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"}}]
|
||||
+1
-22
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
@@ -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)
|
||||
|
||||
|
||||
+18
-33
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Whitespace-only changes.
@@ -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))
|
||||
@@ -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)),
|
||||
)
|
||||
+2
-16
@@ -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)
|
||||
Reference in new issue
Block a user