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