http-cookie 1.0.0.pre12 → 1.0.4

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.
@@ -10,7 +10,7 @@ class HTTP::CookieJar
10
10
  # stored persistently in the SQLite3 database.
11
11
  class MozillaStore < AbstractStore
12
12
  # :stopdoc:
13
- SCHEMA_VERSION = 5
13
+ SCHEMA_VERSION = 7
14
14
 
15
15
  def default_options
16
16
  {
@@ -22,14 +22,11 @@ class HTTP::CookieJar
22
22
 
23
23
  ALL_COLUMNS = %w[
24
24
  baseDomain
25
- appId inBrowserElement
25
+ originAttributes
26
26
  name value
27
27
  host path
28
28
  expiry creationTime lastAccessed
29
29
  isSecure isHttpOnly
30
- ]
31
- UK_COLUMNS = %w[
32
- name host path
33
30
  appId inBrowserElement
34
31
  ]
35
32
 
@@ -68,6 +65,9 @@ class HTTP::CookieJar
68
65
  end
69
66
  # :startdoc:
70
67
 
68
+ # :call-seq:
69
+ # new(**options)
70
+ #
71
71
  # Generates a Mozilla cookie store. If the file does not exist,
72
72
  # it is created. If it does and its schema is old, it is
73
73
  # automatically upgraded with a new schema keeping the existing
@@ -92,6 +92,11 @@ class HTTP::CookieJar
92
92
  def initialize(options = nil)
93
93
  super
94
94
 
95
+ @origin_attributes = encode_www_form({}.tap { |params|
96
+ params['appId'] = @app_id if @app_id.nonzero?
97
+ params['inBrowserElement'] = 1 if @in_browser_element
98
+ })
99
+
95
100
  @filename = options[:filename] or raise ArgumentError, ':filename option is missing'
96
101
 
97
102
  @sjar = HTTP::CookieJar::HashStore.new
@@ -109,6 +114,7 @@ class HTTP::CookieJar
109
114
  @gc_index = 0
110
115
  end
111
116
 
117
+ # Raises TypeError. Cloning is inhibited in this store class.
112
118
  def initialize_copy(other)
113
119
  raise TypeError, 'can\'t clone %s' % self.class
114
120
  end
@@ -138,13 +144,13 @@ class HTTP::CookieJar
138
144
 
139
145
  protected
140
146
 
141
- def schema_version=(version)
147
+ def schema_version= version
142
148
  @db.execute("PRAGMA user_version = %d" % version)
143
149
  @schema_version = version
144
150
  end
145
151
 
146
- def create_table
147
- self.schema_version = SCHEMA_VERSION
152
+ def create_table_v5
153
+ self.schema_version = 5
148
154
  @db.execute("DROP TABLE IF EXISTS moz_cookies")
149
155
  @db.execute(<<-'SQL')
150
156
  CREATE TABLE moz_cookies (
@@ -172,6 +178,62 @@ class HTTP::CookieJar
172
178
  SQL
173
179
  end
174
180
 
181
+ def create_table_v6
182
+ self.schema_version = 6
183
+ @db.execute("DROP TABLE IF EXISTS moz_cookies")
184
+ @db.execute(<<-'SQL')
185
+ CREATE TABLE moz_cookies (
186
+ id INTEGER PRIMARY KEY,
187
+ baseDomain TEXT,
188
+ originAttributes TEXT NOT NULL DEFAULT '',
189
+ name TEXT,
190
+ value TEXT,
191
+ host TEXT,
192
+ path TEXT,
193
+ expiry INTEGER,
194
+ lastAccessed INTEGER,
195
+ creationTime INTEGER,
196
+ isSecure INTEGER,
197
+ isHttpOnly INTEGER,
198
+ CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)
199
+ )
200
+ SQL
201
+ @db.execute(<<-'SQL')
202
+ CREATE INDEX moz_basedomain
203
+ ON moz_cookies (baseDomain,
204
+ originAttributes);
205
+ SQL
206
+ end
207
+
208
+ def create_table
209
+ self.schema_version = SCHEMA_VERSION
210
+ @db.execute("DROP TABLE IF EXISTS moz_cookies")
211
+ @db.execute(<<-'SQL')
212
+ CREATE TABLE moz_cookies (
213
+ id INTEGER PRIMARY KEY,
214
+ baseDomain TEXT,
215
+ originAttributes TEXT NOT NULL DEFAULT '',
216
+ name TEXT,
217
+ value TEXT,
218
+ host TEXT,
219
+ path TEXT,
220
+ expiry INTEGER,
221
+ lastAccessed INTEGER,
222
+ creationTime INTEGER,
223
+ isSecure INTEGER,
224
+ isHttpOnly INTEGER,
225
+ appId INTEGER DEFAULT 0,
226
+ inBrowserElement INTEGER DEFAULT 0,
227
+ CONSTRAINT moz_uniqueid UNIQUE (name, host, path, originAttributes)
228
+ )
229
+ SQL
230
+ @db.execute(<<-'SQL')
231
+ CREATE INDEX moz_basedomain
232
+ ON moz_cookies (baseDomain,
233
+ originAttributes);
234
+ SQL
235
+ end
236
+
175
237
  def db_prepare(sql)
176
238
  st = @db.prepare(sql)
177
239
  yield st
@@ -222,7 +284,7 @@ class HTTP::CookieJar
222
284
  when 4
223
285
  @db.execute("ALTER TABLE moz_cookies RENAME TO moz_cookies_old")
224
286
  @db.execute("DROP INDEX moz_basedomain")
225
- create_table
287
+ create_table_v5
226
288
  @db.execute(<<-'SQL')
227
289
  INSERT INTO moz_cookies
228
290
  (baseDomain, appId, inBrowserElement, name, value, host, path, expiry,
@@ -232,7 +294,42 @@ class HTTP::CookieJar
232
294
  FROM moz_cookies_old
233
295
  SQL
234
296
  @db.execute("DROP TABLE moz_cookies_old")
297
+ when 5
298
+ @db.execute("ALTER TABLE moz_cookies RENAME TO moz_cookies_old")
299
+ @db.execute("DROP INDEX moz_basedomain")
300
+ create_table_v6
301
+ @db.create_function('CONVERT_TO_ORIGIN_ATTRIBUTES', 2) { |func, appId, inBrowserElement|
302
+ params = {}
303
+ params['appId'] = appId if appId.nonzero?
304
+ params['inBrowserElement'] = inBrowserElement if inBrowserElement.nonzero?
305
+ func.result = encode_www_form(params)
306
+ }
307
+ @db.execute(<<-'SQL')
308
+ INSERT INTO moz_cookies
309
+ (baseDomain, originAttributes, name, value, host, path, expiry,
310
+ lastAccessed, creationTime, isSecure, isHttpOnly)
311
+ SELECT baseDomain,
312
+ CONVERT_TO_ORIGIN_ATTRIBUTES(appId, inBrowserElement),
313
+ name, value, host, path, expiry, lastAccessed, creationTime,
314
+ isSecure, isHttpOnly
315
+ FROM moz_cookies_old
316
+ SQL
317
+ @db.execute("DROP TABLE moz_cookies_old")
318
+ when 6
319
+ @db.execute("ALTER TABLE moz_cookies ADD appId INTEGER DEFAULT 0")
320
+ @db.execute("ALTER TABLE moz_cookies ADD inBrowserElement INTEGER DEFAULT 0")
321
+ @db.create_function('SET_APP_ID', 1) { |func, originAttributes|
322
+ func.result = get_query_param(originAttributes, 'appId').to_i # nil.to_i == 0
323
+ }
324
+ @db.create_function('SET_IN_BROWSER', 1) { |func, originAttributes|
325
+ func.result = get_query_param(originAttributes, 'inBrowserElement').to_i # nil.to_i == 0
326
+ }
327
+ @db.execute(<<-'SQL')
328
+ UPDATE moz_cookies SET appId = SET_APP_ID(originAttributes),
329
+ inBrowserElement = SET_IN_BROWSER(originAttributes)
330
+ SQL
235
331
  @logger.info("Upgraded database to schema version %d" % schema_version) if @logger
332
+ self.schema_version += 1
236
333
  else
237
334
  break
238
335
  end
@@ -255,16 +352,17 @@ class HTTP::CookieJar
255
352
  def db_add(cookie)
256
353
  @stmt[:add].execute({
257
354
  :baseDomain => cookie.domain_name.domain || cookie.domain,
258
- :appId => @app_id,
259
- :inBrowserElement => @in_browser_element ? 1 : 0,
355
+ :originAttributes => @origin_attributes,
260
356
  :name => cookie.name, :value => cookie.value,
261
357
  :host => cookie.dot_domain,
262
358
  :path => cookie.path,
263
359
  :expiry => cookie.expires_at.to_i,
264
- :creationTime => cookie.created_at.to_i,
265
- :lastAccessed => cookie.accessed_at.to_i,
360
+ :creationTime => serialize_usectime(cookie.created_at),
361
+ :lastAccessed => serialize_usectime(cookie.accessed_at),
266
362
  :isSecure => cookie.secure? ? 1 : 0,
267
363
  :isHttpOnly => cookie.httponly? ? 1 : 0,
364
+ :appId => @app_id,
365
+ :inBrowserElement => @in_browser_element ? 1 : 0,
268
366
  })
269
367
  cleanup if (@gc_index += 1) >= @gc_threshold
270
368
 
@@ -291,6 +389,36 @@ class HTTP::CookieJar
291
389
  self
292
390
  end
293
391
 
392
+ if RUBY_VERSION >= '1.9'
393
+ def encode_www_form(enum)
394
+ URI.encode_www_form(enum)
395
+ end
396
+
397
+ def get_query_param(str, key)
398
+ URI.decode_www_form(str).find { |k, v|
399
+ break v if k == key
400
+ }
401
+ end
402
+ else
403
+ require 'cgi'
404
+
405
+ def encode_www_form(enum)
406
+ enum.map { |k, v| "#{CGI.escape(k)}=#{CGI.escape(v)}" }.join('&')
407
+ end
408
+
409
+ def get_query_param(str, key)
410
+ CGI.parse(str)[key].first
411
+ end
412
+ end
413
+
414
+ def serialize_usectime(time)
415
+ time ? (time.to_f * 1e6).floor : 0
416
+ end
417
+
418
+ def deserialize_usectime(value)
419
+ Time.at(value ? value / 1e6 : 0)
420
+ end
421
+
294
422
  public
295
423
 
296
424
  def add(cookie)
@@ -329,11 +457,10 @@ class HTTP::CookieJar
329
457
  expiry >= :expiry
330
458
  SQL
331
459
 
332
- def each(uri = nil, &block)
460
+ def each(uri = nil, &block) # :yield: cookie
333
461
  now = Time.now
334
462
  if uri
335
463
  thost = DomainName.new(uri.host)
336
- tpath = uri.path
337
464
 
338
465
  @stmt[:cookies_for_domain].execute({
339
466
  :baseDomain => thost.domain || thost.hostname,
@@ -351,8 +478,8 @@ class HTTP::CookieJar
351
478
  attrs[:domain] = row['host']
352
479
  attrs[:path] = row['path']
353
480
  attrs[:expires_at] = Time.at(row['expiry'])
354
- attrs[:accessed_at] = Time.at(row['lastAccessed'] || 0)
355
- attrs[:created_at] = Time.at(row['creationTime'] || 0)
481
+ attrs[:accessed_at] = deserialize_usectime(row['lastAccessed'])
482
+ attrs[:created_at] = deserialize_usectime(row['creationTime'])
356
483
  attrs[:secure] = secure
357
484
  attrs[:httponly] = row['isHttpOnly'] != 0
358
485
  })
@@ -360,7 +487,7 @@ class HTTP::CookieJar
360
487
  if cookie.valid_for_uri?(uri)
361
488
  cookie.accessed_at = now
362
489
  @stmt[:update_lastaccessed].execute({
363
- 'lastAccessed' => now.to_i,
490
+ 'lastAccessed' => serialize_usectime(now),
364
491
  'id' => row['id'],
365
492
  })
366
493
  yield cookie
@@ -379,8 +506,8 @@ class HTTP::CookieJar
379
506
  attrs[:domain] = row['host']
380
507
  attrs[:path] = row['path']
381
508
  attrs[:expires_at] = Time.at(row['expiry'])
382
- attrs[:accessed_at] = Time.at(row['lastAccessed'] || 0)
383
- attrs[:created_at] = Time.at(row['creationTime'] || 0)
509
+ attrs[:accessed_at] = deserialize_usectime(row['lastAccessed'])
510
+ attrs[:created_at] = deserialize_usectime(row['creationTime'])
384
511
  attrs[:secure] = row['isSecure'] != 0
385
512
  attrs[:httponly] = row['isHttpOnly'] != 0
386
513
  })
