Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 Source Mailchimp: Connection Check Error Handling #32466

Merged
merged 14 commits into from Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -10,7 +10,7 @@ data:
connectorSubtype: api
connectorType: source
definitionId: b03a9f3e-22a5-11eb-adc1-0242ac120002
dockerImageTag: 0.8.1
dockerImageTag: 0.8.2
dockerRepository: airbyte/source-mailchimp
documentationUrl: https://docs.airbyte.com/integrations/sources/mailchimp
githubIssueLabel: source-mailchimp
Expand Down
Expand Up @@ -18,14 +18,26 @@

class MailChimpAuthenticator:
@staticmethod
def get_server_prefix(access_token: str) -> str:
def get_oauth_data_center(access_token: str) -> str:
"""
Every Mailchimp API request must be sent to a specific data center.
The data center is already embedded in API keys, but not OAuth access tokens.
This method retrieves the data center for OAuth credentials.
"""
try:
response = requests.get(
"https://login.mailchimp.com/oauth2/metadata", headers={"Authorization": "OAuth {}".format(access_token)}
)

# Requests to this endpoint will return a 200 status code even if the access token is invalid.
error = response.json().get("error")
if error == "invalid_token":
raise ValueError("The access token you provided was invalid. Please check your credentials and try again.")
return response.json()["dc"]

# Handle any other exceptions that may occur.
except Exception as e:
raise Exception(f"Cannot retrieve server_prefix for you account. \n {repr(e)}")
raise Exception(f"An error occured while retrieving the data center for your account. \n {repr(e)}")

def get_auth(self, config: Mapping[str, Any]) -> AuthBase:
authorization = config.get("credentials", {})
Expand All @@ -35,7 +47,7 @@ def get_auth(self, config: Mapping[str, Any]) -> AuthBase:
# See https://mailchimp.com/developer/marketing/docs/fundamentals/#api-structure
apikey = authorization.get("apikey") or config.get("apikey")
if not apikey:
raise Exception("No apikey in creds")
raise Exception("Please provide a valid API key for authentication.")
auth_string = f"anystring:{apikey}".encode("utf8")
b64_encoded = base64.b64encode(auth_string).decode("utf8")
auth = TokenAuthenticator(token=b64_encoded, auth_method="Basic")
Expand All @@ -44,7 +56,7 @@ def get_auth(self, config: Mapping[str, Any]) -> AuthBase:
elif auth_type == "oauth2.0":
access_token = authorization["access_token"]
auth = TokenAuthenticator(token=access_token, auth_method="Bearer")
auth.data_center = self.get_server_prefix(access_token)
auth.data_center = self.get_oauth_data_center(access_token)

else:
raise Exception(f"Invalid auth type: {auth_type}")
Expand All @@ -56,8 +68,21 @@ class SourceMailchimp(AbstractSource):
def check_connection(self, logger: AirbyteLogger, config: Mapping[str, Any]) -> Tuple[bool, Any]:
try:
authenticator = MailChimpAuthenticator().get_auth(config)
requests.get(f"https://{authenticator.data_center}.api.mailchimp.com/3.0/ping", headers=authenticator.get_auth_header())
response = requests.get(
f"https://{authenticator.data_center}.api.mailchimp.com/3.0/ping", headers=authenticator.get_auth_header()
)

# A successful response will return a simple JSON object with a single key: health_status.
# Otherwise, errors are returned as a JSON object with keys:
# {type, title, status, detail, instance}

if not response.json().get("health_status"):
error_title = response.json().get("title", "Unknown Error")
error_details = response.json().get("details", "An unknown error occurred. Please verify your credentials and try again.")
return False, f"Encountered an error while connecting to Mailchimp. Type: {error_title}. Details: {error_details}"
return True, None

# Handle any other exceptions that may occur.
except Exception as e:
return False, repr(e)

Expand Down
Expand Up @@ -5,15 +5,14 @@
import logging

import pytest
import requests
from source_mailchimp.source import MailChimpAuthenticator, SourceMailchimp

logger = logging.getLogger("airbyte")


