Skip to content

Commit b466390

Browse files
committed
add tests for init, create_session, transform_response, check_response_for_rate_limit, paginate_endpoint, and get_subscriber_hash
1 parent 142a818 commit b466390

File tree

1 file changed

+180
-0
lines changed

1 file changed

+180
-0
lines changed

src/tests/test_mailchimp.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import os
2+
import json
3+
import unittest
4+
from unittest.mock import MagicMock, patch
5+
from src.stac_utils.mailchimp import MailChimpClient, logger
6+
7+
class TestMailChimpClient(unittest.TestCase):
8+
def setUp(self) -> None:
9+
self.test_client = MailChimpClient(api_key="fake-us9")
10+
self.test_logger = logger
11+
12+
def test_init_env_keys(self):
13+
"""Test that client initializes with environmental keys"""
14+
test_api_key = "hgd1204-us20"
15+
with patch.dict(os.environ, values={"MAILCHIMP_API_KEY": test_api_key}):
16+
test_client = MailChimpClient()
17+
self.assertEqual(test_api_key, test_client.api_key)
18+
# test the extracted data center and base_url
19+
self.assertEqual("us20", test_client.data_center)
20+
self.assertEqual(
21+
"https://us20.api.mailchimp.com/3.0", test_client.base_url
22+
)
23+
24+
def test_create_session(self):
25+
"""Test that API token and content type is set in headers for a Mailchimp session"""
26+
session = self.test_client.create_session()
27+
28+
# check that the auth tuple is correctly set
29+
self.assertEqual(session.auth, ("anystring", self.test_client.api_key))
30+
31+
# check that the "Content-Type" header exists and has a value of "application/json"
32+
self.assertIn("Content-Type", session.headers)
33+
self.assertEqual(session.headers["Content-Type"], "application/json")
34+
35+
def test_transform_response_valid_json(self):
36+
"""Test that response is transformed and includes status code"""
37+
mock_data = {"foo_bar": "spam", "email_address": "[email protected]"}
38+
mock_response = MagicMock()
39+
mock_response.status_code = 200
40+
# http response body contains bytes
41+
mock_response.content = json.dumps(mock_data).encode()
42+
mock_response.json.return_value = mock_data
43+
result = self.test_client.transform_response(mock_response)
44+
# make sure output matches expected dict
45+
self.assertEqual(result, {"foo_bar": "spam", "email_address": "[email protected]", "status_code": 200})
46+
47+
def test_transform_response_empty_content(self):
48+
"""Test that empty content or a 204 response returns only the status code"""
49+
mock_response = MagicMock()
50+
mock_response.status_code = 204
51+
# no data returned in 204 response
52+
mock_response.content = b""
53+
result = self.test_client.transform_response(mock_response)
54+
self.assertEqual(result, {"status_code": 204})
55+
56+
def test_transform_response_invalid_json(self):
57+
"""Test that response for invalid JSON returns an empty dict with the status code"""
58+
mock_response = MagicMock()
59+
mock_response.status_code = 500
60+
mock_response.content = b"not a valid json"
61+
# bad json
62+
mock_response.json.side_effect = json.decoder.JSONDecodeError("error", "not a valid json", 0)
63+
result = self.test_client.transform_response(mock_response)
64+
self.assertEqual(result, {"status_code": 500})
65+
66+
@patch.object(logger, "warning")
67+
def test_check_response_for_rate_limit(self, mock_warning):
68+
"""Test that check_response_for_rate_limit always returns 1 and actually logs warning on 429"""
69+
mock_response_valid = MagicMock()
70+
mock_response_valid.status_code = 200
71+
72+
mock_response_limit = MagicMock()
73+
mock_response_limit.status_code = 429
74+
75+
# should always return 1
76+
result_valid = self.test_client.check_response_for_rate_limit(mock_response_valid)
77+
result_limit = self.test_client.check_response_for_rate_limit(mock_response_limit)
78+
79+
self.assertEqual(result_valid, 1)
80+
self.assertEqual(result_limit, 1)
81+
82+
# verify that the logger warning called once (occurs when 429)
83+
mock_warning.assert_called_once_with("Mailchimp rate limit hit (HTTP 429: Too Many Requests)")
84+
85+
@patch.object(MailChimpClient, "transform_response")
86+
@patch.object(logger, "debug")
87+
@patch("requests.Session.get")
88+
def test_paginate_endpoint_valid(self, mock_get, mock_debug, mock_transform):
89+
"""Test that paginate_endpoint correctly aggregates results across multiple pages"""
90+
# set pages
91+
mock_transform.side_effect = [
92+
{"members": [{"id": "1"}, {"id": "2"}], "total_items": 4},
93+
{"members": [{"id": "3"}, {"id": "4"}], "total_items": 4},
94+
]
95+
96+
# set http calls
97+
fake_response_1 = MagicMock()
98+
fake_response_2 = MagicMock()
99+
mock_get.side_effect = [fake_response_1, fake_response_2]
100+
101+
results = self.test_client.paginate_endpoint(
102+
base_endpoint="lists/898/members",
103+
data_key="members",
104+
count=2,
105+
max_pages=2,
106+
)
107+
# all data in list
108+
self.assertEqual(results, [{"id": "1"}, {"id": "2"}, {"id": "3"}, {"id": "4"}])
109+
# two calls
110+
self.assertEqual(mock_get.call_count, 2)
111+
# debug not called
112+
mock_debug.assert_not_called()
113+
114+
@patch.object(MailChimpClient, "transform_response")
115+
@patch.object(logger, "debug")
116+
@patch("requests.Session.get")
117+
def test_paginate_endpoint_debug_logs_when_empty(self, mock_get, mock_debug, mock_transform):
118+
"""Test that paginate_endpoint logs debug message and stops when no items are found"""
119+
# mock first page has data, second page empty (triggers debug...)
120+
mock_transform.side_effect = [
121+
{"members": [{"id": "1"}, {"id": "2"}], "total_items": 4},
122+
{"members": []},
123+
]
124+
125+
# set HTTP calls
126+
fake_response_1 = MagicMock()
127+
fake_response_2 = MagicMock()
128+
mock_get.side_effect = [fake_response_1, fake_response_2]
129+
130+
131+
self.test_client.paginate_endpoint(
132+
base_endpoint="lists/898/members",
133+
data_key="members",
134+
count=2,
135+
)
136+
137+
# should log debug once when second page is empty
138+
mock_debug.assert_called_once_with("No items found at offset 2 for key 'members'")
139+
# two calls
140+
self.assertEqual(mock_get.call_count, 2)
141+
142+
@patch.object(MailChimpClient, "transform_response")
143+
@patch.object(logger, "debug")
144+
@patch("requests.Session.get")
145+
def test_paginate_endpoint_stops_when_total_items_reached(self, mock_get, mock_debug, mock_transform):
146+
"""Test that paginate_endpoint stops paginating when offset >= total_items"""
147+
mock_transform.side_effect = [
148+
{"members": [{"id": "1"}, {"id": "2"}], "total_items": 3},
149+
{"members": [{"id": "3"}], "total_items": 3},
150+
]
151+
152+
# set HTTP calls
153+
fake_response_1 = MagicMock()
154+
fake_response_2 = MagicMock()
155+
mock_get.side_effect = [fake_response_1, fake_response_2]
156+
157+
results = self.test_client.paginate_endpoint(
158+
base_endpoint="lists/010/members",
159+
data_key="members",
160+
count=2,
161+
)
162+
163+
# 3 total members
164+
self.assertEqual(results, [{"id": "1"}, {"id": "2"}, {"id": "3"}])
165+
# two calls
166+
self.assertEqual(mock_get.call_count, 2)
167+
# debug not called
168+
mock_debug.assert_not_called()
169+
170+
def test_get_subscriber_hash(self):
171+
"""Test that get_subscriber_hash returns correct md5 hash and normalizes to lowercase"""
172+
173+
expected_subscriber_hash = "67441e845c03ceda61635c3263393515"
174+
175+
result = MailChimpClient.get_subscriber_hash(email)
176+
self.assertEqual(result, expected_subscriber_hash)
177+
178+
# lowercase version
179+
self.assertEqual(result, MailChimpClient.get_subscriber_hash("[email protected]"))
180+

0 commit comments

Comments
 (0)