diff --git a/dusken/main/fixtures/members.json b/dusken/main/fixtures/members.json new file mode 100644 index 0000000..537945d --- /dev/null +++ b/dusken/main/fixtures/members.json @@ -0,0 +1,15 @@ +[ + { + "pk": 1, + "model": "main.member", + "fields": { + "username": "robertko", + "address_id": null, + "surname": "", + "dateofbirth": null, + "phonenumber": null, + "givenname": "", + "email": "robert.kolner@studentersamfundet.no" + } + } +] diff --git a/dusken/main/tests.py b/dusken/main/tests.py deleted file mode 100644 index 501deb7..0000000 --- a/dusken/main/tests.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". - -Replace this with more appropriate tests for your application. -""" - -from django.test import TestCase - - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) diff --git a/dusken/main/tests/MemberTest.py b/dusken/main/tests/MemberTest.py new file mode 100644 index 0000000..2c2f3d2 --- /dev/null +++ b/dusken/main/tests/MemberTest.py @@ -0,0 +1,33 @@ +import logging + +from support.test import ResourceTestCase +from main.models import Member + +class MemberTest(ResourceTestCase): + fixtures = [ 'members.json' ] + + def setUp(self): + super(MemberTest, self).setUp() + + # Get the preloaded member, which will be used for + # comparison with fetched object. + self.member = Member.objects.get(username='robertko') + + # URI to get the existing member. We'll probably + # need it at one point or another. + self.member_url = '/api/v1/member/{0}/'.format(self.member.pk) + + def test_get_member(self): + """ + Tests that we can get a premade user correctly. + """ + resp = self.api_client.get(self.member_url, format='json') + self.assertValidJSONResponse(resp) + + # Check if the returned data is correct: + member = self.deserialize(resp) + + self.assertKeys(member, [ u'member_id', u'email', u'username', u'phonenumber', u'givenname', u'dateofbirth', u'surname', u'resource_uri' ]) + self.assertEqual(member['username'], 'robertko') + self.assertEqual(member['email'], 'robert.kolner@studentersamfundet.no') + diff --git a/dusken/main/tests/__init__.py b/dusken/main/tests/__init__.py new file mode 100644 index 0000000..ea052a6 --- /dev/null +++ b/dusken/main/tests/__init__.py @@ -0,0 +1 @@ +from MemberTest import * diff --git a/dusken/main/tests/support/__init__.py b/dusken/main/tests/support/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dusken/main/tests/support/test.py b/dusken/main/tests/support/test.py new file mode 100644 index 0000000..e07fd5f --- /dev/null +++ b/dusken/main/tests/support/test.py @@ -0,0 +1,516 @@ +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.assertTrue(resp.status_code in [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))