rack-test 0.6.3 → 2.1.0

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