Skip to content
Open
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
33 changes: 33 additions & 0 deletions app/controllers/messages/boosts/by_bots_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class Messages::Boosts::ByBotsController < ApplicationController
allow_bot_access only: :create

def create
set_message
@boost = @message.boosts.create!(content: read_body)

broadcast_create
head :created
rescue ActiveRecord::RecordNotFound
head :not_found
end

private
def set_message
@room = Current.user.rooms.find(params[:room_id])
@message = @room.messages.find(params[:message_id])
end

def read_body
request.body.rewind
request.body.read.force_encoding("UTF-8")
ensure
request.body.rewind
end

def broadcast_create
@boost.broadcast_append_to @boost.message.room, :messages,
target: "boosts_message_#{@boost.message.client_message_id}",
partial: "messages/boosts/boost",
attributes: { maintain_scroll: true }
end
end
53 changes: 51 additions & 2 deletions app/controllers/messages/by_bots_controller.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,61 @@
class Messages::ByBotsController < MessagesController
allow_bot_access only: :create
allow_bot_access only: %i[ index create ]

def index
set_room
@messages = find_paged_messages
render json: messages_as_json(@messages)
rescue ActiveRecord::RecordNotFound
head :not_found
end

def create
super
set_room
@message = @room.messages.create_with_attachment!(message_params)
@message.broadcast_create
deliver_webhooks_to_bots
head :created, location: message_url(@message)
rescue ActiveRecord::RecordNotFound
head :not_found
end

private
def messages_as_json(messages)
{
room: {
id: @room.id,
name: @room.name
},
messages: messages.map { |m| message_as_json(m) },
pagination: pagination_info(messages)
}
end

def message_as_json(message)
{
id: message.id,
body: {
plain: message.plain_text_body,
html: message.body&.body&.to_s
},
created_at: message.created_at.iso8601,
creator: {
id: message.creator.id,
name: message.creator.name,
is_bot: message.creator.bot?
}
}
end

def pagination_info(messages)
return {} if messages.empty?
{
oldest_id: messages.last.id,
newest_id: messages.first.id,
has_more: messages.size == Message::PAGE_SIZE
}
end

def message_params
if params[:attachment]
params.permit(:attachment)
Expand Down
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@
resources :rooms do
resources :messages

# Bot API endpoints - authenticated via bot_key in URL
get ":bot_key/messages", to: "messages/by_bots#index", as: :bot_messages_index
post ":bot_key/messages", to: "messages/by_bots#create", as: :bot_messages
post ":bot_key/messages/:message_id/boosts", to: "messages/boosts/by_bots#create", as: :bot_message_boosts

scope module: "rooms" do
resource :refresh, only: :show
Expand Down
76 changes: 76 additions & 0 deletions test/controllers/messages/boosts/by_bots_controller_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
require "test_helper"

class Messages::Boosts::ByBotsControllerTest < ActionDispatch::IntegrationTest
setup do
@room = rooms(:watercooler)
@message = messages(:fourth) # Message in watercooler room where bender bot is a member
@bot = users(:bender)
end

test "create adds a boost to the message" do
assert_difference -> { @message.boosts.count }, +1 do
post room_bot_message_boosts_url(@room, @bot.bot_key, @message), params: +"👀"
assert_response :created
end

assert_equal "👀", @message.boosts.last.content
end

test "create with emoji reaction" do
assert_difference -> { Boost.count }, +1 do
post room_bot_message_boosts_url(@room, @bot.bot_key, @message), params: +"🎉"
assert_response :created
end
end

test "create with text reaction" do
assert_difference -> { Boost.count }, +1 do
post room_bot_message_boosts_url(@room, @bot.bot_key, @message), params: +"Nice!"
assert_response :created
end

assert_equal "Nice!", @message.boosts.last.content
end

test "create broadcasts the boost" do
assert_turbo_stream_broadcasts [ @message.room, :messages ], count: 1 do
post room_bot_message_boosts_url(@room, @bot.bot_key, @message), params: +"👍"
end
end

test "create requires valid bot key" do
assert_no_difference -> { Boost.count } do
post room_bot_message_boosts_url(@room, "invalid-bot-key", @message), params: +"👀"
end
assert_response :redirect # Redirects to login
end

