diff --git a/bigchaindb/backend/schema.py b/bigchaindb/backend/schema.py index f6ce466f..4f2ebd6c 100644 --- a/bigchaindb/backend/schema.py +++ b/bigchaindb/backend/schema.py @@ -16,10 +16,17 @@ import logging import bigchaindb from bigchaindb.backend.connection import connect +from bigchaindb.common.exceptions import ValidationError +from bigchaindb.common.utils import validate_all_values_for_key logger = logging.getLogger(__name__) TABLES = ('bigchain', 'backlog', 'votes', 'assets') +VALID_LANGUAGES = ('danish', 'dutch', 'english', 'finnish', 'french', 'german', + 'hungarian', 'italian', 'norwegian', 'portuguese', 'romanian', + 'russian', 'spanish', 'swedish', 'turkish', 'none', + 'da', 'nl', 'en', 'fi', 'fr', 'de', 'hu', 'it', 'nb', 'pt', + 'ro', 'ru', 'es', 'sv', 'tr') @singledispatch @@ -99,3 +106,44 @@ def init_database(connection=None, dbname=None): create_database(connection, dbname) create_tables(connection, dbname) create_indexes(connection, dbname) + + +def validate_language_key(obj, key): + """Validate all nested "language" key in `obj`. + + Args: + obj (dict): dictionary whose "language" key is to be validated. + + Returns: + None: validation successful + + Raises: + ValidationError: will raise exception in case language is not valid. + """ + backend = bigchaindb.config['database']['backend'] + + if backend == 'mongodb': + data = obj.get(key, {}) + if isinstance(data, dict): + validate_all_values_for_key(data, 'language', validate_language) + + +def validate_language(value): + """Check if `value` is a valid language. + https://docs.mongodb.com/manual/reference/text-search-languages/ + + Args: + value (str): language to validated + + Returns: + None: validation successful + + Raises: + ValidationError: will raise exception in case language is not valid. + """ + if value not in VALID_LANGUAGES: + error_str = ('MongoDB does not support text search for the ' + 'language "{}". If you do not understand this error ' + 'message then please rename key/field "language" to ' + 'something else like "lang".').format(value) + raise ValidationError(error_str) diff --git a/bigchaindb/common/utils.py b/bigchaindb/common/utils.py index e472f380..9ad448f5 100644 --- a/bigchaindb/common/utils.py +++ b/bigchaindb/common/utils.py @@ -52,53 +52,73 @@ def deserialize(data): def validate_txn_obj(obj_name, obj, key, validation_fun): - """Validates value associated to `key` in `obj` by applying - `validation_fun`. + """Validate value of `key` in `obj` using `validation_fun`. Args: obj_name (str): name for `obj` being validated. - obj (dict): dictonary object. + obj (dict): dictionary object. key (str): key to be validated in `obj`. validation_fun (function): function used to validate the value of `key`. Returns: - None: indicates validation successfull + None: indicates validation successful Raises: - ValidationError: `validation_fun` will raise this error on failure + ValidationError: `validation_fun` will raise exception on failure """ backend = bigchaindb.config['database']['backend'] if backend == 'mongodb': - data = obj.get(key, {}) or {} - validate_all_keys(obj_name, data, validation_fun) + data = obj.get(key, {}) + if isinstance(data, dict): + validate_all_keys(obj_name, data, validation_fun) def validate_all_keys(obj_name, obj, validation_fun): - """Validates all (nested) keys in `obj` by using `validation_fun` + """Validate all (nested) keys in `obj` by using `validation_fun`. Args: obj_name (str): name for `obj` being validated. - obj (dict): dictonary object. + obj (dict): dictionary object. validation_fun (function): function used to validate the value of `key`. Returns: - None: indicates validation successfull + None: indicates validation successful Raises: ValidationError: `validation_fun` will raise this error on failure """ for key, value in obj.items(): validation_fun(obj_name, key) - if type(value) is dict: + if isinstance(value, dict): validate_all_keys(obj_name, value, validation_fun) - return + + +def validate_all_values_for_key(obj, key, validation_fun): + """Validate value for all (nested) occurrence of `key` in `obj` + using `validation_fun`. + + Args: + obj (dict): dictionary object. + key (str): key whose value is to be validated. + validation_fun (function): function used to validate the value + of `key`. + + Raises: + ValidationError: `validation_fun` will raise this error on failure + """ + for vkey, value in obj.items(): + if vkey == key: + validation_fun(value) + elif isinstance(value, dict): + validate_all_values_for_key(value, key, validation_fun) def validate_key(obj_name, key): - """Check if `key` contains ".", "$" or null characters + """Check if `key` contains ".", "$" or null characters. + https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names Args: @@ -106,13 +126,13 @@ def validate_key(obj_name, key): key (str): key to validated Returns: - None: indicates validation successfull + None: validation successful Raises: - ValidationError: raise execption incase of regex match. + ValidationError: will raise exception in case of regex match. """ if re.search(r'^[$]|\.|\x00', key): error_str = ('Invalid key name "{}" in {} object. The ' 'key name cannot contain characters ' '".", "$" or null characters').format(key, obj_name) - raise ValidationError(error_str) from ValueError() + raise ValidationError(error_str) diff --git a/bigchaindb/models.py b/bigchaindb/models.py index 1ecd964e..8e7a6bde 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -11,6 +11,7 @@ from bigchaindb.common.transaction import Transaction from bigchaindb.common.utils import (gen_timestamp, serialize, validate_txn_obj, validate_key) from bigchaindb.common.schema import validate_transaction_schema +from bigchaindb.backend.schema import validate_language_key class Transaction(Transaction): @@ -87,6 +88,7 @@ class Transaction(Transaction): validate_transaction_schema(tx_body) validate_txn_obj('asset', tx_body['asset'], 'data', validate_key) validate_txn_obj('metadata', tx_body, 'metadata', validate_key) + validate_language_key(tx_body['asset'], 'data') return super().from_dict(tx_body) @classmethod diff --git a/tests/web/test_transactions.py b/tests/web/test_transactions.py index e5034697..ab01357a 100644 --- a/tests/web/test_transactions.py +++ b/tests/web/test_transactions.py @@ -47,6 +47,47 @@ def test_post_create_transaction_endpoint(b, client): assert res.json['outputs'][0]['public_keys'][0] == user_pub +@pytest.mark.parametrize("nested", [False, True]) +@pytest.mark.parametrize("language,expected_status_code", [ + ('danish', 202), ('dutch', 202), ('english', 202), ('finnish', 202), + ('french', 202), ('german', 202), ('hungarian', 202), ('italian', 202), + ('norwegian', 202), ('portuguese', 202), ('romanian', 202), ('none', 202), + ('russian', 202), ('spanish', 202), ('swedish', 202), ('turkish', 202), + ('da', 202), ('nl', 202), ('en', 202), ('fi', 202), ('fr', 202), + ('de', 202), ('hu', 202), ('it', 202), ('nb', 202), ('pt', 202), + ('ro', 202), ('ru', 202), ('es', 202), ('sv', 202), ('tr', 202), + ('any', 400) +]) +@pytest.mark.language +@pytest.mark.bdb +def test_post_create_transaction_with_language(b, client, nested, language, + expected_status_code): + from bigchaindb.models import Transaction + from bigchaindb.backend.mongodb.connection import MongoDBConnection + + if isinstance(b.connection, MongoDBConnection): + user_priv, user_pub = crypto.generate_key_pair() + lang_obj = {'language': language} + + if nested: + asset = {'root': lang_obj} + else: + asset = lang_obj + + tx = Transaction.create([user_pub], [([user_pub], 1)], + asset=asset) + tx = tx.sign([user_priv]) + res = client.post(TX_ENDPOINT, data=json.dumps(tx.to_dict())) + assert res.status_code == expected_status_code + if res.status_code == 400: + expected_error_message = ( + 'Invalid transaction (ValidationError): MongoDB does not support ' + 'text search for the language "{}". If you do not understand this ' + 'error message then please rename key/field "language" to something ' + 'else like "lang".').format(language) + assert res.json['message'] == expected_error_message + + @pytest.mark.parametrize("field", ['asset', 'metadata']) @pytest.mark.parametrize("value,err_key,expected_status_code", [ ({'bad.key': 'v'}, 'bad.key', 400),