majoun 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +37 -0
- data/.rspec +4 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +17 -0
- data/CONTRIBUTING.md +11 -0
- data/Changelog.md +1 -0
- data/Gemfile +12 -0
- data/Gemfile.devtools +71 -0
- data/Guardfile +18 -0
- data/LICENSE +20 -0
- data/README.md +24 -0
- data/Rakefile +6 -0
- data/TODO.md +1 -0
- data/config/devtools.yml +2 -0
- data/config/flay.yml +3 -0
- data/config/flog.yml +2 -0
- data/config/mutant.yml +3 -0
- data/config/reek.yml +110 -0
- data/config/rubocop.yml +78 -0
- data/config/yardstick.yml +2 -0
- data/lib/cookie.rb +89 -0
- data/lib/cookie/header.rb +70 -0
- data/lib/cookie/header/attribute.rb +172 -0
- data/lib/cookie/registry.rb +53 -0
- data/lib/cookie/version.rb +8 -0
- data/majoun.gemspec +25 -0
- data/spec/integration/public_api_spec.rb +186 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/unit/cookie/decrypt_spec.rb +19 -0
- data/spec/unit/cookie/encrypt_spec.rb +19 -0
- data/spec/unit/cookie/header/attribute/set/each_spec.rb +22 -0
- data/spec/unit/cookie/header/attribute/set/empty/merge_spec.rb +13 -0
- data/spec/unit/cookie/header/attribute/set/empty/to_s_spec.rb +11 -0
- data/spec/unit/cookie/version_spec.rb +7 -0
- metadata +149 -0
data/lib/cookie.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
require 'concord'
|
7
|
+
require 'adamantium'
|
8
|
+
require 'abstract_type'
|
9
|
+
|
10
|
+
# Models an HTTP Cookie
|
11
|
+
class Cookie
|
12
|
+
|
13
|
+
# An empty frozen array
|
14
|
+
EMPTY_ARRAY = [].freeze
|
15
|
+
|
16
|
+
# An empty frozen hash
|
17
|
+
EMPTY_HASH = {}.freeze
|
18
|
+
|
19
|
+
# An empty frozen string
|
20
|
+
EMPTY_STRING = ''.freeze
|
21
|
+
|
22
|
+
# Separates the cookie name from its value
|
23
|
+
NAME_VALUE_SEPARATOR = '='.freeze
|
24
|
+
|
25
|
+
# Separates cookies
|
26
|
+
COOKIE_SEPARATOR = '; '.freeze
|
27
|
+
|
28
|
+
# Separates ruby class names in a FQN
|
29
|
+
DOUBLE_COLON = '::'.freeze
|
30
|
+
|
31
|
+
# Helper for cookie deletion
|
32
|
+
class Empty < self
|
33
|
+
def initialize(name)
|
34
|
+
super(name, nil)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Namespace for cookie encoders
|
39
|
+
module Encoder
|
40
|
+
Base64 = ->(string) { ::Base64.urlsafe_encode64(string) }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Namespace for cookie decoders
|
44
|
+
module Decoder
|
45
|
+
Base64 = ->(string) { ::Base64.urlsafe_decode64(string) }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Cookie error base class
|
49
|
+
Error = Class.new(StandardError)
|
50
|
+
|
51
|
+
include Concord::Public.new(:name, :value)
|
52
|
+
include Adamantium::Flat
|
53
|
+
|
54
|
+
def self.coerce(string)
|
55
|
+
new(*string.split(NAME_VALUE_SEPARATOR, 2))
|
56
|
+
end
|
57
|
+
|
58
|
+
def encode(encoder = Encoder::Base64)
|
59
|
+
new(encoder.call(value))
|
60
|
+
end
|
61
|
+
|
62
|
+
def decode(decoder = Decoder::Base64)
|
63
|
+
new(decoder.call(value))
|
64
|
+
end
|
65
|
+
|
66
|
+
def encrypt(box)
|
67
|
+
new(box.encrypt(value))
|
68
|
+
end
|
69
|
+
|
70
|
+
def decrypt(box)
|
71
|
+
new(box.decrypt(value))
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
"#{name}=#{value}"
|
76
|
+
end
|
77
|
+
memoize :to_s
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def new(new_value)
|
82
|
+
self.class.new(name, new_value)
|
83
|
+
end
|
84
|
+
|
85
|
+
end # class Cookie
|
86
|
+
|
87
|
+
require 'cookie/header'
|
88
|
+
require 'cookie/header/attribute'
|
89
|
+
require 'cookie/registry'
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Cookie
|
4
|
+
|
5
|
+
# Models a transient, new cookie on the server that can be serialized
|
6
|
+
# into an HTTP 'Set-Cookie' header
|
7
|
+
class Header
|
8
|
+
|
9
|
+
include Equalizer.new(:cookie, :attributes)
|
10
|
+
include Adamantium::Flat
|
11
|
+
|
12
|
+
attr_reader :cookie
|
13
|
+
protected :cookie
|
14
|
+
|
15
|
+
attr_reader :attributes
|
16
|
+
protected :attributes
|
17
|
+
|
18
|
+
def self.build(name, value, attributes)
|
19
|
+
new(Cookie.new(name, value), attributes)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(cookie, attributes = Attribute::Set::EMPTY)
|
23
|
+
@cookie, @attributes = cookie, attributes
|
24
|
+
end
|
25
|
+
|
26
|
+
def with_domain(domain)
|
27
|
+
with_attribute(Attribute::Domain.new(domain))
|
28
|
+
end
|
29
|
+
|
30
|
+
def with_path(path)
|
31
|
+
with_attribute(Attribute::Path.new(path))
|
32
|
+
end
|
33
|
+
|
34
|
+
def with_max_age(seconds)
|
35
|
+
with_attribute(Attribute::MaxAge.new(seconds))
|
36
|
+
end
|
37
|
+
|
38
|
+
def with_expires(time)
|
39
|
+
with_attribute(Attribute::Expires.new(time))
|
40
|
+
end
|
41
|
+
|
42
|
+
def secure
|
43
|
+
with_attribute(Attribute::Secure.instance)
|
44
|
+
end
|
45
|
+
|
46
|
+
def http_only
|
47
|
+
with_attribute(Attribute::HttpOnly.instance)
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete
|
51
|
+
new(Empty.new(cookie.name), attributes.merge(Attribute::Expired))
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
"#{cookie}#{attributes}"
|
56
|
+
end
|
57
|
+
memoize :to_s
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def with_attribute(attribute)
|
62
|
+
new(cookie, attributes.merge(attribute))
|
63
|
+
end
|
64
|
+
|
65
|
+
def new(cookie, attributes)
|
66
|
+
self.class.new(cookie, attributes)
|
67
|
+
end
|
68
|
+
|
69
|
+
end # class Header
|
70
|
+
end # class Cookie
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Cookie
|
4
|
+
class Header
|
5
|
+
|
6
|
+
# Baseclass for cookie attributes
|
7
|
+
class Attribute
|
8
|
+
|
9
|
+
include Concord::Public.new(:name)
|
10
|
+
include Adamantium
|
11
|
+
|
12
|
+
REGISTRY = {}
|
13
|
+
|
14
|
+
def self.coerce(name, value)
|
15
|
+
REGISTRY.fetch(name.to_sym).build(value)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.register_as(name)
|
19
|
+
REGISTRY[name.to_sym] = self
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.attribute_name
|
23
|
+
name.split(DOUBLE_COLON).last
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
name
|
28
|
+
end
|
29
|
+
|
30
|
+
# Models a set of attributes used within a {Serializable} cookie
|
31
|
+
class Set
|
32
|
+
|
33
|
+
include Concord.new(:attributes)
|
34
|
+
include Enumerable
|
35
|
+
include Adamantium
|
36
|
+
|
37
|
+
def self.coerce(attributes)
|
38
|
+
new(attributes.each_with_object({}) { |(name, value), hash|
|
39
|
+
attribute = Attribute.coerce(name, value)
|
40
|
+
hash[attribute.name] = attribute if attribute
|
41
|
+
})
|
42
|
+
end
|
43
|
+
|
44
|
+
def each(&block)
|
45
|
+
return to_enum unless block
|
46
|
+
attributes.each_value(&block)
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def merge(attribute)
|
51
|
+
Set.new(attributes.merge(attribute.name => attribute))
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
"#{COOKIE_SEPARATOR}#{map(&:to_s).join(COOKIE_SEPARATOR)}"
|
56
|
+
end
|
57
|
+
memoize :to_s
|
58
|
+
|
59
|
+
# An empty {Set} to be serialized to {EMPTY_STRING}
|
60
|
+
class Empty < self
|
61
|
+
|
62
|
+
def initialize
|
63
|
+
super(EMPTY_HASH)
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
EMPTY_STRING
|
68
|
+
end
|
69
|
+
|
70
|
+
end # class Empty
|
71
|
+
|
72
|
+
EMPTY = Empty.new
|
73
|
+
|
74
|
+
end # class Set
|
75
|
+
|
76
|
+
# Abstract baseclass for attributes that have no value
|
77
|
+
#
|
78
|
+
# @abstract
|
79
|
+
class Unary < self
|
80
|
+
|
81
|
+
include AbstractType
|
82
|
+
|
83
|
+
CACHE = {}
|
84
|
+
|
85
|
+
def self.build(value)
|
86
|
+
value ? instance : nil
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.instance
|
90
|
+
CACHE.fetch(attribute_name) {
|
91
|
+
CACHE[attribute_name] = new(attribute_name)
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
# Abstract baseclass for attributes that consist of a name-value
|
98
|
+
# pair
|
99
|
+
#
|
100
|
+
# @abstract
|
101
|
+
class Binary < self
|
102
|
+
|
103
|
+
include AbstractType
|
104
|
+
include Equalizer.new(:name, :value)
|
105
|
+
|
106
|
+
attr_reader :value
|
107
|
+
protected :value
|
108
|
+
|
109
|
+
def self.build(value)
|
110
|
+
new(value)
|
111
|
+
end
|
112
|
+
|
113
|
+
def initialize(value)
|
114
|
+
super(self.class.attribute_name)
|
115
|
+
@value = value
|
116
|
+
end
|
117
|
+
|
118
|
+
def to_s
|
119
|
+
"#{name}=#{serialized_value}"
|
120
|
+
end
|
121
|
+
memoize :to_s
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def serialized_value
|
126
|
+
value
|
127
|
+
end
|
128
|
+
|
129
|
+
end # class Binary
|
130
|
+
|
131
|
+
# The Domain attribute
|
132
|
+
class Domain < Binary
|
133
|
+
register_as :domain
|
134
|
+
end
|
135
|
+
|
136
|
+
# The Path attribute
|
137
|
+
class Path < Binary
|
138
|
+
register_as :path
|
139
|
+
end
|
140
|
+
|
141
|
+
# The Max-Age attribute
|
142
|
+
class MaxAge < Binary
|
143
|
+
register_as :max_age
|
144
|
+
end
|
145
|
+
|
146
|
+
# The Expires attribute
|
147
|
+
class Expires < Binary
|
148
|
+
register_as :expires
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def serialized_value
|
153
|
+
super.dup.gmtime.rfc2822
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# The Secure attribute
|
158
|
+
class Secure < Unary
|
159
|
+
register_as :secure
|
160
|
+
end
|
161
|
+
|
162
|
+
# The HttpOnly attribute
|
163
|
+
class HttpOnly < Unary
|
164
|
+
register_as :http_only
|
165
|
+
end
|
166
|
+
|
167
|
+
# Already expired {Expires} attribute useful for cookie deletion
|
168
|
+
Expired = Expires.new(Time.at(0))
|
169
|
+
|
170
|
+
end # class Attribute
|
171
|
+
end # class Header
|
172
|
+
end # class Cookie
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Cookie
|
4
|
+
|
5
|
+
# Models a registry of {Cookie} instances
|
6
|
+
class Registry
|
7
|
+
|
8
|
+
include Enumerable
|
9
|
+
include Equalizer.new(:entries)
|
10
|
+
include Adamantium::Flat
|
11
|
+
|
12
|
+
# Message for {UnknownCookieError}
|
13
|
+
UNKNOWN_COOKIE_MSG = 'No cookie named %s is registered'.freeze
|
14
|
+
|
15
|
+
# Raised when trying to {#fetch} an unknown {Cookie}
|
16
|
+
UnknownCookieError = Class.new(Cookie::Error)
|
17
|
+
|
18
|
+
def self.coerce(header)
|
19
|
+
new(cookie_hash(header))
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.cookie_hash(header)
|
23
|
+
header.split(COOKIE_SEPARATOR).each_with_object({}) { |string, hash|
|
24
|
+
cookie = Cookie.coerce(string)
|
25
|
+
hash[cookie.name] = cookie
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
private_class_method :cookie_hash
|
30
|
+
|
31
|
+
attr_reader :entries
|
32
|
+
protected :entries
|
33
|
+
|
34
|
+
def initialize(entries = EMPTY_HASH)
|
35
|
+
@entries = entries
|
36
|
+
end
|
37
|
+
|
38
|
+
def each(&block)
|
39
|
+
return to_enum unless block
|
40
|
+
entries.each(&block)
|
41
|
+
self
|
42
|
+
end
|
43
|
+
|
44
|
+
def fetch(name)
|
45
|
+
get(name) or raise UnknownCookieError, UNKNOWN_COOKIE_MSG % name.inspect
|
46
|
+
end
|
47
|
+
|
48
|
+
def get(name)
|
49
|
+
@entries[name]
|
50
|
+
end
|
51
|
+
|
52
|
+
end # class Registry
|
53
|
+
end # class Cookie
|
data/majoun.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require File.expand_path('../lib/cookie/version', __FILE__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.name = "majoun"
|
7
|
+
gem.version = Cookie::VERSION.dup
|
8
|
+
gem.authors = [ "Martin Gamsjaeger (snusnu)" ]
|
9
|
+
gem.email = [ "gamsnjaga@gmail.com" ]
|
10
|
+
gem.description = "An HTTP cookie implemented in ruby"
|
11
|
+
gem.summary = "Support for sending and receiving HTTP cookies on ruby servers"
|
12
|
+
gem.homepage = "https://github.com/snusnu/cookie"
|
13
|
+
|
14
|
+
gem.require_paths = [ "lib" ]
|
15
|
+
gem.files = `git ls-files`.split("\n")
|
16
|
+
gem.test_files = `git ls-files -- {spec}/*`.split("\n")
|
17
|
+
gem.extra_rdoc_files = %w[LICENSE README.md TODO.md]
|
18
|
+
gem.license = 'MIT'
|
19
|
+
|
20
|
+
gem.add_dependency 'adamantium', '~> 0.2.0'
|
21
|
+
gem.add_dependency 'abstract_type', '~> 0.0.7'
|
22
|
+
gem.add_dependency 'concord', '~> 0.1.4'
|
23
|
+
|
24
|
+
gem.add_development_dependency 'bundler', '~> 1.6.1'
|
25
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
module Spec
|
6
|
+
class CryptoBox
|
7
|
+
def encrypt(string)
|
8
|
+
string
|
9
|
+
end
|
10
|
+
|
11
|
+
def decrypt(string)
|
12
|
+
string
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Encoder
|
17
|
+
Noop = ->(string) { string }
|
18
|
+
end
|
19
|
+
|
20
|
+
module Decoder
|
21
|
+
Noop = ->(string) { string }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
shared_context 'integration specs' do
|
26
|
+
let(:cookie) { Cookie.new(name, value) }
|
27
|
+
let(:name) { 'SID' }
|
28
|
+
let(:value) { '{"id": 11}' } # triggers padding char '=' when base64 encoded
|
29
|
+
end
|
30
|
+
|
31
|
+
describe Cookie do
|
32
|
+
|
33
|
+
include_context 'integration specs'
|
34
|
+
|
35
|
+
let(:encoder) { Spec::Encoder::Noop }
|
36
|
+
let(:decoder) { Spec::Decoder::Noop }
|
37
|
+
let(:box) { Spec::CryptoBox.new }
|
38
|
+
|
39
|
+
it 'supports coercion from string to a Cookie instance' do
|
40
|
+
c = cookie
|
41
|
+
expect(Cookie.coerce(c.to_s)).to eql(cookie)
|
42
|
+
|
43
|
+
c = cookie.encode
|
44
|
+
expect(Cookie.coerce(c.to_s)).to eql(cookie.encode)
|
45
|
+
|
46
|
+
c = c.decode
|
47
|
+
expect(Cookie.coerce(c.to_s)).to eql(cookie)
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'supports #name and #value' do
|
51
|
+
expect(cookie.name).to be(name)
|
52
|
+
expect(cookie.value).to be(value)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'supports #to_s' do
|
56
|
+
expect(cookie.to_s).to eql("#{cookie.name}=#{cookie.value}")
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'encoding and decoding' do
|
60
|
+
it 'defaults to base64' do
|
61
|
+
encoded = cookie.encode
|
62
|
+
expect(encoded).to eql(Cookie.new(name, Base64.urlsafe_encode64(cookie.value)))
|
63
|
+
|
64
|
+
decoded = encoded.decode
|
65
|
+
expect(decoded).to eql(Cookie.new(name, Base64.urlsafe_decode64(encoded.value)))
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'supports custom encoders and decoders' do
|
69
|
+
encoded = cookie.encode(encoder)
|
70
|
+
expect(encoded).to eql(Cookie.new(name, encoder.call(cookie.value)))
|
71
|
+
|
72
|
+
decoded = encoded.decode(decoder)
|
73
|
+
expect(decoded).to eql(Cookie.new(name, decoder.call(encoded.value)))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'encryption and decryption' do
|
78
|
+
it 'supports crypto boxes which provide #encrypt(msg) and #decrypt(msg)' do
|
79
|
+
encrypted = cookie.encrypt(box)
|
80
|
+
expect(encrypted).to eql(Cookie.new(name, box.encrypt(cookie.value)))
|
81
|
+
|
82
|
+
decrypted = encrypted.decrypt(box)
|
83
|
+
expect(decrypted).to eql(Cookie.new(name, box.decrypt(encrypted.value)))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe Cookie::Header do
|
89
|
+
|
90
|
+
include_context 'integration specs'
|
91
|
+
|
92
|
+
let(:definition) { Cookie::Header.new(cookie) }
|
93
|
+
|
94
|
+
it 'supports all cookie attributes' do
|
95
|
+
d = definition
|
96
|
+
expect(d.to_s).to eql('SID={"id": 11}')
|
97
|
+
|
98
|
+
d = definition.with_domain('.foo.bar')
|
99
|
+
expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar')
|
100
|
+
|
101
|
+
d = d.with_path('/foo')
|
102
|
+
expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo')
|
103
|
+
|
104
|
+
d = d.with_expires(Time.at(42))
|
105
|
+
expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:42 -0000')
|
106
|
+
|
107
|
+
d = d.with_max_age(42)
|
108
|
+
expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:42 -0000; MaxAge=42')
|
109
|
+
|
110
|
+
d = d.secure
|
111
|
+
expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:42 -0000; MaxAge=42; Secure')
|
112
|
+
|
113
|
+
d = d.http_only
|
114
|
+
expect(d.to_s).to eql('SID={"id": 11}; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:42 -0000; MaxAge=42; Secure; HttpOnly')
|
115
|
+
|
116
|
+
d = definition.secure
|
117
|
+
expect(d.to_s).to eql('SID={"id": 11}; Secure')
|
118
|
+
|
119
|
+
d = d.http_only
|
120
|
+
expect(d.to_s).to eql('SID={"id": 11}; Secure; HttpOnly')
|
121
|
+
|
122
|
+
d = definition.http_only
|
123
|
+
expect(d.to_s).to eql('SID={"id": 11}; HttpOnly')
|
124
|
+
|
125
|
+
d = d.secure
|
126
|
+
expect(d.to_s).to eql('SID={"id": 11}; HttpOnly; Secure')
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'overwrites previously defined attributes' do
|
130
|
+
d = definition.with_max_age(0).with_max_age(42)
|
131
|
+
expect(d.to_s).to eql('SID={"id": 11}; MaxAge=42')
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'supports deleting cookies on the client' do
|
135
|
+
d = definition.delete
|
136
|
+
expect(d.to_s).to eql('SID=; Expires=Thu, 01 Jan 1970 00:00:00 -0000')
|
137
|
+
|
138
|
+
d = definition.with_domain('.foo.bar').with_path('/foo')
|
139
|
+
d = d.delete
|
140
|
+
expect(d.to_s).to eql('SID=; Domain=.foo.bar; Path=/foo; Expires=Thu, 01 Jan 1970 00:00:00 -0000')
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe Cookie::Registry do
|
145
|
+
|
146
|
+
include_context 'integration specs'
|
147
|
+
|
148
|
+
let(:registry) { Cookie::Registry.coerce(header) }
|
149
|
+
let(:header) { cookie.to_s }
|
150
|
+
|
151
|
+
it 'contains no entries after #initialize' do
|
152
|
+
expect(Cookie::Registry.new.count).to be(0)
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'supports coercion from a Set-Cookie header' do
|
156
|
+
expect(registry.get(name)).to eql(cookie)
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'supports #get(name)' do
|
160
|
+
expect(registry.get(name)).to eql(cookie)
|
161
|
+
expect(registry.get(:foo)).to be(nil)
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'supports #fetch(name)' do
|
165
|
+
expect(registry.fetch(name)).to eql(cookie)
|
166
|
+
expect { registry.fetch(:foo) }.to raise_error(Cookie::Registry::UnknownCookieError)
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'is an Enumerable' do
|
170
|
+
expect(registry).to be_kind_of(Enumerable)
|
171
|
+
end
|
172
|
+
|
173
|
+
context '#each' do
|
174
|
+
it 'returns self when a block is given' do
|
175
|
+
expect(registry.each { |_| }).to be(registry)
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'returns an enumerator when no block is given' do
|
179
|
+
expect(registry.each).to be_instance_of(Enumerator)
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'yields all cookies' do
|
183
|
+
expect { |block| registry.each(&block) }.to yield_successive_args([name, cookie])
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|