1+ package suite .operator ;
2+
3+ import app .AppsMap ;
4+ import app .component .Operator ;
5+ import com .fasterxml .jackson .databind .JsonNode ;
6+ import com .fasterxml .jackson .databind .ObjectMapper ;
7+ import com .uid2 .shared .util .Mapper ;
8+ import common .Const ;
9+ import common .EnvUtil ;
10+ import common .HttpClient ;
11+ import okhttp3 .*;
12+ import org .junit .jupiter .api .Test ;
13+
14+ import java .lang .reflect .Constructor ;
15+ import java .lang .reflect .Method ;
16+ import java .nio .ByteBuffer ;
17+ import java .nio .charset .StandardCharsets ;
18+ import java .security .SecureRandom ;
19+ import java .time .Instant ;
20+ import java .util .*;
21+
22+ import static org .assertj .core .api .Assertions .assertThat ;
23+
24+
25+ public class IdentityMapDirectHttpTest {
26+
27+ private static final ObjectMapper OBJECT_MAPPER = Mapper .getInstance ();
28+ private static final SecureRandom SECURE_RANDOM = new SecureRandom ();
29+ private static final String CLIENT_API_KEY = EnvUtil .getEnv (Const .Config .Operator .CLIENT_API_KEY );
30+ private static final String CLIENT_API_SECRET = EnvUtil .getEnv (Const .Config .Operator .CLIENT_API_SECRET );
31+ private static final int TIMESTAMP_LENGTH = 8 ;
32+
33+ private record V2Envelope (String envelope , byte [] nonce ) {}
34+
35+ /**
36+ * Test 1: JSON content type with base64 encoding (standard approach)
37+ */
38+ @ Test
39+ public void test1_JsonWithBase64 () throws Exception {
40+ System .out .println ("🧪 Test 1: JSON content type + Base64 encoding" );
41+
42+ Operator operator = getFirstOperator ();
43+ String jsonPayload = createTestPayload ();
44+
45+ // Create base64-encoded envelope
46+ V2Envelope envelope = createV2Envelope (jsonPayload , CLIENT_API_SECRET );
47+
48+ // Send with JSON content type
49+ JsonNode response = sendWithJsonContentType (operator , envelope .envelope (), envelope .nonce ());
50+
51+ validateSuccessResponse (response , "Test 1" );
52+ System .out .println ("✅ Test 1 completed successfully\n " );
53+ }
54+
55+ /**
56+ * Test 2: Octet-stream content type with base64 encoding
57+ */
58+ @ Test
59+ public void test2_OctetStreamWithBase64 () throws Exception {
60+ System .out .println ("🧪 Test 2: Octet-stream content type + Base64 encoding" );
61+
62+ Operator operator = getFirstOperator ();
63+ String jsonPayload = createTestPayload ();
64+
65+ // Create base64-encoded envelope
66+ V2Envelope envelope = createV2Envelope (jsonPayload , CLIENT_API_SECRET );
67+
68+ // Send with octet-stream content type
69+ JsonNode response = sendWithOctetStreamContentType (operator , envelope .envelope ().getBytes (StandardCharsets .UTF_8 ), envelope .nonce ());
70+
71+ validateSuccessResponse (response , "Test 2" );
72+ System .out .println ("✅ Test 2 completed successfully\n " );
73+ }
74+
75+ // Helper methods
76+
77+ private Operator getFirstOperator () {
78+ return AppsMap .getApps (Operator .class ).iterator ().next ();
79+ }
80+
81+ private String createTestPayload () throws Exception {
82+ Map <String , Object > payload = new HashMap <>();
83+ payload .
put (
"email" ,
List .
of (
"[email protected] " ,
"[email protected] " ));
84+ payload .put ("policy" , 1 );
85+
86+ String jsonPayload = OBJECT_MAPPER .writeValueAsString (payload );
87+ System .out .println ("Request payload: " + jsonPayload );
88+ return jsonPayload ;
89+ }
90+
91+ /**
92+ * Send request with JSON content type (string body)
93+ */
94+ private JsonNode sendWithJsonContentType (Operator operator , String body , byte [] nonce ) throws Exception {
95+ String encryptedResponse = HttpClient .post (
96+ operator .getBaseUrl () + "/v2/identity/map" ,
97+ body ,
98+ CLIENT_API_KEY
99+ );
100+
101+ return decryptV2Response (encryptedResponse , nonce );
102+ }
103+
104+ /**
105+ * Send request with octet-stream content type
106+ */
107+ private JsonNode sendWithOctetStreamContentType (Operator operator , byte [] bodyBytes , byte [] nonce ) throws Exception {
108+ Request request = new Request .Builder ()
109+ .url (operator .getBaseUrl () + "/v2/identity/map" )
110+ .addHeader ("Authorization" , "Bearer " + CLIENT_API_KEY )
111+ .post (RequestBody .create (bodyBytes , MediaType .get ("application/octet-stream" )))
112+ .build ();
113+
114+ String encryptedResponse = executeRequest (request );
115+ return decryptV2Response (encryptedResponse , nonce );
116+ }
117+
118+ /**
119+ * Execute HTTP request and return response body
120+ */
121+ private String executeRequest (Request request ) throws Exception {
122+ try (Response response = HttpClient .RAW_CLIENT .newCall (request ).execute ()) {
123+ if (!response .isSuccessful ()) {
124+ throw new RuntimeException ("HTTP request failed: " + response .code () + " " + response .message ());
125+ }
126+ return Objects .requireNonNull (response .body ()).string ();
127+ }
128+ }
129+
130+ /**
131+ * Creates encrypted V2 envelope with base64 encoding
132+ */
133+ private V2Envelope createV2Envelope (String payload , String secret ) throws Exception {
134+ // Create unencrypted envelope: timestamp + nonce + payload
135+ Instant timestamp = Instant .now ();
136+
137+ byte [] nonce = new byte [8 ];
138+ SECURE_RANDOM .nextBytes (nonce );
139+
140+ byte [] payloadBytes = payload .getBytes (StandardCharsets .UTF_8 );
141+
142+ ByteBuffer writer = ByteBuffer .allocate (TIMESTAMP_LENGTH + nonce .length + payloadBytes .length );
143+ writer .putLong (timestamp .toEpochMilli ());
144+ writer .put (nonce );
145+ writer .put (payloadBytes );
146+
147+ // Encrypt the envelope
148+ byte envelopeVersion = 1 ;
149+ byte [] encrypted = encryptGCM (writer .array (), Base64 .getDecoder ().decode (secret ));
150+
151+ ByteBuffer envelopeBuffer = ByteBuffer .allocate (1 + encrypted .length );
152+ envelopeBuffer .put (envelopeVersion );
153+ envelopeBuffer .put (encrypted );
154+
155+ return new V2Envelope (Base64 .getEncoder ().encodeToString (envelopeBuffer .array ()), nonce );
156+ }
157+
158+ /**
159+ * Decrypts V2 response using UID2 internal methods
160+ */
161+ private JsonNode decryptV2Response (String encryptedResponse , byte [] nonce ) throws Exception {
162+ Class <?> uid2HelperClass = Class .forName ("com.uid2.client.Uid2Helper" );
163+ Constructor <?> constructor = uid2HelperClass .getDeclaredConstructor (String .class );
164+ constructor .setAccessible (true );
165+ Object uid2Helper = constructor .newInstance (CLIENT_API_SECRET );
166+
167+ Method decryptMethod = uid2HelperClass .getDeclaredMethod ("decrypt" , String .class , byte [].class );
168+ decryptMethod .setAccessible (true );
169+ String decryptedResponse = (String ) decryptMethod .invoke (uid2Helper , encryptedResponse , nonce );
170+
171+ return OBJECT_MAPPER .readTree (decryptedResponse );
172+ }
173+
174+ /**
175+ * Encrypts data using GCM encryption via UID2 internal methods
176+ */
177+ private byte [] encryptGCM (byte [] data , byte [] secretBytes ) throws Exception {
178+ Class <?> encryptionClass = Class .forName ("com.uid2.client.Uid2Encryption" );
179+ Method encryptMethod = encryptionClass .getDeclaredMethod ("encryptGCM" , byte [].class , byte [].class , byte [].class );
180+ encryptMethod .setAccessible (true );
181+ return (byte []) encryptMethod .invoke (encryptionClass , data , null , secretBytes );
182+ }
183+
184+ /**
185+ * Validates successful response structure
186+ */
187+ private void validateSuccessResponse (JsonNode response , String testName ) {
188+ System .out .println (testName + " - Response status: " + response .get ("status" ).asText ());
189+
190+ // Validate successful response
191+ assertThat (response .get ("status" ).asText ()).isEqualTo ("success" );
192+ assertThat (response .has ("body" )).isTrue ();
193+
194+ JsonNode body = response .get ("body" );
195+ assertThat (body .has ("mapped" )).isTrue ();
196+
197+ JsonNode mapped = body .get ("mapped" );
198+ assertThat (mapped .isArray ()).isTrue ();
199+ assertThat (mapped .size ()).isEqualTo (2 );
200+
201+ // Validate each mapped identity has required fields
202+ for (JsonNode mappedIdentity : mapped ) {
203+ assertThat (mappedIdentity .has ("advertising_id" )).isTrue ();
204+ assertThat (mappedIdentity .has ("bucket_id" )).isTrue ();
205+ assertThat (mappedIdentity .get ("advertising_id" ).asText ()).isNotBlank ();
206+ assertThat (mappedIdentity .get ("bucket_id" ).asText ()).isNotBlank ();
207+ }
208+
209+ System .out .println (testName + " - Successfully mapped 2 identities" );
210+ }
211+ }
0 commit comments