@@ -398,19 +525,6 @@ class HTTP::CookieJar
398
525
  self
399
526
  end
400
527
 
401
- SQL[:count] = <<-'SQL'
402
- SELECT COUNT(id) FROM moz_cookies
403
- SQL
404
-
405
- def count
406
- @stmt[:count].execute.first[0]
407
- end
408
- protected :count
409
-
410
- def empty?
411
- @sjar.empty? && count == 0
412
- end
413
-
414
528
  SQL[:delete_expired] = <<-'SQL'
415
529
  DELETE FROM moz_cookies WHERE expiry < :expiry
416
530
  SQL
@@ -1,16 +1,27 @@
1
+ # :markup: markdown
1
2
  require 'http/cookie_jar'
2
3
  require 'psych' if !defined?(YAML) && RUBY_VERSION == "1.9.2"
3
4
  require 'yaml'
4
5
 
5
- # YAMLSaver saves and loads cookies in the YAML format.
6
+ # YAMLSaver saves and loads cookies in the YAML format. It can load a
7
+ # YAML file saved by Mechanize, but the saving format is not
8
+ # compatible with older versions of Mechanize (< 2.7).
6
9
  class HTTP::CookieJar::YAMLSaver < HTTP::CookieJar::AbstractSaver
10
+ # :singleton-method: new
11
+ # :call-seq:
12
+ # new(**options)
13
+ #
14
+ # There is no option keyword supported at the moment.
15
+
16
+ ##
17
+
7
18
  def save(io, jar)
