diff --git a/Gemfile.lock b/Gemfile.lock index 98d163e..c040098 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -101,7 +101,7 @@ GEM bootstrap-sass (3.4.1) autoprefixer-rails (>= 5.2.1) sassc (>= 2.0.0) - brakeman (8.0.2) + brakeman (8.0.4) racc builder (3.3.0) capybara (3.40.0) diff --git a/app/controllers/slack/application_controller.rb b/app/controllers/slack/application_controller.rb index e587022..43a5783 100644 --- a/app/controllers/slack/application_controller.rb +++ b/app/controllers/slack/application_controller.rb @@ -43,7 +43,8 @@ def open_view(view, trigger_id:) def send_message(message, channel_id:) SlackClient::Client.instance.chat_postMessage(channel: channel_id, blocks: message) - rescue Slack::Web::Api::Errors::SlackError - head :unprocessable_entity + rescue Slack::Web::Api::Errors::SlackError => e + Rails.logger.error "Failed to send Slack message: #{e.message}" + Sentry.capture_exception(e) end end diff --git a/test/controllers/slack/puzzles_controller_test.rb b/test/controllers/slack/puzzles_controller_test.rb new file mode 100644 index 0000000..ee183e8 --- /dev/null +++ b/test/controllers/slack/puzzles_controller_test.rb @@ -0,0 +1,55 @@ +require "test_helper" + +class Slack::PuzzlesControllerTest < ActionDispatch::IntegrationTest + setup do + ENV["SLACK_SIGNING_SECRET"] = "test_signing_secret" + end + + test "renders ok when puzzle fails validation" do + params = { payload: puzzle_payload(question: "") } + + post slack_puzzle_path, params: params, + headers: slack_headers(body: params.to_query) + + assert_response :ok + end + + test "renders ok even when Slack notification fails after puzzle is saved" do + # Notification failure should only log — it must not affect the response to Slack. + original = Slack::ApplicationController.instance_method(:send_message) + Slack::ApplicationController.define_method(:send_message) { |*| nil } + + params = { payload: puzzle_payload(question: "What is unique about Ruby's blocks?") } + + post slack_puzzle_path, params: params, + headers: slack_headers(body: params.to_query) + + assert_response :ok + ensure + Slack::ApplicationController.define_method(:send_message, original) + end + + private + + def puzzle_payload(question: "What is Ruby?") + { + user: { id: "U123" }, + view: { + state: { + values: { + question: { question: { value: question } }, + answer: { answer: { selected_option: { value: "ruby" } } }, + explanation: { explanation: { value: "It is a programming language." } }, + link: { link: { value: nil } } + } + } + } + }.to_json + end + + def slack_headers(secret: ENV["SLACK_SIGNING_SECRET"], timestamp: Time.now.to_i, body: "") + ts = timestamp.to_s + sig = "v0=" + OpenSSL::HMAC.hexdigest("SHA256", secret, "v0:#{ts}:#{body}") + { "X-Slack-Request-Timestamp" => ts, "X-Slack-Signature" => sig } + end +end