rack-test 1.1.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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