Skip to content

Commit 25b7f24

Browse files
committed
Use webmock
1 parent 5cb9de7 commit 25b7f24

File tree

1 file changed

+55
-73
lines changed

1 file changed

+55
-73
lines changed

spec/cmab_client_spec.rb

Lines changed: 55 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@
1818
require 'spec_helper'
1919
require 'optimizely/logger'
2020
require 'optimizely/cmab/cmab_client'
21+
require 'webmock/rspec'
2122

2223
describe Optimizely::DefaultCmabClient do
2324
let(:spy_logger) { spy('logger') }
25+
let(:retry_config) { Optimizely::CmabRetryConfig.new(max_retries: 3, retry_delay: 0.01, max_backoff: 1, backoff_multiplier: 2) }
2426
let(:rule_id) { 'test_rule' }
2527
let(:user_id) { 'user123' }
2628
let(:attributes) { {'attr1': 'value1', 'attr2': 'value2'} }
2729
let(:cmab_uuid) { 'uuid-1234' }
2830
let(:expected_url) { "https://prediction.cmab.optimizely.com/predict/#{rule_id}" }
29-
let(:expected_body) do
31+
let(:expected_body_for_webmock) do
3032
{
3133
instances: [{
3234
visitorId: user_id,
@@ -37,170 +39,150 @@
3739
],
3840
cmabUUID: cmab_uuid
3941
}]
40-
}
42+
}.to_json
4143
end
4244
let(:expected_headers) { {'Content-Type' => 'application/json'} }
4345

4446
before do
4547
allow(Kernel).to receive(:sleep)
48+
WebMock.disable_net_connect!
4649
end
4750

4851
after do
4952
RSpec::Mocks.space.proxy_for(spy_logger).reset
53+
WebMock.reset!
54+
WebMock.allow_net_connect!
5055
end
5156

5257
context 'when client is configured without retries' do
53-
let(:mock_http_client) { double('http_client') }
54-
let(:client) { described_class.new(mock_http_client, nil, spy_logger) }
58+
let(:client) { described_class.new(nil, nil, spy_logger) }
5559

5660
it 'should return the variation id on success' do
57-
mock_response = double('response', status_code: 200, json: {'predictions' => [{'variationId' => 'abc123'}]})
58-
allow(mock_http_client).to receive(:post).and_return(mock_response)
61+
WebMock.stub_request(:post, expected_url)
62+
.with(body: expected_body_for_webmock, headers: expected_headers)
63+
.to_return(status: 200, body: {'predictions' => [{'variationId' => 'abc123'}]}.to_json, headers: {'Content-Type' => 'application/json'})
5964

6065
result = client.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
6166

6267
expect(result).to eq('abc123')
63-
expect(mock_http_client).to have_received(:post).with(
64-
expected_url,
65-
hash_including(
66-
json: expected_body,
67-
headers: expected_headers,
68-
timeout: 10
69-
)
70-
).once
68+
expect(WebMock).to have_requested(:post, expected_url)
69+
.with(body: expected_body_for_webmock, headers: expected_headers).once
7170
expect(Kernel).not_to have_received(:sleep)
7271
end
7372

7473
it 'should return HTTP exception' do
75-
allow(mock_http_client).to receive(:post).and_raise(StandardError.new('Connection error'))
74+
WebMock.stub_request(:post, expected_url)
75+
.with(body: expected_body_for_webmock, headers: expected_headers)
76+
.to_raise(StandardError.new('Connection error'))
7677

7778
expect do
7879
client.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
7980
end.to raise_error(Optimizely::CmabFetchError, /Connection error/)
8081

81-
expect(mock_http_client).to have_received(:post).once
82+
expect(WebMock).to have_requested(:post, expected_url)
83+
.with(body: expected_body_for_webmock, headers: expected_headers).once
8284
expect(spy_logger).to have_received(:log).with(Logger::ERROR, a_string_including('Connection error'))
8385
expect(Kernel).not_to have_received(:sleep)
8486
end
8587

