Repository: mozmeao/basket

Issue:
Fix country code handling to CTMS
https://mozilla.sentry.io/issues/5141542909/events/a6cb34c44f2549288b4eb4ddb9a9ebb0/

```
AttributeError: 'NoneType' object has no attribute 'strip'
  File "rq/worker.py", line 1431, in perform_job
    rv = job.perform()
  File "rq/job.py", line 1280, in perform
    self._result = self._execute()
  File "rq/job.py", line 1317, in _execute
    result = self.func(*self.args, **self.kwargs)
  File "basket/news/tasks.py", line 143, in fxa_verified
    upsert_contact(SUBSCRIBE, new_data, user_data)
  File "basket/news/tasks.py", line 345, in upsert_contact
    new_user = ctms.add(update_data)
  File "basket/news/backends/ctms.py", line 885, in add
    return self.interface.post_to_create(to_vendor(data))
  File "basket/news/backends/ctms.py", line 298, in to_vendor
    value = processor(raw_value)
  File "basket/news/backends/ctms.py", line 177, in process_country
    country = raw_country.strip().lower()
```

We can likely check if country has a value first before processing.


Discussion:

Resolving patch:
diff --git a/basket/news/backends/ctms.py b/basket/news/backends/ctms.py
--- a/basket/news/backends/ctms.py
+++ b/basket/news/backends/ctms.py
@@ -174,27 +174,28 @@ def from_vendor(contact):
 
 def process_country(raw_country):
     """Convert to 2-letter country, and throw out unknown countries."""
-    country = raw_country.strip().lower()
-    if len(country) == 3:
-        new_country = convert_country_3_to_2(country)
-        if new_country:
-            country = new_country
+    if raw_country is not None:
+        country = raw_country.strip().lower()
+        if len(country) == 3:
+            new_country = convert_country_3_to_2(country)
+            if new_country:
+                country = new_country
 
-    if country not in SFDC_COUNTRIES_LIST:
-        raise ValueError(f"{country} not in SFDC_COUNTRIES_LIST")
-    return country
+        if country not in SFDC_COUNTRIES_LIST:
+            raise ValueError(f"{country} not in SFDC_COUNTRIES_LIST")
+        return country
 
 
 def process_lang(raw_lang):
     """Ensure language is supported."""
-    lang = raw_lang.strip()
-    if lang.lower() in settings.EXTRA_SUPPORTED_LANGS:
-        return lang
-    elif is_supported_newsletter_language(lang):
-        return lang[:2].lower()
-    else:
-        # Use the default language (English) for unsupported languages
-        return "en"
+    if raw_lang is not None:
+        lang = raw_lang.strip()
+        if lang.lower() in settings.EXTRA_SUPPORTED_LANGS:
+            return lang
+        elif is_supported_newsletter_language(lang):
+            return lang[:2].lower()
+    # Use the default language (English) for unsupported languages
+    return "en"
 
 
 def truncate_string(max_length, raw_string):


Test patch:
diff --git a/basket/news/tests/test_ctms.py b/basket/news/tests/test_ctms.py
--- a/basket/news/tests/test_ctms.py
+++ b/basket/news/tests/test_ctms.py
@@ -282,6 +282,7 @@ def test_country(self):
             (" USA ", "us"),
             ("en", None),
             (" ABC ", None),
+            (None, None),
         )
 
         for original, converted in tests:
@@ -311,6 +312,7 @@ def test_lang(self, mock_languages):
             (" ru", "en"),
             ("en-CA", "en"),
             ("es-MX", "es"),
+            (None, "en"),
         )
         for original, converted in tests:
             data = to_vendor({"lang": original})