Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/slack/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Slack::ApplicationController < ApplicationController
private

def valid_slack_request?
@verified ||= verify_slack_signature
head :unauthorized and return unless verify_slack_signature
end

def verify_slack_signature
Expand Down
56 changes: 56 additions & 0 deletions test/controllers/slack/puzzles_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
require "test_helper"

class Slack::PuzzlesControllerTest < ActionDispatch::IntegrationTest
setup do
ENV["SLACK_SIGNING_SECRET"] = "test_signing_secret"
end

test "rejects request with invalid Slack signature" do
post slack_puzzle_path, params: { payload: puzzle_payload },
headers: slack_headers(secret: "wrong_secret")

assert_response :unauthorized
end

test "rejects request with expired Slack timestamp" do
post slack_puzzle_path, params: { payload: puzzle_payload },
headers: slack_headers(timestamp: Time.now.to_i - 400)

assert_response :unauthorized
end

test "allows request with valid Slack signature" do
# Use a payload that fails model validation so no Slack API call is made,
# but the controller still renders 200 (its behavior on both save success/failure).
params = { payload: puzzle_payload(question: "") }

post slack_puzzle_path, params: params,
headers: slack_headers(body: params.to_query)

assert_response :ok
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