import base64 from django.contrib.auth.models import User from django.test import TestCase from django.test.utils import override_settings import json import re from student.tests.factories import UserFactory from unittest import SkipTest from user_api.models import UserPreference from user_api.tests.factories import UserPreferenceFactory TEST_API_KEY = "test_api_key" USER_LIST_URI = "/user_api/v1/users/" USER_PREFERENCE_LIST_URI = "/user_api/v1/user_prefs/" @override_settings(EDX_API_KEY=TEST_API_KEY) class UserApiTestCase(TestCase): def setUp(self): super(UserApiTestCase, self).setUp() self.users = [ UserFactory.create( email="test{0}@test.org".format(i), profile__name="Test {0}".format(i) ) for i in range(5) ] self.prefs = [ UserPreferenceFactory.create(user=self.users[0], key="key0"), UserPreferenceFactory.create(user=self.users[0], key="key1"), UserPreferenceFactory.create(user=self.users[1], key="key0") ] def basic_auth(self, username, password): return {'HTTP_AUTHORIZATION': 'Basic ' + base64.b64encode('%s:%s' % (username, password))} def request_with_auth(self, method, *args, **kwargs): """Issue a get request to the given URI with the API key header""" return getattr(self.client, method)(*args, HTTP_X_EDX_API_KEY=TEST_API_KEY, **kwargs) def get_json(self, *args, **kwargs): """Make a request with the given args and return the parsed JSON repsonse""" resp = self.request_with_auth("get", *args, **kwargs) self.assertHttpOK(resp) self.assertTrue(resp["Content-Type"].startswith("application/json")) return json.loads(resp.content) def get_uri_for_user(self, target_user): """Given a user object, get the URI for the corresponding resource""" users = self.get_json(USER_LIST_URI)["results"] for user in users: if user["id"] == target_user.id: return user["url"] self.fail() def get_uri_for_pref(self, target_pref): """Given a user preference object, get the URI for the corresponding resource""" prefs = self.get_json(USER_PREFERENCE_LIST_URI)["results"] for pref in prefs: if (pref["user"]["id"] == target_pref.user.id and pref["key"] == target_pref.key): return pref["url"] self.fail() def assertAllowedMethods(self, uri, expected_methods): """Assert that the allowed methods for the given URI match the expected list""" resp = self.request_with_auth("options", uri) self.assertHttpOK(resp) allow_header = resp.get("Allow") self.assertIsNotNone(allow_header) allowed_methods = re.split('[^A-Z]+', allow_header) self.assertItemsEqual(allowed_methods, expected_methods) def assertSelfReferential(self, obj): """Assert that accessing the "url" entry in the given object returns the same object""" copy = self.get_json(obj["url"]) self.assertEqual(obj, copy) def assertUserIsValid(self, user): """Assert that the given user result is valid""" self.assertItemsEqual(user.keys(), ["email", "id", "name", "username", "url"]) self.assertSelfReferential(user) def assertPrefIsValid(self, pref): self.assertItemsEqual(pref.keys(), ["user", "key", "value", "url"]) self.assertSelfReferential(pref) self.assertUserIsValid(pref["user"]) def assertHttpOK(self, response): """Assert that the given response has the status code 200""" self.assertEqual(response.status_code, 200) def assertHttpForbidden(self, response): """Assert that the given response has the status code 403""" self.assertEqual(response.status_code, 403) def assertHttpMethodNotAllowed(self, response): """Assert that the given response has the status code 405""" self.assertEqual(response.status_code, 405) class UserViewSetTest(UserApiTestCase): LIST_URI = USER_LIST_URI def setUp(self): super(UserViewSetTest, self).setUp() self.detail_uri = self.get_uri_for_user(self.users[0]) # List view tests def test_options_list(self): self.assertAllowedMethods(self.LIST_URI, ["OPTIONS", "GET", "HEAD"]) def test_post_list_not_allowed(self): self.assertHttpMethodNotAllowed(self.request_with_auth("post", self.LIST_URI)) def test_put_list_not_allowed(self): self.assertHttpMethodNotAllowed(self.request_with_auth("put", self.LIST_URI)) def test_patch_list_not_allowed(self): raise SkipTest("Django 1.4's test client does not support patch") def test_delete_list_not_allowed(self): self.assertHttpMethodNotAllowed(self.request_with_auth("delete", self.LIST_URI)) def test_list_unauthorized(self): self.assertHttpForbidden(self.client.get(self.LIST_URI)) @override_settings(DEBUG=True) @override_settings(EDX_API_KEY=None) def test_debug_auth(self): self.assertHttpOK(self.client.get(self.LIST_URI)) @override_settings(DEBUG=False) @override_settings(EDX_API_KEY=TEST_API_KEY) def test_basic_auth(self): # ensure that having basic auth headers in the mix does not break anything self.assertHttpOK( self.request_with_auth("get", self.LIST_URI, **self.basic_auth('someuser', 'somepass'))) self.assertHttpForbidden( self.client.get(self.LIST_URI, **self.basic_auth('someuser', 'somepass'))) def test_get_list_empty(self): User.objects.all().delete() result = self.get_json(self.LIST_URI) self.assertEqual(result["count"], 0) self.assertIsNone(result["next"]) self.assertIsNone(result["previous"]) self.assertEqual(result["results"], []) def test_get_list_nonempty(self): result = self.get_json(self.LIST_URI) self.assertEqual(result["count"], 5) self.assertIsNone(result["next"]) self.assertIsNone(result["previous"]) users = result["results"] self.assertEqual(len(users), 5) for user in users: self.assertUserIsValid(user) def test_get_list_pagination(self): first_page = self.get_json(self.LIST_URI, data={"page_size": 3}) self.assertEqual(first_page["count"], 5) first_page_next_uri = first_page["next"] self.assertIsNone(first_page["previous"]) first_page_users = first_page["results"] self.assertEqual(len(first_page_users), 3) second_page = self.get_json(first_page_next_uri) self.assertEqual(second_page["count"], 5) self.assertIsNone(second_page["next"]) second_page_prev_uri = second_page["previous"] second_page_users = second_page["results"] self.assertEqual(len(second_page_users), 2) self.assertEqual(self.get_json(second_page_prev_uri), first_page) for user in first_page_users + second_page_users: self.assertUserIsValid(user) all_user_uris = [user["url"] for user in first_page_users + second_page_users] self.assertEqual(len(set(all_user_uris)), 5) # Detail view tests def test_options_detail(self): self.assertAllowedMethods(self.detail_uri, ["OPTIONS", "GET", "HEAD"]) def test_post_detail_not_allowed(self): self.assertHttpMethodNotAllowed(self.request_with_auth("post", self.detail_uri)) def test_put_detail_not_allowed(self): self.assertHttpMethodNotAllowed(self.request_with_auth("put", self.detail_uri)) def test_patch_detail_not_allowed(self): raise SkipTest("Django 1.4's test client does not support patch") def test_delete_detail_not_allowed(self): self.assertHttpMethodNotAllowed(self.request_with_auth("delete", self.detail_uri)) def test_get_detail_unauthorized(self): self.assertHttpForbidden(self.client.get(self.detail_uri)) def test_get_detail(self): user = self.users[1] uri = self.get_uri_for_user(user) self.assertEqual( self.get_json(uri), { "email": user.email, "id": user.id, "name": user.profile.name, "username": user.username, "url": uri } ) class UserPreferenceViewSetTest(UserApiTestCase): LIST_URI = USER_PREFERENCE_LIST_URI def setUp(self): super(UserPreferenceViewSetTest, self).setUp() self.detail_uri = self.get_uri_for_pref(self.prefs[0]) # List view tests def test_options_list(self): self.assertAllowedMethods(self.LIST_URI, ["OPTIONS", "GET", "HEAD"]) def test_put_list_not_allowed(self): self.assertHttpMethodNotAllowed(self.request_with_auth("put", self.LIST_URI)) def test_patch_list_not_allowed(self): raise SkipTest("Django 1.4's test client does not support patch") def test_delete_list_not_allowed(self): self.assertHttpMethodNotAllowed(self.request_with_auth("delete", self.LIST_URI)) def test_list_unauthorized(self): self.assertHttpForbidden(self.client.get(self.LIST_URI)) @override_settings(DEBUG=True) @override_settings(EDX_API_KEY=None) def test_debug_auth(self): self.assertHttpOK(self.client.get(self.LIST_URI)) def test_get_list_empty(self): UserPreference.objects.all().delete() result = self.get_json(self.LIST_URI) self.assertEqual(result["count"], 0) self.assertIsNone(result["next"]) self.assertIsNone(result["previous"]) self.assertEqual(result["results"], []) def test_get_list_nonempty(self): result = self.get_json(self.LIST_URI) self.assertEqual(result["count"], 3) self.assertIsNone(result["next"]) self.assertIsNone(result["previous"]) prefs = result["results"] self.assertEqual(len(prefs), 3) for pref in prefs: self.assertPrefIsValid(pref) def test_get_list_filter_key_empty(self): result = self.get_json(self.LIST_URI, data={"key": "non-existent"}) self.assertEqual(result["count"], 0) self.assertEqual(result["results"], []) def test_get_list_filter_key_nonempty(self): result = self.get_json(self.LIST_URI, data={"key": "key0"}) self.assertEqual(result["count"], 2) prefs = result["results"] self.assertEqual(len(prefs), 2) for pref in prefs: self.assertPrefIsValid(pref) self.assertEqual(pref["key"], "key0") def test_get_list_filter_user_empty(self): def test_id(user_id): result = self.get_json(self.LIST_URI, data={"user": user_id}) self.assertEqual(result["count"], 0) self.assertEqual(result["results"], []) test_id(self.users[2].id) # TODO: If the given id does not match a user, then the filter is a no-op # test_id(42) # test_id("asdf") def test_get_list_filter_user_nonempty(self): user_id = self.users[0].id result = self.get_json(self.LIST_URI, data={"user": user_id}) self.assertEqual(result["count"], 2) prefs = result["results"] self.assertEqual(len(prefs), 2) for pref in prefs: self.assertPrefIsValid(pref) self.assertEqual(pref["user"]["id"], user_id) def test_get_list_pagination(self): first_page = self.get_json(self.LIST_URI, data={"page_size": 2}) self.assertEqual(first_page["count"], 3) first_page_next_uri = first_page["next"] self.assertIsNone(first_page["previous"]) first_page_prefs = first_page["results"] self.assertEqual(len(first_page_prefs), 2) second_page = self.get_json(first_page_next_uri) self.assertEqual(second_page["count"], 3) self.assertIsNone(second_page["next"]) second_page_prev_uri = second_page["previous"] second_page_prefs = second_page["results"] self.assertEqual(len(second_page_prefs), 1) self.assertEqual(self.get_json(second_page_prev_uri), first_page) for pref in first_page_prefs + second_page_prefs: self.assertPrefIsValid(pref) all_pref_uris = [pref["url"] for pref in first_page_prefs + second_page_prefs] self.assertEqual(len(set(all_pref_uris)), 3) # Detail view tests def test_options_detail(self): self.assertAllowedMethods(self.detail_uri, ["OPTIONS", "GET", "HEAD"]) def test_post_detail_not_allowed(self): self.assertHttpMethodNotAllowed(self.request_with_auth("post", self.detail_uri)) def test_put_detail_not_allowed(self): self.assertHttpMethodNotAllowed(self.request_with_auth("put", self.detail_uri)) def test_patch_detail_not_allowed(self): raise SkipTest("Django 1.4's test client does not support patch") def test_delete_detail_not_allowed(self): self.assertHttpMethodNotAllowed(self.request_with_auth("delete", self.detail_uri)) def test_detail_unauthorized(self): self.assertHttpForbidden(self.client.get(self.detail_uri)) def test_get_detail(self): pref = self.prefs[1] uri = self.get_uri_for_pref(pref) self.assertEqual( self.get_json(uri), { "user": { "email": pref.user.email, "id": pref.user.id, "name": pref.user.profile.name, "username": pref.user.username, "url": self.get_uri_for_user(pref.user), }, "key": pref.key, "value": pref.value, "url": uri, } )