Hello world

First open source release of Campfire 🎉
This commit is contained in:
Kevin McConnell committed 2025-08-21 09:31:59 +01:00
commit df76a227dc
664 files changed
+36235

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
require "test_helper"
WebMock.disable!
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ]
include SystemTestHelper
end
@@ -0,0 +1,21 @@
require "test_helper"
class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
test "connects with valid user_id cookie" do
cookies.signed[:session_token] = sessions(:david_safari).token
connect
assert_equal users(:david), connection.current_user
end
test "rejects connection with missing user_id cookie" do
assert_reject_connection { connect }
end
test "rejects connection with invalid user_id cookie" do
cookies.signed[:session_token] = -1
assert_reject_connection { connect }
end
end
+51
View File
@@ -0,0 +1,51 @@
require "test_helper"
class PresenceChannelTest < ActionCable::Channel::TestCase
setup do
stub_connection(current_user: users(:david))
end
test "subscribes" do
room = users(:david).rooms.first
subscribe room_id: room.id
assert subscription.confirmed?
assert_has_stream_for room
end
test "rejects subscription to a room that the user is not a member of" do
subscribe room_id: Rooms::Closed.create!(name: "New Room", creator: users(:david)).id
assert subscription.rejected?
end
test "rejects subscription to non-existent room" do
subscribe room_id: -1
assert subscription.rejected?
end
test "rejects subscription without a room" do
subscribe room_id: -1
assert subscription.rejected?
end
test "subscribing marks the membership as connected" do
membership = users(:david).memberships.first
assert_changes -> { membership.reload.connected? }, from: false, to: true do
subscribe room_id: membership.room_id
end
end
test "unsubscribing marks the membership as disconnected" do
membership = users(:david).memberships.first
subscribe room_id: membership.room_id
assert_changes -> { membership.reload.connected? }, from: true, to: false do
unsubscribe
end
end
end
View File
Whitespace-only changes.
@@ -0,0 +1,14 @@
require "test_helper"
class Accounts::Bots::KeysControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "update" do
assert_changes -> { users(:bender).reload.bot_token } do
put account_bot_key_url(users(:bender))
assert_redirected_to account_bots_url
end
end
end
@@ -0,0 +1,45 @@
require "test_helper"
class Accounts::BotsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "index" do
get account_bots_url
assert_response :ok
end
test "create" do
get new_account_bot_url
assert_response :ok
post account_bots_url, params: { user: { name: "Bender's Friend" } }
assert_redirected_to account_bots_url
assert_equal "Bender's Friend", User.bot.last.name
end
test "update" do
get edit_account_bot_url(users(:bender))
assert_response :ok
put account_bot_url(users(:bender)), params: { user: { name: "Bender's New Friend" } }
assert_redirected_to account_bots_url
assert_equal "Bender's New Friend", users(:bender).reload.name
end
test "destroy" do
assert_difference -> { User.active_bots.count }, -1 do
delete account_bot_url(users(:bender))
end
assert users(:bender).reload.deactivated?
end
test "remove webhook" do
assert_difference -> { Webhook.count }, -1 do
put account_bot_url(users(:bender)), params: { user: { name: "Bender's New Friend", webook_url: "" } }
assert_redirected_to account_bots_url
end
end
end
@@ -0,0 +1,29 @@
require "test_helper"
class Accounts::CustomStylesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "edit" do
get edit_account_custom_styles_url
assert_response :ok
end
test "update" do
assert users(:david).administrator?
put account_custom_styles_url, params: { account: { custom_styles: ":root { --color-text: red; }" } }
assert_redirected_to edit_account_custom_styles_url
assert_equal accounts(:signal).custom_styles, ":root { --color-text: red; }"
end
test "non-admins cannot update" do
sign_in :kevin
assert users(:kevin).member?
put account_custom_styles_url, params: { account: { custom_styles: ":root { --color-text: red; }" } }
assert_response :forbidden
end
end
@@ -0,0 +1,20 @@
require "test_helper"
class Accounts::JoinCodesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "create new join code" do
assert_changes -> { accounts(:signal).reload.join_code } do
post account_join_code_url
assert_redirected_to edit_account_url
end
end
test "only administrators can create new join codes" do
sign_in :jz
post account_join_code_url
assert_response :forbidden
end
end
@@ -0,0 +1,49 @@
require "test_helper"
require "vips"
class Accounts::LogosControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "show stock" do
get account_logo_url
assert_valid_png_response size: 512
end
test "show stock small size" do
get account_logo_url(size: :small)
assert_valid_png_response size: 192
end
test "show custom" do
accounts(:signal).update! logo: fixture_file_upload("moon.jpg", "image/jpeg")
get account_logo_url
assert_valid_png_response size: 512
end
test "show custom small size" do
accounts(:signal).update! logo: fixture_file_upload("moon.jpg", "image/jpeg")
get account_logo_url(size: :small)
assert_valid_png_response size: 192
end
test "destroy" do
accounts(:signal).update! logo: fixture_file_upload("moon.jpg", "image/jpeg")
delete account_logo_url
assert_redirected_to edit_account_url
assert_not accounts(:signal).reload.logo.attached?
end
private
def assert_valid_png_response(size:)
assert_equal @response.headers["content-type"], "image/png"
image = ::Vips::Image.new_from_buffer(@response.body, "")
assert_equal size, image.width
assert_equal size, image.height
end
end
@@ -0,0 +1,35 @@
require "test_helper"
class Accounts::UsersControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "update" do
assert users(:david).administrator?
put account_user_url(users(:david)), params: { user: { role: "administrator" } }
assert_redirected_to edit_account_url
assert users(:david).reload.administrator?
end
test "destroy" do
assert_difference -> { User.active.count }, -1 do
delete account_user_url(users(:david))
end
assert_redirected_to edit_account_url
assert_nil User.active.find_by(id: users(:david).id)
end
test "non-admins cannot perform actions" do
sign_in :kevin
put account_user_url(users(:david)), params: { user: { role: "administrator" } }
assert_response :forbidden
delete account_user_url(users(:david))
assert_response :forbidden
end
end
@@ -0,0 +1,29 @@
require "test_helper"
class AccountsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "edit" do
get edit_account_url
assert_response :ok
end
test "update" do
assert users(:david).administrator?
put account_url, params: { account: { name: "Different" } }
assert_redirected_to edit_account_url
assert_equal accounts(:signal).name, "Different"
end
test "non-admins cannot update" do
sign_in :kevin
assert users(:kevin).member?
put account_url, params: { account: { name: "Different" } }
assert_response :forbidden
end
end
@@ -0,0 +1,40 @@
require "test_helper"
class Autocompletable::UsersControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "search returns matching users" do
get autocompletable_users_url(format: :json), params: { query: "da" }
assert_response :success
assert_equal "David", response.parsed_body.first["name"]
end
test "search results escape HTML in names" do
users(:david).update!(name: "David <script>alert(123)</script>")
get autocompletable_users_url(format: :json), params: { query: "da" }
assert_response :success
assert_equal "David &lt;script&gt;alert(123)&lt;/script&gt;", response.parsed_body.first["name"]
end
test "room search returns matching users" do
get autocompletable_users_url(room_id: rooms(:hq).id, format: :json), params: { query: "da" }
assert_response :success
assert_equal "David", response.parsed_body.first["name"]
end
test "room search is scoped by membership" do
sign_in :kevin
assert_not_includes users(:kevin).rooms, rooms(:watercooler)
assert_raises ActiveRecord::RecordNotFound do
get autocompletable_users_url(room_id: rooms(:watercooler).id, format: :json), params: { query: "da" }
end
end
end
@@ -0,0 +1,33 @@
require "test_helper"
class FirstRunsControllerTest < ActionDispatch::IntegrationTest
setup do
Account.destroy_all
User.destroy_all
Room.destroy_all
end
test "new is permitted when no other users exit" do
get first_run_url
assert_response :success
end
test "new is not permitted when account exist" do
Account.create!(name: "Chat")
get first_run_url
assert_redirected_to root_url
end
test "create" do
assert_difference -> { Room.count }, 1 do
assert_difference -> { User.count }, 1 do
post first_run_url, params: { account: { name: "37signals" }, user: { name: "New Person", email_address: "new@37signals.com", password: "secret123456" } }
end
end
assert_redirected_to root_url
assert parsed_cookies.signed[:session_token]
end
end
@@ -0,0 +1,26 @@
require "test_helper"
class Messages::BoostsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
@message = messages(:first)
end
test "create" do
assert_turbo_stream_broadcasts [ @message.room, :messages ], count: 1 do
assert_difference -> { @message.boosts.count }, 1 do
post message_boosts_url(@message, format: :turbo_stream), params: { boost: { content: "Morning!" } }
assert_redirected_to message_boosts_url(@message)
end
end
end
test "destroy" do
assert_turbo_stream_broadcasts [ @message.room, :messages ], count: 1 do
assert_difference -> { @message.boosts.count }, -1 do
delete message_boost_url(@message, boosts(:first), format: :turbo_stream)
assert_response :success
end
end
end
end
@@ -0,0 +1,47 @@
require "test_helper"
class Messages::ByBotsControlleTest < ActionDispatch::IntegrationTest
setup do
@room = rooms(:watercooler)
end
test "create" do
assert_difference -> { Message.count }, +1 do
post room_bot_messages_url(@room, users(:bender).bot_key), params: "Hello Bot World!"
assert_equal "Hello Bot World!", Message.last.plain_text_body
end
end
test "create with UTF-8 content" do
assert_difference -> { Message.count }, +1 do
post room_bot_messages_url(@room, users(:bender).bot_key), params: "Hello 👋!"
assert_equal "Hello 👋!", Message.last.plain_text_body
end
end
test "create file" do
assert_difference -> { Message.count }, +1 do
post room_bot_messages_url(@room, users(:bender).bot_key), params: { attachment: fixture_file_upload("moon.jpg", "image/jpeg") }
assert Message.last.attachment.present?
end
end
test "create does not trigger a webhook to the sending bot if it mentions itself" do
body = "<div>Hey #{mention_attachment_for(:bender)}</div>"
assert_no_enqueued_jobs only: Bot::WebhookJob do
post room_bot_messages_url(@room, users(:bender).bot_key), params: body
end
end
test "create does not trigger a webhook to the sending bot in a direct room" do
assert_no_enqueued_jobs only: Bot::WebhookJob do
post room_bot_messages_url(rooms(:bender_and_kevin), users(:bender).bot_key), params: "Talking to myself again!"
end
end
test "denied index" do
get room_messages_url(@room, bot_key: users(:bender).bot_key, format: :json)
assert_response :forbidden
end
end
@@ -0,0 +1,152 @@
require "test_helper"
class MessagesControllerTest < ActionDispatch::IntegrationTest
setup do
host! "once.campfire.test"
sign_in :david
@room = rooms(:watercooler)
@messages = @room.messages.ordered.to_a
end
test "index returns the last page by default" do
get room_messages_url(@room)
assert_response :success
ensure_messages_present @messages.last
end
test "index returns a page before the specified message" do
get room_messages_url(@room, before: @messages.third)
assert_response :success
ensure_messages_present @messages.first, @messages.second
ensure_messages_not_present @messages.third, @messages.fourth, @messages.fifth
end
test "index returns a page after the specified message" do
get room_messages_url(@room, after: @messages.third)
assert_response :success
ensure_messages_present @messages.fourth, @messages.fifth
ensure_messages_not_present @messages.first, @messages.second, @messages.third
end
test "index returns no_content when there are no messages" do
@room.messages.destroy_all
get room_messages_url(@room)
assert_response :no_content
end
test "get renders a single message belonging to the user" do
message = @room.messages.where(creator: users(:david)).first
get room_message_url(@room, message)
assert_response :success
end
test "creating a message broadcasts the message to the room" do
post room_messages_url(@room, format: :turbo_stream), params: { message: { body: "New one", client_message_id: 999 } }
assert_rendered_turbo_stream_broadcast @room, :messages, action: "append", target: [ @room, :messages ] do
assert_select ".message__body", text: /New one/
assert_copy_link_button room_at_message_url(@room, Message.last, host: "once.campfire.test")
end
end
test "creating a message broadcasts unread room" do
assert_broadcasts "unread_rooms", 1 do
post room_messages_url(@room, format: :turbo_stream), params: { message: { body: "New one", client_message_id: 999 } }
end
end
test "update updates a message belonging to the user" do
message = @room.messages.where(creator: users(:david)).first
Turbo::StreamsChannel.expects(:broadcast_replace_to).once
put room_message_url(@room, message), params: { message: { body: "Updated body" } }
assert_redirected_to room_message_url(@room, message)
assert_equal "Updated body", message.reload.plain_text_body
end
test "admin updates a message belonging to another user" do
message = @room.messages.where(creator: users(:jason)).first
Turbo::StreamsChannel.expects(:broadcast_replace_to).once
put room_message_url(@room, message), params: { message: { body: "Updated body" } }
assert_redirected_to room_message_url(@room, message)
assert_equal "Updated body", message.reload.plain_text_body
end
test "destroy destroys a message belonging to the user" do
message = @room.messages.where(creator: users(:david)).first
assert_difference -> { Message.count }, -1 do
Turbo::StreamsChannel.expects(:broadcast_remove_to).once
delete room_message_url(@room, message, format: :turbo_stream)
assert_response :success
end
end
test "admin destroy destroys a message belonging to another user" do
assert users(:david).administrator?
message = @room.messages.where(creator: users(:jason)).first
assert_difference -> { Message.count }, -1 do
Turbo::StreamsChannel.expects(:broadcast_remove_to).once
delete room_message_url(@room, message, format: :turbo_stream)
assert_response :success
end
end
test "ensure non-admin can't update a message belonging to another user" do
sign_in :jz
assert_not users(:jz).administrator?
room = rooms(:designers)
message = room.messages.where(creator: users(:jason)).first
put room_message_url(room, message), params: { message: { body: "Updated body" } }
assert_response :forbidden
end
test "ensure non-admin can't destroy a message belonging to another user" do
sign_in :jz
assert_not users(:jz).administrator?
room = rooms(:designers)
message = room.messages.where(creator: users(:jason)).first
delete room_message_url(room, message, format: :turbo_stream)
assert_response :forbidden
end
test "mentioning a bot triggers a webhook" do
WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200)
assert_enqueued_jobs 1, only: Bot::WebhookJob do
post room_messages_url(@room, format: :turbo_stream), params: { message: {
body: "<div>Hey #{mention_attachment_for(:bender)}</div>", client_message_id: 999 } }
end
end
private
def ensure_messages_present(*messages, count: 1)
messages.each do |message|
assert_select "#" + dom_id(message), count:
end
end
def ensure_messages_not_present(*messages)
ensure_messages_present *messages, count: 0
end
def assert_copy_link_button(url)
assert_select ".btn[title='Copy link'][data-copy-to-clipboard-content-value='#{url}']"
end
end
@@ -0,0 +1,15 @@
require "test_helper"
class QrCodeControllerTest < ActionDispatch::IntegrationTest
test "show renders a QR code as a cacheable SVG image" do
id = Base64.urlsafe_encode64("http://example.com")
get qr_code_path(id)
assert_response :success
assert_includes response.content_type, "image/svg+xml"
assert_equal 1.year, response.cache_control[:max_age].to_i
assert response.cache_control[:public]
end
end
@@ -0,0 +1,68 @@
require "test_helper"
class Rooms::ClosedsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "show redirects to get general show" do
get rooms_open_url(users(:david).rooms.closeds.last)
assert_redirected_to room_url(users(:david).rooms.closeds.last)
end
test "new" do
get new_rooms_closed_url
assert_response :success
end
test "create" do
assert_turbo_stream_broadcasts [ users(:david), :rooms ], count: 1 do
assert_turbo_stream_broadcasts [ users(:kevin), :rooms ], count: 1 do
assert_turbo_stream_broadcasts [ users(:jason), :rooms ], count: 1 do
post rooms_closeds_url, params: { room: { name: "My New Room" }, user_ids: [ users(:david).id, users(:kevin).id, users(:jason).id ] }
end
end
end
new_room = Room.last
assert_equal new_room.memberships.count, 3
assert_redirected_to room_url(Room.last)
end
test "update with membership revisions" do
assert_difference -> { rooms(:designers).reload.users.count }, -1 do
put rooms_closed_url(rooms(:designers)), params: {
room: { name: "New Name" }, user_ids: rooms(:designers).users.without(users(:jason)).collect(&:id)
}
end
assert_redirected_to room_url(rooms(:designers))
assert rooms(:designers).reload.name, "New Name"
end
test "update an open room to be closed" do
put rooms_closed_url(rooms(:pets)), params: { room: { name: "Doesn't matter" }, user_ids: [ users(:david).id, users(:jason).id ] }
assert_equal rooms(:pets).memberships.count, 2
end
test "only admins or creators can update" do
sign_in :jz
assert_turbo_stream_broadcasts :rooms, count: 0 do
put rooms_closed_url(rooms(:designers)), params: { room: { name: "New Name" } }
end
assert_response :forbidden
assert rooms(:designers).reload.name, "Designers"
end
test "remove yourself" do
assert_difference -> { users(:david).rooms.count }, -1 do
put rooms_closed_url(rooms(:designers), params: { room: { name: "Designers" }, user_ids: [ users(:jason).id, users(:jz).id ] })
assert_redirected_to room_url(rooms(:designers))
follow_redirect!
assert_redirected_to root_url
end
end
end
@@ -0,0 +1,32 @@
require "test_helper"
class Rooms::DirectsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "create" do
post rooms_directs_url, params: { user_ids: [ users(:jz).id ] }
room = Room.last
assert_redirected_to room_url(room)
assert room.users.include?(users(:david))
assert room.users.include?(users(:jz))
end
test "create only once per user set" do
assert_difference -> { Room.all.count }, +1 do
post rooms_directs_url, params: { user_ids: [ users(:jz).id ] }
post rooms_directs_url, params: { user_ids: [ users(:jz).id ] }
end
end
test "destroy only allowed for all room users" do
sign_in :kevin
assert_difference -> { Room.count }, -1 do
delete rooms_direct_url(rooms(:david_and_kevin))
assert_redirected_to root_url
end
end
end
@@ -0,0 +1,46 @@
require "test_helper"
class Rooms::InvolvementsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "show" do
get room_involvement_url(rooms(:designers))
assert_response :success
end
test "update involvement sends turbo update when becoming visible and when going invisible" do
assert_turbo_stream_broadcasts [ users(:david), :rooms ], count: 1 do
assert_changes -> { memberships(:david_watercooler).reload.involvement }, from: "everything", to: "invisible" do
put room_involvement_url(rooms(:watercooler)), params: { involvement: "invisible" }
assert_redirected_to room_involvement_url(rooms(:watercooler))
end
end
assert_turbo_stream_broadcasts [ users(:david), :rooms ], count: 2 do
assert_changes -> { memberships(:david_watercooler).reload.involvement }, from: "invisible", to: "everything" do
put room_involvement_url(rooms(:watercooler)), params: { involvement: "everything" }
assert_redirected_to room_involvement_url(rooms(:watercooler))
end
end
end
test "updating involvement does not send turbo update changing visible states" do
assert_no_turbo_stream_broadcasts [ users(:david), :rooms ] do
assert_changes -> { memberships(:david_watercooler).reload.involvement }, from: "everything", to: "mentions" do
put room_involvement_url(rooms(:watercooler)), params: { involvement: "mentions" }
assert_redirected_to room_involvement_url(rooms(:watercooler))
end
end
end
test "updating involvement does not send turbo update for direct rooms" do
assert_no_turbo_stream_broadcasts [ users(:david), :rooms ] do
assert_changes -> { memberships(:david_david_and_jason).reload.involvement }, from: "everything", to: "nothing" do
put room_involvement_url(rooms(:david_and_jason)), params: { involvement: "nothing" }
assert_redirected_to room_involvement_url(rooms(:david_and_jason))
end
end
end
end
@@ -0,0 +1,51 @@
require "test_helper"
class Rooms::OpensControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "show redirects to get general show" do
get rooms_open_url(users(:david).rooms.opens.last)
assert_redirected_to room_url(users(:david).rooms.opens.last)
end
test "new" do
get new_rooms_open_url
assert_response :success
end
test "create" do
assert_turbo_stream_broadcasts :rooms, count: 1 do
post rooms_opens_url, params: { room: { name: "My New Room" } }
end
assert_equal Room.last.memberships.count, User.count
assert_redirected_to room_url(Room.last)
end
test "only admins or creators can update" do
sign_in :jz
assert_turbo_stream_broadcasts :rooms, count: 0 do
put rooms_open_url(rooms(:hq)), params: { room: { name: "New Name" } }
end
assert_response :forbidden
assert rooms(:hq).reload.name, "HQ"
end
test "update" do
assert_turbo_stream_broadcasts :rooms, count: 1 do
put rooms_open_url(rooms(:pets)), params: { room: { name: "New Name" } }
end
assert_redirected_to room_url(rooms(:pets))
assert rooms(:pets).reload.name, "New Name"
end
test "update a closed room to be open" do
put rooms_open_url(rooms(:designers)), params: { room: { name: "Doesn't matter" } }
assert_equal rooms(:designers).memberships.count, User.count
end
end
@@ -0,0 +1,32 @@
require "test_helper"
class Rooms::RefreshesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "refresh includes new messages since the last known" do
travel_to 1.day.ago do
@old_message = rooms(:hq).messages.create!(creator: users(:jason), body: "Old message", client_message_id: "old")
end
travel_to 1.minute.ago do
@new_message = rooms(:hq).messages.create!(creator: users(:jason), body: "New message", client_message_id: "new")
@old_message.touch
end
get room_refresh_url(rooms(:hq), format: :turbo_stream), params: { since: 10.minutes.ago.to_fs(:epoch) }
assert_response :success
assert_select "turbo-stream[action='append']" do
assert_select "#" + dom_id(@new_message)
assert_select "template", count: 1
end
assert_select "turbo-stream[action='replace']" do
assert_select "#" + dom_id(@old_message)
assert_select "template", count: 1
end
end
end
+45
View File
@@ -0,0 +1,45 @@
require "test_helper"
class RoomsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "index redirects to the user's last room" do
get rooms_url
assert_redirected_to room_url(users(:david).rooms.last)
end
test "show" do
get room_url(users(:david).rooms.last)
assert_response :success
end
test "shows records the last room visited in a cookie" do
get room_url(users(:david).rooms.last)
assert response.cookies[:last_room] = users(:david).rooms.last.id
end
test "destroy" do
assert_turbo_stream_broadcasts :rooms, count: 1 do
assert_difference -> { Room.count }, -1 do
delete room_url(rooms(:designers))
end
end
end
test "destroy only allowed for creators or those who can administer" do
sign_in :jz
assert_no_difference -> { Room.count } do
delete room_url(rooms(:designers))
assert_response :forbidden
end
rooms(:designers).update! creator: users(:jz)
assert_difference -> { Room.count }, -1 do
delete room_url(rooms(:designers))
end
end
end
@@ -0,0 +1,48 @@
require "test_helper"
class SearchesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
@message = rooms(:designers).messages.create! body: "Hello world!", client_message_id: "search", creator: users(:david)
end
test "index initial view" do
get searches_url
assert_response :success
assert_select ".message", count: 0
end
test "finding reachable messages" do
get searches_url, params: { q: "hello" }
assert_response :success
assert_select ".message", text: /Hello world!/
end
test "unreachable messages are not found" do
memberships(:david_designers).destroy!
get searches_url, params: { q: "hello" }
assert_response :success
assert_select ".message", count: 0
end
test "create saves the search term" do
assert_difference -> { users(:david).searches.count }, +1 do
post searches_url, params: { q: "hello" }
end
assert_redirected_to searches_url(q: "hello")
assert users(:david).searches.exists?(query: "hello")
end
test "clear search history" do
assert users(:david).searches.any?
delete clear_searches_url
assert users(:david).searches.none?
end
end
@@ -0,0 +1,18 @@
require "test_helper"
class Sessions::TransfersControllerTest < ActionDispatch::IntegrationTest
test "show renders when not signed in" do
get session_transfer_url("some-token")
assert_response :success
end
test "update establishes a session when the code is valid" do
user = users(:david)
put session_transfer_url(user.transfer_id)
assert_redirected_to root_url
assert parsed_cookies.signed[:session_token]
end
end
@@ -0,0 +1,63 @@
require "test_helper"
class SessionsControllerTest < ActionDispatch::IntegrationTest
ALLOWED_BROWSER = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15"
DISALLOWED_BROWSER = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0"
test "new" do
get new_session_url
assert_response :success
end
test "new redirects to first run when no users exist" do
User.destroy_all
get new_session_url
assert_redirected_to first_run_url
end
test "new denied with incompatible browser" do
get new_session_url, env: { "HTTP_USER_AGENT" => DISALLOWED_BROWSER }
assert_select "h1", /Upgrade to a supported web browser/
end
test "new allowed with compatible browser" do
get new_session_url, env: { "HTTP_USER_AGENT" => ALLOWED_BROWSER }
assert_select "h1", text: /Upgrade to a supported web browser/, count: 0
end
test "create with valid credentials" do
post session_url, params: { email_address: "david@37signals.com", password: "secret123456" }
assert_redirected_to root_url
assert parsed_cookies.signed[:session_token]
end
test "create with invalid credentials" do
post session_url, params: { email_address: "david@37signals.com", password: "wrong" }
assert_response :unauthorized
assert_nil parsed_cookies.signed[:session_token]
end
test "destroy" do
sign_in :david
delete session_url
assert_redirected_to root_url
assert_not cookies[:session_token].present?
end
test "destroy removes the push subscription for the device" do
sign_in :david
assert_difference -> { users(:david).push_subscriptions.count }, -1 do
delete session_url, params: { push_subscription_endpoint: push_subscriptions(:david_chrome).endpoint }
end
assert_redirected_to root_url
assert_not cookies[:session_token].present?
end
end
@@ -0,0 +1,64 @@
require "test_helper"
class UnfurlLinksControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "create" do
stub_successful_request
post unfurl_link_url, params: { url: "https://www.example.com" }
assert_response :success
json_response = JSON.parse(response.body)
assert_equal "Hey!", json_response["title"]
assert_equal "https://example.com", json_response["url"]
assert_equal "https://example.com/image.png", json_response["image"]
assert_equal "desc..", json_response["description"]
end
test "create with missing opengraph meta tags" do
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: "<html><head></head></html>", headers: {})
post unfurl_link_url, params: { url: "https://www.example.com" }
assert_response :no_content
end
test "create with a missing URL" do
assert_raise ActionController::ParameterMissing do
post unfurl_link_url, params: { url: "" }
assert_response :bad_request
end
end
test "create for twitter.com" do
stub_successful_request url: "https://fxtwitter.com/dhh/status/834146806594433025"
post unfurl_link_url, params: { url: "https://twitter.com/dhh/status/834146806594433025" }
assert_response :success
assert_equal "Hey!", JSON.parse(response.body)["title"]
end
test "create for x.com" do
stub_successful_request url: "https://fxtwitter.com/dhh/status/834146806594433025"
post unfurl_link_url, params: { url: "https://x.com/dhh/status/834146806594433025" }
assert_response :success
assert_equal "Hey!", JSON.parse(response.body)["title"]
end
private
def stub_successful_request(url: "https://www.example.com/")
WebMock.stub_request(:get, url).to_return(
status: 200,
body: "<html><head><meta property=\"og:url\" content=\"https://example.com\"><meta property=\"og:title\" content=\"Hey!\"><meta property=\"og:description\" content=\"desc..\"><meta property=\"og:image\" content=\"https://example.com/image.png\"></head></html>",
headers: { content_type: "text/html" }
)
WebMock.stub_request(:head, "https://example.com/image.png").to_return(
status: 200,
headers: { content_type: "image/png" }
)
end
end
@@ -0,0 +1,26 @@
require "test_helper"
class Users::AvatarsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "show initials" do
get user_avatar_url(users(:kevin).avatar_token)
assert_select "text", text: "K"
end
test "show image" do
users(:kevin).update! avatar: fixture_file_upload("moon.jpg", "image/jpeg")
get user_avatar_url(users(:kevin).avatar_token)
assert_response :success
assert_equal "image/webp", @response.content_type
end
test "show image with invalid token responds 404" do
get user_avatar_url("not-a-valid-token")
assert_response :not_found
end
end
@@ -0,0 +1,28 @@
require "test_helper"
class Users::ProfilesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "show" do
get user_profile_url
assert_response :success
end
test "update" do
put user_profile_url, params: { user: { name: "John Doe", bio: "Acrobat" } }
assert_redirected_to user_profile_url
assert_equal "John Doe", users(:david).reload.name
assert_equal "Acrobat", users(:david).bio
assert_equal "david@37signals.com", users(:david).email_address
end
test "updates are limited to the current user" do
put user_profile_url(users(:jason)), params: { user: { name: "John Doe" } }
assert_equal "Jason", users(:jason).reload.name
end
end
@@ -0,0 +1,38 @@
require "test_helper"
class Users::PushSubscriptionsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "create new push subscription" do
subscription_params = { "endpoint" => "https://apple", "p256dh_key" => "123", "auth_key" => "456" }
post user_push_subscriptions_url,
params: { push_subscription: subscription_params }, headers: { "HTTP_USER_AGENT" => "Mozilla/5.0" }
assert_response :ok
assert_equal subscription_params, users(:david).push_subscriptions.last.attributes.slice("endpoint", "p256dh_key", "auth_key")
assert_equal "Mozilla/5.0", users(:david).push_subscriptions.last.user_agent
end
test "touch existing subscription" do
assert_no_difference -> { users(:david).push_subscriptions.count } do
assert_changes -> { push_subscriptions(:david_chrome).reload.updated_at } do
post user_push_subscriptions_url(params: {
push_subscription: push_subscriptions(:david_chrome).attributes.slice("endpoint", "p256dh_key", "auth_key")
})
end
end
assert_response :ok
end
test "destroy a push subscription via dev mode" do
assert_difference -> { Push::Subscription.count }, -1 do
delete user_push_subscription_url(push_subscriptions(:david_chrome))
assert_redirected_to user_push_subscriptions_url
end
end
end
@@ -0,0 +1,30 @@
require "test_helper"
class Users::SidebarsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "show" do
get user_sidebar_url
users(:david).rooms.opens.each do |room|
assert_match /#{room.name}/, @response.body
end
end
test "unread directs" do
rooms(:david_and_jason).messages.create! client_message_id: 999, body: "Hello", creator: users(:jason)
get user_sidebar_url
assert_select ".unread", count: users(:david).memberships.select { |m| m.room.direct? && m.unread? }.count
end
test "unread other" do
rooms(:watercooler).messages.create! client_message_id: 999, body: "Hello", creator: users(:jason)
get user_sidebar_url
assert_select ".unread", count: users(:david).memberships.reject { |m| m.room.direct? || !m.unread? }.count
end
end
+50
View File
@@ -0,0 +1,50 @@
require "test_helper"
class UsersControllerTest < ActionDispatch::IntegrationTest
setup do
@join_code = accounts(:signal).join_code
end
test "show" do
sign_in :david
get user_url(users(:david))
assert_response :ok
end
test "new" do
get join_url(@join_code)
assert_response :success
end
test "new does not allow a signed in user" do
sign_in :david
get join_url(@join_code)
assert_redirected_to root_url
end
test "new requires a join code" do
get join_url("not")
assert_response :not_found
end
test "create" do
assert_difference -> { User.count }, 1 do
post join_url(@join_code), params: { user: { name: "New Person", email_address: "new@37signals.com", password: "secret123456" } }
end
assert_redirected_to root_url
user = User.last
assert_equal user.id, Session.find_by(token: parsed_cookies.signed[:session_token]).user.id
assert_equal Rooms::Open.all, user.rooms
end
test "creating a new user with an existing email address will redirect to login screen" do
assert_no_difference -> { User.count } do
post join_url(@join_code), params: { user: { name: "Another David", email_address: users(:david).email_address, password: "secret123456" } }
end
assert_redirected_to new_session_url(email_address: users(:david).email_address)
end
end
@@ -0,0 +1,21 @@
require "test_helper"
class WelcomeControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in :david
end
test "redirects to the first created visible room the user has access to" do
get root_url
assert_redirected_to room_url(users(:david).rooms.original)
end
test "redirects to the last room visited, if we have one" do
cookies[:last_room] = rooms(:watercooler).id
get root_url
assert_redirected_to room_url(rooms(:watercooler))
end
end
+3
View File
@@ -0,0 +1,3 @@
signal:
name: 37signals
join_code: CRMu-l8Ge-KB9B
+64
View File
@@ -0,0 +1,64 @@
first:
record: first (Message)
name: body
body: First post!
second:
record: second (Message)
name: body
body: Seconded.
third:
record: third (Message)
name: body
body: Third time's a charm.
fourth:
record: fourth (Message)
name: body
body: You know what the fellow said – in Italy, for thirty years under the Borgias, they had warfare, terror, murder and bloodshed, but they produced Michelangelo, Leonardo da Vinci and the Renaissance. In Switzerland, they had brotherly love, they had five hundred years of democracy and peace – and what did that produce? The cuckoo clock.
fifth:
record: fifth (Message)
name: body
body: Champagne, if you are seeking the truth, is better than a lie detector.
sixth:
record: sixth (Message)
name: body
body: The sense of unhappiness is so much easier to convey than that of happiness. In misery we seem aware of our own existence, even though it may be in the form of a monstrous egotism—this pain of mine is individual, this nerve that winces belongs to me and to no other. But happiness annihilates us—we lose our identity.
seventh:
record: seventh (Message)
name: body
body: A story has no beginning or end—arbitrarily one chooses that moment of experience from which to look back or from which to look ahead.
eighth:
record: eighth (Message)
name: body
body: Hate is a lack of imagination.
ninth:
record: ninth (Message)
name: body
body: And innocence is a kind of insanity
tenth:
record: tenth (Message)
name: body
body: I don't care a damn about men who are loyal to the people who pay them, to organizations...I don't think even my country means all that much. There are many countries in our blood, aren't there, but only one person. Would the world be in the mess it is if we were loyal to love and not to countries?
eleventh:
record: eleventh (Message)
name: body
body: People who like quotes love meaningless generalizations
twelfth:
record: twelfth (Message)
name: body
body: Pain is easy to write. In pain we're all happily individual. But what can one write about happiness?
thirteenth:
record: thirteenth (Message)
name: body
body: When we are not sure, we are alive.
+8
View File
@@ -0,0 +1,8 @@
first:
message: first
booster: david
content: Hello
thirteenth:
message: thirteenth
booster: jason
content: 💯
View File
Whitespace-only changes.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+91
View File
@@ -0,0 +1,91 @@
david_designers:
room: designers
user: david
jason_designers:
room: designers
user: jason
involvement: everything
jz_designers:
room: designers
user: jz
involvement: everything
kevin_designers:
room: designers
user: kevin
david_pets:
room: pets
user: david
involvement: everything
jason_pets:
room: pets
user: jason
involvement: everything
david_watercooler:
room: watercooler
user: david
involvement: everything
jason_watercooler:
room: watercooler
user: jason
involvement: everything
bender_watercooler:
room: watercooler
user: bender
david_hq:
room: hq
user: david
involvement: everything
jason_hq:
room: hq
user: jason
involvement: everything
jz_hq:
room: hq
user: jz
involvement: everything
kevin_hq:
room: hq
user: kevin
involvement: everything
david_david_and_jason:
room: david_and_jason
user: david
involvement: everything
jason_david_and_jason:
room: david_and_jason
user: jason
involvement: everything
david_david_and_kevin:
room: david_and_kevin
user: david
involvement: everything
kevin_david_and_kevin:
room: david_and_kevin
user: kevin
involvement: everything
bender_bender_and_kevin:
room: bender_and_kevin
user: bender
involvement: everything
kevin_bender_and_kevin:
room: bender_and_kevin
user: kevin
involvement: everything
+77
View File
@@ -0,0 +1,77 @@
first:
room: designers
creator: jason
client_message_id: "0001"
created_at: <%= 1.hour.ago %>
second:
room: designers
creator: david
client_message_id: "0002"
created_at: <%= 36.minutes.ago %>
third:
room: designers
creator: jz
client_message_id: "0003"
created_at: <%= 7.minutes.ago %>
fourth:
room: watercooler
creator: jz
client_message_id: "0004"
created_at: <%= 30.minutes.ago %>
fifth:
room: watercooler
creator: jason
client_message_id: "0005"
created_at: <%= 29.minutes.ago %>
sixth:
room: watercooler
creator: david
client_message_id: "0006"
created_at: <%= 27.minutes.ago %>
seventh:
room: watercooler
creator: jason
client_message_id: "0007"
created_at: <%= 26.minutes.ago %>
eighth:
room: watercooler
creator: jz
client_message_id: "0008"
created_at: <%= 25.minutes.ago %>
ninth:
room: watercooler
creator: jz
client_message_id: "0009"
created_at: <%= 24.minutes.ago %>
tenth:
room: watercooler
creator: david
client_message_id: "0010"
created_at: <%= 23.minutes.ago %>
eleventh:
room: watercooler
creator: jason
client_message_id: "0011"
created_at: <%= 22.minutes.ago %>
twelfth:
room: watercooler
creator: jason
client_message_id: "0012"
created_at: <%= 21.minutes.ago %>
thirteenth:
room: watercooler
creator: jz
client_message_id: "0013"
created_at: <%= 5.minutes.ago %>
+27
View File
@@ -0,0 +1,27 @@
david_chrome:
user: david
endpoint: https://fcm.googleapis.com/fcm/send/123
p256dh_key: 123-RIXcMgkdjhRnFZaYjjGvo00dydRQbCpQTuXFjLaCPSE7ofxi19awgGc3Doqa1RmYQqsbQDfQTifFZgc
auth_key: xxx2DtgvmLkevKRwoyahJl0efg
user_agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36
jason_chrome:
user: jason
endpoint: https://fcm.googleapis.com/fcm/send/567
p256dh_key: 456-RIXcMgkdjhRnFZaYjjGvo00dydRQbCpQTuXFjLaCPSE7ofxi19awgGc3Doqa1RmYQqsbQDfQTifFZgc
auth_key: xxx2DtgvmLkevKRwoyahJl0efg
user_agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36
jz_chrome:
user: jz
endpoint: https://fcm.googleapis.com/fcm/send/456
p256dh_key: 456-RIXcMgkdjhRnFZaYjjGvo00dydRQbCpQTuXFjLaCPSE7ofxi19awgGc3Doqa1RmYQqsbQDfQTifFZgc
auth_key: xxx2DtgvmLkevKRwoyahJl0efg
user_agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36
kevin_chrome:
user: kevin
endpoint: https://fcm.googleapis.com/fcm/send/789
p256dh_key: 456-RIXcMgkdjhRnFZaYjjGvo00dydRQbCpQTuXFjLaCPSE7ofxi19awgGc3Doqa1RmYQqsbQDfQTifFZgc
auth_key: xxx2DtgvmLkevKRwoyahJl0efg
user_agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36
+31
View File
@@ -0,0 +1,31 @@
pets:
name: All Pets
type: Rooms::Open
creator: :david
hq:
name: HQ
type: Rooms::Open
creator: :david
watercooler:
name: All Talk
type: Rooms::Closed
creator: :david
designers:
name: Designers
type: Rooms::Closed
creator: :david
david_and_jason:
type: Rooms::Direct
creator: :david
david_and_kevin:
type: Rooms::Direct
creator: :david
bender_and_kevin:
type: Rooms::Direct
creator: :kevin
+3
View File
@@ -0,0 +1,3 @@
david_pizza:
user: david
query: pizza
+5
View File
@@ -0,0 +1,5 @@
david_safari:
user: david
token: AxJs94fteQ5Autv2VrKsH68c
user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) HEY/1.0.4 Chrome/83.0.4103.100 Electron/9.0.3 Safari/537.36"
last_active_at: <%= 2.hours.ago %>
+30
View File
@@ -0,0 +1,30 @@
<% password_digest = BCrypt::Password.create("secret123456") %>
david:
name: David
email_address: david@37signals.com
password_digest: <%= password_digest %>
role: administrator
jason:
name: Jason
email_address: jason@37signals.com
password_digest: <%= password_digest %>
role: administrator
jz:
name: JZ
email_address: jz@37signals.com
password_digest: <%= password_digest %>
bio: Designer
kevin:
name: Kevin
email_address: kevin@37signals.com
password_digest: <%= password_digest %>
bio: Programmer
bender:
name: Bender Bot
bot_token: <%= User.generate_bot_token %>
role: bot
+3
View File
@@ -0,0 +1,3 @@
bender:
user: bender
url: http://example.com/bender
View File
Whitespace-only changes.
+93
View File
@@ -0,0 +1,93 @@
require "test_helper"
class ContentFiltersTest < ActionView::TestCase
test "entire message contains an unfurled URL" do
text = "https://basecamp.com/"
message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_basecamp(text), client_message_id: "0015", creator: users(:jason)
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
assert_not_equal message.body.body.to_html, filtered.to_html
assert_match /<div><action-text-attachment/, filtered.to_html
end
test "message includes additional text besides an unfurled URL" do
text = "Hello https://basecamp.com/"
message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_basecamp(text), client_message_id: "0015", creator: users(:jason)
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
assert_equal message.body.body.to_html, filtered.to_html
assert_match %r{<div>Hello https://basecamp\.com/<action-text-attachment}, filtered.to_html
end
test "unfurled tweet without any image" do
text = "<div>https://twitter.com/37signals/status/1750290547908952568<action-text-attachment content-type=\"application/vnd.actiontext.opengraph-embed\" url=\"https://pbs.twimg.com/profile_images/1671940407633010689/9P5gi6LF_200x200.jpg\" href=\"https://twitter.com/37signals/status/1750290547908952568\" filename=\"37signals (@37signals)\" caption=\"We're back up on all apps, everyone. Really sorry for the disruption to your day.\" content=\"<actiontext-opengraph-embed>\n <div class=&quot;og-embed&quot;>\n <div class=&quot;og-embed__content&quot;>\n <div class=&quot;og-embed__title&quot;>37signals (@37signals)</div>\n <div class=&quot;og-embed__description&quot;>We're back up on all apps, everyone. Really sorry for the disruption to your day.</div>\n </div>\n <div class=&quot;og-embed__image&quot;>\n <img src=&quot;https://pbs.twimg.com/profile_images/1671940407633010689/9P5gi6LF_200x200.jpg&quot; class=&quot;image&quot; alt=&quot;&quot; />\n </div>\n </div>\n </actiontext-opengraph-embed>\"></action-text-attachment></div>"
message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_basecamp(text), client_message_id: "0015", creator: users(:jason)
filtered = ContentFilters::StyleUnfurledTwitterAvatars.apply(message.body.body)
assert_match %r{<div class="cf-twitter-avatar">}, filtered.to_html
end
test "unfurled tweet containing an image" do
text = "<div>https://twitter.com/dhh/status/1748445489648050505<action-text-attachment content-type=\"application/vnd.actiontext.opengraph-embed\" url=\"https://pbs.twimg.com/media/GEO5l04bsAA9f6H.jpg\" href=\"https://twitter.com/dhh/status/1748445489648050505\" filename=\"DHH (@dhh)\" caption=\"We pay homage to the glorious MIT License with the ONCE license. May all our future legalese be as succinct!\" content=\"<actiontext-opengraph-embed>\n <div class=&quot;og-embed&quot;>\n <div class=&quot;og-embed__content&quot;>\n <div class=&quot;og-embed__title&quot;>DHH (@dhh)</div>\n <div class=&quot;og-embed__description&quot;>We pay homage to the glorious MIT License with the ONCE license. May all our future legalese be as succinct!</div>\n </div>\n <div class=&quot;og-embed__image&quot;>\n <img src=&quot;https://pbs.twimg.com/media/GEO5l04bsAA9f6H.jpg&quot; class=&quot;image&quot; alt=&quot;&quot; />\n </div>\n </div>\n </actiontext-opengraph-embed>\"></action-text-attachment></div>"
message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_basecamp(text), client_message_id: "0015", creator: users(:jason)
filtered = ContentFilters::StyleUnfurledTwitterAvatars.apply(message.body.body)
assert_no_match %r{<div class="cf-twitter-avatar">}, filtered.to_html
end
test "entire message contains an unfurled URL from x.com but unfurls to twitter.com" do
text = "https://x.com/dhh/status/1752476663303323939"
message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_twitter(text), client_message_id: "0015", creator: users(:jason)
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
assert_not_equal message.body.body.to_html, filtered.to_html
assert_match /<div><action-text-attachment/, filtered.to_html
end
test "entire message contains an unfurled URL from x.com with query params" do
text = "https://x.com/dhh/status/1752476663303323939?s=20"
message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_twitter(text), client_message_id: "0015", creator: users(:jason)
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
assert_not_equal message.body.body.to_html, filtered.to_html
assert_match /<div><action-text-attachment/, filtered.to_html
end
test "message contains a forbidden tag" do
exploit_image_tag = 'Hello <img src="https://ssecurityrise.com/tests/billionlaughs-cache.svg">World'
message = Message.create! room: rooms(:pets), body: exploit_image_tag, client_message_id: "0015", creator: users(:jason)
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
assert_equal "Hello World", filtered.to_html
end
test "message with a mention attachment" do
message = Message.create! room: rooms(:pets), body: "<div>Hey #{mention_attachment_for(:david)}</div>", creator: users(:jason)
filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
expected = /<action-text-attachment sgid="#{users(:david).attachable_sgid}" content-type="application\/vnd\.campfire\.mention" content="(.*?)"><\/action-text-attachment>/m
assert_match expected, filtered.to_html
end
private
def unfurled_message_body_for_basecamp(text)
"<div>#{text}#{unfurled_link_trix_attachment_for_basecamp}</div>"
end
def unfurled_link_trix_attachment_for_basecamp
<<~BASECAMP
<action-text-attachment content-type=\"application/vnd.actiontext.opengraph-embed\" url=\"https://basecamp.com/assets/general/opengraph.png\" href=\"https://basecamp.com/\" filename=\"Project management software, online collaboration\" caption=\"Trusted by millions, Basecamp puts everything you need to get work done in one place. It’s the calm, organized way to manage projects, work with clients, and communicate company-wide.\" content=\"<actiontext-opengraph-embed>\n <div class=&quot;og-embed&quot;>\n <div class=&quot;og-embed__content&quot;>\n <div class=&quot;og-embed__title&quot;>Project management software, online collaboration</div>\n <div class=&quot;og-embed__description&quot;>Trusted by millions, Basecamp puts everything you need to get work done in one place. It’s the calm, organized way to manage projects, work with clients, and communicate company-wide.</div>\n </div>\n <div class=&quot;og-embed__image&quot;>\n <img src=&quot;https://basecamp.com/assets/general/opengraph.png&quot; class=&quot;image&quot; alt=&quot;&quot; />\n </div>\n </div>\n </actiontext-opengraph-embed>\"></action-text-attachment>
BASECAMP
end
def unfurled_message_body_for_twitter(text)
"<div>#{text}#{unfurled_link_trix_attachment_for_twitter}</div>"
end
def unfurled_link_trix_attachment_for_twitter
<<~TWEET
<action-text-attachment content-type=\"application/vnd.actiontext.opengraph-embed\" url=\"https://pbs.twimg.com/ext_tw_video_thumb/1752476502791503873/pu/img/WEAqUgarUxWjPNHD.jpg\" href=\"https://twitter.com/dhh/status/1752476663303323939\" filename=\"DHH (@dhh)\" caption=\"We're playing with adding easy extension points to ONCE/Campfire. Here's one experiment for allowing any type of CSS to be easily added.\" content=\"&lt;actiontext-opengraph-embed&gt;\n &lt;div class=&quot;og-embed&quot;&gt;\n &lt;div class=&quot;og-embed__content&quot;&gt;\n &lt;div class=&quot;og-embed__title&quot;&gt;DHH (@dhh)&lt;/div&gt;\n &lt;div class=&quot;og-embed__description&quot;&gt;We're playing with adding easy extension points to ONCE/Campfire. Here's one experiment for allowing any type of CSS to be easily added.&lt;/div&gt;\n &lt;/div&gt;\n &lt;div class=&quot;og-embed__image&quot;&gt;\n &lt;img src=&quot;https://pbs.twimg.com/ext_tw_video_thumb/1752476502791503873/pu/img/WEAqUgarUxWjPNHD.jpg&quot; class=&quot;image&quot; alt=&quot;&quot; /&gt;\n &lt;/div&gt;\n &lt;/div&gt;\n &lt;/actiontext-opengraph-embed&gt;\"><figure class=\"attachment attachment--content attachment--og\">\n \n <div class=\"og-embed gap\">\n <div class=\"og-embed__content\">\n <div class=\"og-embed__title\">\n <a href=\"https://twitter.com/dhh/status/1752476663303323939\">DHH (@dhh)</a>\n </div>\n <div class=\"og-embed__description\">We're playing with adding easy extension points to ONCE/Campfire. Here's one experiment for allowing any type of CSS to be easily added.</div>\n </div>\n <div class=\"og-embed__image\">\n <img src=\"https://pbs.twimg.com/ext_tw_video_thumb/1752476502791503873/pu/img/WEAqUgarUxWjPNHD.jpg\" class=\"image center\" alt=\"\">\n </div>\n </div>\n \n</figure></action-text-attachment>
TWEET
end
end
View File
Whitespace-only changes.
+15
View File
@@ -0,0 +1,15 @@
require "test_helper"
class Account::JoinableTest < ActiveSupport::TestCase
test "new accounts get a joinable code" do
Account.destroy_all
account = Account.create!(name: "Chat")
assert_match /\w{4}-\w{4}-\w{4}/, account.join_code
end
test "accounts can reset join code" do
assert_changes -> { accounts(:signal).reload.join_code } do
accounts(:signal).reset_join_code
end
end
end
+4
View File
@@ -0,0 +1,4 @@
require "test_helper"
class AccountTest < ActiveSupport::TestCase
end
@@ -0,0 +1,38 @@
require "test_helper"
class ActionTextAttachmentTest < ActiveSupport::TestCase
setup do
@user = users(:david)
end
test "lookup user attachable with invalid sgid" do
message, signature = @user.attachable_sgid.split("--")
html = %Q(<action-text-attachment sgid="#{message}--invalid"></action-text-attachment>)
node = ActionText::Fragment.wrap(html).find_all(ActionText::Attachment.tag_name).first
attachment = ActionText::Attachment.from_node(node)
assert_equal @user, attachment.attachable
end
test "lookup attachable with nil sgid" do
html = %Q(<action-text-attachment></action-text-attachment>)
node = ActionText::Fragment.wrap(html).find_all(ActionText::Attachment.tag_name).first
attachment = ActionText::Attachment.from_node(node)
assert_kind_of ActionText::Attachables::MissingAttachable, attachment.attachable
end
test "lookup invalid sgid for an attachable requiring a valid sgid" do
# Make room instance attachable for testing purposes
room = rooms(:pets).tap { |r| r.extend ActionText::Attachable }
message, signature = rooms(:pets).attachable_sgid.split("--")
html = %Q(<action-text-attachment sgid="#{message}--invalid"></action-text-attachment>)
node = ActionText::Fragment.wrap(html).find_all(ActionText::Attachment.tag_name).first
attachment = ActionText::Attachment.from_node(node)
assert_kind_of ActionText::Attachables::MissingAttachable, attachment.attachable
end
end
+29
View File
@@ -0,0 +1,29 @@
require "test_helper"
class FirstRunTest < ActiveSupport::TestCase
setup do
Account.destroy_all
Room.destroy_all
User.destroy_all
end
test "creating makes first user an administrator" do
user = create_first_run_user
assert user.administrator?
end
test "first user has access to first room" do
user = create_first_run_user
assert user.rooms.one?
end
test "first room is an open room" do
create_first_run_user
assert Room.first.open?
end
private
def create_first_run_user
FirstRun.create!({ name: "User", email_address: "user@example.com", password: "secret123456" })
end
end
+90
View File
@@ -0,0 +1,90 @@
require "test_helper"
class MembershipTest < ActiveSupport::TestCase
setup do
@membership = memberships(:david_watercooler)
end
test "connected scope" do
@membership.connected
assert Membership.connected.exists?(@membership.id)
@membership.disconnected
assert_not Membership.connected.exists?(@membership.id)
travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
assert_not Membership.connected.exists?(@membership.id)
end
test "disconnected scope" do
@membership.disconnected
assert Membership.disconnected.exists?(@membership.id)
@membership.connected
assert_not Membership.disconnected.exists?(@membership.id)
travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
assert Membership.disconnected.exists?(@membership.id)
end
test "connected? is false when connection is stale" do
@membership.connected
travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
assert_not @membership.connected?
end
test "connecting" do
@membership.connected
assert @membership.connected?
assert_equal 1, @membership.connections
@membership.connected
assert_equal 2, @membership.connections
end
test "connecting resets stale connection count" do
2.times { @membership.connected }
assert_equal 2, @membership.connections
travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
@membership.connected
assert_equal 1, @membership.connections
end
test "disconnecting" do
2.times { @membership.connected }
@membership.disconnected
assert @membership.connected?
assert_equal 1, @membership.connections
@membership.disconnected
assert_not @membership.connected?
assert_equal 0, @membership.connections
end
test "disconnecting resets stale connection count" do
2.times { @membership.connected }
assert_equal 2, @membership.connections
travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
@membership.disconnected
assert_equal 0, @membership.connections
end
test "refreshing the connection" do
@membership.connected
travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
assert_not @membership.connected?
@membership.refresh_connection
assert @membership.connected?
end
test "removing a membership resets the user's connections" do
@membership.user.expects :reset_remote_connections
@membership.destroy
end
end
+30
View File
@@ -0,0 +1,30 @@
require "test_helper"
class Message::AttachmentTest < ActiveSupport::TestCase
include ActiveJob::TestHelper
include ActionDispatch::TestProcess
test "creating a message creates image thumbnail" do
message = create_attachment_message("moon.jpg", "image/jpeg")
assert message.attachment.representation(:thumb).image.present?
end
test "creating a message creates video preview" do
message = create_attachment_message("alpha-centuri.mov", "video/quicktime")
assert message.reload.attachment.preview(format: :webp).image.attached?
end
test "creating a blank message with attachment will use filename as plain text body" do
message = create_attachment_message("moon.jpg", "image/jpeg")
assert_equal message.plain_text_body, "moon.jpg"
end
private
def create_attachment_message(file, content_type)
rooms(:hq).messages.create_with_attachment! \
creator: users(:david),
client_message_id: "message",
attachment: fixture_file_upload(file, content_type)
end
end
+29
View File
@@ -0,0 +1,29 @@
require "test_helper"
class Message::SearchableTest < ActiveSupport::TestCase
test "message body is indexed and searchable" do
message = rooms(:designers).messages.create! body: "My hovercraft is full of eels", client_message_id: "earth", creator: users(:david)
assert_equal [ message ], rooms(:designers).messages.search("eel")
message.update! body: "My hovercraft is full of sharks"
assert_equal [ message ], rooms(:designers).messages.search("sharks")
message.destroy!
assert_equal [], rooms(:designers).messages.search("sharks")
end
test "search results are returned in message order" do
messages = [ "first cat", "second cat", "third cat", "cat cat cat" ].map do |body|
rooms(:designers).messages.create! body: body, client_message_id: body, creator: users(:david)
end
assert_equal messages, rooms(:designers).messages.search("cat")
end
test "rich text body is converted to plain text for indexing" do
message = rooms(:designers).messages.create! body: "<span>My hovercraft is full of eels</span>", client_message_id: "earth", creator: users(:david)
assert_equal [], rooms(:designers).messages.search("span")
assert_equal [ message ], rooms(:designers).messages.search("eel")
end
end
+34
View File
@@ -0,0 +1,34 @@
require "test_helper"
class MessageTest < ActiveSupport::TestCase
include ActionCable::TestHelper, ActiveJob::TestHelper
test "creating a message enqueues to push later" do
assert_enqueued_jobs 1, only: [ Room::PushMessageJob ] do
create_new_message_in rooms(:designers)
end
end
test "all emoji" do
assert Message.new(body: "😄🤘").plain_text_body.all_emoji?
assert_not Message.new(body: "Haha! 😄🤘").plain_text_body.all_emoji?
assert_not Message.new(body: "🔥\nmultiple lines\n💯").plain_text_body.all_emoji?
assert_not Message.new(body: "🔥 💯").plain_text_body.all_emoji?
end
test "mentionees" do
message = Message.new room: rooms(:pets), body: "<div>Hey #{mention_attachment_for(:david)}</div>", creator: users(:jason), client_message_id: "earth"
assert_equal [ users(:david) ], message.mentionees
message_with_duplicate_mentions = Message.new room: rooms(:pets), body: "<div>Hey #{mention_attachment_for(:david)} #{mention_attachment_for(:david)}</div>", creator: users(:jason), client_message_id: "earth"
assert_equal [ users(:david) ], message.mentionees
message_mentioning_a_non_member = Message.new room: rooms(:pets), body: "<div>Hey #{mention_attachment_for(:kevin)}</div>", creator: users(:jason), client_message_id: "earth"
assert_equal [], message_mentioning_a_non_member.mentionees
end
private
def create_new_message_in(room)
room.messages.create!(creator: users(:jason), body: "Hello", client_message_id: "123")
end
end
+33
View File
@@ -0,0 +1,33 @@
require "test_helper"
class Opengraph::DocumentTest < ActiveSupport::TestCase
test "extract opengraph tags using property attribute" do
document = Opengraph::Document.new("<html><head><meta property=\"og:url\" content=\"https://example.com\"><meta property=\"og:title\" content=\"Hey!\"><meta property=\"og:description\" content=\"desc..\"><meta property=\"og:image\" content=\"https://example.com/image.png\"></head></html>")
attributes = document.opengraph_attributes
assert_equal "https://example.com", attributes[:url]
assert_equal "Hey!", attributes[:title]
assert_equal "desc..", attributes[:description]
assert_equal "https://example.com/image.png", attributes[:image]
end
test "extract opengraph tags using name attribute" do
document = Opengraph::Document.new("<html><head><meta name=\"og:url\" content=\"https://example.com\"><meta name=\"og:title\" content=\"Hey!\"><meta name=\"og:description\" content=\"desc..\"><meta name=\"og:image\" content=\"https://example.com/image.png\"></head></html>")
attributes = document.opengraph_attributes
assert_equal "https://example.com", attributes[:url]
assert_equal "Hey!", attributes[:title]
assert_equal "desc..", attributes[:description]
assert_equal "https://example.com/image.png", attributes[:image]
end
test "document containing missing meta encoding tag and non-UTF8 characters" do
document = Opengraph::Document.new("<html><head><meta name=\"og:url\" content=\"https://example.com\"><meta name=\"og:title\" content=\"Hey!\"><meta name=\"og:description\" content=\"Hello â\u0080\u0099World\"><meta name=\"og:image\" content=\"https://example.com/image.png\"></head></html>")
attributes = document.opengraph_attributes
assert_equal "https://example.com", attributes[:url]
assert_equal "Hey!", attributes[:title]
assert_equal "Hello World", attributes[:description]
assert_equal "https://example.com/image.png", attributes[:image]
end
end
+118
View File
@@ -0,0 +1,118 @@
require "test_helper"
require "restricted_http/private_network_guard"
class Opengraph::FetchTest < ActiveSupport::TestCase
setup do
@fetch = Opengraph::Fetch.new
@url = URI.parse("https://www.example.com")
end
test "#fetch_document fetches valid HTML" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
assert_equal "<body>ok<body>", @fetch.fetch_document(@url)
end
test "#fetch_document discards other content types" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: "I'm not HTML!", headers: { content_type: "text/plain" })
assert_nil @fetch.fetch_document(@url)
end
test "#fetch_document follows redirects" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 302, headers: { location: "https://www.other.com/" })
WebMock.stub_request(:get, "https://www.other.com/")
.to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
assert_equal "<body>ok<body>", @fetch.fetch_document(@url)
end
test "#fetch_document does not follow redirects to private networks" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 302, headers: { location: "https://www.other.com/" })
WebMock.stub_request(:get, "https://www.other.com/")
.to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
Resolv.stubs(:getaddress).with("www.other.com").returns("127.0.0.1")
assert_raises RestrictedHTTP::Violation do
@fetch.fetch_document(@url, ip: "1.2.3.4")
end
end
test "#fetch_document resolves hostnames once to avoid DNS rebinding" do
# Allow but interrupt a real connection to demonstrate that we connect
# to a resolved IP, not a hostname to re-resolve.
WebMock.disable_net_connect! allow: [ @url.host ]
Resolv.stubs(:getaddress).with(@url.host).returns("1.2.3.4", "127.0.0.1")
TCPSocket.expects(:open).with(@url.host, 443, nil, nil).never
TCPSocket.expects(:open).with("1.2.3.4", 443, nil, nil).throws(:dns_not_rebound)
assert_throws :dns_not_rebound do
@fetch.fetch_document(@url)
end
end
test "#fetch_document resolves redirect location hostnames once to avoid DNS rebinding" do
# Stub the initial URL to redirect to a DNS-rebound location
WebMock.stub_request(:get, "https://www.other.com/")
.to_return(status: 302, headers: { location: @url.to_s })
# Allow but interrupt a real connection to demonstrate that we connect
# to a resolved IP, not a hostname to re-resolve.
WebMock.disable_net_connect! allow: [ @url.host ]
Resolv.stubs(:getaddress).with(@url.host).returns("1.2.3.4", "127.0.0.1")
TCPSocket.expects(:open).with(@url.host, 443, nil, nil).never
TCPSocket.expects(:open).with("1.2.3.4", 443, nil, nil).throws(:dns_not_rebound)
assert_throws :dns_not_rebound do
@fetch.fetch_document(URI.parse("https://www.other.com/"), ip: "1.2.3.4")
end
end
test "#fetch_document is empty following redirects that never finish" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 302, headers: { location: "https://www.example.com/" })
assert_raises Opengraph::Fetch::TooManyRedirectsError do
@fetch.fetch_document(@url)
end
end
test "#fetch_document ignores large responses" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: "too large", headers: { content_length: 1.gigabyte, content_type: "text/html" })
assert_nil @fetch.fetch_document(@url)
end
test "#fetch_document ignores large responses that were missing their content length" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: large_body_content, headers: { content_type: "text/html" })
assert_nil @fetch.fetch_document(@url)
end
test "#fetch_document ignores large responses that were lying about their content length" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: large_body_content, headers: { content_length: 1.megabyte, content_type: "text/html" })
assert_nil @fetch.fetch_document(@url)
end
test "fetch content type" do
WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
url = URI.parse("https://example.com/image.png")
assert_equal "image/png", @fetch.fetch_content_type(url)
end
private
def large_body_content
"x" * (Opengraph::Fetch::MAX_BODY_SIZE + 1)
end
end
+50
View File
@@ -0,0 +1,50 @@
require "test_helper"
require "restricted_http/private_network_guard"
class Opengraph::LocationTest < ActiveSupport::TestCase
test "url validations" do
assert Opengraph::Location.new("https://www.example.com").valid?
assert Opengraph::Location.new("http://www.example.com").valid?
assert_not Opengraph::Location.new("~/etc/password").valid?
assert_not Opengraph::Location.new("ftp://speedtest.tele2.net").valid?
assert_not Opengraph::Location.new("httpfake").valid?
assert_not Opengraph::Location.new(" foo").valid?
assert_not Opengraph::Location.new("https/incorrect").valid?
end
test "private network urls" do
Resolv.stubs(:getaddress).with("www.example.com").returns("172.16.0.0")
location = Opengraph::Location.new("https://www.example.com")
assert_not location.valid?
assert_equal [ "is not public" ], location.errors[:url]
end
test "avoid reading file urls when expecting HTML" do
large_file = Opengraph::Location.new("https://www.example.com/100gb.zip")
assert_nil Opengraph::Location.new("http://www.example.com/video.mp4").read_html
assert_nil Opengraph::Location.new("http://www.example.com/archive.tar").read_html
assert_nil Opengraph::Location.new("https://www.example.com/large.heic").read_html
assert_nil Opengraph::Location.new("https://www.example.com/image.jpeg").read_html
assert_nil Opengraph::Location.new("https://www.example.com/malware.exe").read_html
assert_nil Opengraph::Location.new("https://www.example.com/massiveOS.iso").read_html
end
test "read valid HTML" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
location = Opengraph::Location.new("https://www.example.com")
assert_equal "<body>ok<body>", location.read_html
end
test "read ignores invalid responses" do
WebMock.stub_request(:get, "https://www.example.com/")
.to_return(status: 200, body: "too large", headers: { content_length: 1.gigabyte, content_type: "text/html" })
location = Opengraph::Location.new("https://www.example.com")
assert_nil location.read_html
end
end
+170
View File
@@ -0,0 +1,170 @@
require "test_helper"
class Opengraph::MetadataTest < ActiveSupport::TestCase
test "successful fetch" do
body = <<~HTML
<html>
<head>
<meta property="og:url" content="https://example.com">
<meta property="og:title" content="Hey!">
<meta property="og:description" content="Hello">
<meta property="og:image" content="https://example.com/image.png">
</head>
</html>
HTML
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
metadata = Opengraph::Metadata.from_url("https://www.example.com")
assert metadata.valid?
assert_equal "https://example.com", metadata.url
assert_equal "Hey!", metadata.title
assert_equal "Hello", metadata.description
assert_equal "https://example.com/image.png", metadata.image
end
test "missing opengraph meta tags" do
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: "<html><head></head></html>", headers: { content_type: "text/html" })
opengraph = Opengraph::Metadata.from_url("https://www.example.com")
assert_not opengraph.valid?
assert_equal [ "Title can't be blank", "Description can't be blank" ], opengraph.errors.full_messages
end
test "URL uses the provided value if the returned value is missing" do
body = <<~HTML
<html>
<head>
<meta property="og:title" content="Hey!">
<meta property="og:description" content="Hello">
<meta property="og:image" content="https://example.com/image.png">
</head>
</html>
HTML
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
metadata = Opengraph::Metadata.from_url("https://www.example.com")
assert metadata.valid?
assert_equal "https://www.example.com", metadata.url
end
test "URL uses the provided value if the returned value is invalid" do
body = <<~HTML
<html>
<head>
<meta property="og:url" content="/foo">
<meta property="og:title" content="Hey!">
<meta property="og:description" content="Hello">
<meta property="og:image" content="https://example.com/image.png">
</head>
</html>
HTML
WebMock.stub_request(:get, "https://www.example.com/foo").to_return(status: 200, body: body, headers: { content_type: "text/html" })
WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
metadata = Opengraph::Metadata.from_url("https://www.example.com/foo")
assert metadata.valid?
assert_equal "https://www.example.com/foo", metadata.url
end
test "missing response body" do
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 403, body: "", headers: { content_type: "text/html" })
assert_not Opengraph::Metadata.from_url("https://www.example.com").valid?
end
test "non html response" do
WebMock.stub_request(:get, "https://www.example.com/image").to_return(status: 200, body: "[blob]", headers: { content_type: "image/jpeg" })
assert_not Opengraph::Metadata.from_url("https://www.example.com/image").valid?
end
test "relative and invalid image URLs are ignored" do
body = <<~HTML
<html>
<head>
<meta property="og:url" content="https://example.com">
<meta property="og:title" content="Hey!">
<meta property="og:description" content="Hello">
<meta property="og:image" content="%s">
</head>
</html>
HTML
[ "/image.png", "foo", "https/incorrect", "~/etc/password" ].each do |invalid_image_url|
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body % invalid_image_url, headers: { content_type: "text/html" })
opengraph = Opengraph::Metadata.from_url("https://www.example.com")
assert opengraph.valid?
assert_nil opengraph.image
end
end
test "sanitize title and description" do
body = <<~HTML
<html>
<head>
<meta property="og:title" content="Hey!<script>alert('hi')</script>">
<meta property="og:description" content="Hello<script>alert('hi')</script>">
<meta property="og:image" content="https://example.com/image.png">
</head>
</html>
HTML
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
metadata = Opengraph::Metadata.from_url("https://www.example.com")
assert metadata.valid?
assert_equal "Hey!", metadata.title
assert_equal "Hello", metadata.description
end
test "remove encoded tags from title and description" do
body = <<~HTML
<html>
<head>
<meta property="og:title" content="Hey!&#x3c;&#x2f;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3e;&#x3c;&#x69;&#x6d;&#x67;&#x20;&#x73;&#x72;&#x63;&#x3d;&#x61;&#x20;&#x6f;&#x6e;&#x65;&#x72;&#x72;&#x6f;&#x72;&#x3d;&#x70;&#x72;&#x6f;&#x6d;&#x70;&#x74;&#x28;&#x31;&#x29;&#x3e;">
<meta property="og:description" content="Hello&#x3c;&#x2f;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&#x3e;&#x3c;&#x69;&#x6d;&#x67;&#x20;&#x73;&#x72;&#x63;&#x3d;&#x61;&#x20;&#x6f;&#x6e;&#x65;&#x72;&#x72;&#x6f;&#x72;&#x3d;&#x70;&#x72;&#x6f;&#x6d;&#x70;&#x74;&#x28;&#x32;&#x29;&#x3e;</script>">
<meta property="og:image" content="https://example.com/image.png">
</head>
</html>
HTML
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
metadata = Opengraph::Metadata.from_url("https://www.example.com")
assert metadata.valid?
assert_equal "Hey!", metadata.title
assert_equal "Hello", metadata.description
end
test "does not allow SVG content type for preview image" do
body = <<~HTML
<html>
<head>
<meta property="og:url" content="https://example.com">
<meta property="og:title" content="Hey!">
<meta property="og:description" content="Hello">
<meta property="og:image" content="https://example.com/image.svg">
</head>
</html>
HTML
WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
WebMock.stub_request(:head, "https://example.com/image.svg").to_return(status: 200, headers: { content_type: "image/svg+xml" })
metadata = Opengraph::Metadata.from_url("https://www.example.com")
assert metadata.valid?
assert_nil metadata.image
end
end
+79
View File
@@ -0,0 +1,79 @@
require "test_helper"
class Room::PushTest < ActiveSupport::TestCase
include ActiveJob::TestHelper
test "deliver new message to other room users with push subscriptions" do
task_count = Push::Subscription.count - users(:david).push_subscriptions.count
perform_enqueued_jobs only: Room::PushMessageJob do
WebPush.expects(:payload_send).times(task_count)
rooms(:hq).messages.create! body: "This is from earth", client_message_id: "earth", creator: users(:david)
end
wait_for_web_push_delivery_pool_tasks(task_count)
end
test "notifies subscribed users" do
perform_enqueued_jobs only: Room::PushMessageJob do
WebPush.expects(:payload_send).times(2)
rooms(:designers).messages.create! body: "This is from earth", client_message_id: "earth", creator: users(:david)
end
wait_for_web_push_delivery_pool_tasks(2)
perform_enqueued_jobs only: Room::PushMessageJob do
WebPush.expects(:payload_send).times(3)
rooms(:designers).messages.create! body: "Hey #{mention_attachment_for(:kevin)}", client_message_id: "earth", creator: users(:david)
end
wait_for_web_push_delivery_pool_tasks(5)
end
test "does not notify for connected rooms" do
memberships(:kevin_designers).connected
perform_enqueued_jobs only: Room::PushMessageJob do
WebPush.expects(:payload_send).times(2)
rooms(:designers).messages.create! body: "Hey @kevin", client_message_id: "earth", creator: users(:david)
end
wait_for_web_push_delivery_pool_tasks(2)
end
test "does not notify for invisible rooms" do
memberships(:kevin_designers).update! involvement: "invisible"
perform_enqueued_jobs only: Room::PushMessageJob do
WebPush.expects(:payload_send).times(2)
rooms(:designers).messages.create! body: "Hey @kevin", client_message_id: "earth", creator: users(:david)
end
wait_for_web_push_delivery_pool_tasks(2)
end
test "destroys invalid subscriptions" do
memberships(:kevin_designers).update! involvement: "invisible"
assert_difference -> { Push::Subscription.count }, -2 do
perform_enqueued_jobs only: Room::PushMessageJob do
WebPush.expects(:payload_send).times(2).raises(WebPush::ExpiredSubscription.new(Struct.new(:body).new, "example.com"))
rooms(:designers).messages.create! body: "Hey @kevin", client_message_id: "earth", creator: users(:david)
end
wait_for_web_push_delivery_pool_tasks(2)
wait_for_invalidation_pool_tasks(2)
end
end
private
def wait_for_web_push_delivery_pool_tasks(count)
wait_for_pool_tasks(Rails.configuration.x.web_push_pool.delivery_pool, count)
end
def wait_for_invalidation_pool_tasks(count)
wait_for_pool_tasks(Rails.configuration.x.web_push_pool.invalidation_pool, count)
end
def wait_for_pool_tasks(pool, count)
start = Time.now
timeout = 0.2
while pool.completed_task_count < count
raise "Timeout waiting for pool tasks to complete" if Time.now - start > timeout
sleep timeout / 10.0
end
end
end
+37
View File
@@ -0,0 +1,37 @@
require "test_helper"
class RoomTest < ActiveSupport::TestCase
test "grant membership to user" do
rooms(:watercooler).memberships.grant_to(users(:kevin))
assert rooms(:watercooler).users.include?(users(:kevin))
end
test "revoke membership from user" do
rooms(:watercooler).memberships.revoke_from(users(:david))
assert_not rooms(:watercooler).users.include?(users(:david))
end
test "revise memberships" do
rooms(:watercooler).memberships.revise(granted: users(:kevin), revoked: users(:david))
assert rooms(:watercooler).users.include?(users(:kevin))
assert_not rooms(:watercooler).users.include?(users(:david))
end
test "create for users by giving them immediate membership" do
room = Rooms::Closed.create_for({ name: "Hello!", creator: users(:david) }, users: [ users(:kevin), users(:david) ])
assert room.users.include?(users(:kevin))
assert room.users.include?(users(:david))
end
test "type" do
assert Rooms::Open.new.open?
assert_not Rooms::Open.new.direct?
assert Rooms::Direct.new.direct?
assert Rooms::Closed.new.closed?
end
test "default involvement for new users" do
room = Rooms::Closed.create_for({ name: "Hello!", creator: users(:david) }, users: [ users(:kevin), users(:david) ])
assert room.memberships.all? { |m| m.involved_in_mentions? }
end
end
+21
View File
@@ -0,0 +1,21 @@
require "test_helper"
class Rooms::DirectTest < ActiveSupport::TestCase
test "create room for same users" do
room = Rooms::Direct.find_or_create_for([ users(:david), users(:kevin) ])
assert room.users.include?(users(:david))
assert room.users.include?(users(:kevin))
assert_not room.users.include?(users(:jason))
end
test "only one room will exist for the same users" do
room1 = Rooms::Direct.find_or_create_for([ users(:david), users(:kevin) ])
room2 = Rooms::Direct.find_or_create_for([ users(:kevin), users(:david) ])
assert_equal room1, room2
end
test "default involvement for new users" do
room = Rooms::Direct.find_or_create_for([ users(:david), users(:kevin) ])
assert room.memberships.all? { |m| m.involved_in_everything? }
end
end
+14
View File
@@ -0,0 +1,14 @@
require "test_helper"
class Rooms::OpenTest < ActiveSupport::TestCase
test "grants access to all users after creation" do
room = Rooms::Open.create!(name: "My open room with everyone!", creator: users(:david))
assert_equal User.count, room.users.count
end
test "grants access to all users after becoming open" do
room = rooms(:watercooler).becomes!(Rooms::Open)
room.save!
assert_equal User.count, room.users.count
end
end
+41
View File
@@ -0,0 +1,41 @@
require "test_helper"
class User::BotTest < ActiveSupport::TestCase
test "create bot" do
token = "5M0aLYwQyBXOXa5Wsz6NZb11EE4tW2"
SecureRandom.stubs(:alphanumeric).returns(token)
uuid = "3574925f-479d-44f8-82b7-fc039af5367c"
Random.stubs(:uuid).returns(uuid)
bot = User.create_bot!(name: "Bender")
assert_equal "#{bot.id}-#{token}", bot.bot_key
end
test "reset bot key" do
first_token = "5M0aLYwQyBXOXa5Wsz6NZb11EE4tW2"
SecureRandom.stubs(:alphanumeric).returns(first_token)
bot = User.create_bot!(name: "Bender")
assert_equal "#{bot.id}-#{first_token}", bot.bot_key
second_token = "R4kme9anwWRuz3sSoBXiB8Li8ioZPP"
SecureRandom.stubs(:alphanumeric).returns(second_token)
bot.reset_bot_key
assert_equal "#{bot.id}-#{second_token}", bot.bot_key
end
test "authenticate" do
bot = User.create_bot!(name: "Bender")
assert User.authenticate_bot(bot.bot_key)
end
test "deliver message by webhook" do
WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200)
perform_enqueued_jobs only: Bot::WebhookJob do
users(:bender).deliver_webhook_later(messages(:first))
end
end
end
+23
View File
@@ -0,0 +1,23 @@
require "test_helper"
class User::RoleTest < ActiveSupport::TestCase
test "creating subsequent users makes them members" do
assert User.create!(name: "User", email_address: "user@example.com", password: "secret123456").member?
end
test "can_administer?" do
assert User.new(role: :administrator).can_administer?
assert_not User.new(role: :member).can_administer?
assert_not User.new.can_administer?
end
test "can administer a record" do
member = User.new(role: :member)
assert member.can_administer?(Room.new(creator: member))
another_member = User.new(role: :member)
assert another_member.can_administer?(Room.new(creator: member))
assert_not another_member.can_administer?(rooms(:designers))
end
end
+37
View File
@@ -0,0 +1,37 @@
require "test_helper"
class UserTest < ActiveSupport::TestCase
test "user does not prevent very long passwords" do
users(:david).update(password: "secret" * 50)
assert users(:david).valid?
end
test "creating users grants membership to the open rooms" do
assert_difference -> { Membership.count }, +Rooms::Open.count do
create_new_user
end
end
test "deactivating a user deletes push subscriptions, searches, memberships for non-direct rooms, and changes their email address" do
assert_difference -> { Membership.count }, -users(:david).memberships.without_direct_rooms.count do
assert_difference -> { Push::Subscription.count }, -users(:david).push_subscriptions.count do
assert_difference -> { Search.count }, -users(:david).searches.count do
SecureRandom.stubs(:uuid).returns("2e7de450-cf04-4fa8-9b02-ff5ab2d733e7")
users(:david).deactivate
assert_equal "david-deactivated-2e7de450-cf04-4fa8-9b02-ff5ab2d733e7@37signals.com", users(:david).reload.email_address
end
end
end
end
test "deactivating a user deletes their sessions" do
assert_changes -> { users(:david).sessions.count }, from: 1, to: 0 do
users(:david).deactivate
end
end
private
def create_new_user
User.create!(name: "User", email_address: "user@example.com", password: "secret123456")
end
end
+56
View File
@@ -0,0 +1,56 @@
require "test_helper"
class WebhookTest < ActiveSupport::TestCase
test "payload" do
message = messages(:first)
message_path = Rails.application.routes.url_helpers.room_at_message_path(message.room, message)
bot_messages_path = Rails.application.routes.url_helpers.room_bot_messages_path(message.room, users(:bender).bot_key)
WebMock.stub_request(:post, webhooks(:bender).url).
with(body: hash_including(
user: { id: message.creator.id, name: message.creator.name },
room: { id: message.room.id, name: message.room.name, path: bot_messages_path },
message: { id: message.id, body: { html: "First post!", plain: "First post!" }, path: message_path },
))
response = webhooks(:bender).deliver(messages(:first))
assert_equal 200, response.code.to_i
end
test "delivery" do
WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200, body: "", headers: {})
response = webhooks(:bender).deliver(messages(:first))
assert_equal 200, response.code.to_i
end
test "delivery with OK text reply" do
WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200, body: "Hello back!", headers: { "Content-Type" => "text/plain" })
response = webhooks(:bender).deliver(messages(:first))
reply_message = Message.last
assert_equal "Hello back!", reply_message.body.to_plain_text
end
test "delivery with OK attachment reply" do
WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200, body: file_fixture("moon.jpg"), headers: { "Content-Type" => "image/jpeg" })
response = webhooks(:bender).deliver(messages(:first))
reply_message = Message.last
assert reply_message.attachment.present?
end
test "delivery with error reply" do
assert_no_difference -> { Message.count } do
WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 500, body: "Internal Error!", headers: {})
response = webhooks(:bender).deliver(messages(:first))
end
end
test "delivery that times out" do
Webhook.any_instance.stubs(:post).raises(Net::OpenTimeout)
response = webhooks(:bender).deliver(messages(:first))
reply_message = Message.last
assert_equal "Failed to respond within 7 seconds", reply_message.body.to_plain_text
end
end
+107
View File
@@ -0,0 +1,107 @@
import ws from 'k6/ws';
import http from 'k6/http';
import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js';
import { SharedArray } from 'k6/data';
const turboSignedStreamNames = [
"InJvb21zIg==--54acd827f0a7db144c75316a9fc488c0a949f9635b1e47956ce1bd9d1cf2c41d",
"IloybGtPaTh2WTJGdGNHWnBjbVV2VW05dmJYTTZPa05zYjNObFpDOHg6bWVzc2FnZXMi--84f0f3dde5d23eb0fdb410746c2fb76813a4ddff1e2798aac4be0c3d969702ba",
"IloybGtPaTh2WTJGdGNHWnBjbVV2VlhObGNpOHg6cm9vbXMi--df547a679cd41f7531b53d9e48f9883c02481a4da0d862453106441d8546d084"
];
const dummyCookies = new SharedArray('another data name', function () {
// Load CSV file and parse it using Papa Parse
return papaparse.parse(open('cookies.txt'), { header: false }).data;
});
const host = __ENV.HOST == "localhost" ? "host.docker.internal" : __ENV.HOST;
const port = __ENV.PORT ? `:${__ENV.PORT}` : "";
const users = parseInt(__ENV.USERS)
export const options = {
discardResponseBodies: true,
scenarios: {
sockets: {
executor: 'constant-arrival-rate',
duration: '60s',
rate: Math.ceil(users / 3.0),
timeUnit: '1s',
preAllocatedVUs: users,
env: { SCENARIO: 'sockets' },
gracefulStop: "0s"
},
messages: {
executor: 'shared-iterations',
iterations: 1,
vus: 1,
startTime: '30s',
env: { SCENARIO: 'messages' },
gracefulStop: "0s"
},
},
};
export default function() {
if (__ENV.SCENARIO == 'sockets') {
sockets();
} else if (__ENV.SCENARIO == 'messages') {
messages();
}
}
export function sockets() {
const cookie = dummyCookies[Math.floor(Math.random() * parseInt(__ENV.USERS))][0];
const url = `ws://${host}${port}/cable`;
const params = {
headers: { 'Origin': `http://localhost`, 'Cookie': `session_token=${cookie}` }
};
ws.connect(url, params, function(socket) {
socket.on('open', function open() {
// Subscribe to an ActionCable channel
socket.send(JSON.stringify({ command: 'subscribe', identifier: '{"channel":"PresenceChannel", "room_id":1}' }));
socket.send(JSON.stringify({ command: 'subscribe', identifier: '{"channel":"UnreadRoomsChannel"}' }));
socket.send(JSON.stringify({ command: 'subscribe', identifier: '{"channel":"HeartbeatChannel"}' }));
turboSignedStreamNames.forEach((signedStreamName) => {
socket.send(JSON.stringify({ command: 'subscribe', identifier: `{"channel":"Turbo::StreamsChannel", "signed_stream_name":"${signedStreamName}"}` }));
});
// Handle incoming messages
socket.on('message', function(message) {
if (message.includes("confirm_subscription")) {
console.log("Subscription confirmed");
} else if (message.includes("append")) {
console.log("Message received");
}
});
});
socket.on('error', function(e) {
if (e.error() != 'websocket: close sent') {
console.log('An unexpected error occurred: ', e.error());
}
});
});
}
export function messages() {
const cookie = `session_token=${dummyCookies[0][0]}`;
const response = http.get(`http://${host}${port}/rooms/1`, { headers: { "Cookie": cookie }, responseType: "text" });
const csrfToken = response.body.match(/<meta name="csrf-token" content="([^"]*)"/i)[1];
const postHeaders = {
"Cookie": cookie,
"Accept": "text/vnd.turbo-stream.html, text/html, application/xhtml+xml"
}
const payload = {
"message[body]": "Hello from k6",
"message[client_message_id]": Math.random().toString(36),
"authenticity_token": csrfToken
};
for (let i = 0; i < 100; i++) {
http.post(`http://${host}${port}/rooms/1/messages`, payload, { headers: postHeaders, responseType: "text" });
};
}
File diff suppressed because it is too large. Load diff
+8
View File
@@ -0,0 +1,8 @@
key_generator = ActiveSupport::KeyGenerator.new("dummy", iterations: 1000)
signed_cookie_secret = key_generator.generate_key("signed cookie")
signed_cookie_verifier = ActiveSupport::MessageVerifier.new(signed_cookie_secret, digest: "SHA1", serializer: ActiveSupport::MessageEncryptor::NullSerializer)
(1..10000).each do |id|
token = "a" * 19 + id.to_s.rjust(5, "0")
puts signed_cookie_verifier.generate("\"#{token}\"", expires_in: 20.years, purpose: "cookie.session_token")
end
View File
Whitespace-only changes.
+99
View File
@@ -0,0 +1,99 @@
require "application_system_test_case"
class BoostingMessagesTest < ApplicationSystemTestCase
setup do
sign_in "kevin@37signals.com"
join_room rooms(:designers)
end
test "boosting a message" do
within_message messages(:third) do
reveal_message_actions
fill_in_boost_input "Good morning"
click_on "Submit"
assert_boost_text "Good morning"
end
end
test "deleting a boost" do
using_session("David") do
sign_in "david@37signals.com"
join_room rooms(:designers)
within "#" + dom_id(boosts(:first)) do
find("span", text: "Hello").click
assert_selector "button", text: "Delete this boost", wait: 5
click_on "Delete this boost"
end
assert_no_text "Hello"
end
end
test "message update preserves the input state" do
within_message messages(:third) do
assert_message_text "Third time's a charm."
reveal_message_actions
fill_in_boost_input "Hey!"
end
using_session("JZ") do
sign_in "jz@37signals.com"
join_room rooms(:designers)
within_message messages(:third) do
reveal_message_actions
find(".message__edit-btn").click
fill_in_rich_text_area "message_body", with: "Redacted!"
click_on "Save changes"
end
end
within_message messages(:third) do
assert_message_text "Redacted!"
assert_boost_input_value "Hey!"
end
end
test "boost by another user preserves the input state" do
within_message messages(:third) do
assert_message_text "Third time's a charm."
reveal_message_actions
fill_in_boost_input "Hey!"
end
using_session("David") do
sign_in "david@37signals.com"
join_room rooms(:designers)
within_message messages(:third) do
reveal_message_actions
fill_in_boost_input "Morning"
click_on "Submit"
assert_boost_text "Morning"
end
end
perform_enqueued_jobs
within_message messages(:third) do
assert_boost_text "Morning"
assert_boost_input_value "Hey!"
end
end
private
def fill_in_boost_input(text)
click_on "New boost"
fill_in "boost[content]", with: text
end
def assert_boost_input_value(text)
assert page.has_field? "boost[content]", with: text
end
def assert_boost_text(text, **options)
assert_selector ".boost", text: text, **options
end
end
+70
View File
@@ -0,0 +1,70 @@
require "application_system_test_case"
class SendingMessagesTest < ApplicationSystemTestCase
setup do
sign_in "jz@37signals.com"
join_room rooms(:designers)
end
test "sending messages between two users" do
using_session("Kevin") do
sign_in "kevin@37signals.com"
join_room rooms(:designers)
end
join_room rooms(:designers)
send_message "Is this thing on?"
using_session("Kevin") do
join_room rooms(:designers)
assert_message_text "Is this thing on?"
send_message "👍👍"
end
join_room rooms(:designers)
assert_message_text "👍👍"
end
test "editing messages" do
using_session("Kevin") do
sign_in "kevin@37signals.com"
join_room rooms(:designers)
end
within_message messages(:third) do
reveal_message_actions
find(".message__edit-btn").click
fill_in_rich_text_area "message_body", with: "Redacted!"
click_on "Save changes"
end
using_session("Kevin") do
join_room rooms(:designers)
assert_message_text "Redacted!"
end
end
test "deleting messages" do
using_session("Kevin") do
sign_in "kevin@37signals.com"
join_room rooms(:designers)
assert_message_text "Third time's a charm."
end
within_message messages(:third) do
reveal_message_actions
find(".message__edit-btn").click
accept_confirm do
click_on "Delete message"
end
end
using_session("Kevin") do
assert_message_text "Third time's a charm.", count: 0
end
end
end
+27
View File
@@ -0,0 +1,27 @@
require "application_system_test_case"
class UnreadRoomsTest < ApplicationSystemTestCase
setup do
sign_in "jz@37signals.com"
end
test "sending messages between two users" do
designers_room = rooms(:designers)
hq_room = rooms(:hq)
join_room hq_room
assert_room_read hq_room
using_session("Kevin") do
sign_in "kevin@37signals.com"
join_room designers_room
send_message("Hello!!")
send_message("Talking to myself?")
end
assert_room_unread designers_room
join_room designers_room
assert_room_read designers_room
end
end
+38
View File
@@ -0,0 +1,38 @@
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
require "minitest/unit"
require "mocha/minitest"
require "webmock/minitest"
WebMock.enable!
class ActiveSupport::TestCase
# FIXME: Why isn't this included in ActiveSupport::TestCase by default?
include ActiveJob::TestHelper, Turbo::Broadcastable::TestHelper
# FIXME: sqlite3 isn't correctly creating the additional databases per core
# parallelize(workers: :number_of_processors)
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
include SessionTestHelper, MentionTestHelper, TurboTestHelper
setup do
ActionCable.server.pubsub.clear
Rails.configuration.tap do |config|
config.x.web_push_pool.shutdown
config.x.web_push_pool = WebPush::Pool.new \
invalid_subscription_handler: config.x.web_push_pool.invalid_subscription_handler
end
WebMock.disable_net_connect!
end
teardown do
WebMock.reset!
end
end
+7
View File
@@ -0,0 +1,7 @@
module MentionTestHelper
def mention_attachment_for(name)
user = users(name)
attachment_body = ApplicationController.render partial: "users/mention", locals: { user: user }
"<action-text-attachment sgid=\"#{user.attachable_sgid}\" content-type=\"application/vnd.campfire.mention\" content=\"#{attachment_body.gsub('"', '&quot;')}\"></action-text-attachment>"
end
end
+11
View File
@@ -0,0 +1,11 @@
module SessionTestHelper
def parsed_cookies
ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash)
end
def sign_in(user)
user = users(user) unless user.is_a? User
post session_url, params: { email_address: user.email_address, password: "secret123456" }
assert cookies[:session_token].present?
end
end
+56
View File
@@ -0,0 +1,56 @@
module SystemTestHelper
def sign_in(email_address, password = "secret123456")
visit root_url
fill_in "email_address", with: email_address
fill_in "password", with: password
click_on "log_in"
assert_selector "a.btn", text: "Designers"
end
def wait_for_cable_connection
assert_selector "turbo-cable-stream-source[connected]", count: 3, visible: false
end
def join_room(room)
visit room_url(room)
wait_for_cable_connection
dismiss_pwa_install_prompt
end
def send_message(message)
fill_in_rich_text_area "message_body", with: message
click_on "send"
end
def within_message(message, &block)
within "#" + dom_id(message), &block
end
def assert_message_text(text, **options)
assert_selector ".message__body", text: text, **options
end
def assert_room_read(room)
assert_selector ".rooms a", class: "!unread", text: "#{room.name}", wait: 5
end
def assert_room_unread(room)
assert_selector ".rooms a", class: "unread", text: "#{room.name}", wait: 5
end
def reveal_message_actions
find(".message__options-btn").click
rescue Capybara::ElementNotFound
find(".message__options-btn", visible: false).hover.click
ensure
assert_selector ".message__boost-btn", visible: true
end
def dismiss_pwa_install_prompt
if page.has_css?("[data-pwa-install-target~='dialog']", visible: :visible, wait: 5)
click_on("Close")
end
end
end
+17
View File
@@ -0,0 +1,17 @@
module TurboTestHelper
def assert_rendered_turbo_stream_broadcast(*streambles, action:, target:, &block)
streams = find_broadcasts_for(*streambles)
target = ActionView::RecordIdentifier.dom_id(*target)
assert_select Nokogiri::HTML.fragment(streams), %(turbo-stream[action="#{action}"][target="#{target}"]), &block
end
private
def find_broadcasts_for(*streambles)
broadcasting = streambles.collect do |streamble|
streamble.try(:to_gid_param) || streamble
end.join(":")
broadcasts = ActionCable.server.pubsub.broadcasts(broadcasting)
broadcasts.collect { |b| JSON.parse(b) }.join("\n\n")
end
end