rack-test 0.6.3 → 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,132 +1,191 @@
1
- require "uri"
2
- require "time"
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'time'
3
5
 
4
6
  module Rack
5
7
  module Test
6
-
8
+ # Represents individual cookies in the cookie jar. This is considered private
9
+ # API and behavior of this class can change at any time.
7
10
  class Cookie # :nodoc:
8
11
  include Rack::Utils
9
12
 
10
- # :api: private
11
- attr_reader :name, :value
13
+ # The name of the cookie, will be a string
14
+ attr_reader :name
15
+
16
+ # The value of the cookie, will be a string or nil if there is no value.
17
+ attr_reader :value
18
+
19
+ # The raw string for the cookie, without options. Will generally be in
20
+ # name=value format is name and value are provided.
21
+ attr_reader :raw
12
22
 
13
- # :api: private
14
23
  def initialize(raw, uri = nil, default_host = DEFAULT_HOST)
15
24
  @default_host = default_host
16
25
  uri ||= default_uri
17
26
 
18
27
  # separate the name / value pair from the cookie options
19
- @name_value_raw, options = raw.split(/[;,] */n, 2)
28
+ @raw, options = raw.split(/[;,] */n, 2)
20
29
 
21
- @name, @value = parse_query(@name_value_raw, ';').to_a.first
30
+ @name, @value = parse_query(@raw, ';').to_a.first
22
31
  @options = parse_query(options, ';')
23
32
 
24
- @options["domain"] ||= (uri.host || default_host)
25
- @options["path"] ||= uri.path.sub(/\/[^\/]*\Z/, "")
33
+ if domain = @options['domain']
34
+ @exact_domain_match = false
35
+ domain[0] = '' if domain[0] == '.'
36
+ else
37
+ # If the domain attribute is not present in the cookie,
38
+ # the domain must match exactly.
39
+ @exact_domain_match = true
40
+ @options['domain'] = (uri.host || default_host)
41
+ end
42
+
43
+ # Set the path for the cookie to the directory containing
44
+ # the request if it isn't set.
45
+ @options['path'] ||= uri.path.sub(/\/[^\/]*\Z/, '')
26
46
  end
27
47
 
48
+ # Wether the given cookie can replace the current cookie in the cookie jar.
28
49
  def replaces?(other)
29
50
  [name.downcase, domain, path] == [other.name.downcase, other.domain, other.path]
30
51
  end
31
52
 
32
- # :api: private
33
- def raw
34
- @name_value_raw
35
- end
36
-
37
- # :api: private
53
+ # Whether the cookie has a value.
38
54
  def empty?
39
55
  @value.nil? || @value.empty?
40
56
  end
41
57
 
42
- # :api: private
58
+ # The explicit or implicit domain for the cookie.
43
59
  def domain
44
- @options["domain"]
60
+ @options['domain']
45
61
  end
46
62
 
63
+ # Whether the cookie has the secure flag, indicating it can only be sent over
64
+ # an encrypted connection.
47
65
  def secure?
48
- @options.has_key?("secure")
66
+ @options.key?('secure')
49
67
  end
50
68
 
51
- # :api: private
69
+ # Whether the cookie has the httponly flag, indicating it is not available via
70
+ # a javascript API.
71
+ def http_only?
72
+ @options.key?('HttpOnly') || @options.key?('httponly')
73
+ end
74
+
75
+ # The explicit or implicit path for the cookie.
52
76
  def path
53
- @options["path"].strip || "/"
77
+ ([*@options['path']].first.split(',').first || '/').strip
54
78
  end
55
79
 
56
- # :api: private
80
+ # A Time value for when the cookie expires, if the expires option is set.
57
81
  def expires
58
- Time.parse(@options["expires"]) if @options["expires"]
82
+ Time.parse(@options['expires']) if @options['expires']
59
83
  end
60
84
 
61
- # :api: private
85
+ # Whether the cookie is currently expired.
62
86
  def expired?
63
87
  expires && expires < Time.now
64
88
  end
65
89
 
66
- # :api: private
90
+ # Whether the cookie is valid for the given URI.
67
91
  def valid?(uri)
68
92
  uri ||= default_uri
69
93
 
70
- if uri.host.nil?
71
- uri.host = @default_host
72
- end
94
+ uri.host = @default_host if uri.host.nil?
73
95
 
74
96
  real_domain = domain =~ /^\./ ? domain[1..-1] : domain
