From 180b8fe9dc1ba2d0f7f21b129ea75d8662128497 Mon Sep 17 00:00:00 2001
From: Carina Antunes <carina.oliveira.antunes@cern.ch>
Date: Thu, 10 Feb 2022 18:58:39 +0100
Subject: [PATCH] Etcd: refresh token

---
 notifications_routing/auditing.py    | 25 ++++++++++++++++-----
 notifications_routing/preferences.py | 10 ++++++---
 notifications_routing/router.py      | 11 ++++++++--
 tests/unit/test_router.py            | 33 +++++++++++++++++++++++++---
 4 files changed, 66 insertions(+), 13 deletions(-)

diff --git a/notifications_routing/auditing.py b/notifications_routing/auditing.py
index 1202dfd..ce11218 100644
--- a/notifications_routing/auditing.py
+++ b/notifications_routing/auditing.py
@@ -5,6 +5,7 @@ import uuid
 from datetime import datetime
 
 from etcd3 import Client
+from etcd3.errors import ErrInvalidAuthToken
 
 from notifications_routing.config import Config
 
@@ -22,16 +23,23 @@ def audit_notification(notification_id, value, user_id=None, key=None):
         logging.info("Audit disabled")
         return
 
-    if not key:
-        key = uuid.uuid4()
-    try:
+    def put():
         client.put(
             (
                 f"/notifications/{notification_id}/{Config.AUDIT_ID}"
                 f"/{'target_users/' + user_id + '/' if user_id else ''}{key}"
             ),
-            json.dumps({"date": datetime.now().strftime("%d/%m/%Y %H:%M:%S"), **value}),
+            json.dumps({"date": datetime.now().strftime("%d/%m/%Y %H:%M:%S"), **value}, default=str),
         )
+
+    if not key:
+        key = uuid.uuid4()
+    try:
+        put()
+    except ErrInvalidAuthToken:
+        logging.debug("refresh etcd token")
+        client.auth(username=Config.ETCD_USER, password=Config.ETCD_PASSWORD)
+        put()
     except Exception:
         logging.exception("Error auditing to etcd3:")
 
@@ -42,7 +50,7 @@ def get_audit_notification(notification_id, key, user_id=None):
         logging.info("Audit disabled")
         return
 
