Skip to content

(For Jintae or Gary) Add session level decision. #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 13, 2018
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
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
3.2.0.0 2018-02-12
==================

- Add session level decisions in Apply Decisions APIs.
- Add support for filtering get decisions by entity type session.

3.1.0.0 2017-01-17
==================

Expand Down
51 changes: 47 additions & 4 deletions sift/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ def get_decisions(self, entity_type, limit=None, start_from=None, abuse_types=No
"""Get decisions available to customer

Args:
entity_type: only return decisions applicable to entity type {USER|ORDER}
entity_type: only return decisions applicable to entity type {USER|ORDER|SESSION}
limit: number of query results (decisions) to return [optional, default: 100]
start_from: result set offset for use in pagination [optional, default: 0]
abuse_types: comma-separated list of abuse_types used to filter returned decisions (optional)
Expand All @@ -334,8 +334,8 @@ def get_decisions(self, entity_type, limit=None, start_from=None, abuse_types=No
params = {}

if not isinstance(entity_type, self.UNICODE_STRING) or len(entity_type.strip()) == 0 \
or entity_type.lower() not in ['user', 'order']:
raise ApiException("entity_type must be one of {user, order}")
or entity_type.lower() not in ['user', 'order', 'session']:
raise ApiException("entity_type must be one of {user, order, session}")

params['entity_type'] = entity_type

Expand Down Expand Up @@ -395,7 +395,7 @@ def apply_order_decision(self, user_id, order_id, properties, timeout=None):
user_id: id of user
order_id: id of order
properties:
decision_id: decision to apply to user
decision_id: decision to apply to order
source: {one of MANUAL_REVIEW | AUTOMATED_RULE | CHARGEBACK}
analyst: id or email, required if 'source: MANUAL_REVIEW'
description: free form text (optional)
Expand Down Expand Up @@ -502,6 +502,46 @@ def get_order_decisions(self, order_id, timeout=None):
raise ApiException(str(e))


def apply_session_decision(self, user_id, session_id, properties, timeout=None):
"""Apply decision to session

Args:
user_id: id of user
session_id: id of session
properties:
decision_id: decision to apply to session
source: {one of MANUAL_REVIEW | AUTOMATED_RULE | CHARGEBACK}
analyst: id or email, required if 'source: MANUAL_REVIEW'
description: free form text (optional)
time: in millis when decision was applied (optional)
Returns
A sift.client.Response object if the call succeeded, else raises an ApiException
"""

if timeout is None:
timeout = self.timeout


if session_id is None or not isinstance(session_id, self.UNICODE_STRING) or \
len(session_id.strip()) == 0:
raise ApiException("session_id must be a string")

self._validate_apply_decision_request(properties, user_id)

try:
return Response(requests.post(
self._session_apply_decisions_url(self.account_id, user_id, session_id),
data=json.dumps(properties),
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
headers={'Content-type': 'application/json',
'Accept': '*/*',
'User-Agent': self._user_agent()},
timeout=timeout))

except requests.exceptions.RequestException as e:
raise ApiException(str(e))


def _user_agent(self):
return 'SiftScience/v%s sift-python/%s' % (sift.version.API_VERSION, sift.version.VERSION)

Expand Down Expand Up @@ -529,6 +569,9 @@ def _order_decisions_url(self, account_id, order_id):
def _order_apply_decisions_url(self, account_id, user_id, order_id):
return API3_URL + '/v3/accounts/%s/users/%s/orders/%s/decisions' % (account_id, user_id, order_id)

def _session_apply_decisions_url(self, account_id, user_id, session_id):
return API3_URL + '/v3/accounts/%s/users/%s/sessions/%s/decisions' % (account_id, user_id, session_id)

class Response(object):

HTTP_CODES_WITHOUT_BODY = [204, 304]
Expand Down
2 changes: 1 addition & 1 deletion sift/version.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
VERSION = '3.1.0.0'
VERSION = '3.2.0.0'
API_VERSION = '204'
90 changes: 89 additions & 1 deletion tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,52 @@ def test_get_decisions(self):
assert(response.is_ok())
assert(response.body['data'][0]['id'] == 'block_user')

def test_get_decisions_entity_session(self):
mock_response = mock.Mock()
get_decisions_response_json = \
'{' \
'"data": [' \
'{' \
'"id": "block_session",' \
'"name" : "Block session",' \
'"description": "session has problems",' \
'"entity_type": "session",' \
'"abuse_type": "legacy",' \
'"category": "block",' \
'"webhook_url": "http://web.hook",' \
'"created_at": "1468005577348",' \
'"created_by": "admin@biz.com",' \
'"updated_at": "1469229177756",' \
'"updated_by": "analyst@biz.com"' \
'}' \
'],' \
'"has_more": "true",' \
'"next_ref": "v3/accounts/accountId/decisions"' \
'}'

mock_response.content = get_decisions_response_json
mock_response.json.return_value = json.loads(mock_response.content)
mock_response.status_code = 200
mock_response.headers = response_with_data_header()
with mock.patch('requests.get') as mock_get:
mock_get.return_value = mock_response

response = self.sift_client.get_decisions(entity_type="session",
limit=10,
start_from=None,
abuse_types="account_takeover",
timeout=3)
mock_get.assert_called_with(
'https://api3.siftscience.com/v3/accounts/ACCT/decisions',
headers=mock.ANY,
auth=mock.ANY,
params={'entity_type':'session','limit':10,'abuse_types':'account_takeover'},
timeout=3)

assert(isinstance(response, sift.client.Response))
assert(response.is_ok())
assert(response.body['data'][0]['id'] == 'block_session')

def test_apply_decision_to_user_ok(self):
user_id = '54321'
mock_response = mock.Mock()
Expand Down Expand Up @@ -394,12 +440,18 @@ def test_validate_no_user_id_string_fails(self):
except Exception as e:
assert(isinstance(e, sift.client.ApiException))

def test_apply_decision_to_order(self):
def test_apply_decision_to_order_fails_with_no_order_id(self):
try:
self.sift_client.apply_order_decision("user_id", None, {})
except Exception as e:
assert(isinstance(e, sift.client.ApiException))

def test_apply_decision_to_session_fails_with_no_session_id(self):
try:
self.sift_client.apply_session_decision("user_id", None, {})
except Exception as e:
assert(isinstance(e, sift.client.ApiException))

def test_validate_apply_decision_request_no_analyst_fails(self):
apply_decision_request = {
'decision_id': 'user_looks_ok_legacy',
Expand Down Expand Up @@ -522,6 +574,42 @@ def test_apply_decision_to_order_ok(self):
assert(response.http_status_code == 200)
assert(response.body['entity']['type'] == 'order')

def test_apply_decision_to_session_ok(self):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a quick question: no need to test failed cases?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that test_apply_decision_to_session does test with no session_id. I think it's enough to change the name of test_apply_decision_to_session.

user_id = '54321'
session_id = 'gigtleqddo84l8cm15qe4il'
mock_response = mock.Mock()
apply_decision_request = {
'decision_id': 'session_looks_bad_ato',
'source': 'AUTOMATED_RULE',
'time': 1481569575
}

apply_decision_response_json = '{' \
'"entity": {' \
'"id": "54321",' \
'"type": "login"' \
'},' \
'"decision": {' \
'"id":"session_looks_bad_ato"' \
'},' \
'"time":"1481569575"}'

mock_response.content = apply_decision_response_json
mock_response.json.return_value = json.loads(mock_response.content)
mock_response.status_code = 200
mock_response.headers = response_with_data_header()
with mock.patch('requests.post') as mock_post:
mock_post.return_value = mock_response
response = self.sift_client.apply_session_decision(user_id, session_id, apply_decision_request)
data = json.dumps(apply_decision_request)
mock_post.assert_called_with(
'https://api3.siftscience.com/v3/accounts/ACCT/users/%s/sessions/%s/decisions' % (user_id,session_id),
auth=mock.ANY, data=data, headers=mock.ANY, timeout=mock.ANY)
assert(isinstance(response, sift.client.Response))
assert(response.is_ok())
assert(response.http_status_code == 200)
assert(response.body['entity']['type'] == 'login')

def test_label_user_ok(self):
user_id = '54321'
mock_response = mock.Mock()
Expand Down