rack-test 1.1.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
 
3
5
  module Rack
@@ -5,7 +7,10 @@ module Rack
5
7
  # This module serves as the primary integration point for using Rack::Test
6
8
  # in a testing environment. It depends on an app method being defined in the
7
9
  # same context, and provides the Rack::Test API methods (see Rack::Test::Session
8
- # for their documentation).
10
+ # for their documentation). It defines the following methods that are delegated
11
+ # to the current session: :request, :get, :post, :put, :patch, :delete, :options,
12
+ # :head, :custom_request, :follow_redirect!, :header, :env, :set_cookie,
13
+ # :clear_cookies, :authorize, :basic_authorize, :last_response, and :last_request.
9
14
  #
10
15
  # Example:
11
16
  #
@@ -13,23 +18,14 @@ module Rack
13
18
  # include Rack::Test::Methods
14
19
  #
15
20
  # def app
16
- # MyApp.new
21
+ # MyApp
17
22
  # end
18
23
  # end
19
24
  module Methods
20
25
  extend Forwardable
21
26
 
22
- def rack_mock_session(name = :default) # :nodoc:
23
- return build_rack_mock_session unless name
24
-
25
- @_rack_mock_sessions ||= {}
26
- @_rack_mock_sessions[name] ||= build_rack_mock_session
27
- end
28
-
29
- def build_rack_mock_session # :nodoc:
30
- Rack::MockSession.new(app)
31
- end
32
-
27
+ # Return the existing session with the given name, or a new
28
+ # rack session. Always use a new session if name is nil.
33
29
  def rack_test_session(name = :default) # :nodoc:
34
30
  return build_rack_test_session(name) unless name
35
31
 
@@ -37,47 +33,62 @@ module Rack
37
33
  @_rack_test_sessions[name] ||= build_rack_test_session(name)
38
34
  end
39
35
 
40
- def build_rack_test_session(name) # :nodoc:
41
- Rack::Test::Session.new(rack_mock_session(name))
42
- end
36
+ # For backwards compatibility with older rack-test versions.
37
+ alias rack_mock_session rack_test_session # :nodoc:
43
38
 
44
- def current_session # :nodoc:
45
- rack_test_session(_current_session_names.last)
39
+ # Create a new Rack::Test::Session for #app.
40
+ def build_rack_test_session(_name) # :nodoc:
41
+ if respond_to?(:build_rack_mock_session, true)
42
+ # Backwards compatibility for capybara
43
+ build_rack_mock_session
44
+ else
45
+ if respond_to?(:default_host)
46
+ Session.new(app, default_host)
47
+ else
48
+ Session.new(app)
49
+ end
50
+ end
46
51
  end
47
52
 
48
- def with_session(name) # :nodoc:
49
- _current_session_names.push(name)
50
- yield rack_test_session(name)
51
- _current_session_names.pop
53
+ # Return the currently actively session. This is the session to
54
+ # which the delegated methods are sent.
55
+ def current_session
56
+ @_rack_test_current_session ||= rack_test_session
52
57
  end
53
58
 
54
- def _current_session_names # :nodoc:
55
- @_current_session_names ||= [:default]
59
+ # Create a new session (or reuse an existing session with the given name),
60
+ # and make it the current session for the given block.
61
+ def with_session(name)
62
+ session = _rack_test_current_session
63
+ yield(@_rack_test_current_session = rack_test_session(name))
64
+ ensure
65
+ @_rack_test_current_session = session
56
66
  end
57
67
 
58
- METHODS = %i[
59
- request
60
- get
61
- post
62
- put
63
- patch
64
- delete
65
- options
66
- head
67
- custom_request
68
- follow_redirect!
69
- header
70
- env
71
- set_cookie
72
- clear_cookies
73
- authorize
74
- basic_authorize
75
- digest_authorize
76
- last_response
77
- last_request
78
- ].freeze
68
+ def_delegators(:current_session,
69
+ :request,
70
+ :get,
71
+ :post,
72
+ :put,
73
+ :patch,
74
+ :delete,
75
+ :options,
76
+ :head,
77
+ :custom_request,
78
+ :follow_redirect!,
79
+ :header,
80
+ :env,
81
+ :set_cookie,
82
+ :clear_cookies,
83
+ :authorize,
84
+ :basic_authorize,
85
+ :last_response,
86
+ :last_request,
87
+ )
79
88
 
