Repository: sosw/sosw

Issue:
Fix `self.aws_account` initialization
In `app.Processor.__init__`
And make sure it doesn't break other use-cases

Should try to work when run both in Lambda and from local script.
- When running in Lambda definitely take from the context.
- When running locally - call STS.

And this one has to be covered carefully with both Unit and Integration tests in different runtimes.
This issue appeared somewhere when upgrading from older versions of Python.

Discussion:

Patch:
diff --git a/sosw/app.py b/sosw/app.py
index ae6ac18..aee950a 100644
--- a/sosw/app.py
+++ b/sosw/app.py
@@ -74,13 +74,13 @@ class Processor:
 
         self.test = kwargs.get('test') or True if os.environ.get('STAGE') in ['test', 'autotest'] else False
 
-        self.lambda_context = kwargs.pop('context', None)
-        if self.lambda_context:
-            logger.warning("DEPRECATED: Processor.lambda_context is deprecated. Use global_vars.lambda_context instead")
-            self.aws_account = trim_arn_to_account(self.lambda_context.invoked_function_arn)
+        if global_vars.lambda_context:
+            self.aws_account = trim_arn_to_account(getattr(global_vars.lambda_context, 'invoked_function_arn', None))
+            logger.info("Setting self.aws_account from Lambda context to: %s", self.aws_account)
 
         self.init_config(custom_config=custom_config)
-        logger.info(f"Final {self.__class__.__name__} processor config: {self.config}")
+        logger.info("Final %s processor config", self.__class__.__name__)
+        logger.info(self.config)
 
         self.stats = defaultdict(int)
         self.result = defaultdict(int)
@@ -94,7 +94,9 @@ class Processor:
         After that, a specific custom config of the Lambda will recursively update the existing one.
         The last step is update config recursively with a passed custom_config.
 
-        Overwrite this method if custom logic of recursive updates in configs is required
+        Overwrite this method if custom logic of recursive updates in configs is required.
+
+        ..  note:: Read more about :ref:`Config_Sourse`
 
         :param Dict custom_config: dict with custom configurations
         """
@@ -229,17 +231,15 @@ class Processor:
         """
         Get current AWS Account to construct different ARNs.
 
-        We dont' have this parameter in Environmental variables, only can parse from Context.
-        Context is not global and is supposed to be passed by your `lambda_handler` during initialization.
-
-        As a fallback we have an autodetection mechanism, but it is pretty heavy (~0.3 seconds).
-        So it is not called by default. This method should be used only if you really need it.
+        We don't have this parameter in Environmental variables, only can parse from Context. It is stored
+        in ``global_vars`` and is supposed to be passed by your `lambda_handler` during initialization.
 
-        It is highly recommended to pass the `context` during initialization.
+        As a fallback for cases when we use ``Processor`` not in the Lambda environment, we have a lazy autodetection
+        mechanism using STS, but it is pretty heavy (~0.3 seconds).
 
         Some things to note:
          - We store this value in class variable for fast access
-         - If not yet set on the first call we initialise it.
+         - It uses Lazy initialization.
          - We first try from context and only if not provided - use the autodetection.
         """
 
@@ -444,8 +444,8 @@ def get_lambda_handler(processor_class, global_vars=None, custom_config=None):
     """
 
     if global_vars is None:
-        logging.error(f"Your Lambda did not pass global_vars. It should be an instance of LambdaGlobals class, "
-                      f"initialised in your Lambda function at the root level. Some functionality will break soon.")
+        logger.error("Your Lambda did not pass global_vars. It should be an instance of LambdaGlobals class, "
+                     "initialised in your Lambda function at the root level. Some functionality will break soon.")
         global_vars = LambdaGlobals()
 
 
@@ -461,9 +461,10 @@ def get_lambda_handler(processor_class, global_vars=None, custom_config=None):
         if event.get('logging_level'):
             logger.setLevel(event.get('logging_level'))
 
-        logger.info(f"Called {os.environ.get('AWS_LAMBDA_FUNCTION_NAME')} lambda of "
-                    f"version {os.environ.get('AWS_LAMBDA_FUNCTION_VERSION')} with __name__: {__name__},"
-                    f"event: {event}, context: {context}")
+        logger.info("Called %s lambda of version %s with __name__: %s, context: %s",
+                    os.environ.get('AWS_LAMBDA_FUNCTION_NAME'), os.environ.get('AWS_LAMBDA_FUNCTION_VERSION'),
+                    __name__, context)
+        logger.info(event)
 
         test = event.get('test') or True if os.environ.get('STAGE') in ['test', 'autotest'] else False
 
