webmock 3.14.0 → 3.19.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +124 -2
  3. data/README.md +46 -18
  4. data/lib/webmock/api.rb +2 -0
  5. data/lib/webmock/assertion_failure.rb +2 -0
  6. data/lib/webmock/callback_registry.rb +2 -0
  7. data/lib/webmock/config.rb +2 -0
  8. data/lib/webmock/cucumber.rb +2 -0
  9. data/lib/webmock/deprecation.rb +2 -0
  10. data/lib/webmock/errors.rb +2 -0
  11. data/lib/webmock/http_lib_adapters/async_http_client_adapter.rb +8 -1
  12. data/lib/webmock/http_lib_adapters/curb_adapter.rb +4 -2
  13. data/lib/webmock/http_lib_adapters/em_http_request_adapter.rb +14 -6
  14. data/lib/webmock/http_lib_adapters/excon_adapter.rb +2 -0
  15. data/lib/webmock/http_lib_adapters/http_lib_adapter.rb +2 -0
  16. data/lib/webmock/http_lib_adapters/http_lib_adapter_registry.rb +2 -0
  17. data/lib/webmock/http_lib_adapters/http_rb/client.rb +3 -3
  18. data/lib/webmock/http_lib_adapters/http_rb/request.rb +3 -1
  19. data/lib/webmock/http_lib_adapters/http_rb/response.rb +6 -1
  20. data/lib/webmock/http_lib_adapters/http_rb/streamer.rb +6 -2
  21. data/lib/webmock/http_lib_adapters/http_rb/webmock.rb +8 -2
  22. data/lib/webmock/http_lib_adapters/http_rb_adapter.rb +2 -0
  23. data/lib/webmock/http_lib_adapters/httpclient_adapter.rb +2 -0
  24. data/lib/webmock/http_lib_adapters/manticore_adapter.rb +2 -0
  25. data/lib/webmock/http_lib_adapters/net_http.rb +24 -115
  26. data/lib/webmock/http_lib_adapters/net_http_response.rb +2 -0
  27. data/lib/webmock/http_lib_adapters/patron_adapter.rb +2 -0
  28. data/lib/webmock/http_lib_adapters/typhoeus_hydra_adapter.rb +2 -0
  29. data/lib/webmock/matchers/any_arg_matcher.rb +2 -0
  30. data/lib/webmock/matchers/hash_argument_matcher.rb +2 -0
  31. data/lib/webmock/matchers/hash_excluding_matcher.rb +2 -0
  32. data/lib/webmock/matchers/hash_including_matcher.rb +2 -0
  33. data/lib/webmock/minitest.rb +2 -0
  34. data/lib/webmock/rack_response.rb +2 -0
  35. data/lib/webmock/request_body_diff.rb +2 -0
  36. data/lib/webmock/request_execution_verifier.rb +2 -0
  37. data/lib/webmock/request_pattern.rb +26 -8
  38. data/lib/webmock/request_registry.rb +2 -0
  39. data/lib/webmock/request_signature.rb +4 -2
  40. data/lib/webmock/request_signature_snippet.rb +2 -0
  41. data/lib/webmock/request_stub.rb +34 -0
  42. data/lib/webmock/response.rb +10 -8
  43. data/lib/webmock/responses_sequence.rb +2 -0
  44. data/lib/webmock/rspec/matchers/request_pattern_matcher.rb +2 -0
  45. data/lib/webmock/rspec/matchers/webmock_matcher.rb +2 -0
  46. data/lib/webmock/rspec/matchers.rb +2 -0
  47. data/lib/webmock/rspec.rb +2 -0
  48. data/lib/webmock/stub_registry.rb +2 -0
  49. data/lib/webmock/stub_request_snippet.rb +2 -0
  50. data/lib/webmock/test_unit.rb +2 -0
  51. data/lib/webmock/util/hash_counter.rb +12 -6
  52. data/lib/webmock/util/hash_keys_stringifier.rb +2 -0
  53. data/lib/webmock/util/hash_validator.rb +2 -0
  54. data/lib/webmock/util/headers.rb +22 -8
  55. data/lib/webmock/util/json.rb +2 -0
  56. data/lib/webmock/util/query_mapper.rb +2 -0
  57. data/lib/webmock/util/uri.rb +3 -1
  58. data/lib/webmock/util/values_stringifier.rb +2 -0
  59. data/lib/webmock/util/version_checker.rb +7 -5
  60. data/lib/webmock/version.rb +3 -1
  61. data/lib/webmock/webmock.rb +12 -0
  62. data/lib/webmock.rb +2 -0
  63. metadata +51 -183
  64. data/.gemtest +0 -0
  65. data/.github/workflows/CI.yml +0 -37
  66. data/.gitignore +0 -34
  67. data/.rspec-tm +0 -2
  68. data/Gemfile +0 -9
  69. data/Rakefile +0 -38
  70. data/minitest/test_helper.rb +0 -34
  71. data/minitest/test_webmock.rb +0 -9
  72. data/minitest/webmock_spec.rb +0 -60
  73. data/spec/acceptance/async_http_client/async_http_client_spec.rb +0 -375
  74. data/spec/acceptance/async_http_client/async_http_client_spec_helper.rb +0 -73
  75. data/spec/acceptance/curb/curb_spec.rb +0 -499
  76. data/spec/acceptance/curb/curb_spec_helper.rb +0 -147
  77. data/spec/acceptance/em_http_request/em_http_request_spec.rb +0 -462
  78. data/spec/acceptance/em_http_request/em_http_request_spec_helper.rb +0 -77
  79. data/spec/acceptance/excon/excon_spec.rb +0 -77
  80. data/spec/acceptance/excon/excon_spec_helper.rb +0 -52
  81. data/spec/acceptance/http_rb/http_rb_spec.rb +0 -93
  82. data/spec/acceptance/http_rb/http_rb_spec_helper.rb +0 -54
  83. data/spec/acceptance/httpclient/httpclient_spec.rb +0 -217
  84. data/spec/acceptance/httpclient/httpclient_spec_helper.rb +0 -57
  85. data/spec/acceptance/manticore/manticore_spec.rb +0 -107
  86. data/spec/acceptance/manticore/manticore_spec_helper.rb +0 -35
  87. data/spec/acceptance/net_http/net_http_shared.rb +0 -153
  88. data/spec/acceptance/net_http/net_http_spec.rb +0 -369
  89. data/spec/acceptance/net_http/net_http_spec_helper.rb +0 -64
  90. data/spec/acceptance/net_http/real_net_http_spec.rb +0 -20
  91. data/spec/acceptance/patron/patron_spec.rb +0 -125
  92. data/spec/acceptance/patron/patron_spec_helper.rb +0 -54
  93. data/spec/acceptance/shared/allowing_and_disabling_net_connect.rb +0 -313
  94. data/spec/acceptance/shared/callbacks.rb +0 -148
  95. data/spec/acceptance/shared/complex_cross_concern_behaviors.rb +0 -36
  96. data/spec/acceptance/shared/enabling_and_disabling_webmock.rb +0 -95
  97. data/spec/acceptance/shared/precedence_of_stubs.rb +0 -15
  98. data/spec/acceptance/shared/request_expectations.rb +0 -930
  99. data/spec/acceptance/shared/returning_declared_responses.rb +0 -409
  100. data/spec/acceptance/shared/stubbing_requests.rb +0 -678
  101. data/spec/acceptance/typhoeus/typhoeus_hydra_spec.rb +0 -135
  102. data/spec/acceptance/typhoeus/typhoeus_hydra_spec_helper.rb +0 -60
  103. data/spec/acceptance/webmock_shared.rb +0 -41
  104. data/spec/fixtures/test.txt +0 -1
  105. data/spec/quality_spec.rb +0 -84
  106. data/spec/spec_helper.rb +0 -48
  107. data/spec/support/example_curl_output.txt +0 -22
  108. data/spec/support/failures.rb +0 -9
  109. data/spec/support/my_rack_app.rb +0 -53
  110. data/spec/support/network_connection.rb +0 -19
  111. data/spec/support/webmock_server.rb +0 -70
  112. data/spec/unit/api_spec.rb +0 -175
  113. data/spec/unit/errors_spec.rb +0 -129
  114. data/spec/unit/http_lib_adapters/http_lib_adapter_registry_spec.rb +0 -17
  115. data/spec/unit/http_lib_adapters/http_lib_adapter_spec.rb +0 -12
  116. data/spec/unit/matchers/hash_excluding_matcher_spec.rb +0 -61
  117. data/spec/unit/matchers/hash_including_matcher_spec.rb +0 -87
  118. data/spec/unit/rack_response_spec.rb +0 -112
  119. data/spec/unit/request_body_diff_spec.rb +0 -90
  120. data/spec/unit/request_execution_verifier_spec.rb +0 -208
  121. data/spec/unit/request_pattern_spec.rb +0 -736
  122. data/spec/unit/request_registry_spec.rb +0 -95
  123. data/spec/unit/request_signature_snippet_spec.rb +0 -89
  124. data/spec/unit/request_signature_spec.rb +0 -155
  125. data/spec/unit/request_stub_spec.rb +0 -199
  126. data/spec/unit/response_spec.rb +0 -286
  127. data/spec/unit/stub_registry_spec.rb +0 -103
  128. data/spec/unit/stub_request_snippet_spec.rb +0 -115
  129. data/spec/unit/util/hash_counter_spec.rb +0 -39
  130. data/spec/unit/util/hash_keys_stringifier_spec.rb +0 -27
  131. data/spec/unit/util/headers_spec.rb +0 -28
  132. data/spec/unit/util/json_spec.rb +0 -33
  133. data/spec/unit/util/query_mapper_spec.rb +0 -157
  134. data/spec/unit/util/uri_spec.rb +0 -371
  135. data/spec/unit/util/version_checker_spec.rb +0 -65
  136. data/spec/unit/webmock_spec.rb +0 -60
  137. data/test/http_request.rb +0 -24
  138. data/test/shared_test.rb +0 -108
  139. data/test/test_helper.rb +0 -23
  140. data/test/test_webmock.rb +0 -12
  141. data/webmock.gemspec +0 -54