80
- def_delegators :current_session, *METHODS
89
+ # Private accessor to avoid uninitialized instance variable warning in Ruby 2.*
90
+ attr_accessor :_rack_test_current_session
91
+ private :_rack_test_current_session
81
92
  end
82
93
  end
83
94
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'fileutils'
2
- require 'pathname'
3
4
  require 'tempfile'
5
+ require 'stringio'
4
6
 
5
7
  module Rack
6
8
  module Test
@@ -21,40 +23,62 @@ module Rack
21
23
 
22
24
  # Creates a new UploadedFile instance.
23
25
  #
24
- # @param content [IO, Pathname, String, StringIO] a path to a file, or an {IO} or {StringIO} object representing the
25
- # file.
26
- # @param content_type [String]
27
- # @param binary [Boolean] an optional flag that indicates whether the file should be open in binary mode or not.
28
- # @param original_filename [String] an optional parameter that provides the original filename if `content` is a StringIO
29
- # object. Not used for other kind of `content` objects.
26
+ # Arguments:
27
+ # content :: is a path to a file, or an {IO} or {StringIO} object representing the content.
28
+ # content_type :: MIME type of the file
29
+ # binary :: Whether the file should be set to binmode (content treated as binary).
30
+ # original_filename :: The filename to use for the file. Required if content is StringIO, optional override if not
30
31
  def initialize(content, content_type = 'text/plain', binary = false, original_filename: nil)
31
- if original_filename
32
- initialize_from_stringio(content, original_filename)
32
+ @content_type = content_type
33
+ @original_filename = original_filename
34
+
35
+ case content
36
+ when StringIO
37
+ initialize_from_stringio(content)
33
38
  else
34
39
  initialize_from_file_path(content)
35
40
  end
36
- @content_type = content_type
41
+
37
42
  @tempfile.binmode if binary
38
43
  end
39
44
 
45
+ # The path to the tempfile. Will not work if the receiver's content is from a StringIO.
40
46
  def path
41
47
  tempfile.path
42
48
  end
43
-
44
49
  alias local_path path
45
50
 
46
- def method_missing(method_name, *args, &block) #:nodoc:
51
+ # Delegate all methods not handled to the tempfile.
52
+ def method_missing(method_name, *args, &block)
47
53
  tempfile.public_send(method_name, *args, &block)
48
54
  end
49
55
 
56
+ # Append to given buffer in 64K chunks to avoid multiple large
57
+ # copies of file data in memory. Rewind tempfile before and
58
+ # after to make sure all data in tempfile is appended to the
59
+ # buffer.
60
+ def append_to(buffer)
61
+ tempfile.rewind
62
+
63
+ buf = String.new
64
+ buffer << tempfile.readpartial(65_536, buf) until tempfile.eof?
65
+
66
+ tempfile.rewind
67
+
68
+ nil
69
+ end
70
+
50
71
  def respond_to_missing?(method_name, include_private = false) #:nodoc:
51
72
  tempfile.respond_to?(method_name, include_private) || super
52
73
  end
53
74
 
75
+ # A proc that can be used as a finalizer to close and unlink the tempfile.
54
76
  def self.finalize(file)
55
77
  proc { actually_finalize file }
56
78
  end
57
79
 
80
+ # Close and unlink the given file, used as a finalizer for the tempfile,
81
+ # if the tempfile is backed by a file in the filesystem.
58
82
  def self.actually_finalize(file)
59
83
  file.close
60
84
  file.unlink
@@ -62,19 +86,23 @@ module Rack
62
86
 
63
87
  private
64
88
 
65
- def initialize_from_stringio(stringio, original_filename)
89
+ # Use the StringIO as the tempfile.
90
+ def initialize_from_stringio(stringio)
91
+ raise(ArgumentError, 'Missing `original_filename` for StringIO object') unless @original_filename
92
+
66
93
  @tempfile = stringio
67
- @original_filename = original_filename || raise(ArgumentError, 'Missing `original_filename` for StringIO object')
68
94
  end
69
95
 
