Merge pull request 'validation endpoint implementation' (#7) from experiments-cerberus into master
This commit is contained in:
commit
619fd089e3
9 changed files with 363 additions and 5 deletions
8
app.py
8
app.py
|
@ -12,6 +12,7 @@ import json
|
||||||
|
|
||||||
import util
|
import util
|
||||||
|
|
||||||
|
from entity_validator import validate
|
||||||
|
|
||||||
startup_timestamp = datetime.now()
|
startup_timestamp = datetime.now()
|
||||||
|
|
||||||
|
@ -74,12 +75,9 @@ class ValidateHandler(tornado.web.RequestHandler, metaclass=ABCMeta):
|
||||||
|
|
||||||
entity = self.request.body.decode()
|
entity = self.request.body.decode()
|
||||||
|
|
||||||
# TODO call validator
|
validation_result = validate(json.loads(entity))
|
||||||
validation_result = {
|
|
||||||
"valid": "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
self.write(validation_result)
|
self.write(json.dumps(validation_result))
|
||||||
self.finish()
|
self.finish()
|
||||||
|
|
||||||
|
|
||||||
|
|
106
entity_validator.py
Normal file
106
entity_validator.py
Normal file
|
@ -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
|
|
@ -1,3 +1,6 @@
|
||||||
tornado==6.0.4
|
tornado==6.0.4
|
||||||
isodate==0.6.0
|
isodate==0.6.0
|
||||||
pytest==5.4.1
|
pytest==5.4.1
|
||||||
|
schwifty==2020.11.0
|
||||||
|
Cerberus==1.3.2
|
||||||
|
validator-collection==1.5.0
|
63
test.py
63
test.py
|
@ -54,9 +54,72 @@ class TestValidation(tornado.testing.AsyncHTTPTestCase):
|
||||||
|
|
||||||
validation_result = json.loads(response.body.decode())
|
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.assertIn('valid', validation_result, "Key 'valid' expected in validation result")
|
||||||
self.assertTrue(validation_result['valid'], "Validation result is expected to be valid==true")
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
36
test_cases/invalid/invalid_iban.json
Normal file
36
test_cases/invalid/invalid_iban.json
Normal file
|
@ -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"
|
||||||
|
}
|
36
test_cases/invalid/invalid_id.json
Normal file
36
test_cases/invalid/invalid_id.json
Normal file
|
@ -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"
|
||||||
|
}
|
36
test_cases/invalid/missing_id.json
Normal file
36
test_cases/invalid/missing_id.json
Normal file
|
@ -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": ""
|
||||||
|
}
|
36
test_cases/valid/valid.json
Normal file
36
test_cases/valid/valid.json
Normal file
|
@ -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"
|
||||||
|
}
|
44
validation_functions.py
Normal file
44
validation_functions.py
Normal file
|
@ -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')
|
Loading…
Reference in a new issue