@@ -474,12 +475,13 @@ def get_lambda_handler(processor_class, global_vars=None, custom_config=None):
 
         result = global_vars.processor(event)
 
-        logger.info(f"stats: {global_vars.processor.get_stats()}")
-        global_vars.processor.reset_stats()
-        logger.info(f"result: {result}")
+        logger.info(global_vars.processor.get_stats())
 
+        global_vars.processor.reset_stats()
         global_vars.processor.reset_stats(recursive=True)
 
+        logger.info(result)
+
         return result
 
 
diff --git a/sosw/components/config.py b/sosw/components/config.py
index 2e5cd82..8e9c552 100644
--- a/sosw/components/config.py
+++ b/sosw/components/config.py
@@ -132,13 +132,13 @@ class SecretsManager:
         can_paginate = getattr(secretsmanager_client, 'can_paginate')(f)
 
         if can_paginate:
-            logging.debug("SecretsManager.%s can natively paginate", f)
+            logger.debug("SecretsManager.%s can natively paginate", f)
             paginator = secretsmanager_client.get_paginator(f)
             response = paginator.paginate(**kwargs)
             return response
 
         else:
-            logging.debug("SecretsManager.%s can not natively paginate", f)
+            logger.debug("SecretsManager.%s can not natively paginate", f)
             response_list = []
             response = func(**kwargs)
             response_list.append(response)
@@ -187,7 +187,7 @@ class SecretsManager:
                 secret_value = secretsmanager_client.get_secret_value(SecretId=secret['ARN'])
                 secrets_dict[secret['Name']] = secret_value['SecretString']
         else:
-            logging.warning('No credentials found in SecretsManager for %s with %s', filter_type, value)
+            logger.warning('No credentials found in SecretsManager for %s with %s', filter_type, value)
             return secrets_dict
 
         return secrets_dict


Test patch:
diff --git a/sosw/test/unit/test_app.py b/sosw/test/unit/test_app.py
index b2e993a..9236d6e 100644
--- a/sosw/test/unit/test_app.py
+++ b/sosw/test/unit/test_app.py
@@ -2,7 +2,6 @@ import boto3
 import os
 import unittest
 
-from unittest import mock
 from unittest.mock import MagicMock, patch
 
 os.environ["STAGE"] = "test"
@@ -26,21 +25,26 @@ class app_UnitTestCase(unittest.TestCase):
     def setUp(self):
         pass
 
-
     def tearDown(self):
         try:
             del (os.environ['AWS_LAMBDA_FUNCTION_NAME'])
         except Exception:
             pass
 
+        # global_vars.processor is a property that refers to another global. So we have to reset it explicitly.
+        # And at the same time we don't want to reset it during reinitialization in the working environment
+        global _processor, global_vars
+        global_vars = LambdaGlobals()
+        global_vars.processor = None
+
 
-    @mock.patch("boto3.client")
+    @patch("boto3.client")
     def test_app_init(self, mock_boto_client):
         Processor(custom_config=self.TEST_CONFIG)
         self.assertTrue(True)
 
 
-    @mock.patch("boto3.client")
+    @patch("boto3.client")
     def test_app__pre_call__reset_stats(self, _):
         processor = Processor(custom_config=self.TEST_CONFIG)
         processor.__call__(event={'k': 'success'})
@@ -50,7 +54,7 @@ class app_UnitTestCase(unittest.TestCase):
         self.assertEqual(processor.stats['total_processor_calls'], 1)
 
 