8
19
  YAML.dump(@session ? jar.to_a : jar.reject(&:session?), io)
9
20
  end
10
21
 
11
22
  def load(io, jar)
12
23
  begin
13
- data = YAML.load(io)
24
+ data = load_yaml(io)
14
25
  rescue ArgumentError => e
15
26
  case e.message
16
27
  when %r{\Aundefined class/module Mechanize::}
@@ -20,7 +31,7 @@ class HTTP::CookieJar::YAMLSaver < HTTP::CookieJar::AbstractSaver
20
31
  yaml = io.read
21
32
  # a gross hack
22
33
  yaml.gsub!(%r{^( [^ ].*:) !ruby/object:Mechanize::Cookie$}, "\\1")
23
- data = YAML.load(yaml)
34
+ data = load_yaml(yaml)
24
35
  rescue Errno::ESPIPE
25
36
  @logger.warn "could not rewind the stream for conversion" if @logger
26
37
  rescue ArgumentError
@@ -42,7 +53,11 @@ class HTTP::CookieJar::YAMLSaver < HTTP::CookieJar::AbstractSaver
42
53
  # YAML::Object of Syck
43
54
  cookie_hash = cookie_hash.ivars
44
55
  end
45
- cookie = HTTP::Cookie.new(cookie_hash)
56
+ cookie = HTTP::Cookie.new({}.tap { |hash|
57
+ cookie_hash.each_pair { |key, value|
58
+ hash[key.to_sym] = value
59
+ }
60
+ })
46
61
  jar.add(cookie)
