From 031361801d3fdf22f0bcedcbef92c6bdb09b950b Mon Sep 17 00:00:00 2001 From: Ian Cordasco Date: Wed, 26 Oct 2016 11:56:04 -0500 Subject: [PATCH] Add new tests for the cratonclient.crud module This adds unit-only testing for the CRUDManager and Resource classes in cratonclient.crud to solidify our testing posture in cratonclient. In the process of writing these tests, I also identified and fixed a couple tiny bugs introduced by copying the crud module over from keystoneclient. Change-Id: I85103fae92f6135facee39c1bf7afb11e8ca1030 --- cratonclient/crud.py | 22 +- cratonclient/tests/unit/test_crud.py | 298 +++++++++++++++++++++++++++ 2 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 cratonclient/tests/unit/test_crud.py diff --git a/cratonclient/crud.py b/cratonclient/crud.py index 1fb7936..9dc2c73 100644 --- a/cratonclient/crud.py +++ b/cratonclient/crud.py @@ -80,28 +80,34 @@ class CRUDClient(object): """Create a new item based on the keyword arguments provided.""" url = self.build_url(path_arguments=kwargs) response = self.session.post(url, json=kwargs) - return self.resource_class(self, response.json()) + return self.resource_class(self, response.json(), loaded=True) - def get(self, **kwargs): + def get(self, item_id=None, **kwargs): """Retrieve the item based on the keyword arguments provided.""" + kwargs.setdefault(self.key + '_id', item_id) url = self.build_url(path_arguments=kwargs) response = self.session.get(url) - return self.resource_class(self, response.json()) + return self.resource_class(self, response.json(), loaded=True) def list(self, **kwargs): """List the items from this endpoint.""" url = self.build_url(path_arguments=kwargs) response = self.session.get(url, params=kwargs) - return [self.resource_class(self, item) for item in response.json()] + return [ + self.resource_class(self, item, loaded=True) + for item in response.json() + ] - def update(self, **kwargs): + def update(self, item_id=None, **kwargs): """Update the item based on the keyword arguments provided.""" + kwargs.setdefault(self.key + '_id', item_id) url = self.build_url(path_arguments=kwargs) response = self.session.put(url, json=kwargs) - return self.resource_class(self, response.json()) + return self.resource_class(self, response.json(), loaded=True) - def delete(self, **kwargs): + def delete(self, item_id=None, **kwargs): """Delete the item based on the keyword arguments provided.""" + kwargs.setdefault(self.key + '_id', item_id) url = self.build_url(path_arguments=kwargs) response = self.session.delete(url, params=kwargs) if 200 <= response.status_code < 300: @@ -210,4 +216,4 @@ class Resource(object): def delete(self): """Delete the resource from the service.""" - return self.manager.delete(self) + return self.manager.delete(self.id) diff --git a/cratonclient/tests/unit/test_crud.py b/cratonclient/tests/unit/test_crud.py new file mode 100644 index 0000000..24311b0 --- /dev/null +++ b/cratonclient/tests/unit/test_crud.py @@ -0,0 +1,298 @@ +# -*- coding: utf-8 -*- + +# Copyright 2010-2011 OpenStack Foundation +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +"""Unit tests for the cratonclient.crud module members.""" + +import mock + +from cratonclient import crud +from cratonclient.tests import base + + +class TestCRUDClient(base.TestCase): + """Test for the CRUDClient class.""" + + def setUp(self): + """Create necessary test resources prior to each test.""" + super(TestCRUDClient, self).setUp() + self.session = mock.Mock() + self.resource_spec = mock.Mock(spec=crud.Resource) + self.client = crud.CRUDClient(self.session, 'http://example.com/v1/') + self.client.base_path = '/test' + self.client.key = 'test_key' + self.client.resource_class = self.resource_spec + + def test_strips_trailing_forward_slash_from_url(self): + """Verify the client strips the trailing / in a URL.""" + self.assertEqual('http://example.com/v1', self.client.url) + + def test_builds_url_correctly_with_no_path_args(self): + """Verify the generated URL from CRUDClient#build_url without args.""" + self.assertEqual( + 'http://example.com/v1/test', + self.client.build_url(), + ) + + def test_builds_url_correctly_with_key(self): + """Verify the generated URL from CRUDClient#build_url with key.""" + self.assertEqual( + 'http://example.com/v1/test/1', + self.client.build_url({'test_key_id': '1'}), + ) + + def test_builds_url_correctly_with_path_args(self): + """Verify the generated URL from CRUDClient#build_url with args.""" + self.assertEqual( + 'http://example.com/v1/test/1', + self.client.build_url({ + 'test_key_id': '1', + 'extra_arg': 'foo', + }), + ) + + def test_build_url_allows_base_path_override(self): + """Verify we can override the client's base_path attribute.""" + self.assertEqual( + 'http://example.com/v1/override/1', + self.client.build_url({ + 'test_key_id': '1', + 'base_path': '/override', + }), + ) + + def test_create_generates_a_post_request(self): + """Verify that using our create method will POST to our service.""" + response = self.session.post.return_value = mock.Mock() + response.json.return_value = {'name': 'fake-name', 'id': 1} + + self.client.create(name='fake-name') + + self.session.post.assert_called_once_with( + 'http://example.com/v1/test', + json={'name': 'fake-name'}, + ) + self.resource_spec.assert_called_once_with( + self.client, + {'name': 'fake-name', 'id': 1}, + loaded=True, + ) + + def test_delete_generates_a_delete_request(self): + """Verify that using our delete method will send DELETE.""" + response = self.session.delete.return_value = mock.Mock() + response.status_code = 204 + + self.client.delete(test_key_id='1') + + self.session.delete.assert_called_once_with( + 'http://example.com/v1/test/1', + params={} + ) + self.assertFalse(self.resource_spec.called) + + def test_delete_generates_a_delete_request_positionally(self): + """Verify passing the id positionally works as well.""" + response = self.session.delete.return_value = mock.Mock() + response.status_code = 204 + + self.client.delete(1) + + self.session.delete.assert_called_once_with( + 'http://example.com/v1/test/1', + params={} + ) + self.assertFalse(self.resource_spec.called) + + def test_get_generates_a_get_request(self): + """Verify that using our get method will GET from our service.""" + response = self.session.get.return_value = mock.Mock() + response.json.return_value = {'name': 'fake-name', 'id': 1} + + self.client.get(test_key_id=1) + + self.session.get.assert_called_once_with( + 'http://example.com/v1/test/1', + ) + self.resource_spec.assert_called_once_with( + self.client, + {'name': 'fake-name', 'id': 1}, + loaded=True, + ) + + def test_get_generates_a_get_request_positionally(self): + """Verify passing the id positionally works as well.""" + response = self.session.get.return_value = mock.Mock() + response.json.return_value = {'name': 'fake-name', 'id': 1} + + self.client.get(1) + + self.session.get.assert_called_once_with( + 'http://example.com/v1/test/1', + ) + self.resource_spec.assert_called_once_with( + self.client, + {'name': 'fake-name', 'id': 1}, + loaded=True, + ) + + def test_list_generates_a_get_request(self): + """Verify that using our list method will GET from our service.""" + response = self.session.get.return_value = mock.Mock() + response.json.return_value = [{'name': 'fake-name', 'id': 1}] + + self.client.list(sort='asc') + + self.session.get.assert_called_once_with( + 'http://example.com/v1/test', + params={'sort': 'asc'}, + ) + self.resource_spec.assert_called_once_with( + self.client, + {'name': 'fake-name', 'id': 1}, + loaded=True, + ) + + def test_update_generates_a_put_request(self): + """Verify that using our update method will PUT to our service.""" + response = self.session.put.return_value = mock.Mock() + response.json.return_value = {'name': 'fake-name', 'id': 1} + + self.client.update(test_key_id='1', name='new-name') + + self.session.put.assert_called_once_with( + 'http://example.com/v1/test/1', + json={'name': 'new-name'}, + ) + self.resource_spec.assert_called_once_with( + self.client, + {'name': 'fake-name', 'id': 1}, + loaded=True, + ) + + def test_update_generates_a_put_request_positionally(self): + """Verify passing the id positionally works as well.""" + response = self.session.put.return_value = mock.Mock() + response.json.return_value = {'name': 'fake-name', 'id': 1} + + self.client.update(1, name='new-name') + + self.session.put.assert_called_once_with( + 'http://example.com/v1/test/1', + json={'name': 'new-name'}, + ) + self.resource_spec.assert_called_once_with( + self.client, + {'name': 'fake-name', 'id': 1}, + loaded=True, + ) + + +class TestResource(base.TestCase): + """Tests for our crud.Resource object.""" + + def setUp(self): + """Create necessary fixture data for our Resource tests.""" + super(TestResource, self).setUp() + self.manager = mock.Mock() + self.info = {'name': 'fake-name', 'id': 1234} + self.resource = crud.Resource(self.manager, self.info) + + def test_data_storage(self): + """Verify we store our info privately.""" + self.assertEqual(self.info, self.resource._info) + + def test_manager(self): + """Verify we store the manager passed in.""" + self.assertIs(self.manager, self.resource.manager) + + def test_human_id_is_false(self): + """Test that None is returned when HUMAN_ID is False.""" + self.assertIsNone(self.resource.human_id) + + def test_human_id_is_true(self): + """Verify we return our human-readable name.""" + self.resource.HUMAN_ID = True + + self.assertEqual('fake-name', self.resource.human_id) + + def test_info_is_converted_to_attributes(self): + """Verify we add info data as attributes.""" + self.assertEqual('fake-name', getattr(self.resource, 'name')) + self.assertEqual(1234, getattr(self.resource, 'id')) + + def test_retrieves_info_when_not_loaded(self): + """Verify the resource tries to retrieve data from the service.""" + self.manager.get.return_value._info = {'non_existent': 'foo'} + + self.assertEqual('foo', self.resource.non_existent) + self.manager.get.assert_called_once_with(1234) + + def test_raises_attributeerror_for_missing_attributes_when_loaded(self): + """Verify we raise an AttributeError for missing attributes.""" + self.resource.set_loaded(True) + + self.assertRaises( + AttributeError, + getattr, self.resource, 'non_existent', + ) + + def test_equality(self): + """Verify we check for equality correctly.""" + manager = mock.Mock() + info = {'name': 'fake-name', 'id': 1234} + new_resource = crud.Resource(manager, info) + self.assertEqual(new_resource, self.resource) + + def test_to_dict_clones(self): + """Prove that we return a new dictionary from to_dict.""" + self.assertIsNot(self.info, self.resource.to_dict()) + + def test_to_dict_equality(self): + """Prove that the new dictionary is equal.""" + self.assertEqual(self.info, self.resource.to_dict()) + + def test_delete_calls_manager_delete(self): + """Verify the manager's delete method is called.""" + self.resource.delete() + + self.manager.delete.assert_called_once_with(1234) + + def test_defaults_to_unloaded(self): + """Verify by default a Resource is not loaded.""" + self.assertFalse(self.resource.is_loaded()) + + def test_set_loaded_updates_loaded_status(self): + """Verify set_loaded updates our loaded status.""" + self.resource.set_loaded(True) + self.assertTrue(self.resource.is_loaded()) + self.resource.set_loaded(False) + self.assertFalse(self.resource.is_loaded()) + + def test_get_updates_with_new_info(self): + """Verify we change the attribute values for new info.""" + self.manager.get.return_value._info = {'name': 'new-name'} + + self.resource.get() + self.assertTrue(self.resource.is_loaded()) + self.assertEqual('new-name', self.resource.name) + + def test_get_updates_for_no_new_info(self): + """Verify we don't add new details when there's nothing to add.""" + self.manager.get.return_value = None + + self.resource.get() + self.assertTrue(self.resource.is_loaded()) + self.assertEqual('fake-name', self.resource.name)