gin 1.0.4 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|