diff --git a/go/apps/jsbox/optout.py b/go/apps/jsbox/optout.py index cf3df189b..25789381f 100644 --- a/go/apps/jsbox/optout.py +++ b/go/apps/jsbox/optout.py @@ -9,11 +9,59 @@ from vumi.application.sandbox import SandboxResource +class OptoutException(Exception): + pass + + +def optout_authorized(func): + @inlineCallbacks + def wrapper(self, api, command): + if not (yield self.is_allowed(api)): + returnValue(self.reply( + command, success=False, + reason='Account not allowed to manage optouts.')) + + resp = yield func(self, api, command) + returnValue(resp) + return wrapper + + +def ensure_params(*keys): + def decorator(func): + def wrapper(self, api, command): + for key in keys: + if key not in command: + return self.reply(command, success=False, + reason='Missing key: %s' % (key,)) + + value = command[key] + # value is not allowed to be `False`, `None` or an empty string. + if not value: + return self.reply( + command, success=False, + reason='Invalid value "%s" for "%s"' % (value, key)) + + return func(self, api, command) + return wrapper + return decorator + + class OptoutResource(SandboxResource): + def get_user_api(self, api): + return self.app_worker.user_api_for_api(api) + def optout_store_for_api(self, api): - return self.app_worker.user_api_for_api(api).optout_store + return self.get_user_api(api).optout_store + + def is_allowed(self, api): + user_api = self.get_user_api(api) + d = user_api.get_user_account() + d.addCallback(lambda account: account.can_manage_optouts) + return d + @ensure_params('address_type', 'address_value') + @optout_authorized @inlineCallbacks def handle_status(self, api, command): """ @@ -22,17 +70,22 @@ def handle_status(self, api, command): Returns ``None`` if it doesn't exist. """ - address_type = command['address_type'] - address_value = command['address_value'] - optout = yield self.optout_store_for_api(api).get_opt_out( - address_type, address_value) - if optout is not None: - returnValue(self.reply(command, success=True, opted_out=True, - created_at=optout.created_at, - message_id=optout.message)) - else: - returnValue(self.reply(command, success=True, opted_out=False)) + try: + address_type = command['address_type'] + address_value = command['address_value'] + optout = yield self.optout_store_for_api(api).get_opt_out( + address_type, address_value) + if optout is not None: + returnValue(self.reply(command, success=True, opted_out=True, + created_at=optout.created_at, + message_id=optout.message)) + else: + returnValue(self.reply(command, success=True, opted_out=False)) + except (OptoutException,), e: + returnValue(self.reply(command, success=False, reason=e.strerror)) + + @optout_authorized def handle_count(self, api, command): """ Return a count of however many opt-outs there are @@ -43,6 +96,8 @@ def handle_count(self, api, command): lambda count: self.reply(command, success=True, count=count)) return d + @ensure_params('address_type', 'address_value', 'message_id') + @optout_authorized @inlineCallbacks def handle_optout(self, api, command): """ @@ -59,6 +114,8 @@ def handle_optout(self, api, command): created_at=optout.created_at, message_id=optout.message)) + @ensure_params('address_type', 'address_value') + @optout_authorized def handle_cancel_optout(self, api, command): """ Cancel an opt-out, effectively opting an address_type & address_value diff --git a/go/apps/jsbox/tests/test_optout.py b/go/apps/jsbox/tests/test_optout.py index 713f01da3..b051c8b90 100644 --- a/go/apps/jsbox/tests/test_optout.py +++ b/go/apps/jsbox/tests/test_optout.py @@ -2,12 +2,12 @@ from mock import Mock -from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.internet.defer import inlineCallbacks, returnValue, succeed from vumi.application.tests.test_sandbox import ( ResourceTestCaseBase, DummyAppWorker) -from go.apps.jsbox.optout import OptoutResource +from go.apps.jsbox.optout import OptoutResource, OptoutException from go.vumitools.tests.utils import GoPersistenceMixin from go.vumitools.account import AccountStore from go.vumitools.contact import ContactStore @@ -41,6 +41,7 @@ def setUp(self): self.manager = self.get_riak_manager() self.account_store = AccountStore(self.manager) self.account = yield self.mk_user(self, u'user') + self.account.can_manage_optouts = True self.contact_store = ContactStore.from_user_account(self.account) self.optout_store = OptOutStore.from_user_account(self.account) yield self.contact_store.contacts.enable_search() @@ -50,6 +51,8 @@ def setUp(self): self.user_api = self.app_worker.user_api self.user_api.contact_store = self.contact_store self.user_api.optout_store = self.optout_store + self.user_api.get_user_account = Mock( + return_value=succeed(self.account)) self.contact1 = yield self.new_contact( name=u'A', @@ -99,6 +102,20 @@ def test_handle_status_optedin(self): self.assertTrue(reply['success']) self.assertFalse(reply['opted_out']) + @inlineCallbacks + def test_ensure_params_missing_key(self): + reply = yield self.dispatch_command('status') + self.assertFalse(reply['success']) + self.assertEqual(reply['reason'], + 'Missing key: address_type') + + @inlineCallbacks + def test_ensure_params_invalid_value(self): + reply = yield self.dispatch_command('status', address_type=None) + self.assertFalse(reply['success']) + self.assertEqual(reply['reason'], + 'Invalid value "None" for "address_type"') + @inlineCallbacks def test_handle_count(self): def assert_count(count): @@ -131,3 +148,12 @@ def test_handle_cancel_optout(self): address_value=self.contact1.msisdn) self.assertTrue(reply['success']) self.assertFalse(reply['opted_out']) + + @inlineCallbacks + def test_account_can_manage_optouts(self): + self.account.can_manage_optouts = False + reply = yield self.dispatch_command('count') + self.assertFalse(reply['success']) + self.assertEqual( + reply['reason'], + 'Account not allowed to manage optouts.') diff --git a/go/vumitools/account/models.py b/go/vumitools/account/models.py index 55333d752..0f923407e 100644 --- a/go/vumitools/account/models.py +++ b/go/vumitools/account/models.py @@ -42,6 +42,7 @@ class UserAccount(Model): event_handler_config = Json(default=list) msisdn = Unicode(max_length=255, null=True) confirm_start_conversation = Boolean(default=False) + can_manage_optouts = Boolean(default=False) email_summary = Unicode(max_length=255, null=True) tags = Json(default=[]) routing_table = RoutingTableField(default=RoutingTable({}))