actionpack 6.0.0

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