import unittest try: from http.client import responses except ImportError: from httplib import responses try: from django.conf import settings except ImportError: settings = None DjangoResource = object else: from django.http import Http404 from django.core.exceptions import ObjectDoesNotExist # Ugh. Settings for Django. settings.configure(DEBUG=True) from restless.dj import DjangoResource from restless.exceptions import Unauthorized from restless.preparers import FieldsPreparer from restless.resources import skip_prepare from restless.utils import json from .fakes import FakeHttpRequest, FakeModel class DjTestResource(DjangoResource): preparer = FieldsPreparer(fields={ 'id': 'id', 'title': 'title', 'author': 'username', 'body': 'content' }) fake_db = [] def __init__(self, *args, **kwargs): super(DjTestResource, self).__init__(*args, **kwargs) self.http_methods.update({ 'schema': { 'GET': 'schema', } }) def fake_init(self): # Just for testing. self.__class__.fake_db = [ FakeModel( id='dead-beef', title='First post', username='daniel', content='Hello world!'), FakeModel( id='de-faced', title='Another', username='daniel', content='Stuff here.'), FakeModel( id='bad-f00d', title='Last', username='daniel', content="G'bye!"), ] def is_authenticated(self): if self.request_method() == 'DELETE' and self.endpoint == 'list': return False return True def list(self): return self.fake_db def detail(self, pk): for item in self.fake_db: if item.id == pk: return item # If it wasn't found in our fake DB, raise a Django-esque exception. raise ObjectDoesNotExist("Model with pk {} not found.".format(pk)) def create(self): self.fake_db.append(FakeModel( **self.data )) def update(self, pk): for item in self.fake_db: if item.id == pk: for k, v in self.data: setattr(item, k, v) return def create_detail(self): raise ValueError({ 'code': 'random-crazy', 'message': 'This is a random & crazy exception.', }) def delete(self, pk): for i, item in enumerate(self.fake_db): if item.id == pk: del self.fake_db[i] return @skip_prepare def schema(self): # A WILD SCHEMA VIEW APPEARS! return { 'fields': { 'id': { 'type': 'integer', 'required': True, 'help_text': 'The unique id for the post', }, 'title': { 'type': 'string', 'required': True, 'help_text': "The post's title", }, 'author': { 'type': 'string', 'required': True, 'help_text': 'The username of the author of the post', }, 'body': { 'type': 'string', 'required': False, 'default': '', 'help_text': 'The content of the post', } }, 'format': 'application/json', 'allowed_list_http_methods': ['GET', 'POST'], 'allowed_detail_http_methods': ['GET', 'PUT', 'DELETE'], } class DjTestResourcePaginated(DjTestResource): paginate = True class DjTestResourcePaginatedOnePerPage(DjTestResourcePaginated): page_size = 1 class DjTestResourceHttp404Handling(DjTestResource): def detail(self, pk): for item in self.fake_db: if item.id == pk: return item # If it wasn't found in our fake DB, raise a Django-esque exception. raise Http404("Model with pk {} not found.".format(pk)) @unittest.skipIf(not settings, "Django is not available") class DjangoResourceTestCase(unittest.TestCase): def setUp(self): super(DjangoResourceTestCase, self).setUp() self.res = DjTestResource() # Just for the fake data. self.res.fake_init() def test_as_list(self): list_endpoint = DjTestResource.as_list() req = FakeHttpRequest('GET') resp = list_endpoint(req) self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 200) self.assertEqual(json.loads(resp.content.decode('utf-8')), { 'objects': [ { 'author': 'daniel', 'body': 'Hello world!', 'id': 'dead-beef', 'title': 'First post' }, { 'author': 'daniel', 'body': 'Stuff here.', 'id': 'de-faced', 'title': 'Another' }, { 'author': 'daniel', 'body': "G'bye!", 'id': 'bad-f00d', 'title': 'Last' } ] }) def test_as_list_paginated(self): list_endpoint = DjTestResourcePaginated().as_list() req = FakeHttpRequest('GET') resp = list_endpoint(req) self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 200) self.assertEqual( json.loads(resp.content.decode('utf-8')), { 'objects': [ { 'author': 'daniel', 'body': 'Hello world!', 'id': 'dead-beef', 'title': 'First post', }, { 'author': 'daniel', 'body': 'Stuff here.', 'id': 'de-faced', 'title': 'Another', }, { 'author': 'daniel', 'body': "G'bye!", 'id': 'bad-f00d', 'title': 'Last', }, ], 'pagination': { 'num_pages': 1, 'count': 3, 'page': 1, 'start_index': 1, 'end_index': 3, 'next_page': None, 'previous_page': None, 'per_page': 10, }, }, ) def test_as_list_paginated_second_page(self): list_endpoint = DjTestResourcePaginatedOnePerPage(page_size=1).as_list() req = FakeHttpRequest('GET', get_request={'p': 2}) resp = list_endpoint(req) self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 200) self.assertEqual( json.loads(resp.content.decode('utf-8')), { 'objects': [ { 'author': 'daniel', 'body': 'Stuff here.', 'id': 'de-faced', 'title': 'Another', }, ], 'pagination': { 'num_pages': 3, 'count': 3, 'page': 2, 'start_index': 2, 'end_index': 2, 'next_page': 3, 'previous_page': 1, 'per_page': 1, }, }, ) def test_as_list_paginated_invalid_page(self): list_endpoint = DjTestResourcePaginated().as_list() req = FakeHttpRequest('GET', get_request={'p': 2}) resp = list_endpoint(req) self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 400) def test_as_detail(self): detail_endpoint = DjTestResource.as_detail() req = FakeHttpRequest('GET') resp = detail_endpoint(req, 'de-faced') self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 200) self.assertEqual(json.loads(resp.content.decode('utf-8')), { 'author': 'daniel', 'body': 'Stuff here.', 'id': 'de-faced', 'title': 'Another' }) def test_as_view(self): # This would be hooked up via the URLconf... schema_endpoint = DjTestResource.as_view('schema') req = FakeHttpRequest('GET') resp = schema_endpoint(req) self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 200) schema = json.loads(resp.content.decode('utf-8')) self.assertEqual( sorted(list(schema['fields'].keys())), [ 'author', 'body', 'id', 'title', ] ) self.assertEqual(schema['fields']['id']['type'], 'integer') self.assertEqual(schema['format'], 'application/json') def test_handle_not_implemented(self): self.res.request = FakeHttpRequest('TRACE') resp = self.res.handle('list') self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 501) self.assertEqual(resp.reason_phrase.title(), responses[501]) resp_json = json.loads(resp.content.decode('utf-8')) self.assertEqual( resp_json['error'], "Unsupported method 'TRACE' for list endpoint.") self.assertIn('traceback', resp_json) def test_handle_not_authenticated(self): # Special-cased above for testing. self.res.request = FakeHttpRequest('DELETE') # First with DEBUG on resp = self.res.handle('list') self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 401) self.assertEqual(resp.reason_phrase.title(), responses[401]) resp_json = json.loads(resp.content.decode('utf-8')) self.assertEqual(resp_json['error'], 'Unauthorized.') self.assertIn('traceback', resp_json) # Now with DEBUG off. settings.DEBUG = False self.addCleanup(setattr, settings, 'DEBUG', True) resp = self.res.handle('list') self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 401) resp_json = json.loads(resp.content.decode('utf-8')) self.assertEqual(resp_json, { 'error': 'Unauthorized.', }) self.assertNotIn('traceback', resp_json) # Last, with bubble_exceptions. class Bubbly(DjTestResource): def bubble_exceptions(self): return True with self.assertRaises(Unauthorized): bubb = Bubbly() bubb.request = FakeHttpRequest('DELETE') bubb.handle('list') def test_handle_build_err(self): # Special-cased above for testing. self.res.request = FakeHttpRequest('POST') settings.DEBUG = False self.addCleanup(setattr, settings, 'DEBUG', True) resp = self.res.handle('detail') self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 500) self.assertEqual(resp.reason_phrase.title(), responses[500]) self.assertEqual(json.loads(resp.content.decode('utf-8')), { 'error': { 'code': 'random-crazy', 'message': 'This is a random & crazy exception.', } }) def test_object_does_not_exist(self): # Make sure we get a proper Not Found exception rather than a # generic 500, when code raises a ObjectDoesNotExist exception. self.res.request = FakeHttpRequest('GET') settings.DEBUG = False self.addCleanup(setattr, settings, 'DEBUG', True) resp = self.res.handle('detail', 1001) self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 404) self.assertEqual(resp.reason_phrase.title(), responses[404]) self.assertEqual(json.loads(resp.content.decode('utf-8')), { 'error': 'Model with pk 1001 not found.' }) def test_http404_exception_handling(self): # Make sure we get a proper Not Found exception rather than a # generic 500, when code raises a Http404 exception. res = DjTestResourceHttp404Handling() res.request = FakeHttpRequest('GET') settings.DEBUG = False self.addCleanup(setattr, settings, 'DEBUG', True) resp = res.handle('detail', 1001) self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 404) self.assertEqual(resp.reason_phrase.title(), responses[404]) self.assertEqual(json.loads(resp.content.decode('utf-8')), { 'error': 'Model with pk 1001 not found.' }) def test_build_url_name(self): self.assertEqual( DjTestResource.build_url_name('list'), 'api_djtest_list' ) self.assertEqual( DjTestResource.build_url_name('detail'), 'api_djtest_detail' ) self.assertEqual( DjTestResource.build_url_name('schema'), 'api_djtest_schema' ) self.assertEqual( DjTestResource.build_url_name('list', name_prefix='v2_'), 'v2_list' ) self.assertEqual( DjTestResource.build_url_name('detail', name_prefix='v2_'), 'v2_detail' ) self.assertEqual( DjTestResource.build_url_name('schema', name_prefix='v2_'), 'v2_schema' ) def test_urls(self): patterns = DjTestResource.urls() self.assertEqual(len(patterns), 2) self.assertEqual(patterns[0].name, 'api_djtest_list') self.assertEqual(patterns[1].name, 'api_djtest_detail') patterns = DjTestResource.urls(name_prefix='v2_tests') self.assertEqual(len(patterns), 2) self.assertEqual(patterns[0].name, 'v2_tests_list') self.assertEqual(patterns[1].name, 'v2_tests_detail') def test_create(self): self.res.request = FakeHttpRequest( 'POST', body='{"id": 6, "title": "Moved hosts", "author": "daniel"}') self.assertEqual(len(self.res.fake_db), 3) resp = self.res.handle('list') self.assertEqual(resp['Content-Type'], 'application/json') self.assertEqual(resp.status_code, 201) self.assertEqual(resp.content.decode('utf-8'), '') # Check the internal state. self.assertEqual(len(self.res.fake_db), 4) self.assertEqual(self.res.data, { 'author': 'daniel', 'id': 6, 'title': 'Moved hosts' }) def test_delete(self): self.res.request = FakeHttpRequest('DELETE') self.assertEqual(len(self.res.fake_db), 3) resp = self.res.handle('detail', pk='de-faced') self.assertEqual(resp['Content-Type'], 'text/plain') self.assertEqual(resp.status_code, 204) self.assertEqual(resp.content.decode('utf-8'), '') # Check the internal state. self.res.request = FakeHttpRequest('GET') self.assertEqual(len(self.res.fake_db), 2) resp = self.res.handle('detail', pk='de-faced') self.assertEqual(resp.status_code, 404)