From ef4f3caa5fa719522b5cb5ca083dcb8007530fae Mon Sep 17 00:00:00 2001 From: Max Melentiev Date: Fri, 6 Oct 2017 11:05:25 +0300 Subject: [PATCH] Make #session raise error when store is not configured Add more explanation on sessions in readme --- README.md | 39 ++++++++++++++---- .../bot/updates_controller/session.rb | 17 +++++--- .../bot/updates_controller/session_spec.rb | 41 +++++++++++++++++++ 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1d5e076..cd95aff 100644 --- a/README.md +++ b/README.md @@ -199,12 +199,33 @@ end #### Session -There is support for sessions using `ActiveSupport::Cache` stores. +This API is very close to ActiveController's session API, but works different +under the hood. Cookies can not be used to store session id or +whole session (like CookieStore does). So it uses key-value store and `session_key` +method to build identifier from update. + +Store can be one of numerous `ActiveSupport::Cache` stores. +While `:file_store` is suitable for development and single-server deployments +without heavy load, it doesn't scale well. Key-value databases with persistance +like Redis are more appropriate for production use. ```ruby -# configure store in env files: +# In rails app store can be configured in env files: config.telegram_updates_controller.session_store = :redis_store, {expires_in: 1.month} +# In other app it can be done for all controllers with: +Telegram::Bot::UpdatesController.session_store = :redis_store, {expires_in: 1.month} +# or for specific one: +OneOfUpdatesController.session_store = :redis_store, {expires_in: 1.month} +``` + +Default session id is made from bot's username and `(from || chat)['id']`. +It means that session will be the same for updates from user in every chat, +and different for every user in the same group chat. +To change this behavior you can override `session_key` method, or even +define multiple sessions in single controller. For details see `Session` module. + +```ruby class Telegram::WebhookController < Telegram::Bot::UpdatesController include Telegram::Bot::UpdatesController::Session # or just shortcut: @@ -222,12 +243,16 @@ class Telegram::WebhookController < Telegram::Bot::UpdatesController end private - # By default it uses bot's username and user's id as a session key. - # Chat's id is used only when `from` field is empty. - # Override `session_key` method to change this behavior. + + # In this case session will persist for user only in specific chat. + # Same user in other chat will have different session. def session_key - # In this case session will persist for user only in specific chat: - "#{bot.username}:#{chat['id']}:#{from['id']}" + "#{bot.username}:#{chat['id']}:#{from['id']}" if chat && from + end + + # This session will be the same for all updates in chat. + def chat_session + @_chat_session ||= self.class.build_session(chat && "#{bot.username}:#{chat['id']}") end end ``` diff --git a/lib/telegram/bot/updates_controller/session.rb b/lib/telegram/bot/updates_controller/session.rb index 3324fb8..c40313e 100644 --- a/lib/telegram/bot/updates_controller/session.rb +++ b/lib/telegram/bot/updates_controller/session.rb @@ -8,19 +8,26 @@ module Telegram module Session extend ActiveSupport::Concern + module ClassMethods + # Builds session with given key and optional store (default to session_store). + # This way it's easier to define multiple custom sessions, + # ex. one for group chat and one for user. + def build_session(key, store = session_store) + raise 'session_store is not configured' unless store + key ? SessionHash.new(store, key) : NullSessionHash.new + end + end + def process_action(*) super ensure - session.commit + session.commit if @_session end protected def session - @_session ||= begin - key = session_key - key ? SessionHash.new(self.class.session_store, key) : NullSessionHash.new - end + @_session ||= self.class.build_session(session_key) end def session_key diff --git a/spec/telegram/bot/updates_controller/session_spec.rb b/spec/telegram/bot/updates_controller/session_spec.rb index 83c61d2..103fd56 100644 --- a/spec/telegram/bot/updates_controller/session_spec.rb +++ b/spec/telegram/bot/updates_controller/session_spec.rb @@ -54,4 +54,45 @@ RSpec.describe Telegram::Bot::UpdatesController::Session do end end end + + describe '.build_session' do + subject { controller_class.build_session(key, *args) } + let(:key) {} + let(:args) { [] } + it { expect { subject }.to raise_error(/session_store is not configured/) } + + shared_examples 'NullSessionHash when key is not present' do |store_proc| + it { should be_instance_of(described_class::NullSessionHash) } + + context 'and key is present' do + let(:key) { :test_key } + it 'is valid SessionHash' do + expect(subject).to be_instance_of(described_class::SessionHash) + expect(subject.id).to eq key + expect(subject.instance_variable_get(:@store)).to be(instance_exec(&store_proc)) + end + end + end + + context 'when store configured' do + before { controller_class.session_store = nil } + include_examples 'NullSessionHash when key is not present', + -> { controller_class.session_store } + end + + context 'when store is given' do + let(:args) { [double(:store)] } + include_examples 'NullSessionHash when key is not present', -> { args[0] } + end + end + + describe '.session_store=' do + subject { ->(val) { controller_class.session_store = val } } + it 'casts to AS::Cache' do + expect { subject[:null_store] }.to change(controller_class, :session_store). + to(instance_of(ActiveSupport::Cache::NullStore)) + expect { subject[nil] }.to change(controller_class, :session_store). + to(instance_of(ActiveSupport::Cache::MemoryStore)) + end + end end