47
62
  }
48
63
  }
@@ -58,4 +73,14 @@ class HTTP::CookieJar::YAMLSaver < HTTP::CookieJar::AbstractSaver
58
73
  def default_options
59
74
  {}
60
75
  end
76
+
77
+ if YAML.name == 'Psych' && Psych::VERSION >= '3.1'
78
+ def load_yaml(yaml)
79
+ YAML.safe_load(yaml, :permitted_classes => %w[Time HTTP::Cookie Mechanize::Cookie DomainName], :aliases => true)
80
+ end
81
+ else
82
+ def load_yaml(yaml)
83
+ YAML.load(yaml)
84
+ end
85
+ end
61
86
  end
data/test/helper.rb CHANGED
@@ -3,6 +3,30 @@ require 'test-unit'
3
3
  require 'uri'
4
4
  require 'http/cookie'
5
5
 
6
+ module Test
7
+ module Unit
8
+ module Assertions
9
+ def assert_warn(pattern, message = nil, &block)
10
+ class << (output = "")
11
+ alias write <<
12
+ end
13
+ stderr, $stderr = $stderr, output
14
+ yield
15
+ assert_match(pattern, output, message)
16
+ ensure
17
+ $stderr = stderr
18
+ end
19
+
20
+ def assert_warning(pattern, message = nil, &block)
21
+ verbose, $VERBOSE = $VERBOSE, true
22
+ assert_warn(pattern, message, &block)
23
+ ensure
24
+ $VERBOSE = verbose
25
+ end
26
+ end
27
+ end
28
+ end
29
+
6
30
  module Enumerable
