Repository: SFDO-Tooling/CumulusCI
Pull request #3074 (2022-02-14 20:02:30)

Derive the MC userinfo endpoint from the marketing_cloud service's rest_instance_url
instead of from the oauth2_client service's auth_uri.
The latter is not available in the context of a tubular worker.

Other changes:
- Moved get_user_info to be a method of the MarketingCloudServiceConfig, because it seemed to fit nicely there
- Adjusted tests to use a real MarketingCloudServiceConfig instead of a mock to make sure we're actually testing it

No release notes because this is a fix for an unreleased bug.


Patch:
diff --git a/cumulusci/core/config/marketing_cloud_service_config.py b/cumulusci/core/config/marketing_cloud_service_config.py
index 08421cd64..2fd023fe3 100644
--- a/cumulusci/core/config/marketing_cloud_service_config.py
+++ b/cumulusci/core/config/marketing_cloud_service_config.py
@@ -1,9 +1,12 @@
 from typing import Dict
 from urllib.parse import urlparse
 
+import requests
+
 from cumulusci.core.config.oauth2_service_config import OAuth2ServiceConfig
 from cumulusci.core.keychain import BaseProjectKeychain
 from cumulusci.oauth.client import OAuth2Client
+from cumulusci.utils.http.requests_utils import safe_json_from_response
 
 
 class MarketingCloudServiceConfig(OAuth2ServiceConfig):
@@ -56,3 +59,17 @@ class MarketingCloudServiceConfig(OAuth2ServiceConfig):
     def save(self):
         assert self._keychain, "Keychain not set on MarketingCloudServiceConfig"
         self._keychain.set_service("marketing_cloud", self._name, self)
+
+    def get_user_info(self) -> dict:
+        """Make a call to the Marketing Cloud REST API UserInfo endpoint.
+        Raises HTTPError for bad response status, otherwise returns the payload
+        in full."""
+        auth_uri = self.rest_instance_url.replace(".rest", ".auth")
+        headers = {
+            "Authorization": f"Bearer {self.access_token}",
+            "Content-Type": "application/json",
+        }
+        response = requests.get(f"{auth_uri}v2/userinfo", headers=headers)
+        response.raise_for_status()
+
+        return safe_json_from_response(response)
diff --git a/cumulusci/tasks/marketing_cloud/base.py b/cumulusci/tasks/marketing_cloud/base.py
index a7f15fccb..8752fbacc 100644
--- a/cumulusci/tasks/marketing_cloud/base.py
+++ b/cumulusci/tasks/marketing_cloud/base.py
@@ -1,5 +1,4 @@
 from cumulusci.core.tasks import BaseTask
-from cumulusci.tasks.marketing_cloud.util import get_mc_user_info
 from cumulusci.utils.xml import lxml_parse_string
 
 
@@ -17,9 +16,6 @@ class BaseMarketingCloudTask(BaseTask):
     def _init_task(self):
         super()._init_task()
         self.mc_config = self.project_config.keychain.get_service("marketing_cloud")
-        self.mc_oauth2_client_config = self.project_config.keychain.get_service(
-            "oauth2_client", self.mc_config.oauth2_client
-        )
 
     def _check_soap_response(self, response):
         """Make sure the response indicates success."""
@@ -38,7 +34,5 @@ class BaseMarketingCloudTask(BaseTask):
 
     def get_mc_stack_key(self) -> str:
         """Return the stack_key associated with this tasks's marketing_cloud service"""
-        user_info_payload = get_mc_user_info(
-            self.mc_oauth2_client_config, self.mc_config
-        )
+        user_info_payload = self.mc_config.get_user_info()
         return user_info_payload["organization"]["stack_key"]
diff --git a/cumulusci/tasks/marketing_cloud/get_user_info.py b/cumulusci/tasks/marketing_cloud/get_user_info.py
index 32f3b1c18..efe49f40c 100644
--- a/cumulusci/tasks/marketing_cloud/get_user_info.py
+++ b/cumulusci/tasks/marketing_cloud/get_user_info.py
@@ -3,7 +3,6 @@ import json
 import requests
 
 from .base import BaseMarketingCloudTask
-from .util import get_mc_user_info
 
 
 class GetUserInfoTask(BaseMarketingCloudTask):
@@ -12,7 +11,7 @@ class GetUserInfoTask(BaseMarketingCloudTask):
 
     def _run_task(self):
         try:
