rack-test 1.1.0 → 2.0.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,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