Skip to content

Commit 50e2f7d

Browse files
add: implement unit tests for DefaultCmabClient with various scenarios and error handling
1 parent 8eb694e commit 50e2f7d

File tree

1 file changed

+197
-0
lines changed

1 file changed

+197
-0
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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

Comments
 (0)