-            payload = get_mc_user_info(self.mc_oauth2_client_config, self.mc_config)
+            payload = self.mc_config.get_user_info()
         except requests.exceptions.HTTPError as e:
             self.logger.error(
                 f"Exception occurred fetching user info: {e.response.text}"
diff --git a/cumulusci/tasks/marketing_cloud/util.py b/cumulusci/tasks/marketing_cloud/util.py
deleted file mode 100644
index 75eda6f04..000000000
--- a/cumulusci/tasks/marketing_cloud/util.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import urllib.parse
-
-import requests
-
-from cumulusci.core.config import ServiceConfig
-from cumulusci.core.config.marketing_cloud_service_config import (
-    MarketingCloudServiceConfig,
-)
-from cumulusci.utils.http.requests_utils import safe_json_from_response
-
-from .mc_constants import MC_API_VERSION
-
-
-def get_mc_user_info(
-    mc_oauth2_client_config: ServiceConfig, mc_config: MarketingCloudServiceConfig
-) -> dict:
-    """Make a call to the Marketing Cloud REST API UserInfo endpoint.
-    Raises HTTPError for bad response status, otherwise returns the payload
-    in full."""
-
-    parsed = urllib.parse.urlparse(mc_oauth2_client_config.auth_uri)
-    hostname_parts = parsed.hostname.split(".")
-    hostname_parts[0] = mc_config.tssd
-    url = parsed._replace(
-        netloc=".".join(hostname_parts), path=f"/{MC_API_VERSION}/userinfo"
-    )
-    endpoint = urllib.parse.urlunparse(url)
-    headers = {
-        "Authorization": f"Bearer {mc_config.access_token}",
-        "Content-Type": "application/json",
-    }
-    response = requests.get(endpoint, headers=headers)
-    response.raise_for_status()
-
-    return safe_json_from_response(response)
diff --git a/cumulusci/version.txt b/cumulusci/version.txt
index 50a071c06..0a1ea1295 100644
--- a/cumulusci/version.txt
+++ b/cumulusci/version.txt
@@ -1,1 +1,1 @@
-3.53.0.dev0
\ No newline at end of file
+3.53.0.dev1
\ No newline at end of file


Test patch:
diff --git a/cumulusci/tasks/marketing_cloud/tests/test_base.py b/cumulusci/tasks/marketing_cloud/tests/test_base.py
index b16a0914e..2e5725f1a 100644
--- a/cumulusci/tasks/marketing_cloud/tests/test_base.py
+++ b/cumulusci/tasks/marketing_cloud/tests/test_base.py
@@ -12,4 +12,3 @@ class TestBaseMarketingCloudTask:
         task._init_task()
 
         assert task.mc_config is not None
-        assert task.mc_oauth2_client_config is not None
diff --git a/cumulusci/tasks/marketing_cloud/tests/test_deploy.py b/cumulusci/tasks/marketing_cloud/tests/test_deploy.py
index eb8ace44c..fb6c2cbb4 100644
--- a/cumulusci/tasks/marketing_cloud/tests/test_deploy.py
+++ b/cumulusci/tasks/marketing_cloud/tests/test_deploy.py
@@ -7,7 +7,10 @@ from unittest import mock
 import pytest
 import responses
 
-from cumulusci.core.config import ServiceConfig, TaskConfig
+from cumulusci.core.config import TaskConfig
+from cumulusci.core.config.marketing_cloud_service_config import (
+    MarketingCloudServiceConfig,
+)
 from cumulusci.core.exceptions import DeploymentException
 from cumulusci.tasks.marketing_cloud.deploy import (
     MCPM_ENDPOINT,
@@ -34,11 +37,13 @@ def task(project_config):
             }
         ),
     )
-    task.mc_config = mock.Mock()
-    task.mc_config.access_token = "foo"
-    task.mc_config.tssd = TEST_TSSD
-    task.mc_oauth2_client_config = ServiceConfig(
-        {"auth_uri": "https://TSSD.auth.marketingcloudapis.com/v2/authorize"}
+    task.mc_config = MarketingCloudServiceConfig(
+        {
+            "rest_instance_url": f"https://{TEST_TSSD}.rest.marketingcloudapis.com/",
+            "access_token": "foo",
+        },
+        "mc",
+        None,
     )
     return task
 
@@ -57,11 +62,13 @@ def task_without_custom_inputs(project_config):
             }
         ),
     )
-    task.mc_config = mock.Mock()
-    task.mc_config.access_token = "foo"
-    task.mc_config.tssd = TEST_TSSD
-    task.mc_oauth2_client_config = ServiceConfig(
-        {"auth_uri": "https://TSSD.auth.marketingcloudapis.com/v2/authorize"}
+    task.mc_config = MarketingCloudServiceConfig(
+        {
+            "rest_instance_url": f"https://{TEST_TSSD}.rest.marketingcloudapis.com/",
+            "access_token": "foo",
+        },
+        "mc",
+        None,
     )
     return task