webmock 3.5.1 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +15 -12
- data/CHANGELOG.md +211 -0
- data/README.md +92 -26
- data/Rakefile +0 -2
- data/lib/webmock.rb +1 -0
- data/lib/webmock/http_lib_adapters/async_http_client_adapter.rb +214 -0
- data/lib/webmock/http_lib_adapters/curb_adapter.rb +10 -1
- data/lib/webmock/http_lib_adapters/em_http_request_adapter.rb +1 -1
- data/lib/webmock/http_lib_adapters/excon_adapter.rb +3 -0
- data/lib/webmock/http_lib_adapters/http_rb/client.rb +4 -1
- data/lib/webmock/http_lib_adapters/http_rb/response.rb +11 -1
- data/lib/webmock/http_lib_adapters/http_rb/streamer.rb +2 -2
- data/lib/webmock/http_lib_adapters/httpclient_adapter.rb +23 -6
- data/lib/webmock/http_lib_adapters/manticore_adapter.rb +25 -14
- data/lib/webmock/http_lib_adapters/net_http.rb +35 -17
- data/lib/webmock/http_lib_adapters/patron_adapter.rb +1 -1
- data/lib/webmock/request_body_diff.rb +1 -1
- data/lib/webmock/request_pattern.rb +76 -48
- data/lib/webmock/response.rb +11 -5
- data/lib/webmock/rspec.rb +2 -1
- data/lib/webmock/stub_registry.rb +26 -11
- data/lib/webmock/test_unit.rb +1 -3
- data/lib/webmock/util/query_mapper.rb +4 -2
- data/lib/webmock/util/uri.rb +8 -8
- data/lib/webmock/version.rb +1 -1
- data/lib/webmock/webmock.rb +10 -3
- data/spec/acceptance/async_http_client/async_http_client_spec.rb +353 -0
- data/spec/acceptance/async_http_client/async_http_client_spec_helper.rb +73 -0
- data/spec/acceptance/curb/curb_spec.rb +23 -5
- data/spec/acceptance/em_http_request/em_http_request_spec_helper.rb +1 -1
- data/spec/acceptance/excon/excon_spec_helper.rb +2 -0
- data/spec/acceptance/http_rb/http_rb_spec.rb +11 -0
- data/spec/acceptance/manticore/manticore_spec.rb +19 -0
- data/spec/acceptance/net_http/net_http_spec.rb +12 -0
- data/spec/acceptance/shared/callbacks.rb +2 -1
- data/spec/acceptance/shared/request_expectations.rb +7 -0
- data/spec/acceptance/shared/returning_declared_responses.rb +36 -15
- data/spec/acceptance/shared/stubbing_requests.rb +40 -0
- data/spec/support/webmock_server.rb +1 -0
- data/spec/unit/request_pattern_spec.rb +119 -3
- data/spec/unit/response_spec.rb +22 -18
- data/spec/unit/util/query_mapper_spec.rb +7 -0
- data/spec/unit/util/uri_spec.rb +74 -2
- data/spec/unit/webmock_spec.rb +54 -5
- data/test/test_webmock.rb +6 -0
- data/webmock.gemspec +9 -2
- metadata +39 -10
@@ -228,15 +228,16 @@ class PatchedStringIO < StringIO #:nodoc:
|
|
228
228
|
|
229
229
|
alias_method :orig_read_nonblock, :read_nonblock
|
230
230
|
|
231
|
-
def read_nonblock(size, *args)
|
232
|
-
|
231
|
+
def read_nonblock(size, *args, **kwargs)
|
232
|
+
args.reject! {|arg| !arg.is_a?(Hash)}
|
233
|
+
orig_read_nonblock(size, *args, **kwargs)
|
233
234
|
end
|
234
235
|
|
235
236
|
end
|
236
237
|
|
237
238
|
class StubSocket #:nodoc:
|
238
239
|
|
239
|
-
attr_accessor :read_timeout, :continue_timeout
|
240
|
+
attr_accessor :read_timeout, :continue_timeout, :write_timeout
|
240
241
|
|
241
242
|
def initialize(*args)
|
242
243
|
end
|
@@ -251,12 +252,19 @@ class StubSocket #:nodoc:
|
|
251
252
|
def readuntil(*args)
|
252
253
|
end
|
253
254
|
|
255
|
+
def io
|
256
|
+
@io ||= StubIO.new
|
257
|
+
end
|
258
|
+
|
259
|
+
class StubIO
|
260
|
+
def setsockopt(*args); end
|
261
|
+
end
|
254
262
|
end
|
255
263
|
|
256
264
|
module Net #:nodoc: all
|
257
265
|
|
258
266
|
class WebMockNetBufferedIO < BufferedIO
|
259
|
-
def initialize(io, *args)
|
267
|
+
def initialize(io, *args, **kwargs)
|
260
268
|
io = case io
|
261
269
|
when Socket, OpenSSL::SSL::SSLSocket, IO
|
262
270
|
io
|
@@ -267,23 +275,33 @@ module Net #:nodoc: all
|
|
267
275
|
end
|
268
276
|
raise "Unable to create local socket" unless io
|
269
277
|
|
270
|
-
|
278
|
+
# Prior to 2.4.0 `BufferedIO` only takes a single argument (`io`) with no
|
279
|
+
# options. Here we pass through our full set of arguments only if we're
|
280
|
+
# on 2.4.0 or later, and use a simplified invocation otherwise.
|
281
|
+
if RUBY_VERSION >= '2.4.0'
|
282
|
+
super
|
283
|
+
else
|
284
|
+
super(io)
|
285
|
+
end
|
271
286
|
end
|
272
287
|
|
273
288
|
if RUBY_VERSION >= '2.6.0'
|
289
|
+
# https://github.com/ruby/ruby/blob/7d02441f0d6e5c9d0a73a024519eba4f69e36dce/lib/net/protocol.rb#L208
|
290
|
+
# Modified version of method from ruby, so that nil is always passed into orig_read_nonblock to avoid timeout
|
274
291
|
def rbuf_fill
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
292
|
+
case rv = @io.read_nonblock(BUFSIZE, nil, exception: false)
|
293
|
+
when String
|
294
|
+
return if rv.nil?
|
295
|
+
@rbuf << rv
|
296
|
+
rv.clear
|
297
|
+
return
|
298
|
+
when :wait_readable
|
299
|
+
@io.to_io.wait_readable(@read_timeout) or raise Net::ReadTimeout
|
300
|
+
when :wait_writable
|
301
|
+
@io.to_io.wait_writable(@read_timeout) or raise Net::ReadTimeout
|
302
|
+
when nil
|
303
|
+
raise EOFError, 'end of file reached'
|
304
|
+
end while true
|
287
305
|
end
|
288
306
|
end
|
289
307
|
end
|
@@ -118,7 +118,7 @@ if defined?(::Patron)
|
|
118
118
|
def self.build_webmock_response(patron_response)
|
119
119
|
webmock_response = WebMock::Response.new
|
120
120
|
reason = patron_response.status_line.
|
121
|
-
scan(%r(\AHTTP/(\d
|
121
|
+
scan(%r(\AHTTP/(\d+(?:\.\d+)?)\s+(\d\d\d)\s*([^\r\n]+)?))[0][2]
|
122
122
|
webmock_response.status = [patron_response.status, reason]
|
123
123
|
webmock_response.body = patron_response.body
|
124
124
|
webmock_response.headers = patron_response.headers
|
@@ -12,7 +12,7 @@ module WebMock
|
|
12
12
|
def body_diff
|
13
13
|
return {} unless request_signature_diffable? && request_stub_diffable?
|
14
14
|
|
15
|
-
|
15
|
+
Hashdiff.diff(request_signature_body_hash, request_stub_body_hash)
|
16
16
|
end
|
17
17
|
|
18
18
|
attr_reader :request_signature, :request_stub
|
@@ -24,7 +24,7 @@ module WebMock
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def with(options = {}, &block)
|
27
|
-
raise ArgumentError.new('#with method invoked with no arguments. Either options hash or block must be specified.') if options.empty? && !block_given?
|
27
|
+
raise ArgumentError.new('#with method invoked with no arguments. Either options hash or block must be specified. Created a block with do..end? Try creating it with curly braces {} instead.') if options.empty? && !block_given?
|
28
28
|
assign_options(options)
|
29
29
|
@with_block = block
|
30
30
|
self
|
@@ -80,6 +80,8 @@ module WebMock
|
|
80
80
|
URIRegexpPattern.new(uri)
|
81
81
|
elsif uri.is_a?(Addressable::Template)
|
82
82
|
URIAddressablePattern.new(uri)
|
83
|
+
elsif uri.respond_to?(:call)
|
84
|
+
URICallablePattern.new(uri)
|
83
85
|
else
|
84
86
|
URIStringPattern.new(uri)
|
85
87
|
end
|
@@ -107,11 +109,13 @@ module WebMock
|
|
107
109
|
include RSpecMatcherDetector
|
108
110
|
|
109
111
|
def initialize(pattern)
|
110
|
-
@pattern =
|
111
|
-
|
112
|
+
@pattern = if pattern.is_a?(Addressable::URI) \
|
113
|
+
|| pattern.is_a?(Addressable::Template)
|
114
|
+
pattern
|
115
|
+
elsif pattern.respond_to?(:call)
|
112
116
|
pattern
|
113
117
|
else
|
114
|
-
|
118
|
+
WebMock::Util::URI.normalize_uri(pattern)
|
115
119
|
end
|
116
120
|
@query_params = nil
|
117
121
|
end
|
@@ -131,38 +135,44 @@ module WebMock
|
|
131
135
|
end
|
132
136
|
end
|
133
137
|
|
138
|
+
def matches?(uri)
|
139
|
+
pattern_matches?(uri) && query_params_matches?(uri)
|
140
|
+
end
|
141
|
+
|
134
142
|
def to_s
|
135
|
-
str =
|
143
|
+
str = pattern_inspect
|
136
144
|
str += " with query params #{@query_params.inspect}" if @query_params
|
137
145
|
str
|
138
146
|
end
|
139
|
-
end
|
140
147
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
148
|
+
private
|
149
|
+
|
150
|
+
def pattern_inspect
|
151
|
+
@pattern.inspect
|
145
152
|
end
|
146
153
|
|
147
|
-
def
|
148
|
-
|
149
|
-
str += " with query params #{@query_params.inspect}" if @query_params
|
150
|
-
str
|
154
|
+
def query_params_matches?(uri)
|
155
|
+
@query_params.nil? || @query_params == WebMock::Util::QueryMapper.query_to_values(uri.query, notation: Config.instance.query_values_notation)
|
151
156
|
end
|
152
157
|
end
|
153
158
|
|
154
|
-
class
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
else
|
160
|
-
# WebMock checks the query, Addressable checks everything else
|
161
|
-
matches_with_variations?(uri.omit(:query)) &&
|
162
|
-
@query_params == WebMock::Util::QueryMapper.query_to_values(uri.query)
|
163
|
-
end
|
159
|
+
class URICallablePattern < URIPattern
|
160
|
+
private
|
161
|
+
|
162
|
+
def pattern_matches?(uri)
|
163
|
+
@pattern.call(uri)
|
164
164
|
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class URIRegexpPattern < URIPattern
|
168
|
+
private
|
165
169
|
|
170
|
+
def pattern_matches?(uri)
|
171
|
+
WebMock::Util::URI.variations_of_uri_as_strings(uri).any? { |u| u.match(@pattern) }
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
class URIAddressablePattern < URIPattern
|
166
176
|
def add_query_params(query_params)
|
167
177
|
@@add_query_params_warned ||= false
|
168
178
|
if not @@add_query_params_warned
|
@@ -172,28 +182,57 @@ module WebMock
|
|
172
182
|
super(query_params)
|
173
183
|
end
|
174
184
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
185
|
+
private
|
186
|
+
|
187
|
+
def pattern_matches?(uri)
|
188
|
+
if @query_params.nil?
|
189
|
+
# Let Addressable check the whole URI
|
190
|
+
matches_with_variations?(uri)
|
191
|
+
else
|
192
|
+
# WebMock checks the query, Addressable checks everything else
|
193
|
+
matches_with_variations?(uri.omit(:query))
|
194
|
+
end
|
179
195
|
end
|
180
196
|
|
181
|
-
|
197
|
+
def pattern_inspect
|
198
|
+
@pattern.pattern.inspect
|
199
|
+
end
|
182
200
|
|
183
201
|
def matches_with_variations?(uri)
|
184
|
-
|
202
|
+
template =
|
203
|
+
begin
|
204
|
+
Addressable::Template.new(WebMock::Util::URI.heuristic_parse(@pattern.pattern))
|
205
|
+
rescue Addressable::URI::InvalidURIError
|
206
|
+
Addressable::Template.new(@pattern.pattern)
|
207
|
+
end
|
208
|
+
WebMock::Util::URI.variations_of_uri_as_strings(uri).any? { |u|
|
209
|
+
template_matches_uri?(template, u)
|
210
|
+
}
|
211
|
+
end
|
185
212
|
|
186
|
-
|
187
|
-
|
213
|
+
def template_matches_uri?(template, uri)
|
214
|
+
template.match(uri)
|
215
|
+
rescue Addressable::URI::InvalidURIError
|
216
|
+
false
|
188
217
|
end
|
189
218
|
end
|
190
219
|
|
191
220
|
class URIStringPattern < URIPattern
|
192
|
-
def
|
221
|
+
def add_query_params(query_params)
|
222
|
+
super
|
223
|
+
if @query_params.is_a?(Hash) || @query_params.is_a?(String)
|
224
|
+
query_hash = (WebMock::Util::QueryMapper.query_to_values(@pattern.query, notation: Config.instance.query_values_notation) || {}).merge(@query_params)
|
225
|
+
@pattern.query = WebMock::Util::QueryMapper.values_to_query(query_hash, notation: WebMock::Config.instance.query_values_notation)
|
226
|
+
@query_params = nil
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
private
|
231
|
+
|
232
|
+
def pattern_matches?(uri)
|
193
233
|
if @pattern.is_a?(Addressable::URI)
|
194
234
|
if @query_params
|
195
|
-
uri.omit(:query) === @pattern
|
196
|
-
(@query_params.nil? || @query_params == WebMock::Util::QueryMapper.query_to_values(uri.query, notation: Config.instance.query_values_notation))
|
235
|
+
uri.omit(:query) === @pattern
|
197
236
|
else
|
198
237
|
uri === @pattern
|
199
238
|
end
|
@@ -202,19 +241,8 @@ module WebMock
|
|
202
241
|
end
|
203
242
|
end
|
204
243
|
|
205
|
-
def
|
206
|
-
|
207
|
-
if @query_params.is_a?(Hash) || @query_params.is_a?(String)
|
208
|
-
query_hash = (WebMock::Util::QueryMapper.query_to_values(@pattern.query, notation: Config.instance.query_values_notation) || {}).merge(@query_params)
|
209
|
-
@pattern.query = WebMock::Util::QueryMapper.values_to_query(query_hash, notation: WebMock::Config.instance.query_values_notation)
|
210
|
-
@query_params = nil
|
211
|
-
end
|
212
|
-
end
|
213
|
-
|
214
|
-
def to_s
|
215
|
-
str = WebMock::Util::URI.strip_default_port_from_uri_string(@pattern.to_s)
|
216
|
-
str += " with query params #{@query_params.inspect}" if @query_params
|
217
|
-
str
|
244
|
+
def pattern_inspect
|
245
|
+
WebMock::Util::URI.strip_default_port_from_uri_string(@pattern.to_s)
|
218
246
|
end
|
219
247
|
end
|
220
248
|
|
data/lib/webmock/response.rb
CHANGED
@@ -91,10 +91,10 @@ module WebMock
|
|
91
91
|
|
92
92
|
def ==(other)
|
93
93
|
self.body == other.body &&
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
94
|
+
self.headers === other.headers &&
|
95
|
+
self.status == other.status &&
|
96
|
+
self.exception == other.exception &&
|
97
|
+
self.should_timeout == other.should_timeout
|
98
98
|
end
|
99
99
|
|
100
100
|
private
|
@@ -111,7 +111,13 @@ module WebMock
|
|
111
111
|
valid_types = [Proc, IO, Pathname, String, Array]
|
112
112
|
return if @body.nil?
|
113
113
|
return if valid_types.any? { |c| @body.is_a?(c) }
|
114
|
-
|
114
|
+
|
115
|
+
if @body.class.is_a?(Hash)
|
116
|
+
raise InvalidBody, "must be one of: #{valid_types}, but you've used a #{@body.class}' instead." \
|
117
|
+
"\n What shall we encode it to? try calling .to_json .to_xml instead on the hash instead, or otherwise convert it to a string."
|
118
|
+
else
|
119
|
+
raise InvalidBody, "must be one of: #{valid_types}. '#{@body.class}' given"
|
120
|
+
end
|
115
121
|
end
|
116
122
|
|
117
123
|
def read_raw_response(raw_response)
|
data/lib/webmock/rspec.rb
CHANGED
@@ -10,25 +10,39 @@ module WebMock
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def global_stubs
|
13
|
-
@global_stubs ||= []
|
13
|
+
@global_stubs ||= Hash.new { |h, k| h[k] = [] }
|
14
14
|
end
|
15
15
|
|
16
16
|
def reset!
|
17
17
|
self.request_stubs = []
|
18
18
|
end
|
19
19
|
|
20
|
-
def register_global_stub(&block)
|
20
|
+
def register_global_stub(order = :before_local_stubs, &block)
|
21
|
+
unless %i[before_local_stubs after_local_stubs].include?(order)
|
22
|
+
raise ArgumentError.new("Wrong order. Use :before_local_stubs or :after_local_stubs")
|
23
|
+
end
|
24
|
+
|
21
25
|
# This hash contains the responses returned by the block,
|
22
26
|
# keyed by the exact request (using the object_id).
|
23
27
|
# That way, there's no race condition in case #to_return
|
24
28
|
# doesn't run immediately after stub.with.
|
25
29
|
responses = {}
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
30
|
+
response_lock = Mutex.new
|
31
|
+
|
32
|
+
stub = ::WebMock::RequestStub.new(:any, ->(uri) { true }).with { |request|
|
33
|
+
update_response = -> { responses[request.object_id] = yield(request) }
|
34
|
+
|
35
|
+
# The block can recurse, so only lock if we don't already own it
|
36
|
+
if response_lock.owned?
|
37
|
+
update_response.call
|
38
|
+
else
|
39
|
+
response_lock.synchronize(&update_response)
|
40
|
+
end
|
41
|
+
}.to_return(lambda { |request|
|
42
|
+
response_lock.synchronize { responses.delete(request.object_id) }
|
43
|
+
})
|
44
|
+
|
45
|
+
global_stubs[order].push stub
|
32
46
|
end
|
33
47
|
|
34
48
|
def register_request_stub(stub)
|
@@ -54,9 +68,10 @@ module WebMock
|
|
54
68
|
private
|
55
69
|
|
56
70
|
def request_stub_for(request_signature)
|
57
|
-
(global_stubs + request_stubs
|
58
|
-
registered_request_stub
|
59
|
-
|
71
|
+
(global_stubs[:before_local_stubs] + request_stubs + global_stubs[:after_local_stubs])
|
72
|
+
.detect { |registered_request_stub|
|
73
|
+
registered_request_stub.request_pattern.matches?(request_signature)
|
74
|
+
}
|
60
75
|
end
|
61
76
|
|
62
77
|
def evaluate_response_for_request(response, request_signature)
|
data/lib/webmock/test_unit.rb
CHANGED
@@ -8,12 +8,10 @@ module Test
|
|
8
8
|
class TestCase
|
9
9
|
include WebMock::API
|
10
10
|
|
11
|
-
|
11
|
+
teardown
|
12
12
|
def teardown_with_webmock
|
13
|
-
teardown_without_webmock
|
14
13
|
WebMock.reset!
|
15
14
|
end
|
16
|
-
alias_method :teardown, :teardown_with_webmock
|
17
15
|
|
18
16
|
end
|
19
17
|
end
|
@@ -161,7 +161,7 @@ module WebMock::Util
|
|
161
161
|
else
|
162
162
|
if array_value
|
163
163
|
current_node[last_key] ||= []
|
164
|
-
current_node[last_key] << value
|
164
|
+
current_node[last_key] << value unless value.nil?
|
165
165
|
else
|
166
166
|
current_node[last_key] = value
|
167
167
|
end
|
@@ -186,7 +186,9 @@ module WebMock::Util
|
|
186
186
|
new_query_values = new_query_values.to_hash
|
187
187
|
new_query_values = new_query_values.inject([]) do |object, (key, value)|
|
188
188
|
key = key.to_s if key.is_a?(::Symbol) || key.nil?
|
189
|
-
if value.is_a?(Array)
|
189
|
+
if value.is_a?(Array) && value.empty?
|
190
|
+
object << [key.to_s + '[]']
|
191
|
+
elsif value.is_a?(Array)
|
190
192
|
value.each { |v| object << [key.to_s + '[]', v] }
|
191
193
|
elsif value.is_a?(Hash)
|
192
194
|
value.each { |k, v| object << ["#{key.to_s}[#{k}]", v]}
|
data/lib/webmock/util/uri.rb
CHANGED
@@ -41,12 +41,12 @@ module WebMock
|
|
41
41
|
uris = uris_with_trailing_slash_and_without(uris)
|
42
42
|
end
|
43
43
|
|
44
|
-
uris = uris_encoded_and_unencoded(uris)
|
45
|
-
|
46
44
|
if normalized_uri.port == Addressable::URI.port_mapping[normalized_uri.scheme]
|
47
45
|
uris = uris_with_inferred_port_and_without(uris)
|
48
46
|
end
|
49
47
|
|
48
|
+
uris = uris_encoded_and_unencoded(uris)
|
49
|
+
|
50
50
|
if normalized_uri.scheme == "http" && !only_with_scheme
|
51
51
|
uris = uris_with_scheme_and_without(uris)
|
52
52
|
end
|
@@ -80,27 +80,27 @@ module WebMock
|
|
80
80
|
|
81
81
|
def self.uris_with_inferred_port_and_without(uris)
|
82
82
|
uris.map { |uri|
|
83
|
-
|
84
|
-
[ uri, uri.gsub(%r{(:80)|(:443)}, "").freeze ]
|
83
|
+
[ uri, uri.omit(:port)]
|
85
84
|
}.flatten
|
86
85
|
end
|
87
86
|
|
88
87
|
def self.uris_encoded_and_unencoded(uris)
|
89
88
|
uris.map do |uri|
|
90
|
-
[
|
89
|
+
[
|
90
|
+
uri.to_s.force_encoding(Encoding::ASCII_8BIT),
|
91
|
+
Addressable::URI.unencode(uri, String).force_encoding(Encoding::ASCII_8BIT).freeze
|
92
|
+
]
|
91
93
|
end.flatten
|
92
94
|
end
|
93
95
|
|
94
96
|
def self.uris_with_scheme_and_without(uris)
|
95
97
|
uris.map { |uri|
|
96
|
-
uri = uri.dup.force_encoding(Encoding::ASCII_8BIT) if uri.respond_to?(:force_encoding)
|
97
98
|
[ uri, uri.gsub(%r{^https?://},"").freeze ]
|
98
99
|
}.flatten
|
99
100
|
end
|
100
101
|
|
101
102
|
def self.uris_with_trailing_slash_and_without(uris)
|
102
|
-
uris
|
103
|
-
uri = uri.dup.force_encoding(Encoding::ASCII_8BIT) if uri.respond_to?(:force_encoding)
|
103
|
+
uris.map { |uri|
|
104
104
|
[ uri, uri.omit(:path).freeze ]
|
105
105
|
}.flatten
|
106
106
|
end
|