diff --git a/ckan/new_tests/data.py b/ckan/new_tests/data.py deleted file mode 100644 index c67058e2c93..00000000000 --- a/ckan/new_tests/data.py +++ /dev/null @@ -1,32 +0,0 @@ -'''A collection of static data for use in tests. - -These are functions that return objects because the data is intended to be -read-only: if they were simply module-level objects then one test may modify -eg. a dict or list and then break the tests that follow it. - -''' - - -def typical_user(): - '''Return a dictionary representation of a typical, valid CKAN user.''' - - return {'name': 'fred', - 'email': 'fred@fred.com', - 'password': 'wilma', - } - - -def validator_data_dict(): - '''Return a data dict with some arbitrary data in it, suitable to be passed - to validator functions for testing. - - ''' - return {('other key',): 'other value'} - - -def validator_errors_dict(): - '''Return an errors dict with some arbitrary errors in it, suitable to be - passed to validator functions for testing. - - ''' - return {('other key',): ['other error']} diff --git a/ckan/new_tests/factories.py b/ckan/new_tests/factories.py new file mode 100644 index 00000000000..b68fa7789f7 --- /dev/null +++ b/ckan/new_tests/factories.py @@ -0,0 +1,96 @@ +'''A collection of factory classes for building CKAN users, datasets, etc. + +These are meant to be used by tests to create any objects or "test fixtures" +that are needed for the tests. They're written using factory_boy: + + http://factoryboy.readthedocs.org/en/latest/ + +These are not meant to be used for the actual testing, e.g. if you're writing a +test for the user_create action function then call user_create, don't test it +via the User factory below. + +Usage: + + # Create a user with the factory's default attributes, and get back a + # user dict: + user_dict = factories.User() + + # You can create a second user the same way. For attributes that can't be + # the same (e.g. you can't have two users with the same name) a new value + # will be generated each time you use the factory: + another_user_dict = factories.User() + + # Create a user and specify your own user name and email (this works + # with any params that CKAN's user_create() accepts): + custom_user_dict = factories.User(name='bob', email='bob@bob.com') + + # Get a user dict containing the attributes (name, email, password, etc.) + # that the factory would use to create a user, but without actually + # creating the user in CKAN: + user_attributes_dict = factories.User.attributes() + + # If you later want to create a user using these attributes, just pass them + # to the factory: + user = factories.User(**user_attributes_dict) + +''' +import factory + +import ckan.model +import ckan.logic +import ckan.new_tests.helpers as helpers + + +class User(factory.Factory): + '''A factory class for creating CKAN users.''' + + # This is the class that UserFactory will create and return instances + # of. + FACTORY_FOR = ckan.model.User + + # These are the default params that will be used to create new users. + fullname = 'Mr. Test User' + password = 'pass' + about = 'Just another test user.' + + # Generate a different user name param for each user that gets created. + name = factory.Sequence( + lambda n: 'test_user_{n}'.format(n=n)) + + # Compute the email param for each user based on the values of the other + # params above. + email = factory.LazyAttribute( + lambda a: '{0}@ckan.org'.format(a.name).lower()) + + # I'm not sure how to support factory_boy's .build() feature in CKAN, + # so I've disabled it here. + @classmethod + def _build(cls, target_class, *args, **kwargs): + raise NotImplementedError(".build() isn't supported in CKAN") + + # To make factory_boy work with CKAN we override _create() and make it call + # a CKAN action function. + # We might also be able to do this by using factory_boy's direct SQLAlchemy + # support: http://factoryboy.readthedocs.org/en/latest/orms.html#sqlalchemy + @classmethod + def _create(cls, target_class, *args, **kwargs): + if args: + assert False, "Positional args aren't supported, use keyword args." + user_dict = helpers.call_action('user_create', **kwargs) + return user_dict + + +def validator_data_dict(): + '''Return a data dict with some arbitrary data in it, suitable to be passed + to validator functions for testing. + + ''' + return {('other key',): 'other value'} + + +def validator_errors_dict(): + '''Return an errors dict with some arbitrary errors in it, suitable to be + passed to validator functions for testing. + + ''' + return {('other key',): ['other error']} diff --git a/ckan/new_tests/lib/navl/test_validators.py b/ckan/new_tests/lib/navl/test_validators.py index 51df87da648..cbebfddd467 100644 --- a/ckan/new_tests/lib/navl/test_validators.py +++ b/ckan/new_tests/lib/navl/test_validators.py @@ -6,7 +6,7 @@ import nose.tools -import ckan.new_tests.data as test_data +import ckan.new_tests.factories as factories class TestValidators(object): @@ -28,12 +28,12 @@ def test_ignore_missing_with_value_missing(self): key = ('key to be validated',) # The data to pass to the validator function for validation. - data = test_data.validator_data_dict() + data = factories.validator_data_dict() if value != 'skip': data[key] = value # The errors dict to pass to the validator function. - errors = test_data.validator_errors_dict() + errors = factories.validator_errors_dict() errors[key] = [] # Make copies of the data and errors dicts for asserting later. @@ -66,9 +66,9 @@ def test_ignore_missing_with_a_value(self): import ckan.lib.navl.validators as validators key = ('key to be validated',) - data = test_data.validator_data_dict() + data = factories.validator_data_dict() data[key] = 'value to be validated' - errors = test_data.validator_errors_dict() + errors = factories.validator_errors_dict() errors[key] = [] # Make copies of the data and errors dicts for asserting later. diff --git a/ckan/new_tests/logic/action/test_update.py b/ckan/new_tests/logic/action/test_update.py index 46235fd778f..d94a930ae0f 100644 --- a/ckan/new_tests/logic/action/test_update.py +++ b/ckan/new_tests/logic/action/test_update.py @@ -6,7 +6,7 @@ import ckan.logic as logic import ckan.new_tests.helpers as helpers -import ckan.new_tests.data as data +import ckan.new_tests.factories as factories def datetime_from_string(s): @@ -49,7 +49,7 @@ def test_user_update_name(self): # 4. Do absolutely nothing else! # 1. Setup. - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() # 2. Call the function that is being tested, once only. # FIXME we have to pass the email address and password to user_update @@ -57,7 +57,7 @@ def test_user_update_name(self): # fails. helpers.call_action('user_update', id=user['name'], email=user['email'], - password=data.typical_user()['password'], + password=factories.User.attributes()['password'], name='updated', ) @@ -70,21 +70,22 @@ def test_user_update_name(self): # 4. Do absolutely nothing else! def test_user_update_with_id_that_does_not_exist(self): - user_dict = data.typical_user() + user_dict = factories.User.attributes() user_dict['id'] = "there's no user with this id" + with nose.tools.assert_raises(logic.NotFound) as context: helpers.call_action('user_update', **user_dict) # TODO: Could assert the actual error message, not just the exception? # (Could also do this with many of the tests below.) def test_user_update_with_no_id(self): - user_dict = data.typical_user() + user_dict = factories.User.attributes() assert 'id' not in user_dict with nose.tools.assert_raises(logic.ValidationError) as context: helpers.call_action('user_update', **user_dict) def test_user_update_with_invalid_name(self): - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() invalid_names = ('', 'a', False, 0, -1, 23, 'new', 'edit', 'search', 'a'*200, 'Hi!', ) @@ -94,9 +95,8 @@ def test_user_update_with_invalid_name(self): helpers.call_action('user_update', **user) def test_user_update_to_name_that_already_exists(self): - fred = helpers.call_action('user_create', **data.typical_user()) - bob = helpers.call_action('user_create', name='bob', - email='bob@bob.com', password='pass') + fred = factories.User(name='fred') + bob = factories.User(name='bob') # Try to update fred and change his user name to bob, which is already # bob's user name @@ -107,7 +107,7 @@ def test_user_update_to_name_that_already_exists(self): def test_user_update_password(self): '''Test that updating a user's password works successfully.''' - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() # FIXME we have to pass the email address to user_update even though # we're not updating it, otherwise validation fails. @@ -123,7 +123,7 @@ def test_user_update_password(self): assert updated_user.validate_password('new password') def test_user_update_with_short_password(self): - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() user['password'] = 'xxx' # This password is too short. with nose.tools.assert_raises(logic.ValidationError) as context: @@ -137,9 +137,9 @@ def test_user_update_with_empty_password(self): changed either. ''' - user_dict = data.typical_user() + user_dict = factories.User.attributes() original_password = user_dict['password'] - user_dict = helpers.call_action('user_create', **user_dict) + user_dict = factories.User(**user_dict) user_dict['password'] = '' helpers.call_action('user_update', **user_dict) @@ -149,14 +149,14 @@ def test_user_update_with_empty_password(self): assert updated_user.validate_password(original_password) def test_user_update_with_null_password(self): - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() user['password'] = None with nose.tools.assert_raises(logic.ValidationError) as context: helpers.call_action('user_update', **user) def test_user_update_with_invalid_password(self): - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() for password in (False, -1, 23, 30.7): user['password'] = password @@ -168,7 +168,7 @@ def test_user_update_with_invalid_password(self): def test_user_update_activity_stream(self): '''Test that the right activity is emitted when updating a user.''' - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() before = datetime.datetime.now() # FIXME we have to pass the email address and password to user_update @@ -176,7 +176,7 @@ def test_user_update_activity_stream(self): # fails. helpers.call_action('user_update', id=user['name'], email=user['email'], - password=data.typical_user()['password'], + password=factories.User.attributes()['password'], name='updated', ) @@ -203,7 +203,7 @@ def test_user_update_with_custom_schema(self): ''' import ckan.logic.schema - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() # A mock validator method, it doesn't do anything but it records what # params it gets called with and how many times. @@ -219,7 +219,7 @@ def test_user_update_with_custom_schema(self): # trying to update them, or validation fails. helpers.call_action('user_update', context={'schema': schema}, id=user['name'], email=user['email'], - password=data.typical_user()['password'], + password=factories.User.attributes()['password'], name='updated', ) diff --git a/ckan/new_tests/logic/auth/test_update.py b/ckan/new_tests/logic/auth/test_update.py index 49bf163178f..be58fe89407 100644 --- a/ckan/new_tests/logic/auth/test_update.py +++ b/ckan/new_tests/logic/auth/test_update.py @@ -2,7 +2,7 @@ ''' import ckan.new_tests.helpers as helpers -import ckan.new_tests.data as data +import ckan.new_tests.factories as factories class TestUpdate(object): @@ -35,7 +35,7 @@ def _call_auth(self, auth_name, context=None, **kwargs): def test_user_update_visitor_cannot_update_user(self): '''Visitors should not be able to update users' accounts.''' - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() user['name'] = 'updated' # Try to update the user, but without passing any API key. @@ -49,9 +49,8 @@ def test_user_update_visitor_cannot_update_user(self): def test_user_update_user_cannot_update_another_user(self): '''Users should not be able to update other users' accounts.''' - fred = helpers.call_action('user_create', **data.typical_user()) - bob = helpers.call_action('user_create', name='bob', - email='bob@bob.com', password='pass') + fred = factories.User(name='fred') + bob = factories.User(name='bob') fred['name'] = 'updated' # Make Bob try to update Fred's user account. @@ -62,7 +61,7 @@ def test_user_update_user_cannot_update_another_user(self): def test_user_update_user_can_update_herself(self): '''Users should be authorized to update their own accounts.''' - user = helpers.call_action('user_create', **data.typical_user()) + user = factories.User() context = {'user': user['name']} user['name'] = 'updated' diff --git a/ckan/new_tests/logic/test_validators.py b/ckan/new_tests/logic/test_validators.py index 43264300bc8..a74cec28069 100644 --- a/ckan/new_tests/logic/test_validators.py +++ b/ckan/new_tests/logic/test_validators.py @@ -8,7 +8,205 @@ import nose.tools import ckan.new_tests.helpers as helpers -import ckan.new_tests.data as test_data +import ckan.new_tests.factories as factories + + +def returns_arg(message): + '''A decorator that tests that the decorated function returns the argument + that it is called with, unmodified. + + :param message: the message that will be printed if the function doesn't + return the same argument that it was called with and the assert fails + :type message: string + + Usage: + + @returns_arg('user_name_validator() should return the same arg that ' + 'it is called with, when called with a valid arg') + def call_validator(*args, **kwargs): + return validators.user_name_validator(*args, **kwargs) + call_validator(key, data, errors) + + ''' + def decorator(function): + def call_and_assert(arg, context=None): + if context is None: + context = {} + result = function(arg, context=context) + assert result == arg, message + return result + return call_and_assert + return decorator + + +def returns_None(message): + '''A decorator that asserts that the decorated function returns None. + + :param message: the message that will be printed if the function doesn't + return None and the assert fails + :type message: string + + Usage: + + @returns_None('user_name_validator() should return None when given ' + 'valid input') + def call_validator(*args, **kwargs): + return validators.user_name_validator(*args, **kwargs) + call_validator(key, data, errors) + + ''' + def decorator(function): + def call_and_assert(*args, **kwargs): + result = function(*args, **kwargs) + assert result is None, message + return result + return call_and_assert + return decorator + + +def raises_Invalid(function): + '''A decorator that asserts that the decorated function raises + dictization_functions.Invalid. + + Usage: + + @raises_Invalid + def call_validator(*args, **kwargs): + return validators.user_name_validator(*args, **kwargs) + call_validator(key, data, errors) + + ''' + def call_and_assert(*args, **kwargs): + import ckan.lib.navl.dictization_functions as df + with nose.tools.assert_raises(df.Invalid): + return function(*args, **kwargs) + return call_and_assert + + +def does_not_modify_data_dict(message): + '''A decorator that asserts that the decorated validator doesn't modify + its `data` dict param. + + :param message: the message that will be printed if the function does + modify the data dict and the assert fails + :type message: string + + Usage: + + @does_not_modify_data_dict('user_name_validator() should not modify ' + 'the data dict') + def call_validator(*args, **kwargs): + return validators.user_name_validator(*args, **kwargs) + call_validator(key, data, errors) + + ''' + def decorator(validator): + def call_and_assert(key, data, errors, context=None): + if context is None: + context = {} + # Make a copy of the data dict so we can assert against it later. + original_data_dict = copy.deepcopy(data) + result = validator(key, data, errors, context=context) + assert data == original_data_dict, message + return result + return call_and_assert + return decorator + + +def does_not_modify_errors_dict(message): + '''A decorator that asserts that the decorated validator doesn't modify its + `errors` dict param. + + :param message: the message that will be printed if the function does + modify the errors dict and the assert fails + :type message: string + + Usage: + + @does_not_modify_errors_dict('user_name_validator() should not modify ' + 'the errors dict') + def call_validator(*args, **kwargs): + return validators.user_name_validator(*args, **kwargs) + call_validator(key, data, errors) + + ''' + def decorator(validator): + def call_and_assert(key, data, errors, context=None): + if context is None: + context = {} + # Make a copy of the errors dict so we can assert against it later. + original_errors_dict = copy.deepcopy(errors) + result = validator(key, data, errors, context=context) + assert errors == original_errors_dict, message + return result + return call_and_assert + return decorator + + +def does_not_modify_other_keys_in_errors_dict(message): + '''A decorator that asserts that the decorated validator doesn't add, + modify the value of, or remove any other keys from its `errors` dict param. + + The function *may* modify its own errors `key`. + + :param message: the message that will be printed if the function does + modify another key in the errors dict and the assert fails + :type message: string + + Usage: + + @does_not_modify_other_keys_in_errors_dict('user_name_validator() ' + 'should not modify other keys in the errors dict') + def call_validator(*args, **kwargs): + return validators.user_name_validator(*args, **kwargs) + call_validator(key, data, errors) + + ''' + def decorator(validator): + def call_and_assert(key, data, errors, context=None): + if context is None: + context = {} + # Make a copy of the errors dict so we can assert against it later. + original_errors_dict = copy.deepcopy(errors) + result = validator(key, data, errors, context=context) + # Copy the errors dict because we don't want to modify it. + errors = copy.deepcopy(errors) + errors[key] = [] + assert errors == original_errors_dict, message + return result + return call_and_assert + return decorator + + +def adds_message_to_errors_dict(error_message, message): + '''A decorator that asserts the the decorated validator adds a given + error message to the `errors` dict. + + :param error_message: the error message that the validator is expected to + add to the `errors` dict + :type error_message: string + + :param message: the message that will be printed if the function doesn't + add the right error message to the errors dict, and the assert fails + :type message: string + + Usage: + + @adds_message_to_errors_dict('That login name is not available.', + 'user_name_validator() should add to the errors dict when called ' + 'with a user name with already exists') + def call_validator(*args, **kwargs): + return validators.user_name_validator(*args, **kwargs) + call_validator(key, data, errors) + + ''' + def decorator(validator): + def call_and_assert(key, data, errors, context): + result = validator(key, data, errors, context) + assert errors[key] == [error_message], message + return result + return call_and_assert + return decorator def returns_arg(message): @@ -333,9 +531,9 @@ def test_user_name_validator_with_non_string_value(self): key = ('name',) for non_string_value in non_string_values: - data = test_data.validator_data_dict() + data = factories.validator_data_dict() data[key] = non_string_value - errors = test_data.validator_errors_dict() + errors = factories.validator_errors_dict() errors[key] = [] @does_not_modify_errors_dict('user_name_validator() should not ' @@ -359,10 +557,10 @@ def test_user_name_validator_with_a_name_that_already_exists(self): # the same user name in the database. mock_model = mock.MagicMock() - data = test_data.validator_data_dict() + data = factories.validator_data_dict() key = ('name',) data[key] = 'user_name' - errors = test_data.validator_errors_dict() + errors = factories.validator_errors_dict() errors[key] = [] @does_not_modify_other_keys_in_errors_dict('user_name_validator() ' @@ -383,10 +581,10 @@ def test_user_name_validator_successful(self): import ckan.logic.validators as validators - data = test_data.validator_data_dict() + data = factories.validator_data_dict() key = ('name',) data[key] = 'new_user_name' - errors = test_data.validator_errors_dict() + errors = factories.validator_errors_dict() errors[key] = [] # Mock ckan.model.