rack-test 1.1.0 → 2.0.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,63 @@ 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))
36
+ # For backwards compatibility with older rack-test versions.
37
+ alias rack_mock_session rack_test_session # :nodoc:
38
+
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
+ Session.new(app)
46
+ end
42
47
  end
43
48
 
44
- def current_session # :nodoc:
45
- rack_test_session(_current_session_names.last)
49
+ # Return the currently actively session. This is the session to
50
+ # which the delegated methods are sent.
51
+ def current_session
52
+ @_rack_test_current_session ||= rack_test_session
46
53
  end
47
54
 
48
- def with_session(name) # :nodoc:
49
- _current_session_names.push(name)
50
- yield rack_test_session(name)
51
- _current_session_names.pop
55
+ # Create a new session (or reuse an existing session with the given name),
56
+ # and make it the current session for the given block.
57
+ def with_session(name)
58
+ session = _rack_test_current_session
59
+ yield(@_rack_test_current_session = rack_test_session(name))
60
+ ensure
61
+ @_rack_test_current_session = session
52
62
  end
53
63
 
54
- def _current_session_names # :nodoc:
55
- @_current_session_names ||= [:default]
64
+ def digest_authorize(username, password) # :nodoc:
65
+ warn 'digest authentication support will be removed in rack-test 2.1', uplevel: 1
66
+ current_session._digest_authorize(username, password)
56
67
  end
57
68
 
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
69
+ def_delegators(:current_session,
70
+ :request,
71
+ :get,
72
+ :post,
73
+ :put,
74
+ :patch,
75
+ :delete,
76
+ :options,
77
+ :head,
78
+ :custom_request,
79
+ :follow_redirect!,
80
+ :header,
81
+ :env,
82
+ :set_cookie,
83
+ :clear_cookies,
84
+ :authorize,
85
+ :basic_authorize,
86
+ :last_response,
87
+ :last_request,
88
+ )
79
89
 
80
- def_delegators :current_session, *METHODS
90
+ # Private accessor to avoid uninitialized instance variable warning in Ruby 2.*
91
+ attr_accessor :_rack_test_current_session
92
+ private :_rack_test_current_session
81
93
  end
82
94
  end
83
95
  end
@@ -1,6 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nocov:
4
+ require 'rack/auth/digest' unless defined?(Rack::Auth::Digest)
5
+ # :nocov:
6
+
1
7
  module Rack
2
8
  module Test
3
- class MockDigestRequest # :nodoc:
9
+ class MockDigestRequest_ # :nodoc:
4
10
  def initialize(params)
5
11
  @params = params
6
12
  end
@@ -21,5 +27,9 @@ module Rack
21
27
  Rack::Auth::Digest::MD5.new(nil).send :digest, self, password
22
28
  end
23
29
  end
30
+ MockDigestRequest = MockDigestRequest_
31
+ # :nocov:
32
+ deprecate_constant :MockDigestRequest if respond_to?(:deprecate_constant, true)
33
+ # :nocov:
24
34
  end
25
35
  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,60 @@ 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 if +content+ is a StringIO.
30
31
  def initialize(content, content_type = 'text/plain', binary = false, original_filename: nil)
31
- if original_filename
32
+ case content
33
+ when StringIO
32
34
  initialize_from_stringio(content, original_filename)
33
35
  else
34
36
  initialize_from_file_path(content)
35
37
  end
38
+
36
39
  @content_type = content_type
37
40
  @tempfile.binmode if binary
38
41
  end
39
42
 
43
+ # The path to the tempfile. Will not work if the receiver's content is from a StringIO.
40
44
  def path
41
45
  tempfile.path
42
46
  end
43
-
44
47
  alias local_path path
45
48
 
46
- def method_missing(method_name, *args, &block) #:nodoc:
49
+ # Delegate all methods not handled to the tempfile.
50
+ def method_missing(method_name, *args, &block)
47
51
  tempfile.public_send(method_name, *args, &block)
48
52
  end
49
53
 
54
+ # Append to given buffer in 64K chunks to avoid multiple large
55
+ # copies of file data in memory. Rewind tempfile before and
56
+ # after to make sure all data in tempfile is appended to the
57
+ # buffer.
58
+ def append_to(buffer)
59
+ tempfile.rewind
60
+
61
+ buf = String.new
62
+ buffer << tempfile.readpartial(65_536, buf) until tempfile.eof?
63
+
64
+ tempfile.rewind
65
+
66
+ nil
67
+ end
68
+
50
69
  def respond_to_missing?(method_name, include_private = false) #:nodoc:
51
70
  tempfile.respond_to?(method_name, include_private) || super
52
71
  end
53
72
 
73
+ # A proc that can be used as a finalizer to close and unlink the tempfile.
54
74
  def self.finalize(file)
55
75
  proc { actually_finalize file }
56
76
  end
57
77
 
78
+ # Close and unlink the given file, used as a finalizer for the tempfile,
79
+ # if the tempfile is backed by a file in the filesystem.
58
80
  def self.actually_finalize(file)
59
81
  file.close
60
82
  file.unlink
@@ -62,11 +84,13 @@ module Rack
62
84
 
63
85
  private
64
86
 
87
+ # Use the StringIO as the tempfile.
65
88
  def initialize_from_stringio(stringio, original_filename)
66
89
  @tempfile = stringio
67
90
  @original_filename = original_filename || raise(ArgumentError, 'Missing `original_filename` for StringIO object')
