From 1fca0c5e0dd501ca5782c65a9b8dfb1249b9ada9 Mon Sep 17 00:00:00 2001 From: Jeremy Audet Date: Wed, 21 Oct 2015 16:34:17 -0400 Subject: [PATCH] Add CRUD tests for users Add tests that create, read, update, delete and search for users. Document the new tests. The new test module should be a functional superset of the tests in Pulp Automation's `tests.general_tests.test_05_user` module. Test results: $ python -m unittest2 pulp_smash.tests.platform.api_v2.test_user ............... ---------------------------------------------------------------------- Ran 15 tests in 16.291s OK --- docs/api.rst | 1 + ..._smash.tests.platform.api_v2.test_user.rst | 7 + pulp_smash/tests/platform/api_v2/test_user.py | 298 ++++++++++++++++++ 3 files changed, 306 insertions(+) create mode 100644 docs/api/pulp_smash.tests.platform.api_v2.test_user.rst create mode 100644 pulp_smash/tests/platform/api_v2/test_user.py diff --git a/docs/api.rst b/docs/api.rst index 783e3900f..f156284af 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -17,5 +17,6 @@ developers, not a gospel. api/pulp_smash.tests.platform.api_v2 api/pulp_smash.tests.platform.api_v2.test_content_applicability api/pulp_smash.tests.platform.api_v2.test_login + api/pulp_smash.tests.platform.api_v2.test_user api/tests api/tests.test_config diff --git a/docs/api/pulp_smash.tests.platform.api_v2.test_user.rst b/docs/api/pulp_smash.tests.platform.api_v2.test_user.rst new file mode 100644 index 000000000..23cc138f5 --- /dev/null +++ b/docs/api/pulp_smash.tests.platform.api_v2.test_user.rst @@ -0,0 +1,7 @@ +`pulp_smash.tests.platform.api_v2.test_user` +============================================ + +Location: :doc:`/index` → :doc:`/api` → +:doc:`pulp_smash.tests.platform.api_v2.test_user` + +.. automodule:: pulp_smash.tests.platform.api_v2.test_user diff --git a/pulp_smash/tests/platform/api_v2/test_user.py b/pulp_smash/tests/platform/api_v2/test_user.py new file mode 100644 index 000000000..9153ee93c --- /dev/null +++ b/pulp_smash/tests/platform/api_v2/test_user.py @@ -0,0 +1,298 @@ +# coding=utf-8 +"""Test the `user`_ API endpoints. + +The assumptions explored in this module have the following dependencies:: + + It is possible to create a user. + ├── It is impossible to create a duplicate user. + ├── It is possible to read a user. + ├── It is possible to update a user. + │ └── It is possible to search for a (updated) user. + └── It is possible to delete a user. + +.. _user: + https://pulp.readthedocs.org/en/latest/dev-guide/integration/rest-api/user/index.html + +""" +from __future__ import unicode_literals + +import requests +from pulp_smash.config import get_config +from random import randint +from unittest2 import TestCase + + +USER_PATH = '/pulp/api/v2/users/' + + +def _rand_str(): + """Return a randomized string.""" + return type('')(randint(-100000, 100000)) + + +def _search_logins(response): + """Return a tuple of all logins in a search response.""" + response.raise_for_status() + return tuple(resp['login'] for resp in response.json()) + + +class CreateTestCase(TestCase): + """Can we create users? No prior assumptions are made.""" + + @classmethod + def setUpClass(cls): + """Create several users. + + Create one user with the minimum required attributes, and another with + all available attributes. + + """ + cls.cfg = get_config() + cls.bodies = ( + {'login': _rand_str()}, + {key: _rand_str() for key in {'login', 'password', 'name'}}, + ) + cls.responses = tuple(( + requests.post( + cls.cfg.base_url + USER_PATH, + json=body, + **cls.cfg.get_requests_kwargs() + ) + for body in cls.bodies + )) + + def test_status_code(self): + """Assert that each response has an HTTP 201 status code.""" + for i, response in enumerate(self.responses): + with self.subTest(self.bodies[i]): + self.assertEqual(response.status_code, 201) + + def test_password(self): + """Assert that responses do not contain passwords.""" + for i, response in enumerate(self.responses): + with self.subTest(self.bodies[i]): + self.assertNotIn('password', response.json()) + + def test_attrs(self): + """Assert that each user has the requested attributes.""" + bodies = [body.copy() for body in self.bodies] + for body in bodies: + body.pop('password', None) + for i, body in enumerate(bodies): + with self.subTest(body): + # First check response keys… + attrs = self.responses[i].json() + self.assertLessEqual(set(body.keys()), set(attrs.keys())) + # …then check response values. + attrs = {key: attrs[key] for key in body.keys()} + self.assertEqual(body, attrs) + + @classmethod + def tearDownClass(cls): + """Delete the created users.""" + for response in cls.responses: + requests.delete( + cls.cfg.base_url + response.json()['_href'], + **cls.cfg.get_requests_kwargs() + ).raise_for_status() + + +class ReadUpdateDeleteTestCase(TestCase): + """Can we read, update and delete users? + + This test case assumes that the assertions in :class:`CreateTestCase` are + valid. + + """ + + @classmethod + def setUpClass(cls): + """Create three users and read, update and delete them respectively.""" + cls.update_body = {'delta': { + 'name': _rand_str(), + 'password': _rand_str(), + 'roles': ['super-users'], + }} + cls.cfg = get_config() + cls.paths = [] + for _ in range(3): + response = requests.post( + cls.cfg.base_url + USER_PATH, + json={'login': _rand_str()}, + **cls.cfg.get_requests_kwargs() + ) + response.raise_for_status() + cls.paths.append(response.json()['_href']) + cls.read_response = requests.get( + cls.cfg.base_url + cls.paths[0], + **cls.cfg.get_requests_kwargs() + ) + cls.update_response = requests.put( + cls.cfg.base_url + cls.paths[1], + json=cls.update_body, + **cls.cfg.get_requests_kwargs() + ) + cls.delete_response = requests.delete( + cls.cfg.base_url + cls.paths[2], + **cls.cfg.get_requests_kwargs() + ) + + def test_status_codes(self): + """Do the read, update and delete responses have 200 status codes?""" + for attr in ('read_response', 'update_response', 'delete_response'): + with self.subTest(attr): + self.assertEqual(getattr(self, attr).status_code, 200) + + def test_password_in_responses(self): + """Ensure read and update responses do not contain a password. + + Target https://bugzilla.redhat.com/show_bug.cgi?id=1020300. + + """ + for response in (self.read_response, self.update_response): + with self.subTest(response): + self.assertNotIn('password', response.json()) + + def test_use_deleted_user(self): + """Assert that one cannot read, update or delete a deleted user.""" + http_actions = ('get', 'put', 'delete') + responses = tuple(( + getattr(requests, http_action)( + self.cfg.base_url + self.paths[-1], + **self.cfg.get_requests_kwargs() + ) + for http_action in http_actions + )) + for i, response in enumerate(responses): + with self.subTest(http_actions[i]): + self.assertEqual(response.status_code, 404) + + def test_updated_user(self): + """Assert that the updated user has the assigned attributes.""" + attrs = self.update_response.json() + for key in set(self.update_body['delta'].keys()) - {'password'}: + with self.subTest(key): + self.assertIn(key, attrs.keys()) + self.assertEqual(self.update_body['delta'][key], attrs[key]) + + def test_updated_user_password(self): + """Assert that one can log in with a user with an updated password.""" + login = self.update_response.json()['login'] + requests.post( + self.cfg.base_url + '/pulp/api/v2/actions/login/', + auth=(login, self.update_body['delta']['password']), + verify=self.cfg.verify, + ).raise_for_status() + + def test_create_duplicate_user(self): + """Verify that one cannot create a duplicate user.""" + response = requests.post( + self.cfg.base_url + USER_PATH, + json={'login': self.read_response.json()['login']}, + **self.cfg.get_requests_kwargs() + ) + self.assertEqual(response.status_code, 409) + + @classmethod + def tearDownClass(cls): + """Delete created users. + + :meth:`setUpClass` makes a super-user. Thus, this method tests whether + it is possible to delete a super-user. + + """ + for path in cls.paths[0:1]: + requests.delete( + cls.cfg.base_url + path, + **cls.cfg.get_requests_kwargs() + ).raise_for_status() + + +class SearchTestCase(TestCase): + """Can we search for users? + + This test case assumes that the assertions in + :class:`ReadUpdateDeleteTestCase` are valid. + + """ + + @classmethod + def setUpClass(cls): + """Create a user and add it to the 'super-users' role. + + Search for: + + * Nothing at all. + * All super-users. + * A user by their login. + * A non-existent user by their login. + + """ + # Create a user and note information about it. + cls.cfg = get_config() + cls.login = _rand_str() + response = requests.post( + cls.cfg.base_url + USER_PATH, + json={'login': cls.login}, + **cls.cfg.get_requests_kwargs() + ) + response.raise_for_status() + cls.path = response.json()['_href'] + + # Make user a super-user. + requests.put( + cls.cfg.base_url + cls.path, + json={'delta': {'roles': ['super-users']}}, + **cls.cfg.get_requests_kwargs() + ).raise_for_status() + + # Formulate and execute searches. Save responses. + searches = tuple(( + {'criteria': {}}, + {'criteria': {'filters': {'roles': ['super-users']}}}, + {'criteria': {'filters': {'roles': []}}}, + {'criteria': {'filters': {'login': cls.login}}}, + {'criteria': {'filters': {'login': _rand_str()}}}, + )) + cls.responses = tuple(( + requests.post( + cls.cfg.base_url + USER_PATH + 'search/', + json=search, + **cls.cfg.get_requests_kwargs() + ) + for search in searches + )) + + def test_status_codes(self): + """Assert that each response has an HTTP 200 status code.""" + for i, response in enumerate(self.responses): + with self.subTest(i): + self.assertEqual(response.status_code, 200, response.json()) + + def test_global_search(self): + """Assert that the global search includes the user's login.""" + self.assertIn(self.login, _search_logins(self.responses[0])) + + def test_roles_filter_inclusion(self): + """Assert that the "roles" filter can be used for inclusion.""" + self.assertIn(self.login, _search_logins(self.responses[1])) + + def test_roles_filter_exclusion(self): + """Assert that the "roles" filter can be used for exclusion.""" + self.assertNotIn(self.login, _search_logins(self.responses[2])) + + def test_login_filter_inclusion(self): + """Search for a user via the "login" filter.""" + self.assertEqual({self.login}, set(_search_logins(self.responses[3]))) + + def test_login_filter_exclusion(self): + """Search for a non-existent user via the "login" filter.""" + self.assertEqual(len(_search_logins(self.responses[4])), 0) + + @classmethod + def tearDownClass(cls): + """Delete created users.""" + requests.delete( + cls.cfg.base_url + cls.path, + **cls.cfg.get_requests_kwargs() + ).raise_for_status()