diff --git a/app.py b/app.py index 96a27ef..26f16af 100644 --- a/app.py +++ b/app.py @@ -12,6 +12,7 @@ import json import util +from entity_validator import validate startup_timestamp = datetime.now() @@ -74,12 +75,9 @@ class ValidateHandler(tornado.web.RequestHandler, metaclass=ABCMeta): entity = self.request.body.decode() - # TODO call validator - validation_result = { - "valid": "true" - } + validation_result = validate(json.loads(entity)) - self.write(validation_result) + self.write(json.dumps(validation_result)) self.finish() diff --git a/entity_validator.py b/entity_validator.py new file mode 100644 index 0000000..6bbb497 --- /dev/null +++ b/entity_validator.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +from cerberus import Validator +from validation_functions import * + +def validate(entity): + schema_fin={ + 'bic': { + 'type': 'string', + 'required': True, + 'check_with': valid_bic}, + 'iban': { + 'type': 'string', + 'required': True, + 'check_with': valid_iban}, + 'issuance': { + 'type': 'string', + 'required': True, + 'check_with': iso_date}, + 'reference': {'type': 'string'}, + 'scan-sepa-mandate': {'type': 'string'}, + 'holder': {'type': 'string'}} + + schema_membership={ + 'bis': { + 'type': 'string', + 'oneof': [{'check_with': iso_date},{'empty': True}]}, + 'mitgliedsbeitrag': { + 'type': 'string', + 'check_with': valid_money_amount}, + 'scan-antrag': {'type': 'string'}, + 'schliessberechtigung': { + 'type': 'string', + 'allowed': ['Ja', 'Nein', 'J', 'N', 'j', 'n', 'y', 'Y']}, + 'spendenbeitrag': { + 'type': 'string', + 'check_with': valid_money_amount}, + 'status': { + 'type': 'string', + 'required': True, + 'allowed': ['V', 'E', 'F']}, + 'von': { + 'type': 'string', + 'required': True, + 'check_with': iso_date}} + + schema_base={ + 'address_code': { + 'type': 'string'}, + 'address_country': { + 'type': 'string'}, + 'address_label': { + 'type': 'string'}, + 'address_locality': { + 'type': 'string'}, + 'address_region': { + 'type': 'string'}, + 'address_street': { + 'type': 'string'}, + 'birth_date': { + 'type': 'string', + 'required': True, + 'check_with': iso_date}, + 'birth_location': { + 'type': 'string'}, + 'email': { + 'type': 'string', + 'required': True, + 'check_with': valid_email}, + 'fullname': { + 'type': 'string', + 'required': True}, + 'nickname': { + 'type': 'string'}, + 'pgp-key': { + 'type': 'string'}, + 'ssh-key': { + 'type': 'string'}} + + schema = { + 'finanzdaten': { + 'type': 'dict', + 'required': True, + 'schema': schema_fin}, + 'mitgliederdaten': { + 'type': 'dict', + 'required': True, + 'schema': schema_membership}, + 'stammdaten': { + 'type': 'dict', + 'required': True, + 'schema': schema_base}, + 'id': { + 'type': 'string', + 'required': True, + 'regex': '^[a-f0-9]{5}$'}, + 'timestamp': { + 'type': 'string', + 'required': True} + } + + v = Validator() + result = { + 'valid': v.validate(entity, schema), + 'errors': v.errors} + return result \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 757931f..04e43f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ tornado==6.0.4 isodate==0.6.0 pytest==5.4.1 +schwifty==2020.11.0 +Cerberus==1.3.2 +validator-collection==1.5.0 \ No newline at end of file diff --git a/test.py b/test.py index ee268d3..708df04 100644 --- a/test.py +++ b/test.py @@ -54,9 +54,72 @@ class TestValidation(tornado.testing.AsyncHTTPTestCase): validation_result = json.loads(response.body.decode()) + self.assertIn('valid', validation_result, "Key 'valid' expected in validation result") + self.assertFalse(validation_result['valid'], "Validation result is expected to be valid==false") + + def test_valid_entity(self): + with open('test_cases/valid/valid.json', 'r') as f: + entity_file = json.load(f) + + response = self.fetch('/v0/validate', + method='POST', + body=json.dumps(entity_file)) + + self.assertEqual(200, response.code, "Validation must always return 200") + + validation_result = json.loads(response.body.decode()) + self.assertIn('valid', validation_result, "Key 'valid' expected in validation result") self.assertTrue(validation_result['valid'], "Validation result is expected to be valid==true") + def test_invalid_iban(self): + with open('test_cases/invalid/invalid_iban.json', 'r') as f: + entity_file = json.load(f) + + response = self.fetch('/v0/validate', + method='POST', + body=json.dumps(entity_file)) + + self.assertEqual(200, response.code, "Validation must always return 200") + + validation_result = json.loads(response.body.decode()) + + self.assertIn('valid', validation_result, "Key 'valid' expected in validation result") + self.assertFalse(validation_result['valid'], "Validation result is expected to be valid==false") + + def test_missing_id(self): + with open('test_cases/invalid/missing_id.json', 'r') as f: + entity_file = json.load(f) + + response = self.fetch('/v0/validate', + method='POST', + body=json.dumps(entity_file)) + + self.assertEqual(200, response.code, "Validation must always return 200") + + validation_result = json.loads(response.body.decode()) + + self.assertIn('valid', validation_result, "Key 'valid' expected in validation result") + self.assertFalse(validation_result['valid'], "Validation result is expected to be valid==false") + self.assertIn('id', validation_result['errors']) + + def test_invalid_id(self): + with open('test_cases/invalid/invalid_id.json', 'r') as f: + entity_file = json.load(f) + + response = self.fetch('/v0/validate', + method='POST', + body=json.dumps(entity_file)) + + self.assertEqual(200, response.code, + "Validation must always return 200") + + validation_result = json.loads(response.body.decode()) + + self.assertIn('valid', validation_result, "Key 'valid' expected in validation result") + self.assertFalse(validation_result['valid'], "Validation result is expected to be valid==false") + self.assertIn('id', validation_result['errors']) + if __name__ == "__main__": unittest.main() diff --git a/test_cases/invalid/invalid_iban.json b/test_cases/invalid/invalid_iban.json new file mode 100644 index 0000000..c1357ee --- /dev/null +++ b/test_cases/invalid/invalid_iban.json @@ -0,0 +1,36 @@ +{ + "finanzdaten": { + "bic": "PBNKDEFFXXX", + "holder": "", + "iban": "DX89370400440532013000", + "issuance": "2012-01-01", + "reference": "0042", + "scan-sepa-mandate": "" + }, + "mitgliederdaten": { + "bis": "", + "mitgliedsbeitrag": "30", + "scan-antrag": "", + "schliessberechtigung": "Ja", + "spendenbeitrag": "0", + "status": "V", + "von": "2012-01-01" + }, + "stammdaten": { + "address_code": "39104", + "address_country": "DE", + "address_label": "Max Hackerberg\nLeibnizstr. 32\n39104 Magdeburg", + "address_locality": "Magdeburg", + "address_region": "", + "address_street": "Leibnizstr. 32", + "birth_date": "1970-01-01", + "birth_location": "Hackstadt", + "email": "max.hackerberg@netz39.de", + "fullname": "Max Hackerberg", + "nickname": "maxH", + "pgp-key": "", + "ssh-key": "" + }, + "timestamp": "2020-03-25T23:58:11", + "id": "6af68" +} \ No newline at end of file diff --git a/test_cases/invalid/invalid_id.json b/test_cases/invalid/invalid_id.json new file mode 100644 index 0000000..ed6e759 --- /dev/null +++ b/test_cases/invalid/invalid_id.json @@ -0,0 +1,36 @@ +{ + "finanzdaten": { + "bic": "PBNKDEFFXXX", + "holder": "", + "iban": "DE89370400440532013000", + "issuance": "2012-01-01", + "reference": "0042", + "scan-sepa-mandate": "" + }, + "mitgliederdaten": { + "bis": "", + "mitgliedsbeitrag": "30", + "scan-antrag": "", + "schliessberechtigung": "Ja", + "spendenbeitrag": "0", + "status": "V", + "von": "2012-01-01" + }, + "stammdaten": { + "address_code": "39104", + "address_country": "DE", + "address_label": "Max Hackerberg\nLeibnizstr. 32\n39104 Magdeburg", + "address_locality": "Magdeburg", + "address_region": "", + "address_street": "Leibnizstr. 32", + "birth_date": "1970-01-01", + "birth_location": "Hackstadt", + "email": "max.hackerberg@netz39.de", + "fullname": "Max Hackerberg", + "nickname": "maxH", + "pgp-key": "", + "ssh-key": "" + }, + "timestamp": "2020-03-25T23:58:11", + "id": "6ay68" +} \ No newline at end of file diff --git a/test_cases/invalid/missing_id.json b/test_cases/invalid/missing_id.json new file mode 100644 index 0000000..21beb16 --- /dev/null +++ b/test_cases/invalid/missing_id.json @@ -0,0 +1,36 @@ +{ + "finanzdaten": { + "bic": "PBNKDEFFXXX", + "holder": "", + "iban": "DE89370400440532013000", + "issuance": "2012-01-01", + "reference": "0042", + "scan-sepa-mandate": "" + }, + "mitgliederdaten": { + "bis": "", + "mitgliedsbeitrag": "30", + "scan-antrag": "", + "schliessberechtigung": "Ja", + "spendenbeitrag": "0", + "status": "V", + "von": "2012-01-01" + }, + "stammdaten": { + "address_code": "39104", + "address_country": "DE", + "address_label": "Max Hackerberg\nLeibnizstr. 32\n39104 Magdeburg", + "address_locality": "Magdeburg", + "address_region": "", + "address_street": "Leibnizstr. 32", + "birth_date": "1970-01-01", + "birth_location": "Hackstadt", + "email": "max.hackerberg@netz39.de", + "fullname": "Max Hackerberg", + "nickname": "maxH", + "pgp-key": "", + "ssh-key": "" + }, + "timestamp": "2020-03-25T23:58:11", + "id": "" +} \ No newline at end of file diff --git a/test_cases/valid/valid.json b/test_cases/valid/valid.json new file mode 100644 index 0000000..d11361e --- /dev/null +++ b/test_cases/valid/valid.json @@ -0,0 +1,36 @@ +{ + "finanzdaten": { + "bic": "PBNKDEFFXXX", + "holder": "", + "iban": "DE89370400440532013000", + "issuance": "2012-01-01", + "reference": "0042", + "scan-sepa-mandate": "" + }, + "mitgliederdaten": { + "bis": "", + "mitgliedsbeitrag": "30", + "scan-antrag": "", + "schliessberechtigung": "Ja", + "spendenbeitrag": "0", + "status": "V", + "von": "2012-01-01" + }, + "stammdaten": { + "address_code": "39104", + "address_country": "DE", + "address_label": "Max Hackerberg\nLeibnizstr. 32\n39104 Magdeburg", + "address_locality": "Magdeburg", + "address_region": "", + "address_street": "Leibnizstr. 32", + "birth_date": "1970-01-01", + "birth_location": "Hackstadt", + "email": "max.hackerberg@netz39.de", + "fullname": "Max Hackerberg", + "nickname": "maxH", + "pgp-key": "", + "ssh-key": "" + }, + "timestamp": "2020-03-25T23:58:11", + "id": "6af68" +} \ No newline at end of file diff --git a/validation_functions.py b/validation_functions.py new file mode 100644 index 0000000..e31d498 --- /dev/null +++ b/validation_functions.py @@ -0,0 +1,44 @@ +from schwifty import IBAN, BIC +import datetime +from validator_collection import validators, errors + + +def valid_iban(field, value, error): + try: + IBAN(value) + return True + except ValueError: + error(field, 'not a valid IBAN') + + +def valid_bic(field, value, error): + try: + BIC(value) + return True + except ValueError: + error(field, 'not a valid BIC') + + +def iso_date(field, value, error): + try: + datetime.datetime.strptime(value, "%Y-%m-%d") + return True + except ValueError: + error(field, 'not a valid ISO 8601 date') + + +def valid_money_amount(field, value, error): + try: + # value is string, check formatting by parsing as float + float(value) + return True + except (ValueError, TypeError): + error(field, 'not a valid money value') + + +def valid_email(field, value, error): + try: + validators.email(value) + return True + except errors.InvalidEmailError: + error(field, 'not a valid email')