Skip to content

Commit 0192b52

Browse files
Merge pull request #58 from IABTechLab/sch-UID2-5486-identity-map-v3
sch-UID2-5486 added identity map v3 client and binary support
2 parents c1a1a47 + 09aa42a commit 0192b52

21 files changed

+1031
-72
lines changed

tests/test_bidstream_client_e2e.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import os
2+
import unittest
3+
4+
from uid2_client import BidstreamClient, Uid2PublisherClient, TokenGenerateInput, DecryptionStatus
5+
6+
7+
@unittest.skipIf(
8+
os.getenv("UID2_BASE_URL") is None
9+
or os.getenv("UID2_API_KEY") is None
10+
or os.getenv("UID2_SECRET_KEY") is None,
11+
"Environment variables UID2_BASE_URL, UID2_API_KEY, and UID2_SECRET_KEY must be set",
12+
)
13+
class BidstreamClientIntegrationTests(unittest.TestCase):
14+
@classmethod
15+
def setUpClass(cls):
16+
cls.UID2_BASE_URL = os.getenv("UID2_BASE_URL")
17+
cls.UID2_API_KEY = os.getenv("UID2_API_KEY")
18+
cls.UID2_SECRET_KEY = os.getenv("UID2_SECRET_KEY")
19+
20+
if cls.UID2_BASE_URL and cls.UID2_API_KEY and cls.UID2_SECRET_KEY:
21+
cls.bidstream_client = BidstreamClient(cls.UID2_BASE_URL, cls.UID2_API_KEY, cls.UID2_SECRET_KEY)
22+
cls.publisher_client = Uid2PublisherClient(cls.UID2_BASE_URL, cls.UID2_API_KEY, cls.UID2_SECRET_KEY)
23+
else:
24+
raise Exception("set the required UID2_BASE_URL/UID2_API_KEY/UID2_SECRET_KEY environment variables first")
25+
26+
def test_bidstream_client_key_refresh(self):
27+
refresh_response = self.bidstream_client.refresh()
28+
self.assertTrue(refresh_response.success)
29+
30+
def test_bidstream_client_with_generated_token(self):
31+
token_response = self.publisher_client.generate_token(
32+
TokenGenerateInput.from_email("[email protected]").do_not_generate_tokens_for_opted_out()
33+
)
34+
identity = token_response.get_identity()
35+
36+
advertising_token = identity.get_advertising_token()
37+
self.assertIsNotNone(advertising_token)
38+
39+
refresh_response = self.bidstream_client.refresh()
40+
self.assertTrue(refresh_response.success)
41+
42+
decryption_response = self.bidstream_client.decrypt_token_into_raw_uid(
43+
advertising_token, "example.com"
44+
)
45+
46+
self.assertTrue(decryption_response.success)
47+
self.assertIsNotNone(decryption_response.uid)
48+
self.assertIsNotNone(decryption_response.established)
49+
self.assertIsNotNone(decryption_response.site_id)
50+
51+
def test_bidstream_client_with_invalid_token(self):
52+
refresh_response = self.bidstream_client.refresh()
53+
self.assertTrue(refresh_response.success)
54+
55+
invalid_token = "invalid-token"
56+
decryption_response = self.bidstream_client.decrypt_token_into_raw_uid(
57+
invalid_token, "example.com"
58+
)
59+
self.assertFalse(decryption_response.success)
60+
61+
def test_bidstream_client_without_refresh(self):
62+
token_response = self.publisher_client.generate_token(
63+
TokenGenerateInput.from_email("[email protected]").do_not_generate_tokens_for_opted_out()
64+
)
65+
identity = token_response.get_identity()
66+
advertising_token = identity.get_advertising_token()
67+
68+
fresh_client = BidstreamClient(self.UID2_BASE_URL, self.UID2_API_KEY, self.UID2_SECRET_KEY)
69+
70+
decryption_response = fresh_client.decrypt_token_into_raw_uid(
71+
advertising_token, "example.com"
72+
)
73+
self.assertFalse(decryption_response.success)
74+
75+
def test_bidstream_client_error_handling(self):
76+
bad_client = BidstreamClient(self.UID2_BASE_URL, "bad-api-key", self.UID2_SECRET_KEY)
77+
refresh_response = bad_client.refresh()
78+
self.assertFalse(refresh_response.success)
79+
80+
bad_client = BidstreamClient(self.UID2_BASE_URL, self.UID2_API_KEY, "bad-secret-key")
81+
refresh_response = bad_client.refresh()
82+
self.assertFalse(refresh_response.success)
83+
84+
def test_bidstream_client_phone_token_decryption(self):
85+
token_response = self.publisher_client.generate_token(
86+
TokenGenerateInput.from_phone("+12345678901").do_not_generate_tokens_for_opted_out()
87+
)
88+
self.assertFalse(token_response.is_optout())
89+
90+
identity = token_response.get_identity()
91+
advertising_token = identity.get_advertising_token()
92+
93+
refresh_response = self.bidstream_client.refresh()
94+
self.assertTrue(refresh_response.success)
95+
96+
decryption_response = self.bidstream_client.decrypt_token_into_raw_uid(
97+
advertising_token, "example.com"
98+
)
99+
100+
self.assertTrue(decryption_response.success)
101+
self.assertIsNotNone(decryption_response.uid)
102+
103+
104+
if __name__ == '__main__':
105+
unittest.main()

tests/test_identity_map_client.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from uid2_client import IdentityMapClient, IdentityMapInput, normalize_and_hash_email, normalize_and_hash_phone
88

9-
109
@unittest.skipIf(
1110
os.getenv("UID2_BASE_URL") == None
1211
or os.getenv("UID2_API_KEY") == None

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

0 commit comments

Comments
 (0)