@@ -1,60 +0,0 @@
1
- require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
-
3
- describe "Webmock" do
4
- include HttpRequestTestHelper
5
-
6
- before do
7
- @stub_http = stub_http_request(:any, "http://www.example.com")
8
- @stub_https = stub_http_request(:any, "https://www.example.com")
9
- end
10
-
11
- it "should update assertions count" do
12
- assert_equal 0, assertions
13
- http_request(:get, "http://www.example.com/")
14
-
15
- assert_requested(@stub_http)
16
- assert_equal 2, assertions
17
-
18
- assert_not_requested(:post, "http://www.example.com")
19
- assert_equal 4, assertions
20
- end
21
-
22
- it "should raise error on non stubbed request" do
23
- lambda { http_request(:get, "http://www.example.net/") }.must_raise(WebMock::NetConnectNotAllowedError)
24
- end
25
-
26
- it "should verify that expected request occured" do
27
- http_request(:get, "http://www.example.com/")
28
- assert_requested(:get, "http://www.example.com", times: 1)
29
- assert_requested(:get, "http://www.example.com")
30
- end
31
-
32
- it "should verify that expected http stub occured" do
33
- http_request(:get, "http://www.example.com/")
34
- assert_requested(@stub_http, times: 1)
35
- assert_requested(@stub_http)
36
- end
37
-
38
- it "should verify that expected https stub occured" do
39
- http_request(:get, "https://www.example.com/")
40
- http_request(:get, "https://www.example.com/")
41
- assert_requested(@stub_https, times: 2)
42
- end
43
-
44
- it "should verify that expect request didn't occur" do
45
- expected_message = "The request GET http://www.example.com/ was expected to execute 1 time but it executed 0 times"
46
- expected_message += "\n\nThe following requests were made:\n\nNo requests were made.\n============================================================"
47
- assert_fail(expected_message) do
48
- assert_requested(:get, "http://www.example.com")
49
- end
50
- end
51
-
52
- it "should verify that expect stub didn't occur" do
53
- expected_message = "The request ANY http://www.example.com/ was expected to execute 1 time but it executed 0 times"
54
- expected_message += "\n\nThe following requests were made:\n\nNo requests were made.\n============================================================"
55
- assert_fail(expected_message) do
56
- assert_requested(@stub_http)
57
- end
58
- end
59
- end
60
-
@@ -1,375 +0,0 @@
1
- # encoding: utf-8
2
- require 'spec_helper'
3
- require 'acceptance/webmock_shared'
4
- require_relative './async_http_client_spec_helper'
5
-
6
- require 'protocol/http/body/file'
7
-
8
- Async.logger.debug! if ENV['ASYNC_LOGGER_DEBUG']
9
-
10
- unless RUBY_PLATFORM =~ /java/
11
- describe 'Async::HTTP::Client' do
12
- include AsyncHttpClientSpecHelper
13
-
14
- include_context "with WebMock",
15
- :no_status_message,
16
- :no_url_auth,
17
- :no_content_length_header
18
-
19
- it 'works' do
20
- stub_request(:get, 'http://www.example.com')
21
- expect(make_request(:get, 'http://www.example.com')).to eq(
22
- status: 200,
23
- headers: {},
24
- body: nil
25
- )
26
- end
27
-
28
- it 'works with request path' do
29
- stub_request(:get, 'http://www.example.com/foo')
30
- expect(make_request(:get, 'http://www.example.com/foo')).to eq(
31
- status: 200,
32
- headers: {},
33
- body: nil
34
- )
35
- end
36
-
37
- it 'works with request query' do
38
- stub_request(:get, 'http://www.example.com/').with(
39
- query: {
40
- 'foo' => 'bar'
41
- }
42
- )
43
- expect(make_request(:get, 'http://www.example.com/?foo=bar')).to eq(
44
- status: 200,
45
- headers: {},
46
- body: nil
47
- )
48
- end
49
-
50
- it 'works with request headers' do
51
- stub_request(:get, 'http://www.example.com').with(
52
- headers: {
53
- 'X-Token' => 'Token'
54
- }
55
- )
56
- expect(
57
- make_request :get, 'http://www.example.com',
58
- headers: {
59
- 'X-Token' => 'Token'
60
- }
61
- ).to eq(
62
- status: 200,
63
- headers: {},
64
- body: nil
65
- )
66
- end
67
-
68
- it 'works with request body as text' do
69
- stub_request(:post, 'http://www.example.com').with(
70
- body: 'x'*10_000
71
- )
72
- expect(
73
- make_request :post, 'http://www.example.com',
74
- body: 'x'*10_000
75
- ).to eq(
76
- status: 200,
77
- headers: {},
78
- body: nil
79
- )
80
- end
81
-
82
- it 'works with request body as file' do
83
- stub_request(:post, "www.example.com").with(
84
- body: File.read(__FILE__)
85
- )
86
- expect(
87
- make_request :post, "http://www.example.com",
88
- body: ::Protocol::HTTP::Body::File.open(__FILE__, block_size: 32)
89
- ).to eq(
90
- status: 200,
91
- headers: {},
92
- body: nil
93
- )
94
- end
95
-
96
- it 'works with response status' do
97
- stub_request(:get, 'http://www.example.com').to_return(
98
- status: 400
99
- )
100
- expect(make_request(:get, 'http://www.example.com')).to eq(
101
- status: 400,
102
- headers: {},
103
- body: nil
104
- )
105
- end
106
-
107
- it 'works with response headers' do
108
- stub_request(:get, 'http://www.example.com').to_return(
109
- headers: {
110
- 'X-Token' => 'TOKEN'
111
- }
112
- )
113
- expect(make_request(:get, 'http://www.example.com')).to eq(
114
- status: 200,
115
- headers: {
116
- 'x-token' => ['TOKEN']
117
- },
118
- body: nil
119
- )
120
- end
121
-
122
- it 'works with response body' do
123
- stub_request(:get, 'http://www.example.com').to_return(
124
- body: 'abc'
125
- )
126
- expect(make_request(:get, 'http://www.example.com')).to eq(
127
- status: 200,
128
- headers: {},
129
- body: 'abc'
130
- )
131
- end
132
-
133
- it 'works with to_timeout' do
134
- stub_request(:get, 'http://www.example.com').to_timeout
135
- expect { make_request(:get, 'http://www.example.com') }.to raise_error Async::TimeoutError
136
- end
137
-
138
- it 'does not invoke "after real request" callbacks for stubbed requests' do
139
- WebMock.allow_net_connect!
140
- stub_request(:get, 'http://www.example.com').to_return(body: 'abc')
141
-
142
- callback_invoked = false
143
- WebMock.after_request(real_requests_only: true) { |_| callback_invoked = true }
144
-
145
- make_request(:get, 'http://www.example.com')
146
- expect(callback_invoked).to eq(false)
147
- end
148
-
149
- it 'does invoke "after request" callbacks for stubbed requests' do
150
- WebMock.allow_net_connect!
151
- stub_request(:get, 'http://www.example.com').to_return(body: 'abc')
152
-
153
- callback_invoked = false
154
- WebMock.after_request(real_requests_only: false) { |_| callback_invoked = true }
155
-
156
- make_request(:get, 'http://www.example.com')
157
- expect(callback_invoked).to eq(true)
158
- end
159
-
160
- context 'scheme and protocol' do
161
- let(:default_response_headers) { {} }
162
-
163
- before do
164
- stub_request(
165
- :get, "#{scheme}://www.example.com"
166
- ).and_return(
167
- body: 'BODY'
168
- )
169
- end
170
-
171
- subject do
172
- make_request(:get, "#{scheme}://www.example.com", protocol: protocol)
173
- end
174
-
175
- shared_examples :common do
176
- specify do
177
- expect(subject).to eq(
178
- status: 200,
179
- headers: default_response_headers,
180
- body: 'BODY'
181
- )
182
- end
183
- end
184
-
185
- context 'http scheme' do
186
- let(:scheme) { 'http' }
187
-
188
- context 'default protocol' do
189
- let(:protocol) { nil }
190
-
191
- include_examples :common
192
- end
193
-
194
- context 'HTTP10 protocol' do
195
- let(:protocol) { Async::HTTP::Protocol::HTTP10 }
196
- let(:default_response_headers) { {"connection"=>["keep-alive"]} }
197
-
198
- include_examples :common
199
- end
200
-
201
- context 'HTTP11 protocol' do
202
- let(:protocol) { Async::HTTP::Protocol::HTTP11 }
203
-
204
- include_examples :common
205
- end
206
-
207
- context 'HTTP2 protocol' do
208
- let(:protocol) { Async::HTTP::Protocol::HTTP2 }
209
-
210
- include_examples :common
211
- end
212
- end
213
-
214
- context 'https scheme' do
215
- let(:scheme) { 'https' }
216
-
217
- context 'default protocol' do
218
- let(:protocol) { nil }
219
-
220
- include_examples :common
221
- end
222
-
223
- context 'HTTP10 protocol' do
224
- let(:protocol) { Async::HTTP::Protocol::HTTP10 }
225
- let(:default_response_headers) { {"connection"=>["keep-alive"]} }
226
-
227
- include_examples :common
228
- end
229
-
230
- context 'HTTP11 protocol' do
231
- let(:protocol) { Async::HTTP::Protocol::HTTP11 }
232
-
233
- include_examples :common
234
- end
235
-
236
- context 'HTTP2 protocol' do
237
- let(:protocol) { Async::HTTP::Protocol::HTTP2 }
238
-
239
- include_examples :common
240
- end
241
-
242
- context 'HTTPS protocol' do
243
- let(:protocol) { Async::HTTP::Protocol::HTTPS }
244
-
245
- include_examples :common
246
- end
247
- end
248
- end
249
-
250
- context 'multiple requests' do
251
- let(:endpoint) { Async::HTTP::Endpoint.parse('http://www.example.com') }
252
- let(:requests_count) { 3 }
253
-
254
- shared_examples :common do
255
- before do
256
- requests_count.times do |index|
257
- stub_request(
258
- :get, "http://www.example.com/foo#{index}"
259
- ).to_return(
260
- status: 200 + index,
261
- headers: {'X-Token' => "foo#{index}"},
262
- body: "FOO#{index}"
263
- )
264
- end
265
- end
266
-
267
- specify do
268
- expect(subject).to eq(
269
- 0 => {
270
- status: 200,
271
- headers: {'x-token' => ['foo0']},
272
- body: 'FOO0'
273
- },
274
- 1 => {
275
- status: 201,
276
- headers: {'x-token' => ['foo1']},
277
- body: 'FOO1'
278
- },
279
- 2 => {
280
- status: 202,
281
- headers: {'x-token' => ['foo2']},
282
- body: 'FOO2'
283
- }
284
- )
285
- end
286
- end
287
-
288
- context 'sequential' do
289
- subject do
290
- responses = {}
291
- Async do |task|
292
- Async::HTTP::Client.open(endpoint, protocol) do |client|
293
- requests_count.times do |index|
294
- response = client.get "/foo#{index}"
295
- responses[index] = response_to_hash(response)
296
- end
297
- end
298
- end
299
- responses
300
- end
301
-
302
- context 'HTTP1 protocol' do
303
- let(:protocol) { Async::HTTP::Protocol::HTTP1 }
304
-
305
- include_examples :common
306
- end
307
-
308
- context 'HTTP2 protocol' do
309
- let(:protocol) { Async::HTTP::Protocol::HTTP2 }
310
-
311
- include_examples :common
312
- end
313
- end
314
-
315
- context 'asynchronous' do
316
- subject do
317
- responses = {}
318
- Async do |task|
319
- Async::HTTP::Client.open(endpoint, protocol) do |client|
320
- tasks = requests_count.times.map do |index|
321
- task.async do
322
- response = client.get "/foo#{index}"
323
- responses[index] = response_to_hash(response)
324
- end
325
- end
326
-
327
- tasks.map(&:wait)
328
- end
329
- end
330
- responses
331
- end
332
-
333
- context 'HTTP1 protocol' do
334
- let(:protocol) { Async::HTTP::Protocol::HTTP1 }
335
-
336
- include_examples :common
337
- end
338
-
339
- context 'HTTP2 protocol' do
340
- let(:protocol) { Async::HTTP::Protocol::HTTP2 }
341
-
342
- include_examples :common
343
- end
344
- end
345
- end
346
-
347
- def make_request(method, url, protocol: nil, headers: {}, body: nil)
348
- Async do
349
- endpoint = Async::HTTP::Endpoint.parse(url)
350
-
351
- begin
352
- Async::HTTP::Client.open(endpoint, protocol || endpoint.protocol) do |client|
353
- response = client.send(
354
- method,
355
- endpoint.path,
356
- headers,
357
- body
358
- )
359
- response_to_hash(response)
360
- end
361
- rescue Async::TimeoutError => e
362
- e
363
- end
364
- end.wait
365
- end
366
-
367
- def response_to_hash(response)
368
- {
369
- status: response.status,
370
- headers: response.headers.to_h,
371
- body: response.read
372
- }
373
- end
374
- end
375
- end
@@ -1,73 +0,0 @@
1
- module AsyncHttpClientSpecHelper
2
- def http_request(method, url, options = {}, &block)
3
- endpoint = Async::HTTP::Endpoint.parse(url)
4
-
5
- path = endpoint.path
6
- path = path + "?" + options[:query] if options[:query]
7
-
8
- headers = (options[:headers] || {}).each_with_object([]) do |(k, v), o|
9
- Array(v).each do |v|
10
- o.push [k, v]
11
- end
12
- end
13
- headers.push(
14
- ['authorization', 'Basic ' + Base64.strict_encode64(options[:basic_auth].join(':'))]
15
- ) if options[:basic_auth]
16
-
17
- body = options[:body]
18
-
19
- Async do
20
- begin
21
- Async::HTTP::Client.open(endpoint) do |client|
22
- response = client.send(
23
- method,
24
- path,
25
- headers,
26
- body
27
- )
28
-
29
- OpenStruct.new(
30
- build_hash_response(response)
31
- )
32
- end
33
- rescue Exception => e
34
- e
35
- end
36
- end.wait
37
- end
38
-
39
- def client_timeout_exception_class
40
- Async::TimeoutError
41
- end
42
-
43
- def connection_refused_exception_class
44
- Errno::ECONNREFUSED
45
- end
46
-
47
- def http_library
48
- :async_http_client
49
- end
50
-
51
- private
52
-
53
- def build_hash_response(response)
54
- {
55
-
56
- status: response.status.to_s,
57
- message: Protocol::HTTP1::Reason::DESCRIPTIONS[response.status],
58
- headers: build_response_headers(response),
59
- body: response.read
60
- }
61
- end
62
-
63
- def build_response_headers(response)
64
- response.headers.each.each_with_object({}) do |(k, v), o|
65
- o[k] ||= []
66
- o[k] << v
67
- end.tap do |o|
68
- o.each do |k, v|
69
- o[k] = v.join(', ')
70
- end
71
- end
72
- end
73
- end