diff --git a/CHANGELOG.md b/CHANGELOG.md index 954ccb2..ce712c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# Unreleased + +- Make integration & controller specs consistent. + __Breaking changes__ for controller specs: + - Changed signature `dispatch(bot, update) => dispatch(update, bot)`. + - `update` helper is symbolized by default. + - `build_update(type, data)` is dropped in favor of `deep_stringify(type => data)`. +- Provide support for integration testing of bots in poller mode and non-Rails apps. + __Breaking changes__: + - Requiring `telegram/bot/rspec/integration` is deprecated in favor of + `telegram/bot/rspec/integration/rails`. + - `:telegram_bot` rspec tag is replaced with `telegram_bot: :rails`. + # 0.13.1 - Extracted typed response mappings to telegram-bot-types gem. diff --git a/README.md b/README.md index e4bc808..9c6c9de 100644 --- a/README.md +++ b/README.md @@ -192,8 +192,9 @@ end #### Reply helpers There are helpers to respond for basic actions. They just set chat/message/query -identifiers from update. See [`ReplyHelpers`](https://github.com/telegram-bot-rb/telegram-bot/blob/master/lib/telegram/bot/updates_controller/reply_helpers.rb) module for more information. -Here are this methods signatures: +identifiers from update. See +[`ReplyHelpers`](https://github.com/telegram-bot-rb/telegram-bot/blob/master/lib/telegram/bot/updates_controller/reply_helpers.rb) +module for more information. Here are this methods signatures: ```ruby def respond_with(type, params); end @@ -394,11 +395,11 @@ Telegram::Bot::UpdatesPoller.start(bot, controller_class) ### Testing -There is `Telegram::Bot::ClientStub` class to stub client for tests. -Instead of performing API requests it stores them in `requests` hash. +There is a `Telegram::Bot::ClientStub` class to stub client for tests. +Instead of performing API requests it stores them in a `requests` hash. To stub all possible clients use `Telegram::Bot::ClientStub.stub_all!` before -initializing clients. Here is template for RSpec: +initializing clients. Here is a template for RSpec: ```ruby # environments/test.rb @@ -416,47 +417,82 @@ RSpec.configure do |config| end ``` -There are integration and controller contexts for RSpec and some built-in matchers: +RSpec contexts and helpers are included automatically for groups and examples with matching +tags. In RSpec < 3.4 it's required to use `include_context` explicitly. +See [list of available helpers](https://github.com/telegram-bot-rb/telegram-bot/tree/master/lib/telegram/bot/rspec) +for details. + +There are 3 types of integration tests: + +- `:rails` - for testing bot in webhooks-mode in Rails application. + It simulates webhook requests POSTing data to controller's endpoint. + It works on the top of requests specs, so `rspec-rails` gem is required. +- `:rack` - For testing bot in webhooks-mode in non-Rails application. + It uses `rack-test` gem to POST requests to bot's endpoint. +- `:poller` - Calls `.dispatch` directly on controller class. + +Pick the appropriate one, then require `telegram/bot/rspec/integration/#{type}` +and mark spec group with tag `telegram_bot: type`. See configuration options +for each type in +[telegram/bot/rspec/integration/](https://github.com/telegram-bot-rb/telegram-bot/tree/master/lib/telegram/bot/rspec/integration). + +Here is an example test for a Rails app: ```ruby # spec/requests/telegram_webhooks_spec.rb -require 'telegram/bot/rspec/integration' +require 'telegram/bot/rspec/integration/rails' -RSpec.describe TelegramWebhooksController, :telegram_bot do - # for old rspec add: - # include_context 'telegram/bot/integration' +RSpec.describe TelegramWebhooksController, telegram_bot: :rails do + # for old RSpec: + # include_context 'telegram/bot/integration/rails' + + # Main method is #dispatch(update). Some helpers are: + # dispatch_message(text, options = {}) + # dispatch_command(cmd, *args) + + # Available matchers can be found in Telegram::Bot::RSpec::ClientMatchers. + it 'shows usage of basic matchers' + # The most basic one is #make_telegram_request(bot, endpoint, params_matcher) + expect { dispatch_command(:start) }. + to make_telegram_request(bot, :sendMessage, hash_including(text: 'msg text')) + + # There are some shortcuts for dispatching basic updates and testing responses. + expect { dispatch_message('Hi') }.to send_telegram_message(bot, /msg regexp/, some: :option) + end describe '#start' do subject { -> { dispatch_command :start } } + # Using built in matcher for `respond_to`: it { should respond_with_message 'Hi there!' } end - # There is context for callback queries with related matchers. + # There is context for callback queries with related matchers, + # use :callback_query tag to include it. describe '#hey_callback_query', :callback_query do let(:data) { "hey:#{name}" } let(:name) { 'Joe' } it { should answer_callback_query('Hey Joe') } it { should edit_current_message :text, text: 'Done' } + end end - -# For controller specs use -require 'telegram/bot/updates_controller/rspec_helpers' -RSpec.describe TelegramWebhooksController, type: :telegram_bot_controller do - # for old rspec add: - # include_context 'telegram/bot/updates_controller' -end - -# Matchers are available for custom specs: -include Telegram::Bot::RSpec::ClientMatchers - -expect(&process_update).to send_telegram_message(bot, /msg regexp/, some: :option) -expect(&process_update). - to make_telegram_request(bot, :sendMessage, hash_including(text: 'msg text')) ``` -Place integration tests inside `spec/requests` -when using RSpec's `infer_spec_type_from_file_location!`, -or just add `type: :request` to `describe`. +There is a context for testing bot controller in the way similar to Rails controller tests. +It's supposed to be a low-level alternative for integration tests. Among the differences is +that controller tests use a single controller instance for all dispatches in specific exaple, +session is stubbed (does not use configured store engine), and update is not serialized +so it also supports mocks. This can be useful for unit testing, but should not be used as +the default way to test the bot. + +```ruby +require 'telegram/bot/updates_controller/rspec_helpers' +RSpec.describe TelegramWebhooksController, type: :telegram_bot_controller do + # for old RSpec: + # include_context 'telegram/bot/updates_controller' + + # Same helpers and matchers like dispatch_command, answer_callback_query are available here. +end +``` See sample app for more examples. diff --git a/lib/telegram/bot.rb b/lib/telegram/bot.rb index fdb15fe..ce860e3 100644 --- a/lib/telegram/bot.rb +++ b/lib/telegram/bot.rb @@ -14,10 +14,10 @@ module Telegram module_function - def deprecation_0_14 + def deprecation_0_15 @deprecation ||= begin require 'active_support/deprecation' - ActiveSupport::Deprecation.new('0.14', 'Telegram::Bot') + ActiveSupport::Deprecation.new('0.15', 'Telegram::Bot') end end diff --git a/lib/telegram/bot/middleware.rb b/lib/telegram/bot/middleware.rb index 70bbd30..b8931f3 100644 --- a/lib/telegram/bot/middleware.rb +++ b/lib/telegram/bot/middleware.rb @@ -1,8 +1,7 @@ require 'active_support/concern' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/json' -require 'action_dispatch/http/mime_type' -require 'action_dispatch/http/request' +require 'action_dispatch' module Telegram module Bot diff --git a/lib/telegram/bot/routes_helper.rb b/lib/telegram/bot/routes_helper.rb index 9862c83..4c386ef 100644 --- a/lib/telegram/bot/routes_helper.rb +++ b/lib/telegram/bot/routes_helper.rb @@ -24,33 +24,6 @@ module Telegram end end - # # Create routes for all Telegram.bots to use same controller: - # telegram_webhooks TelegramController - # - # # Or pass custom bots usin any of supported config options: - # telegram_webhooks TelegramController, [ - # bot, - # {token: token, username: username}, - # other_bot_token, - # ] - def telegram_webhooks(controllers, bots = nil, **options) - Bot.deprecation_0_14.deprecation_warning(:telegram_webhooks, <<-TXT.strip_heredoc) - It brings unnecessary complexity and encourages writeng less readable code. - Please use telegram_webhook method instead. - It's signature `telegram_webhook(controller, bot = :default, **options)`. - Multiple-bot environments now requires calling this method in a loop - or using statement for each bot. - TXT - unless controllers.is_a?(Hash) - bots = bots ? Array.wrap(bots) : Telegram.bots.values - controllers = Hash[bots.map { |x| [x, controllers] }] - end - controllers.each do |bot, controller| - controller, bot_options = controller if controller.is_a?(Array) - telegram_webhook(controller, bot, options.merge(bot_options || {})) - end - end - # Define route which processes requests using given controller and bot. # # telegram_webhook TelegramController, bot diff --git a/lib/telegram/bot/rspec.rb b/lib/telegram/bot/rspec.rb index 691eaad..aeba788 100644 --- a/lib/telegram/bot/rspec.rb +++ b/lib/telegram/bot/rspec.rb @@ -2,6 +2,15 @@ module Telegram module Bot module RSpec autoload :ClientMatchers, 'telegram/bot/rspec/client_matchers' + + module_function + + # Yelds a block if `include_context` is supported. + def with_include_context + ::RSpec.configure do |config| + yield(config) if config.respond_to?(:include_context) + end + end end end end diff --git a/lib/telegram/bot/rspec/callback_query_helpers.rb b/lib/telegram/bot/rspec/callback_query_helpers.rb new file mode 100644 index 0000000..64a22fe --- /dev/null +++ b/lib/telegram/bot/rspec/callback_query_helpers.rb @@ -0,0 +1,39 @@ +require 'telegram/bot/rspec' +require 'telegram/bot/rspec/message_helpers' + +# Shared helpers for testing callback query updates. +RSpec.shared_context 'telegram/bot/callback_query' do + include_context 'telegram/bot/message_helpers' + + subject { -> { dispatch callback_query: payload } } + let(:payload) { {id: callback_query_id, from: from, message: message, data: data} } + let(:callback_query_id) { 11 } + let(:message_id) { 22 } + let(:message) { {message_id: message_id, chat: chat, text: 'message text'} } + let(:data) { raise '`let(:data) { "callback query data here" }` is required' } + + # Matcher to check that origin message got edited. + def edit_current_message(type, options = {}) + description = 'edit current message' + options = options.merge( + message_id: message[:message_id], + chat_id: chat_id, + ) + Telegram::Bot::RSpec::ClientMatchers::MakeTelegramRequest.new( + bot, :"editMessage#{type.to_s.camelize}", description: description + ).with(hash_including(options)) + end + + # Matcher to check that callback query is answered. + def answer_callback_query(text = Regexp.new(''), options = {}) + description = "answer callback query with #{text.inspect}" + text = a_string_matching(text) if text.is_a?(Regexp) + options = options.merge( + callback_query_id: payload[:id], + text: text, + ) + Telegram::Bot::RSpec::ClientMatchers::MakeTelegramRequest.new( + bot, :answerCallbackQuery, description: description + ).with(hash_including(options)) + end +end diff --git a/lib/telegram/bot/rspec/integration.rb b/lib/telegram/bot/rspec/integration.rb index a6f9eda..e28ca80 100644 --- a/lib/telegram/bot/rspec/integration.rb +++ b/lib/telegram/bot/rspec/integration.rb @@ -1,85 +1,10 @@ -RSpec.shared_context 'telegram/bot/integration' do - let(:bot) { Telegram.bot } - let(:default_message_options) { {from: from, chat: chat} } - let(:from) { {id: from_id} } - let(:from_id) { 123 } - let(:chat) { {id: chat_id} } - let(:chat_id) { 456 } - let(:controller_path) do - route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot) - Rails.application.routes.url_helpers.public_send("#{route_name}_path") - end - let(:request_headers) do - { - 'ACCEPT' => 'application/json', - 'Content-Type' => 'application/json', - } - end - let(:clear_session?) { described_class.respond_to?(:session_store) } - before { described_class.session_store.try!(:clear) if clear_session? } +require 'telegram/bot' +Telegram::Bot.deprecation_0_15.warn( + "`require 'telegram/bot/rspec/integration'` is deprecated in favor of " \ + "`require 'telegram/bot/rspec/integration/rails'`" +) +require 'telegram/bot/rspec/integration/rails' - include Telegram::Bot::RSpec::ClientMatchers - - def dispatch(update) - if ActionPack::VERSION::MAJOR >= 5 - post(controller_path, params: update.to_json, headers: request_headers) - else - post(controller_path, update.to_json, request_headers) - end - end - - def dispatch_message(text, options = {}) - dispatch message: default_message_options.merge(options).merge(text: text) - end - - def dispatch_command(*args) - options = args.last.is_a?(Hash) ? args.pop : {} - dispatch_message("/#{args.join ' '}", options) - end - - # Matcher to check response. Make sure to define `let(:chat_id)`. - def respond_with_message(expected = Regexp.new('')) - raise 'Define chat_id to use respond_with_message' unless defined?(chat_id) - send_telegram_message(bot, expected, chat_id: chat_id) - end -end - -RSpec.shared_context 'telegram/bot/callback_query', callback_query: true do - include_context 'telegram/bot/integration' - - subject { -> { dispatch callback_query: payload } } - let(:payload) { {id: 11, from: from, message: message, data: data} } - let(:message) { {message_id: 22, chat: chat, text: 'message text'} } - - # Matcher to check that origin message got edited. - def edit_current_message(type, options = {}) - description = 'edit current message' - options = options.merge( - message_id: message[:message_id], - chat_id: chat_id, - ) - Telegram::Bot::RSpec::ClientMatchers::MakeTelegramRequest.new( - bot, :"editMessage#{type.to_s.camelize}", description: description - ).with(hash_including(options)) - end - - # Matcher to check that callback query is answered. - def answer_callback_query(text = Regexp.new(''), options = {}) - description = "answer callback query with #{text.inspect}" - text = a_string_matching(text) if text.is_a?(Regexp) - options = options.merge( - callback_query_id: payload[:id], - text: text, - ) - Telegram::Bot::RSpec::ClientMatchers::MakeTelegramRequest.new( - bot, :answerCallbackQuery, description: description - ).with(hash_including(options)) - end -end - -RSpec.configure do |config| - if config.respond_to?(:include_context) - config.include_context 'telegram/bot/integration', :telegram_bot - config.include_context 'telegram/bot/callback_query', :telegram_bot, :callback_query - end +Telegram::Bot::RSpec.with_include_context do |config| + config.include_context 'telegram/bot/integration/rails', telegram_bot: true end diff --git a/lib/telegram/bot/rspec/integration/poller.rb b/lib/telegram/bot/rspec/integration/poller.rb new file mode 100644 index 0000000..54f7b8d --- /dev/null +++ b/lib/telegram/bot/rspec/integration/poller.rb @@ -0,0 +1,14 @@ +require 'telegram/bot/rspec/integration/shared' + +RSpec.shared_context 'telegram/bot/integration/poller' do + include_context 'telegram/bot/integration/shared' + let(:controller_class) { described_class } + + def dispatch(update) + controller_class.dispatch(bot, update.as_json) + end +end + +Telegram::Bot::RSpec.with_include_context do |config| + config.include_context 'telegram/bot/integration/poller', telegram_bot: :poller +end diff --git a/lib/telegram/bot/rspec/integration/rack.rb b/lib/telegram/bot/rspec/integration/rack.rb new file mode 100644 index 0000000..520d90c --- /dev/null +++ b/lib/telegram/bot/rspec/integration/rack.rb @@ -0,0 +1,24 @@ +require 'telegram/bot/rspec/integration/shared' +require 'rack/test' + +RSpec.shared_context 'telegram/bot/integration/rack' do + include_context 'telegram/bot/integration/shared' + include Rack::Test::Methods + + let(:request_path) { raise '`let(:request_path) { path to bot }` is required' } + let(:app) { raise '`let(:app) { your rack app here }` is required' } + let(:request_headers) do + { + 'ACCEPT' => 'application/json', + 'CONTENT_TYPE' => 'application/json', + } + end + + def dispatch(update) + post request_path, update.to_json, request_headers + end +end + +Telegram::Bot::RSpec.with_include_context do |config| + config.include_context 'telegram/bot/integration/rack', telegram_bot: :rack +end diff --git a/lib/telegram/bot/rspec/integration/rails.rb b/lib/telegram/bot/rspec/integration/rails.rb new file mode 100644 index 0000000..1e43e09 --- /dev/null +++ b/lib/telegram/bot/rspec/integration/rails.rb @@ -0,0 +1,28 @@ +require 'telegram/bot/rspec/integration/shared' + +RSpec.shared_context 'telegram/bot/integration/rails', type: :request do + include_context 'telegram/bot/integration/shared' + + let(:controller_path) do + route_name = Telegram::Bot::RoutesHelper.route_name_for_bot(bot) + Rails.application.routes.url_helpers.public_send("#{route_name}_path") + end + let(:request_headers) do + { + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + } + end + + def dispatch(update) + if ActionPack::VERSION::MAJOR >= 5 + post(controller_path, params: update.to_json, headers: request_headers) + else + post(controller_path, update.to_json, request_headers) + end + end +end + +Telegram::Bot::RSpec.with_include_context do |config| + config.include_context 'telegram/bot/integration/rails', telegram_bot: :rails +end diff --git a/lib/telegram/bot/rspec/integration/shared.rb b/lib/telegram/bot/rspec/integration/shared.rb new file mode 100644 index 0000000..73f3087 --- /dev/null +++ b/lib/telegram/bot/rspec/integration/shared.rb @@ -0,0 +1,14 @@ +require 'active_support/json' +require 'telegram/bot' +require 'telegram/bot/rspec/message_helpers' +require 'telegram/bot/rspec/callback_query_helpers' + +RSpec.shared_context 'telegram/bot/integration/shared' do + include Telegram::Bot::RSpec::ClientMatchers + include_context 'telegram/bot/message_helpers' + include_context 'telegram/bot/callback_query', :callback_query + + let(:bot) { Telegram.bot } + let(:clear_session?) { described_class.respond_to?(:session_store) } + before { described_class.session_store.try!(:clear) if clear_session? } +end diff --git a/lib/telegram/bot/rspec/message_helpers.rb b/lib/telegram/bot/rspec/message_helpers.rb new file mode 100644 index 0000000..6585f0c --- /dev/null +++ b/lib/telegram/bot/rspec/message_helpers.rb @@ -0,0 +1,26 @@ +# Shared helpers for testing message updates. +RSpec.shared_context 'telegram/bot/message_helpers' do + let(:default_message_options) { {from: from, chat: chat} } + let(:from) { {id: from_id} } + let(:from_id) { 123 } + let(:chat) { {id: chat_id} } + let(:chat_id) { 456 } + + # Shortcut for dispatching messages with default params. + def dispatch_message(text, options = {}) + dispatch message: default_message_options.merge(options).merge(text: text) + end + + # Dispatch command message. + def dispatch_command(cmd, *args) + options = args.last.is_a?(Hash) ? args.pop : {} + args.unshift("/#{cmd}") + dispatch_message(args.join(' '), options) + end + + # Matcher to check response. Make sure to define `let(:chat_id)`. + def respond_with_message(expected = Regexp.new('')) + raise 'Define chat_id to use respond_with_message' unless defined?(chat_id) + send_telegram_message(bot, expected, chat_id: chat_id) + end +end diff --git a/lib/telegram/bot/updates_controller/rspec_helpers.rb b/lib/telegram/bot/updates_controller/rspec_helpers.rb index 1145a61..df3d25f 100644 --- a/lib/telegram/bot/updates_controller/rspec_helpers.rb +++ b/lib/telegram/bot/updates_controller/rspec_helpers.rb @@ -1,37 +1,32 @@ require 'telegram/bot/updates_controller/testing' +require 'telegram/bot/rspec/message_helpers' +require 'telegram/bot/rspec/callback_query_helpers' RSpec.shared_context 'telegram/bot/updates_controller' do + include Telegram::Bot::RSpec::ClientMatchers + include_context 'telegram/bot/message_helpers' + include_context 'telegram/bot/callback_query', :callback_query + let(:controller_class) { described_class } let(:controller) do - controller_class.new(bot, update).tap do |x| + controller_class.new(*controller_args).tap do |x| x.extend Telegram::Bot::UpdatesController::Testing end end - let(:update) { build_update(payload_type, payload) } + let(:controller_args) { [bot, deep_stringify(update)] } + let(:update) { {payload_type => payload} } let(:payload_type) { :some_type } let(:payload) { double(:payload) } let(:bot) { Telegram::Bot::ClientStub.new(bot_name) } let(:bot_name) { 'bot' } let(:session) { controller.send(:session) } - let(:from_id) { 123 } - let(:chat_id) { 456 } - let(:default_message_options) { {from: {id: from_id}, chat: {id: chat_id}} } - include Telegram::Bot::RSpec::ClientMatchers - - def dispatch(bot = self.bot, update = self.update) - controller.dispatch_again(bot, update) - end - - def dispatch_message(text, options = {}) - update = build_update :message, default_message_options.merge(options).merge(text: text) - dispatch bot, update - end - - def build_update(type, content) - deep_stringify type => content + # Process update. + def dispatch(update = self.update, bot = self.bot) + controller.dispatch_again(bot, deep_stringify(update)) end + # Same as `.as_json` but mocks-friendly. def deep_stringify(input) case input when Array then input.map(&method(__callee__)) @@ -39,15 +34,8 @@ RSpec.shared_context 'telegram/bot/updates_controller' do else input end end - - # Matcher to check response. Make sure to define `let(:chat_id)`. - def respond_with_message(expected) - send_telegram_message(bot, expected, chat_id: chat_id) - end end -RSpec.configure do |config| - if config.respond_to?(:include_context) - config.include_context 'telegram/bot/updates_controller', type: :telegram_bot_controller - end +Telegram::Bot::RSpec.with_include_context do |config| + config.include_context 'telegram/bot/updates_controller', type: :telegram_bot_controller end diff --git a/spec/integration/requests/default_bot_spec.rb b/spec/integration/requests/default_bot_spec.rb index 3791341..81a442b 100644 --- a/spec/integration/requests/default_bot_spec.rb +++ b/spec/integration/requests/default_bot_spec.rb @@ -1,6 +1,6 @@ require 'integration_helper' -RSpec.describe DefaultBotController, :telegram_bot, type: :request do +RSpec.describe DefaultBotController, telegram_bot: :rails do describe '#start' do subject { -> { dispatch_command :start } } it { should respond_with_message 'from default' } diff --git a/spec/integration/requests/named_bot_spec.rb b/spec/integration/requests/named_bot_spec.rb index 11e2eb2..cccfbd4 100644 --- a/spec/integration/requests/named_bot_spec.rb +++ b/spec/integration/requests/named_bot_spec.rb @@ -1,6 +1,6 @@ require 'integration_helper' -RSpec.describe NamedBotController, :telegram_bot, type: :request do +RSpec.describe NamedBotController, telegram_bot: :rails do let(:bot) { Telegram.bots[:named] } describe '#start' do subject { -> { dispatch_command :start } } diff --git a/spec/integration/requests/other_bot_spec.rb b/spec/integration/requests/other_bot_spec.rb index ddcdb90..8ccb783 100644 --- a/spec/integration/requests/other_bot_spec.rb +++ b/spec/integration/requests/other_bot_spec.rb @@ -1,6 +1,6 @@ require 'integration_helper' -RSpec.describe OtherBotController, :telegram_bot, type: :request do +RSpec.describe OtherBotController, telegram_bot: :rails do let(:bot) { Telegram.bots[:other] } describe '#start' do subject { -> { dispatch_command :start } } diff --git a/spec/integration_helper.rb b/spec/integration_helper.rb index 941d11e..e6eef97 100644 --- a/spec/integration_helper.rb +++ b/spec/integration_helper.rb @@ -1,7 +1,6 @@ -require 'telegram/bot/rspec/integration' +require 'telegram/bot/rspec/integration/rails' require 'action_controller' require 'action_dispatch' -require 'action_dispatch/testing/integration' require 'rails' require 'telegram/bot/railtie' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 54ce8e2..1be5648 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -32,6 +32,14 @@ RSpec.configure do |config| expectations.include_chain_clauses_in_custom_matcher_descriptions = true end + # This config option will be enabled by default on RSpec 4, + # but for reasons of backwards compatibility, you have to + # set it on RSpec 3. + # + # It causes the host group and examples to inherit metadata + # from the shared context. + config.shared_context_metadata_behavior = :apply_to_host_groups + config.mock_with :rspec do |mocks| # Prevents you from mocking or stubbing a method that does not exist on # a real object. This is generally recommended, and will default to diff --git a/spec/support/examples/integration.rb b/spec/support/examples/integration.rb new file mode 100644 index 0000000..00ee1ac --- /dev/null +++ b/spec/support/examples/integration.rb @@ -0,0 +1,24 @@ +RSpec.shared_examples 'shared integration examples' do + let(:bot) { Telegram::Bot::ClientStub.new('token') } + let(:controller_class) do + Class.new(Telegram::Bot::UpdatesController) do + def start(data = nil, *) + respond_with :message, text: "Hi #{data}" + end + + def callback_query(data, *) + answer_callback_query "pong: #{data}" + end + end + end + + describe '#start' do + subject { -> { dispatch_command(:start, :test_data) } } + it { should respond_with_message('Hi test_data') } + end + + describe '#callback_query', :callback_query do + let(:data) { :test_data } + it { should answer_callback_query "pong: #{data}" } + end +end diff --git a/spec/telegram/bot/middleware_spec.rb b/spec/telegram/bot/middleware_spec.rb index dff57d6..7d84f51 100644 --- a/spec/telegram/bot/middleware_spec.rb +++ b/spec/telegram/bot/middleware_spec.rb @@ -7,7 +7,6 @@ RSpec.describe Telegram::Bot::Middleware do describe '#call' do subject { instance.call(env) } - let(:env) { {'action_dispatch.request.request_parameters' => json_body} } let(:update) { {'message' => {'id' => 1}} } let(:env) do Rack::MockRequest.env_for('/', @@ -21,7 +20,6 @@ RSpec.describe Telegram::Bot::Middleware do if ActionPack::VERSION::MAJOR < 5 # Before Rails 5, params are parsed in middleware. # In Rails 5, they are parsed in Request#request_parameters. - require 'action_dispatch/middleware/params_parser' let(:instance) { ActionDispatch::ParamsParser.new(super()) } end diff --git a/spec/telegram/bot/routes_helper_spec.rb b/spec/telegram/bot/routes_helper_spec.rb index dbfa1bc..0a92af2 100644 --- a/spec/telegram/bot/routes_helper_spec.rb +++ b/spec/telegram/bot/routes_helper_spec.rb @@ -95,108 +95,4 @@ RSpec.describe Telegram::Bot::RoutesHelper do end end end - - describe '#telegram_webhooks' do - subject { mapper.telegram_webhooks(*input) } - let(:mapper) { double(:mapper).tap { |x| x.extend described_class } } - let(:bots) { {default: bot, other: other_bot} } - let(:controller) { double(:controller, name: :controller) } - let(:other_controller) { double(:other_controller, name: :other_controller) } - before { allow(Telegram).to receive(:bots) { bots } } - around { |ex| Telegram::Bot.deprecation_0_14.silence { ex.run } } - - def assert_routes(*expected) # rubocop:disable AbcSize - expected.each do |(bot, controller, route_name, options)| - expected_path = options.delete(:path) || "telegram/#{bot.token}" - expect(mapper).to receive(:post) do |path, params| - expect(path).to eq expected_path - middleware = params[:to] - expect(middleware.controller).to eq(controller) - expect(middleware.bot.token).to eq(bot.token) - expect(middleware.bot.username).to eq(bot.username) - expect(params[:as]).to eq route_name - expect(params).to include(options) if options - end - end - subject - end - - context 'when called with controller' do - let(:input) { [controller, option: :val] } - - it 'creates routes for every bot and this controller' do - assert_routes [bot, controller, 'default_telegram_webhook', option: :val], - [other_bot, controller, 'other_telegram_webhook', option: :val] - end - - context 'and bot does not have configured token' do - let(:bot) { create_bot(nil) } - it 'creates routes for every bot and this controller' do - assert_routes [bot, controller, 'default_telegram_webhook', option: :val], - [other_bot, controller, 'other_telegram_webhook', option: :val] - end - end - - context 'and bot has colon in token' do - let(:bot) { create_bot('some:token') } - it 'replaces colon with underscore' do - assert_routes [ - bot, - controller, - 'default_telegram_webhook', - option: :val, - path: 'telegram/some_token', - ], [other_bot, controller, 'other_telegram_webhook', option: :val] - end - end - end - - context 'when called with hash' do - let(:input) do - [ - { - bot => controller, - 'custom_token' => [other_controller, as: :custom_route, option: :other_val], - other: controller, - }, - option: :val, - ] - end - - it 'creates routes for every bot and its controller' do - assert_routes [bot, controller, 'default_telegram_webhook', option: :val], - [ - create_bot('custom_token'), - other_controller, - :custom_route, - option: :other_val, - ], - [Telegram.bots[:other], controller, 'other_telegram_webhook', option: :val] - end - end - - context 'when called with controller and smth castable to bot' do - let(:input) do - [ - controller, - ['custom_token', token: bot.token, username: 'new_name'], - option: :val, - ] - end - - it 'creates routes for every created bot and controller' do - assert_routes [ - create_bot('custom_token'), - controller, - 'telegram_webhook', - option: :val, - ], [ - create_bot(bot.token, 'new_name'), - controller, - 'telegram_webhook', - option: :val, - ] - end - end - end end diff --git a/spec/telegram/bot/rspec/callback_query_helpers_spec.rb b/spec/telegram/bot/rspec/callback_query_helpers_spec.rb new file mode 100644 index 0000000..f32397e --- /dev/null +++ b/spec/telegram/bot/rspec/callback_query_helpers_spec.rb @@ -0,0 +1,42 @@ +require 'telegram/bot/rspec/integration/poller' + +RSpec.describe 'Integration spec helpers', telegram_bot: :poller do + let(:bot) { Telegram::Bot::ClientStub.new('token') } + let(:controller_class) do + Class.new(Telegram::Bot::UpdatesController) do + include Telegram::Bot::UpdatesController::CallbackQueryContext + + def callback_query(data = nil, *) + answer_callback_query "data: #{data}" + end + + def context_callback_query(data = nil, *) + answer_callback_query "data: #{data}", extra: :param + end + + def answer_and_edit_callback_query(data = nil, *) + answer_callback_query "data: #{data}" + edit_message :text, text: 'edited-text', extra: :param + end + end + end + + describe '#callback_query', :callback_query do + let(:data) { 'unknown:command' } + it { should answer_callback_query("data: #{data}") } + end + + describe '#context_callback_query', :callback_query do + let(:data) { 'context:test:payload' } + it { should answer_callback_query('data: test:payload', extra: :param) } + it { should_not edit_current_message(:text) } + end + + describe '#answer_and_edit_callback_query', :callback_query do + let(:data) { 'answer_and_edit:test:payload' } + it do + should answer_callback_query(/test:payload/). + and edit_current_message(:text, text: /edited/, extra: :param) + end + end +end diff --git a/spec/telegram/bot/rspec/integration/poller_spec.rb b/spec/telegram/bot/rspec/integration/poller_spec.rb new file mode 100644 index 0000000..27608aa --- /dev/null +++ b/spec/telegram/bot/rspec/integration/poller_spec.rb @@ -0,0 +1,5 @@ +require 'telegram/bot/rspec/integration/poller' + +RSpec.describe 'Poller integration spec', telegram_bot: :poller do + include_examples 'shared integration examples' +end diff --git a/spec/telegram/bot/rspec/integration/rack_spec.rb b/spec/telegram/bot/rspec/integration/rack_spec.rb new file mode 100644 index 0000000..89c3622 --- /dev/null +++ b/spec/telegram/bot/rspec/integration/rack_spec.rb @@ -0,0 +1,20 @@ +require 'telegram/bot/rspec/integration/rack' + +RSpec.describe 'Rack integration spec', telegram_bot: :rack do + include_examples 'shared integration examples' + let(:request_path) { '/bot' } + let(:app) do + path = request_path + bot_app = Telegram::Bot::Middleware.new(bot, controller_class) + app = Rack::Builder.new do + map(path) { run bot_app } + run ->(env) { raise "Route is not mapped: #{env['PATH_INFO']}" } + end + if ActionPack::VERSION::MAJOR >= 5 + app + else + require 'action_dispatch/middleware/params_parser' + ActionDispatch::ParamsParser.new(app) + end + end +end diff --git a/spec/telegram/bot/rspec/integration/rails_spec.rb b/spec/telegram/bot/rspec/integration/rails_spec.rb new file mode 100644 index 0000000..05c498e --- /dev/null +++ b/spec/telegram/bot/rspec/integration/rails_spec.rb @@ -0,0 +1 @@ +# Tested in spec/integration diff --git a/spec/telegram/bot/rspec/integration_spec.rb b/spec/telegram/bot/rspec/integration_spec.rb deleted file mode 100644 index 070947a..0000000 --- a/spec/telegram/bot/rspec/integration_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -require 'telegram/bot/rspec/integration' -require 'action_controller' -require 'action_dispatch' -require 'action_dispatch/testing/integration' - -RSpec.describe 'Integrations helper', :telegram_bot do - include ActionDispatch::Integration::Runner - def reset_template_assertion - end - - let(:app) do - app = Telegram::Bot::Middleware.new(bot, controller) - if ActionPack::VERSION::MAJOR >= 5 - app - else - require 'action_dispatch/middleware/params_parser' - ActionDispatch::ParamsParser.new(app) - end - end - let(:bot) { Telegram::Bot::ClientStub.new('token') } - let(:controller_path) { '/' } - let(:controller) do - Class.new(Telegram::Bot::UpdatesController) do - def start(*args) - respond_with :message, text: "Start: #{args.inspect}, option: #{payload[:option]}" - end - end - end - - describe '#default_message_options' do - subject { default_message_options } - it { should eq from: {id: from_id}, chat: {id: chat_id} } - end - - describe '#dispatch' do - subject { -> { dispatch message: {text: '/start', **default_message_options} } } - it { should respond_with_message 'Start: [], option: ' } - end - - describe '#dispatch_message' do - subject { -> { dispatch_message "/start #{args.join ' '}", options } } - let(:args) { %w[asd qwe] } - let(:options) { {} } - it { should respond_with_message "Start: #{args.inspect}, option: " } - - context 'with options' do - let(:options) { {option: 1} } - it { should respond_with_message "Start: #{args.inspect}, option: 1" } - - context 'and chat_id is not set' do - let(:options) { super().merge(chat: nil) } - it { should raise_error(/chat is not present/) } - end - end - end - - describe '#dispatch_command' do - subject { -> { dispatch_command :start, *args } } - let(:args) { [] } - it { should respond_with_message "Start: #{args.inspect}, option: " } - - context 'with args' do - let(:args) { %w[asd qwe] } - it { should respond_with_message "Start: #{args.inspect}, option: " } - end - - context 'with options' do - let(:args) { ['asd', 'qwe', option: 1] } - it { should respond_with_message "Start: #{args[0...-1].inspect}, option: 1" } - end - end - - describe 'callback queries', :callback_query do - let(:controller) do - Class.new(Telegram::Bot::UpdatesController) do - include Telegram::Bot::UpdatesController::CallbackQueryContext - - def callback_query(data = nil, *) - answer_callback_query "data: #{data}" - end - - def context_callback_query(data = nil, *) - answer_callback_query "data: #{data}", extra: :param - end - - def answer_and_edit_callback_query(data = nil, *) - answer_callback_query "data: #{data}" - edit_message :text, text: 'edited-text', extra: :param - end - end - end - - describe '#callback_query' do - let(:data) { 'unknown:command' } - it { should answer_callback_query("data: #{data}") } - end - - describe '#context_callback_query' do - let(:data) { 'context:test:payload' } - it { should answer_callback_query('data: test:payload', extra: :param) } - it { should_not edit_current_message(:text) } - end - - describe '#answer_and_edit_callback_query' do - let(:data) { 'answer_and_edit:test:payload' } - it do - should answer_callback_query(/test:payload/). - and edit_current_message(:text, text: /edited/, extra: :param) - end - end - end -end diff --git a/spec/telegram/bot/rspec/message_helpers_spec.rb b/spec/telegram/bot/rspec/message_helpers_spec.rb new file mode 100644 index 0000000..372783d --- /dev/null +++ b/spec/telegram/bot/rspec/message_helpers_spec.rb @@ -0,0 +1,115 @@ +require 'telegram/bot/rspec/integration/poller' + +RSpec.describe 'Integration: message helpers', telegram_bot: :poller do + describe '#default_message_options' do + subject { default_message_options } + it { should eq from: {id: from_id}, chat: {id: chat_id} } + end + + describe '#dispatch_message' do + subject { -> { dispatch_message text, options } } + let(:text) { '/start asd qwe' } + let(:options) { {} } + let(:result) { double(:result) } + + it 'invokes dispatch' do + expect(self).to receive(:dispatch).with( + message: hash_including(default_message_options.merge( + text: text, + )), + ) { result } + expect(subject.call).to eq result + end + + context 'with options' do + let(:options) { {option: 1} } + it 'invokes dispatch' do + expect(self).to receive(:dispatch).with( + message: hash_including(default_message_options.merge( + text: text, + ).merge(options)), + ) { result } + expect(subject.call).to eq result + end + end + end + + describe '#dispatch_command' do + subject { -> { dispatch_command :start, *args } } + let(:args) { [] } + let(:result) { double(:result) } + + it 'invokes dispatch' do + expect(self).to receive(:dispatch).with( + message: hash_including(default_message_options.merge( + text: '/start', + )), + ) { result } + expect(subject.call).to eq result + end + + context 'with args & options' do + let(:args) { [*params, options] } + let(:params) { %w[qwe asd] } + let(:options) { {option: 1} } + it 'invokes dispatch' do + expect(self).to receive(:dispatch).with( + message: hash_including(default_message_options.merge( + text: "/start #{params.join(' ')}", + ).merge(options)), + ) { result } + expect(subject.call).to eq result + end + end + end +end + +# Old specs +RSpec.describe 'Integration: message helpers', telegram_bot: :poller do + let(:bot) { Telegram::Bot::ClientStub.new('token') } + let(:controller_class) do + Class.new(Telegram::Bot::UpdatesController) do + def start(*args) + respond_with :message, text: "Start: #{args.inspect}, option: #{payload['option']}" + end + end + end + + describe '#default_message_options' do + subject { default_message_options } + it { should eq from: {id: from_id}, chat: {id: chat_id} } + end + + describe '#dispatch_message' do + subject { -> { dispatch_message "/start #{args.join ' '}", options } } + let(:args) { %w[asd qwe] } + let(:options) { {} } + it { should respond_with_message "Start: #{args.inspect}, option: " } + + context 'with options' do + let(:options) { {option: 1} } + it { should respond_with_message "Start: #{args.inspect}, option: 1" } + + context 'and chat_id is not set' do + let(:options) { super().merge(chat: nil) } + it { should raise_error(/chat is not present/) } + end + end + end + + describe '#dispatch_command' do + subject { -> { dispatch_command :start, *args } } + let(:args) { [] } + it { should respond_with_message "Start: #{args.inspect}, option: " } + + context 'with args' do + let(:args) { %w[asd qwe] } + it { should respond_with_message "Start: #{args.inspect}, option: " } + end + + context 'with options' do + let(:args) { ['asd', 'qwe', option: 1] } + it { should respond_with_message "Start: #{args[0...-1].inspect}, option: 1" } + end + end +end diff --git a/spec/telegram/bot/updates_controller/callback_query_context_spec.rb b/spec/telegram/bot/updates_controller/callback_query_context_spec.rb index c4e5588..757e939 100644 --- a/spec/telegram/bot/updates_controller/callback_query_context_spec.rb +++ b/spec/telegram/bot/updates_controller/callback_query_context_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Telegram::Bot::UpdatesController::CallbackQueryContext do describe '#dispatch' do subject { -> { dispatch } } let(:payload_type) { :callback_query } - let(:payload) { {'data' => data} } + let(:payload) { {data: data} } let(:data) { text } let(:text) { 'asd qwe zxc' } diff --git a/spec/telegram/bot/updates_controller/instrumentation_spec.rb b/spec/telegram/bot/updates_controller/instrumentation_spec.rb index e7f588b..c8a7258 100644 --- a/spec/telegram/bot/updates_controller/instrumentation_spec.rb +++ b/spec/telegram/bot/updates_controller/instrumentation_spec.rb @@ -2,9 +2,7 @@ RSpec.describe Telegram::Bot::UpdatesController::Instrumentation do include_context 'telegram/bot/updates_controller' subject { -> { dispatch } } - let(:update) do - build_update :message, default_message_options.merge(text: '/start') - end + let(:update) { {message: default_message_options.merge(text: '/start')} } let(:controller_class) do Class.new(Telegram::Bot::UpdatesController) do @@ -36,11 +34,11 @@ RSpec.describe Telegram::Bot::UpdatesController::Instrumentation do action = action_name(:start_processing) expect(events[action].size).to eq(1) - expect(events[action][0].last).to include(update: update) + expect(events[action][0].last).to include(update: deep_stringify(update)) action = action_name(:process_action) expect(events[action].size).to eq(1) - expect(events[action][0].last).to include(update: update) + expect(events[action][0].last).to include(update: deep_stringify(update)) end end diff --git a/spec/telegram/bot/updates_controller/reply_helpers_spec.rb b/spec/telegram/bot/updates_controller/reply_helpers_spec.rb index d5f2e0c..e5ad3b8 100644 --- a/spec/telegram/bot/updates_controller/reply_helpers_spec.rb +++ b/spec/telegram/bot/updates_controller/reply_helpers_spec.rb @@ -4,8 +4,8 @@ RSpec.describe Telegram::Bot::UpdatesController do let(:respond_type) { :photo } let(:result) { double(:result) } let(:payload_type) { :message } - let(:payload) { {message_id: double(:message_id)} } - let(:chat) { {'id' => double(:chat_id)} } + let(:payload) { {message_id: double(:message_id), chat: chat} } + let(:chat) { {id: double(:chat_id)} } shared_examples 'missing chat' do context 'when chat is missing' do @@ -19,9 +19,8 @@ RSpec.describe Telegram::Bot::UpdatesController do include_examples 'missing chat' it 'sets chat_id & reply_to_message_id' do - expect(controller).to receive(:chat) { chat } expect(bot).to receive("send_#{respond_type}"). - with(params.merge(chat_id: chat['id'])) { result } + with(params.merge(chat_id: chat[:id])) { result } should eq result end end @@ -31,19 +30,18 @@ RSpec.describe Telegram::Bot::UpdatesController do include_examples 'missing chat' it 'sets chat_id & reply_to_message_id' do - expect(controller).to receive(:chat) { chat } expect(bot).to receive("send_#{respond_type}").with(params.merge( - chat_id: chat['id'], + chat_id: chat[:id], reply_to_message_id: payload[:message_id], )) { result } should eq result end context 'when update is not set' do - let(:update) { {chat: chat} } + let(:controller_args) { [bot, chat: deep_stringify(chat)] } it 'sets chat_id' do expect(bot).to receive("send_#{respond_type}"). - with(params.merge(chat_id: chat['id'])) { result } + with(params.merge(chat_id: chat[:id])) { result } should eq result end end @@ -70,7 +68,7 @@ RSpec.describe Telegram::Bot::UpdatesController do it 'sets chat_id & message_id' do expect(bot).to receive("edit_message_#{type}").with(params.merge( message_id: payload[:message][:message_id], - chat_id: payload[:message][:chat]['id'], + chat_id: payload[:message][:chat][:id], )) { result } should eq result end diff --git a/spec/telegram/bot/updates_controller/session_spec.rb b/spec/telegram/bot/updates_controller/session_spec.rb index 103fd56..97406fb 100644 --- a/spec/telegram/bot/updates_controller/session_spec.rb +++ b/spec/telegram/bot/updates_controller/session_spec.rb @@ -36,7 +36,7 @@ RSpec.describe Telegram::Bot::UpdatesController::Session do end def build_message(text, from) - {'message' => {'text' => text, 'from' => from.stringify_keys}} + deep_stringify(message: {text: text, from: from}) end it 'stores session between requests' do diff --git a/spec/telegram/bot/updates_controller_spec.rb b/spec/telegram/bot/updates_controller_spec.rb index 4389dfb..93550ad 100644 --- a/spec/telegram/bot/updates_controller_spec.rb +++ b/spec/telegram/bot/updates_controller_spec.rb @@ -278,11 +278,13 @@ RSpec.describe Telegram::Bot::UpdatesController do instance_eval(&block) context 'when re-initialized' do let(:controller) do - described_class.new(double(:other_bot), build_update(:message, + initial_update = deep_stringify message: { text: 'original message', from: double(:original_from), chat: double(:original_chat), - )).tap { |x| x.send(:initialize, bot, update) } + } + described_class.new(double(:other_bot), initial_update). + tap { |x| x.send(:initialize, *controller_args) } end instance_eval(&block) end @@ -300,7 +302,7 @@ RSpec.describe Telegram::Bot::UpdatesController do end context 'when options hash is given' do - let(:update) { {from: from, chat: chat} } + let(:controller_args) { [bot, from: from, chat: chat] } with_reinitialize do its(:bot) { should eq bot } its(:update) { should eq nil }