Skip to content

Commit 68254a1

Browse files
added identity map v3 client
1 parent c1a1a47 commit 68254a1

17 files changed

+779
-66
lines changed

tests/test_identity_map_client_unit_tests.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import base64
2-
import json
32
import unittest
43
import datetime as dt
54
from unittest.mock import patch, MagicMock
65

7-
from uid2_client import IdentityMapClient, get_datetime_utc_iso_format
6+
from uid2_client import IdentityMapClient, get_datetime_utc_iso_format, Uid2Response, Envelope
87

98

109
class IdentityMapUnitTests(unittest.TestCase):
@@ -34,23 +33,22 @@ def test_get_datetime_utc_iso_format_timestamp(self):
3433
iso_format_timestamp = get_datetime_utc_iso_format(timestamp)
3534
self.assertEqual(expected_timestamp, iso_format_timestamp)
3635

37-
@patch('uid2_client.identity_map_client.make_v2_request')
38-
@patch('uid2_client.identity_map_client.post')
39-
@patch('uid2_client.identity_map_client.parse_v2_response')
40-
def test_identity_buckets_request(self, mock_parse_v2_response, mock_post, mock_make_v2_request):
36+
@patch('uid2_client.identity_map_client.create_envelope')
37+
@patch('uid2_client.identity_map_client.make_request')
38+
@patch('uid2_client.identity_map_client.parse_response')
39+
def test_identity_buckets_request(self, mock_parse_response, mock_make_request, mock_create_envelope):
4140
expected_req = b'{"since_timestamp": "2024-07-02T14:30:15.123456"}'
4241
test_cases = ["2024-07-02T14:30:15.123456+00:00", "2024-07-02 09:30:15.123456-05:00",
4342
"2024-07-02T08:30:15.123456-06:00", "2024-07-02T10:30:15.123456-04:00",
4443
"2024-07-02T06:30:15.123456-08:00", "2024-07-02T23:30:15.123456+09:00",
4544
"2024-07-03T00:30:15.123456+10:00", "2024-07-02T20:00:15.123456+05:30"]
4645
mock_req = b'mocked_request_data'
4746
mock_nonce = 'mocked_nonce'
48-
mock_make_v2_request.return_value = (mock_req, mock_nonce)
49-
mock_response = MagicMock()
50-
mock_response.read.return_value = b'{"mocked": "response"}'
51-
mock_post.return_value = mock_response
52-
mock_parse_v2_response.return_value = b'{"body":[],"status":"success"}'
47+
mock_create_envelope.return_value = Envelope(mock_req, mock_nonce)
48+
mock_response = '{"mocked": "response"}'
49+
mock_make_request.return_value = Uid2Response.from_string(mock_response)
50+
mock_parse_response.return_value = b'{"body":[],"status":"success"}'
5351
for timestamp in test_cases:
5452
self.identity_map_client.get_identity_buckets(dt.datetime.fromisoformat(timestamp))
55-
called_args, called_kwargs = mock_make_v2_request.call_args
53+
called_args, called_kwargs = mock_create_envelope.call_args
5654
self.assertEqual(expected_req, called_args[2])
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import os
2+
import unittest
3+
4+
from datetime import datetime, timedelta, timezone
5+
from urllib.error import URLError, HTTPError
6+
7+
from uid2_client import IdentityMapV3Client, IdentityMapV3Input, IdentityMapV3Response, normalize_and_hash_email, normalize_and_hash_phone
8+
from uid2_client.unmapped_identity_reason import UnmappedIdentityReason
9+
10+
11+
@unittest.skipIf(
12+
os.getenv("UID2_BASE_URL") == None
13+
or os.getenv("UID2_API_KEY") == None
14+
or os.getenv("UID2_SECRET_KEY") == None,
15+
reason="Environment variables UID2_BASE_URL, UID2_API_KEY, and UID2_SECRET_KEY must be set",
16+
)
17+
class IdentityMapV3IntegrationTests(unittest.TestCase):
18+
UID2_BASE_URL = None
19+
UID2_API_KEY = None
20+
UID2_SECRET_KEY = None
21+
22+
identity_map_client = None
23+
24+
@classmethod
25+
def setUpClass(cls):
26+
cls.UID2_BASE_URL = os.getenv("UID2_BASE_URL")
27+
cls.UID2_API_KEY = os.getenv("UID2_API_KEY")
28+
cls.UID2_SECRET_KEY = os.getenv("UID2_SECRET_KEY")
29+
30+
if cls.UID2_BASE_URL and cls.UID2_API_KEY and cls.UID2_SECRET_KEY:
31+
cls.identity_map_client = IdentityMapV3Client(cls.UID2_BASE_URL, cls.UID2_API_KEY, cls.UID2_SECRET_KEY)
32+
else:
33+
raise Exception("set the required UID2_BASE_URL/UID2_API_KEY/UID2_SECRET_KEY environment variables first")
34+
35+
def test_identity_map_emails(self):
36+
identity_map_input = IdentityMapV3Input.from_emails(
37+
38+
response = self.identity_map_client.generate_identity_map(identity_map_input)
39+
self.assert_mapped(response, "[email protected]")
40+
self.assert_mapped(response, "[email protected]")
41+
42+
self.assert_unmapped(response, UnmappedIdentityReason.OPTOUT, "[email protected]")
43+
44+
def test_identity_map_nothing_unmapped(self):
45+
identity_map_input = IdentityMapV3Input.from_emails(
46+
47+
response = self.identity_map_client.generate_identity_map(identity_map_input)
48+
self.assert_mapped(response, "[email protected]")
49+
self.assert_mapped(response, "[email protected]")
50+
51+
def test_identity_map_nothing_mapped(self):
52+
identity_map_input = IdentityMapV3Input.from_emails(["[email protected]"])
53+
response = self.identity_map_client.generate_identity_map(identity_map_input)
54+
self.assert_unmapped(response, UnmappedIdentityReason.OPTOUT, "[email protected]")
55+
56+
def test_identity_map_invalid_email(self):
57+
self.assertRaises(ValueError, IdentityMapV3Input.from_emails,
58+
["[email protected]", "this is not an email"])
59+
60+
def test_identity_map_invalid_phone(self):
61+
self.assertRaises(ValueError, IdentityMapV3Input.from_phones,
62+
["+12345678901", "this is not a phone number"])
63+
64+
def test_identity_map_invalid_hashed_email(self):
65+
identity_map_input = IdentityMapV3Input.from_hashed_emails(["this is not a hashed email"])
66+
response = self.identity_map_client.generate_identity_map(identity_map_input)
67+
self.assert_unmapped(response, UnmappedIdentityReason.INVALID_IDENTIFIER, "this is not a hashed email")
68+
69+
def test_identity_map_invalid_hashed_phone(self):
70+
identity_map_input = IdentityMapV3Input.from_hashed_phones(["this is not a hashed phone"])
71+
response = self.identity_map_client.generate_identity_map(identity_map_input)
72+
self.assert_unmapped(response, UnmappedIdentityReason.INVALID_IDENTIFIER, "this is not a hashed phone")
73+
74+
def test_identity_map_hashed_emails(self):
75+
hashed_email1 = normalize_and_hash_email("[email protected]")
76+
hashed_email2 = normalize_and_hash_email("[email protected]")
77+
hashed_opted_out_email = normalize_and_hash_email("[email protected]")
78+
identity_map_input = IdentityMapV3Input.from_hashed_emails([hashed_email1, hashed_email2, hashed_opted_out_email])
79+
80+
response = self.identity_map_client.generate_identity_map(identity_map_input)
81+
82+
self.assert_mapped(response, hashed_email1)
83+
self.assert_mapped(response, hashed_email2)
84+
85+
self.assert_unmapped(response, UnmappedIdentityReason.OPTOUT, hashed_opted_out_email)
86+
87+
def test_identity_map_duplicate_emails(self):
88+
identity_map_input = IdentityMapV3Input.from_emails(
89+
90+
91+
response = self.identity_map_client.generate_identity_map(identity_map_input)
92+
93+
mapped_identities = response.mapped_identities
94+
self.assertEqual(4, len(mapped_identities))
95+
96+
raw_uid = mapped_identities.get("[email protected]").current_raw_uid
97+
self.assertEqual(raw_uid, mapped_identities.get("[email protected]").current_raw_uid)
98+
self.assertEqual(raw_uid, mapped_identities.get("[email protected]").current_raw_uid)
99+
self.assertEqual(raw_uid, mapped_identities.get("[email protected]").current_raw_uid)
100+
101+
def test_identity_map_duplicate_hashed_emails(self):
102+
hashed_email = normalize_and_hash_email("[email protected]")
103+
duplicate_hashed_email = hashed_email
104+
hashed_opted_out_email = normalize_and_hash_email("[email protected]")
105+
duplicate_hashed_opted_out_email = hashed_opted_out_email
106+
107+
identity_map_input = IdentityMapV3Input.from_hashed_emails(
108+
[hashed_email, duplicate_hashed_email, hashed_opted_out_email, duplicate_hashed_opted_out_email])
109+
response = self.identity_map_client.generate_identity_map(identity_map_input)
110+
111+
self.assert_mapped(response, hashed_email)
112+
self.assert_mapped(response, duplicate_hashed_email)
113+
114+
self.assert_unmapped(response, UnmappedIdentityReason.OPTOUT, hashed_opted_out_email)
115+
self.assert_unmapped(response, UnmappedIdentityReason.OPTOUT, duplicate_hashed_opted_out_email)
116+
117+
def test_identity_map_empty_input(self):
118+
identity_map_input = IdentityMapV3Input.from_emails([])
119+
response = self.identity_map_client.generate_identity_map(identity_map_input)
120+
self.assertTrue(len(response.mapped_identities) == 0)
121+
self.assertTrue(len(response.unmapped_identities) == 0)
122+
123+
def test_identity_map_phones(self):
124+
identity_map_input = IdentityMapV3Input.from_phones(["+12345678901", "+98765432109", "+00000000000"])
125+
response = self.identity_map_client.generate_identity_map(identity_map_input)
126+
self.assert_mapped(response, "+12345678901")
127+
self.assert_mapped(response, "+98765432109")
128+
129+
self.assert_unmapped(response, UnmappedIdentityReason.OPTOUT, "+00000000000")
130+
131+
def test_identity_map_hashed_phones(self):
132+
hashed_phone1 = normalize_and_hash_phone("+12345678901")
133+
hashed_phone2 = normalize_and_hash_phone("+98765432109")
134+
hashed_opted_out_phone = normalize_and_hash_phone("+00000000000")
135+
identity_map_input = IdentityMapV3Input.from_hashed_phones([hashed_phone1, hashed_phone2, hashed_opted_out_phone])
136+
response = self.identity_map_client.generate_identity_map(identity_map_input)
137+
self.assert_mapped(response, hashed_phone1)
138+
self.assert_mapped(response, hashed_phone2)
139+
140+
self.assert_unmapped(response, UnmappedIdentityReason.OPTOUT, hashed_opted_out_phone)
141+
142+
def test_identity_map_all_identity_types_in_one_request(self):
143+
mapped_email = "[email protected]"
144+
optout_email = "[email protected]"
145+
mapped_phone = "+12345678901"
146+
optout_phone = "+00000000000"
147+
148+
mapped_email_hash = normalize_and_hash_email("[email protected]")
149+
optout_email_hash = normalize_and_hash_email(optout_email)
150+
mapped_phone_hash = normalize_and_hash_phone(mapped_phone)
151+
optout_phone_hash = normalize_and_hash_phone(optout_phone)
152+
153+
identity_map_input = (IdentityMapV3Input.from_emails([mapped_email, optout_email])
154+
.with_hashed_emails([mapped_email_hash, optout_email_hash])
155+
.with_phones([mapped_phone, optout_phone])
156+
.with_hashed_phones([mapped_phone_hash, optout_phone_hash]))
157+
158+
response = self.identity_map_client.generate_identity_map(identity_map_input)
159+
160+
# Test mapped identities
161+
self.assert_mapped(response, mapped_email)
162+
self.assert_mapped(response, mapped_email_hash)
163+
self.assert_mapped(response, mapped_phone)
164+
self.assert_mapped(response, mapped_phone_hash)
165+
166+
# Test unmapped identities
167+
self.assert_unmapped(response, UnmappedIdentityReason.OPTOUT, optout_email)
168+
self.assert_unmapped(response, UnmappedIdentityReason.OPTOUT, optout_email_hash)
169+
self.assert_unmapped(response, UnmappedIdentityReason.OPTOUT, optout_phone)
170+
self.assert_unmapped(response, UnmappedIdentityReason.OPTOUT, optout_phone_hash)
171+
172+
def test_identity_map_all_identity_types_added_one_by_one(self):
173+
mapped_email = "[email protected]"
174+
optout_phone = "+00000000000"
175+
mapped_phone_hash = normalize_and_hash_phone("+12345678901")
176+
optout_email_hash = normalize_and_hash_email("[email protected]")
177+
178+
identity_map_input = IdentityMapV3Input()
179+
identity_map_input.with_email(mapped_email)
180+
identity_map_input.with_phone(optout_phone)
181+
identity_map_input.with_hashed_phone(mapped_phone_hash)
182+
identity_map_input.with_hashed_email(optout_email_hash)
183+
184+
response = self.identity_map_client.generate_identity_map(identity_map_input)
185+
186+
# Test mapped identities
187+
self.assert_mapped(response, mapped_email)
188+
self.assert_mapped(response, mapped_phone_hash)
189+
190+
# Test unmapped identities
191+
self.assert_unmapped(response, UnmappedIdentityReason.OPTOUT, optout_phone)
192+
self.assert_unmapped(response, UnmappedIdentityReason.OPTOUT, optout_email_hash)
193+
194+
def test_identity_map_client_bad_url(self):
195+
identity_map_input = IdentityMapV3Input.from_emails(
196+
197+
client = IdentityMapV3Client("https://operator-bad-url.uidapi.com", os.getenv("UID2_API_KEY"), os.getenv("UID2_SECRET_KEY"))
198+
self.assertRaises(URLError, client.generate_identity_map, identity_map_input)
199+
200+
def test_identity_map_client_bad_api_key(self):
201+
identity_map_input = IdentityMapV3Input.from_emails(
202+
203+
client = IdentityMapV3Client(os.getenv("UID2_BASE_URL"), "bad-api-key", os.getenv("UID2_SECRET_KEY"))
204+
self.assertRaises(HTTPError, client.generate_identity_map, identity_map_input)
205+
206+
def test_identity_map_client_bad_secret(self):
207+
identity_map_input = IdentityMapV3Input.from_emails(
208+
209+
210+
client = IdentityMapV3Client(os.getenv("UID2_BASE_URL"), os.getenv("UID2_API_KEY"), "wJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg=")
211+
self.assertRaises(HTTPError, client.generate_identity_map, identity_map_input)
212+
213+
def assert_mapped(self, response: IdentityMapV3Response, dii):
214+
mapped_identity = response.mapped_identities.get(dii)
215+
self.assertIsNotNone(mapped_identity)
216+
self.assertIsNotNone(mapped_identity.current_raw_uid)
217+
218+
# Refresh from should be now or in the future, allow some slack for time between request and this assertion
219+
one_minute_ago = datetime.now(timezone.utc) - timedelta(seconds=60)
220+
self.assertTrue(mapped_identity.refresh_from > one_minute_ago)
221+
222+
unmapped_identity = response.unmapped_identities.get(dii)
223+
self.assertIsNone(unmapped_identity)
224+
225+
def assert_unmapped(self, response, reason, dii):
226+
unmapped_identity = response.unmapped_identities.get(dii)
227+
self.assertEqual(reason, unmapped_identity.reason)
228+
229+
mapped_identity = response.mapped_identities.get(dii)
230+
self.assertIsNone(mapped_identity)
231+
232+
233+
234+
if __name__ == '__main__':
235+
unittest.main()

0 commit comments

Comments
 (0)