7
31
  def combine
8
32
  masks = inject([[], 1]){|(ar, m), e| [ar << m, m << 1 ] }[0]
@@ -23,3 +47,9 @@ end
23
47
  def test_file(filename)
24
48
  File.expand_path(filename, File.dirname(__FILE__))
25
49
  end
50
+
51
+ def sleep_until(time)
52
+ if (s = time - Time.now) > 0
53
+ sleep s
54
+ end
55
+ end
@@ -2,13 +2,6 @@
2
2
  require File.expand_path('helper', File.dirname(__FILE__))
3
3
 
4
4
  class TestHTTPCookie < Test::Unit::TestCase
5
- def silently
6
- warn_level, $VERBOSE = $VERBOSE, false
7
- yield
8
- ensure
9
- $VERBOSE = warn_level
10
- end
11
-
12
5
  def setup
13
6
  httpdate = 'Sun, 27-Sep-2037 00:00:00 GMT'
14
7
 
@@ -46,12 +39,10 @@ class TestHTTPCookie < Test::Unit::TestCase
46
39
 
47
40
  dates.each do |date|
48
41
  cookie = "PREF=1; expires=#{date}"
49
- silently do
50
- assert_equal 1, HTTP::Cookie.parse(cookie, url) { |c|
51
- assert c.expires, "Tried parsing: #{date}"
52
- assert_send [c.expires, :<, yesterday]
53
- }.size
54
- end
42
+ assert_equal 1, HTTP::Cookie.parse(cookie, url) { |c|
43
+ assert c.expires, "Tried parsing: #{date}"
44
+ assert_send [c.expires, :<, yesterday]
45
+ }.size
55
46
  end
56
47
 
57
48
  [
@@ -135,6 +126,22 @@ class TestHTTPCookie < Test::Unit::TestCase
135
126
  assert_equal 0, HTTP::Cookie.parse(cookie, url).size
136
127
  end
137
128
 
129
+ def test_parse_bad_name
130
+ cookie = "a\001b=c"
131
+ url = URI.parse('http://www.example.com/')
132
+ assert_nothing_raised {
133
+ assert_equal 0, HTTP::Cookie.parse(cookie, url).size
134
+ }
135
+ end
136
+
137
+ def test_parse_bad_value
138
+ cookie = "a=b\001c"
139
+ url = URI.parse('http://www.example.com/')
140
+ assert_nothing_raised {
141
+ assert_equal 0, HTTP::Cookie.parse(cookie, url).size
142
+ }
143
+ end
144
+
138
145
  def test_parse_weird_cookie
139
146
  cookie = 'n/a, ASPSESSIONIDCSRRQDQR=FBLDGHPBNDJCPCGNCPAENELB; path=/'
140
147
  url = URI.parse('http://www.searchinnovation.com/')
@@ -177,14 +184,12 @@ class TestHTTPCookie < Test::Unit::TestCase
177
184
  "20/06/95 21:07",
178
185
  ]
179
186
 
180
- silently do
181
- dates.each do |date|
182
- cookie = "PREF=1; expires=#{date}"
183
- assert_equal 1, HTTP::Cookie.parse(cookie, url) { |c|
184
- assert_equal(true, c.expires.nil?)
185
- }.size
186
- end
187
- end
187
+ dates.each { |date|
188
+ cookie = "PREF=1; expires=#{date}"
189
+ assert_equal 1, HTTP::Cookie.parse(cookie, url) { |c|
190
+ assert_equal(true, c.expires.nil?)
191
+ }.size
192
+ }
188
193
  end
189
194
 
190
195
  def test_parse_domain_dot
@@ -436,17 +441,28 @@ class TestHTTPCookie < Test::Unit::TestCase
436
441
  ['Bar', 'value 2'],