75
- (!secure? || (secure? && uri.scheme == "https")) &&
76
- uri.host =~ Regexp.new("#{Regexp.escape(real_domain)}$", Regexp::IGNORECASE) &&
77
- uri.path =~ Regexp.new("^#{Regexp.escape(path)}")
97
+ !!((!secure? || (secure? && uri.scheme == 'https')) &&
98
+ uri.host =~ Regexp.new("#{'^' if @exact_domain_match}#{Regexp.escape(real_domain)}$", Regexp::IGNORECASE))
78
99
  end
79
100
 
80
- # :api: private
101
+ # Cookies that do not match the URI will not be sent in requests to the URI.
81
102
  def matches?(uri)
82
- ! expired? && valid?(uri)
103
+ !expired? && valid?(uri) && uri.path.start_with?(path)
83
104
  end
84
105
 
85
- # :api: private
106
+ # Order cookies by name, path, and domain.
86
107
  def <=>(other)
87
- # Orders the cookies from least specific to most
88
108
  [name, path, domain.reverse] <=> [other.name, other.path, other.domain.reverse]
89
109
  end
90
110
 
91
- protected
111
+ # A hash of cookie options, including the cookie value, but excluding the cookie name.
112
+ def to_h
113
+ @options.merge(
114
+ 'value' => @value,
115
+ 'HttpOnly' => http_only?,
116
+ 'secure' => secure?
117
+ )
118
+ end
119
+ alias to_hash to_h
120
+
121
+ private
92
122
 
123
+ # The default URI to use for the cookie, including just the host.
93
124
  def default_uri
94
- URI.parse("//" + @default_host + "/")
125
+ URI.parse('//' + @default_host + '/')
95
126
  end
96
-
97
127
  end
98
128
 
129
+ # Represents all cookies for a session, handling adding and
130
+ # removing cookies, and finding which cookies apply to a given
131
+ # request. This is considered private API and behavior of this
132
+ # class can change at any time.
99
133
  class CookieJar # :nodoc:
134
+ DELIMITER = '; '.freeze
100
135
 
101
- # :api: private
102
136
  def initialize(cookies = [], default_host = DEFAULT_HOST)
103
137
  @default_host = default_host
104
- @cookies = cookies
105
- @cookies.sort!
138
+ @cookies = cookies.sort!
139
+ end
140
+
141
+ # Ensure the copy uses a distinct cookies array.
142
+ def initialize_copy(other)
143
+ super
144
+ @cookies = @cookies.dup
106
145
  end
107
146
 
147
+ # Return the value for first cookie with the given name, or nil
148
+ # if no such cookie exists.
108
149
  def [](name)
109
- cookies = hash_for(nil)
110
- # TODO: Should be case insensitive
111
- cookies[name] && cookies[name].value
150
+ name = name.to_s
151
+ @cookies.each do |cookie|
152
+ return cookie.value if cookie.name == name
153
+ end
154
+ nil
112
155
  end
113
156
 
157
+ # Set a cookie with the given name and value in the
158
+ # cookie jar.
114
159
  def []=(name, value)
115
160
  merge("#{name}=#{Rack::Utils.escape(value)}")
116
161
  end
117
162
 
163
+ # Return the first cookie with the given name, or nil if
164
+ # no such cookie exists.
165
+ def get_cookie(name)
166
+ @cookies.each do |cookie|
167
+ return cookie if cookie.name == name
168
+ end
169
+ nil
170
+ end
171
+
172
+ # Delete all cookies with the given name from the cookie jar.
118
173
  def delete(name)
119
174
  @cookies.reject! do |cookie|
120
175
  cookie.name == name
121
176
  end
177
+ nil
122
178
  end
123
179
 
180
+ # Add a string of raw cookie information to the cookie jar,
181
+ # if the cookie is valid for the given URI.
182
+ # Cookies should be separated with a newline.
124
183
  def merge(raw_cookies, uri = nil)
125
184
  return unless raw_cookies
126
185
 
127
186
  if raw_cookies.is_a? String
128
187
  raw_cookies = raw_cookies.split("\n")
129
- raw_cookies.reject!{|c| c.empty? }
188
+ raw_cookies.reject!(&:empty?)
130
189
  end
131
190
 
132
191
  raw_cookies.each do |raw_cookie|
@@ -135,6 +194,7 @@ module Rack
135
194
  end
136
195
  end
137
196
 
197
+ # Add a Cookie to the cookie jar.
138
198
  def <<(new_cookie)
139
199
  @cookies.reject! do |existing_cookie|
