From 060fc63c2a7f3d5c584825cba01800147e46280c Mon Sep 17 00:00:00 2001 From: Akinori MUSHA Date: Sun, 14 Oct 2012 18:52:52 +0900 Subject: [PATCH] Import cookie related stuff from Mechanize. --- .gitignore | 17 ++ Gemfile | 4 + LICENSE.txt | 23 ++ README.md | 49 ++++ Rakefile | 20 ++ http-cookie.gemspec | 26 ++ lib/http-cookie.rb | 1 + lib/http/cookie.rb | 243 ++++++++++++++++ lib/http/cookie/version.rb | 5 + lib/http/cookie_jar.rb | 221 +++++++++++++++ test/helper.rb | 21 ++ test/test_http_cookie.rb | 482 ++++++++++++++++++++++++++++++++ test/test_http_cookie_jar.rb | 518 +++++++++++++++++++++++++++++++++++ 13 files changed, 1630 insertions(+) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 Rakefile create mode 100644 http-cookie.gemspec create mode 100644 lib/http-cookie.rb create mode 100644 lib/http/cookie.rb create mode 100644 lib/http/cookie/version.rb create mode 100644 lib/http/cookie_jar.rb create mode 100644 test/helper.rb create mode 100644 test/test_http_cookie.rb create mode 100644 test/test_http_cookie_jar.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d87d4be --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..527e606 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in http-cookie.gemspec +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..69fa166 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,23 @@ +Copyright (c) 2011-2012 Akinori MUSHA, Eric Hodel +Copyright (c) 2006-2011 Aaron Patterson, Mike Dalessio + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1293364 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# HTTP::Cookie + +HTTP::Cookie is a ruby library to handle HTTP cookies in a way both +compliant with RFCs and compatible with today's major browsers. + +It was originally a part of the Mechanize library, separated as an +independent library in the hope of serving as a common component that +is reusable from any HTTP related piece of software. + +## Installation + +Add this line to your application's Gemfile: + + gem 'http-cookie' + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install http-cookie + +## Usage + + # Initialize a cookie jar + jar = HTTP::CookieJar.new + + # Load from a file + jar.load(filename) if File.exist?(filename) + + # Store received cookies + HTTP::Cookie.parse(uri, set_cookie_header_value) { |cookie| + jar << cookie + } + + # Extract cookies to send + cookie_value_to_send = jar.cookies(uri).join(', ') + + # Save to a file + jar.save_as(filename) + +## Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..0a4671d --- /dev/null +++ b/Rakefile @@ -0,0 +1,20 @@ +require "bundler/gem_tasks" + +require 'rake/testtask' +Rake::TestTask.new(:test) do |test| + test.libs << 'lib' << 'test' + test.pattern = 'test/**/test_*.rb' + test.verbose = true +end + +task :default => :test + +require 'rdoc/task' +Rake::RDocTask.new do |rdoc| + version = HTTP::Cookie::VERSION + + rdoc.rdoc_dir = 'rdoc' + rdoc.title = "http-cookie #{version}" + rdoc.rdoc_files.include('lib/**/*.rb') + rdoc.rdoc_files.include(Bundler::GemHelper.gemspec.extra_rdoc_files) +end diff --git a/http-cookie.gemspec b/http-cookie.gemspec new file mode 100644 index 0000000..9620bb0 --- /dev/null +++ b/http-cookie.gemspec @@ -0,0 +1,26 @@ +# -*- encoding: utf-8 -*- +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'http/cookie/version' + +Gem::Specification.new do |gem| + gem.name = "http-cookie" + gem.version = HTTP::Cookie::VERSION + gem.authors, gem.email = { + 'Akinori MUSHA' => 'knu@idaemons.org', + 'Aaron Patterson' => 'aaronp@rubyforge.org', + 'Eric Hodel' => 'drbrain@segment7.net', + 'Mike Dalessio' => 'mike.dalessio@gmail.com', + }.instance_eval { [keys, values] } + + gem.description = %q{A Ruby library to handle HTTP Cookies} + gem.summary = %q{A Ruby library to handle HTTP Cookies} + gem.homepage = "https://github.com/sparklemotion/http-cookie" + + gem.files = `git ls-files`.split($/) + gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } + gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) + gem.require_paths = ["lib"] + + gem.add_runtime_dependency("domain_name", ["~> 0.5"]) +end diff --git a/lib/http-cookie.rb b/lib/http-cookie.rb new file mode 100644 index 0000000..eac38ef --- /dev/null +++ b/lib/http-cookie.rb @@ -0,0 +1 @@ +require 'http/cookie' diff --git a/lib/http/cookie.rb b/lib/http/cookie.rb new file mode 100644 index 0000000..f78c4bc --- /dev/null +++ b/lib/http/cookie.rb @@ -0,0 +1,243 @@ +require 'http/cookie/version' +require 'time' +require 'webrick/httputils' +require 'domain_name' + +module HTTP + autoload :CookieJar, 'http/cookie_jar' +end + +# This class is used to represent an HTTP Cookie. +class HTTP::Cookie + attr_reader :name + attr_accessor :value, :version + attr_accessor :domain, :path, :secure + attr_accessor :comment, :max_age + + attr_accessor :session + + attr_accessor :created_at + attr_accessor :accessed_at + + # :call-seq: + # new(name, value) + # new(name, value, attr_hash) + # new(attr_hash) + # + # Creates a cookie object. For each key of +attr_hash+, the setter + # is called if defined. Each key can be either a symbol or a + # string, downcased or not. + # + # e.g. + # new("uid", "a12345") + # new("uid", "a12345", :domain => 'example.org', + # :for_domain => true, :expired => Time.now + 7*86400) + # new("name" => "uid", "value" => "a12345", "Domain" => 'www.example.org') + # + def initialize(*args) + @version = 0 # Netscape Cookie + + @domain = @path = @secure = @comment = @max_age = + @expires = @comment_url = @discard = @port = nil + + @created_at = @accessed_at = Time.now + case args.size + when 2 + @name, @value = *args + @for_domain = false + return + when 3 + @name, @value, attr_hash = *args + when 1 + attr_hash = args.first + else + raise ArgumentError, "wrong number of arguments (#{args.size} for 1-3)" + end + for_domain = false + attr_hash.each_pair { |key, val| + skey = key.to_s.downcase + skey.sub!(/[!?]\z/, '') + case skey + when 'for_domain' + for_domain = !!val + when 'name' + @name = val + when 'value' + @value = val + else + setter = :"#{skey}=" + send(setter, val) if respond_to?(setter) + end + } + @for_domain = for_domain + end + + # If this flag is true, this cookie will be sent to any host in the + # +domain+. If it is false, this cookie will be sent only to the + # host indicated by the +domain+. + attr_accessor :for_domain + alias for_domain? for_domain + + class << self + # Parses a Set-Cookie header line +str+ sent from +uri+ into an + # array of Cookie objects. Note that this array may contain + # nil's when some of the cookie-pairs are malformed. + def parse(uri, str, log = nil) + return str.split(/,(?=[^;,]*=)|,$/).map { |c| + cookie_elem = c.split(/;+/) + first_elem = cookie_elem.shift + first_elem.strip! + key, value = first_elem.split(/\=/, 2) + + begin + cookie = new(key, value.dup) + rescue + log.warn("Couldn't parse key/value: #{first_elem}") if log + next + end + + cookie_elem.each do |pair| + pair.strip! + key, value = pair.split(/=/, 2) + next unless key + value = WEBrick::HTTPUtils.dequote(value.strip) if value + + case key.downcase + when 'domain' + next unless value && !value.empty? + begin + cookie.domain = value + cookie.for_domain = true + rescue + log.warn("Couldn't parse domain: #{value}") if log + end + when 'path' + next unless value && !value.empty? + cookie.path = value + when 'expires' + next unless value && !value.empty? + begin + cookie.expires = Time::parse(value) + rescue + log.warn("Couldn't parse expires: #{value}") if log + end + when 'max-age' + next unless value && !value.empty? + begin + cookie.max_age = Integer(value) + rescue + log.warn("Couldn't parse max age '#{value}'") if log + end + when 'comment' + next unless value + cookie.comment = value + when 'version' + next unless value + begin + cookie.version = Integer(value) + rescue + log.warn("Couldn't parse version '#{value}'") if log + cookie.version = nil + end + when 'secure' + cookie.secure = true + end + end + + cookie.path ||= (uri + './').path + cookie.secure ||= false + cookie.domain ||= uri.host + + # RFC 6265 4.1.2.2 + cookie.expires = Time.now + cookie.max_age if cookie.max_age + cookie.session = !cookie.expires + + # Move this in to the cookie jar + yield cookie if block_given? + + cookie + } + end + end + + alias set_domain domain= + + # Sets the domain attribute. A leading dot in +domain+ implies + # turning the +for_domain?+ flag on. + def domain=(domain) + if DomainName === domain + @domain_name = domain + else + domain.is_a?(String) or + (domain.respond_to?(:to_str) && (domain = domain.to_str).is_a?(String)) or + raise TypeError, "#{domain.class} is not a String" + if domain.start_with?('.') + @for_domain = true + domain = domain[1..-1] + end + # Do we really need to support this? + if domain.match(/\A([^:]+):[0-9]+\z/) + domain = $1 + end + @domain_name = DomainName.new(domain) + end + set_domain(@domain_name.hostname) + end + + def expires=(t) + @expires = t && (t.is_a?(Time) ? t.httpdate : t.to_s) + end + + def expires + @expires && Time.parse(@expires) + end + + def expired? + return false unless expires + Time.now > expires + end + + alias secure? secure + + def acceptable_from_uri?(uri) + host = DomainName.new(uri.host) + + # RFC 6265 5.3 + # When the user agent "receives a cookie": + return host.hostname == domain unless @for_domain + + if host.cookie_domain?(@domain_name) + true + elsif host.hostname == domain + @for_domain = false + true + else + false + end + end + + def valid_for_uri?(uri) + return false if secure? && uri.scheme != 'https' + acceptable_from_uri?(uri) && uri.path.start_with?(path) + end + + def to_s + "#{@name}=#{@value}" + end + + def init_with(coder) + yaml_initialize(coder.tag, coder.map) + end + + def yaml_initialize(tag, map) + @for_domain = true # for forward compatibility + map.each { |key, value| + case key + when 'domain' + self.domain = value # ditto + else + instance_variable_set(:"@#{key}", value) + end + } + end +end diff --git a/lib/http/cookie/version.rb b/lib/http/cookie/version.rb new file mode 100644 index 0000000..39701a0 --- /dev/null +++ b/lib/http/cookie/version.rb @@ -0,0 +1,5 @@ +module HTTP + class Cookie + VERSION = "0.0.1" + end +end diff --git a/lib/http/cookie_jar.rb b/lib/http/cookie_jar.rb new file mode 100644 index 0000000..3dba734 --- /dev/null +++ b/lib/http/cookie_jar.rb @@ -0,0 +1,221 @@ +module HTTP + autoload :Cookie, 'http/cookie' +end + +## +# This class is used to manage the Cookies that have been returned from +# any particular website. + +class HTTP::CookieJar + include Enumerable + + # add_cookie wants something resembling a URI. + + attr_reader :jar + + def initialize + @jar = {} + end + + def initialize_copy other # :nodoc: + @jar = Marshal.load Marshal.dump other.jar + end + + # Add a +cookie+ to the jar if it is considered acceptable from + # +uri+. Return nil if the cookie was not added, otherwise return + # the cookie added. + def add(uri, cookie) + return nil unless cookie.acceptable_from_uri?(uri) + add!(cookie) + cookie + end + + # Add a +cookie+ to the jar and return self. + def add!(cookie) + normal_domain = cookie.domain.downcase + + @jar[normal_domain] ||= {} unless @jar.has_key?(normal_domain) + + @jar[normal_domain][cookie.path] ||= {} + @jar[normal_domain][cookie.path][cookie.name] = cookie + + self + end + alias << add! + + # Fetch the cookies that should be used for the URI object passed in. + def cookies(url) + cleanup + url.path = '/' if url.path.empty? + now = Time.now + + select { |cookie| + !cookie.expired? && cookie.valid_for_uri?(url) && (cookie.accessed_at = now) + }.sort_by { |cookie| + # RFC 6265 5.4 + # Precedence: 1. longer path 2. older creation + [-cookie.path.length, cookie.created_at] + } + end + + def empty?(url) + cookies(url).length > 0 ? false : true + end + + def each + block_given? or return enum_for(__method__) + cleanup + @jar.each { |domain, paths| + paths.each { |path, hash| + hash.each_value { |cookie| + yield cookie + } + } + } + end + + # call-seq: + # jar.save_as(file, format = :yaml) + # jar.save_as(file, options) + # + # Save the cookie jar to a file in the format specified and return + # self. + # + # Available option keywords are below: + # + # * +format+ + # [:yaml] + # YAML structure (default) + # [:cookiestxt] + # Mozilla's cookies.txt format + # * +session+ + # [+true+] + # Save session cookies as well. + # [+false+] + # Do not save session cookies. (default) + def save_as(file, options = nil) + if Symbol === options + format = options + session = false + else + options ||= {} + format = options[:format] || :yaml + session = !!options[:session] + end + + jar = dup + jar.cleanup !session + + open(file, 'w') { |f| + case format + when :yaml then + load_yaml + + YAML.dump(jar.jar, f) + when :cookiestxt then + jar.dump_cookiestxt(f) + else + raise ArgumentError, "Unknown cookie jar file format" + end + } + + self + end + + # Load cookie jar from a file in the format specified. + # + # Available formats: + # :yaml <- YAML structure. + # :cookiestxt <- Mozilla's cookies.txt format + def load(file, format = :yaml) + @jar = open(file) { |f| + case format + when :yaml then + load_yaml + + YAML.load(f) + when :cookiestxt then + load_cookiestxt(f) + else + raise ArgumentError, "Unknown cookie jar file format" + end + } + + cleanup + + self + end + + def load_yaml # :nodoc: + begin + require 'psych' + rescue LoadError + end + + require 'yaml' + end + + # Clear the cookie jar + def clear! + @jar = {} + end + + # Read cookies from Mozilla cookies.txt-style IO stream + def load_cookiestxt(io) + now = Time.now + + io.each_line do |line| + line.chomp! + line.gsub!(/#.+/, '') + fields = line.split("\t") + + next if fields.length != 7 + + expires_seconds = fields[4].to_i + expires = (expires_seconds == 0) ? nil : Time.at(expires_seconds) + next if expires and (expires < now) + + c = HTTP::Cookie.new(fields[5], fields[6]) + c.domain = fields[0] + c.for_domain = (fields[1] == "TRUE") # Whether this cookie is for domain + c.path = fields[2] # Path for which the cookie is relevant + c.secure = (fields[3] == "TRUE") # Requires a secure connection + c.expires = expires # Time the cookie expires. + c.version = 0 # Conforms to Netscape cookie spec. + + add!(c) + end + + @jar + end + + # Write cookies to Mozilla cookies.txt-style IO stream + def dump_cookiestxt(io) + to_a.each do |cookie| + io.puts([ + cookie.domain, + cookie.for_domain? ? "TRUE" : "FALSE", + cookie.path, + cookie.secure ? "TRUE" : "FALSE", + cookie.expires.to_i.to_s, + cookie.name, + cookie.value + ].join("\t")) + end + end + + protected + + # Remove expired cookies + def cleanup session = false + @jar.each do |domain, paths| + paths.each do |path, names| + names.each do |cookie_name, cookie| + paths[path].delete(cookie_name) if + cookie.expired? or (session and cookie.session) + end + end + end + end +end + diff --git a/test/helper.rb b/test/helper.rb new file mode 100644 index 0000000..02e9deb --- /dev/null +++ b/test/helper.rb @@ -0,0 +1,21 @@ +require 'rubygems' +require 'test/unit' +require 'uri' +require 'http/cookie' + +module Enumerable + def combine + masks = inject([[], 1]){|(ar, m), e| [ar << m, m << 1 ] }[0] + all = masks.inject(0){ |al, m| al|m } + + result = [] + for i in 1..all do + tmp = [] + each_with_index do |e, idx| + tmp << e unless (masks[idx] & i) == 0 + end + result << tmp + end + result + end +end diff --git a/test/test_http_cookie.rb b/test/test_http_cookie.rb new file mode 100644 index 0000000..6224e98 --- /dev/null +++ b/test/test_http_cookie.rb @@ -0,0 +1,482 @@ +require File.expand_path('helper', File.dirname(__FILE__)) + +class TestHTTPCookie < Test::Unit::TestCase +#class TestHTTPCookie < MiniTest::Unit::TestCase + def silently + warn_level = $VERBOSE + $VERBOSE = false + res = yield + $VERBOSE = warn_level + res + end + + def test_parse_dates + url = URI.parse('http://localhost/') + + yesterday = Time.now - 86400 + + dates = [ "14 Apr 89 03:20:12", + "14 Apr 89 03:20 GMT", + "Fri, 17 Mar 89 4:01:33", + "Fri, 17 Mar 89 4:01 GMT", + "Mon Jan 16 16:12 PDT 1989", + "Mon Jan 16 16:12 +0130 1989", + "6 May 1992 16:41-JST (Wednesday)", + #"22-AUG-1993 10:59:12.82", + "22-AUG-1993 10:59pm", + "22-AUG-1993 12:59am", + "22-AUG-1993 12:59 PM", + #"Friday, August 04, 1995 3:54 PM", + #"06/21/95 04:24:34 PM", + #"20/06/95 21:07", + "95-06-08 19:32:48 EDT", + ] + + dates.each do |date| + cookie = "PREF=1; expires=#{date}" + silently do + HTTP::Cookie.parse(url, cookie) { |c| + assert c.expires, "Tried parsing: #{date}" + assert_equal(true, c.expires < yesterday) + } + end + end + end + + def test_parse_empty + cookie_str = 'a=b; ; c=d' + + uri = URI.parse 'http://example' + + HTTP::Cookie.parse uri, cookie_str do |cookie| + assert_equal 'a', cookie.name + assert_equal 'b', cookie.value + end + end + + def test_parse_no_space + cookie_str = "foo=bar;Expires=Sun, 06 Nov 2011 00:28:06 GMT;Path=/" + + uri = URI.parse 'http://example' + + HTTP::Cookie.parse uri, cookie_str do |cookie| + assert_equal 'foo', cookie.name + assert_equal 'bar', cookie.value + assert_equal '/', cookie.path + assert_equal Time.at(1320539286), cookie.expires + end + end + + def test_parse_quoted + cookie_str = + "quoted=\"value\"; Expires=Sun, 06 Nov 2011 00:11:18 GMT; Path=/" + + uri = URI.parse 'http://example' + + HTTP::Cookie.parse uri, cookie_str do |cookie| + assert_equal 'quoted', cookie.name + assert_equal '"value"', cookie.value + end + end + + def test_parse_weird_cookie + cookie = 'n/a, ASPSESSIONIDCSRRQDQR=FBLDGHPBNDJCPCGNCPAENELB; path=/' + url = URI.parse('http://www.searchinnovation.com/') + HTTP::Cookie.parse(url, cookie) { |c| + assert_equal('ASPSESSIONIDCSRRQDQR', c.name) + assert_equal('FBLDGHPBNDJCPCGNCPAENELB', c.value) + } + end + + def test_double_semicolon + double_semi = 'WSIDC=WEST;; domain=.williams-sonoma.com; path=/' + url = URI.parse('http://williams-sonoma.com/') + HTTP::Cookie.parse(url, double_semi) { |cookie| + assert_equal('WSIDC', cookie.name) + assert_equal('WEST', cookie.value) + } + end + + def test_parse_bad_version + bad_cookie = 'PRETANET=TGIAqbFXtt; Name=/PRETANET; Path=/; Version=1.2; Content-type=text/html; Domain=192.168.6.196; expires=Friday, 13-November-2026 23:01:46 GMT;' + url = URI.parse('http://localhost/') + HTTP::Cookie.parse(url, bad_cookie) { |cookie| + assert_nil(cookie.version) + } + end + + def test_parse_bad_max_age + bad_cookie = 'PRETANET=TGIAqbFXtt; Name=/PRETANET; Path=/; Max-Age=1.2; Content-type=text/html; Domain=192.168.6.196; expires=Friday, 13-November-2026 23:01:46 GMT;' + url = URI.parse('http://localhost/') + HTTP::Cookie.parse(url, bad_cookie) { |cookie| + assert_nil(cookie.max_age) + } + end + + def test_parse_date_fail + url = URI.parse('http://localhost/') + + dates = [ + "20/06/95 21:07", + ] + + silently do + dates.each do |date| + cookie = "PREF=1; expires=#{date}" + HTTP::Cookie.parse(url, cookie) { |c| + assert_equal(true, c.expires.nil?) + } + end + end + end + + def test_parse_domain_dot + url = URI.parse('http://host.example.com/') + + cookie_str = 'a=b; domain=.example.com' + + cookie = HTTP::Cookie.parse(url, cookie_str).first + + assert_equal 'example.com', cookie.domain + assert cookie.for_domain? + end + + def test_parse_domain_no_dot + url = URI.parse('http://host.example.com/') + + cookie_str = 'a=b; domain=example.com' + + cookie = HTTP::Cookie.parse(url, cookie_str).first + + assert_equal 'example.com', cookie.domain + assert cookie.for_domain? + end + + def test_parse_domain_none + url = URI.parse('http://example.com/') + + cookie_str = 'a=b;' + + cookie = HTTP::Cookie.parse(url, cookie_str).first + + assert_equal 'example.com', cookie.domain + assert !cookie.for_domain? + end + + def test_parse_max_age + url = URI.parse('http://localhost/') + + date = 'Mon, 19 Feb 2012 19:26:04 GMT' + + cookie = HTTP::Cookie.parse(url, "name=Akinori; expires=#{date}").first + assert_equal Time.at(1329679564), cookie.expires + + cookie = HTTP::Cookie.parse(url, 'name=Akinori; max-age=3600').first + assert_in_delta Time.now + 3600, cookie.expires, 1 + + # Max-Age has precedence over Expires + cookie = HTTP::Cookie.parse(url, "name=Akinori; max-age=3600; expires=#{date}").first + assert_in_delta Time.now + 3600, cookie.expires, 1 + + cookie = HTTP::Cookie.parse(url, "name=Akinori; expires=#{date}; max-age=3600").first + assert_in_delta Time.now + 3600, cookie.expires, 1 + end + + def test_parse_expires_session + url = URI.parse('http://localhost/') + + [ + 'name=Akinori', + 'name=Akinori; expires', + 'name=Akinori; max-age', + 'name=Akinori; expires=', + 'name=Akinori; max-age=', + ].each { |str| + cookie = HTTP::Cookie.parse(url, str).first + assert cookie.session, str + } + + [ + 'name=Akinori; expires=Mon, 19 Feb 2012 19:26:04 GMT', + 'name=Akinori; max-age=3600', + ].each { |str| + cookie = HTTP::Cookie.parse(url, str).first + assert !cookie.session, str + } + end + + def test_parse_many + url = URI 'http://example/' + cookie_str = + "name=Aaron; Domain=localhost; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/, " \ + "name=Aaron; Domain=localhost; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/, " \ + "name=Aaron; Domain=localhost; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/, " \ + "name=Aaron; Domain=localhost; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/; HttpOnly, " \ + "expired=doh; Expires=Fri, 04 Nov 2011 00:29:51 GMT; Path=/, " \ + "a_path=some_path; Expires=Sun, 06 Nov 2011 00:29:51 GMT; Path=/some_path, " \ + "no_path1=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT, no_expires=nope; Path=/, " \ + "no_path2=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path, " \ + "no_path3=no_path; Expires=Sun, 06 Nov 2011 00:29:52 GMT; no_expires=nope; Path=, " \ + "no_domain1=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope, " \ + "no_domain2=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope; Domain, " \ + "no_domain3=no_domain; Expires=Sun, 06 Nov 2011 00:29:53 GMT; no_expires=nope; Domain=" + + cookies = HTTP::Cookie.parse url, cookie_str + assert_equal 13, cookies.length + + name = cookies.find { |c| c.name == 'name' } + assert_equal "Aaron", name.value + assert_equal "/", name.path + assert_equal Time.at(1320539391), name.expires + + a_path = cookies.find { |c| c.name == 'a_path' } + assert_equal "some_path", a_path.value + assert_equal "/some_path", a_path.path + assert_equal Time.at(1320539391), a_path.expires + + no_expires = cookies.find { |c| c.name == 'no_expires' } + assert_equal "nope", no_expires.value + assert_equal "/", no_expires.path + assert_nil no_expires.expires + + no_path_cookies = cookies.select { |c| c.value == 'no_path' } + assert_equal 3, no_path_cookies.size + no_path_cookies.each { |c| + assert_equal "/", c.path, c.name + assert_equal Time.at(1320539392), c.expires, c.name + } + + no_domain_cookies = cookies.select { |c| c.value == 'no_domain' } + assert_equal 3, no_domain_cookies.size + no_domain_cookies.each { |c| + assert !c.for_domain?, c.name + assert_equal c.domain, url.host, c.name + assert_equal Time.at(1320539393), c.expires, c.name + } + + assert cookies.find { |c| c.name == 'expired' } + end + + def test_parse_valid_cookie + url = URI.parse('http://rubyforge.org/') + cookie_params = {} + cookie_params['expires'] = 'expires=Sun, 27-Sep-2037 00:00:00 GMT' + cookie_params['path'] = 'path=/' + cookie_params['domain'] = 'domain=.rubyforge.org' + cookie_params['httponly'] = 'HttpOnly' + cookie_value = '12345%7D=ASDFWEE345%3DASda' + + expires = Time.parse('Sun, 27-Sep-2037 00:00:00 GMT') + + cookie_params.keys.combine.each do |c| + cookie_text = "#{cookie_value}; " + c.each_with_index do |key, idx| + if idx == (c.length - 1) + cookie_text << "#{cookie_params[key]}" + else + cookie_text << "#{cookie_params[key]}; " + end + end + cookie = nil + HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie } + + assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s) + assert_equal('/', cookie.path) + + # if expires was set, make sure we parsed it + if c.find { |k| k == 'expires' } + assert_equal(expires, cookie.expires) + else + assert_nil(cookie.expires) + end + end + end + + def test_parse_valid_cookie_empty_value + url = URI.parse('http://rubyforge.org/') + cookie_params = {} + cookie_params['expires'] = 'expires=Sun, 27-Sep-2037 00:00:00 GMT' + cookie_params['path'] = 'path=/' + cookie_params['domain'] = 'domain=.rubyforge.org' + cookie_params['httponly'] = 'HttpOnly' + cookie_value = '12345%7D=' + + expires = Time.parse('Sun, 27-Sep-2037 00:00:00 GMT') + + cookie_params.keys.combine.each do |c| + cookie_text = "#{cookie_value}; " + c.each_with_index do |key, idx| + if idx == (c.length - 1) + cookie_text << "#{cookie_params[key]}" + else + cookie_text << "#{cookie_params[key]}; " + end + end + cookie = nil + HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie } + + assert_equal('12345%7D=', cookie.to_s) + assert_equal('', cookie.value) + assert_equal('/', cookie.path) + + # if expires was set, make sure we parsed it + if c.find { |k| k == 'expires' } + assert_equal(expires, cookie.expires) + else + assert_nil(cookie.expires) + end + end + end + + # If no path was given, use the one from the URL + def test_cookie_using_url_path + url = URI.parse('http://rubyforge.org/login.php') + cookie_params = {} + cookie_params['expires'] = 'expires=Sun, 27-Sep-2037 00:00:00 GMT' + cookie_params['path'] = 'path=/' + cookie_params['domain'] = 'domain=.rubyforge.org' + cookie_params['httponly'] = 'HttpOnly' + cookie_value = '12345%7D=ASDFWEE345%3DASda' + + expires = Time.parse('Sun, 27-Sep-2037 00:00:00 GMT') + + cookie_params.keys.combine.each do |c| + next if c.find { |k| k == 'path' } + cookie_text = "#{cookie_value}; " + c.each_with_index do |key, idx| + if idx == (c.length - 1) + cookie_text << "#{cookie_params[key]}" + else + cookie_text << "#{cookie_params[key]}; " + end + end + cookie = nil + HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie } + + assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s) + assert_equal('/', cookie.path) + + # if expires was set, make sure we parsed it + if c.find { |k| k == 'expires' } + assert_equal(expires, cookie.expires) + else + assert_nil(cookie.expires) + end + end + end + + # Test using secure cookies + def test_cookie_with_secure + url = URI.parse('http://rubyforge.org/') + cookie_params = {} + cookie_params['expires'] = 'expires=Sun, 27-Sep-2037 00:00:00 GMT' + cookie_params['path'] = 'path=/' + cookie_params['domain'] = 'domain=.rubyforge.org' + cookie_params['secure'] = 'secure' + cookie_value = '12345%7D=ASDFWEE345%3DASda' + + expires = Time.parse('Sun, 27-Sep-2037 00:00:00 GMT') + + cookie_params.keys.combine.each do |c| + next unless c.find { |k| k == 'secure' } + cookie_text = "#{cookie_value}; " + c.each_with_index do |key, idx| + if idx == (c.length - 1) + cookie_text << "#{cookie_params[key]}" + else + cookie_text << "#{cookie_params[key]}; " + end + end + cookie = nil + HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie } + + assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s) + assert_equal('/', cookie.path) + assert_equal(true, cookie.secure) + + # if expires was set, make sure we parsed it + if c.find { |k| k == 'expires' } + assert_equal(expires, cookie.expires) + else + assert_nil(cookie.expires) + end + end + end + + def test_parse_cookie_no_spaces + url = URI.parse('http://rubyforge.org/') + cookie_params = {} + cookie_params['expires'] = 'expires=Sun, 27-Sep-2037 00:00:00 GMT' + cookie_params['path'] = 'path=/' + cookie_params['domain'] = 'domain=.rubyforge.org' + cookie_params['httponly'] = 'HttpOnly' + cookie_value = '12345%7D=ASDFWEE345%3DASda' + + expires = Time.parse('Sun, 27-Sep-2037 00:00:00 GMT') + + cookie_params.keys.combine.each do |c| + cookie_text = "#{cookie_value};" + c.each_with_index do |key, idx| + if idx == (c.length - 1) + cookie_text << "#{cookie_params[key]}" + else + cookie_text << "#{cookie_params[key]};" + end + end + cookie = nil + HTTP::Cookie.parse(url, cookie_text) { |p_cookie| cookie = p_cookie } + + assert_equal('12345%7D=ASDFWEE345%3DASda', cookie.to_s) + assert_equal('/', cookie.path) + + # if expires was set, make sure we parsed it + if c.find { |k| k == 'expires' } + assert_equal(expires, cookie.expires) + else + assert_nil(cookie.expires) + end + end + end + + def test_new + cookie = HTTP::Cookie.new('key', 'value') + assert_equal 'key', cookie.name + assert_equal 'value', cookie.value + assert_equal nil, cookie.expires + + # Minimum unit for the expires attribute is second + expires = Time.at((Time.now + 3600).to_i) + + cookie = HTTP::Cookie.new('key', 'value', :expires => expires.dup) + assert_equal 'key', cookie.name + assert_equal 'value', cookie.value + assert_equal expires, cookie.expires + + cookie = HTTP::Cookie.new(:value => 'value', :name => 'key', :expires => expires.dup) + assert_equal 'key', cookie.name + assert_equal 'value', cookie.value + assert_equal expires, cookie.expires + end + + def test_domain= + url = URI.parse('http://host.dom.example.com:8080/') + + cookie_str = 'a=b; domain=Example.Com' + cookie = HTTP::Cookie.parse(url, cookie_str).first + assert 'example.com', cookie.domain + + cookie.domain = DomainName(url.host) + assert 'host.dom.example.com', cookie.domain + + cookie.domain = 'Dom.example.com' + assert 'dom.example.com', cookie.domain + + cookie.domain = Object.new.tap { |o| + def o.to_str + 'Example.com' + end + } + assert 'example.com', cookie.domain + end +end + diff --git a/test/test_http_cookie_jar.rb b/test/test_http_cookie_jar.rb new file mode 100644 index 0000000..e3329e1 --- /dev/null +++ b/test/test_http_cookie_jar.rb @@ -0,0 +1,518 @@ +require File.expand_path('helper', File.dirname(__FILE__)) + +class TestMechanizeCookieJar < Test::Unit::TestCase +#class TestMechanizeCookieJar < MiniTest::Unit::TestCase + + def setup + @jar = HTTP::CookieJar.new + end + + def in_tmpdir + Dir.mktmpdir do |dir| + Dir.chdir dir do + yield + end + end + end + + def cookie_values(options = {}) + { + :name => 'Foo', + :value => 'Bar', + :path => '/', + :expires => Time.now + (10 * 86400), + :for_domain => true, + :domain => 'rubyforge.org' + }.merge(options) + end + + def test_two_cookies_same_domain_and_name_different_paths + url = URI 'http://rubyforge.org/' + + cookie = HTTP::Cookie.new(cookie_values) + @jar.add(url, cookie) + @jar.add(url, HTTP::Cookie.new(cookie_values(:path => '/onetwo'))) + + assert_equal(1, @jar.cookies(url).length) + assert_equal 2, @jar.cookies(URI('http://rubyforge.org/onetwo')).length + end + + def test_domain_case + url = URI 'http://rubyforge.org/' + + # Add one cookie with an expiration date in the future + cookie = HTTP::Cookie.new(cookie_values) + @jar.add(url, cookie) + assert_equal(1, @jar.cookies(url).length) + + @jar.add(url, HTTP::Cookie.new( + cookie_values(:domain => 'RuByForge.Org', :name => 'aaron'))) + + assert_equal(2, @jar.cookies(url).length) + + url2 = URI 'http://RuByFoRgE.oRg/' + assert_equal(2, @jar.cookies(url2).length) + end + + def test_host_only + url = URI.parse('http://rubyforge.org/') + + @jar.add(url, HTTP::Cookie.new( + cookie_values(:domain => 'rubyforge.org', :for_domain => false))) + + assert_equal(1, @jar.cookies(url).length) + + assert_equal(1, @jar.cookies(URI('http://RubyForge.org/')).length) + + assert_equal(1, @jar.cookies(URI('https://RubyForge.org/')).length) + + assert_equal(0, @jar.cookies(URI('http://www.rubyforge.org/')).length) + end + + def test_empty_value + values = cookie_values(:value => "") + url = URI 'http://rubyforge.org/' + + # Add one cookie with an expiration date in the future + cookie = HTTP::Cookie.new(values) + @jar.add(url, cookie) + assert_equal(1, @jar.cookies(url).length) + + @jar.add url, HTTP::Cookie.new(values.merge(:domain => 'RuByForge.Org', + :name => 'aaron')) + + assert_equal(2, @jar.cookies(url).length) + + url2 = URI 'http://RuByFoRgE.oRg/' + assert_equal(2, @jar.cookies(url2).length) + end + + def test_add_future_cookies + url = URI 'http://rubyforge.org/' + + # Add one cookie with an expiration date in the future + cookie = HTTP::Cookie.new(cookie_values) + @jar.add(url, cookie) + assert_equal(1, @jar.cookies(url).length) + + # Add the same cookie, and we should still only have one + @jar.add(url, HTTP::Cookie.new(cookie_values)) + assert_equal(1, @jar.cookies(url).length) + + # Make sure we can get the cookie from different paths + assert_equal(1, @jar.cookies(URI('http://rubyforge.org/login')).length) + + # Make sure we can't get the cookie from different domains + assert_equal(0, @jar.cookies(URI('http://google.com/')).length) + end + + def test_add_multiple_cookies + url = URI 'http://rubyforge.org/' + + # Add one cookie with an expiration date in the future + cookie = HTTP::Cookie.new(cookie_values) + @jar.add(url, cookie) + assert_equal(1, @jar.cookies(url).length) + + # Add the same cookie, and we should still only have one + @jar.add(url, HTTP::Cookie.new(cookie_values(:name => 'Baz'))) + assert_equal(2, @jar.cookies(url).length) + + # Make sure we can get the cookie from different paths + assert_equal(2, @jar.cookies(URI('http://rubyforge.org/login')).length) + + # Make sure we can't get the cookie from different domains + assert_equal(0, @jar.cookies(URI('http://google.com/')).length) + end + + def test_add_rejects_cookies_that_do_not_contain_an_embedded_dot + url = URI 'http://rubyforge.org/' + + tld_cookie = HTTP::Cookie.new(cookie_values(:domain => '.org')) + @jar.add(url, tld_cookie) + single_dot_cookie = HTTP::Cookie.new(cookie_values(:domain => '.')) + @jar.add(url, single_dot_cookie) + + assert_equal(0, @jar.cookies(url).length) + end + + def test_fall_back_rules_for_local_domains + url = URI 'http://www.example.local' + + tld_cookie = HTTP::Cookie.new(cookie_values(:domain => '.local')) + @jar.add(url, tld_cookie) + + assert_equal(0, @jar.cookies(url).length) + + sld_cookie = HTTP::Cookie.new(cookie_values(:domain => '.example.local')) + @jar.add(url, sld_cookie) + + assert_equal(1, @jar.cookies(url).length) + end + + def test_add_makes_exception_for_localhost + url = URI 'http://localhost' + + tld_cookie = HTTP::Cookie.new(cookie_values(:domain => 'localhost')) + @jar.add(url, tld_cookie) + + assert_equal(1, @jar.cookies(url).length) + end + + def test_add_cookie_for_the_parent_domain + url = URI 'http://x.foo.com' + + cookie = HTTP::Cookie.new(cookie_values(:domain => '.foo.com')) + @jar.add(url, cookie) + + assert_equal(1, @jar.cookies(url).length) + end + + def test_add_does_not_reject_cookies_from_a_nested_subdomain + url = URI 'http://y.x.foo.com' + + cookie = HTTP::Cookie.new(cookie_values(:domain => '.foo.com')) + @jar.add(url, cookie) + + assert_equal(1, @jar.cookies(url).length) + end + + def test_cookie_without_leading_dot_does_not_cause_substring_match + url = URI 'http://arubyforge.org/' + + cookie = HTTP::Cookie.new(cookie_values(:domain => 'rubyforge.org')) + @jar.add(url, cookie) + + assert_equal(0, @jar.cookies(url).length) + end + + def test_cookie_without_leading_dot_matches_subdomains + url = URI 'http://admin.rubyforge.org/' + + cookie = HTTP::Cookie.new(cookie_values(:domain => 'rubyforge.org')) + @jar.add(url, cookie) + + assert_equal(1, @jar.cookies(url).length) + end + + def test_cookies_with_leading_dot_match_subdomains + url = URI 'http://admin.rubyforge.org/' + + @jar.add(url, HTTP::Cookie.new(cookie_values(:domain => '.rubyforge.org'))) + + assert_equal(1, @jar.cookies(url).length) + end + + def test_cookies_with_leading_dot_match_parent_domains + url = URI 'http://rubyforge.org/' + + @jar.add(url, HTTP::Cookie.new(cookie_values(:domain => '.rubyforge.org'))) + + assert_equal(1, @jar.cookies(url).length) + end + + def test_cookies_with_leading_dot_match_parent_domains_exactly + url = URI 'http://arubyforge.org/' + + @jar.add(url, HTTP::Cookie.new(cookie_values(:domain => '.rubyforge.org'))) + + assert_equal(0, @jar.cookies(url).length) + end + + def test_cookie_for_ipv4_address_matches_the_exact_ipaddress + url = URI 'http://192.168.0.1/' + + cookie = HTTP::Cookie.new(cookie_values(:domain => '192.168.0.1')) + @jar.add(url, cookie) + + assert_equal(1, @jar.cookies(url).length) + end + + def test_cookie_for_ipv4_address_does_not_cause_subdomain_match + url = URI 'http://192.168.0.1/' + + cookie = HTTP::Cookie.new(cookie_values(:domain => '.0.1')) + @jar.add(url, cookie) + + assert_equal(0, @jar.cookies(url).length) + end + + def test_cookie_for_ipv6_address_matches_the_exact_ipaddress + url = URI 'http://[fe80::0123:4567:89ab:cdef]/' + + cookie = HTTP::Cookie.new(cookie_values(:domain => '[fe80::0123:4567:89ab:cdef]')) + @jar.add(url, cookie) + + assert_equal(1, @jar.cookies(url).length) + end + + def test_cookies_dot + url = URI 'http://www.host.example/' + + @jar.add(url, + HTTP::Cookie.new(cookie_values(:domain => 'www.host.example'))) + + url = URI 'http://wwwxhost.example/' + assert_equal(0, @jar.cookies(url).length) + end + + def test_clear_bang + url = URI 'http://rubyforge.org/' + + # Add one cookie with an expiration date in the future + cookie = HTTP::Cookie.new(cookie_values) + @jar.add(url, cookie) + @jar.add(url, HTTP::Cookie.new(cookie_values(:name => 'Baz'))) + assert_equal(2, @jar.cookies(url).length) + + @jar.clear! + + assert_equal(0, @jar.cookies(url).length) + end + + def test_save_cookies_yaml + url = URI 'http://rubyforge.org/' + + # Add one cookie with an expiration date in the future + cookie = HTTP::Cookie.new(cookie_values) + s_cookie = HTTP::Cookie.new(cookie_values(:name => 'Bar', + :expires => nil, + :session => true)) + + @jar.add(url, cookie) + @jar.add(url, s_cookie) + @jar.add(url, HTTP::Cookie.new(cookie_values(:name => 'Baz'))) + + assert_equal(3, @jar.cookies(url).length) + + in_tmpdir do + value = @jar.save_as("cookies.yml") + assert_same @jar, value + + jar = HTTP::CookieJar.new + jar.load("cookies.yml") + assert_equal(2, jar.cookies(url).length) + end + + assert_equal(3, @jar.cookies(url).length) + end + + def test_save_session_cookies_yaml + url = URI 'http://rubyforge.org/' + + # Add one cookie with an expiration date in the future + cookie = HTTP::Cookie.new(cookie_values) + s_cookie = HTTP::Cookie.new(cookie_values(:name => 'Bar', + :expires => nil, + :session => true)) + + @jar.add(url, cookie) + @jar.add(url, s_cookie) + @jar.add(url, HTTP::Cookie.new(cookie_values(:name => 'Baz'))) + + assert_equal(3, @jar.cookies(url).length) + + in_tmpdir do + @jar.save_as("cookies.yml", :format => :yaml, :session => true) + + jar = HTTP::CookieJar.new + jar.load("cookies.yml") + assert_equal(3, jar.cookies(url).length) + end + + assert_equal(3, @jar.cookies(url).length) + end + + + def test_save_cookies_cookiestxt + url = URI 'http://rubyforge.org/' + + # Add one cookie with an expiration date in the future + cookie = HTTP::Cookie.new(cookie_values) + s_cookie = HTTP::Cookie.new(cookie_values(:name => 'Bar', + :expires => nil, + :session => true)) + + @jar.add(url, cookie) + @jar.add(url, s_cookie) + @jar.add(url, HTTP::Cookie.new(cookie_values(:name => 'Baz'))) + + assert_equal(3, @jar.cookies(url).length) + + in_tmpdir do + @jar.save_as("cookies.txt", :cookiestxt) + + jar = HTTP::CookieJar.new + jar.load("cookies.txt", :cookiestxt) # HACK test the format + assert_equal(2, jar.cookies(url).length) + end + + assert_equal(3, @jar.cookies(url).length) + end + + def test_expire_cookies + url = URI 'http://rubyforge.org/' + + # Add one cookie with an expiration date in the future + cookie = HTTP::Cookie.new(cookie_values) + @jar.add(url, cookie) + assert_equal(1, @jar.cookies(url).length) + + # Add a second cookie + @jar.add(url, HTTP::Cookie.new(cookie_values(:name => 'Baz'))) + assert_equal(2, @jar.cookies(url).length) + + # Make sure we can get the cookie from different paths + assert_equal(2, @jar.cookies(URI('http://rubyforge.org/login')).length) + + # Expire the first cookie + @jar.add(url, HTTP::Cookie.new( + cookie_values(:expires => Time.now - (10 * 86400)))) + assert_equal(1, @jar.cookies(url).length) + + # Expire the second cookie + @jar.add(url, HTTP::Cookie.new( + cookie_values( :name => 'Baz', :expires => Time.now - (10 * 86400)))) + assert_equal(0, @jar.cookies(url).length) + end + + def test_session_cookies + values = cookie_values(:expires => nil) + url = URI 'http://rubyforge.org/' + + # Add one cookie with an expiration date in the future + cookie = HTTP::Cookie.new(values) + @jar.add(url, cookie) + assert_equal(1, @jar.cookies(url).length) + + # Add a second cookie + @jar.add(url, HTTP::Cookie.new(values.merge(:name => 'Baz'))) + assert_equal(2, @jar.cookies(url).length) + + # Make sure we can get the cookie from different paths + assert_equal(2, @jar.cookies(URI('http://rubyforge.org/login')).length) + + # Expire the first cookie + @jar.add(url, HTTP::Cookie.new(values.merge(:expires => Time.now - (10 * 86400)))) + assert_equal(1, @jar.cookies(url).length) + + # Expire the second cookie + @jar.add(url, HTTP::Cookie.new( + values.merge(:name => 'Baz', :expires => Time.now - (10 * 86400)))) + assert_equal(0, @jar.cookies(url).length) + + # When given a URI with a blank path, CookieJar#cookies should return + # cookies with the path '/': + url = URI 'http://rubyforge.org' + assert_equal '', url.path + assert_equal(0, @jar.cookies(url).length) + # Now add a cookie with the path set to '/': + @jar.add(url, HTTP::Cookie.new(values.merge( :name => 'has_root_path', + :path => '/'))) + assert_equal(1, @jar.cookies(url).length) + end + + def test_paths + values = cookie_values(:path => "/login", :expires => nil) + url = URI 'http://rubyforge.org/login' + + # Add one cookie with an expiration date in the future + cookie = HTTP::Cookie.new(values) + @jar.add(url, cookie) + assert_equal(1, @jar.cookies(url).length) + + # Add a second cookie + @jar.add(url, HTTP::Cookie.new(values.merge( :name => 'Baz' ))) + assert_equal(2, @jar.cookies(url).length) + + # Make sure we don't get the cookie in a different path + assert_equal(0, @jar.cookies(URI('http://rubyforge.org/hello')).length) + assert_equal(0, @jar.cookies(URI('http://rubyforge.org/')).length) + + # Expire the first cookie + @jar.add(url, HTTP::Cookie.new(values.merge( :expires => Time.now - (10 * 86400)))) + assert_equal(1, @jar.cookies(url).length) + + # Expire the second cookie + @jar.add(url, HTTP::Cookie.new(values.merge( :name => 'Baz', + :expires => Time.now - (10 * 86400)))) + assert_equal(0, @jar.cookies(url).length) + end + + def test_save_and_read_cookiestxt + url = URI 'http://rubyforge.org/' + + # Add one cookie with an expiration date in the future + cookie = HTTP::Cookie.new(cookie_values) + @jar.add(url, cookie) + @jar.add(url, HTTP::Cookie.new(cookie_values(:name => 'Baz'))) + assert_equal(2, @jar.cookies(url).length) + + in_tmpdir do + @jar.save_as("cookies.txt", :cookiestxt) + @jar.clear! + + @jar.load("cookies.txt", :cookiestxt) + end + + assert_equal(2, @jar.cookies(url).length) + end + + def test_save_and_read_cookiestxt_with_session_cookies + url = URI 'http://rubyforge.org/' + + @jar.add(url, HTTP::Cookie.new(cookie_values(:expires => nil))) + + in_tmpdir do + @jar.save_as("cookies.txt", :cookiestxt) + @jar.clear! + + @jar.load("cookies.txt", :cookiestxt) + end + + assert_equal(1, @jar.cookies(url).length) + assert_nil @jar.cookies(url).first.expires + end + + def test_save_and_read_expired_cookies + url = URI 'http://rubyforge.org/' + + @jar.jar['rubyforge.org'] = {} + + + @jar.add url, HTTP::Cookie.new(cookie_values) + + # HACK no asertion + end + + def test_ssl_cookies + # thanks to michal "ocher" ochman for reporting the bug responsible for this test. + values = cookie_values(:expires => nil) + values_ssl = values.merge(:name => 'Baz', :domain => "#{values[:domain]}:443") + url = URI 'https://rubyforge.org/login' + + cookie = HTTP::Cookie.new(values) + @jar.add(url, cookie) + assert_equal(1, @jar.cookies(url).length, "did not handle SSL cookie") + + cookie = HTTP::Cookie.new(values_ssl) + @jar.add(url, cookie) + assert_equal(2, @jar.cookies(url).length, "did not handle SSL cookie with :443") + end + + def test_secure_cookie + nurl = URI 'http://rubyforge.org/login' + surl = URI 'https://rubyforge.org/login' + + ncookie = HTTP::Cookie.new(cookie_values(:name => 'Foo1')) + scookie = HTTP::Cookie.new(cookie_values(:name => 'Foo2', :secure => true)) + + @jar.add(nurl, ncookie) + @jar.add(nurl, scookie) + @jar.add(surl, ncookie) + @jar.add(surl, scookie) + + assert_equal('Foo1', @jar.cookies(nurl).map { |c| c.name }.sort.join(' ') ) + assert_equal('Foo1 Foo2', @jar.cookies(surl).map { |c| c.name }.sort.join(' ') ) + end +end