-    try:
+    def get():
         kvs = client.range(
             (
                 f"/notifications/{notification_id}/{Config.AUDIT_ID}"
@@ -52,6 +60,13 @@ def get_audit_notification(notification_id, key, user_id=None):
         if kvs:
             return json.loads(kvs[0].value)
         return None
+
+    try:
+        return get()
+    except ErrInvalidAuthToken:
+        logging.debug("refresh etcd token")
+        client.auth(username=Config.ETCD_USER, password=Config.ETCD_PASSWORD)
+        return get()
     except Exception:
         logging.exception("Error getting from etcd")
         return None
diff --git a/notifications_routing/preferences.py b/notifications_routing/preferences.py
index c1845c7..0fc4f60 100644
--- a/notifications_routing/preferences.py
+++ b/notifications_routing/preferences.py
@@ -26,12 +26,13 @@ class TargetPreference:
     Holds method and devices together.
     """
 
-    def __init__(self, method, devices, scheduledTime, scheduledDay):
+    def __init__(self, method, devices, scheduledTime, scheduledDay, id):
         """Initialize the Target Preference."""
         self.method = method
         self.devices = devices
         self.scheduledTime = scheduledTime
         self.scheduledDay = scheduledDay
+        self.id = id
 
 
 def get_delivery_methods_and_targets(
@@ -64,7 +65,9 @@ def get_delivery_methods_and_targets(
             logging.debug("\tUser Preferences devices: %s", device.name)
 
         delivery_methods_and_targets.add(
-            TargetPreference(preference.type, preference.devices, preference.scheduledTime, preference.scheduledDay)
+            TargetPreference(
+                preference.type, preference.devices, preference.scheduledTime, preference.scheduledDay, preference.id
+            )
         )
 
     return delivery_methods_and_targets
@@ -121,7 +124,7 @@ def apply_user_preferences(
             )
             audit_notification(
                 message[OutputMessageKeys.ID],
-                {"event": "Preference added to feed", "method": preference.method},
+                {"event": "Preference added to feed", "method": preference.method, "preference": preference.id},
                 user_id=user[data_source.EMAIL],
             )
             continue
@@ -137,6 +140,7 @@ def apply_user_preferences(
                         "device": device.type,
                         "subtype": device.subtype if hasattr(device, "subtype") else "",
                         "token": device.token,
+                        "preference": preference.id,
                     },
                     user_id=user[data_source.EMAIL],
                 )
diff --git a/notifications_routing/router.py b/notifications_routing/router.py
index ff21379..9b3f025 100644
--- a/notifications_routing/router.py
+++ b/notifications_routing/router.py
@@ -101,10 +101,10 @@ class Router(megabus.Listener):
                         try:
                             system_user = self.data_source.get_system_user(user[self.data_source.USERNAME])
                             users.append(system_user)
-                            unique_usernames.append(self.data_source.USERNAME)
+                            unique_usernames.append(user[self.data_source.USERNAME])
                         except NotFoundDataSourceError:
                             users.append(user)
-                            unique_usernames.append(self.data_source.USERNAME)
+                            unique_usernames.append(user[self.data_source.USERNAME])
 
         logging.debug("channel %s final users %s", channel_id, users)
 
@@ -220,6 +220,13 @@ class Router(megabus.Listener):
                 # sending to devices per delivery method extracted from preferences
                 apply_user_preferences(self.publisher, self.data_source, delivery_methods_and_targets, message, user)
 
+                if not delivery_methods_and_targets:
+                    audit_notification(
+                        message[OutputMessageKeys.ID],
+                        {"event": "No preferences, skipped user"},
+                        user_id=user[self.data_source.EMAIL],
+                    )
+
             # Resource not found in the DB, try to fallback to sending by email and continue for other users
             # Dont catch all exceptions - could cause the notification to be successfully processed and be lost
             # and not be retried
diff --git a/tests/unit/test_router.py b/tests/unit/test_router.py
index ee688d3..efba0b0 100644
--- a/tests/unit/test_router.py
+++ b/tests/unit/test_router.py
@@ -164,7 +164,9 @@ def device():
 @pytest.fixture(scope="function")
 def target_preference(preference):
     """Target Preference object."""
-    return TargetPreference(preference.type, preference.devices, preference.userId, preference.scheduledDay)
+    return TargetPreference(
+        preference.type, preference.devices, preference.userId, preference.scheduledDay, preference.id
+    )
 
 
 def test_get_channel_users_one_group_without_no_system_users(
@@ -197,16 +199,41 @@ def test_get_channel_users_multiple_groups_with_overlapping_users(
     router_mock, system_user_1, system_user_2, system_user_3, group_user_1, group_user_2, group_user_3
 ):
     """Test get channel users for multiple groups with overlapping users."""
-    router_mock.data_source.get_channel_users.return_value = [system_user_1, system_user_2, system_user_3]
+    router_mock.data_source.get_channel_users.return_value = [system_user_2, system_user_3]
     router_mock.data_source.get_channel_groups.return_value = [
         {"group_id": "186d8dfc-2774-43a8-91b5-a887fcb6ba4a"},
         {"group_id": "ffff6789-2774-43a8-91b5-a887fcb6ba4a"},
     ]
     router_mock.data_source.get_group_users.side_effect = [[group_user_1, group_user_2], [group_user_1, group_user_3]]
+    router_mock.data_source.get_system_user.side_effect = [system_user_1]
+
     assert router_mock.get_channel_users("c3ccc15b-298f-4dc7-877f-2c8970331caf") == [
-        system_user_1,
         system_user_2,
         system_user_3,
+        system_user_1,
+    ]
+
+
+def test_get_channel_no_users_multiple_groups_with_overlapping_users(
+    router_mock, system_user_1, group_user_1, group_user_2, group_user_3
+):
+    """Test get channel users w/out users for multiple groups with overlapping users."""
+    router_mock.data_source.get_channel_users.return_value = []
+    router_mock.data_source.get_channel_groups.return_value = [
+        {"group_id": "186d8dfc-2774-43a8-91b5-a887fcb6ba4a"},
+        {"group_id": "ffff6789-2774-43a8-91b5-a887fcb6ba4a"},
+    ]
+    router_mock.data_source.get_group_users.side_effect = [[group_user_1, group_user_2], [group_user_1, group_user_3]]
+    router_mock.data_source.get_system_user.side_effect = [
+        system_user_1,
+        NotFoundDataSourceError(""),
+        NotFoundDataSourceError(""),
+    ]
+
+    assert router_mock.get_channel_users("c3ccc15b-298f-4dc7-877f-2c8970331caf") == [
+        system_user_1,
+        group_user_2,
+        group_user_3,
     ]
 
 
-- 
GitLab