# frozen_string_literal: true require 'spec_helper' class FlowMap include Stealth::Flow flow :new_todo do state :new state :get_due_date state :created, fails_to: :new state :error end flow :marco do state :polo end end describe "Stealth::Session" do let(:id) { '0xDEADBEEF' } it "should raise an error if $redis is not set" do $redis = nil expect { Stealth::Session.new(id: id) }.to raise_error(Stealth::Errors::RedisNotConfigured) $redis = MockRedis.new end describe "without a session" do let(:session) { Stealth::Session.new(id: id) } it "should have nil flow and state" do expect(session.flow).to be_nil expect(session.state).to be_nil end it "should have nil flow_string and state_string" do expect(session.flow_string).to be_nil expect(session.state_string).to be_nil end it "should respond to present? and blank?" do expect(session.present?).to be false expect(session.blank?).to be true end end describe "with a session" do let(:session) do session = Stealth::Session.new(id: id) session.set_session(new_flow: 'marco', new_state: 'polo') session end it "should return the FlowMap" do expect(session.flow).to be_a(FlowMap) end it "should return the state" do expect(session.state).to be_a(Stealth::Flow::State) expect(session.state).to eq :polo end it "should return the flow_string" do expect(session.flow_string).to eq "marco" end it "should return the state_string" do expect(session.state_string).to eq "polo" end it "should respond to present? and blank?" do expect(session.present?).to be true expect(session.blank?).to be false end end describe "incrementing and decrementing" do let(:session) { Stealth::Session.new(id: id) } it "should increment the state" do session.set_session(new_flow: 'new_todo', new_state: 'get_due_date') new_session = session + 1.state expect(new_session.state_string).to eq('created') end it "should decrement the state" do session.set_session(new_flow: 'new_todo', new_state: 'error') new_session = session - 2.states expect(new_session.state_string).to eq('get_due_date') end it "should return the first state if the decrement is out of bounds" do session.set_session(new_flow: 'new_todo', new_state: 'get_due_date') new_session = session - 5.states expect(new_session.state_string).to eq('new') end it "should return the last state if the increment is out of bounds" do session.set_session(new_flow: 'new_todo', new_state: 'created') new_session = session + 5.states expect(new_session.state_string).to eq('error') end end describe "==" do let(:session) { _session = Stealth::Session.new(id: id, type: :primary) _session.set_session(new_flow: 'hello', new_state: 'say_hola') _session } it "should return false if the session ids do not" do other_session = Stealth::Session.new(id: 'xyz123', type: :primary) other_session.set_session(new_flow: 'hello', new_state: 'say_hola') expect(session == other_session).to be false end it "should return false if the states do not" do other_session = Stealth::Session.new(id: id, type: :primary) other_session.set_session(new_flow: 'hello', new_state: 'say_hola2') expect(session == other_session).to be false end it "should return false if flows do not match" do other_session = Stealth::Session.new(id: id, type: :primary) other_session.set_session(new_flow: 'hello2', new_state: 'say_hola') expect(session == other_session).to be false end it 'should return false if the session type does not match' do other_session = Stealth::Session.new(id: id, type: :back_to) other_session.set_session(new_flow: 'hello', new_state: 'say_hola') expect(session == other_session).to be false end it 'should return true if the session id, flow, state, and session types match' do other_session = Stealth::Session.new(id: id, type: :primary) other_session.set_session(new_flow: 'hello', new_state: 'say_hola') expect(session == other_session).to be true end end describe "self.is_a_session_string?" do it "should return false for state strings" do session_string = 'say_hello' expect(Stealth::Session.is_a_session_string?(session_string)).to be false end it "should return false for an incomplete session string" do session_string = 'hello->' expect(Stealth::Session.is_a_session_string?(session_string)).to be false end it "should return true for a complete session string" do session_string = 'hello->say_hello' expect(Stealth::Session.is_a_session_string?(session_string)).to be true end end describe "self.canonical_session_slug" do it "should generate a canonical session slug given a flow and state as symbols" do expect( Stealth::Session.canonical_session_slug(flow: :hello, state: :say_hello) ).to eq 'hello->say_hello' end it "should generate a canonical session slug given a flow and state as strings" do expect( Stealth::Session.canonical_session_slug(flow: 'hello', state: 'say_hello') ).to eq 'hello->say_hello' end end describe "self.flow_and_state_from_session_slug" do it "should return the flow and string as a hash with symbolized keys" do slug = 'hello->say_hello' expect( Stealth::Session.flow_and_state_from_session_slug(slug: slug) ).to eq({ flow: 'hello', state: 'say_hello' }) end it "should not raise if slug is nil" do slug = nil expect( Stealth::Session.flow_and_state_from_session_slug(slug: slug) ).to eq({ flow: nil, state: nil }) end end describe "setting sessions" do let(:session) { Stealth::Session.new(id: id) } let(:previous_session) { Stealth::Session.new(id: id, type: :previous) } let(:back_to_session) { Stealth::Session.new(id: id, type: :back_to) } before(:each) do $redis.del(id) $redis.del([id, 'previous'].join('-')) $redis.del([id, 'back_to'].join('-')) end it "should store the new session" do session.set_session(new_flow: 'marco', new_state: 'polo') expect($redis.get(id)).to eq 'marco->polo' end it "should store the current_session to previous_session" do $redis.set(id, 'new_todo->new') $redis.set([id, 'previous'].join('-'), 'new_todo->error') session.set_session(new_flow: 'marco', new_state: 'polo') expect(previous_session.get_session).to eq 'new_todo->new' end it "should not update previous_session if it matches current_session" do $redis.set(id, 'marco->polo') $redis.set([id, 'previous'].join('-'), 'new_todo->new') session.set_session(new_flow: 'marco', new_state: 'polo') expect(previous_session.get_session).to eq 'new_todo->new' end it "should set an expiration for current_session if session_ttl is specified" do Stealth.config.session_ttl = 500 session.set_session(new_flow: 'marco', new_state: 'polo') expect($redis.ttl(id)).to be > 0 Stealth.config.session_ttl = 0 end it "should set an expiration for previous_session if session_ttl is specified" do Stealth.config.session_ttl = 500 $redis.set(id, 'new_todo->new') session.set_session(new_flow: 'marco', new_state: 'polo') expect($redis.ttl([id, 'previous'].join('-'))).to be > 0 Stealth.config.session_ttl = 0 end it "should NOT set an expiration if session_ttl is not specified" do Stealth.config.session_ttl = 0 session.set_session(new_flow: 'new_todo', new_state: 'get_due_date') expect($redis.ttl(id)).to eq -1 # Does not expire end it "should set the session for back_to" do back_to_session.set_session(new_flow: 'marco', new_state: 'polo') expect($redis.get([id, 'back_to'].join('-'))).to eq 'marco->polo' end it "should set an expiration for back_to if session_ttl is specified" do Stealth.config.session_ttl = 500 back_to_session.set_session(new_flow: 'marco', new_state: 'polo') expect($redis.ttl([id, 'back_to'].join('-'))).to be > 0 Stealth.config.session_ttl = 0 end end describe "getting sessions" do let(:session) { Stealth::Session.new(id: id) } let(:previous_session) { Stealth::Session.new(id: id, type: :previous) } let(:back_to_session) { Stealth::Session.new(id: id, type: :back_to) } before(:each) do $redis.del(id) $redis.del([id, 'previous'].join('-')) $redis.del([id, 'back_to'].join('-')) end it "should return the stored current_session" do session.set_session(new_flow: 'marco', new_state: 'polo') expect(session.get_session).to eq 'marco->polo' end it "should return the stored previous_session if previous is requested" do $redis.set(id, 'new_todo->new') session.set_session(new_flow: 'marco', new_state: 'polo') expect(previous_session.get_session).to eq 'new_todo->new' end it "should update the expiration of current_session if session_ttl is set" do Stealth.config.session_ttl = 50 session.set_session(new_flow: 'marco', new_state: 'polo') expect($redis.ttl(id)).to be_between(0, 50).inclusive Stealth.config.session_ttl = 500 session.session = nil # reset memoization session.get_session expect($redis.ttl(id)).to be > 100 Stealth.config.session_ttl = 0 end it "should return the stored back_to_session" do back_to_session.set_session(new_flow: 'marco', new_state: 'polo') expect(back_to_session.get_session).to eq 'marco->polo' end it "should update the expiration of back_to_session if session_ttl is set" do Stealth.config.session_ttl = 50 back_to_session.set_session(new_flow: 'marco', new_state: 'polo') expect($redis.ttl([id, 'back_to'].join('-'))).to be_between(0, 50).inclusive Stealth.config.session_ttl = 500 back_to_session.session = nil # reset memoization back_to_session.get_session expect($redis.ttl([id, 'back_to'].join('-'))).to be > 100 Stealth.config.session_ttl = 0 end end describe "clearing sessions" do let(:session) { Stealth::Session.new(id: id) } let(:previous_session) { Stealth::Session.new(id: id, type: :previous) } let(:back_to_session) { Stealth::Session.new(id: id, type: :back_to) } before(:each) do session.send(:persist_key, key: session.session_key, value: '1') previous_session.send(:persist_key, key: previous_session.session_key, value: '1') back_to_session.send(:persist_key, key: back_to_session.session_key, value: '1') end it "should remove a default session from Redis" do expect($redis.get(session.session_key)).to eq '1' session.clear_session expect($redis.get(session.session_key)).to be_nil end it "should remove a previous session from Redis" do expect($redis.get(previous_session.session_key)).to eq '1' previous_session.clear_session expect($redis.get(previous_session.session_key)).to be_nil end it "should remove a back_to session from Redis" do expect($redis.get(back_to_session.session_key)).to eq '1' back_to_session.clear_session expect($redis.get(back_to_session.session_key)).to be_nil end end describe "self.slugify" do it "should return a session slug given a flow and state" do expect(Stealth::Session.slugify(flow: 'hello', state: 'world')).to eq 'hello->world' end it "should raise an ArgumentError if flow is blank" do expect { Stealth::Session.slugify(flow: '', state: 'world') }.to raise_error(ArgumentError) end it "should raise an ArgumentError if state is blank" do expect { Stealth::Session.slugify(flow: 'hello', state: nil) }.to raise_error(ArgumentError) end it "should raise an ArgumentError if flow and state are blank" do expect { Stealth::Session.slugify(flow: nil, state: nil) }.to raise_error(ArgumentError) end end end