From 9ab782703628ad2c7b334ca558142083bf65deb7 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Thu, 29 May 2025 16:39:09 +0800 Subject: [PATCH 01/26] Added V3 as copy of V2 --- .../com/uid2/client/IdentityMapV3Client.java | 28 +++ .../com/uid2/client/IdentityMapV3Helper.java | 35 +++ .../com/uid2/client/IdentityMapV3Input.java | 91 +++++++ .../uid2/client/IdentityMapV3Response.java | 99 ++++++++ .../client/IdentityMapV3IntegrationTests.java | 237 ++++++++++++++++++ 5 files changed, 490 insertions(+) create mode 100644 src/main/java/com/uid2/client/IdentityMapV3Client.java create mode 100644 src/main/java/com/uid2/client/IdentityMapV3Helper.java create mode 100644 src/main/java/com/uid2/client/IdentityMapV3Input.java create mode 100644 src/main/java/com/uid2/client/IdentityMapV3Response.java create mode 100644 src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java diff --git a/src/main/java/com/uid2/client/IdentityMapV3Client.java b/src/main/java/com/uid2/client/IdentityMapV3Client.java new file mode 100644 index 0000000..bd1dffc --- /dev/null +++ b/src/main/java/com/uid2/client/IdentityMapV3Client.java @@ -0,0 +1,28 @@ +package com.uid2.client; + +public class IdentityMapV3Client { + /** + * @param uid2BaseUrl The UID2 Base URL + * @param clientApiKey Your client API key + * @param base64SecretKey Your client secret key + */ + public IdentityMapV3Client(String uid2BaseUrl, String clientApiKey, String base64SecretKey) { + identityMapHelper = new IdentityMapV3Helper(base64SecretKey); + uid2ClientHelper = new Uid2ClientHelper(uid2BaseUrl, clientApiKey); + } + + /** + * @param identityMapInput represents the input required for /identity/map + * @return an IdentityMapResponse instance + * @throws Uid2Exception if the response did not contain a "success" status, or the response code was not 200, or there was an error communicating with the provided UID2 Base URL + */ + public IdentityMapV3Response generateIdentityMap(IdentityMapV3Input identityMapInput) { + EnvelopeV2 envelope = identityMapHelper.createEnvelopeForIdentityMapRequest(identityMapInput); + + String responseString = uid2ClientHelper.makeRequest(envelope, "/v2/identity/map"); + return identityMapHelper.createIdentityMapResponse(responseString, envelope, identityMapInput); + } + + private final IdentityMapV3Helper identityMapHelper; + private final Uid2ClientHelper uid2ClientHelper; +} diff --git a/src/main/java/com/uid2/client/IdentityMapV3Helper.java b/src/main/java/com/uid2/client/IdentityMapV3Helper.java new file mode 100644 index 0000000..5091d49 --- /dev/null +++ b/src/main/java/com/uid2/client/IdentityMapV3Helper.java @@ -0,0 +1,35 @@ +package com.uid2.client; + +import com.google.gson.Gson; + +import java.nio.charset.StandardCharsets; + +public class IdentityMapV3Helper { + /** + * @param base64SecretKey your UID2 client secret + */ + public IdentityMapV3Helper(String base64SecretKey) {uid2Helper = new Uid2Helper(base64SecretKey);} + + /** + * @param identityMapInput represents the input required for /identity/map + * @return an EnvelopeV2 instance to use in the POST body of /identity/map + */ + public EnvelopeV2 createEnvelopeForIdentityMapRequest(IdentityMapV3Input identityMapInput) { + byte[] jsonBytes = new Gson().toJson(identityMapInput).getBytes(StandardCharsets.UTF_8); + return uid2Helper.createEnvelopeV2(jsonBytes); + } + + + /** + * @param responseString the response body returned by a call to /identity/map + * @param envelope the EnvelopeV2 instance returned by {@link #createEnvelopeForIdentityMapRequest} + * @param identityMapInput the same instance that was passed to {@link #createEnvelopeForIdentityMapRequest}. + * @return an IdentityMapResponse instance + */ + public IdentityMapV3Response createIdentityMapResponse(String responseString, EnvelopeV2 envelope, IdentityMapV3Input identityMapInput) { + String decryptedResponseString = uid2Helper.decrypt(responseString, envelope.getNonce()); + return new IdentityMapV3Response(decryptedResponseString, identityMapInput); + } + + Uid2Helper uid2Helper; +} diff --git a/src/main/java/com/uid2/client/IdentityMapV3Input.java b/src/main/java/com/uid2/client/IdentityMapV3Input.java new file mode 100644 index 0000000..6603a7f --- /dev/null +++ b/src/main/java/com/uid2/client/IdentityMapV3Input.java @@ -0,0 +1,91 @@ +package com.uid2.client; + +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class IdentityMapV3Input { + /** + * @param emails a list of normalized or unnormalized email addresses + * @return a IdentityMapInput instance, to be used in {@link IdentityMapHelper#createEnvelopeForIdentityMapRequest} + */ + public static IdentityMapV3Input fromEmails(Iterable emails) { + return new IdentityMapV3Input(IdentityType.Email, emails, false); + } + + /** + * @param phones a normalized phone number + * @return an IdentityMapInput instance + */ + public static IdentityMapV3Input fromPhones(Iterable phones) { + return new IdentityMapV3Input(IdentityType.Phone, phones, false); + } + + /** + * @param hashedEmails a normalized and hashed email address + * @return an IdentityMapInput instance + */ + public static IdentityMapV3Input fromHashedEmails(Iterable hashedEmails) { + return new IdentityMapV3Input(IdentityType.Email, hashedEmails, true); + } + + /** + * @param hashedPhones a normalized and hashed phone number + * @return an IdentityMapInput instance + */ + public static IdentityMapV3Input fromHashedPhones(Iterable hashedPhones) { + return new IdentityMapV3Input(IdentityType.Phone, hashedPhones, true); + } + + private IdentityMapV3Input(IdentityType identityType, Iterable emailsOrPhones, boolean alreadyHashed) { + if (identityType == IdentityType.Email) { + hashedNormalizedEmails = new ArrayList<>(); + for (String email : emailsOrPhones) { + if (alreadyHashed) { + hashedNormalizedEmails.add(email); + } else { + String hashedEmail = InputUtil.normalizeAndHashEmail(email); + hashedNormalizedEmails.add(hashedEmail); + addHashedToRawDiiMapping(hashedEmail, email); + } + } + } else { //phone + hashedNormalizedPhones = new ArrayList<>(); + for (String phone : emailsOrPhones) { + if (alreadyHashed) { + hashedNormalizedPhones.add(phone); + } else { + if (!InputUtil.isPhoneNumberNormalized(phone)) { + throw new IllegalArgumentException("phone number is not normalized: " + phone); + } + + String hashedNormalizedPhone = InputUtil.getBase64EncodedHash(phone); + addHashedToRawDiiMapping(hashedNormalizedPhone, phone); + hashedNormalizedPhones.add(hashedNormalizedPhone); + } + } + } + } + + private void addHashedToRawDiiMapping(String hashedDii, String rawDii) { + hashedDiiToRawDiis.computeIfAbsent(hashedDii, k -> new ArrayList<>()).add(rawDii); + } + + + List getRawDiis(String identifier) { + final boolean wasInputAlreadyHashed = hashedDiiToRawDiis.isEmpty(); + if (wasInputAlreadyHashed) + return Collections.singletonList(identifier); + return hashedDiiToRawDiis.get(identifier); + } + + @SerializedName("email_hash") + private List hashedNormalizedEmails; + @SerializedName("phone_hash") + private List hashedNormalizedPhones; + + private final transient HashMap> hashedDiiToRawDiis = new HashMap<>(); +} diff --git a/src/main/java/com/uid2/client/IdentityMapV3Response.java b/src/main/java/com/uid2/client/IdentityMapV3Response.java new file mode 100644 index 0000000..7d47a8a --- /dev/null +++ b/src/main/java/com/uid2/client/IdentityMapV3Response.java @@ -0,0 +1,99 @@ +package com.uid2.client; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class IdentityMapV3Response { + IdentityMapV3Response(String response, IdentityMapV3Input identityMapInput) { + JsonObject responseJson = new Gson().fromJson(response, JsonObject.class); + status = responseJson.get("status").getAsString(); + + if (!isSuccess()) { + throw new Uid2Exception("Got unexpected identity map status: " + status); + } + + Gson gson = new Gson(); + JsonObject body = getBodyAsJson(responseJson); + + Iterable mapped = getJsonArray(body, "mapped"); + for (JsonElement identity : mapped) { + List rawDiis = getRawDiis(identity, identityMapInput); + MappedIdentity mappedIdentity = gson.fromJson(identity, MappedIdentity.class); + for (String rawDii : rawDiis) { + mappedIdentities.put(rawDii, mappedIdentity); + } + } + + Iterable unmapped = getJsonArray(body, "unmapped"); + for (JsonElement identity : unmapped) { + List rawDiis = getRawDiis(identity, identityMapInput); + UnmappedIdentity unmappedIdentity = gson.fromJson(identity, UnmappedIdentity.class); + for (String rawDii : rawDiis) { + unmappedIdentities.put(rawDii, unmappedIdentity); + } + } + } + + private static Iterable getJsonArray(JsonObject body, String header) { + JsonElement jsonElement = body.get(header); + if (jsonElement == null) { + return Collections.emptyList(); + } + return jsonElement.getAsJsonArray(); + } + + private List getRawDiis(JsonElement identity, IdentityMapV3Input identityMapInput) { + String identifier = identity.getAsJsonObject().get("identifier").getAsString(); + return identityMapInput.getRawDiis(identifier); + } + + public boolean isSuccess() { + return "success".equals(status); + } + static JsonObject getBodyAsJson(JsonObject jsonResponse) { + return jsonResponse.get("body").getAsJsonObject(); + } + + static public class MappedIdentity { + public MappedIdentity(String rawUid, String bucketId) { + this.rawUid = rawUid; + this.bucketId = bucketId; + } + + public String getRawUid() {return rawUid;} + public String getBucketId() {return bucketId;} + + @SerializedName("advertising_id") + private final String rawUid; + @SerializedName("bucket_id") + private final String bucketId; + } + + static public class UnmappedIdentity { + public UnmappedIdentity(String reason) { + this.reason = reason; + } + + public String getReason() {return reason;} + + private final String reason; + } + + public HashMap getMappedIdentities() { + return mappedIdentities; + } + + public HashMap getUnmappedIdentities() { + return unmappedIdentities; + } + + private final String status; + private final HashMap mappedIdentities = new HashMap<>(); + private final HashMap unmappedIdentities = new HashMap<>(); +} diff --git a/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java new file mode 100644 index 0000000..b8c4abb --- /dev/null +++ b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java @@ -0,0 +1,237 @@ +package com.uid2.client; + +import okhttp3.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.*; + + +//most tests in this class require these env vars to be configured: UID2_BASE_URL, UID2_API_KEY, UID2_SECRET_KEY +@EnabledIfEnvironmentVariable(named = "UID2_BASE_URL", matches = "\\S+") +public class IdentityMapV3IntegrationTests { + final private IdentityMapV3Client identityMapClient = new IdentityMapV3Client(System.getenv("UID2_BASE_URL"), System.getenv("UID2_API_KEY"), System.getenv("UID2_SECRET_KEY")); + + @Test + public void identityMapEmails() { + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Arrays.asList("hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com")); + Response response = new Response(identityMapInput); + + response.assertMapped("hopefully-not-opted-out@example.com"); + response.assertMapped("somethingelse@example.com"); + + response.assertUnmapped("optout", "optout@example.com"); + } + + @Test + public void identityMapNothingUnmapped() { + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Arrays.asList("hopefully-not-opted-out@example.com", "somethingelse@example.com")); + Response response = new Response(identityMapInput); + + response.assertMapped("hopefully-not-opted-out@example.com"); + response.assertMapped("somethingelse@example.com"); + } + + @Test + public void identityMapNothingMapped() { + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Collections.singletonList("optout@example.com")); + Response response = new Response(identityMapInput); + + response.assertUnmapped("optout", "optout@example.com"); + } + + + @Test + public void identityMapInvalidEmail() { + assertThrows(IllegalArgumentException.class, + () -> IdentityMapV3Input.fromEmails(Arrays.asList("email@example.com", "this is not an email"))); + } + + @Test + public void identityMapInvalidPhone() { + assertThrows(IllegalArgumentException.class, + () -> IdentityMapV3Input.fromPhones(Arrays.asList("+12345678901", "this is not a phone number"))); + } + + @Test + public void identityMapInvalidHashedEmail() { + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromHashedEmails(Collections.singletonList("this is not a hashed email")); + + Response response = new Response(identityMapInput); + + response.assertUnmapped("invalid identifier", "this is not a hashed email"); + } + + @Test + public void identityMapInvalidHashedPhone() { + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromHashedPhones(Collections.singletonList("this is not a hashed phone")); + + Response response = new Response(identityMapInput); + response.assertUnmapped("invalid identifier", "this is not a hashed phone"); + } + + @Test + public void identityMapHashedEmails() { + String hashedEmail1 = InputUtil.normalizeAndHashEmail("hopefully-not-opted-out@example.com"); + String hashedEmail2 = InputUtil.normalizeAndHashEmail("somethingelse@example.com"); + String hashedOptedOutEmail = InputUtil.normalizeAndHashEmail("optout@example.com"); + + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromHashedEmails(Arrays.asList(hashedEmail1, hashedEmail2, hashedOptedOutEmail)); + Response response = new Response(identityMapInput); + + response.assertMapped(hashedEmail1); + response.assertMapped(hashedEmail2); + + response.assertUnmapped("optout", hashedOptedOutEmail); + } + + @Test + public void identityMapDuplicateEmails() { + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Arrays.asList("JANE.SAOIRSE@gmail.com", "Jane.Saoirse@gmail.com", "JaneSaoirse+UID2@gmail.com", "janesaoirse@gmail.com", "JANE.SAOIRSE@gmail.com")); + IdentityMapV3Response identityMapResponse = identityMapClient.generateIdentityMap(identityMapInput); + + HashMap mappedIdentities = identityMapResponse.getMappedIdentities(); + assertEquals(4, mappedIdentities.size()); //it's not 5 because the last email is an exact match to the first email + + String rawUid = mappedIdentities.get("JANE.SAOIRSE@gmail.com").getRawUid(); + assertEquals(rawUid, mappedIdentities.get("Jane.Saoirse@gmail.com").getRawUid()); + assertEquals(rawUid, mappedIdentities.get("JaneSaoirse+UID2@gmail.com").getRawUid()); + assertEquals(rawUid, mappedIdentities.get("janesaoirse@gmail.com").getRawUid()); + } + + + @Test + public void identityMapDuplicateHashedEmails() { + String hashedEmail = InputUtil.normalizeAndHashEmail("hopefully-not-opted-out@example.com"); + String duplicateHashedEmail = hashedEmail; + + String hashedOptedOutEmail = InputUtil.normalizeAndHashEmail("optout@example.com"); + String duplicateOptedOutEmail = hashedOptedOutEmail; + + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromHashedEmails(Arrays.asList(hashedEmail, duplicateHashedEmail, hashedOptedOutEmail, duplicateOptedOutEmail)); + Response response = new Response(identityMapInput); + + response.assertMapped(hashedEmail); + response.assertMapped(duplicateHashedEmail); + + response.assertUnmapped("optout", hashedOptedOutEmail); + response.assertUnmapped("optout", duplicateOptedOutEmail); + } + + @Test + public void identityMapEmptyInput() { + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Collections.emptyList()); + IdentityMapV3Response identityMapResponse = identityMapClient.generateIdentityMap(identityMapInput); + assertTrue(identityMapResponse.getMappedIdentities().isEmpty()); + assertTrue(identityMapResponse.getUnmappedIdentities().isEmpty()); + } + + + @Test + public void identityMapPhones() { + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromPhones(Arrays.asList("+12345678901", "+98765432109", "+00000000000")); + Response response = new Response(identityMapInput); + + response.assertMapped("+12345678901"); + response.assertMapped("+98765432109"); + + response.assertUnmapped("optout", "+00000000000"); + } + + @Test + public void identityMapHashedPhones() { + String hashedPhone1 = InputUtil.getBase64EncodedHash("+12345678901"); + String hashedPhone2 = InputUtil.getBase64EncodedHash("+98765432109"); + String hashedOptedOutPhone = InputUtil.getBase64EncodedHash("+00000000000"); + + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromHashedPhones(Arrays.asList(hashedPhone1, hashedPhone2, hashedOptedOutPhone)); + Response response = new Response(identityMapInput); + + response.assertMapped(hashedPhone1); + response.assertMapped(hashedPhone2); + + response.assertUnmapped("optout", hashedOptedOutPhone); + } + + + class Response { + Response(IdentityMapV3Input identityMapInput) { + identityMapResponse = identityMapClient.generateIdentityMap(identityMapInput); + } + + void assertMapped(String dii) { + IdentityMapV3Response.MappedIdentity mappedIdentity = identityMapResponse.getMappedIdentities().get(dii); + assertNotNull(mappedIdentity); + assertFalse(mappedIdentity.getRawUid().isEmpty()); + assertFalse(mappedIdentity.getBucketId().isEmpty()); + + IdentityMapV3Response.UnmappedIdentity unmappedIdentity = identityMapResponse.getUnmappedIdentities().get(dii); + assertNull(unmappedIdentity); + } + + void assertUnmapped(String reason, String dii) { + assertEquals(reason, identityMapResponse.getUnmappedIdentities().get(dii).getReason()); + + IdentityMapV3Response.MappedIdentity mappedIdentity = identityMapResponse.getMappedIdentities().get(dii); + assertNull(mappedIdentity); + } + + + private final IdentityMapV3Response identityMapResponse; + } + + @Test + public void identityMapEmailsUseOwnHttp() { + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Arrays.asList("hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com")); + + final IdentityMapV3Helper identityMapHelper = new IdentityMapV3Helper(System.getenv("UID2_SECRET_KEY")); + EnvelopeV2 envelopeV2 = identityMapHelper.createEnvelopeForIdentityMapRequest(identityMapInput); + String uid2BaseUrl = System.getenv("UID2_BASE_URL"); + String clientApiKey = System.getenv("UID2_API_KEY"); + Headers headers = new Headers.Builder().add("Authorization", "Bearer " + clientApiKey).add("X-UID2-Client-Version: java-" + Uid2Helper.getArtifactAndVersion()).build(); + + Request request = new Request.Builder().url(uid2BaseUrl + "/v2/identity/map").headers(headers) + .post(RequestBody.create(envelopeV2.getEnvelope(), MediaType.get("application/x-www-form-urlencoded"))).build(); + + OkHttpClient client = new OkHttpClient(); + try (okhttp3.Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new Uid2Exception("Unexpected code " + response); + } + + String responseString = response.body() != null ? response.body().string() : response.toString(); + IdentityMapV3Response identityMapResponse = identityMapHelper.createIdentityMapResponse(responseString, envelopeV2, identityMapInput); + + IdentityMapV3Response.MappedIdentity mappedIdentity = identityMapResponse.getMappedIdentities().get("hopefully-not-opted-out@example.com"); + assertFalse(mappedIdentity.getRawUid().isEmpty()); + } catch (IOException e) { + throw new Uid2Exception("error communicating with api endpoint", e); + } + } + + @Test + public void identityMapBadUrl() { + IdentityMapV3Client identityMapClient = new IdentityMapV3Client("https://operator-bad-url.uidapi.com", System.getenv("UID2_API_KEY"), System.getenv("UID2_SECRET_KEY")); + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Collections.singletonList("email@example.com")); + assertThrows(Uid2Exception.class, () -> identityMapClient.generateIdentityMap(identityMapInput)); + } + + @Test + public void identityMapBadApiKey() { + IdentityMapV3Client identityMapClient = new IdentityMapV3Client(System.getenv("UID2_BASE_URL"), "bad-api-key", System.getenv("UID2_SECRET_KEY")); + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Collections.singletonList("email@example.com")); + assertThrows(Uid2Exception.class, () -> identityMapClient.generateIdentityMap(identityMapInput)); + } + + @Test + public void identityMapBadSecret() { + IdentityMapV3Client identityMapClient = new IdentityMapV3Client(System.getenv("UID2_BASE_URL"), System.getenv("UID2_API_KEY"), "wJ0hP19QU4hmpB64Y3fV2dAed8t/mupw3sjN5jNRFzg="); + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Collections.singletonList("email@example.com")); + assertThrows(Uid2Exception.class, () -> identityMapClient.generateIdentityMap(identityMapInput)); + } +} From 7eade1a434b41f52d9d24296caa9b79853c23b19 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Thu, 29 May 2025 17:02:34 +0800 Subject: [PATCH 02/26] Serializing dii in the new format --- .../com/uid2/client/IdentityMapV3Client.java | 4 ++-- .../com/uid2/client/IdentityMapV3Input.java | 21 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/uid2/client/IdentityMapV3Client.java b/src/main/java/com/uid2/client/IdentityMapV3Client.java index bd1dffc..b3b8a1c 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Client.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Client.java @@ -13,13 +13,13 @@ public IdentityMapV3Client(String uid2BaseUrl, String clientApiKey, String base6 /** * @param identityMapInput represents the input required for /identity/map - * @return an IdentityMapResponse instance + * @return an IdentityMapV3Response instance * @throws Uid2Exception if the response did not contain a "success" status, or the response code was not 200, or there was an error communicating with the provided UID2 Base URL */ public IdentityMapV3Response generateIdentityMap(IdentityMapV3Input identityMapInput) { EnvelopeV2 envelope = identityMapHelper.createEnvelopeForIdentityMapRequest(identityMapInput); - String responseString = uid2ClientHelper.makeRequest(envelope, "/v2/identity/map"); + String responseString = uid2ClientHelper.makeRequest(envelope, "/v3/identity/map"); return identityMapHelper.createIdentityMapResponse(responseString, envelope, identityMapInput); } diff --git a/src/main/java/com/uid2/client/IdentityMapV3Input.java b/src/main/java/com/uid2/client/IdentityMapV3Input.java index 6603a7f..e3b817d 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Input.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Input.java @@ -45,10 +45,10 @@ private IdentityMapV3Input(IdentityType identityType, Iterable emailsOrP hashedNormalizedEmails = new ArrayList<>(); for (String email : emailsOrPhones) { if (alreadyHashed) { - hashedNormalizedEmails.add(email); + hashedNormalizedEmails.add(new Identity(email)); } else { String hashedEmail = InputUtil.normalizeAndHashEmail(email); - hashedNormalizedEmails.add(hashedEmail); + hashedNormalizedEmails.add(new Identity(hashedEmail)); addHashedToRawDiiMapping(hashedEmail, email); } } @@ -56,7 +56,7 @@ private IdentityMapV3Input(IdentityType identityType, Iterable emailsOrP hashedNormalizedPhones = new ArrayList<>(); for (String phone : emailsOrPhones) { if (alreadyHashed) { - hashedNormalizedPhones.add(phone); + hashedNormalizedPhones.add(new Identity(phone)); } else { if (!InputUtil.isPhoneNumberNormalized(phone)) { throw new IllegalArgumentException("phone number is not normalized: " + phone); @@ -64,7 +64,7 @@ private IdentityMapV3Input(IdentityType identityType, Iterable emailsOrP String hashedNormalizedPhone = InputUtil.getBase64EncodedHash(phone); addHashedToRawDiiMapping(hashedNormalizedPhone, phone); - hashedNormalizedPhones.add(hashedNormalizedPhone); + hashedNormalizedPhones.add(new Identity(hashedNormalizedPhone)); } } } @@ -83,9 +83,18 @@ List getRawDiis(String identifier) { } @SerializedName("email_hash") - private List hashedNormalizedEmails; + private List hashedNormalizedEmails; @SerializedName("phone_hash") - private List hashedNormalizedPhones; + private List hashedNormalizedPhones; private final transient HashMap> hashedDiiToRawDiis = new HashMap<>(); + + private static class Identity { + @SerializedName("i") + private final String i; + + public Identity(String value) { + this.i = value; + } + } } From ec2777958b4eb243747383715aaf32687c41993e Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Fri, 30 May 2025 18:58:04 +0800 Subject: [PATCH 03/26] Adapted V3 Identity Map clases to the new endpoint --- .../com/uid2/client/IdentityMapV3Helper.java | 2 +- .../com/uid2/client/IdentityMapV3Input.java | 131 ++++++++++-------- .../uid2/client/IdentityMapV3Response.java | 113 +++++++++------ .../client/IdentityMapV3IntegrationTests.java | 120 +++++++++------- 4 files changed, 216 insertions(+), 150 deletions(-) diff --git a/src/main/java/com/uid2/client/IdentityMapV3Helper.java b/src/main/java/com/uid2/client/IdentityMapV3Helper.java index 5091d49..8b89bee 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Helper.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Helper.java @@ -24,7 +24,7 @@ public EnvelopeV2 createEnvelopeForIdentityMapRequest(IdentityMapV3Input identit * @param responseString the response body returned by a call to /identity/map * @param envelope the EnvelopeV2 instance returned by {@link #createEnvelopeForIdentityMapRequest} * @param identityMapInput the same instance that was passed to {@link #createEnvelopeForIdentityMapRequest}. - * @return an IdentityMapResponse instance + * @return an IdentityMapV3Response instance */ public IdentityMapV3Response createIdentityMapResponse(String responseString, EnvelopeV2 envelope, IdentityMapV3Input identityMapInput) { String decryptedResponseString = uid2Helper.decrypt(responseString, envelope.getNonce()); diff --git a/src/main/java/com/uid2/client/IdentityMapV3Input.java b/src/main/java/com/uid2/client/IdentityMapV3Input.java index e3b817d..b8730d9 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Input.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Input.java @@ -2,99 +2,112 @@ import com.google.gson.annotations.SerializedName; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; +import java.util.*; public class IdentityMapV3Input { /** * @param emails a list of normalized or unnormalized email addresses - * @return a IdentityMapInput instance, to be used in {@link IdentityMapHelper#createEnvelopeForIdentityMapRequest} + * @return a IdentityMapV3Input instance, to be used in {@link IdentityMapHelper#createEnvelopeForIdentityMapRequest} */ - public static IdentityMapV3Input fromEmails(Iterable emails) { - return new IdentityMapV3Input(IdentityType.Email, emails, false); + public static IdentityMapV3Input fromEmails(List emails) { + return new IdentityMapV3Input().withEmails(emails); } /** - * @param phones a normalized phone number - * @return an IdentityMapInput instance + * @param hashedEmails a normalized and hashed email address + * @return an IdentityMapV3Input instance */ - public static IdentityMapV3Input fromPhones(Iterable phones) { - return new IdentityMapV3Input(IdentityType.Phone, phones, false); + public static IdentityMapV3Input fromHashedEmails(List hashedEmails) { + return new IdentityMapV3Input().withHashedEmails(hashedEmails); } /** - * @param hashedEmails a normalized and hashed email address - * @return an IdentityMapInput instance + * @param phones a normalized phone number + * @return an IdentityMapV3Input instance */ - public static IdentityMapV3Input fromHashedEmails(Iterable hashedEmails) { - return new IdentityMapV3Input(IdentityType.Email, hashedEmails, true); + public static IdentityMapV3Input fromPhones(List phones) { + return new IdentityMapV3Input().withPhones(phones); } /** * @param hashedPhones a normalized and hashed phone number - * @return an IdentityMapInput instance + * @return an IdentityMapV3Input instance */ - public static IdentityMapV3Input fromHashedPhones(Iterable hashedPhones) { - return new IdentityMapV3Input(IdentityType.Phone, hashedPhones, true); + public static IdentityMapV3Input fromHashedPhones(List hashedPhones) { + return new IdentityMapV3Input().withHashedPhones(hashedPhones); } - private IdentityMapV3Input(IdentityType identityType, Iterable emailsOrPhones, boolean alreadyHashed) { - if (identityType == IdentityType.Email) { - hashedNormalizedEmails = new ArrayList<>(); - for (String email : emailsOrPhones) { - if (alreadyHashed) { - hashedNormalizedEmails.add(new Identity(email)); - } else { - String hashedEmail = InputUtil.normalizeAndHashEmail(email); - hashedNormalizedEmails.add(new Identity(hashedEmail)); - addHashedToRawDiiMapping(hashedEmail, email); - } - } - } else { //phone - hashedNormalizedPhones = new ArrayList<>(); - for (String phone : emailsOrPhones) { - if (alreadyHashed) { - hashedNormalizedPhones.add(new Identity(phone)); - } else { - if (!InputUtil.isPhoneNumberNormalized(phone)) { - throw new IllegalArgumentException("phone number is not normalized: " + phone); - } - - String hashedNormalizedPhone = InputUtil.getBase64EncodedHash(phone); - addHashedToRawDiiMapping(hashedNormalizedPhone, phone); - hashedNormalizedPhones.add(new Identity(hashedNormalizedPhone)); - } - } + private transient final Map> diiMappings = new HashMap<>(); + + @SerializedName("email_hash") + private final List hashedEmails = new ArrayList<>(); + + @SerializedName("phone_hash") + private final List hashedPhones = new ArrayList<>(); + + private IdentityMapV3Input() {} + + public IdentityMapV3Input withHashedEmails(List hashedEmails) { + for (String hashedEmail : hashedEmails) { + this.hashedEmails.add(new Identity(hashedEmail)); + addToDiiMappings(hashedEmail, hashedEmail); + } + return this; + } + + public IdentityMapV3Input withHashedPhones(List hashedPhones) { + for (String hashedPhone : hashedPhones) { + this.hashedPhones.add(new Identity(hashedPhone)); + addToDiiMappings(hashedPhone, hashedPhone); } + return this; } - private void addHashedToRawDiiMapping(String hashedDii, String rawDii) { - hashedDiiToRawDiis.computeIfAbsent(hashedDii, k -> new ArrayList<>()).add(rawDii); + public IdentityMapV3Input withEmails(List emails) { + for (String email : emails) { + String hash = InputUtil.normalizeAndHashEmail(email); + this.hashedEmails.add(new Identity(hash)); + addToDiiMappings(hash, email); + } + return this; } + public IdentityMapV3Input withPhones(List phones) { + for (String phone : phones) { + if (!InputUtil.isPhoneNumberNormalized(phone)) { + throw new IllegalArgumentException("phone number is not normalized: " + phone); + } - List getRawDiis(String identifier) { - final boolean wasInputAlreadyHashed = hashedDiiToRawDiis.isEmpty(); - if (wasInputAlreadyHashed) - return Collections.singletonList(identifier); - return hashedDiiToRawDiis.get(identifier); + String hash = InputUtil.getBase64EncodedHash(phone); + this.hashedPhones.add(new Identity(hash)); + addToDiiMappings(hash, phone); + } + return this; } - @SerializedName("email_hash") - private List hashedNormalizedEmails; - @SerializedName("phone_hash") - private List hashedNormalizedPhones; + private void addToDiiMappings(String hashedDii, String rawDii) { + diiMappings.computeIfAbsent(hashedDii, k -> new ArrayList<>()).add(rawDii); + } + + List getRawDiis(String identityType, int i) { + return diiMappings.get(getEncodedDii(identityType, i)); + } + + private String getEncodedDii(String identityType, int i) { + switch (identityType) { + case "email_hash": return hashedEmails.get(i).identity; + case "phone_hash": return hashedPhones.get(i).identity; + } + throw new Uid2Exception("Unexpected identity type: " + identityType); + } - private final transient HashMap> hashedDiiToRawDiis = new HashMap<>(); private static class Identity { @SerializedName("i") - private final String i; + private final String identity; public Identity(String value) { - this.i = value; + this.identity = value; } } } diff --git a/src/main/java/com/uid2/client/IdentityMapV3Response.java b/src/main/java/com/uid2/client/IdentityMapV3Response.java index 7d47a8a..3bc3ac7 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Response.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Response.java @@ -1,78 +1,99 @@ package com.uid2.client; import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import com.google.gson.annotations.SerializedName; -import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; public class IdentityMapV3Response { IdentityMapV3Response(String response, IdentityMapV3Input identityMapInput) { - JsonObject responseJson = new Gson().fromJson(response, JsonObject.class); - status = responseJson.get("status").getAsString(); + ApiResponse apiResponse = new Gson().fromJson(response, ApiResponse.class); + status = apiResponse.status; if (!isSuccess()) { throw new Uid2Exception("Got unexpected identity map status: " + status); } - Gson gson = new Gson(); - JsonObject body = getBodyAsJson(responseJson); - - Iterable mapped = getJsonArray(body, "mapped"); - for (JsonElement identity : mapped) { - List rawDiis = getRawDiis(identity, identityMapInput); - MappedIdentity mappedIdentity = gson.fromJson(identity, MappedIdentity.class); - for (String rawDii : rawDiis) { - mappedIdentities.put(rawDii, mappedIdentity); - } - } + populateIdentities(apiResponse.body, identityMapInput); + } - Iterable unmapped = getJsonArray(body, "unmapped"); - for (JsonElement identity : unmapped) { - List rawDiis = getRawDiis(identity, identityMapInput); - UnmappedIdentity unmappedIdentity = gson.fromJson(identity, UnmappedIdentity.class); - for (String rawDii : rawDiis) { - unmappedIdentities.put(rawDii, unmappedIdentity); - } + private void populateIdentities(Map> apiResponse, IdentityMapV3Input identityMapInput) { + for (Map.Entry> identitiesForType : apiResponse.entrySet()) { + populateIdentitiesForType(identityMapInput, identitiesForType.getKey(), identitiesForType.getValue()); } } - private static Iterable getJsonArray(JsonObject body, String header) { - JsonElement jsonElement = body.get(header); - if (jsonElement == null) { - return Collections.emptyList(); + private void populateIdentitiesForType(IdentityMapV3Input identityMapInput, String identityType, List identities) { + for (int i = 0; i < identities.size(); i++) { + ApiIdentity apiIdentity = identities.get(i); + List rawDiis = identityMapInput.getRawDiis(identityType, i); + for (String rawDii : rawDiis) + if (apiIdentity.error != null) { + unmappedIdentities.put(rawDii, new UnmappedIdentity(apiIdentity)); + } else { + mappedIdentities.put(rawDii, new MappedIdentity(apiIdentity)); + } } - return jsonElement.getAsJsonArray(); } - private List getRawDiis(JsonElement identity, IdentityMapV3Input identityMapInput) { - String identifier = identity.getAsJsonObject().get("identifier").getAsString(); - return identityMapInput.getRawDiis(identifier); + private static IdentityMapV3Input getIdentityMapInput(IdentityMapV3Input identityMapInput) { + return identityMapInput; } public boolean isSuccess() { return "success".equals(status); } - static JsonObject getBodyAsJson(JsonObject jsonResponse) { - return jsonResponse.get("body").getAsJsonObject(); + + static public class ApiResponse { + @SerializedName("status") + public String status; + + @SerializedName("body") + public Map> body; + } + + static public class ApiIdentity { + @SerializedName("u") + public String currentUid; + + @SerializedName("p") + public String previousUid; + + @SerializedName("r") + public Long refreshFromSeconds; + + @SerializedName("e") + public String error; } static public class MappedIdentity { - public MappedIdentity(String rawUid, String bucketId) { - this.rawUid = rawUid; - this.bucketId = bucketId; + public MappedIdentity(String currentUid, String previousUid, Long refreshFromSeconds) { + this.currentUid = currentUid; + this.previousUid = previousUid; + this.refreshFromSeconds = refreshFromSeconds; } - public String getRawUid() {return rawUid;} - public String getBucketId() {return bucketId;} + public MappedIdentity(ApiIdentity apiIdentity) { + this(apiIdentity.currentUid, apiIdentity.previousUid, apiIdentity.refreshFromSeconds); + } + + private final String currentUid; + private final String previousUid; + private final Long refreshFromSeconds; + + public String getCurrentUid() { + return currentUid; + } + + public String getPreviousUid() { + return previousUid; + } - @SerializedName("advertising_id") - private final String rawUid; - @SerializedName("bucket_id") - private final String bucketId; + public Long getRefreshFromSeconds() { + return refreshFromSeconds; + } } static public class UnmappedIdentity { @@ -80,9 +101,15 @@ public UnmappedIdentity(String reason) { this.reason = reason; } - public String getReason() {return reason;} + public UnmappedIdentity(ApiIdentity apiIdentity) { + this(apiIdentity.error); + } private final String reason; + + public String getReason() { + return reason; + } } public HashMap getMappedIdentities() { diff --git a/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java index b8c4abb..4a2c1f8 100644 --- a/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java +++ b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java @@ -17,45 +17,57 @@ public class IdentityMapV3IntegrationTests { final private IdentityMapV3Client identityMapClient = new IdentityMapV3Client(System.getenv("UID2_BASE_URL"), System.getenv("UID2_API_KEY"), System.getenv("UID2_SECRET_KEY")); + String mappedPhone = "+98765432109"; + String mappedPhone2 = "+12345678901"; + String optedOutPhone = "+00000000000"; + String mappedPhoneHash = InputUtil.getBase64EncodedHash(mappedPhone2); + String optedOutPhoneHash = InputUtil.getBase64EncodedHash(optedOutPhone); + + String mappedEmail = "hopefully-not-opted-out@example.com"; + String optedOutEmail = "optout@example.com"; + String optedOutEmail2 = "somethingelse@example.com"; + String mappedEmailHash = InputUtil.normalizeAndHashEmail("mapped-email@example.com"); + String optedOutEmailHash = InputUtil.normalizeAndHashEmail(optedOutEmail); + @Test public void identityMapEmails() { - IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Arrays.asList("hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com")); + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Arrays.asList(mappedEmail, optedOutEmail2, optedOutEmail)); Response response = new Response(identityMapInput); - response.assertMapped("hopefully-not-opted-out@example.com"); - response.assertMapped("somethingelse@example.com"); + response.assertMapped(mappedEmail); + response.assertMapped(optedOutEmail2); - response.assertUnmapped("optout", "optout@example.com"); + response.assertUnmapped("OPTOUT", optedOutEmail); } @Test public void identityMapNothingUnmapped() { - IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Arrays.asList("hopefully-not-opted-out@example.com", "somethingelse@example.com")); + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Arrays.asList(mappedEmail, optedOutEmail2)); Response response = new Response(identityMapInput); - response.assertMapped("hopefully-not-opted-out@example.com"); - response.assertMapped("somethingelse@example.com"); + response.assertMapped(mappedEmail); + response.assertMapped(optedOutEmail2); } @Test public void identityMapNothingMapped() { - IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Collections.singletonList("optout@example.com")); + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Collections.singletonList(optedOutEmail)); Response response = new Response(identityMapInput); - response.assertUnmapped("optout", "optout@example.com"); + response.assertUnmapped("OPTOUT", optedOutEmail); } @Test public void identityMapInvalidEmail() { assertThrows(IllegalArgumentException.class, - () -> IdentityMapV3Input.fromEmails(Arrays.asList("email@example.com", "this is not an email"))); + () -> IdentityMapV3Input.fromEmails(Arrays.asList(mappedEmail, "this is not an email"))); } @Test public void identityMapInvalidPhone() { assertThrows(IllegalArgumentException.class, - () -> IdentityMapV3Input.fromPhones(Arrays.asList("+12345678901", "this is not a phone number"))); + () -> IdentityMapV3Input.fromPhones(Arrays.asList(mappedPhone, "this is not a phone number"))); } @Test @@ -64,7 +76,7 @@ public void identityMapInvalidHashedEmail() { Response response = new Response(identityMapInput); - response.assertUnmapped("invalid identifier", "this is not a hashed email"); + response.assertUnmapped("INVALID", "this is not a hashed email"); } @Test @@ -72,14 +84,14 @@ public void identityMapInvalidHashedPhone() { IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromHashedPhones(Collections.singletonList("this is not a hashed phone")); Response response = new Response(identityMapInput); - response.assertUnmapped("invalid identifier", "this is not a hashed phone"); + response.assertUnmapped("INVALID", "this is not a hashed phone"); } @Test public void identityMapHashedEmails() { - String hashedEmail1 = InputUtil.normalizeAndHashEmail("hopefully-not-opted-out@example.com"); - String hashedEmail2 = InputUtil.normalizeAndHashEmail("somethingelse@example.com"); - String hashedOptedOutEmail = InputUtil.normalizeAndHashEmail("optout@example.com"); + String hashedEmail1 = InputUtil.normalizeAndHashEmail(mappedEmail); + String hashedEmail2 = mappedEmailHash; + String hashedOptedOutEmail = optedOutEmailHash; IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromHashedEmails(Arrays.asList(hashedEmail1, hashedEmail2, hashedOptedOutEmail)); Response response = new Response(identityMapInput); @@ -87,7 +99,7 @@ public void identityMapHashedEmails() { response.assertMapped(hashedEmail1); response.assertMapped(hashedEmail2); - response.assertUnmapped("optout", hashedOptedOutEmail); + response.assertUnmapped("OPTOUT", hashedOptedOutEmail); } @Test @@ -98,29 +110,21 @@ public void identityMapDuplicateEmails() { HashMap mappedIdentities = identityMapResponse.getMappedIdentities(); assertEquals(4, mappedIdentities.size()); //it's not 5 because the last email is an exact match to the first email - String rawUid = mappedIdentities.get("JANE.SAOIRSE@gmail.com").getRawUid(); - assertEquals(rawUid, mappedIdentities.get("Jane.Saoirse@gmail.com").getRawUid()); - assertEquals(rawUid, mappedIdentities.get("JaneSaoirse+UID2@gmail.com").getRawUid()); - assertEquals(rawUid, mappedIdentities.get("janesaoirse@gmail.com").getRawUid()); + String rawUid = mappedIdentities.get("JANE.SAOIRSE@gmail.com").getCurrentUid(); + assertEquals(rawUid, mappedIdentities.get("Jane.Saoirse@gmail.com").getCurrentUid()); + assertEquals(rawUid, mappedIdentities.get("JaneSaoirse+UID2@gmail.com").getCurrentUid()); + assertEquals(rawUid, mappedIdentities.get("janesaoirse@gmail.com").getCurrentUid()); } @Test public void identityMapDuplicateHashedEmails() { - String hashedEmail = InputUtil.normalizeAndHashEmail("hopefully-not-opted-out@example.com"); - String duplicateHashedEmail = hashedEmail; - - String hashedOptedOutEmail = InputUtil.normalizeAndHashEmail("optout@example.com"); - String duplicateOptedOutEmail = hashedOptedOutEmail; - - IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromHashedEmails(Arrays.asList(hashedEmail, duplicateHashedEmail, hashedOptedOutEmail, duplicateOptedOutEmail)); + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromHashedEmails(Arrays.asList(mappedEmailHash, mappedEmailHash, optedOutEmailHash, optedOutEmailHash)); Response response = new Response(identityMapInput); - response.assertMapped(hashedEmail); - response.assertMapped(duplicateHashedEmail); + response.assertMapped(mappedEmailHash); - response.assertUnmapped("optout", hashedOptedOutEmail); - response.assertUnmapped("optout", duplicateOptedOutEmail); + response.assertUnmapped("OPTOUT", optedOutEmailHash); } @Test @@ -134,20 +138,20 @@ public void identityMapEmptyInput() { @Test public void identityMapPhones() { - IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromPhones(Arrays.asList("+12345678901", "+98765432109", "+00000000000")); + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromPhones(Arrays.asList(mappedPhone, mappedPhone2, optedOutPhone)); Response response = new Response(identityMapInput); - response.assertMapped("+12345678901"); - response.assertMapped("+98765432109"); + response.assertMapped(mappedPhone); + response.assertMapped(mappedPhone2); - response.assertUnmapped("optout", "+00000000000"); + response.assertUnmapped("OPTOUT", optedOutPhone); } @Test public void identityMapHashedPhones() { - String hashedPhone1 = InputUtil.getBase64EncodedHash("+12345678901"); - String hashedPhone2 = InputUtil.getBase64EncodedHash("+98765432109"); - String hashedOptedOutPhone = InputUtil.getBase64EncodedHash("+00000000000"); + String hashedPhone1 = mappedPhoneHash; + String hashedPhone2 = InputUtil.getBase64EncodedHash(mappedPhone); + String hashedOptedOutPhone = optedOutPhoneHash; IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromHashedPhones(Arrays.asList(hashedPhone1, hashedPhone2, hashedOptedOutPhone)); Response response = new Response(identityMapInput); @@ -155,7 +159,28 @@ public void identityMapHashedPhones() { response.assertMapped(hashedPhone1); response.assertMapped(hashedPhone2); - response.assertUnmapped("optout", hashedOptedOutPhone); + response.assertUnmapped("OPTOUT", hashedOptedOutPhone); + } + + @Test + public void identityMapAllIdentityTypesInOneRequest() { + IdentityMapV3Input identityMapInput = IdentityMapV3Input + .fromEmails(Arrays.asList(mappedEmail, optedOutEmail)) + .withHashedEmails(Arrays.asList(mappedEmailHash, optedOutEmailHash)) + .withPhones(Arrays.asList(mappedPhone, optedOutPhone)) + .withHashedPhones(Arrays.asList(mappedPhoneHash, optedOutPhoneHash)); + + Response response = new Response(identityMapInput); + + response.assertMapped(mappedEmail); + response.assertMapped(mappedEmailHash); + response.assertMapped(mappedPhone); + response.assertMapped(mappedPhoneHash); + + response.assertUnmapped("OPTOUT", optedOutEmail); + response.assertUnmapped("OPTOUT", optedOutEmailHash); + response.assertUnmapped("OPTOUT", optedOutPhone); + response.assertUnmapped("OPTOUT", optedOutPhoneHash); } @@ -167,15 +192,16 @@ class Response { void assertMapped(String dii) { IdentityMapV3Response.MappedIdentity mappedIdentity = identityMapResponse.getMappedIdentities().get(dii); assertNotNull(mappedIdentity); - assertFalse(mappedIdentity.getRawUid().isEmpty()); - assertFalse(mappedIdentity.getBucketId().isEmpty()); + assertFalse(mappedIdentity.getCurrentUid().isEmpty()); IdentityMapV3Response.UnmappedIdentity unmappedIdentity = identityMapResponse.getUnmappedIdentities().get(dii); assertNull(unmappedIdentity); } void assertUnmapped(String reason, String dii) { - assertEquals(reason, identityMapResponse.getUnmappedIdentities().get(dii).getReason()); + HashMap unmappedIdentities = identityMapResponse.getUnmappedIdentities(); + IdentityMapV3Response.UnmappedIdentity dii2 = unmappedIdentities.get(dii); + assertEquals(reason, dii2.getReason()); IdentityMapV3Response.MappedIdentity mappedIdentity = identityMapResponse.getMappedIdentities().get(dii); assertNull(mappedIdentity); @@ -187,7 +213,7 @@ void assertUnmapped(String reason, String dii) { @Test public void identityMapEmailsUseOwnHttp() { - IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Arrays.asList("hopefully-not-opted-out@example.com", "somethingelse@example.com", "optout@example.com")); + IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Arrays.asList(mappedEmail, optedOutEmail2, optedOutEmail)); final IdentityMapV3Helper identityMapHelper = new IdentityMapV3Helper(System.getenv("UID2_SECRET_KEY")); EnvelopeV2 envelopeV2 = identityMapHelper.createEnvelopeForIdentityMapRequest(identityMapInput); @@ -195,7 +221,7 @@ public void identityMapEmailsUseOwnHttp() { String clientApiKey = System.getenv("UID2_API_KEY"); Headers headers = new Headers.Builder().add("Authorization", "Bearer " + clientApiKey).add("X-UID2-Client-Version: java-" + Uid2Helper.getArtifactAndVersion()).build(); - Request request = new Request.Builder().url(uid2BaseUrl + "/v2/identity/map").headers(headers) + Request request = new Request.Builder().url(uid2BaseUrl + "/v3/identity/map").headers(headers) .post(RequestBody.create(envelopeV2.getEnvelope(), MediaType.get("application/x-www-form-urlencoded"))).build(); OkHttpClient client = new OkHttpClient(); @@ -207,8 +233,8 @@ public void identityMapEmailsUseOwnHttp() { String responseString = response.body() != null ? response.body().string() : response.toString(); IdentityMapV3Response identityMapResponse = identityMapHelper.createIdentityMapResponse(responseString, envelopeV2, identityMapInput); - IdentityMapV3Response.MappedIdentity mappedIdentity = identityMapResponse.getMappedIdentities().get("hopefully-not-opted-out@example.com"); - assertFalse(mappedIdentity.getRawUid().isEmpty()); + IdentityMapV3Response.MappedIdentity mappedIdentity = identityMapResponse.getMappedIdentities().get(mappedEmail); + assertFalse(mappedIdentity.getCurrentUid().isEmpty()); } catch (IOException e) { throw new Uid2Exception("error communicating with api endpoint", e); } From cf6168e79e9004a906790e36bb978d6096245e89 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Mon, 2 Jun 2025 17:17:54 +0800 Subject: [PATCH 04/26] Ignoring .env --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index dfec95d..5558a8e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ dependencies/ build/** .DS_Store */node_modules/* +.env \ No newline at end of file From 3df6e3565bf73b09e7752764de4961d56a8ec33b Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Mon, 2 Jun 2025 17:35:30 +0800 Subject: [PATCH 05/26] Checking refreshFrom in IdentityMapV3 tests, should be in the future, 1 day buffer to remove flakiness --- .../java/com/uid2/client/IdentityMapV3IntegrationTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java index 4a2c1f8..ac7f1c9 100644 --- a/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java +++ b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import java.io.IOException; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -193,6 +194,7 @@ void assertMapped(String dii) { IdentityMapV3Response.MappedIdentity mappedIdentity = identityMapResponse.getMappedIdentities().get(dii); assertNotNull(mappedIdentity); assertFalse(mappedIdentity.getCurrentUid().isEmpty()); + assertTrue(mappedIdentity.getRefreshFromSeconds() > Instant.now().minusSeconds(24 * 60 * 60).getEpochSecond()); IdentityMapV3Response.UnmappedIdentity unmappedIdentity = identityMapResponse.getUnmappedIdentities().get(dii); assertNull(unmappedIdentity); From c1fb9d43b7485365c53ea6f14137412f888c4b34 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Wed, 4 Jun 2025 14:48:47 +0800 Subject: [PATCH 06/26] Empty constructor pulbic and with methods for one dii are available --- .../com/uid2/client/IdentityMapV3Input.java | 49 +++++++++++++------ .../uid2/client/IdentityMapV3Response.java | 3 +- .../client/IdentityMapV3IntegrationTests.java | 20 +++++++- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/uid2/client/IdentityMapV3Input.java b/src/main/java/com/uid2/client/IdentityMapV3Input.java index b8730d9..9499a17 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Input.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Input.java @@ -45,44 +45,65 @@ public static IdentityMapV3Input fromHashedPhones(List hashedPhones) { @SerializedName("phone_hash") private final List hashedPhones = new ArrayList<>(); - private IdentityMapV3Input() {} + public IdentityMapV3Input() {} public IdentityMapV3Input withHashedEmails(List hashedEmails) { for (String hashedEmail : hashedEmails) { - this.hashedEmails.add(new Identity(hashedEmail)); - addToDiiMappings(hashedEmail, hashedEmail); + withHashedEmail(hashedEmail); } return this; } + public IdentityMapV3Input withHashedEmail(String hashedEmail) { + this.hashedEmails.add(new Identity(hashedEmail)); + addToDiiMappings(hashedEmail, hashedEmail); + return this; + } + public IdentityMapV3Input withHashedPhones(List hashedPhones) { for (String hashedPhone : hashedPhones) { - this.hashedPhones.add(new Identity(hashedPhone)); - addToDiiMappings(hashedPhone, hashedPhone); + withHashedPhone(hashedPhone); } return this; } + public IdentityMapV3Input withHashedPhone(String hashedPhone) { + this.hashedPhones.add(new Identity(hashedPhone)); + addToDiiMappings(hashedPhone, hashedPhone); + return this; + } + public IdentityMapV3Input withEmails(List emails) { for (String email : emails) { - String hash = InputUtil.normalizeAndHashEmail(email); - this.hashedEmails.add(new Identity(hash)); - addToDiiMappings(hash, email); + withEmail(email); } return this; } + public IdentityMapV3Input withEmail(String email) { + String hashedEmail = InputUtil.normalizeAndHashEmail(email); + this.hashedEmails.add(new Identity(hashedEmail)); + addToDiiMappings(hashedEmail, email); + return this; + } + public IdentityMapV3Input withPhones(List phones) { for (String phone : phones) { - if (!InputUtil.isPhoneNumberNormalized(phone)) { - throw new IllegalArgumentException("phone number is not normalized: " + phone); - } + withPhone(phone); + } + return this; + } - String hash = InputUtil.getBase64EncodedHash(phone); - this.hashedPhones.add(new Identity(hash)); - addToDiiMappings(hash, phone); + public IdentityMapV3Input withPhone(String phone) { + if (!InputUtil.isPhoneNumberNormalized(phone)) { + throw new IllegalArgumentException("phone number is not normalized: " + phone); } + + String hashedPhone = InputUtil.getBase64EncodedHash(phone); + this.hashedPhones.add(new Identity(hashedPhone)); + addToDiiMappings(hashedPhone, phone); return this; + } private void addToDiiMappings(String hashedDii, String rawDii) { diff --git a/src/main/java/com/uid2/client/IdentityMapV3Response.java b/src/main/java/com/uid2/client/IdentityMapV3Response.java index 3bc3ac7..d1fb853 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Response.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Response.java @@ -29,12 +29,13 @@ private void populateIdentitiesForType(IdentityMapV3Input identityMapInput, Stri for (int i = 0; i < identities.size(); i++) { ApiIdentity apiIdentity = identities.get(i); List rawDiis = identityMapInput.getRawDiis(identityType, i); - for (String rawDii : rawDiis) + for (String rawDii : rawDiis) { if (apiIdentity.error != null) { unmappedIdentities.put(rawDii, new UnmappedIdentity(apiIdentity)); } else { mappedIdentities.put(rawDii, new MappedIdentity(apiIdentity)); } + } } } diff --git a/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java index ac7f1c9..5bac11b 100644 --- a/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java +++ b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java @@ -170,7 +170,7 @@ public void identityMapAllIdentityTypesInOneRequest() { .withHashedEmails(Arrays.asList(mappedEmailHash, optedOutEmailHash)) .withPhones(Arrays.asList(mappedPhone, optedOutPhone)) .withHashedPhones(Arrays.asList(mappedPhoneHash, optedOutPhoneHash)); - + Response response = new Response(identityMapInput); response.assertMapped(mappedEmail); @@ -184,6 +184,24 @@ public void identityMapAllIdentityTypesInOneRequest() { response.assertUnmapped("OPTOUT", optedOutPhoneHash); } + @Test + public void identityMapAllIdentityTypesInOneRequestAddedOneByOne() { + IdentityMapV3Input identityMapInput = new IdentityMapV3Input(); + + identityMapInput.withEmail(mappedEmail); + identityMapInput.withPhone(optedOutPhone); + identityMapInput.withHashedPhone(mappedPhoneHash); + identityMapInput.withHashedEmail(optedOutEmailHash); + + Response response = new Response(identityMapInput); + + response.assertMapped(mappedEmail); + response.assertMapped(mappedPhoneHash); + + response.assertUnmapped("OPTOUT", optedOutPhone); + response.assertUnmapped("OPTOUT", optedOutEmailHash); + } + class Response { Response(IdentityMapV3Input identityMapInput) { From c3ef65d5004571cf257038aaaecb2f8ca99b5ea1 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Wed, 4 Jun 2025 15:01:34 +0800 Subject: [PATCH 07/26] Javadocs --- .../com/uid2/client/IdentityMapV3Input.java | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/uid2/client/IdentityMapV3Input.java b/src/main/java/com/uid2/client/IdentityMapV3Input.java index 9499a17..f9bd991 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Input.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Input.java @@ -7,7 +7,7 @@ public class IdentityMapV3Input { /** * @param emails a list of normalized or unnormalized email addresses - * @return a IdentityMapV3Input instance, to be used in {@link IdentityMapHelper#createEnvelopeForIdentityMapRequest} + * @return a IdentityMapV3Input instance, to be used in {@link IdentityMapV3Helper#createEnvelopeForIdentityMapRequest} */ public static IdentityMapV3Input fromEmails(List emails) { return new IdentityMapV3Input().withEmails(emails); @@ -47,6 +47,10 @@ public static IdentityMapV3Input fromHashedPhones(List hashedPhones) { public IdentityMapV3Input() {} + /** + * @param hashedEmails a normalized and hashed email address + * @return this IdentityMapV3Input instance + */ public IdentityMapV3Input withHashedEmails(List hashedEmails) { for (String hashedEmail : hashedEmails) { withHashedEmail(hashedEmail); @@ -54,12 +58,20 @@ public IdentityMapV3Input withHashedEmails(List hashedEmails) { return this; } + /** + * @param hashedEmail a normalized and hashed email address + * @return this IdentityMapV3Input instance + */ public IdentityMapV3Input withHashedEmail(String hashedEmail) { this.hashedEmails.add(new Identity(hashedEmail)); addToDiiMappings(hashedEmail, hashedEmail); return this; } + /** + * @param hashedPhones a normalized and hashed phone number + * @return this IdentityMapV3Input instance + */ public IdentityMapV3Input withHashedPhones(List hashedPhones) { for (String hashedPhone : hashedPhones) { withHashedPhone(hashedPhone); @@ -67,12 +79,20 @@ public IdentityMapV3Input withHashedPhones(List hashedPhones) { return this; } + /** + * @param hashedPhone a normalized and hashed phone number + * @return this IdentityMapV3Input instance + */ public IdentityMapV3Input withHashedPhone(String hashedPhone) { this.hashedPhones.add(new Identity(hashedPhone)); addToDiiMappings(hashedPhone, hashedPhone); return this; } + /** + * @param emails a list of normalized or unnormalized email addresses + * @return this IdentityMapV3Input instance + */ public IdentityMapV3Input withEmails(List emails) { for (String email : emails) { withEmail(email); @@ -80,6 +100,10 @@ public IdentityMapV3Input withEmails(List emails) { return this; } + /** + * @param email a normalized or unnormalized email address + * @return this IdentityMapV3Input instance + */ public IdentityMapV3Input withEmail(String email) { String hashedEmail = InputUtil.normalizeAndHashEmail(email); this.hashedEmails.add(new Identity(hashedEmail)); @@ -87,6 +111,10 @@ public IdentityMapV3Input withEmail(String email) { return this; } + /** + * @param phones a normalized phone number + * @return this IdentityMapV3Input instance + */ public IdentityMapV3Input withPhones(List phones) { for (String phone : phones) { withPhone(phone); @@ -94,6 +122,10 @@ public IdentityMapV3Input withPhones(List phones) { return this; } + /** + * @param phone a normalized phone number + * @return this IdentityMapV3Input instance + */ public IdentityMapV3Input withPhone(String phone) { if (!InputUtil.isPhoneNumberNormalized(phone)) { throw new IllegalArgumentException("phone number is not normalized: " + phone); @@ -106,14 +138,14 @@ public IdentityMapV3Input withPhone(String phone) { } - private void addToDiiMappings(String hashedDii, String rawDii) { - diiMappings.computeIfAbsent(hashedDii, k -> new ArrayList<>()).add(rawDii); - } - List getRawDiis(String identityType, int i) { return diiMappings.get(getEncodedDii(identityType, i)); } + private void addToDiiMappings(String hashedDii, String rawDii) { + diiMappings.computeIfAbsent(hashedDii, k -> new ArrayList<>()).add(rawDii); + } + private String getEncodedDii(String identityType, int i) { switch (identityType) { case "email_hash": return hashedEmails.get(i).identity; From 18cfc01be34453d70265d41d9384ddb6384640d6 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Fri, 6 Jun 2025 17:20:37 +0800 Subject: [PATCH 08/26] Addressing feedback --- .../com/uid2/client/IdentityMapV3Response.java | 17 +++++++++-------- .../client/IdentityMapV3IntegrationTests.java | 17 ++++++++++------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/uid2/client/IdentityMapV3Response.java b/src/main/java/com/uid2/client/IdentityMapV3Response.java index d1fb853..f624b77 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Response.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Response.java @@ -3,6 +3,7 @@ import com.google.gson.Gson; import com.google.gson.annotations.SerializedName; +import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -70,30 +71,30 @@ static public class ApiIdentity { } static public class MappedIdentity { - public MappedIdentity(String currentUid, String previousUid, Long refreshFromSeconds) { + public MappedIdentity(String currentUid, String previousUid, Instant refreshFrom) { this.currentUid = currentUid; this.previousUid = previousUid; - this.refreshFromSeconds = refreshFromSeconds; + this.refreshFrom = refreshFrom; } public MappedIdentity(ApiIdentity apiIdentity) { - this(apiIdentity.currentUid, apiIdentity.previousUid, apiIdentity.refreshFromSeconds); + this(apiIdentity.currentUid, apiIdentity.previousUid, Instant.ofEpochSecond(apiIdentity.refreshFromSeconds)); } private final String currentUid; private final String previousUid; - private final Long refreshFromSeconds; + private final Instant refreshFrom; - public String getCurrentUid() { + public String getCurrentRawUid() { return currentUid; } - public String getPreviousUid() { + public String getPreviousRawUid() { return previousUid; } - public Long getRefreshFromSeconds() { - return refreshFromSeconds; + public Instant getRefreshFrom() { + return refreshFrom; } } diff --git a/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java index 5bac11b..2aa40e7 100644 --- a/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java +++ b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java @@ -111,10 +111,10 @@ public void identityMapDuplicateEmails() { HashMap mappedIdentities = identityMapResponse.getMappedIdentities(); assertEquals(4, mappedIdentities.size()); //it's not 5 because the last email is an exact match to the first email - String rawUid = mappedIdentities.get("JANE.SAOIRSE@gmail.com").getCurrentUid(); - assertEquals(rawUid, mappedIdentities.get("Jane.Saoirse@gmail.com").getCurrentUid()); - assertEquals(rawUid, mappedIdentities.get("JaneSaoirse+UID2@gmail.com").getCurrentUid()); - assertEquals(rawUid, mappedIdentities.get("janesaoirse@gmail.com").getCurrentUid()); + String rawUid = mappedIdentities.get("JANE.SAOIRSE@gmail.com").getCurrentRawUid(); + assertEquals(rawUid, mappedIdentities.get("Jane.Saoirse@gmail.com").getCurrentRawUid()); + assertEquals(rawUid, mappedIdentities.get("JaneSaoirse+UID2@gmail.com").getCurrentRawUid()); + assertEquals(rawUid, mappedIdentities.get("janesaoirse@gmail.com").getCurrentRawUid()); } @@ -211,8 +211,11 @@ class Response { void assertMapped(String dii) { IdentityMapV3Response.MappedIdentity mappedIdentity = identityMapResponse.getMappedIdentities().get(dii); assertNotNull(mappedIdentity); - assertFalse(mappedIdentity.getCurrentUid().isEmpty()); - assertTrue(mappedIdentity.getRefreshFromSeconds() > Instant.now().minusSeconds(24 * 60 * 60).getEpochSecond()); + assertFalse(mappedIdentity.getCurrentRawUid().isEmpty()); + + // Refresh from should be now or in the future, allow some slack for time between request and this assertion + Instant aMinuteAgo = Instant.now().minusSeconds(60); + assertTrue(mappedIdentity.getRefreshFrom().isAfter(aMinuteAgo)); IdentityMapV3Response.UnmappedIdentity unmappedIdentity = identityMapResponse.getUnmappedIdentities().get(dii); assertNull(unmappedIdentity); @@ -254,7 +257,7 @@ public void identityMapEmailsUseOwnHttp() { IdentityMapV3Response identityMapResponse = identityMapHelper.createIdentityMapResponse(responseString, envelopeV2, identityMapInput); IdentityMapV3Response.MappedIdentity mappedIdentity = identityMapResponse.getMappedIdentities().get(mappedEmail); - assertFalse(mappedIdentity.getCurrentUid().isEmpty()); + assertFalse(mappedIdentity.getCurrentRawUid().isEmpty()); } catch (IOException e) { throw new Uid2Exception("error communicating with api endpoint", e); } From ee0782a27dc530040cf93bfff3c34c271c3c19fe Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Fri, 6 Jun 2025 17:35:03 +0800 Subject: [PATCH 09/26] More feedback --- src/main/java/com/uid2/client/IdentityMapV3Input.java | 2 +- src/main/java/com/uid2/client/IdentityMapV3Response.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/uid2/client/IdentityMapV3Input.java b/src/main/java/com/uid2/client/IdentityMapV3Input.java index f9bd991..8eaf22c 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Input.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Input.java @@ -138,7 +138,7 @@ public IdentityMapV3Input withPhone(String phone) { } - List getRawDiis(String identityType, int i) { + List getInputDiis(String identityType, int i) { return diiMappings.get(getEncodedDii(identityType, i)); } diff --git a/src/main/java/com/uid2/client/IdentityMapV3Response.java b/src/main/java/com/uid2/client/IdentityMapV3Response.java index f624b77..03be0f9 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Response.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Response.java @@ -29,12 +29,12 @@ private void populateIdentities(Map> apiResponse, Iden private void populateIdentitiesForType(IdentityMapV3Input identityMapInput, String identityType, List identities) { for (int i = 0; i < identities.size(); i++) { ApiIdentity apiIdentity = identities.get(i); - List rawDiis = identityMapInput.getRawDiis(identityType, i); - for (String rawDii : rawDiis) { + List inputDiis = identityMapInput.getInputDiis(identityType, i); + for (String inputDii : inputDiis) { if (apiIdentity.error != null) { - unmappedIdentities.put(rawDii, new UnmappedIdentity(apiIdentity)); + unmappedIdentities.put(inputDii, new UnmappedIdentity(apiIdentity)); } else { - mappedIdentities.put(rawDii, new MappedIdentity(apiIdentity)); + mappedIdentities.put(inputDii, new MappedIdentity(apiIdentity)); } } } From 7b3ae88f33745d13ab3406dbb6a9afdc329243f4 Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Tue, 10 Jun 2025 01:44:57 +0000 Subject: [PATCH 10/26] [CI Pipeline] Released Snapshot version: 4.6.1-alpha-17-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c10b9b8..507c579 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-client - 4.6.0 + 4.6.1-alpha-17-SNAPSHOT ${project.groupId}:${project.artifactId} UID2 Client From b8163ce0692318b2a86555731416b7551a304197 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Wed, 11 Jun 2025 15:40:22 +0800 Subject: [PATCH 11/26] Using enum for unmapped operations instead of strings --- .../uid2/client/IdentityMapV3Response.java | 26 ++++--- .../uid2/client/UnmappedIdentityReason.java | 18 +++++ .../client/IdentityMapV3IntegrationTests.java | 38 +++++----- .../com/uid2/client/IdentityMapV3Tests.java | 75 +++++++++++++++++++ 4 files changed, 127 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/uid2/client/UnmappedIdentityReason.java create mode 100644 src/test/java/com/uid2/client/IdentityMapV3Tests.java diff --git a/src/main/java/com/uid2/client/IdentityMapV3Response.java b/src/main/java/com/uid2/client/IdentityMapV3Response.java index 03be0f9..da511fe 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Response.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Response.java @@ -31,10 +31,10 @@ private void populateIdentitiesForType(IdentityMapV3Input identityMapInput, Stri ApiIdentity apiIdentity = identities.get(i); List inputDiis = identityMapInput.getInputDiis(identityType, i); for (String inputDii : inputDiis) { - if (apiIdentity.error != null) { - unmappedIdentities.put(inputDii, new UnmappedIdentity(apiIdentity)); - } else { + if (apiIdentity.error == null) { mappedIdentities.put(inputDii, new MappedIdentity(apiIdentity)); + } else { + unmappedIdentities.put(inputDii, new UnmappedIdentity(apiIdentity.error)); } } } @@ -99,19 +99,23 @@ public Instant getRefreshFrom() { } static public class UnmappedIdentity { - public UnmappedIdentity(String reason) { - this.reason = reason; + public UnmappedIdentity(String reason) + { + this.reason = UnmappedIdentityReason.fromString(reason); + this.rawReason = reason; } - public UnmappedIdentity(ApiIdentity apiIdentity) { - this(apiIdentity.error); + public UnmappedIdentityReason getReason() { + return reason; } - private final String reason; - - public String getReason() { - return reason; + public String getRawReason() { + return rawReason; } + + private final UnmappedIdentityReason reason; + + private final String rawReason; } public HashMap getMappedIdentities() { diff --git a/src/main/java/com/uid2/client/UnmappedIdentityReason.java b/src/main/java/com/uid2/client/UnmappedIdentityReason.java new file mode 100644 index 0000000..2bc6b1e --- /dev/null +++ b/src/main/java/com/uid2/client/UnmappedIdentityReason.java @@ -0,0 +1,18 @@ +package com.uid2.client; + + +public enum UnmappedIdentityReason { + OPTOUT, + INVALID, + UNKNOWN; + + public static UnmappedIdentityReason fromString(String reason) { + for (UnmappedIdentityReason knownReason : values()) { + if (knownReason.name().equals(reason.toUpperCase())) { + return knownReason; + } + } + + return UNKNOWN; + } +} \ No newline at end of file diff --git a/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java index 2aa40e7..e047a93 100644 --- a/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java +++ b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java @@ -38,7 +38,7 @@ public void identityMapEmails() { response.assertMapped(mappedEmail); response.assertMapped(optedOutEmail2); - response.assertUnmapped("OPTOUT", optedOutEmail); + response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutEmail); } @Test @@ -55,7 +55,7 @@ public void identityMapNothingMapped() { IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Collections.singletonList(optedOutEmail)); Response response = new Response(identityMapInput); - response.assertUnmapped("OPTOUT", optedOutEmail); + response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutEmail); } @@ -77,7 +77,7 @@ public void identityMapInvalidHashedEmail() { Response response = new Response(identityMapInput); - response.assertUnmapped("INVALID", "this is not a hashed email"); + response.assertUnmapped(UnmappedIdentityReason.INVALID, "this is not a hashed email"); } @Test @@ -85,7 +85,7 @@ public void identityMapInvalidHashedPhone() { IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromHashedPhones(Collections.singletonList("this is not a hashed phone")); Response response = new Response(identityMapInput); - response.assertUnmapped("INVALID", "this is not a hashed phone"); + response.assertUnmapped(UnmappedIdentityReason.INVALID, "this is not a hashed phone"); } @Test @@ -100,7 +100,7 @@ public void identityMapHashedEmails() { response.assertMapped(hashedEmail1); response.assertMapped(hashedEmail2); - response.assertUnmapped("OPTOUT", hashedOptedOutEmail); + response.assertUnmapped(UnmappedIdentityReason.OPTOUT, hashedOptedOutEmail); } @Test @@ -125,7 +125,7 @@ public void identityMapDuplicateHashedEmails() { response.assertMapped(mappedEmailHash); - response.assertUnmapped("OPTOUT", optedOutEmailHash); + response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutEmailHash); } @Test @@ -145,7 +145,7 @@ public void identityMapPhones() { response.assertMapped(mappedPhone); response.assertMapped(mappedPhone2); - response.assertUnmapped("OPTOUT", optedOutPhone); + response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutPhone); } @Test @@ -160,7 +160,7 @@ public void identityMapHashedPhones() { response.assertMapped(hashedPhone1); response.assertMapped(hashedPhone2); - response.assertUnmapped("OPTOUT", hashedOptedOutPhone); + response.assertUnmapped(UnmappedIdentityReason.OPTOUT, hashedOptedOutPhone); } @Test @@ -178,10 +178,10 @@ public void identityMapAllIdentityTypesInOneRequest() { response.assertMapped(mappedPhone); response.assertMapped(mappedPhoneHash); - response.assertUnmapped("OPTOUT", optedOutEmail); - response.assertUnmapped("OPTOUT", optedOutEmailHash); - response.assertUnmapped("OPTOUT", optedOutPhone); - response.assertUnmapped("OPTOUT", optedOutPhoneHash); + response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutEmail); + response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutEmailHash); + response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutPhone); + response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutPhoneHash); } @Test @@ -198,8 +198,8 @@ public void identityMapAllIdentityTypesInOneRequestAddedOneByOne() { response.assertMapped(mappedEmail); response.assertMapped(mappedPhoneHash); - response.assertUnmapped("OPTOUT", optedOutPhone); - response.assertUnmapped("OPTOUT", optedOutEmailHash); + response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutPhone); + response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutEmailHash); } @@ -217,14 +217,14 @@ void assertMapped(String dii) { Instant aMinuteAgo = Instant.now().minusSeconds(60); assertTrue(mappedIdentity.getRefreshFrom().isAfter(aMinuteAgo)); - IdentityMapV3Response.UnmappedIdentity unmappedIdentity = identityMapResponse.getUnmappedIdentities().get(dii); - assertNull(unmappedIdentity); + IdentityMapV3Response.UnmappedIdentity unmappedIdentityReason = identityMapResponse.getUnmappedIdentities().get(dii); + assertNull(unmappedIdentityReason); } - void assertUnmapped(String reason, String dii) { + void assertUnmapped(UnmappedIdentityReason reason, String dii) { HashMap unmappedIdentities = identityMapResponse.getUnmappedIdentities(); - IdentityMapV3Response.UnmappedIdentity dii2 = unmappedIdentities.get(dii); - assertEquals(reason, dii2.getReason()); + IdentityMapV3Response.UnmappedIdentity unmappedIdentity = unmappedIdentities.get(dii); + assertEquals(reason, unmappedIdentity.getReason()); IdentityMapV3Response.MappedIdentity mappedIdentity = identityMapResponse.getMappedIdentities().get(dii); assertNull(mappedIdentity); diff --git a/src/test/java/com/uid2/client/IdentityMapV3Tests.java b/src/test/java/com/uid2/client/IdentityMapV3Tests.java new file mode 100644 index 0000000..e0dd39f --- /dev/null +++ b/src/test/java/com/uid2/client/IdentityMapV3Tests.java @@ -0,0 +1,75 @@ +package com.uid2.client; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class IdentityMapV3Tests { + private final static String SOME_EMAIL = "test@example.com"; + + @Test + void identityMapV3UnmappedIdentityReasonUnknown() { + IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); + + IdentityMapV3Response response = new IdentityMapV3Response(payloadJson("SOME_NEW_UNMAPPED_REASON"), input); + + IdentityMapV3Response.UnmappedIdentity unmappedIdentity = response.getUnmappedIdentities().get(SOME_EMAIL); + assertEquals(UnmappedIdentityReason.UNKNOWN, unmappedIdentity.getReason()); + assertEquals("SOME_NEW_UNMAPPED_REASON", unmappedIdentity.getRawReason()); + } + + @Test + void identityMapV3UnmappedIdentityReasonOptout() { + IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); + + IdentityMapV3Response response = new IdentityMapV3Response(payloadJson("OPTOUT"), input); + + IdentityMapV3Response.UnmappedIdentity unmappedIdentity = response.getUnmappedIdentities().get(SOME_EMAIL); + assertEquals(UnmappedIdentityReason.OPTOUT, unmappedIdentity.getReason()); + assertEquals("OPTOUT", unmappedIdentity.getRawReason()); + } + + @Test + void identityMapV3UnmappedIdentityReasonInvalid() { + IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); + + IdentityMapV3Response response = new IdentityMapV3Response(payloadJson("INVALID"), input); + + IdentityMapV3Response.UnmappedIdentity unmappedIdentity = response.getUnmappedIdentities().get(SOME_EMAIL); + assertEquals(UnmappedIdentityReason.INVALID, unmappedIdentity.getReason()); + assertEquals("INVALID", unmappedIdentity.getRawReason()); + } + + @Test + void identityMapV3UnmappedIdentityReasonCaseInsensitive() { + IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); + + IdentityMapV3Response.UnmappedIdentity lowercaseOptout = + new IdentityMapV3Response(payloadJson("optout"), input).getUnmappedIdentities().get(SOME_EMAIL); + assertEquals(UnmappedIdentityReason.OPTOUT, lowercaseOptout.getReason()); + assertEquals("optout", lowercaseOptout.getRawReason()); + + IdentityMapV3Response.UnmappedIdentity lowercaseInvalid = + new IdentityMapV3Response(payloadJson("invalid"), input).getUnmappedIdentities().get(SOME_EMAIL); + assertEquals(UnmappedIdentityReason.INVALID, lowercaseInvalid.getReason()); + assertEquals("invalid", lowercaseInvalid.getRawReason()); + + IdentityMapV3Response.UnmappedIdentity mixedOptout = + new IdentityMapV3Response(payloadJson("OptOut"), input).getUnmappedIdentities().get(SOME_EMAIL); + assertEquals(UnmappedIdentityReason.OPTOUT, mixedOptout.getReason()); + assertEquals("OptOut", mixedOptout.getRawReason()); + + IdentityMapV3Response.UnmappedIdentity mixedInvalid = + new IdentityMapV3Response(payloadJson("InVaLiD"), input).getUnmappedIdentities().get(SOME_EMAIL); + assertEquals(UnmappedIdentityReason.INVALID, mixedInvalid.getReason()); + assertEquals("InVaLiD", mixedInvalid.getRawReason()); + } + + @NotNull + private static String payloadJson(String reason) { + return "{\"status\":\"success\",\"body\":{\"email_hash\":[{\"e\":\"" + reason + "\"}]}}"; + } +} \ No newline at end of file From 9a974ff1ae433a82f68f8131c40fb262e39c1e89 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Wed, 11 Jun 2025 16:52:40 +0800 Subject: [PATCH 12/26] Address Matt's feedback --- .../com/uid2/client/IdentityMapV3Helper.java | 2 +- .../java/com/uid2/client/IdentityMapV3Input.java | 9 +++++---- .../com/uid2/client/IdentityMapV3Response.java | 16 ++++++---------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/uid2/client/IdentityMapV3Helper.java b/src/main/java/com/uid2/client/IdentityMapV3Helper.java index 8b89bee..d324de5 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Helper.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Helper.java @@ -31,5 +31,5 @@ public IdentityMapV3Response createIdentityMapResponse(String responseString, En return new IdentityMapV3Response(decryptedResponseString, identityMapInput); } - Uid2Helper uid2Helper; + private final Uid2Helper uid2Helper; } diff --git a/src/main/java/com/uid2/client/IdentityMapV3Input.java b/src/main/java/com/uid2/client/IdentityMapV3Input.java index 8eaf22c..229abad 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Input.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Input.java @@ -37,7 +37,8 @@ public static IdentityMapV3Input fromHashedPhones(List hashedPhones) { return new IdentityMapV3Input().withHashedPhones(hashedPhones); } - private transient final Map> diiMappings = new HashMap<>(); + // Transient as this should not be part of the serialized JSON payload we send to UID2 Operator + private transient final Map> hashedDiiToRawDii = new HashMap<>(); @SerializedName("email_hash") private final List hashedEmails = new ArrayList<>(); @@ -139,14 +140,14 @@ public IdentityMapV3Input withPhone(String phone) { } List getInputDiis(String identityType, int i) { - return diiMappings.get(getEncodedDii(identityType, i)); + return hashedDiiToRawDii.get(getHashedDii(identityType, i)); } private void addToDiiMappings(String hashedDii, String rawDii) { - diiMappings.computeIfAbsent(hashedDii, k -> new ArrayList<>()).add(rawDii); + hashedDiiToRawDii.computeIfAbsent(hashedDii, k -> new ArrayList<>()).add(rawDii); } - private String getEncodedDii(String identityType, int i) { + private String getHashedDii(String identityType, int i) { switch (identityType) { case "email_hash": return hashedEmails.get(i).identity; case "phone_hash": return hashedPhones.get(i).identity; diff --git a/src/main/java/com/uid2/client/IdentityMapV3Response.java b/src/main/java/com/uid2/client/IdentityMapV3Response.java index da511fe..dc5f611 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Response.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Response.java @@ -40,15 +40,11 @@ private void populateIdentitiesForType(IdentityMapV3Input identityMapInput, Stri } } - private static IdentityMapV3Input getIdentityMapInput(IdentityMapV3Input identityMapInput) { - return identityMapInput; - } - public boolean isSuccess() { return "success".equals(status); } - static public class ApiResponse { + public static class ApiResponse { @SerializedName("status") public String status; @@ -56,7 +52,7 @@ static public class ApiResponse { public Map> body; } - static public class ApiIdentity { + public static class ApiIdentity { @SerializedName("u") public String currentUid; @@ -70,7 +66,7 @@ static public class ApiIdentity { public String error; } - static public class MappedIdentity { + public static class MappedIdentity { public MappedIdentity(String currentUid, String previousUid, Instant refreshFrom) { this.currentUid = currentUid; this.previousUid = previousUid; @@ -98,7 +94,7 @@ public Instant getRefreshFrom() { } } - static public class UnmappedIdentity { + public static class UnmappedIdentity { public UnmappedIdentity(String reason) { this.reason = UnmappedIdentityReason.fromString(reason); @@ -119,11 +115,11 @@ public String getRawReason() { } public HashMap getMappedIdentities() { - return mappedIdentities; + return new HashMap<>(mappedIdentities); } public HashMap getUnmappedIdentities() { - return unmappedIdentities; + return new HashMap<>(unmappedIdentities); } private final String status; From df2c22a3e57035dd91ddbd634fbbe68dfd0c861d Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Wed, 11 Jun 2025 17:22:15 +0800 Subject: [PATCH 13/26] Improving unit test coverage for V3 Identity Map --- .../client/IdentityMapV3ResponseTest.java | 195 ++++++++++++++++++ .../com/uid2/client/IdentityMapV3Tests.java | 75 ------- 2 files changed, 195 insertions(+), 75 deletions(-) create mode 100644 src/test/java/com/uid2/client/IdentityMapV3ResponseTest.java delete mode 100644 src/test/java/com/uid2/client/IdentityMapV3Tests.java diff --git a/src/test/java/com/uid2/client/IdentityMapV3ResponseTest.java b/src/test/java/com/uid2/client/IdentityMapV3ResponseTest.java new file mode 100644 index 0000000..c037fd9 --- /dev/null +++ b/src/test/java/com/uid2/client/IdentityMapV3ResponseTest.java @@ -0,0 +1,195 @@ +package com.uid2.client; + +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +public class IdentityMapV3ResponseTest { + private final static String SOME_EMAIL = "email1@example.com"; + + @Test + void mappedIdentity() { + String email1 = "email1@example.com"; + String email2 = "email2@example.com"; + String phone1 = "+1234567890"; + String phone2 = "+0987654321"; + String hashedEmail1 = "email 1 hash"; + String hashedEmail2 = "email 2 hash"; + String hashedPhone1 = "phone 1 hash"; + String hashedPhone2 = "phone 2 hash"; + + Instant email1RefreshFrom = Instant.parse("2025-01-01T00:00:01Z"); + Instant email2RefreshFrom = Instant.parse("2025-06-30T00:00:20Z"); + Instant phone1RefreshFrom = Instant.parse("2025-01-01T00:05:00Z"); + Instant phone2RefreshFrom = Instant.parse("2025-06-30T00:00:22Z"); + Instant hashedEmail1RefreshFrom = Instant.parse("2025-01-01T00:00:33Z"); + Instant hashedEmail2RefreshFrom = Instant.parse("2025-06-30T00:00:00Z"); + Instant hashedPhone1RefreshFrom = Instant.parse("2025-01-01T00:00:11Z"); + Instant hashedPhone2RefreshFrom = Instant.parse("2025-06-30T00:00:01Z"); + + // Response from Operator + String[] emailHashEntries = { + mappedResponsePayloadEntry("email 1 current uid", "email 1 previous uid", email1RefreshFrom), + mappedResponsePayloadEntry("email 2 current uid", "email 2 previous uid", email2RefreshFrom), + mappedResponsePayloadEntry("hashed email 1 current uid", "hashed email 1 previous uid", hashedEmail1RefreshFrom), + mappedResponsePayloadEntry("hashed email 2 current uid", "hashed email 2 previous uid", hashedEmail2RefreshFrom) + }; + + String[] phoneHashEntries = { + mappedResponsePayloadEntry("phone 1 current uid", "phone 1 previous uid", phone1RefreshFrom), + mappedResponsePayloadEntry("phone 2 current uid", "phone 2 previous uid", phone2RefreshFrom), + mappedResponsePayloadEntry("hashed phone 1 current uid", "hashed phone 1 previous uid", hashedPhone1RefreshFrom), + mappedResponsePayloadEntry("hashed phone 2 current uid", "hashed phone 2 previous uid", hashedPhone2RefreshFrom) + }; + + String responsePayload = mappedResponsePayload(emailHashEntries, phoneHashEntries); + + IdentityMapV3Input input = new IdentityMapV3Input() + .withEmails(Arrays.asList(email1, email2)) + .withHashedEmails(Arrays.asList(hashedEmail1, hashedEmail2)) + .withPhones(Arrays.asList(phone1, phone2)) + .withHashedPhones(Arrays.asList(hashedPhone1, hashedPhone2)); + + IdentityMapV3Response response = new IdentityMapV3Response(responsePayload, input); + + assertTrue(response.isSuccess()); + assertEquals(8, response.getMappedIdentities().size()); + assertEquals(0, response.getUnmappedIdentities().size()); + + // Email + IdentityMapV3Response.MappedIdentity rawEmailMapping1 = response.getMappedIdentities().get(email1); + assertEquals("email 1 current uid", rawEmailMapping1.getCurrentRawUid()); + assertEquals("email 1 previous uid", rawEmailMapping1.getPreviousRawUid()); + assertEquals(email1RefreshFrom, rawEmailMapping1.getRefreshFrom()); + + IdentityMapV3Response.MappedIdentity rawEmailMapping2 = response.getMappedIdentities().get(email2); + assertEquals("email 2 current uid", rawEmailMapping2.getCurrentRawUid()); + assertEquals("email 2 previous uid", rawEmailMapping2.getPreviousRawUid()); + assertEquals(email2RefreshFrom, rawEmailMapping2.getRefreshFrom()); + + // Phone + IdentityMapV3Response.MappedIdentity rawPhoneMapping1 = response.getMappedIdentities().get(phone1); + assertEquals("phone 1 current uid", rawPhoneMapping1.getCurrentRawUid()); + assertEquals("phone 1 previous uid", rawPhoneMapping1.getPreviousRawUid()); + assertEquals(phone1RefreshFrom, rawPhoneMapping1.getRefreshFrom()); + + IdentityMapV3Response.MappedIdentity rawPhoneMapping2 = response.getMappedIdentities().get(phone2); + assertEquals("phone 2 current uid", rawPhoneMapping2.getCurrentRawUid()); + assertEquals("phone 2 previous uid", rawPhoneMapping2.getPreviousRawUid()); + assertEquals(phone2RefreshFrom, rawPhoneMapping2.getRefreshFrom()); + + // Hashed Email + IdentityMapV3Response.MappedIdentity hashedEmailMapping1 = response.getMappedIdentities().get(hashedEmail1); + assertEquals("hashed email 1 current uid", hashedEmailMapping1.getCurrentRawUid()); + assertEquals("hashed email 1 previous uid", hashedEmailMapping1.getPreviousRawUid()); + assertEquals(hashedEmail1RefreshFrom, hashedEmailMapping1.getRefreshFrom()); + + IdentityMapV3Response.MappedIdentity hashedEmailMapping2 = response.getMappedIdentities().get(hashedEmail2); + assertEquals("hashed email 2 current uid", hashedEmailMapping2.getCurrentRawUid()); + assertEquals("hashed email 2 previous uid", hashedEmailMapping2.getPreviousRawUid()); + assertEquals(hashedEmail2RefreshFrom, hashedEmailMapping2.getRefreshFrom()); + + // Hashed Phone + IdentityMapV3Response.MappedIdentity hashedPhoneMapping1 = response.getMappedIdentities().get(hashedPhone1); + assertEquals("hashed phone 1 current uid", hashedPhoneMapping1.getCurrentRawUid()); + assertEquals("hashed phone 1 previous uid", hashedPhoneMapping1.getPreviousRawUid()); + assertEquals(hashedPhone1RefreshFrom, hashedPhoneMapping1.getRefreshFrom()); + + IdentityMapV3Response.MappedIdentity hashedPhoneMapping2 = response.getMappedIdentities().get(hashedPhone2); + assertEquals("hashed phone 2 current uid", hashedPhoneMapping2.getCurrentRawUid()); + assertEquals("hashed phone 2 previous uid", hashedPhoneMapping2.getPreviousRawUid()); + assertEquals(hashedPhone2RefreshFrom, hashedPhoneMapping2.getRefreshFrom()); + } + + @Test + void unmappedIdentityReasonUnknown() { + IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); + + IdentityMapV3Response response = new IdentityMapV3Response(unmappedResponsePayload("SOME_NEW_UNMAPPED_REASON"), input); + assertTrue(response.isSuccess()); + + IdentityMapV3Response.UnmappedIdentity unmappedIdentity = response.getUnmappedIdentities().get(SOME_EMAIL); + assertEquals(UnmappedIdentityReason.UNKNOWN, unmappedIdentity.getReason()); + assertEquals("SOME_NEW_UNMAPPED_REASON", unmappedIdentity.getRawReason()); + } + + @Test + void unmappedIdentityReasonOptout() { + IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); + + IdentityMapV3Response response = new IdentityMapV3Response(unmappedResponsePayload("OPTOUT"), input); + assertTrue(response.isSuccess()); + + IdentityMapV3Response.UnmappedIdentity unmappedIdentity = response.getUnmappedIdentities().get(SOME_EMAIL); + assertEquals(UnmappedIdentityReason.OPTOUT, unmappedIdentity.getReason()); + assertEquals("OPTOUT", unmappedIdentity.getRawReason()); + } + + @Test + void unmappedIdentityReasonInvalid() { + IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); + + IdentityMapV3Response response = new IdentityMapV3Response(unmappedResponsePayload("INVALID"), input); + assertTrue(response.isSuccess()); + + IdentityMapV3Response.UnmappedIdentity unmappedIdentity = response.getUnmappedIdentities().get(SOME_EMAIL); + assertEquals(UnmappedIdentityReason.INVALID, unmappedIdentity.getReason()); + assertEquals("INVALID", unmappedIdentity.getRawReason()); + } + + @Test + void unmappedIdentityReasonCaseInsensitive() { + IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); + + IdentityMapV3Response.UnmappedIdentity lowercaseOptout = + new IdentityMapV3Response(unmappedResponsePayload("optout"), input).getUnmappedIdentities().get(SOME_EMAIL); + assertEquals(UnmappedIdentityReason.OPTOUT, lowercaseOptout.getReason()); + assertEquals("optout", lowercaseOptout.getRawReason()); + + IdentityMapV3Response.UnmappedIdentity lowercaseInvalid = + new IdentityMapV3Response(unmappedResponsePayload("invalid"), input).getUnmappedIdentities().get(SOME_EMAIL); + assertEquals(UnmappedIdentityReason.INVALID, lowercaseInvalid.getReason()); + assertEquals("invalid", lowercaseInvalid.getRawReason()); + + IdentityMapV3Response.UnmappedIdentity mixedOptout = + new IdentityMapV3Response(unmappedResponsePayload("OptOut"), input).getUnmappedIdentities().get(SOME_EMAIL); + assertEquals(UnmappedIdentityReason.OPTOUT, mixedOptout.getReason()); + assertEquals("OptOut", mixedOptout.getRawReason()); + + IdentityMapV3Response.UnmappedIdentity mixedInvalid = + new IdentityMapV3Response(unmappedResponsePayload("InVaLiD"), input).getUnmappedIdentities().get(SOME_EMAIL); + assertEquals(UnmappedIdentityReason.INVALID, mixedInvalid.getReason()); + assertEquals("InVaLiD", mixedInvalid.getRawReason()); + } + + @Test + void responseStatusNotSuccess() { + IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); + + String failureResponsePayload = "{\"status\":\"error\",\"body\":{}}"; + + Uid2Exception exception = assertThrows(Uid2Exception.class, () -> { + new IdentityMapV3Response(failureResponsePayload, input); + }); + + assertEquals("Got unexpected identity map status: error", exception.getMessage()); + } + + private static String unmappedResponsePayload(String reason) { + return "{\"status\":\"success\",\"body\":{\"email_hash\":[{\"e\":\"" + reason + "\"}]}}"; + } + + private static String mappedResponsePayload(String[] emailHashEntries, String[] phoneHashEntries) { + return "{\"status\":\"success\",\"body\":{" + + "\"email_hash\":[" + String.join(",", emailHashEntries) + "]," + + "\"phone_hash\":[" + String.join(",", phoneHashEntries) + "]" + + "}}"; + } + + private static String mappedResponsePayloadEntry(String currentUid, String previousUid, Instant refreshFrom) { + return "{\"u\":\"" + currentUid + "\",\"p\":\"" + previousUid + "\",\"r\":" + refreshFrom.getEpochSecond() + "}"; + } +} \ No newline at end of file diff --git a/src/test/java/com/uid2/client/IdentityMapV3Tests.java b/src/test/java/com/uid2/client/IdentityMapV3Tests.java deleted file mode 100644 index e0dd39f..0000000 --- a/src/test/java/com/uid2/client/IdentityMapV3Tests.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.uid2.client; - -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; - -import java.util.*; - -import static org.junit.jupiter.api.Assertions.*; - -public class IdentityMapV3Tests { - private final static String SOME_EMAIL = "test@example.com"; - - @Test - void identityMapV3UnmappedIdentityReasonUnknown() { - IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); - - IdentityMapV3Response response = new IdentityMapV3Response(payloadJson("SOME_NEW_UNMAPPED_REASON"), input); - - IdentityMapV3Response.UnmappedIdentity unmappedIdentity = response.getUnmappedIdentities().get(SOME_EMAIL); - assertEquals(UnmappedIdentityReason.UNKNOWN, unmappedIdentity.getReason()); - assertEquals("SOME_NEW_UNMAPPED_REASON", unmappedIdentity.getRawReason()); - } - - @Test - void identityMapV3UnmappedIdentityReasonOptout() { - IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); - - IdentityMapV3Response response = new IdentityMapV3Response(payloadJson("OPTOUT"), input); - - IdentityMapV3Response.UnmappedIdentity unmappedIdentity = response.getUnmappedIdentities().get(SOME_EMAIL); - assertEquals(UnmappedIdentityReason.OPTOUT, unmappedIdentity.getReason()); - assertEquals("OPTOUT", unmappedIdentity.getRawReason()); - } - - @Test - void identityMapV3UnmappedIdentityReasonInvalid() { - IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); - - IdentityMapV3Response response = new IdentityMapV3Response(payloadJson("INVALID"), input); - - IdentityMapV3Response.UnmappedIdentity unmappedIdentity = response.getUnmappedIdentities().get(SOME_EMAIL); - assertEquals(UnmappedIdentityReason.INVALID, unmappedIdentity.getReason()); - assertEquals("INVALID", unmappedIdentity.getRawReason()); - } - - @Test - void identityMapV3UnmappedIdentityReasonCaseInsensitive() { - IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); - - IdentityMapV3Response.UnmappedIdentity lowercaseOptout = - new IdentityMapV3Response(payloadJson("optout"), input).getUnmappedIdentities().get(SOME_EMAIL); - assertEquals(UnmappedIdentityReason.OPTOUT, lowercaseOptout.getReason()); - assertEquals("optout", lowercaseOptout.getRawReason()); - - IdentityMapV3Response.UnmappedIdentity lowercaseInvalid = - new IdentityMapV3Response(payloadJson("invalid"), input).getUnmappedIdentities().get(SOME_EMAIL); - assertEquals(UnmappedIdentityReason.INVALID, lowercaseInvalid.getReason()); - assertEquals("invalid", lowercaseInvalid.getRawReason()); - - IdentityMapV3Response.UnmappedIdentity mixedOptout = - new IdentityMapV3Response(payloadJson("OptOut"), input).getUnmappedIdentities().get(SOME_EMAIL); - assertEquals(UnmappedIdentityReason.OPTOUT, mixedOptout.getReason()); - assertEquals("OptOut", mixedOptout.getRawReason()); - - IdentityMapV3Response.UnmappedIdentity mixedInvalid = - new IdentityMapV3Response(payloadJson("InVaLiD"), input).getUnmappedIdentities().get(SOME_EMAIL); - assertEquals(UnmappedIdentityReason.INVALID, mixedInvalid.getReason()); - assertEquals("InVaLiD", mixedInvalid.getRawReason()); - } - - @NotNull - private static String payloadJson(String reason) { - return "{\"status\":\"success\",\"body\":{\"email_hash\":[{\"e\":\"" + reason + "\"}]}}"; - } -} \ No newline at end of file From 162e90588c7c44044f589adf5b4055a4308189c5 Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Thu, 12 Jun 2025 01:28:35 +0000 Subject: [PATCH 14/26] [CI Pipeline] Released Snapshot version: 4.6.2-alpha-19-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 507c579..9e12947 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-client - 4.6.1-alpha-17-SNAPSHOT + 4.6.2-alpha-19-SNAPSHOT ${project.groupId}:${project.artifactId} UID2 Client From ec0b6fd1cf5ba2d61e89ff04d1a94b81acbded64 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Thu, 12 Jun 2025 14:33:53 +0800 Subject: [PATCH 15/26] Adapted SDK to changes in V3 API --- .../uid2/client/UnmappedIdentityReason.java | 11 +++--- .../client/IdentityMapV3IntegrationTests.java | 4 +- .../client/IdentityMapV3ResponseTest.java | 39 ++++--------------- 3 files changed, 15 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/uid2/client/UnmappedIdentityReason.java b/src/main/java/com/uid2/client/UnmappedIdentityReason.java index 2bc6b1e..7c59ad1 100644 --- a/src/main/java/com/uid2/client/UnmappedIdentityReason.java +++ b/src/main/java/com/uid2/client/UnmappedIdentityReason.java @@ -3,14 +3,15 @@ public enum UnmappedIdentityReason { OPTOUT, - INVALID, + INVALID_IDENTIFIER, UNKNOWN; public static UnmappedIdentityReason fromString(String reason) { - for (UnmappedIdentityReason knownReason : values()) { - if (knownReason.name().equals(reason.toUpperCase())) { - return knownReason; - } + if (reason.equals("optout")) { + return OPTOUT; + } + if (reason.equals("invalid identifier")) { + return INVALID_IDENTIFIER; } return UNKNOWN; diff --git a/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java index e047a93..17b8a51 100644 --- a/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java +++ b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java @@ -77,7 +77,7 @@ public void identityMapInvalidHashedEmail() { Response response = new Response(identityMapInput); - response.assertUnmapped(UnmappedIdentityReason.INVALID, "this is not a hashed email"); + response.assertUnmapped(UnmappedIdentityReason.INVALID_IDENTIFIER, "this is not a hashed email"); } @Test @@ -85,7 +85,7 @@ public void identityMapInvalidHashedPhone() { IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromHashedPhones(Collections.singletonList("this is not a hashed phone")); Response response = new Response(identityMapInput); - response.assertUnmapped(UnmappedIdentityReason.INVALID, "this is not a hashed phone"); + response.assertUnmapped(UnmappedIdentityReason.INVALID_IDENTIFIER, "this is not a hashed phone"); } @Test diff --git a/src/test/java/com/uid2/client/IdentityMapV3ResponseTest.java b/src/test/java/com/uid2/client/IdentityMapV3ResponseTest.java index c037fd9..bba321c 100644 --- a/src/test/java/com/uid2/client/IdentityMapV3ResponseTest.java +++ b/src/test/java/com/uid2/client/IdentityMapV3ResponseTest.java @@ -108,61 +108,36 @@ void mappedIdentity() { void unmappedIdentityReasonUnknown() { IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); - IdentityMapV3Response response = new IdentityMapV3Response(unmappedResponsePayload("SOME_NEW_UNMAPPED_REASON"), input); + IdentityMapV3Response response = new IdentityMapV3Response(unmappedResponsePayload("some new unmapped reason"), input); assertTrue(response.isSuccess()); IdentityMapV3Response.UnmappedIdentity unmappedIdentity = response.getUnmappedIdentities().get(SOME_EMAIL); assertEquals(UnmappedIdentityReason.UNKNOWN, unmappedIdentity.getReason()); - assertEquals("SOME_NEW_UNMAPPED_REASON", unmappedIdentity.getRawReason()); + assertEquals("some new unmapped reason", unmappedIdentity.getRawReason()); } @Test void unmappedIdentityReasonOptout() { IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); - IdentityMapV3Response response = new IdentityMapV3Response(unmappedResponsePayload("OPTOUT"), input); + IdentityMapV3Response response = new IdentityMapV3Response(unmappedResponsePayload("optout"), input); assertTrue(response.isSuccess()); IdentityMapV3Response.UnmappedIdentity unmappedIdentity = response.getUnmappedIdentities().get(SOME_EMAIL); assertEquals(UnmappedIdentityReason.OPTOUT, unmappedIdentity.getReason()); - assertEquals("OPTOUT", unmappedIdentity.getRawReason()); + assertEquals("optout", unmappedIdentity.getRawReason()); } @Test void unmappedIdentityReasonInvalid() { IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); - IdentityMapV3Response response = new IdentityMapV3Response(unmappedResponsePayload("INVALID"), input); + IdentityMapV3Response response = new IdentityMapV3Response(unmappedResponsePayload("invalid identifier"), input); assertTrue(response.isSuccess()); IdentityMapV3Response.UnmappedIdentity unmappedIdentity = response.getUnmappedIdentities().get(SOME_EMAIL); - assertEquals(UnmappedIdentityReason.INVALID, unmappedIdentity.getReason()); - assertEquals("INVALID", unmappedIdentity.getRawReason()); - } - - @Test - void unmappedIdentityReasonCaseInsensitive() { - IdentityMapV3Input input = IdentityMapV3Input.fromEmails(Arrays.asList(SOME_EMAIL)); - - IdentityMapV3Response.UnmappedIdentity lowercaseOptout = - new IdentityMapV3Response(unmappedResponsePayload("optout"), input).getUnmappedIdentities().get(SOME_EMAIL); - assertEquals(UnmappedIdentityReason.OPTOUT, lowercaseOptout.getReason()); - assertEquals("optout", lowercaseOptout.getRawReason()); - - IdentityMapV3Response.UnmappedIdentity lowercaseInvalid = - new IdentityMapV3Response(unmappedResponsePayload("invalid"), input).getUnmappedIdentities().get(SOME_EMAIL); - assertEquals(UnmappedIdentityReason.INVALID, lowercaseInvalid.getReason()); - assertEquals("invalid", lowercaseInvalid.getRawReason()); - - IdentityMapV3Response.UnmappedIdentity mixedOptout = - new IdentityMapV3Response(unmappedResponsePayload("OptOut"), input).getUnmappedIdentities().get(SOME_EMAIL); - assertEquals(UnmappedIdentityReason.OPTOUT, mixedOptout.getReason()); - assertEquals("OptOut", mixedOptout.getRawReason()); - - IdentityMapV3Response.UnmappedIdentity mixedInvalid = - new IdentityMapV3Response(unmappedResponsePayload("InVaLiD"), input).getUnmappedIdentities().get(SOME_EMAIL); - assertEquals(UnmappedIdentityReason.INVALID, mixedInvalid.getReason()); - assertEquals("InVaLiD", mixedInvalid.getRawReason()); + assertEquals(UnmappedIdentityReason.INVALID_IDENTIFIER, unmappedIdentity.getReason()); + assertEquals("invalid identifier", unmappedIdentity.getRawReason()); } @Test From 73ee0ab9b48b5a11d9d635be846c7d4831b5e178 Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Thu, 12 Jun 2025 06:59:06 +0000 Subject: [PATCH 16/26] [CI Pipeline] Released Snapshot version: 4.6.3-alpha-20-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9e12947..f5094bd 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-client - 4.6.2-alpha-19-SNAPSHOT + 4.6.3-alpha-20-SNAPSHOT ${project.groupId}:${project.artifactId} UID2 Client From 80b92b9f9c7c9ff291f545c59ce1ff5e3cae0d2b Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Thu, 12 Jun 2025 16:26:39 +0800 Subject: [PATCH 17/26] Using Caroline's branch in build and publish for now since publishing snapshot deosn't work in the existing workflow --- .github/workflows/build-and-publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-publish.yaml b/.github/workflows/build-and-publish.yaml index b148051..d4b2e88 100644 --- a/.github/workflows/build-and-publish.yaml +++ b/.github/workflows/build-and-publish.yaml @@ -26,7 +26,7 @@ on: jobs: build-and-pubish: name: Build and publish JAR packages to Maven repository - uses: IABTechLab/uid2-shared-actions/.github/workflows/shared-publish-to-maven-versioned.yaml@v3 + uses: IABTechLab/uid2-shared-actions/.github/workflows/shared-publish-to-maven-versioned.yaml@ccm-UID2-5546-migrate-maven-to-central-publisher-portal with: release_type: ${{ inputs.release_type }} publish_to_maven: ${{ inputs.publish_to_maven }} From b55415a41545edfbcb906dd7120777f7b2fd3011 Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Thu, 12 Jun 2025 08:28:18 +0000 Subject: [PATCH 18/26] [CI Pipeline] Released Snapshot version: 4.6.4-alpha-21-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f5094bd..bb565d9 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-client - 4.6.3-alpha-20-SNAPSHOT + 4.6.4-alpha-21-SNAPSHOT ${project.groupId}:${project.artifactId} UID2 Client From da5b65b45a990056fcab3935a4cfe8d46059d5b4 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Thu, 12 Jun 2025 16:34:14 +0800 Subject: [PATCH 19/26] Ok don't need it --- .github/workflows/build-and-publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-publish.yaml b/.github/workflows/build-and-publish.yaml index d4b2e88..b148051 100644 --- a/.github/workflows/build-and-publish.yaml +++ b/.github/workflows/build-and-publish.yaml @@ -26,7 +26,7 @@ on: jobs: build-and-pubish: name: Build and publish JAR packages to Maven repository - uses: IABTechLab/uid2-shared-actions/.github/workflows/shared-publish-to-maven-versioned.yaml@ccm-UID2-5546-migrate-maven-to-central-publisher-portal + uses: IABTechLab/uid2-shared-actions/.github/workflows/shared-publish-to-maven-versioned.yaml@v3 with: release_type: ${{ inputs.release_type }} publish_to_maven: ${{ inputs.publish_to_maven }} From aac8510db47d5db740a095f66a66dd2098e40b26 Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Fri, 13 Jun 2025 02:20:56 +0000 Subject: [PATCH 20/26] [CI Pipeline] Released Snapshot version: 4.6.5-alpha-25-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6d9253e..448df67 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-client - 4.6.4-alpha-21-SNAPSHOT + 4.6.5-alpha-25-SNAPSHOT ${project.groupId}:${project.artifactId} UID2 Client From 361ac4e7f133ec3b6d538498dff5ce44a02708d2 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Fri, 13 Jun 2025 18:10:53 +0800 Subject: [PATCH 21/26] Added mandatory email and phone fields to V3 Identity Map API --- src/main/java/com/uid2/client/IdentityMapV3Input.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/uid2/client/IdentityMapV3Input.java b/src/main/java/com/uid2/client/IdentityMapV3Input.java index 229abad..3dbd08c 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Input.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Input.java @@ -46,6 +46,12 @@ public static IdentityMapV3Input fromHashedPhones(List hashedPhones) { @SerializedName("phone_hash") private final List hashedPhones = new ArrayList<>(); + // We never send unhashed emails or phone numbers in the SDK, but they are required fields in the API request + @SerializedName("email") + private List emails = Collections.unmodifiableList(new ArrayList<>()); + @SerializedName("phone") + private List phones = Collections.unmodifiableList(new ArrayList<>()); + public IdentityMapV3Input() {} /** @@ -136,7 +142,6 @@ public IdentityMapV3Input withPhone(String phone) { this.hashedPhones.add(new Identity(hashedPhone)); addToDiiMappings(hashedPhone, phone); return this; - } List getInputDiis(String identityType, int i) { From 5ff919430b9018f426463005da1f1ea4eaa75b1f Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Fri, 13 Jun 2025 10:42:06 +0000 Subject: [PATCH 22/26] [CI Pipeline] Released Snapshot version: 4.6.6-alpha-27-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 448df67..7de280d 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-client - 4.6.5-alpha-25-SNAPSHOT + 4.6.6-alpha-27-SNAPSHOT ${project.groupId}:${project.artifactId} UID2 Client From 127f2195cb333ae08df327634ccd1a816b0d8d82 Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Fri, 13 Jun 2025 10:47:49 +0000 Subject: [PATCH 23/26] [CI Pipeline] Released Snapshot version: 4.6.7-alpha-28-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7de280d..836ec2b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-client - 4.6.6-alpha-27-SNAPSHOT + 4.6.7-alpha-28-SNAPSHOT ${project.groupId}:${project.artifactId} UID2 Client From e66f6ce1a493cfd5bca88a32946cc927bde2f729 Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Fri, 13 Jun 2025 19:02:04 +0800 Subject: [PATCH 24/26] Fixed request to new unrwrapped version for V3 Identity Map --- .../com/uid2/client/IdentityMapV3Input.java | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/uid2/client/IdentityMapV3Input.java b/src/main/java/com/uid2/client/IdentityMapV3Input.java index 3dbd08c..83ee82a 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Input.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Input.java @@ -41,10 +41,10 @@ public static IdentityMapV3Input fromHashedPhones(List hashedPhones) { private transient final Map> hashedDiiToRawDii = new HashMap<>(); @SerializedName("email_hash") - private final List hashedEmails = new ArrayList<>(); + private final List hashedEmails = new ArrayList<>(); @SerializedName("phone_hash") - private final List hashedPhones = new ArrayList<>(); + private final List hashedPhones = new ArrayList<>(); // We never send unhashed emails or phone numbers in the SDK, but they are required fields in the API request @SerializedName("email") @@ -70,7 +70,7 @@ public IdentityMapV3Input withHashedEmails(List hashedEmails) { * @return this IdentityMapV3Input instance */ public IdentityMapV3Input withHashedEmail(String hashedEmail) { - this.hashedEmails.add(new Identity(hashedEmail)); + this.hashedEmails.add(hashedEmail); addToDiiMappings(hashedEmail, hashedEmail); return this; } @@ -91,7 +91,7 @@ public IdentityMapV3Input withHashedPhones(List hashedPhones) { * @return this IdentityMapV3Input instance */ public IdentityMapV3Input withHashedPhone(String hashedPhone) { - this.hashedPhones.add(new Identity(hashedPhone)); + this.hashedPhones.add(hashedPhone); addToDiiMappings(hashedPhone, hashedPhone); return this; } @@ -113,7 +113,7 @@ public IdentityMapV3Input withEmails(List emails) { */ public IdentityMapV3Input withEmail(String email) { String hashedEmail = InputUtil.normalizeAndHashEmail(email); - this.hashedEmails.add(new Identity(hashedEmail)); + this.hashedEmails.add(hashedEmail); addToDiiMappings(hashedEmail, email); return this; } @@ -139,7 +139,7 @@ public IdentityMapV3Input withPhone(String phone) { } String hashedPhone = InputUtil.getBase64EncodedHash(phone); - this.hashedPhones.add(new Identity(hashedPhone)); + this.hashedPhones.add(hashedPhone); addToDiiMappings(hashedPhone, phone); return this; } @@ -154,19 +154,9 @@ private void addToDiiMappings(String hashedDii, String rawDii) { private String getHashedDii(String identityType, int i) { switch (identityType) { - case "email_hash": return hashedEmails.get(i).identity; - case "phone_hash": return hashedPhones.get(i).identity; + case "email_hash": return hashedEmails.get(i); + case "phone_hash": return hashedPhones.get(i); } throw new Uid2Exception("Unexpected identity type: " + identityType); } - - - private static class Identity { - @SerializedName("i") - private final String identity; - - public Identity(String value) { - this.identity = value; - } - } } From db70dee1356d63cd77d2046aab0b7d0834088da5 Mon Sep 17 00:00:00 2001 From: Release Workflow Date: Fri, 13 Jun 2025 11:04:33 +0000 Subject: [PATCH 25/26] [CI Pipeline] Released Snapshot version: 4.6.8-alpha-29-SNAPSHOT --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 836ec2b..d69e08c 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-client - 4.6.7-alpha-28-SNAPSHOT + 4.6.8-alpha-29-SNAPSHOT ${project.groupId}:${project.artifactId} UID2 Client From 43ddf4d1d127b2145f2b6857c92bad80dea3d04a Mon Sep 17 00:00:00 2001 From: Aleksandrs Ulme Date: Tue, 17 Jun 2025 17:54:44 +0800 Subject: [PATCH 26/26] Using binary payload with IdentityMapV3 --- src/main/java/com/uid2/client/EnvelopeV2.java | 12 +++-- .../com/uid2/client/IdentityMapClient.java | 2 +- .../com/uid2/client/IdentityMapV3Client.java | 8 +++- .../com/uid2/client/IdentityMapV3Helper.java | 5 +++ .../com/uid2/client/PublisherUid2Client.java | 6 +-- .../java/com/uid2/client/TokenHelper.java | 2 +- .../com/uid2/client/Uid2ClientHelper.java | 45 ++++++++++++++----- src/main/java/com/uid2/client/Uid2Helper.java | 12 ++++- .../java/com/uid2/client/Uid2Response.java | 31 +++++++++++++ 9 files changed, 100 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/uid2/client/Uid2Response.java diff --git a/src/main/java/com/uid2/client/EnvelopeV2.java b/src/main/java/com/uid2/client/EnvelopeV2.java index 79557e4..5cc5fa2 100644 --- a/src/main/java/com/uid2/client/EnvelopeV2.java +++ b/src/main/java/com/uid2/client/EnvelopeV2.java @@ -2,8 +2,8 @@ public class EnvelopeV2 { - EnvelopeV2(String envelope, byte[] nonce) { - this.envelope = envelope; + EnvelopeV2(byte[] envelope, byte[] nonce) { + this.binaryEnvelope = envelope; this.nonce = nonce; } @@ -11,10 +11,14 @@ public class EnvelopeV2 { * @return an encrypted request envelope which can be used in the POST body of a UID2 endpoint. * See Encrypted Request Envelope */ - public String getEnvelope() { return envelope; } + public String getEnvelope() { return InputUtil.byteArrayToBase64(binaryEnvelope); } byte[] getNonce() { return nonce;} - private final String envelope; + public byte[] getBinaryEnvelope() { + return binaryEnvelope; + } + + private final byte[] binaryEnvelope; private final byte[] nonce; } diff --git a/src/main/java/com/uid2/client/IdentityMapClient.java b/src/main/java/com/uid2/client/IdentityMapClient.java index 41bd425..724d037 100644 --- a/src/main/java/com/uid2/client/IdentityMapClient.java +++ b/src/main/java/com/uid2/client/IdentityMapClient.java @@ -19,7 +19,7 @@ public IdentityMapClient(String uid2BaseUrl, String clientApiKey, String base64S public IdentityMapResponse generateIdentityMap(IdentityMapInput identityMapInput) { EnvelopeV2 envelope = identityMapHelper.createEnvelopeForIdentityMapRequest(identityMapInput); - String responseString = uid2ClientHelper.makeRequest(envelope, "/v2/identity/map"); + String responseString = uid2ClientHelper.makeRequest("/v2/identity/map", envelope).getAsString(); return identityMapHelper.createIdentityMapResponse(responseString, envelope, identityMapInput); } diff --git a/src/main/java/com/uid2/client/IdentityMapV3Client.java b/src/main/java/com/uid2/client/IdentityMapV3Client.java index b3b8a1c..d6b6548 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Client.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Client.java @@ -19,8 +19,12 @@ public IdentityMapV3Client(String uid2BaseUrl, String clientApiKey, String base6 public IdentityMapV3Response generateIdentityMap(IdentityMapV3Input identityMapInput) { EnvelopeV2 envelope = identityMapHelper.createEnvelopeForIdentityMapRequest(identityMapInput); - String responseString = uid2ClientHelper.makeRequest(envelope, "/v3/identity/map"); - return identityMapHelper.createIdentityMapResponse(responseString, envelope, identityMapInput); + Uid2Response response = uid2ClientHelper.makeBinaryRequest("/v3/identity/map", envelope); + if (response.isBinary()) { + return identityMapHelper.createIdentityMapResponse(response.getAsBytes(), envelope, identityMapInput); + } else { + return identityMapHelper.createIdentityMapResponse(response.getAsString(), envelope, identityMapInput); + } } private final IdentityMapV3Helper identityMapHelper; diff --git a/src/main/java/com/uid2/client/IdentityMapV3Helper.java b/src/main/java/com/uid2/client/IdentityMapV3Helper.java index d324de5..705f17f 100644 --- a/src/main/java/com/uid2/client/IdentityMapV3Helper.java +++ b/src/main/java/com/uid2/client/IdentityMapV3Helper.java @@ -31,5 +31,10 @@ public IdentityMapV3Response createIdentityMapResponse(String responseString, En return new IdentityMapV3Response(decryptedResponseString, identityMapInput); } + public IdentityMapV3Response createIdentityMapResponse(byte[] response, EnvelopeV2 envelope, IdentityMapV3Input identityMapInput) { + String decryptedResponseString = uid2Helper.decrypt(response, envelope.getNonce()); + return new IdentityMapV3Response(decryptedResponseString, identityMapInput); + } + private final Uid2Helper uid2Helper; } diff --git a/src/main/java/com/uid2/client/PublisherUid2Client.java b/src/main/java/com/uid2/client/PublisherUid2Client.java index 176347a..2261d99 100644 --- a/src/main/java/com/uid2/client/PublisherUid2Client.java +++ b/src/main/java/com/uid2/client/PublisherUid2Client.java @@ -21,7 +21,7 @@ public PublisherUid2Client(String uid2BaseUrl, String clientApiKey, String base6 public IdentityTokens generateToken(TokenGenerateInput tokenGenerateInput) { EnvelopeV2 envelope = publisherUid2Helper.createEnvelopeForTokenGenerateRequest(tokenGenerateInput); - String responseString = uid2ClientHelper.makeRequest(envelope, "/v2/token/generate"); + String responseString = uid2ClientHelper.makeRequest("/v2/token/generate", envelope).getAsString(); return publisherUid2Helper.createIdentityfromTokenGenerateResponse(responseString, envelope); } @@ -33,7 +33,7 @@ public IdentityTokens generateToken(TokenGenerateInput tokenGenerateInput) { public TokenGenerateResponse generateTokenResponse(TokenGenerateInput tokenGenerateInput) { EnvelopeV2 envelope = publisherUid2Helper.createEnvelopeForTokenGenerateRequest(tokenGenerateInput); - String responseString = uid2ClientHelper.makeRequest(envelope, "/v2/token/generate"); + String responseString = uid2ClientHelper.makeRequest("/v2/token/generate", envelope).getAsString(); return publisherUid2Helper.createTokenGenerateResponse(responseString, envelope); } @@ -42,7 +42,7 @@ public TokenGenerateResponse generateTokenResponse(TokenGenerateInput tokenGener * @return the refreshed IdentityTokens instance (with a new advertising token and updated expiry times). Typically, this will be used to replace the current identity in the user's session */ public TokenRefreshResponse refreshToken(IdentityTokens currentIdentity) { - String responseString = uid2ClientHelper.makeRequest(currentIdentity.getRefreshToken(), "/v2/token/refresh"); + String responseString = uid2ClientHelper.makeRequest("/v2/token/refresh", currentIdentity.getRefreshToken()).getAsString(); return PublisherUid2Helper.createTokenRefreshResponse(responseString, currentIdentity); } diff --git a/src/main/java/com/uid2/client/TokenHelper.java b/src/main/java/com/uid2/client/TokenHelper.java index 1e55734..ece7d3d 100644 --- a/src/main/java/com/uid2/client/TokenHelper.java +++ b/src/main/java/com/uid2/client/TokenHelper.java @@ -48,7 +48,7 @@ EncryptionDataResponse encryptRawUidIntoToken(String rawUid, Instant now) { RefreshResponse refresh(String urlSuffix) { try{ EnvelopeV2 envelope = uid2Helper.createEnvelopeV2("".getBytes()); - String responseString = uid2ClientHelper.makeRequest(envelope, urlSuffix); + String responseString = uid2ClientHelper.makeRequest(urlSuffix, envelope).getAsString(); byte[] response = uid2Helper.decrypt(responseString, envelope.getNonce()).getBytes(); this.container.set(KeyParser.parse(new ByteArrayInputStream(response))); return RefreshResponse.makeSuccess(); diff --git a/src/main/java/com/uid2/client/Uid2ClientHelper.java b/src/main/java/com/uid2/client/Uid2ClientHelper.java index f29bef2..85925c0 100644 --- a/src/main/java/com/uid2/client/Uid2ClientHelper.java +++ b/src/main/java/com/uid2/client/Uid2ClientHelper.java @@ -3,6 +3,7 @@ import okhttp3.*; import java.io.IOException; +import java.util.Objects; public class Uid2ClientHelper { Uid2ClientHelper(String baseUrl, String clientApiKey) { @@ -17,51 +18,73 @@ static Headers getHeaders(String clientApiKey) { .build(); } - String makeRequest(EnvelopeV2 envelope, String urlSuffix) { - return makeRequest(envelope.getEnvelope(), urlSuffix); + Uid2Response makeRequest(String urlSuffix, EnvelopeV2 envelope) { + return makeRequest(urlSuffix, getEnvelope(envelope)); } - String makeRequest(String requestBody, String urlSuffix) { + Uid2Response makeRequest(String urlSuffix, String payload) { + return makeRequest(RequestBody.create(payload, FORM), urlSuffix); + } + + private static String getEnvelope(EnvelopeV2 envelope) { + return envelope.getEnvelope(); + } + + Uid2Response makeBinaryRequest(String urlSuffix, EnvelopeV2 envelope) { + return makeRequest(RequestBody.create(envelope.getBinaryEnvelope(), BINARY), urlSuffix); + } + + Uid2Response makeRequest(RequestBody body, String urlSuffix) { Request request = new Request.Builder() .url(baseUrl + urlSuffix) .headers(headers) - .post(RequestBody.create(requestBody, FORM)) + .post(body) .build(); - try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new Uid2Exception("Unexpected code " + response); } - return getResponse(response); } catch (IOException e) { throw new Uid2Exception("error communicating with api endpoint", e); } } - private static String getResponse(Response response) { - String responseString; + private static Uid2Response getResponse(Response response) { + Uid2Response uid2Response; try { if (response == null) { throw new Uid2Exception("Response is null"); } else { - responseString = response.body() != null ? response.body().string() : response.toString(); + if (responseIsBinary(response)) { + byte[] bytes = response.body() != null ? response.body().bytes() : new byte[0]; + uid2Response = Uid2Response.fromBytes(bytes); + } else { + String string = response.body() != null ? response.body().string() : response.toString(); + uid2Response = Uid2Response.fromString(string); + } + if (!response.isSuccessful()) { - throw new Uid2Exception("Unexpected code " + responseString); + throw new Uid2Exception("Unexpected code " + response); } } - return responseString; + return uid2Response; } catch (IOException e) { throw new Uid2Exception("Error communicating with api endpoint", e); } } + private static boolean responseIsBinary(Response response) { + return Objects.equals(response.headers().get("Content-Type"), "application/octet-stream"); + } + private final OkHttpClient client = new OkHttpClient(); private final String baseUrl; private final Headers headers; private final static MediaType FORM = MediaType.get("application/x-www-form-urlencoded"); + private final static MediaType BINARY = MediaType.get("application/octet-stream"); } diff --git a/src/main/java/com/uid2/client/Uid2Helper.java b/src/main/java/com/uid2/client/Uid2Helper.java index a87e5c3..a8a722a 100644 --- a/src/main/java/com/uid2/client/Uid2Helper.java +++ b/src/main/java/com/uid2/client/Uid2Helper.java @@ -1,5 +1,7 @@ package com.uid2.client; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -30,13 +32,17 @@ public EnvelopeV2 createEnvelopeV2(byte[] nonce, Instant timestamp, byte[] iv, b final byte envelopeVersion = 1; envelopeBuffer.put(envelopeVersion); envelopeBuffer.put(encrypted); - return new EnvelopeV2(InputUtil.byteArrayToBase64(envelopeBuffer.array()), nonce); + return new EnvelopeV2(envelopeBuffer.array(), nonce); } public String decrypt(String response, byte[] nonceInRequest) { return decrypt(response, secretKey, false, nonceInRequest); } + public String decrypt(byte[] response, byte[] nonceInRequest) { + return decrypt(response, secretKey, false, nonceInRequest); + } + static String decryptTokenRefreshResponse(String response, byte[] secretKey) { return decrypt(response, secretKey, true, null); } @@ -44,6 +50,10 @@ static String decryptTokenRefreshResponse(String response, byte[] secretKey) { private static String decrypt(String response, byte[] secretKey, boolean isRefreshResponse, byte[] nonceInRequest) { //from parseV2Response byte[] responseBytes = InputUtil.base64ToByteArray(response); + return decrypt(responseBytes, secretKey, isRefreshResponse, nonceInRequest); + } + + private static String decrypt(byte[] responseBytes, byte[] secretKey, boolean isRefreshResponse, byte[] nonceInRequest) { byte[] payload = Uid2Encryption.decryptGCM(responseBytes, 0, secretKey); byte[] resultBytes; diff --git a/src/main/java/com/uid2/client/Uid2Response.java b/src/main/java/com/uid2/client/Uid2Response.java new file mode 100644 index 0000000..17b25d4 --- /dev/null +++ b/src/main/java/com/uid2/client/Uid2Response.java @@ -0,0 +1,31 @@ +package com.uid2.client; + +public class Uid2Response { + String asString; + byte[] asBytes; + + private Uid2Response(String asString, byte[] asBytes) { + this.asString = asString; + this.asBytes = asBytes; + } + + public static Uid2Response fromString(String asString) { + return new Uid2Response(asString, null); + } + + public static Uid2Response fromBytes(byte[] asBytes) { + return new Uid2Response(null, asBytes); + } + + public String getAsString() { + return asString; + } + + public byte[] getAsBytes() { + return asBytes; + } + + public boolean isBinary() { + return asBytes != null; + } +}