96
+ # Create a tempfile and copy the content from the given path into the tempfile, optionally renaming if
97
+ # original_filename has been set.
70
98
  def initialize_from_file_path(path)
71
99
  raise "#{path} file does not exist" unless ::File.exist?(path)
72
100
 
73
- @original_filename = ::File.basename(path)
101
+ @original_filename ||= ::File.basename(path)
74
102
  extension = ::File.extname(@original_filename)
75
103
 
76
104
  @tempfile = Tempfile.new([::File.basename(@original_filename, extension), extension])
77
- @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
105
+ @tempfile.set_encoding(Encoding::BINARY)
78
106
 
79
107
  ObjectSpace.define_finalizer(self, self.class.finalize(@tempfile))
80
108
 
@@ -1,17 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  module Test
3
5
  module Utils # :nodoc:
4
6
  include Rack::Utils
5
- extend Rack::Utils
7
+ extend self
6
8
 
9
+ # Build a query string for the given value and prefix. The value
10
+ # can be an array or hash of parameters.
7
11
  def build_nested_query(value, prefix = nil)
8
12
  case value
9
13
  when Array
10
14
  if value.empty?
11
15
  "#{prefix}[]="
12
16
  else
17
+ prefix += "[]" unless unescape(prefix).end_with?('[]')
13
18
  value.map do |v|
14
- prefix = "#{prefix}[]" unless unescape(prefix) =~ /\[\]$/
15
19
  build_nested_query(v, prefix.to_s)
16
20
  end.join('&')
17
21
  end
@@ -25,12 +29,12 @@ module Rack
25
29
  "#{prefix}=#{escape(value)}"
26
30
  end
27
31
  end
28
- module_function :build_nested_query
29
32
 
30
- def build_multipart(params, first = true, multipart = false)
31
- if first
32
- raise ArgumentError, 'value must be a Hash' unless params.is_a?(Hash)
33
+ # Build a multipart body for the given params.
34
+ def build_multipart(params, _first = true, multipart = false)
35
+ raise ArgumentError, 'value must be a Hash' unless params.is_a?(Hash)
33
36
 
