actionpack 5.2.3

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (170) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +429 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +57 -0
  5. data/lib/abstract_controller.rb +27 -0
  6. data/lib/abstract_controller/asset_paths.rb +12 -0
  7. data/lib/abstract_controller/base.rb +265 -0
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/caching/fragments.rb +166 -0
  10. data/lib/abstract_controller/callbacks.rb +212 -0
  11. data/lib/abstract_controller/collector.rb +43 -0
  12. data/lib/abstract_controller/error.rb +6 -0
  13. data/lib/abstract_controller/helpers.rb +194 -0
  14. data/lib/abstract_controller/logger.rb +14 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +20 -0
  16. data/lib/abstract_controller/rendering.rb +127 -0
  17. data/lib/abstract_controller/translation.rb +31 -0
  18. data/lib/abstract_controller/url_for.rb +35 -0
  19. data/lib/action_controller.rb +66 -0
  20. data/lib/action_controller/api.rb +149 -0
  21. data/lib/action_controller/api/api_rendering.rb +16 -0
  22. data/lib/action_controller/base.rb +276 -0
  23. data/lib/action_controller/caching.rb +46 -0
  24. data/lib/action_controller/form_builder.rb +50 -0
  25. data/lib/action_controller/log_subscriber.rb +78 -0
  26. data/lib/action_controller/metal.rb +256 -0
  27. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  28. data/lib/action_controller/metal/conditional_get.rb +274 -0
  29. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  30. data/lib/action_controller/metal/cookies.rb +16 -0
  31. data/lib/action_controller/metal/data_streaming.rb +152 -0
  32. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +57 -0
  34. data/lib/action_controller/metal/exceptions.rb +53 -0
  35. data/lib/action_controller/metal/flash.rb +61 -0
  36. data/lib/action_controller/metal/force_ssl.rb +99 -0
  37. data/lib/action_controller/metal/head.rb +60 -0
  38. data/lib/action_controller/metal/helpers.rb +123 -0
  39. data/lib/action_controller/metal/http_authentication.rb +519 -0
  40. data/lib/action_controller/metal/implicit_render.rb +73 -0
  41. data/lib/action_controller/metal/instrumentation.rb +107 -0
  42. data/lib/action_controller/metal/live.rb +312 -0
  43. data/lib/action_controller/metal/mime_responds.rb +313 -0
  44. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  45. data/lib/action_controller/metal/params_wrapper.rb +293 -0
  46. data/lib/action_controller/metal/redirecting.rb +133 -0
  47. data/lib/action_controller/metal/renderers.rb +181 -0
  48. data/lib/action_controller/metal/rendering.rb +122 -0
  49. data/lib/action_controller/metal/request_forgery_protection.rb +445 -0
  50. data/lib/action_controller/metal/rescue.rb +28 -0
  51. data/lib/action_controller/metal/streaming.rb +223 -0
  52. data/lib/action_controller/metal/strong_parameters.rb +1086 -0
  53. data/lib/action_controller/metal/testing.rb +16 -0
  54. data/lib/action_controller/metal/url_for.rb +58 -0
  55. data/lib/action_controller/railtie.rb +89 -0
  56. data/lib/action_controller/railties/helpers.rb +24 -0
  57. data/lib/action_controller/renderer.rb +117 -0
  58. data/lib/action_controller/template_assertions.rb +11 -0
  59. data/lib/action_controller/test_case.rb +629 -0
  60. data/lib/action_dispatch.rb +112 -0
  61. data/lib/action_dispatch/http/cache.rb +222 -0
  62. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  63. data/lib/action_dispatch/http/filter_parameters.rb +84 -0
  64. data/lib/action_dispatch/http/filter_redirect.rb +37 -0
  65. data/lib/action_dispatch/http/headers.rb +132 -0
  66. data/lib/action_dispatch/http/mime_negotiation.rb +175 -0
  67. data/lib/action_dispatch/http/mime_type.rb +342 -0
  68. data/lib/action_dispatch/http/mime_types.rb +50 -0
  69. data/lib/action_dispatch/http/parameter_filter.rb +86 -0
  70. data/lib/action_dispatch/http/parameters.rb +126 -0
  71. data/lib/action_dispatch/http/rack_cache.rb +63 -0
  72. data/lib/action_dispatch/http/request.rb +430 -0
  73. data/lib/action_dispatch/http/response.rb +519 -0
  74. data/lib/action_dispatch/http/upload.rb +84 -0
  75. data/lib/action_dispatch/http/url.rb +350 -0
  76. data/lib/action_dispatch/journey.rb +7 -0
  77. data/lib/action_dispatch/journey/formatter.rb +189 -0
  78. data/lib/action_dispatch/journey/gtg/builder.rb +164 -0
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +41 -0
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +158 -0
  81. data/lib/action_dispatch/journey/nfa/builder.rb +78 -0
  82. data/lib/action_dispatch/journey/nfa/dot.rb +36 -0
  83. data/lib/action_dispatch/journey/nfa/simulator.rb +49 -0
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +120 -0
  85. data/lib/action_dispatch/journey/nodes/node.rb +140 -0
  86. data/lib/action_dispatch/journey/parser.rb +199 -0
  87. data/lib/action_dispatch/journey/parser.y +50 -0
  88. data/lib/action_dispatch/journey/parser_extras.rb +31 -0
  89. data/lib/action_dispatch/journey/path/pattern.rb +198 -0
  90. data/lib/action_dispatch/journey/route.rb +203 -0
  91. data/lib/action_dispatch/journey/router.rb +156 -0
  92. data/lib/action_dispatch/journey/router/utils.rb +102 -0
  93. data/lib/action_dispatch/journey/routes.rb +82 -0
  94. data/lib/action_dispatch/journey/scanner.rb +64 -0
  95. data/lib/action_dispatch/journey/visitors.rb +268 -0
  96. data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
  97. data/lib/action_dispatch/journey/visualizer/fsm.js +134 -0
  98. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  99. data/lib/action_dispatch/middleware/callbacks.rb +36 -0
  100. data/lib/action_dispatch/middleware/cookies.rb +685 -0
  101. data/lib/action_dispatch/middleware/debug_exceptions.rb +205 -0
  102. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  103. data/lib/action_dispatch/middleware/exception_wrapper.rb +147 -0
  104. data/lib/action_dispatch/middleware/executor.rb +21 -0
  105. data/lib/action_dispatch/middleware/flash.rb +300 -0
  106. data/lib/action_dispatch/middleware/public_exceptions.rb +57 -0
  107. data/lib/action_dispatch/middleware/reloader.rb +12 -0
  108. data/lib/action_dispatch/middleware/remote_ip.rb +183 -0
  109. data/lib/action_dispatch/middleware/request_id.rb +43 -0
  110. data/lib/action_dispatch/middleware/session/abstract_store.rb +92 -0
  111. data/lib/action_dispatch/middleware/session/cache_store.rb +54 -0
  112. data/lib/action_dispatch/middleware/session/cookie_store.rb +118 -0
  113. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +28 -0
  114. data/lib/action_dispatch/middleware/show_exceptions.rb +62 -0
  115. data/lib/action_dispatch/middleware/ssl.rb +150 -0
  116. data/lib/action_dispatch/middleware/stack.rb +116 -0
  117. data/lib/action_dispatch/middleware/static.rb +130 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +22 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +27 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  122. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +52 -0
  123. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
  124. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +16 -0
  125. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  126. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +161 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
  137. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +16 -0
  138. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +200 -0
  139. data/lib/action_dispatch/railtie.rb +55 -0
  140. data/lib/action_dispatch/request/session.rb +234 -0
  141. data/lib/action_dispatch/request/utils.rb +78 -0
  142. data/lib/action_dispatch/routing.rb +260 -0
  143. data/lib/action_dispatch/routing/endpoint.rb +17 -0
  144. data/lib/action_dispatch/routing/inspector.rb +225 -0
  145. data/lib/action_dispatch/routing/mapper.rb +2267 -0
  146. data/lib/action_dispatch/routing/polymorphic_routes.rb +352 -0
  147. data/lib/action_dispatch/routing/redirection.rb +201 -0
  148. data/lib/action_dispatch/routing/route_set.rb +890 -0
  149. data/lib/action_dispatch/routing/routes_proxy.rb +69 -0
  150. data/lib/action_dispatch/routing/url_for.rb +236 -0
  151. data/lib/action_dispatch/system_test_case.rb +147 -0
  152. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  153. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  154. data/lib/action_dispatch/system_testing/server.rb +31 -0
  155. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  156. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  157. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  158. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  159. data/lib/action_dispatch/testing/assertions.rb +24 -0
  160. data/lib/action_dispatch/testing/assertions/response.rb +107 -0
  161. data/lib/action_dispatch/testing/assertions/routing.rb +222 -0
  162. data/lib/action_dispatch/testing/integration.rb +652 -0
  163. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  164. data/lib/action_dispatch/testing/test_process.rb +50 -0
  165. data/lib/action_dispatch/testing/test_request.rb +71 -0
  166. data/lib/action_dispatch/testing/test_response.rb +53 -0
  167. data/lib/action_pack.rb +26 -0
  168. data/lib/action_pack/gem_version.rb +17 -0
  169. data/lib/action_pack/version.rb +10 -0
  170. metadata +318 -0
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "active_support/core_ext/hash/indifferent_access"
5
+ require "active_support/core_ext/string/access"
6
+ require "action_controller/metal/exceptions"
7
+
8
+ module ActionDispatch
9
+ module Assertions
10
+ # Suite of assertions to test routes generated by \Rails and the handling of requests made to them.
11
+ module RoutingAssertions
12
+ # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash)
13
+ # match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+.
14
+ #
15
+ # Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes
16
+ # requiring a specific HTTP method. The hash should contain a :path with the incoming request path
17
+ # and a :method containing the required HTTP verb.
18
+ #
19
+ # # Asserts that POSTing to /items will call the create action on ItemsController
20
+ # assert_recognizes({controller: 'items', action: 'create'}, {path: 'items', method: :post})
21
+ #
22
+ # You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used
23
+ # to assert that values in the query string will end up in the params hash correctly. To test query strings you must use the extras
24
+ # argument because appending the query string on the path directly will not work. For example:
25
+ #
26
+ # # Asserts that a path of '/items/list/1?view=print' returns the correct options
27
+ # assert_recognizes({controller: 'items', action: 'list', id: '1', view: 'print'}, 'items/list/1', { view: "print" })
28
+ #
29
+ # The +message+ parameter allows you to pass in an error message that is displayed upon failure.
30
+ #
31
+ # # Check the default route (i.e., the index action)
32
+ # assert_recognizes({controller: 'items', action: 'index'}, 'items')
33
+ #
34
+ # # Test a specific action
35
+ # assert_recognizes({controller: 'items', action: 'list'}, 'items/list')
36
+ #
37
+ # # Test an action with a parameter
38
+ # assert_recognizes({controller: 'items', action: 'destroy', id: '1'}, 'items/destroy/1')
39
+ #
40
+ # # Test a custom route
41
+ # assert_recognizes({controller: 'items', action: 'show', id: '1'}, 'view/item1')
42
+ def assert_recognizes(expected_options, path, extras = {}, msg = nil)
43
+ if path.is_a?(Hash) && path[:method].to_s == "all"
44
+ [:get, :post, :put, :delete].each do |method|
45
+ assert_recognizes(expected_options, path.merge(method: method), extras, msg)
46
+ end
47
+ else
48
+ request = recognized_request_for(path, extras, msg)
49
+
50
+ expected_options = expected_options.clone
51
+
52
+ expected_options.stringify_keys!
53
+
54
+ msg = message(msg, "") {
55
+ sprintf("The recognized options <%s> did not match <%s>, difference:",
56
+ request.path_parameters, expected_options)
57
+ }
58
+
59
+ assert_equal(expected_options, request.path_parameters, msg)
60
+ end
61
+ end
62
+
63
+ # Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+.
64
+ # The +extras+ parameter is used to tell the request the names and values of additional request parameters that would be in
65
+ # a query string. The +message+ parameter allows you to specify a custom error message for assertion failures.
66
+ #
67
+ # The +defaults+ parameter is unused.
68
+ #
69
+ # # Asserts that the default action is generated for a route with no action
70
+ # assert_generates "/items", controller: "items", action: "index"
71
+ #
72
+ # # Tests that the list action is properly routed
73
+ # assert_generates "/items/list", controller: "items", action: "list"
74
+ #
75
+ # # Tests the generation of a route with a parameter
76
+ # assert_generates "/items/list/1", { controller: "items", action: "list", id: "1" }
77
+ #
78
+ # # Asserts that the generated route gives us our custom route
79
+ # assert_generates "changesets/12", { controller: 'scm', action: 'show_diff', revision: "12" }
80
+ def assert_generates(expected_path, options, defaults = {}, extras = {}, message = nil)
81
+ if expected_path =~ %r{://}
82
+ fail_on(URI::InvalidURIError, message) do
83
+ uri = URI.parse(expected_path)
84
+ expected_path = uri.path.to_s.empty? ? "/" : uri.path
85
+ end
86
+ else
87
+ expected_path = "/#{expected_path}" unless expected_path.first == "/"
88
+ end
89
+ # Load routes.rb if it hasn't been loaded.
90
+
91
+ options = options.clone
92
+ generated_path, query_string_keys = @routes.generate_extras(options, defaults)
93
+ found_extras = options.reject { |k, _| ! query_string_keys.include? k }
94
+
95
+ msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras)
96
+ assert_equal(extras, found_extras, msg)
97
+
98
+ msg = message || sprintf("The generated path <%s> did not match <%s>", generated_path,
99
+ expected_path)
100
+ assert_equal(expected_path, generated_path, msg)
101
+ end
102
+
103
+ # Asserts that path and options match both ways; in other words, it verifies that <tt>path</tt> generates
104
+ # <tt>options</tt> and then that <tt>options</tt> generates <tt>path</tt>. This essentially combines +assert_recognizes+
105
+ # and +assert_generates+ into one step.
106
+ #
107
+ # The +extras+ hash allows you to specify options that would normally be provided as a query string to the action. The
108
+ # +message+ parameter allows you to specify a custom error message to display upon failure.
109
+ #
110
+ # # Asserts a basic route: a controller with the default action (index)
111
+ # assert_routing '/home', controller: 'home', action: 'index'
112
+ #
113
+ # # Test a route generated with a specific controller, action, and parameter (id)
114
+ # assert_routing '/entries/show/23', controller: 'entries', action: 'show', id: 23
115
+ #
116
+ # # Asserts a basic route (controller + default action), with an error message if it fails
117
+ # assert_routing '/store', { controller: 'store', action: 'index' }, {}, {}, 'Route for store index not generated properly'
118
+ #
119
+ # # Tests a route, providing a defaults hash
120
+ # assert_routing 'controller/action/9', {id: "9", item: "square"}, {controller: "controller", action: "action"}, {}, {item: "square"}
121
+ #
122
+ # # Tests a route with an HTTP method
123
+ # assert_routing({ method: 'put', path: '/product/321' }, { controller: "product", action: "update", id: "321" })
124
+ def assert_routing(path, options, defaults = {}, extras = {}, message = nil)
125
+ assert_recognizes(options, path, extras, message)
126
+
127
+ controller, default_controller = options[:controller], defaults[:controller]
128
+ if controller && controller.include?(?/) && default_controller && default_controller.include?(?/)
129
+ options[:controller] = "/#{controller}"
130
+ end
131
+
132
+ generate_options = options.dup.delete_if { |k, _| defaults.key?(k) }
133
+ assert_generates(path.is_a?(Hash) ? path[:path] : path, generate_options, defaults, extras, message)
134
+ end
135
+
136
+ # A helper to make it easier to test different route configurations.
137
+ # This method temporarily replaces @routes with a new RouteSet instance.
138
+ #
139
+ # The new instance is yielded to the passed block. Typically the block
140
+ # will create some routes using <tt>set.draw { match ... }</tt>:
141
+ #
142
+ # with_routing do |set|
143
+ # set.draw do
144
+ # resources :users
145
+ # end
146
+ # assert_equal "/users", users_path
147
+ # end
148
+ #
149
+ def with_routing
150
+ old_routes, @routes = @routes, ActionDispatch::Routing::RouteSet.new
151
+ if defined?(@controller) && @controller
152
+ old_controller, @controller = @controller, @controller.clone
153
+ _routes = @routes
154
+
155
+ @controller.singleton_class.include(_routes.url_helpers)
156
+
157
+ if @controller.respond_to? :view_context_class
158
+ @controller.view_context_class = Class.new(@controller.view_context_class) do
159
+ include _routes.url_helpers
160
+ end
161
+ end
162
+ end
163
+ yield @routes
164
+ ensure
165
+ @routes = old_routes
166
+ if defined?(@controller) && @controller
167
+ @controller = old_controller
168
+ end
169
+ end
170
+
171
+ # ROUTES TODO: These assertions should really work in an integration context
172
+ def method_missing(selector, *args, &block)
173
+ if defined?(@controller) && @controller && defined?(@routes) && @routes && @routes.named_routes.route_defined?(selector)
174
+ @controller.send(selector, *args, &block)
175
+ else
176
+ super
177
+ end
178
+ end
179
+
180
+ private
181
+ # Recognizes the route for a given path.
182
+ def recognized_request_for(path, extras = {}, msg)
183
+ if path.is_a?(Hash)
184
+ method = path[:method]
185
+ path = path[:path]
186
+ else
187
+ method = :get
188
+ end
189
+
190
+ request = ActionController::TestRequest.create @controller.class
191
+
192
+ if path =~ %r{://}
193
+ fail_on(URI::InvalidURIError, msg) do
194
+ uri = URI.parse(path)
195
+ request.env["rack.url_scheme"] = uri.scheme || "http"
196
+ request.host = uri.host if uri.host
197
+ request.port = uri.port if uri.port
198
+ request.path = uri.path.to_s.empty? ? "/" : uri.path
199
+ end
200
+ else
201
+ path = "/#{path}" unless path.first == "/"
202
+ request.path = path
203
+ end
204
+
205
+ request.request_method = method if method
206
+
207
+ params = fail_on(ActionController::RoutingError, msg) do
208
+ @routes.recognize_path(path, method: method, extras: extras)
209
+ end
210
+ request.path_parameters = params.with_indifferent_access
211
+
212
+ request
213
+ end
214
+
215
+ def fail_on(exception_class, message)
216
+ yield
217
+ rescue exception_class => e
218
+ raise Minitest::Assertion, message || e.message
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,652 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require "uri"
5
+ require "active_support/core_ext/kernel/singleton_class"
6
+ require "active_support/core_ext/object/try"
7
+ require "rack/test"
8
+ require "minitest"
9
+
10
+ require "action_dispatch/testing/request_encoder"
11
+
12
+ module ActionDispatch
13
+ module Integration #:nodoc:
14
+ module RequestHelpers
15
+ # Performs a GET request with the given parameters. See ActionDispatch::Integration::Session#process
16
+ # for more details.
17
+ def get(path, **args)
18
+ process(:get, path, **args)
19
+ end
20
+
21
+ # Performs a POST request with the given parameters. See ActionDispatch::Integration::Session#process
22
+ # for more details.
23
+ def post(path, **args)
24
+ process(:post, path, **args)
25
+ end
26
+
27
+ # Performs a PATCH request with the given parameters. See ActionDispatch::Integration::Session#process
28
+ # for more details.
29
+ def patch(path, **args)
30
+ process(:patch, path, **args)
31
+ end
32
+
33
+ # Performs a PUT request with the given parameters. See ActionDispatch::Integration::Session#process
34
+ # for more details.
35
+ def put(path, **args)
36
+ process(:put, path, **args)
37
+ end
38
+
39
+ # Performs a DELETE request with the given parameters. See ActionDispatch::Integration::Session#process
40
+ # for more details.
41
+ def delete(path, **args)
42
+ process(:delete, path, **args)
43
+ end
44
+
45
+ # Performs a HEAD request with the given parameters. See ActionDispatch::Integration::Session#process
46
+ # for more details.
47
+ def head(path, *args)
48
+ process(:head, path, *args)
49
+ end
50
+
51
+ # Follow a single redirect response. If the last response was not a
52
+ # redirect, an exception will be raised. Otherwise, the redirect is
53
+ # performed on the location header.
54
+ def follow_redirect!
55
+ raise "not a redirect! #{status} #{status_message}" unless redirect?
56
+ get(response.location)
57
+ status
58
+ end
59
+ end
60
+
61
+ # An instance of this class represents a set of requests and responses
62
+ # performed sequentially by a test process. Because you can instantiate
63
+ # multiple sessions and run them side-by-side, you can also mimic (to some
64
+ # limited extent) multiple simultaneous users interacting with your system.
65
+ #
66
+ # Typically, you will instantiate a new session using
67
+ # IntegrationTest#open_session, rather than instantiating
68
+ # Integration::Session directly.
69
+ class Session
70
+ DEFAULT_HOST = "www.example.com"
71
+
72
+ include Minitest::Assertions
73
+ include TestProcess, RequestHelpers, Assertions
74
+
75
+ %w( status status_message headers body redirect? ).each do |method|
76
+ delegate method, to: :response, allow_nil: true
77
+ end
78
+
79
+ %w( path ).each do |method|
80
+ delegate method, to: :request, allow_nil: true
81
+ end
82
+
83
+ # The hostname used in the last request.
84
+ def host
85
+ @host || DEFAULT_HOST
86
+ end
87
+ attr_writer :host
88
+
89
+ # The remote_addr used in the last request.
90
+ attr_accessor :remote_addr
91
+
92
+ # The Accept header to send.
93
+ attr_accessor :accept
94
+
95
+ # A map of the cookies returned by the last response, and which will be
96
+ # sent with the next request.
97
+ def cookies
98
+ _mock_session.cookie_jar
99
+ end
100
+
101
+ # A reference to the controller instance used by the last request.
102
+ attr_reader :controller
103
+
104
+ # A reference to the request instance used by the last request.
105
+ attr_reader :request
106
+
107
+ # A reference to the response instance used by the last request.
108
+ attr_reader :response
109
+
110
+ # A running counter of the number of requests processed.
111
+ attr_accessor :request_count
112
+
113
+ include ActionDispatch::Routing::UrlFor
114
+
115
+ # Create and initialize a new Session instance.
116
+ def initialize(app)
117
+ super()
118
+ @app = app
119
+
120
+ reset!
121
+ end
122
+
123
+ def url_options
124
+ @url_options ||= default_url_options.dup.tap do |url_options|
125
+ url_options.reverse_merge!(controller.url_options) if controller
126
+
127
+ if @app.respond_to?(:routes)
128
+ url_options.reverse_merge!(@app.routes.default_url_options)
129
+ end
130
+
131
+ url_options.reverse_merge!(host: host, protocol: https? ? "https" : "http")
132
+ end
133
+ end
134
+
135
+ # Resets the instance. This can be used to reset the state information
136
+ # in an existing session instance, so it can be used from a clean-slate
137
+ # condition.
138
+ #
139
+ # session.reset!
140
+ def reset!
141
+ @https = false
142
+ @controller = @request = @response = nil
143
+ @_mock_session = nil
144
+ @request_count = 0
145
+ @url_options = nil
146
+
147
+ self.host = DEFAULT_HOST
148
+ self.remote_addr = "127.0.0.1"
149
+ self.accept = "text/xml,application/xml,application/xhtml+xml," \
150
+ "text/html;q=0.9,text/plain;q=0.8,image/png," \
151
+ "*/*;q=0.5"
152
+
153
+ unless defined? @named_routes_configured
154
+ # the helpers are made protected by default--we make them public for
155
+ # easier access during testing and troubleshooting.
156
+ @named_routes_configured = true
157
+ end
158
+ end
159
+
160
+ # Specify whether or not the session should mimic a secure HTTPS request.
161
+ #
162
+ # session.https!
163
+ # session.https!(false)
164
+ def https!(flag = true)
165
+ @https = flag
166
+ end
167
+
168
+ # Returns +true+ if the session is mimicking a secure HTTPS request.
169
+ #
170
+ # if session.https?
171
+ # ...
172
+ # end
173
+ def https?
174
+ @https
175
+ end
176
+
177
+ # Performs the actual request.
178
+ #
179
+ # - +method+: The HTTP method (GET, POST, PATCH, PUT, DELETE, HEAD, OPTIONS)
180
+ # as a symbol.
181
+ # - +path+: The URI (as a String) on which you want to perform the
182
+ # request.
183
+ # - +params+: The HTTP parameters that you want to pass. This may
184
+ # be +nil+,
185
+ # a Hash, or a String that is appropriately encoded
186
+ # (<tt>application/x-www-form-urlencoded</tt> or
187
+ # <tt>multipart/form-data</tt>).
188
+ # - +headers+: Additional headers to pass, as a Hash. The headers will be
189
+ # merged into the Rack env hash.
190
+ # - +env+: Additional env to pass, as a Hash. The headers will be
191
+ # merged into the Rack env hash.
192
+ #
193
+ # This method is rarely used directly. Use +#get+, +#post+, or other standard
194
+ # HTTP methods in integration tests. +#process+ is only required when using a
195
+ # request method that doesn't have a method defined in the integration tests.
196
+ #
197
+ # This method returns the response status, after performing the request.
198
+ # Furthermore, if this method was called from an ActionDispatch::IntegrationTest object,
199
+ # then that object's <tt>@response</tt> instance variable will point to a Response object
200
+ # which one can use to inspect the details of the response.
201
+ #
202
+ # Example:
203
+ # process :get, '/author', params: { since: 201501011400 }
204
+ def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil)
205
+ request_encoder = RequestEncoder.encoder(as)
206
+ headers ||= {}
207
+
208
+ if method == :get && as == :json && params
209
+ headers["X-Http-Method-Override"] = "GET"
210
+ method = :post
211
+ end
212
+
213
+ if path =~ %r{://}
214
+ path = build_expanded_path(path) do |location|
215
+ https! URI::HTTPS === location if location.scheme
216
+
217
+ if url_host = location.host
218
+ default = Rack::Request::DEFAULT_PORTS[location.scheme]
219
+ url_host += ":#{location.port}" if default != location.port
220
+ host! url_host
221
+ end
222
+ end
223
+ end
224
+
225
+ hostname, port = host.split(":")
226
+
227
+ request_env = {
228
+ :method => method,
229
+ :params => request_encoder.encode_params(params),
230
+
231
+ "SERVER_NAME" => hostname,
232
+ "SERVER_PORT" => port || (https? ? "443" : "80"),
233
+ "HTTPS" => https? ? "on" : "off",
234
+ "rack.url_scheme" => https? ? "https" : "http",
235
+
236
+ "REQUEST_URI" => path,
237
+ "HTTP_HOST" => host,
238
+ "REMOTE_ADDR" => remote_addr,
239
+ "CONTENT_TYPE" => request_encoder.content_type,
240
+ "HTTP_ACCEPT" => request_encoder.accept_header || accept
241
+ }
242
+
243
+ wrapped_headers = Http::Headers.from_hash({})
244
+ wrapped_headers.merge!(headers) if headers
245
+
246
+ if xhr
247
+ wrapped_headers["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest"
248
+ wrapped_headers["HTTP_ACCEPT"] ||= [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
249
+ end
250
+
251
+ # This modifies the passed request_env directly.
252
+ if wrapped_headers.present?
253
+ Http::Headers.from_hash(request_env).merge!(wrapped_headers)
254
+ end
255
+ if env.present?
256
+ Http::Headers.from_hash(request_env).merge!(env)
257
+ end
258
+
259
+ session = Rack::Test::Session.new(_mock_session)
260
+
261
+ # NOTE: rack-test v0.5 doesn't build a default uri correctly
262
+ # Make sure requested path is always a full URI.
263
+ session.request(build_full_uri(path, request_env), request_env)
264
+
265
+ @request_count += 1
266
+ @request = ActionDispatch::Request.new(session.last_request.env)
267
+ response = _mock_session.last_response
268
+ @response = ActionDispatch::TestResponse.from_response(response)
269
+ @response.request = @request
270
+ @html_document = nil
271
+ @url_options = nil
272
+
273
+ @controller = @request.controller_instance
274
+
275
+ response.status
276
+ end
277
+
278
+ # Set the host name to use in the next request.
279
+ #
280
+ # session.host! "www.example.com"
281
+ alias :host! :host=
282
+
283
+ private
284
+ def _mock_session
285
+ @_mock_session ||= Rack::MockSession.new(@app, host)
286
+ end
287
+
288
+ def build_full_uri(path, env)
289
+ "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
290
+ end
291
+
292
+ def build_expanded_path(path)
293
+ location = URI.parse(path)
294
+ yield location if block_given?
295
+ path = location.path
296
+ location.query ? "#{path}?#{location.query}" : path
297
+ end
298
+ end
299
+
300
+ module Runner
301
+ include ActionDispatch::Assertions
302
+
303
+ APP_SESSIONS = {}
304
+
305
+ attr_reader :app
306
+
307
+ def initialize(*args, &blk)
308
+ super(*args, &blk)
309
+ @integration_session = nil
310
+ end
311
+
312
+ def before_setup # :nodoc:
313
+ @app = nil
314
+ super
315
+ end
316
+
317
+ def integration_session
318
+ @integration_session ||= create_session(app)
319
+ end
320
+
321
+ # Reset the current session. This is useful for testing multiple sessions
322
+ # in a single test case.
323
+ def reset!
324
+ @integration_session = create_session(app)
325
+ end
326
+
327
+ def create_session(app)
328
+ klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) {
329
+ # If the app is a Rails app, make url_helpers available on the session.
330
+ # This makes app.url_for and app.foo_path available in the console.
331
+ if app.respond_to?(:routes)
332
+ include app.routes.url_helpers
333
+ include app.routes.mounted_helpers
334
+ end
335
+ }
336
+ klass.new(app)
337
+ end
338
+
339
+ def remove! # :nodoc:
340
+ @integration_session = nil
341
+ end
342
+
343
+ %w(get post patch put head delete cookies assigns follow_redirect!).each do |method|
344
+ define_method(method) do |*args|
345
+ # reset the html_document variable, except for cookies/assigns calls
346
+ unless method == "cookies" || method == "assigns"
347
+ @html_document = nil
348
+ end
349
+
350
+ integration_session.__send__(method, *args).tap do
351
+ copy_session_variables!
352
+ end
353
+ end
354
+ end
355
+
356
+ # Open a new session instance. If a block is given, the new session is
357
+ # yielded to the block before being returned.
358
+ #
359
+ # session = open_session do |sess|
360
+ # sess.extend(CustomAssertions)
361
+ # end
362
+ #
363
+ # By default, a single session is automatically created for you, but you
364
+ # can use this method to open multiple sessions that ought to be tested
365
+ # simultaneously.
366
+ def open_session
367
+ dup.tap do |session|
368
+ session.reset!
369
+ yield session if block_given?
370
+ end
371
+ end
372
+
373
+ # Copy the instance variables from the current session instance into the
374
+ # test instance.
375
+ def copy_session_variables! #:nodoc:
376
+ @controller = @integration_session.controller
377
+ @response = @integration_session.response
378
+ @request = @integration_session.request
379
+ end
380
+
381
+ def default_url_options
382
+ integration_session.default_url_options
383
+ end
384
+
385
+ def default_url_options=(options)
386
+ integration_session.default_url_options = options
387
+ end
388
+
389
+ private
390
+ def respond_to_missing?(method, _)
391
+ integration_session.respond_to?(method) || super
392
+ end
393
+
394
+ # Delegate unhandled messages to the current session instance.
395
+ def method_missing(method, *args, &block)
396
+ if integration_session.respond_to?(method)
397
+ integration_session.public_send(method, *args, &block).tap do
398
+ copy_session_variables!
399
+ end
400
+ else
401
+ super
402
+ end
403
+ end
404
+ end
405
+ end
406
+
407
+ # An integration test spans multiple controllers and actions,
408
+ # tying them all together to ensure they work together as expected. It tests
409
+ # more completely than either unit or functional tests do, exercising the
410
+ # entire stack, from the dispatcher to the database.
411
+ #
412
+ # At its simplest, you simply extend <tt>IntegrationTest</tt> and write your tests
413
+ # using the get/post methods:
414
+ #
415
+ # require "test_helper"
416
+ #
417
+ # class ExampleTest < ActionDispatch::IntegrationTest
418
+ # fixtures :people
419
+ #
420
+ # def test_login
421
+ # # get the login page
422
+ # get "/login"
423
+ # assert_equal 200, status
424
+ #
425
+ # # post the login and follow through to the home page
426
+ # post "/login", params: { username: people(:jamis).username,
427
+ # password: people(:jamis).password }
428
+ # follow_redirect!
429
+ # assert_equal 200, status
430
+ # assert_equal "/home", path
431
+ # end
432
+ # end
433
+ #
434
+ # However, you can also have multiple session instances open per test, and
435
+ # even extend those instances with assertions and methods to create a very
436
+ # powerful testing DSL that is specific for your application. You can even
437
+ # reference any named routes you happen to have defined.
438
+ #
439
+ # require "test_helper"
440
+ #
441
+ # class AdvancedTest < ActionDispatch::IntegrationTest
442
+ # fixtures :people, :rooms
443
+ #
444
+ # def test_login_and_speak
445
+ # jamis, david = login(:jamis), login(:david)
446
+ # room = rooms(:office)
447
+ #
448
+ # jamis.enter(room)
449
+ # jamis.speak(room, "anybody home?")
450
+ #
451
+ # david.enter(room)
452
+ # david.speak(room, "hello!")
453
+ # end
454
+ #
455
+ # private
456
+ #
457
+ # module CustomAssertions
458
+ # def enter(room)
459
+ # # reference a named route, for maximum internal consistency!
460
+ # get(room_url(id: room.id))
461
+ # assert(...)
462
+ # ...
463
+ # end
464
+ #
465
+ # def speak(room, message)
466
+ # post "/say/#{room.id}", xhr: true, params: { message: message }
467
+ # assert(...)
468
+ # ...
469
+ # end
470
+ # end
471
+ #
472
+ # def login(who)
473
+ # open_session do |sess|
474
+ # sess.extend(CustomAssertions)
475
+ # who = people(who)
476
+ # sess.post "/login", params: { username: who.username,
477
+ # password: who.password }
478
+ # assert(...)
479
+ # end
480
+ # end
481
+ # end
482
+ #
483
+ # Another longer example would be:
484
+ #
485
+ # A simple integration test that exercises multiple controllers:
486
+ #
487
+ # require 'test_helper'
488
+ #
489
+ # class UserFlowsTest < ActionDispatch::IntegrationTest
490
+ # test "login and browse site" do
491
+ # # login via https
492
+ # https!
493
+ # get "/login"
494
+ # assert_response :success
495
+ #
496
+ # post "/login", params: { username: users(:david).username, password: users(:david).password }
497
+ # follow_redirect!
498
+ # assert_equal '/welcome', path
499
+ # assert_equal 'Welcome david!', flash[:notice]
500
+ #
501
+ # https!(false)
502
+ # get "/articles/all"
503
+ # assert_response :success
504
+ # assert_select 'h1', 'Articles'
505
+ # end
506
+ # end
507
+ #
508
+ # As you can see the integration test involves multiple controllers and
509
+ # exercises the entire stack from database to dispatcher. In addition you can
510
+ # have multiple session instances open simultaneously in a test and extend
511
+ # those instances with assertion methods to create a very powerful testing
512
+ # DSL (domain-specific language) just for your application.
513
+ #
514
+ # Here's an example of multiple sessions and custom DSL in an integration test
515
+ #
516
+ # require 'test_helper'
517
+ #
518
+ # class UserFlowsTest < ActionDispatch::IntegrationTest
519
+ # test "login and browse site" do
520
+ # # User david logs in
521
+ # david = login(:david)
522
+ # # User guest logs in
523
+ # guest = login(:guest)
524
+ #
525
+ # # Both are now available in different sessions
526
+ # assert_equal 'Welcome david!', david.flash[:notice]
527
+ # assert_equal 'Welcome guest!', guest.flash[:notice]
528
+ #
529
+ # # User david can browse site
530
+ # david.browses_site
531
+ # # User guest can browse site as well
532
+ # guest.browses_site
533
+ #
534
+ # # Continue with other assertions
535
+ # end
536
+ #
537
+ # private
538
+ #
539
+ # module CustomDsl
540
+ # def browses_site
541
+ # get "/products/all"
542
+ # assert_response :success
543
+ # assert_select 'h1', 'Products'
544
+ # end
545
+ # end
546
+ #
547
+ # def login(user)
548
+ # open_session do |sess|
549
+ # sess.extend(CustomDsl)
550
+ # u = users(user)
551
+ # sess.https!
552
+ # sess.post "/login", params: { username: u.username, password: u.password }
553
+ # assert_equal '/welcome', sess.path
554
+ # sess.https!(false)
555
+ # end
556
+ # end
557
+ # end
558
+ #
559
+ # See the {request helpers documentation}[rdoc-ref:ActionDispatch::Integration::RequestHelpers] for help on how to
560
+ # use +get+, etc.
561
+ #
562
+ # === Changing the request encoding
563
+ #
564
+ # You can also test your JSON API easily by setting what the request should
565
+ # be encoded as:
566
+ #
567
+ # require "test_helper"
568
+ #
569
+ # class ApiTest < ActionDispatch::IntegrationTest
570
+ # test "creates articles" do
571
+ # assert_difference -> { Article.count } do
572
+ # post articles_path, params: { article: { title: "Ahoy!" } }, as: :json
573
+ # end
574
+ #
575
+ # assert_response :success
576
+ # assert_equal({ id: Article.last.id, title: "Ahoy!" }, response.parsed_body)
577
+ # end
578
+ # end
579
+ #
580
+ # The +as+ option passes an "application/json" Accept header (thereby setting
581
+ # the request format to JSON unless overridden), sets the content type to
582
+ # "application/json" and encodes the parameters as JSON.
583
+ #
584
+ # Calling +parsed_body+ on the response parses the response body based on the
585
+ # last response MIME type.
586
+ #
587
+ # Out of the box, only <tt>:json</tt> is supported. But for any custom MIME
588
+ # types you've registered, you can add your own encoders with:
589
+ #
590
+ # ActionDispatch::IntegrationTest.register_encoder :wibble,
591
+ # param_encoder: -> params { params.to_wibble },
592
+ # response_parser: -> body { body }
593
+ #
594
+ # Where +param_encoder+ defines how the params should be encoded and
595
+ # +response_parser+ defines how the response body should be parsed through
596
+ # +parsed_body+.
597
+ #
598
+ # Consult the Rails Testing Guide for more.
599
+
600
+ class IntegrationTest < ActiveSupport::TestCase
601
+ include TestProcess::FixtureFile
602
+
603
+ module UrlOptions
604
+ extend ActiveSupport::Concern
605
+ def url_options
606
+ integration_session.url_options
607
+ end
608
+ end
609
+
610
+ module Behavior
611
+ extend ActiveSupport::Concern
612
+
613
+ include Integration::Runner
614
+ include ActionController::TemplateAssertions
615
+
616
+ included do
617
+ include ActionDispatch::Routing::UrlFor
618
+ include UrlOptions # don't let UrlFor override the url_options method
619
+ ActiveSupport.run_load_hooks(:action_dispatch_integration_test, self)
620
+ @@app = nil
621
+ end
622
+
623
+ module ClassMethods
624
+ def app
625
+ if defined?(@@app) && @@app
626
+ @@app
627
+ else
628
+ ActionDispatch.test_app
629
+ end
630
+ end
631
+
632
+ def app=(app)
633
+ @@app = app
634
+ end
635
+
636
+ def register_encoder(*args)
637
+ RequestEncoder.register_encoder(*args)
638
+ end
639
+ end
640
+
641
+ def app
642
+ super || self.class.app
643
+ end
644
+
645
+ def document_root_element
646
+ html_document.root
647
+ end
648
+ end
649
+
650
+ include Behavior
651
+ end
652
+ end