From b841c39e85e4c8bafe95f6ccb66c724dd5734f21 Mon Sep 17 00:00:00 2001 From: Dawid Libiszewski Date: Fri, 21 May 2021 18:43:38 +0200 Subject: [PATCH 1/4] CAS-6995 Add filter, risk and log endpoint clients --- castle/client.py | 54 ++++++++++++++++ castle/commands/filter.py | 20 ++++++ castle/test/commands/filter_test.py | 97 +++++++++++++++++++++++++++++ castle/test/commands/log_test.py | 97 +++++++++++++++++++++++++++++ castle/test/commands/risk_test.py | 97 +++++++++++++++++++++++++++++ 5 files changed, 365 insertions(+) create mode 100644 castle/commands/filter.py create mode 100644 castle/test/commands/filter_test.py create mode 100644 castle/test/commands/log_test.py create mode 100644 castle/test/commands/risk_test.py diff --git a/castle/client.py b/castle/client.py index 75db2a4..c647a78 100644 --- a/castle/client.py +++ b/castle/client.py @@ -3,8 +3,11 @@ from castle.context.default import ContextDefault from castle.context.merger import ContextMerger from castle.commands.authenticate import CommandsAuthenticate +from castle.commands.filter import CommandsFilter from castle.commands.identify import CommandsIdentify from castle.commands.impersonate import CommandsImpersonate +from castle.commands.log import CommandsLog +from castle.commands.risk import CommandsRisk from castle.commands.track import CommandsTrack from castle.exceptions import InternalServerError, RequestError, ImpersonationFailed from castle.failover_response import FailoverResponse @@ -75,6 +78,57 @@ def authenticate(self, options): 'Castle set to do not track.' ).call() + def filter(self, options): + if self.tracked(): + self._add_timestamp_if_necessary(options) + command = CommandsFilter(self.context).build(options) + try: + response = self.api.call(command) + response.update(failover=False, failover_reason=None) + return response + except (RequestError, InternalServerError) as exception: + return Client.failover_response_or_raise(options, exception) + else: + return FailoverResponse( + options.get('user_id'), + 'allow', + 'Castle set to do not track.' + ).call() + + def log(self, options): + if self.tracked(): + self._add_timestamp_if_necessary(options) + command = CommandsLog(self.context).build(options) + try: + response = self.api.call(command) + response.update(failover=False, failover_reason=None) + return response + except (RequestError, InternalServerError) as exception: + return Client.failover_response_or_raise(options, exception) + else: + return FailoverResponse( + options.get('user_id'), + 'allow', + 'Castle set to do not track.' + ).call() + + def risk(self, options): + if self.tracked(): + self._add_timestamp_if_necessary(options) + command = CommandsRisk(self.context).build(options) + try: + response = self.api.call(command) + response.update(failover=False, failover_reason=None) + return response + except (RequestError, InternalServerError) as exception: + return Client.failover_response_or_raise(options, exception) + else: + return FailoverResponse( + options.get('user_id'), + 'allow', + 'Castle set to do not track.' + ).call() + def identify(self, options): if not self.tracked(): return None diff --git a/castle/commands/filter.py b/castle/commands/filter.py new file mode 100644 index 0000000..c0a3609 --- /dev/null +++ b/castle/commands/filter.py @@ -0,0 +1,20 @@ +from castle.command import Command +from castle.utils import timestamp +from castle.context.merger import ContextMerger +from castle.context.sanitizer import ContextSanitizer +from castle.validators.present import ValidatorsPresent + + +class CommandsFilter(object): + def __init__(self, context): + self.context = context + + def build(self, options): + ValidatorsPresent.call(options, 'event') + context = ContextMerger.call(self.context, options.get('context')) + context = ContextSanitizer.call(context) + if context: + options.update({'context': context}) + options.update({'sent_at': timestamp()}) + + return Command(method='post', path='filter', data=options) diff --git a/castle/test/commands/filter_test.py b/castle/test/commands/filter_test.py new file mode 100644 index 0000000..44a6e7d --- /dev/null +++ b/castle/test/commands/filter_test.py @@ -0,0 +1,97 @@ +from castle.test import mock, unittest +from castle.command import Command +from castle.commands.filter import CommandsFilter +from castle.exceptions import InvalidParametersError +from castle.utils import clone + + +def default_options(): + """Default options include all required fields.""" + return { + 'request_token': '7e51335b-f4bc-4bc7-875d-b713fb61eb23-bf021a3022a1a302', + 'event': '$registration', + 'status': '', + 'user': { + 'id': '1234' + } + } + +def default_options_plus(**extra): + """Default options plus the given extra fields.""" + options = default_options() + options.update(extra) + return options + + +def default_command_with_data(**data): + """What we expect the filter command to look like.""" + return Command( + method='post', + path='filter', + data=dict(sent_at=mock.sentinel.timestamp, **data) + ) + + +class CommandsFilterTestCase(unittest.TestCase): + + def setUp(self): + # patch timestamp to return a known value + timestamp_patcher = mock.patch( + 'castle.commands.filter.timestamp') + self.mock_timestamp = timestamp_patcher.start() + self.mock_timestamp.return_value = mock.sentinel.timestamp + self.addCleanup(timestamp_patcher.stop) + + def test_init(self): + context = mock.sentinel.test_init_context + obj = CommandsFilter(context) + self.assertEqual(obj.context, context) + + def test_build(self): + context = {'test': '1'} + options = default_options_plus(context={'spam': True}) + + # expect the original context to have been merged with the context specified in the options + expected_data = clone(options) + expected_data.update(context={'test': '1', 'spam': True}) + expected = default_command_with_data(**expected_data) + + self.assertEqual(CommandsFilter( + context).build(options), expected) + + def test_build_no_event(self): + context = {} + options = default_options() + options.pop('event') + + with self.assertRaises(InvalidParametersError): + CommandsFilter(context).build(options) + + def test_build_no_user_id(self): + context = {} + options = default_options() + options.pop('user_id') + + expected = default_command_with_data(**options) + + self.assertEqual(CommandsFilter(context).build(options), expected) + + def test_build_properties_allowed(self): + context = {} + options = default_options_plus(properties={'test': '1'}) + options.update({'context': context}) + + expected = default_command_with_data(**options) + + self.assertEqual(CommandsFilter( + context).build(options), expected) + + def test_build_user_traits_allowed(self): + context = {} + options = default_options_plus(user_traits={'email': 'a@b.com'}) + options.update({'context': context}) + + expected = default_command_with_data(**options) + + self.assertEqual(CommandsFilter( + context).build(options), expected) diff --git a/castle/test/commands/log_test.py b/castle/test/commands/log_test.py new file mode 100644 index 0000000..3b3088a --- /dev/null +++ b/castle/test/commands/log_test.py @@ -0,0 +1,97 @@ +from castle.test import mock, unittest +from castle.command import Command +from castle.commands.log import CommandsLog +from castle.exceptions import InvalidParametersError +from castle.utils import clone + + +def default_options(): + """Default options include all required fields.""" + return { + 'request_token': '7e51335b-f4bc-4bc7-875d-b713fb61eb23-bf021a3022a1a302', + 'event': '$registration', + 'status': '', + 'user': { + 'id': '1234' + } + } + +def default_options_plus(**extra): + """Default options plus the given extra fields.""" + options = default_options() + options.update(extra) + return options + + +def default_command_with_data(**data): + """What we expect the log command to look like.""" + return Command( + method='post', + path='log', + data=dict(sent_at=mock.sentinel.timestamp, **data) + ) + + +class CommandsLogTestCase(unittest.TestCase): + + def setUp(self): + # patch timestamp to return a known value + timestamp_patcher = mock.patch( + 'castle.commands.log.timestamp') + self.mock_timestamp = timestamp_patcher.start() + self.mock_timestamp.return_value = mock.sentinel.timestamp + self.addCleanup(timestamp_patcher.stop) + + def test_init(self): + context = mock.sentinel.test_init_context + obj = CommandsLog(context) + self.assertEqual(obj.context, context) + + def test_build(self): + context = {'test': '1'} + options = default_options_plus(context={'spam': True}) + + # expect the original context to have been merged with the context specified in the options + expected_data = clone(options) + expected_data.update(context={'test': '1', 'spam': True}) + expected = default_command_with_data(**expected_data) + + self.assertEqual(CommandsLog( + context).build(options), expected) + + def test_build_no_event(self): + context = {} + options = default_options() + options.pop('event') + + with self.assertRaises(InvalidParametersError): + CommandsLog(context).build(options) + + def test_build_no_user_id(self): + context = {} + options = default_options() + options.pop('user_id') + + expected = default_command_with_data(**options) + + self.assertEqual(CommandsLog(context).build(options), expected) + + def test_build_properties_allowed(self): + context = {} + options = default_options_plus(properties={'test': '1'}) + options.update({'context': context}) + + expected = default_command_with_data(**options) + + self.assertEqual(CommandsLog( + context).build(options), expected) + + def test_build_user_traits_allowed(self): + context = {} + options = default_options_plus(user_traits={'email': 'a@b.com'}) + options.update({'context': context}) + + expected = default_command_with_data(**options) + + self.assertEqual(CommandsLog( + context).build(options), expected) diff --git a/castle/test/commands/risk_test.py b/castle/test/commands/risk_test.py new file mode 100644 index 0000000..6aa4f24 --- /dev/null +++ b/castle/test/commands/risk_test.py @@ -0,0 +1,97 @@ +from castle.test import mock, unittest +from castle.command import Command +from castle.commands.risk import CommandsRisk +from castle.exceptions import InvalidParametersError +from castle.utils import clone + + +def default_options(): + """Default options include all required fields.""" + return { + 'request_token': '7e51335b-f4bc-4bc7-875d-b713fb61eb23-bf021a3022a1a302', + 'event': '$login', + 'status': '$succeeded', + 'user': { + 'id': '1234' + } + } + +def default_options_plus(**extra): + """Default options plus the given extra fields.""" + options = default_options() + options.update(extra) + return options + + +def default_command_with_data(**data): + """What we expect the risk command to look like.""" + return Command( + method='post', + path='risk', + data=dict(sent_at=mock.sentinel.timestamp, **data) + ) + + +class CommandsRiskTestCase(unittest.TestCase): + + def setUp(self): + # patch timestamp to return a known value + timestamp_patcher = mock.patch( + 'castle.commands.risk.timestamp') + self.mock_timestamp = timestamp_patcher.start() + self.mock_timestamp.return_value = mock.sentinel.timestamp + self.addCleanup(timestamp_patcher.stop) + + def test_init(self): + context = mock.sentinel.test_init_context + obj = CommandsRisk(context) + self.assertEqual(obj.context, context) + + def test_build(self): + context = {'test': '1'} + options = default_options_plus(context={'spam': True}) + + # expect the original context to have been merged with the context specified in the options + expected_data = clone(options) + expected_data.update(context={'test': '1', 'spam': True}) + expected = default_command_with_data(**expected_data) + + self.assertEqual(CommandsRisk( + context).build(options), expected) + + def test_build_no_event(self): + context = {} + options = default_options() + options.pop('event') + + with self.assertRaises(InvalidParametersError): + CommandsRisk(context).build(options) + + def test_build_no_user_id(self): + context = {} + options = default_options() + options.pop('user_id') + + expected = default_command_with_data(**options) + + self.assertEqual(CommandsRisk(context).build(options), expected) + + def test_build_properties_allowed(self): + context = {} + options = default_options_plus(properties={'test': '1'}) + options.update({'context': context}) + + expected = default_command_with_data(**options) + + self.assertEqual(CommandsRisk( + context).build(options), expected) + + def test_build_user_traits_allowed(self): + context = {} + options = default_options_plus(user_traits={'email': 'a@b.com'}) + options.update({'context': context}) + + expected = default_command_with_data(**options) + + self.assertEqual(CommandsRisk( + context).build(options), expected) From 19deff67c6ec5e94fbd1d21b1b0dfb5d9c7c4947 Mon Sep 17 00:00:00 2001 From: Dawid Libiszewski Date: Fri, 21 May 2021 19:09:03 +0200 Subject: [PATCH 2/4] fixes --- Makefile | 3 +- castle/client.py | 31 ++++--- castle/commands/filter.py | 14 +-- castle/commands/log.py | 20 +++++ castle/commands/risk.py | 20 +++++ castle/test/client_test.py | 180 ++++++++++++++++++++++++++++++++++++- 6 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 castle/commands/log.py create mode 100644 castle/commands/risk.py diff --git a/Makefile b/Makefile index c422f5e..8a639d5 100644 --- a/Makefile +++ b/Makefile @@ -20,12 +20,11 @@ ci-lint: pre-lint lint pre-lint: ${PIP} install pylint - ${PIP} install setuptools-lint ${PIP} install --upgrade pep8 ${PIP} install --upgrade autopep8 lint: - ${PYTHON} setup.py lint + pylint --rcfile=./pylintrc castle autopep8 --in-place -r castle setup: diff --git a/castle/client.py b/castle/client.py index 61bee6e..2a6401d 100644 --- a/castle/client.py +++ b/castle/client.py @@ -1,7 +1,6 @@ from castle.api_request import APIRequest from castle.commands.authenticate import CommandsAuthenticate from castle.commands.filter import CommandsFilter -from castle.commands.impersonate import CommandsImpersonate from castle.commands.log import CommandsLog from castle.commands.risk import CommandsRisk from castle.commands.identify import CommandsIdentify @@ -26,11 +25,11 @@ def from_request(cls, request, options=None): return cls(options) @staticmethod - def failover_response_or_raise(options, exception): + def failover_response_or_raise(user_id, exception): if configuration.failover_strategy == FailoverStrategy.THROW.value: raise exception return FailoverPrepareResponse( - options.get('user_id'), None, exception.__class__.__name__ + user_id, None, exception.__class__.__name__ ).call() def __init__(self, options=None): @@ -54,7 +53,7 @@ def authenticate(self, options): response.update(failover=False, failover_reason=None) return response except (RequestError, InternalServerError) as exception: - return Client.failover_response_or_raise(options, exception) + return Client.failover_response_or_raise(options.get('user_id'), exception) else: return FailoverPrepareResponse( options.get('user_id'), @@ -65,16 +64,16 @@ def authenticate(self, options): def filter(self, options): if self.tracked(): self._add_timestamp_if_necessary(options) - command = CommandsFilter(self.context).build(options) + command = CommandsFilter(self.context).call(options) try: response = self.api.call(command) response.update(failover=False, failover_reason=None) return response except (RequestError, InternalServerError) as exception: - return Client.failover_response_or_raise(options, exception) + return Client.failover_response_or_raise(options.get('user').get('id'), exception) else: - return FailoverResponse( - options.get('user_id'), + return FailoverPrepareResponse( + options.get('user').get('id'), 'allow', 'Castle set to do not track.' ).call() @@ -82,16 +81,16 @@ def filter(self, options): def log(self, options): if self.tracked(): self._add_timestamp_if_necessary(options) - command = CommandsLog(self.context).build(options) + command = CommandsLog(self.context).call(options) try: response = self.api.call(command) response.update(failover=False, failover_reason=None) return response except (RequestError, InternalServerError) as exception: - return Client.failover_response_or_raise(options, exception) + return Client.failover_response_or_raise(options.get('user').get('id'), exception) else: - return FailoverResponse( - options.get('user_id'), + return FailoverPrepareResponse( + options.get('user').get('id'), 'allow', 'Castle set to do not track.' ).call() @@ -99,16 +98,16 @@ def log(self, options): def risk(self, options): if self.tracked(): self._add_timestamp_if_necessary(options) - command = CommandsRisk(self.context).build(options) + command = CommandsRisk(self.context).call(options) try: response = self.api.call(command) response.update(failover=False, failover_reason=None) return response except (RequestError, InternalServerError) as exception: - return Client.failover_response_or_raise(options, exception) + return Client.failover_response_or_raise(options.get('user').get('id'), exception) else: - return FailoverResponse( - options.get('user_id'), + return FailoverPrepareResponse( + options.get('user').get('id'), 'allow', 'Castle set to do not track.' ).call() diff --git a/castle/commands/filter.py b/castle/commands/filter.py index c0a3609..9717eeb 100644 --- a/castle/commands/filter.py +++ b/castle/commands/filter.py @@ -1,7 +1,7 @@ from castle.command import Command -from castle.utils import timestamp -from castle.context.merger import ContextMerger -from castle.context.sanitizer import ContextSanitizer +from castle.utils.timestamp import UtilsTimestamp as generate_timestamp +from castle.context.merge import ContextMerge +from castle.context.sanitize import ContextSanitize from castle.validators.present import ValidatorsPresent @@ -9,12 +9,12 @@ class CommandsFilter(object): def __init__(self, context): self.context = context - def build(self, options): + def call(self, options): ValidatorsPresent.call(options, 'event') - context = ContextMerger.call(self.context, options.get('context')) - context = ContextSanitizer.call(context) + context = ContextMerge.call(self.context, options.get('context')) + context = ContextSanitize.call(context) if context: options.update({'context': context}) - options.update({'sent_at': timestamp()}) + options.update({'sent_at': generate_timestamp.call()}) return Command(method='post', path='filter', data=options) diff --git a/castle/commands/log.py b/castle/commands/log.py new file mode 100644 index 0000000..05df3ab --- /dev/null +++ b/castle/commands/log.py @@ -0,0 +1,20 @@ +from castle.command import Command +from castle.utils.timestamp import UtilsTimestamp as generate_timestamp +from castle.context.merge import ContextMerge +from castle.context.sanitize import ContextSanitize +from castle.validators.present import ValidatorsPresent + + +class CommandsLog(object): + def __init__(self, context): + self.context = context + + def call(self, options): + ValidatorsPresent.call(options, 'event') + context = ContextMerge.call(self.context, options.get('context')) + context = ContextSanitize.call(context) + if context: + options.update({'context': context}) + options.update({'sent_at': generate_timestamp.call()}) + + return Command(method='post', path='log', data=options) diff --git a/castle/commands/risk.py b/castle/commands/risk.py new file mode 100644 index 0000000..85a1238 --- /dev/null +++ b/castle/commands/risk.py @@ -0,0 +1,20 @@ +from castle.command import Command +from castle.utils.timestamp import UtilsTimestamp as generate_timestamp +from castle.context.merge import ContextMerge +from castle.context.sanitize import ContextSanitize +from castle.validators.present import ValidatorsPresent + + +class CommandsRisk(object): + def __init__(self, context): + self.context = context + + def call(self, options): + ValidatorsPresent.call(options, 'event') + context = ContextMerge.call(self.context, options.get('context')) + context = ContextSanitize.call(context) + if context: + options.update({'context': context}) + options.update({'sent_at': generate_timestamp.call()}) + + return Command(method='post', path='risk', data=options) diff --git a/castle/test/client_test.py b/castle/test/client_test.py index f3c5d19..1ba6e23 100644 --- a/castle/test/client_test.py +++ b/castle/test/client_test.py @@ -179,6 +179,184 @@ def test_track_tracked_false(self): client.disable_tracking() self.assertEqual(client.track({}), None) + @responses.activate + def test_filter_tracked_true(self): + response_text = {'action': Verdict.ALLOW.value, 'user_id': '1234'} + responses.add( + responses.POST, + 'https://api.castle.io/v1/filter', + json=response_text, + status=200 + ) + client = Client.from_request(request(), {}) + options = { + 'request_token': '7e51335b-f4bc-4bc7-875d-b713fb61eb23-bf021a3022a1a302', + 'event': '$login', + 'status': '$succeeded', + 'user': {'id': '1234'} + } + response_text.update(failover=False, failover_reason=None) + self.assertEqual(client.filter(options), response_text) + + @responses.activate + def test_filter_tracked_true_status_500(self): + response_text = { + 'action': Verdict.ALLOW.value, + 'user_id': '1234', + 'failover': True, + 'failover_reason': 'InternalServerError' + } + responses.add( + responses.POST, + 'https://api.castle.io/v1/filter', + json='filter', + status=500 + ) + client = Client.from_request(request(), {}) + options = { + 'request_token': '7e51335b-f4bc-4bc7-875d-b713fb61eb23-bf021a3022a1a302', + 'event': '$login', + 'status': '$succeeded', + 'user': {'id': '1234'} + } + self.assertEqual(client.filter(options), response_text) + + def test_filter_tracked_false(self): + response_text = { + 'action': Verdict.ALLOW.value, + 'user_id': '1234', + 'failover': True, + 'failover_reason': 'Castle set to do not track.' + } + client = Client.from_request(request(), {}) + client.disable_tracking() + options = { + 'request_token': '7e51335b-f4bc-4bc7-875d-b713fb61eb23-bf021a3022a1a302', + 'event': '$login', + 'status': '$succeeded', + 'user': {'id': '1234'} + } + self.assertEqual(client.filter(options), response_text) + + + @responses.activate + def test_log_tracked_true(self): + response_text = {'action': Verdict.ALLOW.value, 'user_id': '1234'} + responses.add( + responses.POST, + 'https://api.castle.io/v1/log', + json=response_text, + status=200 + ) + client = Client.from_request(request(), {}) + options = { + 'request_token': '7e51335b-f4bc-4bc7-875d-b713fb61eb23-bf021a3022a1a302', + 'event': '$login', + 'status': '$succeeded', + 'user': {'id': '1234'} + } + response_text.update(failover=False, failover_reason=None) + self.assertEqual(client.log(options), response_text) + + @responses.activate + def test_log_tracked_true_status_500(self): + response_text = { + 'action': Verdict.ALLOW.value, + 'user_id': '1234', + 'failover': True, + 'failover_reason': 'InternalServerError' + } + responses.add( + responses.POST, + 'https://api.castle.io/v1/log', + json='log', + status=500 + ) + client = Client.from_request(request(), {}) + options = { + 'request_token': '7e51335b-f4bc-4bc7-875d-b713fb61eb23-bf021a3022a1a302', + 'event': '$login', + 'status': '$succeeded', + 'user': {'id': '1234'} + } + self.assertEqual(client.log(options), response_text) + + def test_log_tracked_false(self): + response_text = { + 'action': Verdict.ALLOW.value, + 'user_id': '1234', + 'failover': True, + 'failover_reason': 'Castle set to do not track.' + } + client = Client.from_request(request(), {}) + client.disable_tracking() + options = { + 'request_token': '7e51335b-f4bc-4bc7-875d-b713fb61eb23-bf021a3022a1a302', + 'event': '$login', + 'status': '$succeeded', + 'user': {'id': '1234'} + } + self.assertEqual(client.log(options), response_text) + + @responses.activate + def test_risk_tracked_true(self): + response_text = {'action': Verdict.ALLOW.value, 'user_id': '1234'} + responses.add( + responses.POST, + 'https://api.castle.io/v1/risk', + json=response_text, + status=200 + ) + client = Client.from_request(request(), {}) + options = { + 'request_token': '7e51335b-f4bc-4bc7-875d-b713fb61eb23-bf021a3022a1a302', + 'event': '$login', + 'status': '$succeeded', + 'user': {'id': '1234'} + } + response_text.update(failover=False, failover_reason=None) + self.assertEqual(client.risk(options), response_text) + + @responses.activate + def test_risk_tracked_true_status_500(self): + response_text = { + 'action': Verdict.ALLOW.value, + 'user_id': '1234', + 'failover': True, + 'failover_reason': 'InternalServerError' + } + responses.add( + responses.POST, + 'https://api.castle.io/v1/risk', + json='risk', + status=500 + ) + client = Client.from_request(request(), {}) + options = { + 'request_token': '7e51335b-f4bc-4bc7-875d-b713fb61eb23-bf021a3022a1a302', + 'event': '$login', + 'status': '$succeeded', + 'user': {'id': '1234'} + } + self.assertEqual(client.risk(options), response_text) + + def test_risk_tracked_false(self): + response_text = { + 'action': Verdict.ALLOW.value, + 'user_id': '1234', + 'failover': True, + 'failover_reason': 'Castle set to do not track.' + } + client = Client.from_request(request(), {}) + client.disable_tracking() + options = { + 'request_token': '7e51335b-f4bc-4bc7-875d-b713fb61eb23-bf021a3022a1a302', + 'event': '$login', + 'status': '$succeeded', + 'user': {'id': '1234'} + } + self.assertEqual(client.risk(options), response_text) + def test_disable_tracking(self): client = Client.from_request(request(), {}) client.disable_tracking() @@ -202,7 +380,7 @@ def test_tracked_when_do_not_track_true(self): def test_failover_strategy_not_throw(self): options = {'user_id': '1234'} self.assertEqual( - Client.failover_response_or_raise(options, Exception()), + Client.failover_response_or_raise(options.get('user_id'), Exception()), { 'action': Verdict.ALLOW.value, 'user_id': '1234', From 6b0b6d2a801e7ab8cc6e8040f9f64ba83661e382 Mon Sep 17 00:00:00 2001 From: Dawid Libiszewski Date: Wed, 2 Jun 2021 18:22:28 +0200 Subject: [PATCH 3/4] README update --- README.rst | 4 +--- castle/client.py | 6 ------ 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 81628f8..4714303 100644 --- a/README.rst +++ b/README.rst @@ -118,7 +118,6 @@ Here is a simple example of track event. .. code:: python from castle.client import Client - from castle import events castle = Client.from_request(request) castle.track({ @@ -148,11 +147,10 @@ background worker you can generate data for a worker: .. code:: python from castle.payload.prepare import PayloadPrepare - from castle import events payload = PayloadPrepare.call( { - 'event': $login, + 'event': '$login', 'user_id': user.id, 'properties': { 'key': 'value' }, 'user_traits': { 'key': 'value' } diff --git a/castle/client.py b/castle/client.py index e7c8c93..3dba4be 100644 --- a/castle/client.py +++ b/castle/client.py @@ -111,12 +111,6 @@ def risk(self, options): 'Castle set to do not track.' ).call() - def identify(self, options): - if not self.tracked(): - return None - self._add_timestamp_if_necessary(options) - return self.api.call(CommandsIdentify(self.context).call(options)) - def start_impersonation(self, options): self._add_timestamp_if_necessary(options) response = self.api.call(CommandsStartImpersonation(self.context).call(options)) From ee1a4983b1d31e5451dc486fa7775f4e3de9c8af Mon Sep 17 00:00:00 2001 From: bartes Date: Wed, 2 Jun 2021 19:42:06 +0200 Subject: [PATCH 4/4] fixed log command, linter, readme fixes --- README.rst | 4 +-- castle/client.py | 20 +++--------- castle/test/client_test.py | 49 ++++------------------------- castle/test/commands/filter_test.py | 1 + castle/test/commands/log_test.py | 1 + castle/test/commands/risk_test.py | 1 + 6 files changed, 16 insertions(+), 60 deletions(-) diff --git a/README.rst b/README.rst index 4714303..daf1012 100644 --- a/README.rst +++ b/README.rst @@ -121,7 +121,7 @@ Here is a simple example of track event. castle = Client.from_request(request) castle.track({ - 'event': '$login', + 'event': '$login.succeeded', 'user_id': 'user_id' }) @@ -150,7 +150,7 @@ background worker you can generate data for a worker: payload = PayloadPrepare.call( { - 'event': '$login', + 'event': '$login.succeeded', 'user_id': user.id, 'properties': { 'key': 'value' }, 'user_traits': { 'key': 'value' } diff --git a/castle/client.py b/castle/client.py index 3dba4be..11eea88 100644 --- a/castle/client.py +++ b/castle/client.py @@ -78,21 +78,11 @@ def filter(self, options): ).call() def log(self, options): - if self.tracked(): - self._add_timestamp_if_necessary(options) - command = CommandsLog(self.context).call(options) - try: - response = self.api.call(command) - response.update(failover=False, failover_reason=None) - return response - except (RequestError, InternalServerError) as exception: - return Client.failover_response_or_raise(options.get('user').get('id'), exception) - else: - return FailoverPrepareResponse( - options.get('user').get('id'), - 'allow', - 'Castle set to do not track.' - ).call() + if not self.tracked(): + return None + self._add_timestamp_if_necessary(options) + + return self.api.call(CommandsLog(self.context).call(options)) def risk(self, options): if self.tracked(): diff --git a/castle/test/client_test.py b/castle/test/client_test.py index f980f08..e1a58f7 100644 --- a/castle/test/client_test.py +++ b/castle/test/client_test.py @@ -109,7 +109,7 @@ def test_authenticate_tracked_true(self): status=200 ) client = Client.from_request(request(), {}) - options = {'event': '$login.authenticate', 'user_id': '1234'} + options = {'event': '$login.succeeded', 'user_id': '1234'} response_text.update(failover=False, failover_reason=None) self.assertEqual(client.authenticate(options), response_text) @@ -128,7 +128,7 @@ def test_authenticate_tracked_true_status_500(self): status=500 ) client = Client.from_request(request(), {}) - options = {'event': '$login.authenticate', 'user_id': '1234'} + options = {'event': '$login.succeeded', 'user_id': '1234'} self.assertEqual(client.authenticate(options), response_text) def test_authenticate_tracked_false(self): @@ -140,7 +140,7 @@ def test_authenticate_tracked_false(self): } client = Client.from_request(request(), {}) client.disable_tracking() - options = {'event': '$login.authenticate', 'user_id': '1234'} + options = {'event': '$login.succeeded', 'user_id': '1234'} self.assertEqual(client.authenticate(options), response_text) @responses.activate @@ -153,7 +153,7 @@ def test_track_tracked_true(self): status=200 ) client = Client.from_request(request(), {}) - options = {'event': '$login.authenticate', 'user_id': '1234'} + options = {'event': '$login.succeeded', 'user_id': '1234'} self.assertEqual(client.track(options), response_text) def test_track_tracked_false(self): @@ -220,10 +220,9 @@ def test_filter_tracked_false(self): } self.assertEqual(client.filter(options), response_text) - @responses.activate def test_log_tracked_true(self): - response_text = {'action': Verdict.ALLOW.value, 'user_id': '1234'} + response_text = 'log' responses.add( responses.POST, 'https://api.castle.io/v1/log', @@ -237,48 +236,12 @@ def test_log_tracked_true(self): 'status': '$succeeded', 'user': {'id': '1234'} } - response_text.update(failover=False, failover_reason=None) - self.assertEqual(client.log(options), response_text) - - @responses.activate - def test_log_tracked_true_status_500(self): - response_text = { - 'action': Verdict.ALLOW.value, - 'user_id': '1234', - 'failover': True, - 'failover_reason': 'InternalServerError' - } - responses.add( - responses.POST, - 'https://api.castle.io/v1/log', - json='log', - status=500 - ) - client = Client.from_request(request(), {}) - options = { - 'request_token': '7e51335b-f4bc-4bc7-875d-b713fb61eb23-bf021a3022a1a302', - 'event': '$login', - 'status': '$succeeded', - 'user': {'id': '1234'} - } self.assertEqual(client.log(options), response_text) def test_log_tracked_false(self): - response_text = { - 'action': Verdict.ALLOW.value, - 'user_id': '1234', - 'failover': True, - 'failover_reason': 'Castle set to do not track.' - } client = Client.from_request(request(), {}) client.disable_tracking() - options = { - 'request_token': '7e51335b-f4bc-4bc7-875d-b713fb61eb23-bf021a3022a1a302', - 'event': '$login', - 'status': '$succeeded', - 'user': {'id': '1234'} - } - self.assertEqual(client.log(options), response_text) + self.assertEqual(client.log({}), None) @responses.activate def test_risk_tracked_true(self): diff --git a/castle/test/commands/filter_test.py b/castle/test/commands/filter_test.py index 44a6e7d..3912068 100644 --- a/castle/test/commands/filter_test.py +++ b/castle/test/commands/filter_test.py @@ -16,6 +16,7 @@ def default_options(): } } + def default_options_plus(**extra): """Default options plus the given extra fields.""" options = default_options() diff --git a/castle/test/commands/log_test.py b/castle/test/commands/log_test.py index 3b3088a..ec1f518 100644 --- a/castle/test/commands/log_test.py +++ b/castle/test/commands/log_test.py @@ -16,6 +16,7 @@ def default_options(): } } + def default_options_plus(**extra): """Default options plus the given extra fields.""" options = default_options() diff --git a/castle/test/commands/risk_test.py b/castle/test/commands/risk_test.py index 6aa4f24..7a3c67d 100644 --- a/castle/test/commands/risk_test.py +++ b/castle/test/commands/risk_test.py @@ -16,6 +16,7 @@ def default_options(): } } + def default_options_plus(**extra): """Default options plus the given extra fields.""" options = default_options()