def test_check_connection_ok(requests_mock, config, data_center):
responses = [
{"json": [], "status_code": 200},
{"json": {"health_status": "Everything's Chimpy!"}},
]
requests_mock.register_uri("GET", f"https://{data_center}.api.mailchimp.com/3.0/ping", responses)
ok, error_msg = SourceMailchimp().check_connection(logger, config=config)
Expand All @@ -22,30 +21,54 @@ def test_check_connection_ok(requests_mock, config, data_center):
assert not error_msg


def test_check_connection_error(requests_mock, config, data_center):
requests_mock.register_uri("GET", f"https://{data_center}.api.mailchimp.com/3.0/ping", body=requests.ConnectionError())
@pytest.mark.parametrize(
"response, expected_message",
[
(
{
"json": {
"title": "API Key Invalid",
"details": "Your API key may be invalid, or you've attempted to access the wrong datacenter.",
}
},
"Encountered an error while connecting to Mailchimp. Type: API Key Invalid. Details: Your API key may be invalid, or you've attempted to access the wrong datacenter.",
),
(
{"json": {"title": "Forbidden", "details": "You don't have permission to access this resource."}},
"Encountered an error while connecting to Mailchimp. Type: Forbidden. Details: You don't have permission to access this resource.",
),
(
{"json": {}},
"Encountered an error while connecting to Mailchimp. Type: Unknown Error. Details: An unknown error occurred. Please verify your credentials and try again.",
),
],
ids=["API Key Invalid", "Forbidden", "Unknown Error"],
)
def test_check_connection_error(requests_mock, config, data_center, response, expected_message):
requests_mock.register_uri("GET", f"https://{data_center}.api.mailchimp.com/3.0/ping", json=response["json"])
ok, error_msg = SourceMailchimp().check_connection(logger, config=config)

assert not ok
assert error_msg
assert error_msg == expected_message


def test_get_server_prefix_ok(requests_mock, access_token, data_center):
def test_get_oauth_data_center_ok(requests_mock, access_token, data_center):
responses = [
{"json": {"dc": data_center}, "status_code": 200},
]
requests_mock.register_uri("GET", "https://login.mailchimp.com/oauth2/metadata", responses)
assert MailChimpAuthenticator().get_server_prefix(access_token) == data_center
assert MailChimpAuthenticator().get_oauth_data_center(access_token) == data_center


def test_get_server_prefix_exception(requests_mock, access_token, data_center):
def test_get_oauth_data_center_exception(requests_mock, access_token):
responses = [
{"json": {}, "status_code": 200},
{"json": {"error": "invalid_token"}, "status_code": 200},
{"status_code": 403},
]
requests_mock.register_uri("GET", "https://login.mailchimp.com/oauth2/metadata", responses)
with pytest.raises(Exception):
MailChimpAuthenticator().get_server_prefix(access_token)
MailChimpAuthenticator().get_oauth_data_center(access_token)


def test_oauth_config(requests_mock, oauth_config, data_center):
Expand Down
3 changes: 2 additions & 1 deletion docs/integrations/sources/mailchimp.md
Expand Up @@ -76,7 +76,8 @@ Now that you have set up the Mailchimp source connector, check out the following

| Version | Date | Pull Request | Subject |
|---------|------------|----------------------------------------------------------|----------------------------------------------------------------------------|
| 0.8.1 | 2023-11-06 | [32226](https://github.com/airbytehq/airbyte/pull/32226) | Unmute expected records test after data anonymisation |
| 0.8.2 | 2023-11-13 | [32466](https://github.com/airbytehq/airbyte/pull/32466) | Improve error handling during connection check |
| 0.8.1 | 2023-11-06 | [32226](https://github.com/airbytehq/airbyte/pull/32226) | Unmute expected records test after data anonymisation |
| 0.8.0 | 2023-11-01 | [32032](https://github.com/airbytehq/airbyte/pull/32032) | Add ListMembers stream |
| 0.7.0 | 2023-10-27 | [31940](https://github.com/airbytehq/airbyte/pull/31940) | Implement availability strategy |
| 0.6.0 | 2023-10-27 | [31922](https://github.com/airbytehq/airbyte/pull/31922) | Add Segments stream |
Expand Down