Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions minfraud/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,9 @@ def _uri(s):
'last_4_digits': _credit_card_last_4,
},
Required('device'): {
'accept_language': _unicode_or_printable_ascii, Required('ip_address'):
_ip_address, 'user_agent': _unicode_or_printable_ascii
'accept_language': _unicode_or_printable_ascii,
Required('ip_address'): _ip_address,
'user_agent': _unicode_or_printable_ascii
},
'email': {'address': _email_or_md5,
'domain': _hostname, },
Expand Down
21 changes: 16 additions & 5 deletions minfraud/webservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,17 @@ def score(self, transaction, validate=True):

def _response_for(self, path, model_class, request, validate):
"""Send request and create response object"""
cleaned_request = self._copy_and_clean(request)
if validate:
try:
validate_transaction(request)
validate_transaction(cleaned_request)
except MultipleInvalid as ex:
raise InvalidRequestError(
"Invalid transaction data: {0}".format(ex))
uri = '/'.join([self._base_uri, path])
response = requests.post(
uri,
json=request,
json=cleaned_request,
auth=(self._user_id, self._license_key),
headers=
{'Accept': 'application/json',
Expand All @@ -113,6 +114,16 @@ def _response_for(self, path, model_class, request, validate):
else:
self._handle_error(response, uri)

def _copy_and_clean(self, data):
"""This returns a copy of the data structure with Nones removed"""
if isinstance(data, dict):
return dict((k, self._copy_and_clean(v)) for (k, v) in data.items()
if v is not None)
elif isinstance(data, (list, set, tuple)):
return [self._copy_and_clean(x) for x in data if x is not None]
else:
return data

def _user_agent(self):
"""Create User-Agent header"""
return 'minFraud-API/%s %s' % (__version__, default_user_agent())
Expand Down Expand Up @@ -154,9 +165,9 @@ def _handle_4xx_status(self, response, status, uri):
except ValueError:
raise HTTPError(
'Received a {status:d} error but it did not include'
' the expected JSON body: {content}'
.format(status=status,
content=response.content), status, uri)
' the expected JSON body: {content}'.format(
status=status,
content=response.content), status, uri)
else:
if 'code' in body and 'error' in body:
self._handle_web_service_error(body.get('error'),
Expand Down
28 changes: 26 additions & 2 deletions tests/test_webservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ def test_200(self):
if self.type == 'insights':
self.assertEqual('United Kingdom', model.ip_address.country.name)

def test_200_on_request_with_nones(self):
model = self.create_success(
request={
'device': {
'ip_address': '81.2.69.160',
'accept_language': None
},
'event': {
'shop_id': None
},
'shopping_cart': [{
'category': None,
'quantity': 2,
}, None],
})
response = self.response
self.assertEqual(0.01, model.risk_score)

def test_200_with_locales(self):
locales = ('fr', )
client = Client(42, 'abcdef123456', locales=locales)
Expand Down Expand Up @@ -142,7 +160,11 @@ def create_error(self, mock, status_code=400, text='', headers=None):
return getattr(self.client, self.type)(self.full_request)

@requests_mock.mock()
def create_success(self, mock, text=None, headers=None, client=None):
def create_success(self, mock,
text=None,
headers=None,
client=None,
request=None):
if headers is None:
headers = {
'Content-Type':
Expand All @@ -158,7 +180,9 @@ def create_success(self, mock, text=None, headers=None, client=None):
headers=headers)
if client is None:
client = self.client
return getattr(client, self.type)(self.full_request)
if request is None:
request = self.full_request
return getattr(client, self.type)(request)


class TestInsights(BaseTest, unittest.TestCase):
Expand Down