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
diff --git a/pom.xml b/pom.xml
index 071a0e0..d69e08c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.uid2
uid2-client
- 4.6.0
+ 4.6.8-alpha-29-SNAPSHOT
${project.groupId}:${project.artifactId}
UID2 Client
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
new file mode 100644
index 0000000..d6b6548
--- /dev/null
+++ b/src/main/java/com/uid2/client/IdentityMapV3Client.java
@@ -0,0 +1,32 @@
+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 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);
+
+ 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;
+ 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..705f17f
--- /dev/null
+++ b/src/main/java/com/uid2/client/IdentityMapV3Helper.java
@@ -0,0 +1,40 @@
+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 IdentityMapV3Response instance
+ */
+ public IdentityMapV3Response createIdentityMapResponse(String responseString, EnvelopeV2 envelope, IdentityMapV3Input identityMapInput) {
+ String decryptedResponseString = uid2Helper.decrypt(responseString, envelope.getNonce());
+ 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/IdentityMapV3Input.java b/src/main/java/com/uid2/client/IdentityMapV3Input.java
new file mode 100644
index 0000000..83ee82a
--- /dev/null
+++ b/src/main/java/com/uid2/client/IdentityMapV3Input.java
@@ -0,0 +1,162 @@
+package com.uid2.client;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.*;
+
+public class IdentityMapV3Input {
+ /**
+ * @param emails a list of normalized or unnormalized email addresses
+ * @return a IdentityMapV3Input instance, to be used in {@link IdentityMapV3Helper#createEnvelopeForIdentityMapRequest}
+ */
+ public static IdentityMapV3Input fromEmails(List emails) {
+ return new IdentityMapV3Input().withEmails(emails);
+ }
+
+ /**
+ * @param hashedEmails a normalized and hashed email address
+ * @return an IdentityMapV3Input instance
+ */
+ public static IdentityMapV3Input fromHashedEmails(List hashedEmails) {
+ return new IdentityMapV3Input().withHashedEmails(hashedEmails);
+ }
+
+ /**
+ * @param phones a normalized phone number
+ * @return an IdentityMapV3Input instance
+ */
+ public static IdentityMapV3Input fromPhones(List phones) {
+ return new IdentityMapV3Input().withPhones(phones);
+ }
+
+ /**
+ * @param hashedPhones a normalized and hashed phone number
+ * @return an IdentityMapV3Input instance
+ */
+ public static IdentityMapV3Input fromHashedPhones(List hashedPhones) {
+ return new IdentityMapV3Input().withHashedPhones(hashedPhones);
+ }
+
+ // 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<>();
+
+ @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() {}
+
+ /**
+ * @param hashedEmails a normalized and hashed email address
+ * @return this IdentityMapV3Input instance
+ */
+ public IdentityMapV3Input withHashedEmails(List hashedEmails) {
+ for (String hashedEmail : hashedEmails) {
+ withHashedEmail(hashedEmail);
+ }
+ return this;
+ }
+
+ /**
+ * @param hashedEmail a normalized and hashed email address
+ * @return this IdentityMapV3Input instance
+ */
+ public IdentityMapV3Input withHashedEmail(String hashedEmail) {
+ this.hashedEmails.add(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);
+ }
+ return this;
+ }
+
+ /**
+ * @param hashedPhone a normalized and hashed phone number
+ * @return this IdentityMapV3Input instance
+ */
+ public IdentityMapV3Input withHashedPhone(String hashedPhone) {
+ this.hashedPhones.add(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);
+ }
+ 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(hashedEmail);
+ addToDiiMappings(hashedEmail, email);
+ return this;
+ }
+
+ /**
+ * @param phones a normalized phone number
+ * @return this IdentityMapV3Input instance
+ */
+ public IdentityMapV3Input withPhones(List phones) {
+ for (String phone : phones) {
+ withPhone(phone);
+ }
+ 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);
+ }
+
+ String hashedPhone = InputUtil.getBase64EncodedHash(phone);
+ this.hashedPhones.add(hashedPhone);
+ addToDiiMappings(hashedPhone, phone);
+ return this;
+ }
+
+ List getInputDiis(String identityType, int i) {
+ return hashedDiiToRawDii.get(getHashedDii(identityType, i));
+ }
+
+ private void addToDiiMappings(String hashedDii, String rawDii) {
+ hashedDiiToRawDii.computeIfAbsent(hashedDii, k -> new ArrayList<>()).add(rawDii);
+ }
+
+ private String getHashedDii(String identityType, int i) {
+ switch (identityType) {
+ case "email_hash": return hashedEmails.get(i);
+ case "phone_hash": return hashedPhones.get(i);
+ }
+ throw new Uid2Exception("Unexpected identity type: " + identityType);
+ }
+}
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..dc5f611
--- /dev/null
+++ b/src/main/java/com/uid2/client/IdentityMapV3Response.java
@@ -0,0 +1,128 @@
+package com.uid2.client;
+
+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;
+
+public class IdentityMapV3Response {
+ IdentityMapV3Response(String response, IdentityMapV3Input identityMapInput) {
+ ApiResponse apiResponse = new Gson().fromJson(response, ApiResponse.class);
+ status = apiResponse.status;
+
+ if (!isSuccess()) {
+ throw new Uid2Exception("Got unexpected identity map status: " + status);
+ }
+
+ populateIdentities(apiResponse.body, identityMapInput);
+ }
+
+ private void populateIdentities(Map> apiResponse, IdentityMapV3Input identityMapInput) {
+ for (Map.Entry> identitiesForType : apiResponse.entrySet()) {
+ populateIdentitiesForType(identityMapInput, identitiesForType.getKey(), identitiesForType.getValue());
+ }
+ }
+
+ private void populateIdentitiesForType(IdentityMapV3Input identityMapInput, String identityType, List identities) {
+ for (int i = 0; i < identities.size(); i++) {
+ ApiIdentity apiIdentity = identities.get(i);
+ List inputDiis = identityMapInput.getInputDiis(identityType, i);
+ for (String inputDii : inputDiis) {
+ if (apiIdentity.error == null) {
+ mappedIdentities.put(inputDii, new MappedIdentity(apiIdentity));
+ } else {
+ unmappedIdentities.put(inputDii, new UnmappedIdentity(apiIdentity.error));
+ }
+ }
+ }
+ }
+
+ public boolean isSuccess() {
+ return "success".equals(status);
+ }
+
+ public static class ApiResponse {
+ @SerializedName("status")
+ public String status;
+
+ @SerializedName("body")
+ public Map> body;
+ }
+
+ public static class ApiIdentity {
+ @SerializedName("u")
+ public String currentUid;
+
+ @SerializedName("p")
+ public String previousUid;
+
+ @SerializedName("r")
+ public Long refreshFromSeconds;
+
+ @SerializedName("e")
+ public String error;
+ }
+
+ public static class MappedIdentity {
+ public MappedIdentity(String currentUid, String previousUid, Instant refreshFrom) {
+ this.currentUid = currentUid;
+ this.previousUid = previousUid;
+ this.refreshFrom = refreshFrom;
+ }
+
+ public MappedIdentity(ApiIdentity apiIdentity) {
+ this(apiIdentity.currentUid, apiIdentity.previousUid, Instant.ofEpochSecond(apiIdentity.refreshFromSeconds));
+ }
+
+ private final String currentUid;
+ private final String previousUid;
+ private final Instant refreshFrom;
+
+ public String getCurrentRawUid() {
+ return currentUid;
+ }
+
+ public String getPreviousRawUid() {
+ return previousUid;
+ }
+
+ public Instant getRefreshFrom() {
+ return refreshFrom;
+ }
+ }
+
+ public static class UnmappedIdentity {
+ public UnmappedIdentity(String reason)
+ {
+ this.reason = UnmappedIdentityReason.fromString(reason);
+ this.rawReason = reason;
+ }
+
+ public UnmappedIdentityReason getReason() {
+ return reason;
+ }
+
+ public String getRawReason() {
+ return rawReason;
+ }
+
+ private final UnmappedIdentityReason reason;
+
+ private final String rawReason;
+ }
+
+ public HashMap getMappedIdentities() {
+ return new HashMap<>(mappedIdentities);
+ }
+
+ public HashMap getUnmappedIdentities() {
+ return new HashMap<>(unmappedIdentities);
+ }
+
+ private final String status;
+ private final HashMap mappedIdentities = new HashMap<>();
+ private final HashMap unmappedIdentities = new HashMap<>();
+}
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;
+ }
+}
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..7c59ad1
--- /dev/null
+++ b/src/main/java/com/uid2/client/UnmappedIdentityReason.java
@@ -0,0 +1,19 @@
+package com.uid2.client;
+
+
+public enum UnmappedIdentityReason {
+ OPTOUT,
+ INVALID_IDENTIFIER,
+ UNKNOWN;
+
+ public static UnmappedIdentityReason fromString(String reason) {
+ if (reason.equals("optout")) {
+ return OPTOUT;
+ }
+ if (reason.equals("invalid identifier")) {
+ return INVALID_IDENTIFIER;
+ }
+
+ 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
new file mode 100644
index 0000000..17b8a51
--- /dev/null
+++ b/src/test/java/com/uid2/client/IdentityMapV3IntegrationTests.java
@@ -0,0 +1,286 @@
+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.time.Instant;
+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"));
+
+ 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(mappedEmail, optedOutEmail2, optedOutEmail));
+ Response response = new Response(identityMapInput);
+
+ response.assertMapped(mappedEmail);
+ response.assertMapped(optedOutEmail2);
+
+ response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutEmail);
+ }
+
+ @Test
+ public void identityMapNothingUnmapped() {
+ IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Arrays.asList(mappedEmail, optedOutEmail2));
+ Response response = new Response(identityMapInput);
+
+ response.assertMapped(mappedEmail);
+ response.assertMapped(optedOutEmail2);
+ }
+
+ @Test
+ public void identityMapNothingMapped() {
+ IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Collections.singletonList(optedOutEmail));
+ Response response = new Response(identityMapInput);
+
+ response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutEmail);
+ }
+
+
+ @Test
+ public void identityMapInvalidEmail() {
+ assertThrows(IllegalArgumentException.class,
+ () -> IdentityMapV3Input.fromEmails(Arrays.asList(mappedEmail, "this is not an email")));
+ }
+
+ @Test
+ public void identityMapInvalidPhone() {
+ assertThrows(IllegalArgumentException.class,
+ () -> IdentityMapV3Input.fromPhones(Arrays.asList(mappedPhone, "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(UnmappedIdentityReason.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(UnmappedIdentityReason.INVALID_IDENTIFIER, "this is not a hashed phone");
+ }
+
+ @Test
+ public void identityMapHashedEmails() {
+ 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);
+
+ response.assertMapped(hashedEmail1);
+ response.assertMapped(hashedEmail2);
+
+ response.assertUnmapped(UnmappedIdentityReason.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").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());
+ }
+
+
+ @Test
+ public void identityMapDuplicateHashedEmails() {
+ IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromHashedEmails(Arrays.asList(mappedEmailHash, mappedEmailHash, optedOutEmailHash, optedOutEmailHash));
+ Response response = new Response(identityMapInput);
+
+ response.assertMapped(mappedEmailHash);
+
+ response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutEmailHash);
+ }
+
+ @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(mappedPhone, mappedPhone2, optedOutPhone));
+ Response response = new Response(identityMapInput);
+
+ response.assertMapped(mappedPhone);
+ response.assertMapped(mappedPhone2);
+
+ response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutPhone);
+ }
+
+ @Test
+ public void identityMapHashedPhones() {
+ 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);
+
+ response.assertMapped(hashedPhone1);
+ response.assertMapped(hashedPhone2);
+
+ response.assertUnmapped(UnmappedIdentityReason.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(UnmappedIdentityReason.OPTOUT, optedOutEmail);
+ response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutEmailHash);
+ response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutPhone);
+ response.assertUnmapped(UnmappedIdentityReason.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(UnmappedIdentityReason.OPTOUT, optedOutPhone);
+ response.assertUnmapped(UnmappedIdentityReason.OPTOUT, optedOutEmailHash);
+ }
+
+
+ class Response {
+ Response(IdentityMapV3Input identityMapInput) {
+ identityMapResponse = identityMapClient.generateIdentityMap(identityMapInput);
+ }
+
+ void assertMapped(String dii) {
+ IdentityMapV3Response.MappedIdentity mappedIdentity = identityMapResponse.getMappedIdentities().get(dii);
+ assertNotNull(mappedIdentity);
+ 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 unmappedIdentityReason = identityMapResponse.getUnmappedIdentities().get(dii);
+ assertNull(unmappedIdentityReason);
+ }
+
+ void assertUnmapped(UnmappedIdentityReason reason, String dii) {
+ HashMap unmappedIdentities = identityMapResponse.getUnmappedIdentities();
+ IdentityMapV3Response.UnmappedIdentity unmappedIdentity = unmappedIdentities.get(dii);
+ assertEquals(reason, unmappedIdentity.getReason());
+
+ IdentityMapV3Response.MappedIdentity mappedIdentity = identityMapResponse.getMappedIdentities().get(dii);
+ assertNull(mappedIdentity);
+ }
+
+
+ private final IdentityMapV3Response identityMapResponse;
+ }
+
+ @Test
+ public void identityMapEmailsUseOwnHttp() {
+ IdentityMapV3Input identityMapInput = IdentityMapV3Input.fromEmails(Arrays.asList(mappedEmail, optedOutEmail2, optedOutEmail));
+
+ 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 + "/v3/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(mappedEmail);
+ assertFalse(mappedIdentity.getCurrentRawUid().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));
+ }
+}
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..bba321c
--- /dev/null
+++ b/src/test/java/com/uid2/client/IdentityMapV3ResponseTest.java
@@ -0,0 +1,170 @@
+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 identifier"), input);
+ assertTrue(response.isSuccess());
+
+ IdentityMapV3Response.UnmappedIdentity unmappedIdentity = response.getUnmappedIdentities().get(SOME_EMAIL);
+ assertEquals(UnmappedIdentityReason.INVALID_IDENTIFIER, unmappedIdentity.getReason());
+ assertEquals("invalid identifier", unmappedIdentity.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