|
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