Merged updates from trunk into stable branch
[feedcatcher.git] / vendor / rails / actionpack / lib / action_controller / test_process.rb
1 module ActionController #:nodoc:
2 class TestRequest < Request #:nodoc:
3 attr_accessor :cookies, :session_options
4 attr_accessor :query_parameters, :path, :session
5 attr_accessor :host
6
7 def self.new(env = {})
8 super
9 end
10
11 def initialize(env = {})
12 super(Rack::MockRequest.env_for("/").merge(env))
13
14 @query_parameters = {}
15 @session = TestSession.new
16
17 initialize_default_values
18 initialize_containers
19 end
20
21 def reset_session
22 @session.reset
23 end
24
25 # Wraps raw_post in a StringIO.
26 def body_stream #:nodoc:
27 StringIO.new(raw_post)
28 end
29
30 # Either the RAW_POST_DATA environment variable or the URL-encoded request
31 # parameters.
32 def raw_post
33 @env['RAW_POST_DATA'] ||= begin
34 data = url_encoded_request_parameters
35 data.force_encoding(Encoding::BINARY) if data.respond_to?(:force_encoding)
36 data
37 end
38 end
39
40 def port=(number)
41 @env["SERVER_PORT"] = number.to_i
42 end
43
44 def action=(action_name)
45 @query_parameters.update({ "action" => action_name })
46 @parameters = nil
47 end
48
49 # Used to check AbstractRequest's request_uri functionality.
50 # Disables the use of @path and @request_uri so superclass can handle those.
51 def set_REQUEST_URI(value)
52 @env["REQUEST_URI"] = value
53 @request_uri = nil
54 @path = nil
55 end
56
57 def request_uri=(uri)
58 @request_uri = uri
59 @path = uri.split("?").first
60 end
61
62 def request_method=(method)
63 @request_method = method
64 end
65
66 def accept=(mime_types)
67 @env["HTTP_ACCEPT"] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",")
68 @accepts = nil
69 end
70
71 def if_modified_since=(last_modified)
72 @env["HTTP_IF_MODIFIED_SINCE"] = last_modified
73 end
74
75 def if_none_match=(etag)
76 @env["HTTP_IF_NONE_MATCH"] = etag
77 end
78
79 def remote_addr=(addr)
80 @env['REMOTE_ADDR'] = addr
81 end
82
83 def request_uri(*args)
84 @request_uri || super()
85 end
86
87 def path(*args)
88 @path || super()
89 end
90
91 def assign_parameters(controller_path, action, parameters)
92 parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action)
93 extra_keys = ActionController::Routing::Routes.extra_keys(parameters)
94 non_path_parameters = get? ? query_parameters : request_parameters
95 parameters.each do |key, value|
96 if value.is_a? Fixnum
97 value = value.to_s
98 elsif value.is_a? Array
99 value = ActionController::Routing::PathSegment::Result.new(value)
100 end
101
102 if extra_keys.include?(key.to_sym)
103 non_path_parameters[key] = value
104 else
105 path_parameters[key.to_s] = value
106 end
107 end
108 raw_post # populate env['RAW_POST_DATA']
109 @parameters = nil # reset TestRequest#parameters to use the new path_parameters
110 end
111
112 def recycle!
113 self.query_parameters = {}
114 self.path_parameters = {}
115 @headers, @request_method, @accepts, @content_type = nil, nil, nil, nil
116 end
117
118 def user_agent=(user_agent)
119 @env['HTTP_USER_AGENT'] = user_agent
120 end
121
122 private
123 def initialize_containers
124 @cookies = {}
125 end
126
127 def initialize_default_values
128 @host = "test.host"
129 @request_uri = "/"
130 @env['HTTP_USER_AGENT'] = "Rails Testing"
131 @env['REMOTE_ADDR'] = "0.0.0.0"
132 @env["SERVER_PORT"] = 80
133 @env['REQUEST_METHOD'] = "GET"
134 end
135
136 def url_encoded_request_parameters
137 params = self.request_parameters.dup
138
139 %w(controller action only_path).each do |k|
140 params.delete(k)
141 params.delete(k.to_sym)
142 end
143
144 params.to_query
145 end
146 end
147
148 # A refactoring of TestResponse to allow the same behavior to be applied
149 # to the "real" CgiResponse class in integration tests.
150 module TestResponseBehavior #:nodoc:
151 # The response code of the request
152 def response_code
153 status.to_s[0,3].to_i rescue 0
154 end
155
156 # Returns a String to ensure compatibility with Net::HTTPResponse
157 def code
158 status.to_s.split(' ')[0]
159 end
160
161 def message
162 status.to_s.split(' ',2)[1]
163 end
164
165 # Was the response successful?
166 def success?
167 (200..299).include?(response_code)
168 end
169
170 # Was the URL not found?
171 def missing?
172 response_code == 404
173 end
174
175 # Were we redirected?
176 def redirect?
177 (300..399).include?(response_code)
178 end
179
180 # Was there a server-side error?
181 def error?
182 (500..599).include?(response_code)
183 end
184
185 alias_method :server_error?, :error?
186
187 # Was there a client client?
188 def client_error?
189 (400..499).include?(response_code)
190 end
191
192 # Returns the redirection location or nil
193 def redirect_url
194 headers['Location']
195 end
196
197 # Does the redirect location match this regexp pattern?
198 def redirect_url_match?( pattern )
199 return false if redirect_url.nil?
200 p = Regexp.new(pattern) if pattern.class == String
201 p = pattern if pattern.class == Regexp
202 return false if p.nil?
203 p.match(redirect_url) != nil
204 end
205
206 # Returns the template of the file which was used to
207 # render this response (or nil)
208 def rendered
209 template.instance_variable_get(:@_rendered)
210 end
211
212 # A shortcut to the flash. Returns an empty hash if no session flash exists.
213 def flash
214 session['flash'] || {}
215 end
216
217 # Do we have a flash?
218 def has_flash?
219 !flash.empty?
220 end
221
222 # Do we have a flash that has contents?
223 def has_flash_with_contents?
224 !flash.empty?
225 end
226
227 # Does the specified flash object exist?
228 def has_flash_object?(name=nil)
229 !flash[name].nil?
230 end
231
232 # Does the specified object exist in the session?
233 def has_session_object?(name=nil)
234 !session[name].nil?
235 end
236
237 # A shortcut to the template.assigns
238 def template_objects
239 template.assigns || {}
240 end
241
242 # Does the specified template object exist?
243 def has_template_object?(name=nil)
244 !template_objects[name].nil?
245 end
246
247 # Returns the response cookies, converted to a Hash of (name => value) pairs
248 #
249 # assert_equal 'AuthorOfNewPage', r.cookies['author']
250 def cookies
251 cookies = {}
252 Array(headers['Set-Cookie']).each do |cookie|
253 key, value = cookie.split(";").first.split("=")
254 cookies[key] = value
255 end
256 cookies
257 end
258
259 # Returns binary content (downloadable file), converted to a String
260 def binary_content
261 raise "Response body is not a Proc: #{body.inspect}" unless body.kind_of?(Proc)
262 require 'stringio'
263
264 sio = StringIO.new
265 body.call(self, sio)
266
267 sio.rewind
268 sio.read
269 end
270 end
271
272 # Integration test methods such as ActionController::Integration::Session#get
273 # and ActionController::Integration::Session#post return objects of class
274 # TestResponse, which represent the HTTP response results of the requested
275 # controller actions.
276 #
277 # See Response for more information on controller response objects.
278 class TestResponse < Response
279 include TestResponseBehavior
280
281 def recycle!
282 headers.delete('ETag')
283 headers.delete('Last-Modified')
284 end
285 end
286
287 class TestSession < Hash #:nodoc:
288 attr_accessor :session_id
289
290 def initialize(attributes = nil)
291 reset_session_id
292 replace_attributes(attributes)
293 end
294
295 def reset
296 reset_session_id
297 replace_attributes({ })
298 end
299
300 def data
301 to_hash
302 end
303
304 def [](key)
305 super(key.to_s)
306 end
307
308 def []=(key, value)
309 super(key.to_s, value)
310 end
311
312 def update(hash = nil)
313 if hash.nil?
314 ActiveSupport::Deprecation.warn('use replace instead', caller)
315 replace({})
316 else
317 super(hash)
318 end
319 end
320
321 def delete(key = nil)
322 if key.nil?
323 ActiveSupport::Deprecation.warn('use clear instead', caller)
324 clear
325 else
326 super(key.to_s)
327 end
328 end
329
330 def close
331 ActiveSupport::Deprecation.warn('sessions should no longer be closed', caller)
332 end
333
334 private
335
336 def reset_session_id
337 @session_id = ''
338 end
339
340 def replace_attributes(attributes = nil)
341 attributes ||= {}
342 replace(attributes.stringify_keys)
343 end
344 end
345
346 # Essentially generates a modified Tempfile object similar to the object
347 # you'd get from the standard library CGI module in a multipart
348 # request. This means you can use an ActionController::TestUploadedFile
349 # object in the params of a test request in order to simulate
350 # a file upload.
351 #
352 # Usage example, within a functional test:
353 # post :change_avatar, :avatar => ActionController::TestUploadedFile.new(ActionController::TestCase.fixture_path + '/files/spongebob.png', 'image/png')
354 #
355 # Pass a true third parameter to ensure the uploaded file is opened in binary mode (only required for Windows):
356 # post :change_avatar, :avatar => ActionController::TestUploadedFile.new(ActionController::TestCase.fixture_path + '/files/spongebob.png', 'image/png', :binary)
357 require 'tempfile'
358 class TestUploadedFile
359 # The filename, *not* including the path, of the "uploaded" file
360 attr_reader :original_filename
361
362 # The content type of the "uploaded" file
363 attr_accessor :content_type
364
365 def initialize(path, content_type = Mime::TEXT, binary = false)
366 raise "#{path} file does not exist" unless File.exist?(path)
367 @content_type = content_type
368 @original_filename = path.sub(/^.*#{File::SEPARATOR}([^#{File::SEPARATOR}]+)$/) { $1 }
369 @tempfile = Tempfile.new(@original_filename)
370 @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
371 @tempfile.binmode if binary
372 FileUtils.copy_file(path, @tempfile.path)
373 end
374
375 def path #:nodoc:
376 @tempfile.path
377 end
378
379 alias local_path path
380
381 def method_missing(method_name, *args, &block) #:nodoc:
382 @tempfile.__send__(method_name, *args, &block)
383 end
384 end
385
386 module TestProcess
387 def self.included(base)
388 # Executes a request simulating GET HTTP method and set/volley the response
389 def get(action, parameters = nil, session = nil, flash = nil)
390 process(action, parameters, session, flash, "GET")
391 end
392
393 # Executes a request simulating POST HTTP method and set/volley the response
394 def post(action, parameters = nil, session = nil, flash = nil)
395 process(action, parameters, session, flash, "POST")
396 end
397
398 # Executes a request simulating PUT HTTP method and set/volley the response
399 def put(action, parameters = nil, session = nil, flash = nil)
400 process(action, parameters, session, flash, "PUT")
401 end
402
403 # Executes a request simulating DELETE HTTP method and set/volley the response
404 def delete(action, parameters = nil, session = nil, flash = nil)
405 process(action, parameters, session, flash, "DELETE")
406 end
407
408 # Executes a request simulating HEAD HTTP method and set/volley the response
409 def head(action, parameters = nil, session = nil, flash = nil)
410 process(action, parameters, session, flash, "HEAD")
411 end
412 end
413
414 def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET')
415 # Sanity check for required instance variables so we can give an
416 # understandable error message.
417 %w(@controller @request @response).each do |iv_name|
418 if !(instance_variable_names.include?(iv_name) || instance_variable_names.include?(iv_name.to_sym)) || instance_variable_get(iv_name).nil?
419 raise "#{iv_name} is nil: make sure you set it in your test's setup method."
420 end
421 end
422
423 @request.recycle!
424 @response.recycle!
425
426 @html_document = nil
427 @request.env['REQUEST_METHOD'] = http_method
428
429 @request.action = action.to_s
430
431 parameters ||= {}
432 @request.assign_parameters(@controller.class.controller_path, action.to_s, parameters)
433
434 @request.session = ActionController::TestSession.new(session) unless session.nil?
435 @request.session["flash"] = ActionController::Flash::FlashHash.new.update(flash) if flash
436 build_request_uri(action, parameters)
437
438 Base.class_eval { include ProcessWithTest } unless Base < ProcessWithTest
439 @controller.process_with_test(@request, @response)
440 end
441
442 def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
443 @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
444 @request.env['HTTP_ACCEPT'] = [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
445 returning __send__(request_method, action, parameters, session, flash) do
446 @request.env.delete 'HTTP_X_REQUESTED_WITH'
447 @request.env.delete 'HTTP_ACCEPT'
448 end
449 end
450 alias xhr :xml_http_request
451
452 def assigns(key = nil)
453 if key.nil?
454 @response.template.assigns
455 else
456 @response.template.assigns[key.to_s]
457 end
458 end
459
460 def session
461 @request.session
462 end
463
464 def flash
465 @response.flash
466 end
467
468 def cookies
469 @response.cookies
470 end
471
472 def redirect_to_url
473 @response.redirect_url
474 end
475
476 def build_request_uri(action, parameters)
477 unless @request.env['REQUEST_URI']
478 options = @controller.__send__(:rewrite_options, parameters)
479 options.update(:only_path => true, :action => action)
480
481 url = ActionController::UrlRewriter.new(@request, parameters)
482 @request.set_REQUEST_URI(url.rewrite(options))
483 end
484 end
485
486 def html_document
487 xml = @response.content_type =~ /xml$/
488 @html_document ||= HTML::Document.new(@response.body, false, xml)
489 end
490
491 def find_tag(conditions)
492 html_document.find(conditions)
493 end
494
495 def find_all_tag(conditions)
496 html_document.find_all(conditions)
497 end
498
499 def method_missing(selector, *args, &block)
500 if @controller && ActionController::Routing::Routes.named_routes.helpers.include?(selector)
501 @controller.send(selector, *args, &block)
502 else
503 super
504 end
505 end
506
507 # Shortcut for <tt>ActionController::TestUploadedFile.new(ActionController::TestCase.fixture_path + path, type)</tt>:
508 #
509 # post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png')
510 #
511 # To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter.
512 # This will not affect other platforms:
513 #
514 # post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png', :binary)
515 def fixture_file_upload(path, mime_type = nil, binary = false)
516 fixture_path = ActionController::TestCase.send(:fixture_path) if ActionController::TestCase.respond_to?(:fixture_path)
517 ActionController::TestUploadedFile.new("#{fixture_path}#{path}", mime_type, binary)
518 end
519
520 # A helper to make it easier to test different route configurations.
521 # This method temporarily replaces ActionController::Routing::Routes
522 # with a new RouteSet instance.
523 #
524 # The new instance is yielded to the passed block. Typically the block
525 # will create some routes using <tt>map.draw { map.connect ... }</tt>:
526 #
527 # with_routing do |set|
528 # set.draw do |map|
529 # map.connect ':controller/:action/:id'
530 # assert_equal(
531 # ['/content/10/show', {}],
532 # map.generate(:controller => 'content', :id => 10, :action => 'show')
533 # end
534 # end
535 # end
536 #
537 def with_routing
538 real_routes = ActionController::Routing::Routes
539 ActionController::Routing.module_eval { remove_const :Routes }
540
541 temporary_routes = ActionController::Routing::RouteSet.new
542 ActionController::Routing.module_eval { const_set :Routes, temporary_routes }
543
544 yield temporary_routes
545 ensure
546 if ActionController::Routing.const_defined? :Routes
547 ActionController::Routing.module_eval { remove_const :Routes }
548 end
549 ActionController::Routing.const_set(:Routes, real_routes) if real_routes
550 end
551 end
552
553 module ProcessWithTest #:nodoc:
554 def self.included(base)
555 base.class_eval { attr_reader :assigns }
556 end
557
558 def process_with_test(*args)
559 process(*args).tap { set_test_assigns }
560 end
561
562 private
563 def set_test_assigns
564 @assigns = {}
565 (instance_variable_names - self.class.protected_instance_variables).each do |var|
566 name, value = var[1..-1], instance_variable_get(var)
567 @assigns[name] = value
568 response.template.assigns[name] = value if response
569 end
570 end
571 end
572 end