8688
it 'should not return 200 status' do
87-
mock_response = double('response', status_code: 500, json: nil)
88-
allow(mock_http_client).to receive(:post).and_return(mock_response)
89+
WebMock.stub_request(:post, expected_url)
90+
.with(body: expected_body_for_webmock, headers: expected_headers)
91+
.to_return(status: 500)
8992

9093
expect do
9194
client.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
9295
end.to raise_error(Optimizely::CmabFetchError, /500/)
9396

94-
expect(mock_http_client).to have_received(:post).with(
95-
expected_url,
96-
hash_including(
97-
json: expected_body,
98-
headers: expected_headers,
99-
timeout: 10
100-
)
101-
).once
97+
expect(WebMock).to have_requested(:post, expected_url)
98+
.with(body: expected_body_for_webmock, headers: expected_headers).once
10299
expect(spy_logger).to have_received(:log).with(Logger::ERROR, a_string_including('500'))
103100
expect(Kernel).not_to have_received(:sleep)
104101
end
105102

106103
it 'should return invalid json' do
107-
mock_response = double('response', status_code: 200)
108-
allow(mock_response).to receive(:json).and_raise(JSON::ParserError.new('Expecting value'))
109-
allow(mock_http_client).to receive(:post).and_return(mock_response)
104+
WebMock.stub_request(:post, expected_url)
105+
.with(body: expected_body_for_webmock, headers: expected_headers)
106+
.to_return(status: 200, body: 'this is not json', headers: {'Content-Type' => 'text/plain'})
110107

111108
expect do
112109
client.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
113110
end.to raise_error(Optimizely::CmabInvalidResponseError, /Invalid CMAB fetch response/)
114111

115-
expect(mock_http_client).to have_received(:post).with(
116-
expected_url,
117-
hash_including(
118-
json: expected_body,
119-
headers: expected_headers,
120-
timeout: 10
121-
)
122-
).once
112+
expect(WebMock).to have_requested(:post, expected_url)
113+
.with(body: expected_body_for_webmock, headers: expected_headers).once
123114
expect(spy_logger).to have_received(:log).with(Logger::ERROR, a_string_including('Invalid CMAB fetch response'))
124115
expect(Kernel).not_to have_received(:sleep)
125116
end
126117

127118
it 'should return invalid response structure' do
128-
mock_response = double('response', status_code: 200, json: {'no_predictions' => []})
129-
allow(mock_http_client).to receive(:post).and_return(mock_response)
119+
WebMock.stub_request(:post, expected_url)
120+
.with(body: expected_body_for_webmock, headers: expected_headers)
121+
.to_return(status: 200, body: {'no_predictions' => []}.to_json, headers: {'Content-Type' => 'application/json'})
130122

131123
expect do
132124
client.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
133125
end.to raise_error(Optimizely::CmabInvalidResponseError, /Invalid CMAB fetch response/)
134126

135-
expect(mock_http_client).to have_received(:post).with(
136-
expected_url,
137-
hash_including(
138-
json: expected_body,
139-
headers: expected_headers,
140-
timeout: 10
141-
)
142-
).once
127+
expect(WebMock).to have_requested(:post, expected_url)
128+
.with(body: expected_body_for_webmock, headers: expected_headers).once
143129
expect(spy_logger).to have_received(:log).with(Logger::ERROR, a_string_including('Invalid CMAB fetch response'))
144130
expect(Kernel).not_to have_received(:sleep)
145131
end
146132
end
147133

148134
context 'when client is configured with retries' do
149-
let(:mock_http_client) { double('http_client') } # Fresh double for this context
150-
let(:retry_config) { Optimizely::CmabRetryConfig.new(max_retries: 3, retry_delay: 0.01, max_backoff: 1, backoff_multiplier: 2) }
151-
let(:client_with_retry) { described_class.new(mock_http_client, retry_config, spy_logger) }
135+
let(:client_with_retry) { described_class.new(nil, retry_config, spy_logger) }
152136

