Skip to content

Commit 2698545

Browse files
Jules was unable to complete the task in time. Please review the work done so far and provide feedback for Jules to continue.
1 parent 0fcfde5 commit 2698545

File tree

3 files changed

+1851
-0
lines changed

3 files changed

+1851
-0
lines changed
Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
import unittest
2+
import hashlib
3+
from unittest.mock import patch, MagicMock, call, PropertyMock
4+
import sys
5+
import os
6+
from io import StringIO
7+
8+
# Add the project root to sys.path to allow for relative imports
9+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')))
10+
11+
from examples.remarketing.upload_enhanced_conversions_for_web import (
12+
normalize_and_hash,
13+
normalize_and_hash_email_address,
14+
main as main_sut,
15+
)
16+
from google.ads.googleads.client import GoogleAdsClient
17+
18+
19+
class TestNormalizeAndHash(unittest.TestCase):
20+
"""Tests for the normalize_and_hash function."""
21+
22+
def _get_expected_hash(self, value_to_normalize):
23+
normalized_value = value_to_normalize.strip().lower()
24+
return hashlib.sha256(normalized_value.encode()).hexdigest()
25+
26+
def test_simple_string(self):
27+
input_string = "test string"
28+
expected_hash = self._get_expected_hash(input_string)
29+
self.assertEqual(normalize_and_hash(input_string), expected_hash)
30+
31+
def test_string_with_leading_trailing_spaces(self):
32+
input_string = " test string "
33+
expected_hash = self._get_expected_hash("test string")
34+
self.assertEqual(normalize_and_hash(input_string), expected_hash)
35+
36+
def test_string_with_mixed_case(self):
37+
input_string = "Test String"
38+
expected_hash = self._get_expected_hash("test string")
39+
self.assertEqual(normalize_and_hash(input_string), expected_hash)
40+
41+
def test_empty_string(self):
42+
input_string = ""
43+
expected_hash = self._get_expected_hash(input_string)
44+
self.assertEqual(normalize_and_hash(input_string), expected_hash)
45+
46+
47+
class TestNormalizeAndHashEmailAddress(unittest.TestCase):
48+
"""Tests for the normalize_and_hash_email_address function."""
49+
50+
@patch("examples.remarketing.upload_enhanced_conversions_for_web.normalize_and_hash")
51+
def test_regular_email(self, mock_normalize_and_hash_inner):
52+
mock_normalize_and_hash_inner.return_value = "hashed_regular_email"
53+
54+
result = normalize_and_hash_email_address(email)
55+
mock_normalize_and_hash_inner.assert_called_once_with("[email protected]")
56+
self.assertEqual(result, "hashed_regular_email")
57+
58+
@patch("examples.remarketing.upload_enhanced_conversions_for_web.normalize_and_hash")
59+
def test_email_with_leading_trailing_spaces_and_case(self, mock_normalize_and_hash_inner):
60+
mock_normalize_and_hash_inner.return_value = "hashed_spaced_email"
61+
email = " [email protected] "
62+
result = normalize_and_hash_email_address(email)
63+
mock_normalize_and_hash_inner.assert_called_once_with("[email protected]")
64+
self.assertEqual(result, "hashed_spaced_email")
65+
66+
@patch("examples.remarketing.upload_enhanced_conversions_for_web.normalize_and_hash")
67+
def test_gmail_with_dots(self, mock_normalize_and_hash_inner):
68+
mock_normalize_and_hash_inner.return_value = "hashed_gmail_dots"
69+
70+
result = normalize_and_hash_email_address(email)
71+
mock_normalize_and_hash_inner.assert_called_once_with("[email protected]")
72+
self.assertEqual(result, "hashed_gmail_dots")
73+
74+
@patch("examples.remarketing.upload_enhanced_conversions_for_web.normalize_and_hash")
75+
def test_googlemail_with_dots(self, mock_normalize_and_hash_inner):
76+
mock_normalize_and_hash_inner.return_value = "hashed_googlemail_dots"
77+
78+
result = normalize_and_hash_email_address(email)
79+
mock_normalize_and_hash_inner.assert_called_once_with("[email protected]")
80+
self.assertEqual(result, "hashed_googlemail_dots")
81+
82+
@patch("examples.remarketing.upload_enhanced_conversions_for_web.normalize_and_hash")
83+
def test_other_domain_with_dots(self, mock_normalize_and_hash_inner):
84+
mock_normalize_and_hash_inner.return_value = "hashed_other_domain_dots"
85+
86+
result = normalize_and_hash_email_address(email)
87+
mock_normalize_and_hash_inner.assert_called_once_with("[email protected]")
88+
self.assertEqual(result, "hashed_other_domain_dots")
89+
90+
@patch("examples.remarketing.upload_enhanced_conversions_for_web.normalize_and_hash")
91+
def test_gmail_with_plus_alias_and_dots(self, mock_normalize_and_hash_inner):
92+
mock_normalize_and_hash_inner.return_value = "hashed_gmail_plus_dots"
93+
94+
result = normalize_and_hash_email_address(email)
95+
mock_normalize_and_hash_inner.assert_called_once_with("[email protected]")
96+
self.assertEqual(result, "hashed_gmail_plus_dots")
97+
98+
99+
@patch("google.ads.googleads.client.GoogleAdsClient.load_from_storage")
100+
class TestMainFunctionForEnhancedWeb(unittest.TestCase):
101+
102+
def setUp(self):
103+
self.mock_client = MagicMock(spec=GoogleAdsClient)
104+
self.mock_client.enums = MagicMock()
105+
106+
self.mock_ca_upload_service = MagicMock(name="ConversionAdjustmentUploadService")
107+
self.mock_ca_service = MagicMock(name="ConversionActionService")
108+
109+
def get_service_side_effect(service_name, version=None):
110+
if service_name == "ConversionAdjustmentUploadService":
111+
return self.mock_ca_upload_service
112+
elif service_name == "ConversionActionService":
113+
return self.mock_ca_service
114+
return MagicMock()
115+
self.mock_client.get_service.side_effect = get_service_side_effect
116+
117+
self.mock_ca_service.conversion_action_path.return_value = "dummy_conversion_action_path"
118+
119+
self.mock_client.enums.ConversionAdjustmentTypeEnum = MagicMock()
120+
self.mock_client.enums.ConversionAdjustmentTypeEnum.ENHANCEMENT = "ENHANCEMENT_ENUM_VAL"
121+
self.mock_client.enums.UserIdentifierSourceEnum = MagicMock()
122+
self.mock_client.enums.UserIdentifierSourceEnum.FIRST_PARTY = "FIRST_PARTY_ENUM_VAL"
123+
124+
self.mock_conversion_adjustment = MagicMock(name="ConversionAdjustmentInstance")
125+
self.mock_gclid_date_time_pair = MagicMock(name="GclidDateTimePairInstance")
126+
self.mock_gclid_date_time_pair.gclid = None
127+
self.mock_conversion_adjustment.gclid_date_time_pair = self.mock_gclid_date_time_pair
128+
129+
self._created_user_identifiers_for_test = []
130+
self._created_address_info_for_test = None
131+
132+
def get_type_side_effect(type_name):
133+
if type_name == "ConversionAdjustment":
134+
self.mock_conversion_adjustment.user_identifiers = []
135+
self.mock_conversion_adjustment.order_id = None
136+
self.mock_conversion_adjustment.user_agent = None
137+
self.mock_gclid_date_time_pair.gclid = None
138+
self.mock_gclid_date_time_pair.conversion_date_time = None
139+
self.mock_conversion_adjustment.gclid_date_time_pair = self.mock_gclid_date_time_pair
140+
return self.mock_conversion_adjustment
141+
elif type_name == "UserIdentifier":
142+
new_identifier = MagicMock(name="UserIdentifierInstance")
143+
if len(self._created_user_identifiers_for_test) == 2:
144+
self._created_address_info_for_test = MagicMock(name="AddressInfo_linked_to_AddressID")
145+
new_identifier.address_info = self._created_address_info_for_test
146+
else:
147+
new_identifier.address_info = MagicMock(name="AddressInfo_for_non_address_UID")
148+
self._created_user_identifiers_for_test.append(new_identifier)
149+
return new_identifier
150+
return MagicMock(name=f"DefaultMock_{type_name}")
151+
self.mock_client.get_type.side_effect = get_type_side_effect
152+
153+
self.patch_normalize_email_web = patch(
154+
"examples.remarketing.upload_enhanced_conversions_for_web.normalize_and_hash_email_address",
155+
return_value="hashed_email_val"
156+
)
157+
158+
def normalize_hash_side_effect(value_to_hash):
159+
if value_to_hash == "+1 800 5550102":
160+
return "hashed_phone_val"
161+
elif value_to_hash == "Alex":
162+
return "hashed_fn_val"
163+
elif value_to_hash == "Quinn":
164+
return "hashed_ln_val"
165+
return f"hashed_unexpected_{value_to_hash}"
166+
167+
self.patch_normalize_hash_web = patch(
168+
"examples.remarketing.upload_enhanced_conversions_for_web.normalize_and_hash",
169+
side_effect=normalize_hash_side_effect
170+
)
171+
172+
self.mock_normalize_email_sut = self.patch_normalize_email_web.start()
173+
self.mock_normalize_hash_sut = self.patch_normalize_hash_web.start()
174+
175+
self.mock_upload_response = MagicMock(name="UploadResponse")
176+
self.mock_upload_response.partial_failure_error = None
177+
self.mock_upload_result = MagicMock(name="UploadResult")
178+
self.mock_upload_response.results = [self.mock_upload_result]
179+
self.mock_ca_upload_service.upload_conversion_adjustments.return_value = self.mock_upload_response
180+
181+
self.patcher_stdout = patch('sys.stdout', new_callable=StringIO)
182+
self.mock_stdout = self.patcher_stdout.start()
183+
184+
def tearDown(self):
185+
self.patch_normalize_email_web.stop()
186+
self.patch_normalize_hash_web.stop()
187+
self.patcher_stdout.stop()
188+
if hasattr(self.mock_upload_response, 'partial_failure_error'):
189+
self.mock_upload_response.partial_failure_error = None
190+
if hasattr(self, 'mock_gclid_date_time_pair'):
191+
self.mock_gclid_date_time_pair.gclid = None
192+
self.mock_gclid_date_time_pair.conversion_date_time = None
193+
if hasattr(self, 'mock_conversion_adjustment'):
194+
self.mock_conversion_adjustment.user_agent = None
195+
self.mock_conversion_adjustment.order_id = None
196+
197+
198+
def test_main_basic_success_all_args_provided(self, mock_load_storage):
199+
customer_id = "test_customer_123"
200+
conversion_action_id = "test_ca_456"
201+
order_id = "test_order_789"
202+
conversion_date_time_val = "2023-10-26 10:00:00-05:00"
203+
user_agent_val = "TestUserAgent/1.0"
204+
205+
self._created_user_identifiers_for_test = []
206+
self._created_address_info_for_test = None
207+
self.mock_normalize_email_sut.reset_mock()
208+
self.mock_normalize_hash_sut.reset_mock()
209+
self.mock_ca_upload_service.upload_conversion_adjustments.reset_mock()
210+
self.mock_conversion_adjustment.user_identifiers = []
211+
212+
self.mock_upload_result.conversion_action = "dummy_conversion_action_path"
213+
self.mock_upload_result.order_id = order_id
214+
self.mock_upload_response.partial_failure_error = None
215+
216+
217+
main_sut(
218+
self.mock_client,
219+
customer_id,
220+
conversion_action_id,
221+
order_id,
222+
conversion_date_time=conversion_date_time_val,
223+
user_agent=user_agent_val
224+
)
225+
226+
self.mock_normalize_email_sut.assert_called_once_with("[email protected]")
227+
self.mock_normalize_hash_sut.assert_any_call("+1 800 5550102")
228+
self.mock_normalize_hash_sut.assert_any_call("Alex")
229+
self.mock_normalize_hash_sut.assert_any_call("Quinn")
230+
self.assertEqual(self.mock_normalize_hash_sut.call_count, 3)
231+
232+
self.assertEqual(len(self._created_user_identifiers_for_test), 3)
233+
self.assertEqual(len(self.mock_conversion_adjustment.user_identifiers), 3)
234+
235+
email_identifier = None
236+
phone_identifier = None
237+
address_identifier = None
238+
239+
for ident in self.mock_conversion_adjustment.user_identifiers:
240+
if hasattr(ident, 'hashed_email') and ident.hashed_email == "hashed_email_val":
241+
email_identifier = ident
242+
elif hasattr(ident, 'hashed_phone_number') and ident.hashed_phone_number == "hashed_phone_val":
243+
phone_identifier = ident
244+
elif ident.address_info is self._created_address_info_for_test and self._created_address_info_for_test is not None:
245+
address_identifier = ident
246+
247+
self.assertIsNotNone(email_identifier, "Email identifier not found")
248+
self.assertEqual(email_identifier.user_identifier_source, "FIRST_PARTY_ENUM_VAL")
249+
250+
self.assertIsNotNone(phone_identifier, "Phone identifier not found")
251+
252+
self.assertIsNotNone(address_identifier, "Address identifier not found")
253+
self.assertIsNotNone(address_identifier.address_info, "AddressInfo not set on address identifier")
254+
255+
self.assertIs(address_identifier.address_info, self._created_address_info_for_test)
256+
257+
addr_info = address_identifier.address_info
258+
self.assertEqual(addr_info.hashed_first_name, "hashed_fn_val")
259+
self.assertEqual(addr_info.hashed_last_name, "hashed_ln_val")
260+
self.assertEqual(addr_info.country_code, "US")
261+
self.assertEqual(addr_info.postal_code, "94045")
262+
263+
adj = self.mock_conversion_adjustment
264+
self.assertEqual(adj.adjustment_type, "ENHANCEMENT_ENUM_VAL")
265+
self.assertEqual(adj.conversion_action, "dummy_conversion_action_path")
266+
self.assertEqual(adj.order_id, order_id)
267+
self.assertEqual(adj.user_agent, user_agent_val)
268+
269+
self.assertIsNotNone(adj.gclid_date_time_pair)
270+
self.assertIsNone(adj.gclid_date_time_pair.gclid)
271+
self.assertEqual(adj.gclid_date_time_pair.conversion_date_time, conversion_date_time_val)
272+
273+
self.mock_ca_upload_service.upload_conversion_adjustments.assert_called_once_with(
274+
customer_id=customer_id,
275+
conversion_adjustments=[self.mock_conversion_adjustment],
276+
partial_failure=True
277+
)
278+
279+
captured_output = self.mock_stdout.getvalue()
280+
self.assertIn(f"Uploaded conversion adjustment of {self.mock_upload_result.conversion_action} for order ID (", captured_output)
281+
self.assertIn(f", '{order_id}')", captured_output)
282+
283+
def test_main_missing_optional_args(self, mock_load_storage):
284+
customer_id = "test_customer_123"
285+
conversion_action_id = "test_ca_456"
286+
order_id = "test_order_789_missing_opts"
287+
288+
self._created_user_identifiers_for_test = []
289+
self._created_address_info_for_test = None
290+
self.mock_normalize_email_sut.reset_mock()
291+
self.mock_normalize_hash_sut.reset_mock()
292+
self.mock_ca_upload_service.upload_conversion_adjustments.reset_mock()
293+
self.mock_conversion_adjustment.user_identifiers = []
294+
self.mock_conversion_adjustment.user_agent = "previous_value" # Set to ensure it's cleared
295+
self.mock_gclid_date_time_pair.conversion_date_time = "previous_value" # Set to ensure it's cleared
296+
297+
298+
self.mock_upload_result.conversion_action = "dummy_conversion_action_path"
299+
self.mock_upload_result.order_id = order_id
300+
self.mock_upload_response.partial_failure_error = None
301+
302+
main_sut(
303+
self.mock_client,
304+
customer_id,
305+
conversion_action_id,
306+
order_id,
307+
conversion_date_time=None,
308+
user_agent=None
309+
)
310+
311+
self.mock_normalize_email_sut.assert_called_once_with("[email protected]")
312+
self.assertEqual(self.mock_normalize_hash_sut.call_count, 3)
313+
self.assertEqual(len(self.mock_conversion_adjustment.user_identifiers), 3)
314+
315+
adj = self.mock_conversion_adjustment
316+
self.assertEqual(adj.adjustment_type, "ENHANCEMENT_ENUM_VAL")
317+
self.assertEqual(adj.conversion_action, "dummy_conversion_action_path")
318+
self.assertEqual(adj.order_id, order_id)
319+
320+
self.assertIsNone(adj.user_agent)
321+
self.assertIsNone(adj.gclid_date_time_pair.conversion_date_time)
322+
323+
self.mock_ca_upload_service.upload_conversion_adjustments.assert_called_once()
324+
325+
captured_output = self.mock_stdout.getvalue()
326+
self.assertIn(f"Uploaded conversion adjustment of {self.mock_upload_result.conversion_action} for order ID (", captured_output)
327+
self.assertIn(f", '{order_id}')", captured_output)
328+
329+
def test_main_partial_failure_response(self, mock_load_storage):
330+
customer_id = "test_customer_123"
331+
conversion_action_id = "test_ca_456"
332+
order_id = "test_order_789_partial_fail"
333+
partial_failure_message = "Test partial failure from web."
334+
335+
self._created_user_identifiers_for_test = []
336+
self._created_address_info_for_test = None
337+
self.mock_normalize_email_sut.reset_mock()
338+
self.mock_normalize_hash_sut.reset_mock()
339+
self.mock_ca_upload_service.upload_conversion_adjustments.reset_mock()
340+
self.mock_conversion_adjustment.user_identifiers = []
341+
342+
# Configure response for partial failure
343+
self.mock_upload_response.partial_failure_error = MagicMock(message=partial_failure_message)
344+
# SUT for web does not iterate error_details, so just message is enough for partial_failure_error
345+
346+
# Values for the result object, though not printed in partial failure case for this SUT
347+
self.mock_upload_result.conversion_action = "dummy_conversion_action_path"
348+
self.mock_upload_result.order_id = order_id
349+
350+
main_sut(
351+
self.mock_client,
352+
customer_id,
353+
conversion_action_id,
354+
order_id,
355+
conversion_date_time=None,
356+
user_agent=None
357+
)
358+
359+
# Core operations should still occur
360+
self.mock_normalize_email_sut.assert_called_once_with("[email protected]")
361+
self.assertEqual(self.mock_normalize_hash_sut.call_count, 3)
362+
self.mock_ca_upload_service.upload_conversion_adjustments.assert_called_once()
363+
364+
# Check for stdout messages
365+
captured_output = self.mock_stdout.getvalue()
366+
367+
expected_partial_failure_stdout = f"Partial error encountered: {partial_failure_message}"
368+
self.assertIn(expected_partial_failure_stdout, captured_output)
369+
370+
# Success message should NOT be printed if partial_failure_error is set
371+
unexpected_success_stdout = "Uploaded conversion adjustment of"
372+
self.assertNotIn(unexpected_success_stdout, captured_output)
373+
374+
375+
if __name__ == "__main__":
376+
unittest.main()

0 commit comments

Comments
 (0)