|
15 | 15 | # See the License for the specific language governing permissions and
|
16 | 16 | # limitations under the License.
|
17 | 17 | #
|
18 |
| -require "spec_helper" |
19 |
| -require "optimizely/cmab/cmab_client" |
| 18 | +require 'spec_helper' |
| 19 | +require 'optimizely/cmab/cmab_client' |
20 | 20 |
|
21 | 21 | describe Optimizely::CmabClient do
|
22 |
| - let(:mock_http_client) { double("http_client") } |
23 |
| - let(:mock_logger) { double("logger") } |
| 22 | + let(:mock_http_client) { double('http_client') } |
| 23 | + let(:mock_logger) { double('logger') } |
24 | 24 | let(:retry_config) { Optimizely::CmabRetryConfig.new(max_retries: 3, initial_backoff: 0.01, max_backoff: 1, backoff_multiplier: 2) }
|
25 | 25 | let(:client) { described_class.new(http_client: mock_http_client, logger: mock_logger, retry_config: nil) }
|
26 |
| - let(:rule_id) { "test_rule" } |
27 |
| - let(:user_id) { "user123" } |
28 |
| - let(:attributes) { { 'attr1': "value1", 'attr2': "value2" } } |
29 |
| - let(:cmab_uuid) { "uuid-1234" } |
| 26 | + let(:rule_id) { 'test_rule' } |
| 27 | + let(:user_id) { 'user123' } |
| 28 | + let(:attributes) { {'attr1': 'value1', 'attr2': 'value2'} } |
| 29 | + let(:cmab_uuid) { 'uuid-1234' } |
30 | 30 | let(:expected_url) { "https://prediction.cmab.optimizely.com/predict/#{rule_id}" }
|
31 | 31 | let(:expected_body) do
|
32 | 32 | {
|
33 | 33 | instances: [{
|
34 | 34 | visitorId: user_id,
|
35 | 35 | experimentId: rule_id,
|
36 | 36 | attributes: [
|
37 |
| - { id: "attr1", value: "value1", type: "custom_attribute" }, |
38 |
| - { id: "attr2", value: "value2", type: "custom_attribute" }, |
| 37 | + {id: 'attr1', value: 'value1', type: 'custom_attribute'}, |
| 38 | + {id: 'attr2', value: 'value2', type: 'custom_attribute'}, |
39 | 39 | ],
|
40 |
| - cmabUUID: cmab_uuid, |
41 |
| - }], |
| 40 | + cmabUUID: cmab_uuid |
| 41 | + }] |
42 | 42 | }
|
43 | 43 | end
|
44 |
| - let(:expected_headers) { { "Content-Type" => "application/json" } } |
| 44 | + let(:expected_headers) { {'Content-Type' => 'application/json'} } |
45 | 45 |
|
46 |
| - it "should return the variation id on success without retrying" do |
47 |
| - mock_response = double("response", status_code: 200, json: { "predictions" => [{ 'variationId': "abc123" }] }) |
| 46 | + it 'should return the variation id on success without retrying' do |
| 47 | + mock_response = double('response', status_code: 200, json: {'predictions' => [{'variationId': 'abc123'}]}) |
48 | 48 | allow(mock_http_client).to receive(:post).and_return(mock_response)
|
49 | 49 | result = client.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
|
50 |
| - expect(result).to eq("abc123") |
| 50 | + expect(result).to eq('abc123') |
51 | 51 | expect(mock_http_client).to have_received(:post).with(
|
52 | 52 | expected_url,
|
53 | 53 | hash_including(
|
54 | 54 | json: expected_body,
|
55 | 55 | headers: expected_headers,
|
56 |
| - timeout: 10, |
| 56 | + timeout: 10 |
57 | 57 | )
|
58 | 58 | )
|
59 | 59 | end
|
60 | 60 |
|
61 |
| - it "should return HTTP exception without retrying" do |
62 |
| - allow(mock_http_client).to receive(:post).and_raise(StandardError.new("Connection error")) |
| 61 | + it 'should return HTTP exception without retrying' do |
| 62 | + allow(mock_http_client).to receive(:post).and_raise(StandardError.new('Connection error')) |
63 | 63 | allow(mock_logger).to receive(:error)
|
64 | 64 | expect do
|
65 | 65 | client.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
|
66 | 66 | end.to raise_error(Optimizely::CmabFetchError, /Connection error/)
|
67 | 67 | expect(mock_http_client).to have_received(:post).once
|
68 |
| - expect(mock_logger).to have_received(:error).with(a_string_including("Connection error")) |
| 68 | + expect(mock_logger).to have_received(:error).with(a_string_including('Connection error')) |
69 | 69 | end
|
70 | 70 |
|
71 |
| - it "should not return 200 status without retrying" do |
72 |
| - mock_response = double("response", status_code: 500, json: nil) |
| 71 | + it 'should not return 200 status without retrying' do |
| 72 | + mock_response = double('response', status_code: 500, json: nil) |
73 | 73 | allow(mock_http_client).to receive(:post).and_return(mock_response)
|
74 | 74 |
|
75 | 75 | expect do
|
|
81 | 81 | hash_including(
|
82 | 82 | json: expected_body,
|
83 | 83 | headers: expected_headers,
|
84 |
| - timeout: 10, |
| 84 | + timeout: 10 |
85 | 85 | )
|
86 | 86 | )
|
87 |
| - expect(mock_logger).to have_received(:error).with(a_string_including("500")) |
| 87 | + expect(mock_logger).to have_received(:error).with(a_string_including('500')) |
88 | 88 | end
|
89 | 89 |
|
90 |
| - it "should return invalid json without retrying" do |
91 |
| - mock_response = double("response", status_code: 200) |
92 |
| - allow(mock_response).to receive(:json).and_raise(JSON::ParserError.new("Expecting value")) |
| 90 | + it 'should return invalid json without retrying' do |
| 91 | + mock_response = double('response', status_code: 200) |
| 92 | + allow(mock_response).to receive(:json).and_raise(JSON::ParserError.new('Expecting value')) |
93 | 93 | allow(mock_http_client).to receive(:post).and_return(mock_response)
|
94 | 94 |
|
95 | 95 | expect do
|
|
101 | 101 | hash_including(
|
102 | 102 | json: expected_body,
|
103 | 103 | headers: expected_headers,
|
104 |
| - timeout: 10, |
| 104 | + timeout: 10 |
105 | 105 | )
|
106 | 106 | )
|
107 |
| - expect(mock_logger).to have_received(:error).with(a_string_including("Invalid CMAB fetch response")) |
| 107 | + expect(mock_logger).to have_received(:error).with(a_string_including('Invalid CMAB fetch response')) |
108 | 108 | end
|
109 | 109 |
|
110 |
| - it "should return invalid response structure without retrying" do |
111 |
| - mock_response = double("response", status_code: 200, json: { "no_predictions" => [] }) |
| 110 | + it 'should return invalid response structure without retrying' do |
| 111 | + mock_response = double('response', status_code: 200, json: {'no_predictions' => []}) |
112 | 112 | allow(mock_http_client).to receive(:post).and_return(mock_response)
|
113 | 113 |
|
114 | 114 | expect do
|
|
120 | 120 | hash_including(
|
121 | 121 | json: expected_body,
|
122 | 122 | headers: expected_headers,
|
123 |
| - timeout: 10, |
| 123 | + timeout: 10 |
124 | 124 | )
|
125 | 125 | )
|
126 |
| - expect(mock_logger).to have_received(:error).with(a_string_including("Invalid CMAB fetch response")) |
| 126 | + expect(mock_logger).to have_received(:error).with(a_string_including('Invalid CMAB fetch response')) |
127 | 127 | end
|
128 | 128 |
|
129 |
| - it "should return the variation id on first try with retry config but no retry needed" do |
| 129 | + it 'should return the variation id on first try with retry config but no retry needed' do |
130 | 130 | client_with_retry = described_class.new(
|
131 | 131 | http_client: mock_http_client,
|
132 | 132 | logger: mock_logger,
|
133 |
| - retry_config: retry_config, |
| 133 | + retry_config: retry_config |
134 | 134 | )
|
135 | 135 |
|
136 | 136 | # Mock successful response
|
137 |
| - mock_response = double("response", status_code: 200, json: { "predictions" => [{ 'variationId': "abc123" }] }) |
| 137 | + mock_response = double('response', status_code: 200, json: {'predictions' => [{'variationId': 'abc123'}]}) |
138 | 138 | allow(mock_http_client).to receive(:post).and_return(mock_response)
|
139 | 139 | allow_any_instance_of(Object).to receive(:sleep)
|
140 | 140 |
|
141 | 141 | result = client_with_retry.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
|
142 | 142 |
|
143 |
| - expect(result).to eq("abc123") |
| 143 | + expect(result).to eq('abc123') |
144 | 144 | expect(mock_http_client).to have_received(:post).with(
|
145 | 145 | expected_url,
|
146 | 146 | hash_including(
|
147 | 147 | json: expected_body,
|
148 | 148 | headers: expected_headers,
|
149 |
| - timeout: 10, |
| 149 | + timeout: 10 |
150 | 150 | )
|
151 | 151 | ).once
|
152 | 152 | expect_any_instance_of(Object).not_to have_received(:sleep)
|
153 | 153 | end
|
154 | 154 |
|
155 |
| - it "should return the variation id on third try with retry config" do |
| 155 | + it 'should return the variation id on third try with retry config' do |
156 | 156 | client_with_retry = described_class.new(
|
157 | 157 | http_client: mock_http_client,
|
158 | 158 | logger: mock_logger,
|
159 |
| - retry_config: retry_config, |
| 159 | + retry_config: retry_config |
160 | 160 | )
|
161 | 161 |
|
162 | 162 | # Create failure and success responses
|
163 |
| - failure_response = double("response", status_code: 500) |
164 |
| - success_response = double("response", status_code: 200, json: { "predictions" => [{ 'variationId': "xyz456" }] }) |
| 163 | + failure_response = double('response', status_code: 500) |
| 164 | + success_response = double('response', status_code: 200, json: {'predictions' => [{'variationId': 'xyz456'}]}) |
165 | 165 |
|
166 | 166 | # First two calls fail, third succeeds
|
167 | 167 | call_sequence = [failure_response, failure_response, success_response]
|
|
172 | 172 |
|
173 | 173 | result = client_with_retry.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
|
174 | 174 |
|
175 |
| - expect(result).to eq("xyz456") |
| 175 | + expect(result).to eq('xyz456') |
176 | 176 | expect(mock_http_client).to have_received(:post).exactly(3).times
|
177 | 177 |
|
178 | 178 | # Verify all HTTP calls used correct parameters
|
|
181 | 181 | hash_including(
|
182 | 182 | json: expected_body,
|
183 | 183 | headers: expected_headers,
|
184 |
| - timeout: 10, |
| 184 | + timeout: 10 |
185 | 185 | )
|
186 | 186 | )
|
187 | 187 |
|
188 | 188 | # Verify retry logging
|
189 |
| - expect(mock_logger).to have_received(:info).with("Retrying CMAB request (attempt 1) after 0.01 seconds...") |
190 |
| - expect(mock_logger).to have_received(:info).with("Retrying CMAB request (attempt 2) after 0.02 seconds...") |
| 189 | + expect(mock_logger).to have_received(:info).with('Retrying CMAB request (attempt 1) after 0.01 seconds...') |
| 190 | + expect(mock_logger).to have_received(:info).with('Retrying CMAB request (attempt 2) after 0.02 seconds...') |
191 | 191 |
|
192 | 192 | # Verify sleep was called with correct backoff times
|
193 | 193 | expect_any_instance_of(Object).to have_received(:sleep).with(0.01)
|
194 | 194 | expect_any_instance_of(Object).to have_received(:sleep).with(0.02)
|
195 | 195 | end
|
196 | 196 |
|
197 |
| - it "should exhausts all retry attempts" do |
| 197 | + it 'should exhausts all retry attempts' do |
198 | 198 | client_with_retry = described_class.new(
|
199 | 199 | http_client: mock_http_client,
|
200 | 200 | logger: mock_logger,
|
201 |
| - retry_config: retry_config, |
| 201 | + retry_config: retry_config |
202 | 202 | )
|
203 | 203 |
|
204 | 204 | # Create failure response
|
205 |
| - failure_response = double("response", status_code: 500) |
| 205 | + failure_response = double('response', status_code: 500) |
206 | 206 |
|
207 | 207 | # All attempts fail
|
208 | 208 | allow(mock_http_client).to receive(:post).and_return(failure_response)
|
|
218 | 218 | expect(mock_http_client).to have_received(:post).exactly(4).times
|
219 | 219 |
|
220 | 220 | # Verify retry logging
|
221 |
| - expect(mock_logger).to have_received(:info).with("Retrying CMAB request (attempt 1) after 0.01 seconds...") |
222 |
| - expect(mock_logger).to have_received(:info).with("Retrying CMAB request (attempt 2) after 0.02 seconds...") |
223 |
| - expect(mock_logger).to have_received(:info).with("Retrying CMAB request (attempt 3) after 0.08 seconds...") |
| 221 | + expect(mock_logger).to have_received(:info).with('Retrying CMAB request (attempt 1) after 0.01 seconds...') |
| 222 | + expect(mock_logger).to have_received(:info).with('Retrying CMAB request (attempt 2) after 0.02 seconds...') |
| 223 | + expect(mock_logger).to have_received(:info).with('Retrying CMAB request (attempt 3) after 0.08 seconds...') |
224 | 224 |
|
225 | 225 | # Verify sleep was called for each retry
|
226 | 226 | expect_any_instance_of(Object).to have_received(:sleep).with(0.01)
|
227 | 227 | expect_any_instance_of(Object).to have_received(:sleep).with(0.02)
|
228 | 228 | expect_any_instance_of(Object).to have_received(:sleep).with(0.08)
|
229 | 229 |
|
230 | 230 | # Verify final error logging
|
231 |
| - expect(mock_logger).to have_received(:error).with(a_string_including("Max retries exceeded for CMAB request")) |
| 231 | + expect(mock_logger).to have_received(:error).with(a_string_including('Max retries exceeded for CMAB request')) |
232 | 232 | end
|
233 | 233 | end
|
0 commit comments