140
200
  new_cookie.replaces?(existing_cookie)
@@ -144,39 +204,49 @@ module Rack
144
204
  @cookies.sort!
145
205
  end
146
206
 
147
- # :api: private
207
+ # Return a raw cookie string for the cookie header to
208
+ # use for the given URI.
148
209
  def for(uri)
149
- hash_for(uri).values.map { |c| c.raw }.join(';')
210
+ buf = String.new
211
+ delimiter = nil
212
+
213
+ each_cookie_for(uri) do |cookie|
214
+ if delimiter
215
+ buf << delimiter
216
+ else
217
+ delimiter = DELIMITER
218
+ end
219
+ buf << cookie.raw
220
+ end
221
+
222
+ buf
150
223
  end
151
224
 
225
+ # Return a hash cookie names and cookie values for cookies in the jar.
152
226
  def to_hash
153
227
  cookies = {}
154
228
 
155
- hash_for(nil).each do |name, cookie|
156
- cookies[name] = cookie.value
229
+ @cookies.each do |cookie|
230
+ cookies[cookie.name] = cookie.value
157
231
  end
158
232
 
159
- return cookies
233
+ cookies
160
234
  end
161
235
 
162
- protected
163
-
164
- def hash_for(uri = nil)
165
- cookies = {}
236
+ private
166
237
 
167
- # The cookies are sorted by most specific first. So, we loop through
168
- # all the cookies in order and add it to a hash by cookie name if
169
- # the cookie can be sent to the current URI. It's added to the hash
170
- # so that when we are done, the cookies will be unique by name and
171
- # we'll have grabbed the most specific to the URI.
238
+ # Yield each cookie that matches for the URI.
239
+ #
240
+ # The cookies are sorted by most specific first. So, we loop through
241
+ # all the cookies in order and add it to a hash by cookie name if
242
+ # the cookie can be sent to the current URI. It's added to the hash
243
+ # so that when we are done, the cookies will be unique by name and
244
+ # we'll have grabbed the most specific to the URI.
245
+ def each_cookie_for(uri)
172
246
  @cookies.each do |cookie|
173
- cookies[cookie.name] = cookie if !uri || cookie.matches?(uri)
247
+ yield cookie if !uri || cookie.matches?(uri)
174
248
  end
175
-
176
- return cookies
177
249
  end
178
-
179
250
  end
180
-
181
251
  end
182
252
  end
@@ -1,12 +1,16 @@
1
- require "forwardable"
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
2
4
 
3
5
  module Rack
4
6
  module Test
5
-
6
7
  # This module serves as the primary integration point for using Rack::Test
7
8
  # in a testing environment. It depends on an app method being defined in the
8
9
  # same context, and provides the Rack::Test API methods (see Rack::Test::Session
9
- # 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.
10
14
  #
11
15
  # Example:
12
16
  #
@@ -14,23 +18,14 @@ module Rack
14
18
  # include Rack::Test::Methods
15
19
  #
16
20
  # def app
17
- # MyApp.new
21
+ # MyApp
18
22
  # end
19
23
  # end
20
24
  module Methods
21
25
  extend Forwardable
22
26
 
23
- def rack_mock_session(name = :default) # :nodoc:
24
- return build_rack_mock_session unless name
25
-
26
- @_rack_mock_sessions ||= {}
27
- @_rack_mock_sessions[name] ||= build_rack_mock_session
28
- end
29
-
30
- def build_rack_mock_session # :nodoc:
31
- Rack::MockSession.new(app)
32
- end
33
-
27
+ # Return the existing session with the given name, or a new
28
+ # rack session. Always use a new session if name is nil.
34
29
  def rack_test_session(name = :default) # :nodoc:
35
30
  return build_rack_test_session(name) unless name
36
31
 
@@ -38,25 +33,39 @@ module Rack
38
33
  @_rack_test_sessions[name] ||= build_rack_test_session(name)
39
34
  end
40
35
 
41
- def build_rack_test_session(name) # :nodoc:
42
- Rack::Test::Session.new(rack_mock_session(name))
43
- end
36
+ # For backwards compatibility with older rack-test versions.
37
+ alias rack_mock_session rack_test_session # :nodoc:
44
38
 
45
- def current_session # :nodoc:
46
- 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
47
51
  end
48
52
 
49
- def with_session(name) # :nodoc:
50
- _current_session_names.push(name)
51
- yield rack_test_session(name)
52
- _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
53
57
  end
54
58
 
