diff --git a/spec/flash_isolation_spec.cr b/spec/flash_isolation_spec.cr new file mode 100644 index 0000000..2d4585c --- /dev/null +++ b/spec/flash_isolation_spec.cr @@ -0,0 +1,73 @@ +require "./spec_helper" + +# Reads the flash of the request it serves, then optionally sets one for the next request. +private class FlashProbe + include HTTP::Handler + + getter shown = [] of String? + property next_message : String? = nil + + def initialize(@store : Session::MemoryStore(UserSession)) + end + + def call(context : HTTP::Server::Context) + shown << @store.flash.now["notice"]? + @store.flash.next["notice"] = next_message.not_nil! if next_message + end +end + +private def request_for(session_id : String) : HTTP::Server::Context + request = HTTP::Request.new("GET", "/") + request.cookies << HTTP::Cookie.new(name: Session.config.session_key, value: session_id) + HTTP::Server::Context.new(request, HTTP::Server::Response.new(IO::Memory.new)) +end + +describe "flash messages" do + it "reach the session that set them, and nobody else's" do + reset_config + store = Session::MemoryStore(UserSession).new + Session.config.store = store + alice = UserSession.new + bob = UserSession.new + store[alice.session_id] = alice + store[bob.session_id] = bob + + probe = FlashProbe.new(store) + handler = Session::SessionHandler(UserSession).new(store) + handler.next = probe + + probe.next_message = "Alice's private notice" + handler.call(request_for(alice.session_id)) + probe.next_message = nil + handler.call(request_for(bob.session_id)) + handler.call(request_for(alice.session_id)) + + probe.shown.should eq [nil, nil, "Alice's private notice"] + end + + it "lasts for exactly one request" do + reset_config + store = Session::MemoryStore(UserSession).new + Session.config.store = store + alice = UserSession.new + store[alice.session_id] = alice + + probe = FlashProbe.new(store) + handler = Session::SessionHandler(UserSession).new(store) + handler.next = probe + + probe.next_message = "once" + handler.call(request_for(alice.session_id)) + probe.next_message = nil + 2.times { handler.call(request_for(alice.session_id)) } + + probe.shown.should eq [nil, "once", nil] + end + + it "is saved with the session, so a store that serializes it keeps it" do + session = UserSession.new + session.flash.next["notice"] = "saved" + + UserSession.from_json(session.to_json).flash.next["notice"].should eq "saved" + end +end diff --git a/spec/store_spec.cr b/spec/store_spec.cr index 98f5d14..7bad41c 100644 --- a/spec/store_spec.cr +++ b/spec/store_spec.cr @@ -73,11 +73,14 @@ describe Session::Store do store.flash.should be_a Session::Flash end - it "rotates flash on load" do + it "rotates the flash of the session it loads" do store = Session::MemoryStore(UserSession).new - store.flash["notice"] = "Hello" + session = UserSession.new + session.flash["notice"] = "Hello" + store[session.session_id] = session cookies = HTTP::Cookies.new + cookies << HTTP::Cookie.new(name: store.session_key, value: session.session_id) store.load_from(cookies) store.flash.now["notice"].should eq "Hello" diff --git a/src/base.cr b/src/base.cr index fb75a26..043a9af 100644 --- a/src/base.cr +++ b/src/base.cr @@ -23,6 +23,10 @@ module Session property expires_at : Time = Session.config.timeout.from_now + # Messages for the next request. They belong to the session, so they are saved with it + # and reach only the visitor they were meant for. + getter flash : Flash = Flash.new + def initialize end diff --git a/src/store.cr b/src/store.cr index 617101b..ad57ab2 100644 --- a/src/store.cr +++ b/src/store.cr @@ -57,11 +57,10 @@ module Session # Instance variables for session lifecycle @mutex : Mutex = Mutex.new @current_sessions = {} of Fiber => T - @flash : Flash = Flash.new - # Access flash messages + # The current request's flash messages (those of its session). def flash : Flash - @flash + current_session.flash end def session_id : String @@ -116,9 +115,6 @@ module Session end def load_from(request_cookies : HTTP::Cookies) : T? - # Rotate flash messages at the start of each request - @flash.rotate! - if self.is_a?(CookieStore(T)) self.as(CookieStore(T)).cookies = request_cookies end @@ -132,6 +128,9 @@ module Session current_session.touch end + # Messages set by the previous request become this request's + current_session.flash.rotate! + on(:loaded, session_id, current_session) end end