-    @mock.patch("boto3.client")
+    @patch("boto3.client")
     def test_app_init__with_some_clients(self, mock_boto_client):
         custom_config = {
             'init_clients': ['Sns', 'Siblings'],
@@ -65,7 +69,7 @@ class app_UnitTestCase(unittest.TestCase):
         self.assertIsNotNone(getattr(processor, 'siblings_client'))
 
 
-    @mock.patch("boto3.client")
+    @patch("boto3.client")
     def test_app_init__boto_and_components_custom_clients(self, mock_boto_client):
         custom_config = {
             'init_clients': ['dynamodb', 'Siblings'],
@@ -82,7 +86,7 @@ class app_UnitTestCase(unittest.TestCase):
         self.assertEqual(str(type(getattr(processor, 'dynamodb_client'))), str(type(boto3.client('dynamodb'))))
 
 
-    @mock.patch("boto3.client")
+    @patch("boto3.client")
     def test_app_init__with_some_invalid_client(self, mock_boto_client):
         custom_config = {
             'init_clients': ['NotExists']
@@ -91,7 +95,7 @@ class app_UnitTestCase(unittest.TestCase):
         mock_boto_client.assert_called_with('not_exists')
 
 
-    @mock.patch("sosw.app.get_config")
+    @patch("sosw.app.get_config")
     def test_app_calls_get_config(self, mock_ssm):
 
         mock_ssm.return_value = {'mock': 'called'}
@@ -113,6 +117,9 @@ class app_UnitTestCase(unittest.TestCase):
 
     def test_lambda_handler(self):
 
+        mock_context = MagicMock()
+        mock_context.invoked_function_arn = 'arn:aws:lambda:us-east-1:123456789012:function:example:42'
+
         global_vars = LambdaGlobals()
         self.assertIsNone(global_vars.processor)
         self.assertIsNone(global_vars.lambda_context)
@@ -121,25 +128,53 @@ class app_UnitTestCase(unittest.TestCase):
         self.assertIsNotNone(lambda_handler)
 
         for i in range(3):
-            result = lambda_handler(event={'k': 'success'}, context={'context': 'test'})
+            result = lambda_handler(event={'k': 'success'}, context=mock_context)
             self.assertEqual(type(global_vars.processor), self.Child)
-            self.assertEqual(global_vars.lambda_context, {'context': 'test'})
+            self.assertEqual(global_vars.lambda_context, mock_context)
             self.assertEqual(result, 'success')
             self.assertEqual(global_vars.processor.stats['total_processor_calls'], i + 1)
             self.assertEqual(global_vars.processor.stats['total_calls_register_clients'], 1)
 
 
-    @mock.patch("boto3.client")
+    def test_property_account__initialized_from_context(self):
+        mock_context = MagicMock()
+        mock_context.invoked_function_arn = 'arn:aws:lambda:us-east-1:123456789000:function:example:42'
+
+        self.assertIsNone(global_vars.lambda_context)
+
+        lambda_handler = get_lambda_handler(self.Child, global_vars, self.TEST_CONFIG)
+        lambda_handler(event={'k': 'success'}, context=mock_context)
+
+        self.assertEqual('123456789000', global_vars.processor._account)
+
+
+    @patch("boto3.client")
+    def test_property_account__initialized_from_sts(self, boto_client_mock):
+
+        get_caller_identity_mock = MagicMock()
+        get_caller_identity_mock.get.return_value='001234567890'
+
+        client_mock = MagicMock()
+        client_mock.get_caller_identity.return_value = get_caller_identity_mock
+
+        boto_client_mock.return_value = client_mock
+
+        p = Processor()
+        self.assertEqual('001234567890', p._account)
+        get_caller_identity_mock.get.assert_called_once_with('Account')
+
+
+    @patch("boto3.client")
     @patch.object(logger, 'setLevel')
     def test_lambda_handler__logger_level(self, logger_set_level, client_mock):
         global_vars = LambdaGlobals()
         lambda_handler = get_lambda_handler(self.Child, global_vars, self.TEST_CONFIG)
         event = {'k': 'm', 'logging_level': 20}
-        lambda_handler(event=event, context={'context': 'test'})
+        lambda_handler(event=event, context=None)
         logger_set_level.assert_called_once_with(20)
 
 
-    @mock.patch("boto3.client")
+    @patch("boto3.client")
     def test_die(self, mock_boto):
 
         p = Processor(custom_config=self.TEST_CONFIG)
@@ -148,7 +183,7 @@ class app_UnitTestCase(unittest.TestCase):
             p.die()
 
 
-    @mock.patch("boto3.client")
+    @patch("boto3.client")
     def test_die__uncatchable_death(self, mock_boto):
 
         class Child(Processor):
@@ -164,7 +199,7 @@ class app_UnitTestCase(unittest.TestCase):
             p.catch_me()
 
 
-    @mock.patch("boto3.client")
+    @patch("boto3.client")
     def test_die__calls_sns(self, mock_boto):
 
         mock_boto_client = MagicMock()