437
442
  ['Baz', 'value3'],
438
443
  ['Bar', 'value"4'],
444
+ ['Quux', 'x, value=5'],
439
445
  ]
440
446
 
441
447
  cookie_value = HTTP::Cookie.cookie_value(pairs.map { |name, value|
442
448
  HTTP::Cookie.new(:name => name, :value => value)
443
449
  })
444
450
 
445
- assert_equal 'Foo=value1; Bar="value 2"; Baz=value3; Bar="value\\"4"', cookie_value
451
+ assert_equal 'Foo=value1; Bar="value 2"; Baz=value3; Bar="value\\"4"; Quux="x, value=5"', cookie_value
446
452
 
447
453
  hash = HTTP::Cookie.cookie_value_to_hash(cookie_value)
448
454
 
449
- assert_equal 3, hash.size
455
+ assert_equal pairs.map(&:first).uniq.size, hash.size
456
+
457
+ hash.each_pair { |name, value|
458
+ _, pvalue = pairs.assoc(name)
459
+ assert_equal pvalue, value
460
+ }
461
+
462
+ # Do not treat comma in a Cookie header value as separator; see CVE-2016-7401
463
+ hash = HTTP::Cookie.cookie_value_to_hash('Quux=x, value=5; Foo=value1; Bar="value 2"; Baz=value3; Bar="value\\"4"')
464
+
465
+ assert_equal pairs.map(&:first).uniq.size, hash.size
450
466
 
451
467
  hash.each_pair { |name, value|
452
468
  _, pvalue = pairs.assoc(name)
@@ -537,6 +553,30 @@ class TestHTTPCookie < Test::Unit::TestCase
537
553
  cookie.acceptable?
538
554
  }
539
555
 
556
+ # various keywords
557
+ [
558
+ ["Expires", /use downcased symbol/],
559
+ ].each { |key, pattern|
560
+ assert_warning(pattern, "warn of key: #{key.inspect}") {
561
+ cookie = HTTP::Cookie.new(:value => 'value', :name => 'key', key => expires.dup)
562
+ assert_equal 'key', cookie.name
563
+ assert_equal 'value', cookie.value
564
+ assert_equal expires, cookie.expires, "key: #{key.inspect}"
565
+ }
566
+ }
567
+ [
568
+ [:Expires, /unknown attribute name/],
569
+ [:expires?, /unknown attribute name/],
570
+ [[:expires], /invalid keyword/],
571
+ ].each { |key, pattern|
572
+ assert_warning(pattern, "warn of key: #{key.inspect}") {
573
+ cookie = HTTP::Cookie.new(:value => 'value', :name => 'key', key => expires.dup)
574
+ assert_equal 'key', cookie.name
575
+ assert_equal 'value', cookie.value
576
+ assert_equal nil, cookie.expires, "key: #{key.inspect}"
577
+ }
578
+ }
579
+
540
580
  cookie = HTTP::Cookie.new(:value => 'value', :name => 'key', :expires => expires.dup)
541
581
  assert_equal 'key', cookie.name
542
582
  assert_equal 'value', cookie.value
@@ -679,19 +719,32 @@ class TestHTTPCookie < Test::Unit::TestCase
679
719
 
680
720
  def test_max_age=
681
721
  cookie = HTTP::Cookie.new(cookie_values)
722
+ expires = cookie.expires
682
723
 
683
724
  assert_raises(ArgumentError) {
684
725
  cookie.max_age = "+1"
685
726
  }
727
+ # make sure #expires is not destroyed
728
+ assert_equal expires, cookie.expires
729
+
686
730
  assert_raises(ArgumentError) {
687
731
  cookie.max_age = "1.5"
688
732
  }
733
+ # make sure #expires is not destroyed
734
+ assert_equal expires, cookie.expires
735
+
689
736
  assert_raises(ArgumentError) {
690
737
  cookie.max_age = "1 day"
691
738
  }
739
+ # make sure #expires is not destroyed
740
+ assert_equal expires, cookie.expires
741
+
692
742
  assert_raises(TypeError) {
693
743
  cookie.max_age = [1]
694
744
  }
745
+ # make sure #expires is not destroyed
746
+ assert_equal expires, cookie.expires
747
+
695
748
  cookie.max_age = "12"
696
749
  assert_equal 12, cookie.max_age
697
750