68
91
  end
69
92
 
93
+ # Create a tempfile and copy the content from the given path into the tempfile.
70
94
  def initialize_from_file_path(path)
71
95
  raise "#{path} file does not exist" unless ::File.exist?(path)
72
96
 
@@ -74,7 +98,7 @@ module Rack
74
98
  extension = ::File.extname(@original_filename)
75
99
 
76
100
  @tempfile = Tempfile.new([::File.basename(@original_filename, extension), extension])
77
- @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
101
+ @tempfile.set_encoding(Encoding::BINARY)
78
102
 
79
103
  ObjectSpace.define_finalizer(self, self.class.finalize(@tempfile))
80
104
 
@@ -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,20 @@ 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
+ # :nocov:
57
+ buffer.force_encoding(Encoding::BINARY) if Rack::Test.encoding_aware_strings?
58
+ # :nocov:
59
+ buffer
60
+ end
61
+
62
+ private
63
+
64
+ # Return a flattened hash of parameter values based on the given params.
65
+ def normalize_multipart_params(params, first=false)
48
66
  flattened_params = {}
49
67
 
50
68
  params.each do |key, value|
@@ -55,17 +73,16 @@ module Rack
55
73
  value.map do |v|
56
74
  if v.is_a?(Hash)
57
75
  nested_params = {}
58
- build_multipart(v, false).each do |subkey, subvalue|
76
+ normalize_multipart_params(v).each do |subkey, subvalue|
59
77
  nested_params[subkey] = subvalue
60
78
  end
61
- flattened_params["#{k}[]"] ||= []
62
- flattened_params["#{k}[]"] << nested_params
79
+ (flattened_params["#{k}[]"] ||= []) << nested_params
63
80
  else
64
81
  flattened_params["#{k}[]"] = value
65
82
  end
66
83
  end
67
84
  when Hash
68
- build_multipart(value, false).each do |subkey, subvalue|
85
+ normalize_multipart_params(value).each do |subkey, subvalue|
69
86
  flattened_params[k + subkey] = subvalue
70
87
  end
71
88
  else
@@ -73,72 +90,69 @@ module Rack
73
90
  end
74
91
  end
75
92
 
76
- if first
77
- build_parts(flattened_params)
78
- else
79
- flattened_params
80
- end
93
+ flattened_params
81
94
  end
82
- module_function :build_multipart
83
-
84
- private
85
95
 
86
- def build_parts(parameters)
87
- get_parts(parameters).join + "--#{MULTIPART_BOUNDARY}--\r"
96
+ # Build the multipart content for uploading.
97
+ def build_parts(buffer, parameters)
98
+ _build_parts(buffer, parameters)
99
+ buffer << END_BOUNDARY
88
100
  end
89
- module_function :build_parts
90
101
 
91
- def get_parts(parameters)
102
+ # Append each multipart parameter value to the buffer.
103
+ def _build_parts(buffer, parameters)
92
104
  parameters.map do |name, value|
93
105
  if name =~ /\[\]\Z/ && value.is_a?(Array) && value.all? { |v| v.is_a?(Hash) }
94
- value.map do |hash|
106
+ value.each do |hash|
95
107
  new_value = {}
96
108
  hash.each { |k, v| new_value[name + k] = v }
97
- get_parts(new_value).join
98
- end.join
109
+ _build_parts(buffer, new_value)
110
+ end
99
111
  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
112
+ [value].flatten.map do |v|
113
+ if v.respond_to?(:original_filename)
114
+ build_file_part(buffer, name, v)
115
+ else
116
+ build_primitive_part(buffer, name, v)
117
+ end
111
118
  end
112
119
  end
113
120
  end
114
121
  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
122
+
123
+ # Append the multipart fragment for a parameter that isn't a file upload to the buffer.
124
+ def build_primitive_part(buffer, parameter_name, value)
125
+ buffer <<
126
+ START_BOUNDARY <<
127
+ "content-disposition: form-data; name=\"" <<
128
+ parameter_name.to_s <<
129
+ "\"\r\n\r\n" <<
130
+ value.to_s <<
131
+ "\r\n"
127
132
  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
133
+
134
+ # Append the multipart fragment for a parameter that is a file upload to the buffer.
135
+ def build_file_part(buffer, parameter_name, uploaded_file)
136
+ buffer <<
137
+ START_BOUNDARY <<
138
+ "content-disposition: form-data; name=\"" <<
139
+ parameter_name.to_s <<
140
+ "\"; filename=\"" <<
141
+ escape_path(uploaded_file.original_filename) <<
142
+ "\"\r\ncontent-type: " <<
143
+ uploaded_file.content_type.to_s <<
144
+ "\r\ncontent-length: " <<
145
+ uploaded_file.size.to_s <<
146
+ "\r\n\r\n"
147
+
148
+ # Handle old versions of Capybara::RackTest::Form::NilUploadedFile
149
+ if uploaded_file.respond_to?(:set_encoding)
150
+ uploaded_file.set_encoding(Encoding::BINARY)
151
+ uploaded_file.append_to(buffer)
152
+ end
153
+
154
+ buffer << "\r\n"
140
155
  end
141
- module_function :build_file_part
142
156
  end
143
157
  end
144
158
  end
@@ -1,5 +1,5 @@
1
1
  module Rack
2
2
  module Test
3
- VERSION = '1.1.0'.freeze
3
+ VERSION = '2.0.0'.freeze
4
4
  end
5
5
  end