def postUpdateHook(cls, awards, updated_attr_list, is_new_list): # Note, updated_attr_list will always be empty, for now # Still needs to be implemented in updateMerge # See helpers.EventManipulator events = [] for (award, updated_attrs) in zip(awards, updated_attr_list): event = award.event if event not in events: events.append(event) for event in events: if event.get().within_a_day: try: NotificationHelper.send_award_update(event.get()) except Exception: logging.error("Error sending award update for {}".format( event.id())) try: TBANSHelper.awards(event.get()) except Exception: logging.error("Error sending award update for {}".format( event.id())) # Enqueue task to calculate district points for event in events: taskqueue.add(url='/tasks/math/do/district_points_calc/{}'.format( event.id()), method='GET')
def test_send_empty(self): notification = MockNotification() with patch.object(TBANSHelper, '_defer_fcm') as mock_fcm, patch.object( TBANSHelper, '_defer_webhook') as mock_webhook: TBANSHelper._send([], notification) mock_fcm.assert_not_called() mock_webhook.assert_not_called()
def test_send_fcm_retry_backoff_time(self): client = MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id='messaging_id', client_type=ClientType.OS_IOS) client.put() import time batch_response = messaging.BatchResponse([ messaging.SendResponse(None, QuotaExceededError('code', 'message')) ]) for i in range(0, 6): with patch.object( FCMRequest, 'send', return_value=batch_response), patch('logging.error'): call_time = time.time() TBANSHelper._send_fcm([client], MockNotification(), i) # Check that we queue'd for a retry with the proper countdown time tasks = self.taskqueue_stub.get_filtered_tasks( queue_names='push-notifications') if i > 0: self.assertGreater(tasks[0].eta_posix - call_time, 0) # Make sure our taskqueue tasks execute what we expect with patch.object(TBANSHelper, '_send_fcm') as mock_send_fcm: deferred.run(tasks[0].payload) mock_send_fcm.assert_called_once_with([client], ANY, i + 1) self.taskqueue_stub.FlushQueue('push-notifications')
def test_broadcast_fcm(self): for client_type in ClientType.FCM_CLIENTS: client = MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id='token', client_type=client_type, device_uuid='uuid', display_name='Phone') client_key = client.put() from notifications.base_notification import BaseNotification with patch.object(BaseNotification, 'send') as mock_send: TBANSHelper.broadcast([client_type], 'Broadcast', 'Test broadcast') # Make sure we didn't send to Android mock_send.assert_not_called() # Make sure we'll send to FCM clients tasks = self.taskqueue_stub.get_filtered_tasks( queue_names='push-notifications') self.assertEqual(len(tasks), 1) # Make sure our taskqueue tasks execute what we expect with patch.object(TBANSHelper, '_send_fcm') as mock_send_fcm: deferred.run(tasks[0].payload) mock_send_fcm.assert_called_once_with([client], ANY) # Make sure the notification is a BroadcastNotification notification = mock_send_fcm.call_args[0][1] self.assertTrue(isinstance(notification, BroadcastNotification)) self.taskqueue_stub.FlushQueue('push-notifications') client_key.delete()
def test_awards(self): from notifications.awards_updated import AwardsUpdatedNotification with patch.object(TBANSHelper, '_send') as mock_send: TBANSHelper.awards(self.event) # Make sure we sent to FCM/webhooks mock_send.assert_called_once() notification = mock_send.call_args[0][1] self.assertTrue(isinstance(notification, AwardsNotification)) self.assertEqual(notification.event, self.event)
def test_broadcast_none(self): from notifications.base_notification import BaseNotification with patch.object(BaseNotification, 'send') as mock_send: TBANSHelper.broadcast([], 'Broadcast', 'Test broadcast') # Make sure we didn't send to Android mock_send.assert_not_called() # Make sure we didn't send to FCM or webhooks tasks = self.taskqueue_stub.GetTasks('push-notifications') self.assertEqual(len(tasks), 0)
def test_ping_fcm_unsupported(self): client = MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id='token', client_type=-1, device_uuid='uuid', display_name='Phone') with self.assertRaises(Exception, msg='Unsupported FCM client type: -1'): TBANSHelper._ping_client(client)
def test_send_fcm_maximum_backoff(self): for i in range(0, 6): exit_code = TBANSHelper._send_fcm([], MockNotification(), backoff_iteration=i) self.assertEqual(exit_code, 0) # Backoff should start failing at 6 exit_code = TBANSHelper._send_fcm([], MockNotification(), backoff_iteration=6) self.assertEqual(exit_code, 2)
def postUpdateHook(cls, event_details_list, updated_attr_list, is_new_list): """ To run after models have been updated """ for (event_details, updated_attrs) in zip(event_details_list, updated_attr_list): event = Event.get_by_id(event_details.key.id()) if event.within_a_day and "alliance_selections" in updated_attrs: try: NotificationHelper.send_alliance_update(event) except Exception: logging.error( "Error sending alliance update notification for {}". format(event.key_name)) logging.error(traceback.format_exc()) try: TBANSHelper.alliance_selection(event) except Exception: logging.error( "Error sending alliance update notification for {}". format(event.key_name)) logging.error(traceback.format_exc()) # Enqueue task to calculate district points try: taskqueue.add( url='/tasks/math/do/district_points_calc/{}'.format( event.key.id()), method='GET') except Exception: logging.error( "Error enqueuing district_points_calc for {}".format( event.key.id())) logging.error(traceback.format_exc()) # Enqueue task to calculate event team status try: taskqueue.add(url='/tasks/math/do/event_team_status/{}'.format( event.key.id()), method='GET') except Exception: logging.error( "Error enqueuing event_team_status for {}".format( event.key.id())) logging.error(traceback.format_exc()) try: FirebasePusher.update_event_details(event_details) except Exception: logging.warning("Firebase update_event_details failed!")
def ping_client(self, request): current_user = endpoints.get_current_user() if current_user is None: return BaseResponse(code=401, message="Unauthorized to ping client") user_id = PushHelper.user_email_to_id(current_user.email()) gcm_id = request.mobile_id # Find a Client for the current user with the passed GCM ID clients = MobileClient.query(MobileClient.messaging_id == gcm_id, ancestor=ndb.Key(Account, user_id)).fetch(1) if len(clients) == 0: # No Client for user with that push token - bailing return BaseResponse(code=404, message="Invalid push token for user") else: client = clients[0] from helpers.tbans_helper import TBANSHelper success = TBANSHelper.ping(client) if success: return BaseResponse(code=200, message="Ping sent") else: return BaseResponse(code=500, message="Failed to ping client")
def test_send_fcm_unhandled_error(self): client = MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id='messaging_id', client_type=ClientType.OS_IOS) client.put() # Sanity check self.assertEqual(fcm_messaging_ids('user_id'), ['messaging_id']) batch_response = messaging.BatchResponse( [messaging.SendResponse(None, FirebaseError('code', 'message'))]) with patch.object(FCMRequest, 'send', return_value=batch_response), patch( 'logging.error') as mock_error: exit_code = TBANSHelper._send_fcm([client], MockNotification()) self.assertEqual(exit_code, 0) mock_error.assert_called_once_with( 'Unhandled FCM error for messaging_id - code / message') # Sanity check self.assertEqual(fcm_messaging_ids('user_id'), ['messaging_id']) # Check that we didn't queue for a retry tasks = self.taskqueue_stub.get_filtered_tasks( queue_names='push-notifications') self.assertEqual(len(tasks), 0)
def post(self): self._require_registration() # Check to make sure that they aren't trying to edit another user current_user_account_id = self.user_bundle.account.key.id() target_account_id = self.request.get('account_id') if target_account_id == current_user_account_id: url = self.request.get('url') secret_key = self.request.get('secret') query = MobileClient.query(MobileClient.messaging_id == url, ancestor=ndb.Key( Account, current_user_account_id)) if query.count() == 0: # Webhook doesn't exist, add it response = TBANSHelper.verify_webhook(url, secret_key) client = MobileClient( parent=self.user_bundle.account.key, user_id=current_user_account_id, messaging_id=url, display_name=self.request.get('name'), secret=secret_key, client_type=ClientType.WEBHOOK, verified=False, verification_code=response.verification_key) client.put() else: # Webhook already exists. Update the secret current = query.fetch()[0] current.secret = secret_key current.put() self.redirect('/account') else: self.redirect('/')
def post(self): self._require_registration() current_user_account_id = self.user_bundle.account.key.id() target_account_id = self.request.get('account_id') if target_account_id == current_user_account_id: client_id = self.request.get('client_id') webhook = MobileClient.get_by_id(int(client_id), parent=ndb.Key( Account, current_user_account_id)) if webhook.client_type == ClientType.WEBHOOK and current_user_account_id == webhook.user_id: response = TBANSHelper.verify_webhook(webhook.messaging_id, webhook.secret) webhook.verification_code = response.verification_key webhook.verified = False webhook.put() self.redirect('/account') return else: logging.warning("Not webhook, or wrong owner") else: logging.warning("Users don't match. " + current_user_account_id + "/" + target_account_id) self.redirect('/')
def test_send_fcm_unavailable_error(self): client = MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id='messaging_id', client_type=ClientType.OS_IOS) client.put() # Sanity check self.assertEqual(fcm_messaging_ids('user_id'), ['messaging_id']) batch_response = messaging.BatchResponse([ messaging.SendResponse(None, UnavailableError('code', 'message')) ]) with patch.object(FCMRequest, 'send', return_value=batch_response), patch( 'logging.error') as mock_error: exit_code = TBANSHelper._send_fcm([client], MockNotification()) self.assertEqual(exit_code, 0) mock_error.assert_called_once_with( 'FCM unavailable - retrying client...') # Sanity check self.assertEqual(fcm_messaging_ids('user_id'), ['messaging_id']) # Check that we queue'd for a retry tasks = self.taskqueue_stub.get_filtered_tasks( queue_names='push-notifications') self.assertEqual(len(tasks), 1) # Make sure our taskqueue tasks execute what we expect with patch.object(TBANSHelper, '_send_fcm') as mock_send_fcm: deferred.run(tasks[0].payload) mock_send_fcm.assert_called_once_with([client], ANY, 1)
def test_send_fcm_invalid_argument_error(self): client = MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id='messaging_id', client_type=ClientType.OS_IOS) client.put() # Sanity check self.assertEqual(fcm_messaging_ids('user_id'), ['messaging_id']) batch_response = messaging.BatchResponse([ messaging.SendResponse(None, InvalidArgumentError('code', 'message')) ]) with patch.object(FCMRequest, 'send', return_value=batch_response), patch( 'logging.critical') as mock_critical: exit_code = TBANSHelper._send_fcm([client], MockNotification()) self.assertEqual(exit_code, 0) mock_critical.assert_called_once_with( 'Invalid argument when sending to FCM - code') # Sanity check self.assertEqual(fcm_messaging_ids('user_id'), ['messaging_id']) # Make sure we haven't queued for a retry tasks = self.taskqueue_stub.get_filtered_tasks( queue_names='push-notifications') self.assertEqual(len(tasks), 0)
def test_send_fcm_sender_id_mismatch_error(self): client = MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id='messaging_id', client_type=ClientType.OS_IOS) client.put() # Sanity check self.assertEqual(fcm_messaging_ids('user_id'), ['messaging_id']) batch_response = messaging.BatchResponse([ messaging.SendResponse(None, SenderIdMismatchError('code', 'message')) ]) with patch.object(FCMRequest, 'send', return_value=batch_response), \ patch.object(MobileClient, 'delete_for_messaging_id', wraps=MobileClient.delete_for_messaging_id) as mock_delete, \ patch('logging.info') as mock_info: exit_code = TBANSHelper._send_fcm([client], MockNotification()) mock_delete.assert_called_once_with('messaging_id') self.assertEqual(exit_code, 0) mock_info.assert_called_with( 'Deleting mismatched client with ID: messaging_id') # Sanity check self.assertEqual(fcm_messaging_ids('user_id'), []) # Make sure we haven't queued for a retry tasks = self.taskqueue_stub.get_filtered_tasks( queue_names='push-notifications') self.assertEqual(len(tasks), 0)
def test_verification(self): from models.notifications.requests.webhook_request import WebhookRequest with patch.object(WebhookRequest, 'send') as mock_send: verification_key = TBANSHelper.verify_webhook( 'https://thebluealliance.com', 'secret') mock_send.assert_called_once() self.assertIsNotNone(verification_key)
def send_ping(cls, client): if client.client_type == ClientType.OS_ANDROID: notification = PingNotification() notification.send({client.client_type: [client.messaging_id]}) else: # Send iOS/web/webhooks ping via TBANS return TBANSHelper.ping(client)
def test_defer_webhook(self): client = MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id='messaging_id', client_type=ClientType.WEBHOOK) client.put() notification = MockNotification() TBANSHelper._defer_webhook([client], notification) # Make sure we'll send to FCM clients tasks = self.taskqueue_stub.get_filtered_tasks( queue_names='push-notifications') self.assertEqual(len(tasks), 1) # Make sure our taskqueue tasks execute what we expect with patch.object(TBANSHelper, '_send_webhook') as mock_send_webhook: deferred.run(tasks[0].payload) mock_send_webhook.assert_called_once_with([client], ANY)
def test_debug_string_response(self): class MockResponse: def json(self): import json return json.dumps({'mock': 'mock'}) exception = FirebaseError('code', 'message', None, MockResponse()) self.assertEqual(TBANSHelper._debug_string(exception), 'code / message / {"mock": "mock"}')
def test_send_webhook_disabled(self): from sitevars.notifications_enable import NotificationsEnable NotificationsEnable.enable_notifications(False) with patch.object(NotificationsEnable, 'notifications_enabled', wraps=NotificationsEnable.notifications_enabled ) as mock_check_enabled: exit_code = TBANSHelper._send_webhook([], MockNotification()) mock_check_enabled.assert_called_once() self.assertEqual(exit_code, 1)
def test_broadcast_android(self): client_type = ClientType.OS_ANDROID messaging_id = 'token' client = MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id=messaging_id, client_type=client_type, device_uuid='uuid', display_name='Phone') client.put() from notifications.broadcast import BroadcastNotification with patch.object(BroadcastNotification, 'send') as mock_send: TBANSHelper.broadcast([client_type], 'Broadcast', 'Test broadcast') mock_send.assert_called_once_with({client_type: [messaging_id]}) # Make sure we didn't send to FCM or webhooks tasks = self.taskqueue_stub.GetTasks('push-notifications') self.assertEqual(len(tasks), 0)
def post(self): self._require_admin() user_id = self.user_bundle.account.key.id() notification_type = self.request.get('type') if notification_type == "awards": event_key = self.request.get('event_key') event = Event.get_by_id(event_key) if not event: self.template_values.update( {'error': 'No event for key {}'.format(event_key)}) return self.redirect('/admin/tbans') TBANSHelper.awards(event, user_id) elif notification_type == "ping": clients = MobileClient.clients([user_id]) for client in clients: TBANSHelper.ping(client) return self.redirect('/admin/tbans')
def postUpdateHook(cls, matches, updated_attr_list, is_new_list): ''' To run after the match has been updated. Send push notifications to subscribed users Only if the match is part of an active event ''' unplayed_match_events = [] for (match, updated_attrs, is_new) in zip(matches, updated_attr_list, is_new_list): event = match.event.get() # Only continue if the event is currently happening if event.now: if match.has_been_played: if is_new or 'alliances_json' in updated_attrs: # There is a score update for this match, push a notification logging.info( "Sending push notifications for {}".format( match.key_name)) try: NotificationHelper.send_match_score_update(match) except Exception, exception: logging.error( "Error sending match updates: {}".format( exception)) logging.error(traceback.format_exc()) try: TBANSHelper.match_score(match) except Exception, exception: logging.error( "Error sending match {} updates: {}".format( match.key_name, exception)) logging.error(traceback.format_exc()) else: if is_new or (set([ 'alliances_json', 'time', 'time_string' ]).intersection(set(updated_attrs)) != set()): # The match has not been played and we're changing a property that affects the event's schedule # So send a schedule update notification for the parent event if event not in unplayed_match_events: unplayed_match_events.append(event)
def test_ping_client(self): client = MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id='token', client_type=ClientType.OS_IOS, device_uuid='uuid', display_name='Phone') with patch.object(TBANSHelper, '_ping_client', return_value=True) as mock_ping_client: success = TBANSHelper.ping(client) mock_ping_client.assert_called_once_with(client) self.assertTrue(success)
def test_ping_webhook(self): client = MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id='https://thebluealliance.com', client_type=ClientType.WEBHOOK, secret='secret', display_name='Webhook') with patch.object(TBANSHelper, '_ping_webhook', return_value=True) as mock_ping_webhook: success = TBANSHelper.ping(client) mock_ping_webhook.assert_called_once_with(client) self.assertTrue(success)
def test_ping_webhook_failure(self): client = MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id='https://thebluealliance.com', client_type=ClientType.WEBHOOK, secret='secret', display_name='Webhook') from models.notifications.requests.webhook_request import WebhookRequest with patch.object(WebhookRequest, 'send', return_value=False) as mock_send: success = TBANSHelper._ping_webhook(client) mock_send.assert_called_once() self.assertFalse(success)
def postUpdateHook(cls, awards, updated_attr_list, is_new_list): # Note, updated_attr_list will always be empty, for now # Still needs to be implemented in updateMerge # See helpers.EventManipulator events = [] for (award, updated_attrs) in zip(awards, updated_attr_list): event = award.event if event not in events: events.append(event) for event in events: if event.get().within_a_day: try: NotificationHelper.send_award_update(event.get()) except Exception: logging.error("Error sending award update for {}".format( event.id())) try: TBANSHelper.awards(event.get()) except Exception, exception: logging.error("Error sending {} award updates: {}".format( event.id(), exception)) logging.error(traceback.format_exc())
def test_send_webhook_multiple(self): clients = [ MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id='{}'.format(i), client_type=ClientType.WEBHOOK) for i in range(3) ] batch_response = messaging.BatchResponse([]) with patch.object(WebhookRequest, 'send', return_value=batch_response) as mock_send: exit_code = TBANSHelper._send_webhook(clients, MockNotification()) self.assertEqual(mock_send.call_count, 3) self.assertEqual(exit_code, 0)
def test_send(self): expected = [ 'client_type_{}'.format(client_type) for client_type in ClientType.FCM_CLIENTS ] clients = [ MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id='client_type_{}'.format(client_type), client_type=client_type) for client_type in ClientType.names.keys() ] # Insert an unverified webhook, just to test unverified = MobileClient(parent=ndb.Key(Account, 'user_id'), user_id='user_id', messaging_id='client_type_2', client_type=ClientType.WEBHOOK, verified=False) unverified.put() for c in clients: c.put() expected_fcm = [ c for c in clients if c.client_type in ClientType.FCM_CLIENTS ] expected_webhook = [ c for c in clients if c.client_type == ClientType.WEBHOOK ] notification = MockNotification() with patch.object(TBANSHelper, '_defer_fcm') as mock_fcm, patch.object( TBANSHelper, '_defer_webhook') as mock_webhook: TBANSHelper._send(['user_id'], notification) mock_fcm.assert_called_once_with(expected_fcm, notification) mock_webhook.assert_called_once_with(expected_webhook, notification)
def get(self): webhooks = MobileClient.query(MobileClient.client_type == ClientType.WEBHOOK).fetch() failures = [] for client in webhooks: response = TBANSHelper.ping_webhook(client) if not response.code == 200: failures.append(client.key) count = len(failures) if failures: ndb.delete_multi(failures) logging.info("Deleted {} broken webhooks".format(count)) template_values = {'count': count} path = os.path.join(os.path.dirname(__file__), '../../templates/admin/webhooks_clear_do.html') self.response.out.write(template.render(path, template_values))
def post(self): self._require_registration() self._require_request_user_is_bundle_user() # Name and URL must be non-None url = self.request.get('url', None) name = self.request.get('name', None) if not url or not name: return self.redirect('/webhooks/add?error=1') # Secret may be none - but we'll generate a secret for the user secret = self.request.get('secret', None) if not secret: import uuid secret = uuid.uuid4().hex current_user_account_id = self.user_bundle.account.key.id() query = MobileClient.query(MobileClient.messaging_id == url, ancestor=ndb.Key(Account, current_user_account_id)) if query.count() == 0: # Webhook doesn't exist, add it from helpers.tbans_helper import TBANSHelper response = TBANSHelper.verify_webhook(url, secret) client = MobileClient( parent=self.user_bundle.account.key, user_id=current_user_account_id, messaging_id=url, display_name=name, secret=secret, client_type=ClientType.WEBHOOK, verified=False, verification_code=response.verification_key) client.put() else: # Webhook already exists. Update the secret current = query.fetch()[0] current.secret = secret current.put() self.redirect('/account')
def post(self): self._require_registration() self._require_request_user_is_bundle_user() current_user_account_id = self.user_bundle.account.key.id() if not current_user_account_id: return self.redirect('/') client_id = self.request.get('client_id') if not client_id: return self.redirect('/') webhook = MobileClient.get_by_id(int(client_id), parent=ndb.Key(Account, current_user_account_id)) if not webhook or webhook.client_type != ClientType.WEBHOOK or current_user_account_id != webhook.user_id: return self.redirect('/') from helpers.tbans_helper import TBANSHelper response = TBANSHelper.verify_webhook(webhook.messaging_id, webhook.secret) webhook.verification_code = response.verification_key webhook.verified = False webhook.put() return self.redirect('/account')
def test_create_service(self): service = TBANSHelper._create_service() self.assertIsNotNone(service)
def test_ping_client(self): TBANSHelper._create_service = self._create_mock_service client = MobileClient(client_type=ClientType.OS_IOS) TBANSHelper.ping_client(client)
def test_ping_webhook(self): TBANSHelper._create_service = self._create_mock_service client = MobileClient(client_type=ClientType.WEBHOOK) TBANSHelper.ping_webhook(client)
def test_ping_webhook(self): TBANSHelper._create_service = self._create_mock_service TBANSHelper.verify_webhook(url='abc', secret='def')