Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/actionpack/lib/action_controller/test_process.rb

Revision 8782, 14.9 kB (checked in by bitsweat, 1 year ago)

TestSession supports indifferent access. Closes #7372.

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