55
- def _current_session_names # :nodoc:
56
- @_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
57
66
  end
58
67
 
59
- METHODS = [
68
+ def_delegators(:current_session,
60
69
  :request,
61
70
  :get,
62
71
  :post,
@@ -65,6 +74,7 @@ module Rack
65
74
  :delete,
66
75
  :options,
67
76
  :head,
77
+ :custom_request,
68
78
  :follow_redirect!,
69
79
  :header,
70
80
  :env,
@@ -72,12 +82,13 @@ module Rack
72
82
  :clear_cookies,
73
83
  :authorize,
74
84
  :basic_authorize,
75
- :digest_authorize,
76
85
  :last_response,
77
- :last_request
78
- ]
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,16 +1,17 @@
1
- require "tempfile"
2
- require "fileutils"
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tempfile'
5
+ require 'stringio'
3
6
 
4
7
  module Rack
5
8
  module Test
6
-
7
9
  # Wraps a Tempfile with a content type. Including one or more UploadedFile's
8
10
  # in the params causes Rack::Test to build and issue a multipart request.
9
11
  #
10
12
  # Example:
11
13
  # post "/photos", "file" => Rack::Test::UploadedFile.new("me.jpg", "image/jpeg")
12
14
  class UploadedFile
13
-
14
15
  # The filename, *not* including the path, of the "uploaded" file
15
16
  attr_reader :original_filename
16
17
 
@@ -20,34 +21,93 @@ module Rack
20
21
  # The content type of the "uploaded" file
21
22
  attr_accessor :content_type
22
23
 
23
- def initialize(path, content_type = "text/plain", binary = false)
24
- raise "#{path} file does not exist" unless ::File.exist?(path)
25
-
24
+ # Creates a new UploadedFile instance.
25
+ #
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
31
+ def initialize(content, content_type = 'text/plain', binary = false, original_filename: nil)
26
32
  @content_type = content_type
27
- @original_filename = ::File.basename(path)
33
+ @original_filename = original_filename
28
34
 
29
- @tempfile = Tempfile.new(@original_filename)
30
- @tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
31
- @tempfile.binmode if binary
35
+ case content
36
+ when StringIO
37
+ initialize_from_stringio(content)
38
+ else
39
+ initialize_from_file_path(content)
40
+ end
32
41
 
33
- FileUtils.copy_file(path, @tempfile.path)
42
+ @tempfile.binmode if binary
34
43
  end
35
44
 
45
+ # The path to the tempfile. Will not work if the receiver's content is from a StringIO.
36
46
  def path
37
- @tempfile.path
47
+ tempfile.path
48
+ end
49
+ alias local_path path
50
+
51
+ # Delegate all methods not handled to the tempfile.
52
+ def method_missing(method_name, *args, &block)
53
+ tempfile.public_send(method_name, *args, &block)
38
54
  end
39
55
 
40
- alias_method :local_path, :path
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?
41
65
 
42
- def method_missing(method_name, *args, &block) #:nodoc:
43
- @tempfile.__send__(method_name, *args, &block)
66
+ tempfile.rewind
67
+
68
+ nil
44
69
  end
45
70
 
46
- def respond_to?(method_name, include_private = false) #:nodoc:
47
- @tempfile.respond_to?(method_name, include_private) || super
71
+ def respond_to_missing?(method_name, include_private = false) #:nodoc:
72
+ tempfile.respond_to?(method_name, include_private) || super
48
73
  end
49
74
 
50
- end
75
+ # A proc that can be used as a finalizer to close and unlink the tempfile.
76
+ def self.finalize(file)
77
+ proc { actually_finalize file }
78
+ end
51
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.
82
+ def self.actually_finalize(file)
83
+ file.close
84
+ file.unlink
85
+ end
86
+
87
+ private
88
+
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
+
93
+ @tempfile = stringio
94
+ end
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.
98
+ def initialize_from_file_path(path)
99
+ raise "#{path} file does not exist" unless ::File.exist?(path)
100
+
101
+ @original_filename ||= ::File.basename(path)
102
+ extension = ::File.extname(@original_filename)
103
+
104
+ @tempfile = Tempfile.new([::File.basename(@original_filename, extension), extension])
105
+ @tempfile.set_encoding(Encoding::BINARY)
106
+
107
+ ObjectSpace.define_finalizer(self, self.class.finalize(@tempfile))
108
+
109
+ FileUtils.copy_file(path, @tempfile.path)
110
+ end
111
+ end
52
112
  end
53
113
  end