test "create returns not_found for room bot is not a member of" do
room_without_bot = rooms(:designers)
message_in_other_room = messages(:first) # Message in designers room

assert_no_difference -> { Boost.count } do
post room_bot_message_boosts_url(room_without_bot, @bot.bot_key, message_in_other_room), params: +"👀"
end
assert_response :not_found
end

test "create returns not_found for message not in the room" do
message_in_other_room = messages(:first) # Message in designers room, not watercooler

assert_no_difference -> { Boost.count } do
post room_bot_message_boosts_url(@room, @bot.bot_key, message_in_other_room), params: +"👀"
end
assert_response :not_found
end

test "create can't be abused to post boosts as regular user" do
user = users(:kevin)
bot_key = "#{user.id}-"

assert_no_difference -> { Boost.count } do
post room_bot_message_boosts_url(@room, bot_key, @message), params: +"👀"
end
assert_response :redirect
end
end
74 changes: 72 additions & 2 deletions test/controllers/messages/by_bots_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,78 @@ class Messages::ByBotsControlleTest < ActionDispatch::IntegrationTest
assert_response :redirect
end

test "denied index" do
get room_messages_url(@room, bot_key: users(:bender).bot_key, format: :json)
test "index returns messages as JSON" do
get room_bot_messages_index_url(@room, users(:bender).bot_key)
assert_response :success

json = JSON.parse(response.body)
assert json["room"]["id"].present?
assert json["room"]["name"].present?
assert json["messages"].is_a?(Array)
assert json["pagination"].present?
end

test "index includes message details" do
# Create a message in the room first
post room_bot_messages_url(@room, users(:bender).bot_key), params: +"Test message for index"

get room_bot_messages_index_url(@room, users(:bender).bot_key)
assert_response :success

json = JSON.parse(response.body)
message = json["messages"].find { |m| m["body"]["plain"] == "Test message for index" }
assert message.present?, "Expected to find the test message"
assert message["id"].present?
assert message["created_at"].present?
assert message["creator"]["id"].present?
assert message["creator"]["name"].present?
end

test "index supports pagination with before parameter" do
# Use a message from the watercooler room (where bender is a member)
message_in_room = messages(:thirteenth) # Latest message in watercooler
get room_bot_messages_index_url(@room, users(:bender).bot_key, before: message_in_room.id)
assert_response :success
end

test "index supports pagination with after parameter" do
# Use a message from the watercooler room (where bender is a member)
message_in_room = messages(:fourth) # First message in watercooler
get room_bot_messages_index_url(@room, users(:bender).bot_key, after: message_in_room.id)
assert_response :success
end
Comment on lines +88 to +93
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test uses Message.first.id which is 1, but message 1 belongs to the 'designers' room, not the 'watercooler' room (@room). When the controller tries to find this message in the watercooler room, it will raise a RecordNotFound error. Use a message ID that actually belongs to the watercooler room, such as creating a message in setup or using a message ID from the watercooler fixtures.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit c82a8d3 - now using messages(:fourth) and messages(:thirteenth) which are fixtures belonging to the watercooler room where the bender bot is a member.


test "index requires valid bot key" do
get room_bot_messages_index_url(@room, "invalid-bot-key")
assert_response :redirect # Redirects to login
end

test "index returns not_found for room bot is not a member of" do
# bender bot is NOT a member of the designers room
room_without_bot = rooms(:designers)
get room_bot_messages_index_url(room_without_bot, users(:bender).bot_key)
assert_response :not_found
end

test "index works for room bot IS a member of" do
# bender bot IS a member of watercooler
room_with_bot = rooms(:watercooler)
get room_bot_messages_index_url(room_with_bot, users(:bender).bot_key)
assert_response :success
end

test "create returns not_found for room bot is not a member of" do
# bender bot is NOT a member of the designers room - verify create matches index behavior
room_without_bot = rooms(:designers)
assert_no_difference -> { Message.count } do
post room_bot_messages_url(room_without_bot, users(:bender).bot_key), params: +"Hello!"
end
assert_response :not_found
end

test "regular messages index still denied for bots" do
# The standard messages endpoint (not the bot-specific one) should still be forbidden
get room_messages_url(@room, bot_key: users(:bender).bot_key)
assert_response :forbidden
end
end
Loading