diff --git a/lib/protocol/http/header/cookie.rb b/lib/protocol/http/header/cookie.rb index adc441a..e4718ac 100644 --- a/lib/protocol/http/header/cookie.rb +++ b/lib/protocol/http/header/cookie.rb @@ -49,8 +49,11 @@ def initialize(value = nil) # # @returns [Hash(String, HTTP::Cookie)] a hash where keys are cookie names and values are {HTTP::Cookie} objects. def to_h - cookies = self.collect do |string| - HTTP::Cookie.parse(string) + cookies = self.flat_map do |string| + # Each header field can contain multiple cookie pairs separated by semicolons: + string.split(/\s*;\s*/).map do |pair| + HTTP::Cookie.parse(pair) + end end cookies.map{|cookie| [cookie.name, cookie]}.to_h diff --git a/test/protocol/http/header/cookie.rb b/test/protocol/http/header/cookie.rb index d27a3ed..c4baf93 100644 --- a/test/protocol/http/header/cookie.rb +++ b/test/protocol/http/header/cookie.rb @@ -5,6 +5,7 @@ # Copyright, 2022, by Herrick Fang. require "protocol/http/header/cookie" +require "protocol/http/headers" describe Protocol::HTTP::Header::Cookie do let(:header) {subject.parse(description)} @@ -16,40 +17,46 @@ expect(header).to be == ["session=abc123"] end - with "session=123; secure" do + with "session=123; user_id=42" do it "can parse cookies" do - expect(cookies).to have_keys("session") + expect(cookies).to have_keys("session", "user_id") session = cookies["session"] expect(session).to have_attributes( name: be == "session", value: be == "123", + directives: be == {}, + ) + + user_id = cookies["user_id"] + expect(user_id).to have_attributes( + name: be == "user_id", + value: be == "42", + directives: be == {}, ) - expect(session.directives).to have_keys("secure") end end - with "session=123; path=/; secure" do - it "can parse cookies" do - session = cookies["session"] - expect(session).to have_attributes( - name: be == "session", - value: be == "123", - directives: be == {"path" => "/", "secure" => true}, - ) + with "empty=; token=abc==" do + it "preserves empty values and equals signs" do + expect(cookies["empty"].value).to be == "" + expect(cookies["token"].value).to be == "abc==" end end - with "session=abc123; secure" do - it "can parse cookies" do - expect(cookies).to have_keys("session") - - session = cookies["session"] - expect(session).to have_attributes( - name: be == "session", - value: be == "abc123", - ) - expect(session.directives).to have_keys("secure") + with "first=1 ; second=2;\tthird=3" do + it "ignores whitespace around separators" do + expect(cookies.transform_values(&:value)).to be == { + "first" => "1", + "second" => "2", + "third" => "3", + } + end + end + + with "session=first; session=second" do + it "uses the last value for duplicate names" do + expect(cookies["session"].value).to be == "second" end end @@ -65,5 +72,25 @@ it "joins cookies with semicolons and spaces per RFC 6265" do expect(header.to_s).to be == "session=abc123; user_id=42; token=xyz789" end + + it "parses cookies from multiple header fields" do + expect(cookies).to have_keys("session", "user_id", "token") + end + end + + it "parses cookies through protocol headers" do + headers = Protocol::HTTP::Headers[[ + ["cookie", "session=abc123; user_id=42"], + ["cookie", "token=xyz789"], + ]] + + header = headers["cookie"] + + expect(header).to be_a(subject) + expect(header.to_h.transform_values(&:value)).to be == { + "session" => "abc123", + "user_id" => "42", + "token" => "xyz789", + } end end diff --git a/test/protocol/http/header/set_cookie.rb b/test/protocol/http/header/set_cookie.rb index 103b596..8148e14 100644 --- a/test/protocol/http/header/set_cookie.rb +++ b/test/protocol/http/header/set_cookie.rb @@ -4,6 +4,7 @@ # Copyright, 2026, by Samuel Williams. require "protocol/http/header/set_cookie" +require "protocol/http/headers" describe Protocol::HTTP::Header::SetCookie do let(:header) do @@ -19,6 +20,47 @@ end it "can extract parsed cookies" do - expect(header.to_h).to have_keys("session", "theme") + cookies = header.to_h + + expect(cookies).to have_keys("session", "theme") + expect(cookies["session"]).to have_attributes( + value: be == "abc123", + directives: be == {"Path" => "/"}, + ) + expect(cookies["theme"]).to have_attributes( + value: be == "dark", + directives: be == {"HttpOnly" => true}, + ) + end + + it "preserves cookie attributes" do + header = subject.parse("session=abc123; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600; Expires=Wed, 21 Oct 2015 07:28:00 GMT") + cookie = header.to_h["session"] + + expect(cookie.directives).to be == { + "Path" => "/", + "HttpOnly" => true, + "SameSite" => "Lax", + "Max-Age" => "3600", + "Expires" => "Wed, 21 Oct 2015 07:28:00 GMT", + } + end + + it "preserves separate fields through protocol headers" do + headers = Protocol::HTTP::Headers[[ + ["set-cookie", "session=abc123; Path=/; HttpOnly"], + ["set-cookie", "theme=dark; SameSite=Lax"], + ]] + + header = headers["set-cookie"] + cookies = header.to_h + + expect(header).to be_a(subject) + expect(header).to be == [ + "session=abc123; Path=/; HttpOnly", + "theme=dark; SameSite=Lax", + ] + expect(cookies["session"].directives).to be == {"Path" => "/", "HttpOnly" => true} + expect(cookies["theme"].directives).to be == {"SameSite" => "Lax"} end end