1+ /**
2+ * Copyright 2025, Optimizely
3+ *
4+ * Licensed under the Apache License, Version 2.0 (the "License");
5+ * you may not use this file except in compliance with the License.
6+ * You may obtain a copy of the License at
7+ *
8+ * https://www.apache.org/licenses/LICENSE-2.0
9+ *
10+ * Unless required by applicable law or agreed to in writing, software
11+ * distributed under the License is distributed on an "AS IS" BASIS,
12+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ * See the License for the specific language governing permissions and
14+ * limitations under the License.
15+ */
16+ package com .optimizely .ab .cmab ;
17+
18+ import java .io .IOException ;
19+ import java .util .HashMap ;
20+ import java .util .Map ;
21+ import java .util .concurrent .CompletableFuture ;
22+ import java .util .concurrent .ExecutionException ;
23+
24+ import org .apache .http .StatusLine ;
25+ import org .apache .http .client .methods .CloseableHttpResponse ;
26+ import org .apache .http .client .methods .HttpPost ;
27+ import org .apache .http .entity .StringEntity ;
28+ import org .apache .http .util .EntityUtils ;
29+ import static org .junit .Assert .assertEquals ;
30+ import static org .junit .Assert .assertTrue ;
31+ import static org .junit .Assert .fail ;
32+ import org .junit .Before ;
33+ import org .junit .Rule ;
34+ import org .junit .Test ;
35+ import org .mockito .ArgumentCaptor ;
36+ import static org .mockito .Matchers .any ;
37+ import static org .mockito .Mockito .mock ;
38+ import static org .mockito .Mockito .times ;
39+ import static org .mockito .Mockito .verify ;
40+ import static org .mockito .Mockito .when ;
41+
42+ import com .optimizely .ab .OptimizelyHttpClient ;
43+ import com .optimizely .ab .internal .LogbackVerifier ;
44+
45+ import ch .qos .logback .classic .Level ;
46+
47+ public class DefaultCmabClientTest {
48+
49+ private static final String validCmabResponse = "{\" predictions\" :[{\" variation_id\" :\" treatment_1\" }]}" ;
50+
51+ @ Rule
52+ public LogbackVerifier logbackVerifier = new LogbackVerifier ();
53+
54+ OptimizelyHttpClient mockHttpClient ;
55+ DefaultCmabClient cmabClient ;
56+
57+ @ Before
58+ public void setUp () throws Exception {
59+ setupHttpClient (200 );
60+ cmabClient = new DefaultCmabClient (mockHttpClient );
61+ }
62+
63+ private void setupHttpClient (int statusCode ) throws Exception {
64+ mockHttpClient = mock (OptimizelyHttpClient .class );
65+ CloseableHttpResponse httpResponse = mock (CloseableHttpResponse .class );
66+ StatusLine statusLine = mock (StatusLine .class );
67+
68+ when (statusLine .getStatusCode ()).thenReturn (statusCode );
69+ when (httpResponse .getStatusLine ()).thenReturn (statusLine );
70+ when (httpResponse .getEntity ()).thenReturn (new StringEntity (validCmabResponse ));
71+
72+ when (mockHttpClient .execute (any (HttpPost .class )))
73+ .thenReturn (httpResponse );
74+ }
75+
76+ @ Test
77+ public void testBuildRequestJson () throws Exception {
78+ String ruleId = "rule_123" ;
79+ String userId = "user_456" ;
80+ Map <String , Object > attributes = new HashMap <>();
81+ attributes .put ("browser" , "chrome" );
82+ attributes .put ("isMobile" , true );
83+ String cmabUuid = "uuid_789" ;
84+
85+ CompletableFuture <String > future = cmabClient .fetchDecision (ruleId , userId , attributes , cmabUuid );
86+ String result = future .get ();
87+
88+ assertEquals ("treatment_1" , result );
89+ verify (mockHttpClient , times (1 )).execute (any (HttpPost .class ));
90+
91+ ArgumentCaptor <HttpPost > request = ArgumentCaptor .forClass (HttpPost .class );
92+ verify (mockHttpClient ).execute (request .capture ());
93+ String actualRequestBody = EntityUtils .toString (request .getValue ().getEntity ());
94+
95+ assertTrue (actualRequestBody .contains ("\" visitorId\" :\" user_456\" " ));
96+ assertTrue (actualRequestBody .contains ("\" experimentId\" :\" rule_123\" " ));
97+ assertTrue (actualRequestBody .contains ("\" cmabUUID\" :\" uuid_789\" " ));
98+ assertTrue (actualRequestBody .contains ("\" browser\" " ));
99+ assertTrue (actualRequestBody .contains ("\" chrome\" " ));
100+ assertTrue (actualRequestBody .contains ("\" isMobile\" " ));
101+ assertTrue (actualRequestBody .contains ("true" ));
102+ }
103+
104+ @ Test
105+ public void returnVariationWhenStatusIs200 () throws Exception {
106+ String ruleId = "rule_123" ;
107+ String userId = "user_456" ;
108+ Map <String , Object > attributes = new HashMap <>();
109+ attributes .put ("segment" , "premium" );
110+ String cmabUuid = "uuid_789" ;
111+
112+ CompletableFuture <String > future = cmabClient .fetchDecision (ruleId , userId , attributes , cmabUuid );
113+ String result = future .get ();
114+
115+ assertEquals ("treatment_1" , result );
116+ verify (mockHttpClient , times (1 )).execute (any (HttpPost .class ));
117+ logbackVerifier .expectMessage (Level .INFO , "CMAB returned variation 'treatment_1' for rule 'rule_123' and user 'user_456'" );
118+ }
119+
120+ @ Test
121+ public void returnErrorWhenStatusIsNot200AndLogError () throws Exception {
122+ // Create new mock for 500 error
123+ CloseableHttpResponse httpResponse = mock (CloseableHttpResponse .class );
124+ StatusLine statusLine = mock (StatusLine .class );
125+ when (statusLine .getStatusCode ()).thenReturn (500 );
126+ when (httpResponse .getStatusLine ()).thenReturn (statusLine );
127+ when (httpResponse .getEntity ()).thenReturn (new StringEntity ("Server Error" ));
128+ when (mockHttpClient .execute (any (HttpPost .class ))).thenReturn (httpResponse );
129+
130+ String ruleId = "rule_123" ;
131+ String userId = "user_456" ;
132+ Map <String , Object > attributes = new HashMap <>();
133+ String cmabUuid = "uuid_789" ;
134+
135+ CompletableFuture <String > future = cmabClient .fetchDecision (ruleId , userId , attributes , cmabUuid );
136+
137+ try {
138+ future .get ();
139+ fail ("Expected ExecutionException" );
140+ } catch (ExecutionException e ) {
141+ assertTrue (e .getCause ().getMessage ().contains ("status: 500" ));
142+ }
143+
144+ verify (mockHttpClient , times (1 )).execute (any (HttpPost .class ));
145+ logbackVerifier .expectMessage (Level .ERROR , "CMAB decision fetch failed with status: 500 for user: user_456" );
146+ }
147+
148+ @ Test
149+ public void returnErrorWhenInvalidResponseAndLogError () throws Exception {
150+ CloseableHttpResponse httpResponse = mock (CloseableHttpResponse .class );
151+ StatusLine statusLine = mock (StatusLine .class );
152+ when (statusLine .getStatusCode ()).thenReturn (200 );
153+ when (httpResponse .getStatusLine ()).thenReturn (statusLine );
154+ when (httpResponse .getEntity ()).thenReturn (new StringEntity ("{\" predictions\" :[]}" ));
155+ when (mockHttpClient .execute (any (HttpPost .class ))).thenReturn (httpResponse );
156+
157+ String ruleId = "rule_123" ;
158+ String userId = "user_456" ;
159+ Map <String , Object > attributes = new HashMap <>();
160+ String cmabUuid = "uuid_789" ;
161+
162+ CompletableFuture <String > future = cmabClient .fetchDecision (ruleId , userId , attributes , cmabUuid );
163+
164+ try {
165+ future .get ();
166+ fail ("Expected ExecutionException" );
167+ } catch (ExecutionException e ) {
168+ assertEquals ("Invalid CMAB fetch response" , e .getCause ().getMessage ());
169+ }
170+
171+ verify (mockHttpClient , times (1 )).execute (any (HttpPost .class ));
172+ logbackVerifier .expectMessage (Level .ERROR , "Invalid CMAB fetch response for user: user_456" );
173+ }
174+
175+ @ Test
176+ public void testNoRetryWhenNoRetryConfig () throws Exception {
177+ when (mockHttpClient .execute (any (HttpPost .class )))
178+ .thenThrow (new IOException ("Network error" ));
179+
180+ String ruleId = "rule_123" ;
181+ String userId = "user_456" ;
182+ Map <String , Object > attributes = new HashMap <>();
183+ String cmabUuid = "uuid_789" ;
184+
185+ CompletableFuture <String > future = cmabClient .fetchDecision (ruleId , userId , attributes , cmabUuid );
186+
187+ try {
188+ future .get ();
189+ fail ("Expected ExecutionException" );
190+ } catch (ExecutionException e ) {
191+ assertTrue (e .getCause ().getMessage ().contains ("network error" ));
192+ }
193+
194+ verify (mockHttpClient , times (1 )).execute (any (HttpPost .class ));
195+ logbackVerifier .expectMessage (Level .ERROR , "CMAB decision fetch failed with status: network error for user: user_456" );
196+ }
197+ }
0 commit comments