gin 1.0.4 → 1.1.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.
- data/History.rdoc +16 -0
- data/Manifest.txt +12 -2
- data/README.rdoc +33 -0
- data/Rakefile +5 -0
- data/TODO.rdoc +7 -0
- data/bin/gin +158 -0
- data/lib/gin.rb +19 -2
- data/lib/gin/app.rb +420 -173
- data/lib/gin/cache.rb +65 -0
- data/lib/gin/config.rb +174 -18
- data/lib/gin/constants.rb +4 -0
- data/lib/gin/controller.rb +219 -11
- data/lib/gin/core_ext/gin_class.rb +16 -0
- data/lib/gin/errorable.rb +16 -2
- data/lib/gin/filterable.rb +29 -19
- data/lib/gin/reloadable.rb +1 -1
- data/lib/gin/request.rb +11 -0
- data/lib/gin/router.rb +185 -61
- data/lib/gin/rw_lock.rb +109 -0
- data/lib/gin/test.rb +702 -0
- data/public/gin.css +15 -3
- data/test/app/layouts/bar.erb +9 -0
- data/test/app/layouts/foo.erb +9 -0
- data/test/app/views/bar.erb +1 -0
- data/test/mock_app.rb +94 -0
- data/test/mock_config/invalid.yml +2 -0
- data/test/test_app.rb +160 -45
- data/test/test_cache.rb +57 -0
- data/test/test_config.rb +108 -13
- data/test/test_controller.rb +201 -11
- data/test/test_errorable.rb +1 -1
- data/test/test_gin.rb +9 -0
- data/test/test_helper.rb +3 -1
- data/test/test_router.rb +33 -0
- data/test/test_rw_lock.rb +65 -0
- data/test/test_test.rb +627 -0
- metadata +86 -6
- data/.autotest +0 -23
- data/.gitignore +0 -7
data/lib/gin/rw_lock.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
##
|
2
|
+
# Read-Write lock pair for accessing data that is mostly read-bound.
|
3
|
+
# Reading is done without locking until a write operation is started.
|
4
|
+
#
|
5
|
+
# lock = Gin::RWLock.new
|
6
|
+
# lock.write_sync{ write_to_the_object }
|
7
|
+
# value = lock.read_sync{ read_from_the_object }
|
8
|
+
#
|
9
|
+
# The RWLock is built to work primarily in Thread-pool type environments and its
|
10
|
+
# effectiveness is much less for Thread-spawn models.
|
11
|
+
#
|
12
|
+
# RWLock also shows increased performance in GIL-less Ruby implementations such
|
13
|
+
# as Rubinius 2.x.
|
14
|
+
#
|
15
|
+
# Using write_sync from inside a read_sync block is safe, but the inverse isn't:
|
16
|
+
#
|
17
|
+
# lock = Gin::RWLock.new
|
18
|
+
#
|
19
|
+
# # This is OK.
|
20
|
+
# lock.read_sync do
|
21
|
+
# get_value || lock.write_sync{ update_value }
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # This is NOT OK and will raise a ThreadError.
|
25
|
+
# # It's also not necessary because read sync-ing is inferred
|
26
|
+
# # during write syncs.
|
27
|
+
# lock.write_sync do
|
28
|
+
# update_value
|
29
|
+
# lock.read_sync{ get_value }
|
30
|
+
# end
|
31
|
+
|
32
|
+
class Gin::RWLock
|
33
|
+
|
34
|
+
class WriteTimeout < StandardError; end
|
35
|
+
|
36
|
+
TIMEOUT_MSG = "Took too long to lock all config mutexes. \
|
37
|
+
Try increasing the value of Config#write_timeout."
|
38
|
+
|
39
|
+
# The amount of time to wait for writer threads to get all the read locks.
|
40
|
+
attr_accessor :write_timeout
|
41
|
+
|
42
|
+
|
43
|
+
def initialize write_timeout=nil
|
44
|
+
@wmutex = Mutex.new
|
45
|
+
@write_timeout = write_timeout || 0.05
|
46
|
+
@mutex_id = :"rwlock_#{self.object_id}"
|
47
|
+
@mutex_owned_id = :"#{@mutex_id}_owned"
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
def write_sync
|
52
|
+
lock_mutexes = []
|
53
|
+
relock_curr = false
|
54
|
+
was_locked = Thread.current[@mutex_owned_id]
|
55
|
+
|
56
|
+
curr_mutex = read_mutex
|
57
|
+
|
58
|
+
write_mutex.lock unless was_locked
|
59
|
+
Thread.current[@mutex_owned_id] = true
|
60
|
+
|
61
|
+
# Protect against same-thread deadlocks
|
62
|
+
if curr_mutex && curr_mutex.locked?
|
63
|
+
relock_curr = curr_mutex.unlock rescue false
|
64
|
+
end
|
65
|
+
|
66
|
+
start = Time.now
|
67
|
+
|
68
|
+
Thread.list.each do |t|
|
69
|
+
mutex = t[@mutex_id]
|
70
|
+
next if !mutex || !relock_curr && t == Thread.current
|
71
|
+
until mutex.try_lock
|
72
|
+
raise WriteTimeout, TIMEOUT_MSG if Time.now - start > @write_timeout
|
73
|
+
end
|
74
|
+
lock_mutexes << mutex
|
75
|
+
end
|
76
|
+
|
77
|
+
yield
|
78
|
+
ensure
|
79
|
+
lock_mutexes.each(&:unlock)
|
80
|
+
curr_mutex.try_lock if relock_curr
|
81
|
+
unless was_locked
|
82
|
+
Thread.current[@mutex_owned_id] = false
|
83
|
+
write_mutex.unlock
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
def read_sync
|
89
|
+
was_locked = read_mutex.locked?
|
90
|
+
read_mutex.lock unless was_locked
|
91
|
+
yield
|
92
|
+
ensure
|
93
|
+
read_mutex.unlock if was_locked
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
|
100
|
+
def write_mutex
|
101
|
+
@wmutex
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
def read_mutex
|
106
|
+
return Thread.current[@mutex_id] if Thread.current[@mutex_id]
|
107
|
+
@wmutex.synchronize{ Thread.current[@mutex_id] = Mutex.new }
|
108
|
+
end
|
109
|
+
end
|
data/lib/gin/test.rb
ADDED
@@ -0,0 +1,702 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Gin::Test; end
|
4
|
+
|
5
|
+
##
|
6
|
+
# Helper assertion methods for tests.
|
7
|
+
# To contextualize tests to a specific app, use the
|
8
|
+
# automatically generated module assigned to your app's class:
|
9
|
+
#
|
10
|
+
# class MyCtrlTest < Test::Unit::TestCase
|
11
|
+
# include MyApp::TestHelper # Sets App for mock requests.
|
12
|
+
# controller MyHomeController # Sets default controller to use.
|
13
|
+
#
|
14
|
+
# def test_home
|
15
|
+
# get :home
|
16
|
+
# assert_response :success
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
|
20
|
+
module Gin::Test::Assertions
|
21
|
+
|
22
|
+
##
|
23
|
+
# Asserts the response status code and headers.
|
24
|
+
# Takes an integer (status code) or Symbol as the expected value:
|
25
|
+
# :success:: 2XX status codes
|
26
|
+
# :redirect:: 301-303, 307-308 status codes
|
27
|
+
# :forbidden:: 403 status code
|
28
|
+
# :unauthorized:: 401 status code
|
29
|
+
# :not_found:: 404 status code
|
30
|
+
|
31
|
+
def assert_response expected, msg=nil
|
32
|
+
status = rack_response[0]
|
33
|
+
case expected
|
34
|
+
when :success
|
35
|
+
assert((200..299).include?(status),
|
36
|
+
msg || "Status expected to be in range 200..299 but was #{status.inspect}")
|
37
|
+
when :redirect
|
38
|
+
assert [301,302,303,307,308].include?(status),
|
39
|
+
msg || "Status expected to be in range 301..303 or 307..308 but was #{status.inspect}"
|
40
|
+
when :unauthorized
|
41
|
+
assert 401 == status,
|
42
|
+
msg || "Status expected to be 401 but was #{status.inspect}"
|
43
|
+
when :forbidden
|
44
|
+
assert 403 == status,
|
45
|
+
msg || "Status expected to be 403 but was #{status.inspect}"
|
46
|
+
when :not_found
|
47
|
+
assert 404 == status,
|
48
|
+
msg || "Status expected to be 404 but was #{status.inspect}"
|
49
|
+
when :error
|
50
|
+
assert((500..599).include?(status),
|
51
|
+
msg || "Status expected to be in range 500..599 but was #{status.inspect}")
|
52
|
+
else
|
53
|
+
assert expected == status,
|
54
|
+
msg || "Status expected to be #{expected.inspect} but was #{status.inspect}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
##
|
60
|
+
# Checks for data points in the response body.
|
61
|
+
# Looks at the response Content-Type to parse.
|
62
|
+
# Supports JSON, BSON, XML, PLIST, and HTML.
|
63
|
+
#
|
64
|
+
# If value is a Class, Range, or Regex, does a match.
|
65
|
+
# Options supported are:
|
66
|
+
# :count:: Integer - Number of occurences of the data point.
|
67
|
+
# :value:: Object - The expected value of the data point.
|
68
|
+
# :selector:: Symbol - type of selector to use: :css, :xpath, or :rb_path
|
69
|
+
#
|
70
|
+
# If value is a Class, Range, or Regex, does a match.
|
71
|
+
#
|
72
|
+
# # Use CSS3 for HTML
|
73
|
+
# assert_select '.address[domestic=Yes]'
|
74
|
+
#
|
75
|
+
# # Use XPath for XML data
|
76
|
+
# assert_select './/address[@domestic=Yes]'
|
77
|
+
#
|
78
|
+
# # Use ruby-path for JSON, BSON, and PList
|
79
|
+
# assert_select '**/address/domestic=YES/../value'
|
80
|
+
|
81
|
+
def assert_select key_or_path, opts={}, msg=nil
|
82
|
+
value = opts[:value]
|
83
|
+
data = parsed_body
|
84
|
+
val_msg = " with value #{value.inspect}" if !value.nil?
|
85
|
+
count = 0
|
86
|
+
|
87
|
+
selector = opts[:selector] ||
|
88
|
+
case data
|
89
|
+
when Array, Hash then :rb_path
|
90
|
+
when Nokogiri::HTML::Document then :xpath
|
91
|
+
when Nokogiri::XML::Document then :css
|
92
|
+
end
|
93
|
+
|
94
|
+
case selector
|
95
|
+
when :rb_path
|
96
|
+
use_lib 'path', 'ruby-path'
|
97
|
+
data.find_data(key_or_path) do |p,k,pa|
|
98
|
+
count += 1 if value.nil? || value === p[k]
|
99
|
+
break unless opts[:count]
|
100
|
+
end
|
101
|
+
|
102
|
+
when :css
|
103
|
+
data.css(key_or_path).each do |node|
|
104
|
+
count += 1 if value.nil? || value === node.text
|
105
|
+
break unless opts[:count]
|
106
|
+
end
|
107
|
+
|
108
|
+
when :xpath
|
109
|
+
data.xpath(key_or_path).each do |node|
|
110
|
+
count += 1 if value.nil? || value === node.text
|
111
|
+
break unless opts[:count]
|
112
|
+
end
|
113
|
+
|
114
|
+
else
|
115
|
+
raise "Unknown selector #{selector.inspect} for #{data.class}"
|
116
|
+
end
|
117
|
+
|
118
|
+
if opts[:count]
|
119
|
+
assert opts[:count] == count,
|
120
|
+
msg || "Expected #{opts[:count]} items matching '#{key_or_path}'#{val_msg} but found #{count}"
|
121
|
+
else
|
122
|
+
assert((count > 0),
|
123
|
+
msg || "Expected at least one item matching '#{key_or_path}'#{val_msg} but found none")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
##
|
129
|
+
# Uses ruby-path to check for data points in the response body.
|
130
|
+
#
|
131
|
+
# Options supported are:
|
132
|
+
# :count:: Integer - Number of occurences of the data point.
|
133
|
+
# :value:: Object - The expected value of the data point.
|
134
|
+
#
|
135
|
+
# If value is a Class, Range, or Regex, does a match.
|
136
|
+
# Use for JSON, BSON, and PList data.
|
137
|
+
# assert_select '**/address/domestic=YES/../value'
|
138
|
+
|
139
|
+
def assert_data path, opts={}, msg=nil
|
140
|
+
assert_select path, opts.merge(selector: :rb_path), msg
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
##
|
145
|
+
# Uses CSS selectors to check for data points in the response body.
|
146
|
+
#
|
147
|
+
# Options supported are:
|
148
|
+
# :count:: Integer - Number of occurences of the data point.
|
149
|
+
# :value:: Object - The expected value of the data point.
|
150
|
+
#
|
151
|
+
# If value is a Class, Range, or Regex, does a match.
|
152
|
+
# Use for XML or HTML.
|
153
|
+
# assert_select '.address[domestic=Yes]'
|
154
|
+
|
155
|
+
def assert_css path, opts={}, msg=nil
|
156
|
+
assert_select path, opts.merge(selector: :css), msg
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
##
|
161
|
+
# Uses XPath selectors to check for data points in the response body.
|
162
|
+
#
|
163
|
+
# Options supported are:
|
164
|
+
# :count:: Integer - Number of occurences of the data point.
|
165
|
+
# :value:: Object - The expected value of the data point.
|
166
|
+
#
|
167
|
+
# If value is a Class, Range, or Regex, does a match.
|
168
|
+
# Use for XML or HTML.
|
169
|
+
# assert_select './/address[@domestic=Yes]'
|
170
|
+
|
171
|
+
def assert_xpath path, opts={}, msg=nil
|
172
|
+
assert_select path, opts.merge(selector: :xpath), msg
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
##
|
177
|
+
# Checks that the given Cookie is set with the expected values.
|
178
|
+
# Options supported:
|
179
|
+
# :secure:: Boolean - SSL cookies only
|
180
|
+
# :http_only:: Boolean - HTTP only cookie
|
181
|
+
# :domain:: String - Domain on which the cookie is used
|
182
|
+
# :expires_at:: Time - Date and time of cookie expiration
|
183
|
+
# :path:: String - Path cookie applies to
|
184
|
+
# :value:: Object - The value of the cookie
|
185
|
+
|
186
|
+
def assert_cookie name, opts={}, msg=nil
|
187
|
+
opts ||= {}
|
188
|
+
cookie = response_cookies[name]
|
189
|
+
|
190
|
+
assert cookie, msg || "Expected cookie #{name.inspect} but it doesn't exist"
|
191
|
+
|
192
|
+
opts.each do |k,v|
|
193
|
+
next if v == cookie[k]
|
194
|
+
err_msg = msg || "Expected cookie #{k} to be #{v.inspect} but was #{cookie[k].inspect}"
|
195
|
+
|
196
|
+
raise MiniTest::Assertion, err_msg.to_s
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
##
|
202
|
+
# Checks that a rendered view name or path matches the one given.
|
203
|
+
|
204
|
+
def assert_view view, msg=nil
|
205
|
+
path = @controller.template_path(view)
|
206
|
+
expected = @app.template_files(path).first
|
207
|
+
assert templates.include?(expected),
|
208
|
+
msg || "Expected view `#{path}' in:\n #{templates.join("\n ")}"
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
##
|
213
|
+
# Checks that a specific layout was use to render the response.
|
214
|
+
|
215
|
+
def assert_layout layout, msg=nil
|
216
|
+
path = @controller.template_path(layout, true)
|
217
|
+
expected = @app.template_files(path).first
|
218
|
+
assert templates.include?(expected),
|
219
|
+
msg || "Expected layout `#{path}' in:\n #{templates.join("\n ")}"
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
##
|
224
|
+
# Checks that the response is a redirect to a given path or url.
|
225
|
+
# assert_redirect "/path/to/thing"
|
226
|
+
# assert_redirect "http://example.com"
|
227
|
+
# assert_redirect 302, "/path/to/thing"
|
228
|
+
|
229
|
+
def assert_redirect url, *args
|
230
|
+
status = args.shift if Integer === args[0]
|
231
|
+
location = rack_response[1]['Location']
|
232
|
+
|
233
|
+
msg = args.pop ||
|
234
|
+
"Expected redirect to #{url.inspect} but was #{location.inspect}"
|
235
|
+
|
236
|
+
raise MiniTest::Assertion, msg unless url == location
|
237
|
+
assert_response(status || :redirect)
|
238
|
+
end
|
239
|
+
|
240
|
+
|
241
|
+
##
|
242
|
+
# Checks that the given route is valid and points to the expected
|
243
|
+
# controller and action.
|
244
|
+
|
245
|
+
def assert_route verb, path, exp_ctrl, exp_action, msg=nil
|
246
|
+
ctrl, action, = app.router.resources_for(verb, path)
|
247
|
+
expected = "#{exp_ctrl}##{exp_action}"
|
248
|
+
real = "#{ctrl}##{action}"
|
249
|
+
real_msg = ctrl && action ? "got #{real}" : "doesn't exist"
|
250
|
+
|
251
|
+
assert expected == real,
|
252
|
+
msg || "`#{verb.to_s.upcase} #{path}' should map to #{expected} but #{real_msg}"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
|
257
|
+
##
|
258
|
+
# Helper methods for tests. To contextualize tests to a specific app, use the
|
259
|
+
# automatically generated module assigned to your app's class:
|
260
|
+
#
|
261
|
+
# class MyCtrlTest < Test::Unit::TestCase
|
262
|
+
# include MyApp::TestHelper # Sets App for mock requests.
|
263
|
+
# controller MyHomeController # Sets default controller to use.
|
264
|
+
#
|
265
|
+
# def test_home
|
266
|
+
# get :home
|
267
|
+
# assert_response :success
|
268
|
+
# end
|
269
|
+
# end
|
270
|
+
#
|
271
|
+
# All requests are full stack, meaning any in-app middleware will be run as
|
272
|
+
# a part of a request. The goal is to test controllers in the context of the
|
273
|
+
# whole app, and easily do integration-level tests as well.
|
274
|
+
|
275
|
+
module Gin::Test::Helpers
|
276
|
+
|
277
|
+
include Gin::Test::Assertions
|
278
|
+
|
279
|
+
def self.setup_klass subclass # :nodoc:
|
280
|
+
return if subclass.respond_to?(:app_klass)
|
281
|
+
|
282
|
+
subclass.instance_eval do
|
283
|
+
def app_klass klass=nil
|
284
|
+
@app_klass = klass if klass
|
285
|
+
defined?(@app_klass) && @app_klass
|
286
|
+
end
|
287
|
+
|
288
|
+
|
289
|
+
##
|
290
|
+
# Sets the default controller to use when making requests
|
291
|
+
# for all tests in the given class.
|
292
|
+
# class MyCtrlTest < Test::Unit::TestCase
|
293
|
+
# include MyApp::TestHelper
|
294
|
+
# controller MyCtrl
|
295
|
+
# end
|
296
|
+
|
297
|
+
def controller ctrl_klass=nil
|
298
|
+
@default_controller = ctrl_klass if ctrl_klass
|
299
|
+
defined?(@default_controller) && @default_controller
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
|
305
|
+
def use_lib lib, gemname=nil # :nodoc:
|
306
|
+
require lib
|
307
|
+
rescue LoadError => e
|
308
|
+
raise unless e.message == "cannot load such file -- #{lib}"
|
309
|
+
gemname ||= lib
|
310
|
+
$stderr.puts "You need the `#{gemname}' gem to access some of the features \
|
311
|
+
you are trying to use.
|
312
|
+
Run the following command and try again: gem install #{gemname}"
|
313
|
+
exit 1
|
314
|
+
end
|
315
|
+
|
316
|
+
|
317
|
+
def correct_302_redirect?
|
318
|
+
self.class.correct_302_redirect?
|
319
|
+
end
|
320
|
+
|
321
|
+
|
322
|
+
##
|
323
|
+
# The App instance being used for the requests.
|
324
|
+
|
325
|
+
def app
|
326
|
+
@app ||= self.class.app_klass.new
|
327
|
+
end
|
328
|
+
|
329
|
+
|
330
|
+
##
|
331
|
+
# The Rack env for the next mock request.
|
332
|
+
|
333
|
+
def env
|
334
|
+
@env ||= {'rack.input' => ""}
|
335
|
+
end
|
336
|
+
|
337
|
+
|
338
|
+
##
|
339
|
+
# The standard Rack response array.
|
340
|
+
|
341
|
+
def rack_response
|
342
|
+
@rack_response ||= [nil,{},[]]
|
343
|
+
end
|
344
|
+
|
345
|
+
|
346
|
+
##
|
347
|
+
# The Gin::Controller instance used by the last mock request.
|
348
|
+
|
349
|
+
def controller
|
350
|
+
defined?(@controller) && @controller
|
351
|
+
end
|
352
|
+
|
353
|
+
|
354
|
+
##
|
355
|
+
# The Gin::Request instance on the controller used by the last mock request.
|
356
|
+
|
357
|
+
def request
|
358
|
+
controller && controller.request
|
359
|
+
end
|
360
|
+
|
361
|
+
|
362
|
+
##
|
363
|
+
# The Gin::Response instance on the controller used by the last mock request.
|
364
|
+
|
365
|
+
def response
|
366
|
+
controller && controller.response
|
367
|
+
end
|
368
|
+
|
369
|
+
|
370
|
+
##
|
371
|
+
# Array of template file paths used to render the response body.
|
372
|
+
|
373
|
+
def templates
|
374
|
+
@templates ||= []
|
375
|
+
end
|
376
|
+
|
377
|
+
|
378
|
+
##
|
379
|
+
# Make a GET request.
|
380
|
+
# get FooController, :show, :id => 123
|
381
|
+
#
|
382
|
+
# # With default_controller set to FooController
|
383
|
+
# get :show, :id => 123
|
384
|
+
#
|
385
|
+
# # Default named route
|
386
|
+
# get :show_foo, :id => 123
|
387
|
+
#
|
388
|
+
# # Request with headers
|
389
|
+
# get :show_foo, {:id => 123}, 'Cookie' => 'value'
|
390
|
+
# get :show_foo, {}, 'Cookie' => 'value'
|
391
|
+
|
392
|
+
def get *args
|
393
|
+
make_request :get, *args
|
394
|
+
end
|
395
|
+
|
396
|
+
|
397
|
+
##
|
398
|
+
# Make a POST request. See 'get' method for usage.
|
399
|
+
|
400
|
+
def post *args
|
401
|
+
make_request :post, *args
|
402
|
+
end
|
403
|
+
|
404
|
+
|
405
|
+
##
|
406
|
+
# Make a PUT request. See 'get' method for usage.
|
407
|
+
|
408
|
+
def put *args
|
409
|
+
make_request :put, *args
|
410
|
+
end
|
411
|
+
|
412
|
+
|
413
|
+
##
|
414
|
+
# Make a PATCH request. See 'get' method for usage.
|
415
|
+
|
416
|
+
def patch *args
|
417
|
+
make_request :patch, *args
|
418
|
+
end
|
419
|
+
|
420
|
+
|
421
|
+
##
|
422
|
+
# Make a DELETE request. See 'get' method for usage.
|
423
|
+
|
424
|
+
def delete *args
|
425
|
+
make_request :delete, *args
|
426
|
+
end
|
427
|
+
|
428
|
+
|
429
|
+
##
|
430
|
+
# Make a HEAD request. See 'get' method for usage.
|
431
|
+
|
432
|
+
def head *args
|
433
|
+
make_request :head, *args
|
434
|
+
end
|
435
|
+
|
436
|
+
|
437
|
+
##
|
438
|
+
# Make a OPTIONS request. See 'get' method for usage.
|
439
|
+
|
440
|
+
def options *args
|
441
|
+
make_request :options, *args
|
442
|
+
end
|
443
|
+
|
444
|
+
|
445
|
+
##
|
446
|
+
# Make a mock request to the given http verb and path,
|
447
|
+
# controller+action, or named route.
|
448
|
+
#
|
449
|
+
# make_request :get, FooController, :show, :id => 123
|
450
|
+
#
|
451
|
+
# # With default_controller set to FooController
|
452
|
+
# make_request :get, :show, :id => 123
|
453
|
+
#
|
454
|
+
# # Default named route
|
455
|
+
# make_request :get, :show_foo, :id => 123
|
456
|
+
#
|
457
|
+
# # Request with headers
|
458
|
+
# make_request :get, :show_foo, {:id => 123}, 'Cookie' => 'value'
|
459
|
+
# make_request :get, :show_foo, {}, 'Cookie' => 'value'
|
460
|
+
|
461
|
+
def make_request verb, *args
|
462
|
+
headers = (Hash === args[-2] && Hash === args[-1]) ? args.pop : {}
|
463
|
+
path, query = path_to(*args).split("?")
|
464
|
+
|
465
|
+
env['HTTP_COOKIE'] = @set_cookies.map{|k,v| "#{k}=#{v}"}.join("; ") if
|
466
|
+
defined?(@set_cookies) && @set_cookies && !@set_cookies.empty?
|
467
|
+
|
468
|
+
env['REQUEST_METHOD'] = verb.to_s.upcase
|
469
|
+
env['QUERY_STRING'] = query
|
470
|
+
env['PATH_INFO'] = path
|
471
|
+
env.merge! headers
|
472
|
+
|
473
|
+
@rack_response = app.call(env)
|
474
|
+
@controller = env[Gin::Constants::GIN_CTRL]
|
475
|
+
@templates = env[Gin::Constants::GIN_TEMPLATES]
|
476
|
+
|
477
|
+
@env = nil
|
478
|
+
@body = nil
|
479
|
+
@parsed_body = nil
|
480
|
+
@set_cookies = nil
|
481
|
+
|
482
|
+
cookies.each{|n, c| set_cookie(n, c[:value]) }
|
483
|
+
|
484
|
+
@rack_response
|
485
|
+
end
|
486
|
+
|
487
|
+
|
488
|
+
##
|
489
|
+
# Sets a cookie for the next mock request.
|
490
|
+
# set_cookie "mycookie", "FOO"
|
491
|
+
|
492
|
+
def set_cookie name, value
|
493
|
+
@set_cookies ||= {}
|
494
|
+
@set_cookies[name] = value
|
495
|
+
end
|
496
|
+
|
497
|
+
|
498
|
+
COOKIE_MATCH = /\A([^(),\/<>@;:\\\"\[\]?={}\s]+)(?:=([^;]*))?\Z/ # :nodoc:
|
499
|
+
|
500
|
+
##
|
501
|
+
# Cookies assigned to the response. Will not show expired cookies,
|
502
|
+
# but cookies will otherwise persist across multiple requests in the
|
503
|
+
# same test case.
|
504
|
+
# cookies['session']
|
505
|
+
# #=> {:value => "foo", :expires => <#Time>}
|
506
|
+
|
507
|
+
def cookies
|
508
|
+
return @cookies if defined?(@cookie_key) &&
|
509
|
+
@cookie_key == rack_response[1]['Set-Cookie']
|
510
|
+
|
511
|
+
@response_cookies = {}
|
512
|
+
|
513
|
+
Array(rack_response[1]['Set-Cookie']).each do |set_cookie_value|
|
514
|
+
args = { }
|
515
|
+
params=set_cookie_value.split(/;\s*/)
|
516
|
+
|
517
|
+
first=true
|
518
|
+
params.each do |param|
|
519
|
+
result = COOKIE_MATCH.match param
|
520
|
+
if !result
|
521
|
+
raise "Invalid cookie parameter in cookie '#{set_cookie_value}'"
|
522
|
+
end
|
523
|
+
|
524
|
+
key = result[1].downcase.to_sym
|
525
|
+
keyvalue = result[2]
|
526
|
+
if first
|
527
|
+
args[:name] = result[1]
|
528
|
+
args[:value] = CGI.unescape(keyvalue.to_s)
|
529
|
+
first = false
|
530
|
+
else
|
531
|
+
case key
|
532
|
+
when :expires
|
533
|
+
begin
|
534
|
+
args[:expires_at] = Time.parse keyvalue
|
535
|
+
rescue ArgumentError
|
536
|
+
raise unless $!.message == "time out of range"
|
537
|
+
args[:expires_at] = Time.at(0x7FFFFFFF)
|
538
|
+
end
|
539
|
+
when *[:domain, :path]
|
540
|
+
args[key] = keyvalue
|
541
|
+
when :secure
|
542
|
+
args[:secure] = true
|
543
|
+
when :httponly
|
544
|
+
args[:http_only] = true
|
545
|
+
else
|
546
|
+
raise "Unknown cookie parameter '#{key}'"
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
@response_cookies[args[:name]] = args
|
552
|
+
end
|
553
|
+
|
554
|
+
@cookie_key = rack_response[1]['Set-Cookie']
|
555
|
+
(@cookies ||= {}).merge!(@response_cookies)
|
556
|
+
@cookies
|
557
|
+
end
|
558
|
+
|
559
|
+
|
560
|
+
##
|
561
|
+
# Cookies assigned by the last response.
|
562
|
+
|
563
|
+
def response_cookies
|
564
|
+
cookies unless defined?(@response_cookies)
|
565
|
+
@response_cookies ||= {}
|
566
|
+
end
|
567
|
+
|
568
|
+
|
569
|
+
##
|
570
|
+
# The read String body of the response.
|
571
|
+
|
572
|
+
def body
|
573
|
+
return @body if defined?(@body) && @body
|
574
|
+
@body = ""
|
575
|
+
rack_response[2].each{|str| @body << str }
|
576
|
+
@body
|
577
|
+
end
|
578
|
+
|
579
|
+
|
580
|
+
##
|
581
|
+
# The data representing the parsed String body
|
582
|
+
# of the response, according to the Content-Type.
|
583
|
+
#
|
584
|
+
# Supports JSON, BSON, XML, PLIST, and HTML.
|
585
|
+
# Returns plain Ruby objects for JSON, BSON, and PLIST.
|
586
|
+
# Returns a Nokogiri document object for XML and HTML.
|
587
|
+
|
588
|
+
def parsed_body
|
589
|
+
return @parsed_body if defined?(@parsed_body) && @parsed_body
|
590
|
+
ct = rack_response[1]['Content-Type']
|
591
|
+
|
592
|
+
@parsed_body =
|
593
|
+
case ct
|
594
|
+
when /[\/+]json/i
|
595
|
+
use_lib 'json'
|
596
|
+
JSON.parse(body)
|
597
|
+
|
598
|
+
when /[\/+]bson/i
|
599
|
+
use_lib 'bson'
|
600
|
+
BSON.deserialize(body)
|
601
|
+
|
602
|
+
when /[\/+]plist/i
|
603
|
+
use_lib 'plist'
|
604
|
+
Plist.parse_xml(body)
|
605
|
+
|
606
|
+
when /[\/+]xml/i
|
607
|
+
use_lib 'nokogiri'
|
608
|
+
Nokogiri::XML(body)
|
609
|
+
|
610
|
+
when /[\/+]html/i
|
611
|
+
use_lib 'nokogiri'
|
612
|
+
Nokogiri::HTML(body)
|
613
|
+
|
614
|
+
else
|
615
|
+
raise "No parser available for content-type #{ct.inspect}"
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
|
620
|
+
##
|
621
|
+
# The body stream as returned by the Rack response Array.
|
622
|
+
# Responds to #each.
|
623
|
+
|
624
|
+
def stream
|
625
|
+
rack_response[2]
|
626
|
+
end
|
627
|
+
|
628
|
+
|
629
|
+
##
|
630
|
+
# Sets the default controller to use when making requests.
|
631
|
+
# Best used in a test setup context.
|
632
|
+
#
|
633
|
+
# def setup
|
634
|
+
# default_controller HomeController
|
635
|
+
# end
|
636
|
+
|
637
|
+
def default_controller ctrl_klass=nil
|
638
|
+
@default_controller = ctrl_klass if ctrl_klass
|
639
|
+
defined?(@default_controller) && @default_controller || self.class.controller
|
640
|
+
end
|
641
|
+
|
642
|
+
|
643
|
+
##
|
644
|
+
# Build a path to the given controller and action or route name,
|
645
|
+
# with any expected params. If no controller is specified and the default
|
646
|
+
# controller responds to the symbol given, uses the default controller for
|
647
|
+
# path lookup.
|
648
|
+
#
|
649
|
+
# path_to FooController, :show, :id => 123
|
650
|
+
# #=> "/foo/123"
|
651
|
+
#
|
652
|
+
# # With default_controller set to FooController
|
653
|
+
# path_to :show, :id => 123
|
654
|
+
# #=> "/foo/123"
|
655
|
+
#
|
656
|
+
# # Default named route
|
657
|
+
# path_to :show_foo, :id => 123
|
658
|
+
# #=> "/foo/123"
|
659
|
+
|
660
|
+
def path_to *args
|
661
|
+
return "#{args[0]}#{"?" << Gin.build_query(args[1]) if args[1]}" if String === args[0]
|
662
|
+
|
663
|
+
args.unshift(@default_controller) if
|
664
|
+
Symbol === args[0] && defined?(@default_controller) &&
|
665
|
+
@default_controller && @default_controller.actions.include?(args[0])
|
666
|
+
|
667
|
+
app.router.path_to(*args)
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
|
672
|
+
class Gin::App # :nodoc:
|
673
|
+
class << self
|
674
|
+
alias old_inherited inherited
|
675
|
+
end
|
676
|
+
|
677
|
+
def self.inherited subclass
|
678
|
+
old_inherited subclass
|
679
|
+
subclass.define_test_helper
|
680
|
+
end
|
681
|
+
|
682
|
+
|
683
|
+
def self.define_test_helper
|
684
|
+
return const_get(:TestHelper) if const_defined?(:TestHelper)
|
685
|
+
class_eval <<-STR
|
686
|
+
module TestHelper
|
687
|
+
include Gin::Test::Helpers
|
688
|
+
|
689
|
+
def self.included subclass
|
690
|
+
Gin::Test::Helpers.setup_klass(subclass)
|
691
|
+
subclass.app_klass #{self}
|
692
|
+
end
|
693
|
+
end
|
694
|
+
STR
|
695
|
+
|
696
|
+
const_get :TestHelper
|
697
|
+
end
|
698
|
+
end
|
699
|
+
|
700
|
+
ObjectSpace.each_object(Class) do |klass|
|
701
|
+
klass.define_test_helper if klass < Gin::App
|
702
|
+
end
|