37
+ unless multipart
34
38
  query = lambda { |value|
35
39
  case value
36
40
  when Array
@@ -45,6 +49,17 @@ module Rack
45
49
  return nil unless multipart
46
50
  end
47
51
 
52
+ params = normalize_multipart_params(params, true)
53
+
54
+ buffer = String.new
55
+ build_parts(buffer, params)
56
+ buffer
57
+ end
58
+
59
+ private
60
+
61
+ # Return a flattened hash of parameter values based on the given params.
62
+ def normalize_multipart_params(params, first=false)
48
63
  flattened_params = {}
49
64
 
50
65
  params.each do |key, value|
@@ -55,17 +70,16 @@ module Rack
55
70
  value.map do |v|
56
71
  if v.is_a?(Hash)
57
72
  nested_params = {}
58
- build_multipart(v, false).each do |subkey, subvalue|
73
+ normalize_multipart_params(v).each do |subkey, subvalue|
59
74
  nested_params[subkey] = subvalue
60
75
  end
61
- flattened_params["#{k}[]"] ||= []
62
- flattened_params["#{k}[]"] << nested_params
76
+ (flattened_params["#{k}[]"] ||= []) << nested_params
63
77
  else
64
78
  flattened_params["#{k}[]"] = value
65
79
  end
66
80
  end
67
81
  when Hash
68
- build_multipart(value, false).each do |subkey, subvalue|
82
+ normalize_multipart_params(value).each do |subkey, subvalue|
69
83
  flattened_params[k + subkey] = subvalue
70
84
  end
71
85
  else
@@ -73,72 +87,70 @@ module Rack
73
87
  end
74
88
  end
75
89
 
76
- if first
77
- build_parts(flattened_params)
78
- else
79
- flattened_params
80
- end
90
+ flattened_params
81
91
  end
82
- module_function :build_multipart
83
-
84
- private
85
92
 
86
- def build_parts(parameters)
87
- get_parts(parameters).join + "--#{MULTIPART_BOUNDARY}--\r"
93
+ # Build the multipart content for uploading.
94
+ def build_parts(buffer, parameters)
95
+ _build_parts(buffer, parameters)
96
+ buffer << END_BOUNDARY
88
97
  end
89
- module_function :build_parts
90
98
 
91
- def get_parts(parameters)
99
+ # Append each multipart parameter value to the buffer.
100
+ def _build_parts(buffer, parameters)
92
101
  parameters.map do |name, value|
93
102
  if name =~ /\[\]\Z/ && value.is_a?(Array) && value.all? { |v| v.is_a?(Hash) }
94
- value.map do |hash|
103
+ value.each do |hash|
95
104
  new_value = {}
96
105
  hash.each { |k, v| new_value[name + k] = v }
97
- get_parts(new_value).join
98
- end.join
106
+ _build_parts(buffer, new_value)
107
+ end
99
108
  else
100
- if value.respond_to?(:original_filename)
101
- build_file_part(name, value)
102
-
103
- elsif value.is_a?(Array) && value.all? { |v| v.respond_to?(:original_filename) }
104
- value.map do |v|
105
- build_file_part(name, v)
106
- end.join
107
-
108
- else
109
- primitive_part = build_primitive_part(name, value)
110
- Rack::Test.encoding_aware_strings? ? primitive_part.force_encoding('BINARY') : primitive_part
109
+ [value].flatten.map do |v|
110
+ if v.respond_to?(:original_filename)
111
+ build_file_part(buffer, name, v)
112
+ else
113
+ build_primitive_part(buffer, name, v)
114
+ end
111
115
  end
112
116
  end
113
117
  end
114
118
  end
115
- module_function :get_parts
116
-
117
- def build_primitive_part(parameter_name, value)
118
- value = [value] unless value.is_a? Array
119
- value.map do |v|
120
- <<-EOF
121
- --#{MULTIPART_BOUNDARY}\r
122
- Content-Disposition: form-data; name="#{parameter_name}"\r
123
- \r
124
- #{v}\r
125
- EOF
126
- end.join
119
+
120
+ # Append the multipart fragment for a parameter that isn't a file upload to the buffer.
121
+ def build_primitive_part(buffer, parameter_name, value)
122
+ buffer <<
123
+ START_BOUNDARY <<
124
+ "content-disposition: form-data; name=\"" <<
125
+ parameter_name.to_s.b <<
126
+ "\"\r\n\r\n" <<
127
+ value.to_s.b <<
128
+ "\r\n"
129
+ buffer
127
130
  end
128
- module_function :build_primitive_part
129
-
130
- def build_file_part(parameter_name, uploaded_file)
131
- uploaded_file.set_encoding(Encoding::BINARY) if uploaded_file.respond_to?(:set_encoding)
132
- <<-EOF
133
- --#{MULTIPART_BOUNDARY}\r
134
- Content-Disposition: form-data; name="#{parameter_name}"; filename="#{escape(uploaded_file.original_filename)}"\r
135
- Content-Type: #{uploaded_file.content_type}\r
136
- Content-Length: #{uploaded_file.size}\r
137
- \r
138
- #{uploaded_file.read}\r
139
- EOF
131
+
132
+ # Append the multipart fragment for a parameter that is a file upload to the buffer.
133
+ def build_file_part(buffer, parameter_name, uploaded_file)
134
+ buffer <<
135
+ START_BOUNDARY <<
136
+ "content-disposition: form-data; name=\"" <<
137
+ parameter_name.to_s.b <<
138
+ "\"; filename=\"" <<
139
+ escape_path(uploaded_file.original_filename).b <<
140
+ "\"\r\ncontent-type: " <<
141
+ uploaded_file.content_type.to_s.b <<
142
+ "\r\ncontent-length: " <<
143
+ uploaded_file.size.to_s.b <<
144
+ "\r\n\r\n"
145
+
146
+ # Handle old versions of Capybara::RackTest::Form::NilUploadedFile
147
+ if uploaded_file.respond_to?(:set_encoding)
148
+ uploaded_file.set_encoding(Encoding::BINARY)
149
+ uploaded_file.append_to(buffer)
150
+ end
151
+
152
+ buffer << "\r\n"
140
153
  end
141
- module_function :build_file_part
142
154
  end
143
155
  end
144
156
  end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  module Test
3
- VERSION = '1.1.0'.freeze
3
+ VERSION = '2.1.0'.freeze
4
4
  end
5
5
  end