153-
it 'should return the variation id on first try with retry config but no retry needed' do
154-
mock_response = double('response', status_code: 200, json: {'predictions' => [{'variationId' => 'abc123'}]})
155-
allow(mock_http_client).to receive(:post).and_return(mock_response)
137+
it 'should return the variation id on first try' do
138+
WebMock.stub_request(:post, expected_url)
139+
.with(body: expected_body_for_webmock, headers: expected_headers)
140+
.to_return(status: 200, body: {'predictions' => [{'variationId' => 'abc123'}]}.to_json, headers: {'Content-Type' => 'application/json'})
156141

157142
result = client_with_retry.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
158143

159144
expect(result).to eq('abc123')
160-
expect(mock_http_client).to have_received(:post).with(
161-
expected_url,
162-
hash_including(
163-
json: expected_body,
164-
headers: expected_headers,
165-
timeout: 10
166-
)
167-
).once
145+
expect(WebMock).to have_requested(:post, expected_url)
146+
.with(body: expected_body_for_webmock, headers: expected_headers).once
168147
expect(Kernel).not_to have_received(:sleep)
169148
end
170149

171150
it 'should return the variation id on third try' do
172-
failure_response = double('response', status_code: 500)
173-
success_response = double('response', status_code: 200, json: {'predictions' => [{'variationId' => 'xyz456'}]})
174-
175-
# Use a sequence to control responses
176-
allow(mock_http_client).to receive(:post).and_return(failure_response, failure_response, success_response)
151+
WebMock.stub_request(:post, expected_url)
152+
.with(body: expected_body_for_webmock, headers: expected_headers)
153+
.to_return({status: 500},
154+
{status: 500},
155+
{status: 200, body: {'predictions' => [{'variationId' => 'xyz456'}]}.to_json, headers: {'Content-Type' => 'application/json'}})
177156

178157
result = client_with_retry.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
179158

180159
expect(result).to eq('xyz456')
181-
expect(mock_http_client).to have_received(:post).exactly(3).times
160+
expect(WebMock).to have_requested(:post, expected_url)
161+
.with(body: expected_body_for_webmock, headers: expected_headers).exactly(3).times
182162

183163
expect(spy_logger).to have_received(:log).with(Logger::INFO, 'Retrying CMAB request (attempt 1) after 0.01 seconds...').once
184164
expect(spy_logger).to have_received(:log).with(Logger::INFO, 'Retrying CMAB request (attempt 2) after 0.02 seconds...').once
185165
expect(spy_logger).not_to have_received(:log).with(Logger::INFO, a_string_including('Retrying CMAB request (attempt 3)'))
186166

187167
expect(Kernel).to have_received(:sleep).with(0.01).once
188168
expect(Kernel).to have_received(:sleep).with(0.02).once
189-
expect(Kernel).not_to have_received(:sleep).with(0.04)
190169
expect(Kernel).not_to have_received(:sleep).with(0.08)
191170
end
192171

193172
it 'should exhaust all retry attempts' do
194-
failure_response = double('response', status_code: 500)
195-
196-
# All attempts fail
197-
allow(mock_http_client).to receive(:post).and_return(failure_response, failure_response, failure_response, failure_response)
173+
WebMock.stub_request(:post, expected_url)
174+
.with(body: expected_body_for_webmock, headers: expected_headers)
175+
.to_return({status: 500},
176+
{status: 500},
177+
{status: 500},
178+
{status: 500})
198179

199180
expect do
200181
client_with_retry.fetch_decision(rule_id, user_id, attributes, cmab_uuid)
201182
end.to raise_error(Optimizely::CmabFetchError)
202183

203-
expect(mock_http_client).to have_received(:post).exactly(4).times
184+
expect(WebMock).to have_requested(:post, expected_url)
185+
.with(body: expected_body_for_webmock, headers: expected_headers).exactly(4).times
204186

205187
expect(spy_logger).to have_received(:log).with(Logger::INFO, 'Retrying CMAB request (attempt 1) after 0.01 seconds...').once
206188
expect(spy_logger).to have_received(:log).with(Logger::INFO, 'Retrying CMAB request (attempt 2) after 0.02 seconds...').once

0 commit comments

Comments
 (0)