loading
Generated 2024-10-25T20:21:01+00:00

All Files ( 95.94% covered at 7.29 hits/line )

188 files in total.
3101 relevant lines, 2975 lines covered and 126 lines missed. ( 95.94% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/channels/application_cable/channel.rb 100.00 % 4 2 2 0 1.00
app/channels/application_cable/connection.rb 100.00 % 20 11 11 0 1.45
app/channels/presence_channel.rb 93.33 % 27 15 14 1 1.73
app/channels/room_channel.rb 100.00 % 14 8 8 0 2.75
app/controllers/accounts/bots/keys_controller.rb 100.00 % 8 5 5 0 1.00
app/controllers/accounts/bots_controller.rb 100.00 % 39 22 22 0 1.32
app/controllers/accounts/custom_styles_controller.rb 100.00 % 20 11 11 0 1.09
app/controllers/accounts/join_codes_controller.rb 100.00 % 8 5 5 0 1.00
app/controllers/accounts/logos_controller.rb 100.00 % 52 29 29 0 1.72
app/controllers/accounts/users_controller.rb 93.33 % 26 15 14 1 1.00
app/controllers/accounts_controller.rb 100.00 % 22 13 13 0 1.08
app/controllers/application_controller.rb 100.00 % 4 3 3 0 1.00
app/controllers/autocompletable/users_controller.rb 100.00 % 14 8 8 0 2.13
app/controllers/concerns/allow_browser.rb 100.00 % 9 5 5 0 1.20
app/controllers/concerns/authentication.rb 96.23 % 95 53 51 2 50.34
app/controllers/concerns/authentication/session_lookup.rb 100.00 % 7 4 4 0 53.75
app/controllers/concerns/authorization.rb 100.00 % 6 4 4 0 5.75
app/controllers/concerns/room_scoped.rb 100.00 % 13 8 8 0 7.25
app/controllers/concerns/set_current_request.rb 100.00 % 13 7 7 0 191.57
app/controllers/concerns/set_platform.rb 100.00 % 12 7 7 0 9.86
app/controllers/concerns/tracked_room_visit.rb 100.00 % 20 11 11 0 2.45
app/controllers/concerns/version_headers.rb 100.00 % 13 8 8 0 58.75
app/controllers/first_runs_controller.rb 100.00 % 25 14 14 0 1.14
app/controllers/messages/boosts_controller.rb 100.00 % 41 21 21 0 1.05
app/controllers/messages/by_bots_controller.rb 100.00 % 24 14 14 0 3.00
app/controllers/messages_controller.rb 97.62 % 82 42 41 1 2.76
app/controllers/qr_code_controller.rb 100.00 % 11 7 7 0 1.00
app/controllers/rooms/closeds_controller.rb 92.11 % 71 38 35 3 2.16
app/controllers/rooms/directs_controller.rb 94.44 % 35 18 17 1 1.89
app/controllers/rooms/involvements_controller.rb 100.00 % 26 13 13 0 1.92
app/controllers/rooms/opens_controller.rb 96.00 % 46 25 24 1 1.20
app/controllers/rooms/refreshes_controller.rb 100.00 % 15 9 9 0 1.00
app/controllers/rooms_controller.rb 96.43 % 51 28 27 1 2.96
app/controllers/searches_controller.rb 100.00 % 32 19 19 0 2.42
app/controllers/sessions/transfers_controller.rb 87.50 % 15 8 7 1 0.88
app/controllers/sessions_controller.rb 100.00 % 40 23 23 0 15.26
app/controllers/unfurl_links_controller.rb 100.00 % 16 9 9 0 2.44
app/controllers/users/avatars_controller.rb 83.33 % 42 24 20 4 1.08
app/controllers/users/profiles_controller.rb 100.00 % 26 14 14 0 1.86
app/controllers/users/push_subscriptions_controller.rb 100.00 % 30 16 16 0 1.38
app/controllers/users/sidebars_controller.rb 100.00 % 25 15 15 0 3.67
app/controllers/users_controller.rb 100.00 % 34 19 19 0 1.32
app/controllers/welcome_controller.rb 80.00 % 9 5 4 1 1.20
app/helpers/accounts_helper.rb 100.00 % 5 3 3 0 2.00
app/helpers/application_helper.rb 95.24 % 44 21 20 1 9.76
app/helpers/broadcasts_helper.rb 85.71 % 13 7 6 1 1.29
app/helpers/clipboard_helper.rb 100.00 % 8 3 3 0 2.33
app/helpers/content_filters.rb 100.00 % 3 2 2 0 1.00
app/helpers/content_filters/remove_solo_unfurled_link_text.rb 95.24 % 38 21 20 1 19.86
app/helpers/content_filters/sanitize_tags.rb 100.00 % 17 9 9 0 214.89
app/helpers/content_filters/style_unfurled_twitter_avatars.rb 100.00 % 24 14 14 0 10.00
app/helpers/drop_target_helper.rb 100.00 % 5 3 3 0 2.00
app/helpers/emoji_helper.rb 100.00 % 12 2 2 0 1.00
app/helpers/forms_helper.rb 100.00 % 8 5 5 0 1.00
app/helpers/messages/attachment_presentation.rb 72.22 % 107 54 39 15 1.13
app/helpers/messages_helper.rb 69.05 % 109 42 29 13 5.12
app/helpers/qr_code_helper.rb 100.00 % 8 4 4 0 2.00
app/helpers/rich_text_helper.rb 100.00 % 11 5 5 0 1.60
app/helpers/rooms/involvements_helper.rb 100.00 % 36 14 14 0 2.64
app/helpers/rooms_helper.rb 90.63 % 84 32 29 3 5.25
app/helpers/searches_helper.rb 100.00 % 12 3 3 0 1.67
app/helpers/time_helper.rb 100.00 % 5 3 3 0 25.33
app/helpers/translations_helper.rb 100.00 % 38 13 13 0 38.31
app/helpers/users/avatars_helper.rb 100.00 % 18 8 8 0 18.00
app/helpers/users/filter_helper.rb 80.00 % 10 5 4 1 1.00
app/helpers/users/profiles_helper.rb 100.00 % 25 8 8 0 2.00
app/helpers/users/sidebar_helper.rb 100.00 % 10 3 3 0 2.33
app/helpers/users_helper.rb 50.00 % 7 4 2 2 0.50
app/helpers/version_helper.rb 100.00 % 5 3 3 0 2.33
app/jobs/application_job.rb 100.00 % 7 1 1 0 1.00
app/jobs/bot/webhook_job.rb 100.00 % 5 3 3 0 1.00
app/jobs/room/push_message_job.rb 100.00 % 5 3 3 0 2.67
app/models/account.rb 100.00 % 5 3 3 0 1.00
app/models/account/joinable.rb 100.00 % 16 9 9 0 2.56
app/models/application_platform.rb 62.50 % 61 32 20 12 2.31
app/models/application_record.rb 100.00 % 3 2 2 0 1.00
app/models/boost.rb 100.00 % 6 4 4 0 10.50
app/models/current.rb 100.00 % 9 5 5 0 28.00
app/models/first_run.rb 100.00 % 16 10 10 0 2.80
app/models/membership.rb 92.86 % 24 14 13 1 9.36
app/models/membership/connectable.rb 96.43 % 51 28 27 1 6.50
app/models/message.rb 90.48 % 38 21 19 2 55.71
app/models/message/attachment.rb 100.00 % 41 22 22 0 6.95
app/models/message/broadcasts.rb 100.00 % 6 4 4 0 6.00
app/models/message/mentionee.rb 100.00 % 16 9 9 0 5.89
app/models/message/pagination.rb 88.24 % 29 17 15 2 1.71
app/models/message/searchable.rb 100.00 % 28 16 16 0 23.06
app/models/opengraph/document.rb 100.00 % 27 15 15 0 18.07
app/models/opengraph/fetch.rb 100.00 % 79 42 42 0 13.38
app/models/opengraph/location.rb 86.21 % 52 29 25 4 24.03
app/models/opengraph/metadata.rb 100.00 % 27 17 17 0 4.24
app/models/opengraph/metadata/fetching.rb 89.19 % 68 37 33 4 5.92
app/models/push.rb 100.00 % 5 3 3 0 1.00
app/models/push/subscription.rb 100.00 % 7 4 4 0 4.25
app/models/room.rb 100.00 % 75 42 42 0 12.33
app/models/room/message_pusher.rb 93.10 % 65 29 27 2 3.59
app/models/rooms/closed.rb 100.00 % 3 1 1 0 1.00
app/models/rooms/direct.rb 100.00 % 22 10 10 0 6.30
app/models/rooms/open.rb 100.00 % 9 5 5 0 6.40
app/models/search.rb 100.00 % 18 10 10 0 1.40
app/models/session.rb 90.00 % 19 10 9 1 32.80
app/models/user.rb 100.00 % 67 38 38 0 9.89
app/models/user/avatar.rb 100.00 % 17 9 9 0 11.89
app/models/user/bot.rb 97.22 % 68 36 35 1 3.06
app/models/user/mentionable.rb 87.50 % 15 8 7 1 4.13
app/models/user/role.rb 100.00 % 11 6 6 0 15.17
app/models/user/transferable.rb 100.00 % 15 8 8 0 1.25
app/models/webhook.rb 100.00 % 79 40 40 0 3.55
config/environment.rb 100.00 % 5 2 2 0 1.00
config/environments/test.rb 100.00 % 56 16 16 0 1.00
config/initializers/active_storage.rb 66.67 % 5 3 2 1 0.67
config/initializers/assets.rb 100.00 % 4 1 1 0 1.00
config/initializers/content_security_policy.rb 100.00 % 25 0 0 0 0.00
config/initializers/extensions.rb 100.00 % 3 2 2 0 3.50
config/initializers/filter_parameter_logging.rb 100.00 % 8 1 1 0 1.00
config/initializers/inflections.rb 100.00 % 16 0 0 0 0.00
config/initializers/permissions_policy.rb 100.00 % 11 0 0 0 0.00
config/initializers/sentry.rb 16.67 % 8 6 1 5 0.17
config/initializers/session_store.rb 100.00 % 4 1 1 0 1.00
config/initializers/sqlite3.rb 52.17 % 36 23 12 11 0.70
config/initializers/storage_paths.rb 100.00 % 5 3 3 0 1.33
config/initializers/time_formats.rb 100.00 % 2 1 1 0 90.00
config/initializers/vapid.rb 100.00 % 4 3 3 0 1.00
config/initializers/version.rb 100.00 % 2 2 2 0 1.00
config/initializers/web_push.rb 46.15 % 44 26 12 14 0.58
config/routes.rb 100.00 % 97 54 54 0 3.78
lib/rails_ext/action_text_attachables.rb 92.86 % 26 14 13 1 11.57
lib/rails_ext/actiontext_opengraph_embeds.rb 85.71 % 44 21 18 3 6.14
lib/rails_ext/filter.rb 85.71 % 24 14 12 2 27.43
lib/rails_ext/filters.rb 100.00 % 12 7 7 0 24.29
lib/rails_ext/string.rb 100.00 % 5 3 3 0 15.33
lib/restricted_http/private_network_guard.rb 92.31 % 25 13 12 1 24.92
lib/web_push/notification.rb 100.00 % 29 13 13 0 7.00
lib/web_push/pool.rb 93.55 % 56 31 29 2 82.87
test/channels/presence_channel_test.rb 100.00 % 51 27 27 0 1.33
test/controllers/accounts/bots/keys_controller_test.rb 100.00 % 14 8 8 0 1.25
test/controllers/accounts/bots_controller_test.rb 100.00 % 45 27 27 0 1.30
test/controllers/accounts/custom_styles_controller_test.rb 100.00 % 29 17 17 0 1.12
test/controllers/accounts/join_codes_controller_test.rb 100.00 % 20 12 12 0 1.25
test/controllers/accounts/logos_controller_test.rb 100.00 % 49 30 30 0 1.53
test/controllers/accounts/users_controller_test.rb 100.00 % 35 20 20 0 1.20
test/controllers/accounts_controller_test.rb 100.00 % 29 17 17 0 1.12
test/controllers/autocompletable/users_controller_test.rb 100.00 % 40 22 22 0 1.14
test/controllers/first_runs_controller_test.rb 100.00 % 33 19 19 0 1.53
test/controllers/messages/boosts_controller_test.rb 100.00 % 26 15 15 0 1.40
test/controllers/messages/by_bots_controller_test.rb 100.00 % 47 26 26 0 1.42
test/controllers/messages_controller_test.rb 100.00 % 152 88 88 0 1.81
test/controllers/qr_code_controller_test.rb 100.00 % 15 9 9 0 1.00
test/controllers/rooms/closeds_controller_test.rb 100.00 % 68 38 38 0 1.26
test/controllers/rooms/directs_controller_test.rb 100.00 % 32 19 19 0 1.32
test/controllers/rooms/involvements_controller_test.rb 100.00 % 46 26 26 0 1.42
test/controllers/rooms/opens_controller_test.rb 100.00 % 51 29 29 0 1.17
test/controllers/rooms/refreshes_controller_test.rb 100.00 % 32 18 18 0 1.00
test/controllers/rooms_controller_test.rb 100.00 % 45 25 25 0 1.40
test/controllers/searches_controller_test.rb 100.00 % 48 27 27 0 1.37
test/controllers/sessions/transfers_controller_test.rb 100.00 % 18 10 10 0 1.00
test/controllers/sessions_controller_test.rb 100.00 % 63 36 36 0 1.06
test/controllers/unfurl_links_controller_test.rb 97.14 % 64 35 34 1 1.20
test/controllers/users/avatars_controller_test.rb 100.00 % 26 15 15 0 1.13
test/controllers/users/profiles_controller_test.rb 100.00 % 28 16 16 0 1.13
test/controllers/users/push_subscriptions_controller_test.rb 100.00 % 38 19 19 0 1.42
test/controllers/users/sidebars_controller_test.rb 100.00 % 30 16 16 0 1.94
test/controllers/users_controller_test.rb 100.00 % 50 29 29 0 1.31
test/controllers/welcome_controller_test.rb 100.00 % 21 11 11 0 1.09
test/helpers/content_filters_test.rb 100.00 % 93 55 55 0 1.15
test/models/account/joinable_test.rb 100.00 % 15 9 9 0 1.22
test/models/account_test.rb 100.00 % 4 2 2 0 1.00
test/models/action_text_attachment_test.rb 100.00 % 38 22 22 0 1.14
test/models/first_run_test.rb 100.00 % 29 18 18 0 1.44
test/models/membership_test.rb 100.00 % 90 57 57 0 1.25
test/models/message/attachment_test.rb 100.00 % 30 16 16 0 1.13
test/models/message/searchable_test.rb 100.00 % 29 17 17 0 1.18
test/models/message_test.rb 100.00 % 34 21 21 0 1.00
test/models/opengraph/document_test.rb 100.00 % 33 23 23 0 1.00
test/models/opengraph/fetch_test.rb 100.00 % 118 57 57 0 1.37
test/models/opengraph/location_test.rb 100.00 % 50 32 32 0 1.00
test/models/opengraph/metadata_test.rb 100.00 % 170 67 67 0 1.18
test/models/room/push_test.rb 100.00 % 79 49 49 0 1.51
test/models/room_test.rb 100.00 % 37 24 24 0 1.08
test/models/rooms/direct_test.rb 100.00 % 21 14 14 0 1.14
test/models/rooms/open_test.rb 100.00 % 14 9 9 0 1.00
test/models/user/bot_test.rb 100.00 % 41 25 25 0 1.00
test/models/user/role_test.rb 100.00 % 23 14 14 0 1.00
test/models/user_test.rb 100.00 % 37 21 21 0 1.48
test/models/webhook_test.rb 100.00 % 56 32 32 0 1.06
test/test_helpers/mention_test_helper.rb 100.00 % 7 5 5 0 5.20
test/test_helpers/session_test_helper.rb 100.00 % 11 7 7 0 47.00
test/test_helpers/turbo_test_helper.rb 100.00 % 17 11 11 0 1.18

app/channels/application_cable/channel.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 module ApplicationCable
  2. 1 class Channel < ActionCable::Channel::Base
  3. end
  4. end

app/channels/application_cable/connection.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 module ApplicationCable
  2. 1 class Connection < ActionCable::Connection::Base
  3. 1 include Authentication::SessionLookup
  4. 1 identified_by :current_user
  5. 1 def connect
  6. 3 self.current_user = find_verified_user
  7. end
  8. 1 private
  9. 1 def find_verified_user
  10. 3 if verified_session = find_session_by_cookie
  11. 1 verified_session.user
  12. else
  13. 2 reject_unauthorized_connection
  14. end
  15. end
  16. end
  17. end

app/channels/presence_channel.rb

93.33% lines covered

15 relevant lines. 14 lines covered and 1 lines missed.
    
  1. 1 class PresenceChannel < RoomChannel
  2. 1 on_subscribe :present, unless: :subscription_rejected?
  3. 1 on_unsubscribe :absent, unless: :subscription_rejected?
  4. 1 def present
  5. 3 membership.present
  6. 3 broadcast_read_room
  7. end
  8. 1 def absent
  9. 1 membership.disconnected
  10. end
  11. 1 def refresh
  12. membership.refresh_connection
  13. end
  14. 1 private
  15. 1 def membership
  16. 7 @room.memberships.find_by(user: current_user)
  17. end
  18. 1 def broadcast_read_room
  19. 3 ActionCable.server.broadcast "user_#{current_user.id}_reads", { room_id: membership.room_id }
  20. end
  21. end

app/channels/room_channel.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 class RoomChannel < ApplicationCable::Channel
  2. 1 def subscribed
  3. 6 if @room = find_room
  4. 3 stream_for @room
  5. else
  6. 3 reject
  7. end
  8. end
  9. 1 private
  10. 1 def find_room
  11. 6 current_user.rooms.find_by(id: params[:room_id])
  12. end
  13. end

app/controllers/accounts/bots/keys_controller.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Accounts::Bots::KeysController < ApplicationController
  2. 1 before_action :ensure_can_administer
  3. 1 def update
  4. 1 User.active_bots.find(params[:bot_id]).reset_bot_key
  5. 1 redirect_to account_bots_url
  6. end
  7. end

app/controllers/accounts/bots_controller.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. 1 class Accounts::BotsController < ApplicationController
  2. 1 before_action :ensure_can_administer
  3. 1 before_action :set_bot, only: %i[ edit update destroy ]
  4. 1 def index
  5. 1 @bots = User.active_bots.ordered
  6. end
  7. 1 def new
  8. 1 @bot = User.active_bots.new
  9. end
  10. 1 def create
  11. 1 User.create_bot! bot_params
  12. 1 redirect_to account_bots_url
  13. end
  14. 1 def edit
  15. end
  16. 1 def update
  17. 2 @bot.update_bot! bot_params
  18. 2 redirect_to account_bots_url
  19. end
  20. 1 def destroy
  21. 1 @bot.deactivate
  22. 1 redirect_to account_bots_url
  23. end
  24. 1 private
  25. 1 def set_bot
  26. 4 @bot = User.active_bots.find(params[:id])
  27. end
  28. 1 def bot_params
  29. 3 params.require(:user).permit(:name, :avatar, :webhook_url)
  30. end
  31. end

app/controllers/accounts/custom_styles_controller.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 class Accounts::CustomStylesController < ApplicationController
  2. 1 before_action :ensure_can_administer, :set_account
  3. 1 def edit
  4. end
  5. 1 def update
  6. 1 @account.update!(account_params)
  7. 1 redirect_to edit_account_custom_styles_url, notice: "✓"
  8. end
  9. 1 private
  10. 1 def set_account
  11. 2 @account = Current.account
  12. end
  13. 1 def account_params
  14. 1 params.require(:account).permit(:custom_styles)
  15. end
  16. end

app/controllers/accounts/join_codes_controller.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Accounts::JoinCodesController < ApplicationController
  2. 1 before_action :ensure_can_administer
  3. 1 def create
  4. 1 Current.account.reset_join_code
  5. 1 redirect_to edit_account_url
  6. end
  7. end

app/controllers/accounts/logos_controller.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. 1 class Accounts::LogosController < ApplicationController
  2. 1 include ActiveStorage::Streaming, ActionView::Helpers::AssetUrlHelper
  3. 1 allow_unauthenticated_access only: :show
  4. 1 before_action :ensure_can_administer, only: :destroy
  5. 1 def show
  6. 4 if stale?(etag: Current.account)
  7. 4 expires_in 5.minutes, public: true, stale_while_revalidate: 1.week
  8. 4 if Current.account&.logo&.attached?
  9. 2 logo = Current.account.logo.variant(logo_variant).processed
  10. 2 send_png_file ActiveStorage::Blob.service.path_for(logo.key)
  11. else
  12. 2 send_stock_icon
  13. end
  14. end
  15. end
  16. 1 def destroy
  17. 1 Current.account.logo.destroy
  18. 1 redirect_to edit_account_url
  19. end
  20. 1 private
  21. 1 LARGE_SQUARE_PNG_VARIANT = { resize_to_limit: [ 512, 512 ], format: :png }
  22. 1 SMALL_SQUARE_PNG_VARIANT = { resize_to_limit: [ 192, 192 ], format: :png }
  23. 1 def send_png_file(path)
  24. 4 send_file path, content_type: "image/png", disposition: :inline
  25. end
  26. 1 def send_stock_icon
  27. 2 if small_logo?
  28. 1 send_png_file logo_path("app-icon-192.png")
  29. else
  30. 1 send_png_file logo_path("app-icon.png")
  31. end
  32. end
  33. 1 def logo_variant
  34. 2 small_logo? ? SMALL_SQUARE_PNG_VARIANT : LARGE_SQUARE_PNG_VARIANT
  35. end
  36. 1 def small_logo?
  37. 4 params[:size] == "small"
  38. end
  39. 1 def logo_path(filename)
  40. 2 Rails.root.join("app/assets/images/logos/#{filename}")
  41. end
  42. end

app/controllers/accounts/users_controller.rb

93.33% lines covered

15 relevant lines. 14 lines covered and 1 lines missed.
    
  1. 1 class Accounts::UsersController < ApplicationController
  2. 1 before_action :ensure_can_administer, :set_user, only: %i[ update destroy ]
  3. 1 def index
  4. set_page_and_extract_portion_from User.active.ordered.without_bots, per_page: 500
  5. end
  6. 1 def update
  7. 1 @user.update(role_params)
  8. 1 redirect_to edit_account_url
  9. end
  10. 1 def destroy
  11. 1 @user.deactivate
  12. 1 redirect_to edit_account_url
  13. end
  14. 1 private
  15. 1 def set_user
  16. 2 @user = User.active.find(params[:user_id] || params[:id])
  17. end
  18. 1 def role_params
  19. 1 { role: params.require(:user)[:role].presence_in(%w[ member administrator ]) || "member" }
  20. end
  21. end

app/controllers/accounts_controller.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 1 class AccountsController < ApplicationController
  2. 1 before_action :ensure_can_administer, only: :update
  3. 1 before_action :set_account
  4. 1 def edit
  5. 1 set_page_and_extract_portion_from User.active.ordered, per_page: 500
  6. end
  7. 1 def update
  8. 1 @account.update!(account_params)
  9. 1 redirect_to edit_account_url, notice: "✓"
  10. end
  11. 1 private
  12. 1 def set_account
  13. 2 @account = Current.account
  14. end
  15. 1 def account_params
  16. 1 params.require(:account).permit(:name, :logo)
  17. end
  18. end

app/controllers/application_controller.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class ApplicationController < ActionController::Base
  2. 1 include AllowBrowser, Authentication, Authorization, SetCurrentRequest, SetPlatform, TrackedRoomVisit, VersionHeaders
  3. 1 include Turbo::Streams::Broadcasts, Turbo::Streams::StreamName
  4. end

app/controllers/autocompletable/users_controller.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 class Autocompletable::UsersController < ApplicationController
  2. 1 def index
  3. 4 set_page_and_extract_portion_from find_autocompletable_users.with_attached_avatar.ordered, per_page: 20
  4. end
  5. 1 private
  6. 1 def find_autocompletable_users
  7. 4 params[:query].present? ? users_scope.active.filtered_by(params[:query]) : users_scope.active
  8. end
  9. 1 def users_scope
  10. 4 params[:room_id].present? ? Current.user.rooms.find(params[:room_id]).users : User.all
  11. end
  12. end

app/controllers/concerns/allow_browser.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 module AllowBrowser
  2. 1 extend ActiveSupport::Concern
  3. 1 VERSIONS = { safari: 17.2, chrome: 120, firefox: 121, opera: 104, ie: false }
  4. 1 included do
  5. 2 allow_browser versions: VERSIONS, block: -> { render template: "sessions/incompatible_browser" }
  6. end
  7. end

app/controllers/concerns/authentication.rb

96.23% lines covered

53 relevant lines. 51 lines covered and 2 lines missed.
    
  1. 1 module Authentication
  2. 1 extend ActiveSupport::Concern
  3. 1 include SessionLookup
  4. 1 included do
  5. 1 before_action :require_authentication
  6. 1 before_action :deny_bots
  7. 1 helper_method :signed_in?
  8. 232 protect_from_forgery with: :exception, unless: -> { authenticated_by.bot_key? }
  9. end
  10. 1 class_methods do
  11. 1 def allow_unauthenticated_access(**options)
  12. 5 skip_before_action :require_authentication, **options
  13. end
  14. 1 def allow_bot_access(**options)
  15. 1 skip_before_action :deny_bots, **options
  16. end
  17. 1 def require_unauthenticated_access(**options)
  18. 1 skip_before_action :require_authentication, **options
  19. 1 before_action :restore_authentication, :redirect_signed_in_user_to_root, **options
  20. end
  21. end
  22. 1 private
  23. 1 def signed_in?
  24. 5 Current.user.present?
  25. end
  26. 1 def require_authentication
  27. 104 restore_authentication || bot_authentication || request_authentication
  28. end
  29. 1 def restore_authentication
  30. 109 if session = find_session_by_cookie
  31. 99 resume_session session
  32. end
  33. end
  34. 1 def bot_authentication
  35. 6 if params[:bot_key].present? && bot = User.authenticate_bot(params[:bot_key].strip)
  36. 6 Current.user = bot
  37. 6 set_authenticated_by(:bot_key)
  38. end
  39. end
  40. 1 def request_authentication
  41. session[:return_to_after_authenticating] = request.url
  42. redirect_to new_session_url
  43. end
  44. 1 def redirect_signed_in_user_to_root
  45. 5 redirect_to root_url if signed_in?
  46. end
  47. 1 def start_new_session_for(user)
  48. 111 user.sessions.start!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
  49. 111 authenticated_as session
  50. end
  51. end
  52. 1 def resume_session(session)
  53. 99 session.resume user_agent: request.user_agent, ip_address: request.remote_ip
  54. 99 authenticated_as session
  55. end
  56. 1 def authenticated_as(session)
  57. 210 Current.user = session.user
  58. 210 set_authenticated_by(:session)
  59. 210 cookies.signed.permanent[:session_token] = { value: session.token, httponly: true, same_site: :lax }
  60. end
  61. 1 def post_authenticating_url
  62. 109 session.delete(:return_to_after_authenticating) || root_url
  63. end
  64. 1 def reset_authentication
  65. 2 cookies.delete(:session_token)
  66. end
  67. 1 def deny_bots
  68. 227 head :forbidden if authenticated_by.bot_key?
  69. end
  70. 1 def set_authenticated_by(method)
  71. 216 @authenticated_by = method.to_s.inquiry
  72. end
  73. 1 def authenticated_by
  74. 458 @authenticated_by ||= "".inquiry
  75. end
  76. end

app/controllers/concerns/authentication/session_lookup.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 module Authentication::SessionLookup
  2. 1 def find_session_by_cookie
  3. 112 if token = cookies.signed[:session_token]
  4. 101 Session.find_by(token: token)
  5. end
  6. end
  7. end

app/controllers/concerns/authorization.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 module Authorization
  2. 1 private
  3. 1 def ensure_can_administer
  4. 20 head :forbidden unless Current.user.can_administer?
  5. end
  6. end

app/controllers/concerns/room_scoped.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 module RoomScoped
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 3 before_action :set_room
  5. end
  6. 1 private
  7. 1 def set_room
  8. 25 @membership = Current.user.memberships.find_by!(room_id: params[:room_id])
  9. 25 @room = @membership.room
  10. end
  11. end

app/controllers/concerns/set_current_request.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 module SetCurrentRequest
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 before_action do
  5. 232 Current.request = request
  6. end
  7. end
  8. 1 def default_url_options
  9. 1104 { host: Current.request_host, protocol: Current.request_protocol }.compact_blank
  10. end
  11. end

app/controllers/concerns/set_platform.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 module SetPlatform
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 helper_method :platform
  5. end
  6. 1 private
  7. 1 def platform
  8. 63 @platform ||= ApplicationPlatform.new(request.user_agent)
  9. end
  10. end

app/controllers/concerns/tracked_room_visit.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 module TrackedRoomVisit
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 helper_method :last_room_visited
  5. end
  6. 1 def remember_last_room_visited
  7. 4 cookies.permanent[:last_room] = @room.id
  8. end
  9. 1 def last_room_visited
  10. 8 Current.user.rooms.find_by(id: cookies[:last_room]) || default_room
  11. end
  12. 1 private
  13. 1 def default_room
  14. 7 Current.user.rooms.original
  15. end
  16. end

app/controllers/concerns/version_headers.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 module VersionHeaders
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 before_action :set_version_headers
  5. end
  6. 1 private
  7. 1 def set_version_headers
  8. 232 response.headers["X-Version"] = Rails.application.config.app_version
  9. 232 response.headers["X-Rev"] = Rails.application.config.git_revision
  10. end
  11. end

app/controllers/first_runs_controller.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 class FirstRunsController < ApplicationController
  2. 1 allow_unauthenticated_access
  3. 1 before_action :prevent_repeats
  4. 1 def show
  5. 1 @user = User.new
  6. end
  7. 1 def create
  8. 1 user = FirstRun.create!(user_params)
  9. 1 start_new_session_for user
  10. 1 redirect_to root_url
  11. end
  12. 1 private
  13. 1 def prevent_repeats
  14. 3 redirect_to root_url if Account.any?
  15. end
  16. 1 def user_params
  17. 1 params.require(:user).permit(:name, :avatar, :email_address, :password)
  18. end
  19. end

app/controllers/messages/boosts_controller.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. 1 class Messages::BoostsController < ApplicationController
  2. 1 before_action :set_message
  3. 1 def index
  4. end
  5. 1 def new
  6. end
  7. 1 def create
  8. 1 @boost = @message.boosts.create!(boost_params)
  9. 1 broadcast_create
  10. 1 redirect_to message_boosts_url(@message)
  11. end
  12. 1 def destroy
  13. 1 @boost = Current.user.boosts.find(params[:id])
  14. 1 @boost.destroy!
  15. 1 broadcast_remove
  16. end
  17. 1 private
  18. 1 def set_message
  19. 2 @message = Current.user.reachable_messages.find(params[:message_id])
  20. end
  21. 1 def boost_params
  22. 1 params.require(:boost).permit(:content)
  23. end
  24. 1 def broadcast_create
  25. 1 @boost.broadcast_append_to @boost.message.room, :messages,
  26. target: "boosts_message_#{@boost.message.client_message_id}", partial: "messages/boosts/boost", attributes: { maintain_scroll: true }
  27. end
  28. 1 def broadcast_remove
  29. 1 @boost.broadcast_remove_to @boost.message.room, :messages
  30. end
  31. end

app/controllers/messages/by_bots_controller.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 class Messages::ByBotsController < MessagesController
  2. 1 allow_bot_access only: :create
  3. 1 def create
  4. 5 super
  5. 5 head :created, location: message_url(@message)
  6. end
  7. 1 private
  8. 1 def message_params
  9. 5 if params[:attachment]
  10. 1 params.permit(:attachment)
  11. else
  12. 8 reading(request.body) { |body| { body: body } }
  13. end
  14. end
  15. 1 def reading(io)
  16. 4 io.rewind
  17. 4 yield io.read.force_encoding("UTF-8")
  18. ensure
  19. 4 io.rewind
  20. end
  21. end

app/controllers/messages_controller.rb

97.62% lines covered

42 relevant lines. 41 lines covered and 1 lines missed.
    
  1. 1 class MessagesController < ApplicationController
  2. 1 include ActiveStorage::SetCurrent, RoomScoped
  3. 1 before_action :set_room, except: :create
  4. 1 before_action :set_message, only: %i[ show edit update destroy ]
  5. 1 before_action :ensure_can_administer, only: %i[ edit update destroy ]
  6. 1 layout false, only: :index
  7. 1 def index
  8. 4 @messages = find_paged_messages
  9. 4 if @messages.any?
  10. 3 fresh_when @messages
  11. else
  12. 1 head :no_content
  13. end
  14. end
  15. 1 def create
  16. 8 set_room
  17. 8 @message = @room.messages.create_with_attachment!(message_params)
  18. 8 @message.broadcast_create
  19. 8 deliver_webhooks_to_bots
  20. rescue ActiveRecord::RecordNotFound
  21. render action: :room_not_found
  22. end
  23. 1 def show
  24. end
  25. 1 def edit
  26. end
  27. 1 def update
  28. 2 @message.update!(message_params)
  29. 2 @message.broadcast_replace_to @room, :messages, target: [ @message, :presentation ], partial: "messages/presentation", attributes: { maintain_scroll: true }
  30. 2 redirect_to room_message_url(@room, @message)
  31. end
  32. 1 def destroy
  33. 2 @message.destroy
  34. 2 @message.broadcast_remove_to @room, :messages
  35. end
  36. 1 private
  37. 1 def set_message
  38. 7 @message = @room.messages.find(params[:id])
  39. end
  40. 1 def ensure_can_administer
  41. 6 head :forbidden unless Current.user.can_administer?(@message)
  42. end
  43. 1 def find_paged_messages
  44. case
  45. 4 when params[:before].present?
  46. 1 @room.messages.with_creator.page_before(@room.messages.find(params[:before]))
  47. when params[:after].present?
  48. 1 @room.messages.with_creator.page_after(@room.messages.find(params[:after]))
  49. else
  50. 2 @room.messages.with_creator.last_page
  51. end
  52. end
  53. 1 def message_params
  54. 5 params.require(:message).permit(:body, :attachment, :client_message_id)
  55. end
  56. 1 def deliver_webhooks_to_bots
  57. 9 bots_eligible_for_webhook.excluding(@message.creator).each { |bot| bot.deliver_webhook_later(@message) }
  58. end
  59. 1 def bots_eligible_for_webhook
  60. 8 @room.direct? ? @room.users.active_bots : @message.mentionees.active_bots
  61. end
  62. end

app/controllers/qr_code_controller.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 class QrCodeController < ApplicationController
  2. 1 allow_unauthenticated_access
  3. 1 def show
  4. 1 url = Base64.urlsafe_decode64(params[:id])
  5. 1 qr_code = RQRCode::QRCode.new(url).as_svg(viewbox: true, fill: :white, color: :black)
  6. 1 expires_in 1.year, public: true
  7. 1 render plain: qr_code, content_type: "image/svg+xml"
  8. end
  9. end

app/controllers/rooms/closeds_controller.rb

92.11% lines covered

38 relevant lines. 35 lines covered and 3 lines missed.
    
  1. 1 class Rooms::ClosedsController < RoomsController
  2. 1 before_action :force_room_type, only: %i[ edit update ]
  3. 1 DEFAULT_ROOM_NAME = "New room"
  4. 1 def show
  5. redirect_to room_url(@room)
  6. end
  7. 1 def new
  8. 1 @room = Rooms::Closed.new(name: DEFAULT_ROOM_NAME)
  9. 1 @users = User.active.ordered
  10. end
  11. 1 def create
  12. 1 room = Rooms::Closed.create_for(room_params, users: grantees)
  13. 1 broadcast_create_room(room)
  14. 1 redirect_to room_url(room)
  15. end
  16. 1 def edit
  17. selected_user_ids = @room.users.pluck(:id)
  18. @selected_users, @unselected_users = User.active.ordered.partition { |user| selected_user_ids.include?(user.id) }
  19. end
  20. 1 def update
  21. 3 @room.update! room_params
  22. 3 @room.memberships.revise(granted: grantees, revoked: revokees)
  23. 3 broadcast_update_room
  24. 3 redirect_to room_url(@room)
  25. end
  26. 1 private
  27. # Allows us to edit an open room and turn it into a closed one on saving.
  28. 1 def force_room_type
  29. 3 @room = @room.becomes!(Rooms::Closed)
  30. end
  31. 1 def grantees
  32. 4 User.where(id: grantee_ids)
  33. end
  34. 1 def revokees
  35. 3 @room.users.where.not(id: grantee_ids)
  36. end
  37. 1 def grantee_ids
  38. 7 params.fetch(:user_ids, [])
  39. end
  40. 1 def broadcast_create_room(room)
  41. 1 each_user_and_html_for(room) do |user, html|
  42. 3 broadcast_prepend_to user, :rooms, target: :shared_rooms, html: html
  43. end
  44. end
  45. 1 def broadcast_update_room
  46. 3 each_user_and_html_for(@room) do |user, html|
  47. 7 broadcast_replace_to user, :rooms, target: [ @room, :list ], html: html
  48. end
  49. end
  50. 1 def each_user_and_html_for(room)
  51. # Optimization to avoid rendering the same partial for every user
  52. 4 html = render_to_string(partial: "users/sidebars/rooms/shared", locals: { room: room })
  53. 14 room.users.each { |user| yield user, html }
  54. end
  55. end

app/controllers/rooms/directs_controller.rb

94.44% lines covered

18 relevant lines. 17 lines covered and 1 lines missed.
    
  1. 1 class Rooms::DirectsController < RoomsController
  2. 1 def new
  3. @room = Rooms::Direct.new
  4. end
  5. 1 def create
  6. 3 room = Rooms::Direct.find_or_create_for(selected_users)
  7. 3 broadcast_create_room(room)
  8. 3 redirect_to room_url(room)
  9. end
  10. 1 def edit
  11. end
  12. 1 private
  13. 1 def selected_users
  14. 3 User.where(id: selected_users_ids.including(Current.user.id))
  15. end
  16. 1 def selected_users_ids
  17. 3 params.fetch(:user_ids, [])
  18. end
  19. 1 def broadcast_create_room(room)
  20. 3 room.memberships.each do |membership|
  21. 6 membership.broadcast_prepend_to membership.user, :rooms, target: :direct_rooms, partial: "users/sidebars/rooms/direct"
  22. end
  23. end
  24. # All users in a direct room can administer it
  25. 1 def ensure_can_administer
  26. 1 true
  27. end
  28. end

app/controllers/rooms/involvements_controller.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 1 class Rooms::InvolvementsController < ApplicationController
  2. 1 include RoomScoped
  3. 1 def show
  4. 1 @involvement = @membership.involvement
  5. end
  6. 1 def update
  7. 4 @membership.update! involvement: params[:involvement]
  8. 4 broadcast_visibility_changes
  9. 4 redirect_to room_involvement_url(@room)
  10. end
  11. 1 private
  12. 1 def broadcast_visibility_changes
  13. case
  14. 4 when @room.direct?
  15. # Do nothing
  16. when @membership.involved_in_invisible?
  17. 1 broadcast_remove_to @membership.user, :rooms, target: [ @room, :list ]
  18. when @membership.involvement_previously_was.inquiry.invisible?
  19. 1 broadcast_prepend_to @membership.user, :rooms, target: :shared_rooms, partial: "users/sidebars/rooms/shared", locals: { room: @room }
  20. end
  21. end
  22. end

app/controllers/rooms/opens_controller.rb

96.0% lines covered

25 relevant lines. 24 lines covered and 1 lines missed.
    
  1. 1 class Rooms::OpensController < RoomsController
  2. 1 before_action :force_room_type, only: %i[ edit update ]
  3. 1 DEFAULT_ROOM_NAME = "New room"
  4. 1 def show
  5. 2 redirect_to room_url(@room)
  6. end
  7. 1 def new
  8. 1 @room = Rooms::Open.new(name: DEFAULT_ROOM_NAME)
  9. 1 @users = User.active.ordered
  10. end
  11. 1 def create
  12. 1 room = Rooms::Open.create_for(room_params, users: Current.user)
  13. 1 broadcast_create_room(room)
  14. 1 redirect_to room_url(room)
  15. end
  16. 1 def edit
  17. @users = User.active.ordered
  18. end
  19. 1 def update
  20. 2 @room.update! room_params
  21. 2 broadcast_update_room
  22. 2 redirect_to room_url(@room)
  23. end
  24. 1 private
  25. # Allows us to edit a closed room and turn it into an open one on saving.
  26. 1 def force_room_type
  27. 2 @room = @room.becomes!(Rooms::Open)
  28. end
  29. 1 def broadcast_create_room(room)
  30. 1 broadcast_prepend_to :rooms, target: :shared_rooms, partial: "users/sidebars/rooms/shared", locals: { room: room }
  31. end
  32. 1 def broadcast_update_room
  33. 2 broadcast_replace_to :rooms, target: [ @room, :list ], partial: "users/sidebars/rooms/shared", locals: { room: @room }
  34. end
  35. end

app/controllers/rooms/refreshes_controller.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 class Rooms::RefreshesController < ApplicationController
  2. 1 include RoomScoped
  3. 1 before_action :set_last_updated_at
  4. 1 def show
  5. 1 @new_messages = @room.messages.with_creator.page_created_since(@last_updated_at)
  6. 1 @updated_messages = @room.messages.without(@new_messages).with_creator.page_updated_since(@last_updated_at)
  7. end
  8. 1 private
  9. 1 def set_last_updated_at
  10. 1 @last_updated_at = Time.at(0, params[:since].to_i, :millisecond)
  11. end
  12. end

app/controllers/rooms_controller.rb

96.43% lines covered

28 relevant lines. 27 lines covered and 1 lines missed.
    
  1. 1 class RoomsController < ApplicationController
  2. 1 before_action :set_room, only: %i[ edit update show destroy ]
  3. 1 before_action :ensure_can_administer, only: %i[ update destroy ]
  4. 1 before_action :remember_last_room_visited, only: :show
  5. 1 def index
  6. 1 redirect_to room_url(Current.user.rooms.last)
  7. end
  8. 1 def show
  9. 2 @messages = find_messages
  10. end
  11. 1 def destroy
  12. 3 @room.destroy
  13. 3 broadcast_remove_room
  14. 3 redirect_to root_url
  15. end
  16. 1 private
  17. 1 def set_room
  18. 16 if room = Current.user.rooms.find_by(id: params[:room_id] || params[:id])
  19. 15 @room = room
  20. else
  21. 1 redirect_to root_url, alert: "Room not found or inaccessible"
  22. end
  23. end
  24. 1 def ensure_can_administer
  25. 10 head :forbidden unless Current.user.can_administer?(@room)
  26. end
  27. 1 def find_messages
  28. 2 messages = @room.messages.with_creator
  29. 2 if show_first_message = messages.find_by(id: params[:message_id])
  30. @messages = messages.page_around(show_first_message)
  31. else
  32. 2 @messages = messages.last_page
  33. end
  34. end
  35. 1 def room_params
  36. 7 params.require(:room).permit(:name)
  37. end
  38. 1 def broadcast_remove_room
  39. 3 broadcast_remove_to :rooms, target: [ @room, :list ]
  40. end
  41. end

app/controllers/searches_controller.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 1 class SearchesController < ApplicationController
  2. 1 before_action :set_messages
  3. 1 def index
  4. 3 @query = query if query.present?
  5. 3 @recent_searches = Current.user.searches.ordered
  6. 3 @return_to_room = last_room_visited
  7. end
  8. 1 def create
  9. 1 Current.user.searches.record(query)
  10. 1 redirect_to searches_url(q: query)
  11. end
  12. 1 def clear
  13. 1 Current.user.searches.destroy_all
  14. 1 redirect_to searches_url
  15. end
  16. 1 private
  17. 1 def set_messages
  18. 5 if query.present?
  19. 3 @messages = Current.user.reachable_messages.search(query).last(100)
  20. else
  21. 2 @messages = Message.none
  22. end
  23. end
  24. 1 def query
  25. 15 params[:q]&.gsub(/[^[:word:]]/, " ")
  26. end
  27. end

app/controllers/sessions/transfers_controller.rb

87.5% lines covered

8 relevant lines. 7 lines covered and 1 lines missed.
    
  1. 1 class Sessions::TransfersController < ApplicationController
  2. 1 allow_unauthenticated_access
  3. 1 def show
  4. end
  5. 1 def update
  6. 1 if user = User.active.find_by_transfer_id(params[:id])
  7. 1 start_new_session_for user
  8. 1 redirect_to post_authenticating_url
  9. else
  10. head :bad_request
  11. end
  12. end
  13. end

app/controllers/sessions_controller.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. 1 class SessionsController < ApplicationController
  2. 1 allow_unauthenticated_access only: %i[ new create ]
  3. 1 rate_limit to: 10, within: 3.minutes, only: :create, with: -> { render_rejection :too_many_requests }
  4. 1 before_action :ensure_user_exists, only: :new
  5. 1 def new
  6. end
  7. 1 def create
  8. 109 if user = User.active.authenticate_by(email_address: params[:email_address], password: params[:password])
  9. 108 start_new_session_for user
  10. 108 redirect_to post_authenticating_url
  11. else
  12. 1 render_rejection :unauthorized
  13. end
  14. end
  15. 1 def destroy
  16. 2 remove_push_subscription
  17. 2 reset_authentication
  18. 2 redirect_to root_url
  19. end
  20. 1 private
  21. 1 def ensure_user_exists
  22. 3 redirect_to first_run_url if User.none?
  23. end
  24. 1 def render_rejection(status)
  25. 1 flash.now[:alert] = "Too many requests or unauthorized."
  26. 1 render :new, status: status
  27. end
  28. 1 def remove_push_subscription
  29. 2 if endpoint = params[:push_subscription_endpoint]
  30. 1 Push::Subscription.destroy_by(endpoint: endpoint, user_id: Current.user.id)
  31. end
  32. end
  33. end

app/controllers/unfurl_links_controller.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 class UnfurlLinksController < ApplicationController
  2. 1 def create
  3. 5 opengraph = Opengraph::Metadata.from_url(url_param)
  4. 4 if opengraph.valid?
  5. 3 render json: opengraph
  6. else
  7. 1 head :no_content
  8. end
  9. end
  10. 1 private
  11. 1 def url_param
  12. 5 params.require(:url)
  13. end
  14. end

app/controllers/users/avatars_controller.rb

83.33% lines covered

24 relevant lines. 20 lines covered and 4 lines missed.
    
  1. 1 class Users::AvatarsController < ApplicationController
  2. 1 include ActiveStorage::Streaming
  3. 2 rescue_from(ActiveSupport::MessageVerifier::InvalidSignature) { head :not_found }
  4. 1 def show
  5. 3 @user = User.from_avatar_token(params[:user_id])
  6. 2 if stale?(etag: @user)
  7. 2 expires_in 30.minutes, public: true, stale_while_revalidate: 1.week
  8. 2 if @user.avatar.attached?
  9. 1 avatar_variant = @user.avatar.variant(SQUARE_WEBP_VARIANT).processed
  10. 1 send_webp_blob_file avatar_variant.key
  11. 1 elsif @user.bot?
  12. render_default_bot
  13. else
  14. 1 render_initials
  15. end
  16. end
  17. end
  18. 1 def destroy
  19. Current.user.avatar.destroy
  20. redirect_to user_profile_url
  21. end
  22. 1 private
  23. 1 SQUARE_WEBP_VARIANT = { resize_to_limit: [ 512, 512 ], format: :webp }
  24. 1 def send_webp_blob_file(key)
  25. 1 send_file ActiveStorage::Blob.service.path_for(key), content_type: "image/webp", disposition: :inline
  26. end
  27. 1 def render_default_bot
  28. send_file Rails.root.join("app/assets/images/default-bot-avatar.svg"), content_type: "image/svg+xml", disposition: :inline
  29. end
  30. 1 def render_initials
  31. 1 render formats: :svg
  32. end
  33. end

app/controllers/users/profiles_controller.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 class Users::ProfilesController < ApplicationController
  2. 1 before_action :set_user
  3. 1 def show
  4. @direct_memberships, @shared_memberships =
  5. 7 Current.user.memberships.with_ordered_room.partition { |m| m.room.direct? }
  6. end
  7. 1 def update
  8. 2 @user.update user_params
  9. 2 redirect_to user_profile_url, notice: update_notice
  10. end
  11. 1 private
  12. 1 def set_user
  13. 3 @user = Current.user
  14. end
  15. 1 def user_params
  16. 2 params.require(:user).permit(:name, :avatar, :email_address, :password, :bio).compact
  17. end
  18. 1 def update_notice
  19. 2 params[:user][:avatar] ? "It may take up to 30 minutes to change everywhere." : "✓"
  20. end
  21. end

app/controllers/users/push_subscriptions_controller.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 1 class Users::PushSubscriptionsController < ApplicationController
  2. 1 before_action :set_push_subscriptions
  3. 1 def index
  4. end
  5. 1 def create
  6. 2 if subscription = @push_subscriptions.find_by(push_subscription_params)
  7. 1 subscription.touch
  8. else
  9. 1 @push_subscriptions.create! push_subscription_params.merge(user_agent: request.user_agent)
  10. end
  11. 2 head :ok
  12. end
  13. 1 def destroy
  14. 1 @push_subscriptions.destroy_by(id: params[:id])
  15. 1 redirect_to user_push_subscriptions_url
  16. end
  17. 1 private
  18. 1 def set_push_subscriptions
  19. 3 @push_subscriptions = Current.user.push_subscriptions
  20. end
  21. 1 def push_subscription_params
  22. 3 params.require(:push_subscription).permit(:endpoint, :p256dh_key, :auth_key)
  23. end
  24. end

app/controllers/users/sidebars_controller.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 1 class Users::SidebarsController < ApplicationController
  2. 1 DIRECT_PLACEHOLDERS = 20
  3. 1 def show
  4. 3 all_memberships = Current.user.memberships.visible.with_ordered_room
  5. 3 @direct_memberships = extract_direct_memberships(all_memberships)
  6. 3 @other_memberships = all_memberships.without(@direct_memberships)
  7. 3 @direct_placeholder_users = find_direct_placeholder_users
  8. end
  9. 1 private
  10. 1 def extract_direct_memberships(all_memberships)
  11. 27 all_memberships.select { |m| m.room.direct? }.sort_by { |m| m.room.updated_at }.reverse
  12. end
  13. 1 def find_direct_placeholder_users
  14. 3 exclude_user_ids = user_ids_already_in_direct_rooms_with_current_user.including(Current.user.id)
  15. 3 User.active.where.not(id: exclude_user_ids).order(:created_at).limit(DIRECT_PLACEHOLDERS - exclude_user_ids.count)
  16. end
  17. 1 def user_ids_already_in_direct_rooms_with_current_user
  18. 3 Membership.where(room_id: Current.user.rooms.directs.pluck(:id)).pluck(:user_id).uniq
  19. end
  20. end

app/controllers/users_controller.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 1 class UsersController < ApplicationController
  2. 1 require_unauthenticated_access only: %i[ new create ]
  3. 1 before_action :set_user, only: :show
  4. 1 before_action :verify_join_code, only: %i[ new create ]
  5. 1 def new
  6. 1 @user = User.new
  7. end
  8. 1 def create
  9. 2 @user = User.create!(user_params)
  10. 1 start_new_session_for @user
  11. 1 redirect_to root_url
  12. rescue ActiveRecord::RecordNotUnique
  13. 1 redirect_to new_session_url(email_address: user_params[:email_address])
  14. end
  15. 1 def show
  16. end
  17. 1 private
  18. 1 def set_user
  19. 1 @user = User.find(params[:id])
  20. end
  21. 1 def verify_join_code
  22. 4 head :not_found if Current.account.join_code != params[:join_code]
  23. end
  24. 1 def user_params
  25. 3 params.require(:user).permit(:name, :avatar, :email_address, :password)
  26. end
  27. end

app/controllers/welcome_controller.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. 1 class WelcomeController < ApplicationController
  2. 1 def show
  3. 2 if Current.user.rooms.any?
  4. 2 redirect_to room_url(last_room_visited)
  5. else
  6. render
  7. end
  8. end
  9. end

app/helpers/accounts_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module AccountsHelper
  2. 1 def account_logo_tag(style: nil)
  3. 4 tag.figure image_tag(fresh_account_logo_path, alt: "Account logo", size: 300), class: "account-logo avatar #{style}"
  4. end
  5. end

app/helpers/application_helper.rb

95.24% lines covered

21 relevant lines. 20 lines covered and 1 lines missed.
    
  1. 1 module ApplicationHelper
  2. 1 def page_title_tag
  3. 26 tag.title @page_title || "Campfire"
  4. end
  5. 1 def current_user_meta_tags
  6. 26 unless Current.user.nil?
  7. 19 safe_join [
  8. tag(:meta, name: "current-user-id", content: Current.user.id),
  9. tag(:meta, name: "current-user-name", content: Current.user.name)
  10. ]
  11. end
  12. end
  13. 1 def custom_styles_tag
  14. 26 if custom_styles = Current.account&.custom_styles
  15. tag.style(custom_styles.to_s.html_safe, data: { turbo_track: "reload" })
  16. end
  17. end
  18. 1 def body_classes
  19. 26 [ @body_class, admin_body_class, account_logo_body_class ].compact.join(" ")
  20. end
  21. 1 def link_back
  22. 2 link_back_to request.referrer || root_path
  23. end
  24. 1 def link_back_to(destination)
  25. 9 link_to destination, class: "btn" do
  26. 9 image_tag("arrow-left.svg", aria: { hidden: "true" }, size: 20) +
  27. tag.span("Go Back", class: "for-screen-reader")
  28. end
  29. end
  30. 1 private
  31. 1 def admin_body_class
  32. 26 "admin" if Current.user&.can_administer?
  33. end
  34. 1 def account_logo_body_class
  35. 26 "account-has-logo" if Current.account&.logo&.attached?
  36. end
  37. end

app/helpers/broadcasts_helper.rb

85.71% lines covered

7 relevant lines. 6 lines covered and 1 lines missed.
    
  1. 1 module BroadcastsHelper
  2. 1 def broadcast_image_tag(image, options)
  3. 2 image_tag(broadcast_image_path(image), options)
  4. end
  5. 1 def broadcast_image_path(image)
  6. 2 if image.is_a?(Symbol) || image.is_a?(String)
  7. image_path(image)
  8. else
  9. 2 polymorphic_url(image, only_path: true)
  10. end
  11. end
  12. end

app/helpers/clipboard_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module ClipboardHelper
  2. 1 def button_to_copy_to_clipboard(url, &)
  3. 5 tag.button class: "btn", data: {
  4. controller: "copy-to-clipboard", action: "copy-to-clipboard#copy",
  5. copy_to_clipboard_success_class: "btn--success", copy_to_clipboard_content_value: url
  6. }, &
  7. end
  8. end

app/helpers/content_filters.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 module ContentFilters
  2. 1 TextMessagePresentationFilters = ActionText::Content::Filters.new(RemoveSoloUnfurledLinkText, StyleUnfurledTwitterAvatars, SanitizeTags)
  3. end

app/helpers/content_filters/remove_solo_unfurled_link_text.rb

95.24% lines covered

21 relevant lines. 20 lines covered and 1 lines missed.
    
  1. 1 class ContentFilters::RemoveSoloUnfurledLinkText < ActionText::Content::Filter
  2. 1 def applicable?
  3. 41 normalize_tweet_url(solo_unfurled_url) == normalize_tweet_url(content.to_plain_text)
  4. end
  5. 1 def apply
  6. 9 fragment.replace("div") { |node| node.tap { |n| n.inner_html = unfurled_links.first.to_s } }
  7. end
  8. 1 private
  9. 1 TWITTER_DOMAINS = %w[ x.com twitter.com ]
  10. 1 TWITTER_DOMAIN_MAPPING = { "x.com" => "twitter.com" }
  11. 1 def solo_unfurled_url
  12. 41 unfurled_links.first["href"] if unfurled_links.size == 1
  13. end
  14. 1 def unfurled_links
  15. 48 fragment.find_all("action-text-attachment[@content-type='#{ActionText::Attachment::OpengraphEmbed::OPENGRAPH_EMBED_CONTENT_TYPE}']")
  16. end
  17. 1 def normalize_tweet_url(url)
  18. 82 return url unless twitter_url?(url)
  19. 4 uri = URI.parse(url)
  20. 4 uri.dup.tap do |u|
  21. 4 u.host = TWITTER_DOMAIN_MAPPING[uri.host&.downcase] || uri.host
  22. 4 u.query = nil
  23. end.to_s
  24. rescue URI::InvalidURIError
  25. url
  26. end
  27. 1 def twitter_url?(url)
  28. 170 url.present? && TWITTER_DOMAINS.any? { |domain| url.strip.include?(domain) }
  29. end
  30. end

app/helpers/content_filters/sanitize_tags.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 class ContentFilters::SanitizeTags < ActionText::Content::Filter
  2. 1 def applicable?
  3. 41 true
  4. end
  5. 1 def apply
  6. 42 fragment.replace(not_allowed_tags_css_selector) { nil }
  7. end
  8. 1 private
  9. 1 ALLOWED_TAGS = %w[ a abbr acronym address b big blockquote br cite code dd del dfn div dl dt em h1 h2 h3 h4 h5 h6 hr i ins kbd li ol
  10. p pre samp small span strong sub sup time tt ul var ] + [ ActionText::Attachment.tag_name, "figure", "figcaption" ]
  11. 1 def not_allowed_tags_css_selector
  12. 1845 ALLOWED_TAGS.map { |tag| ":not(#{tag})" }.join("")
  13. end
  14. end

app/helpers/content_filters/style_unfurled_twitter_avatars.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 class ContentFilters::StyleUnfurledTwitterAvatars < ActionText::Content::Filter
  2. 1 def applicable?
  3. 43 unfurled_twitter_avatars.present?
  4. end
  5. 1 def apply
  6. 1 fragment.update do |source|
  7. 1 div = source.at_css("div")
  8. 1 div["class"] = UNFURLED_TWITTER_AVATAR_CSS_CLASS
  9. end
  10. end
  11. 1 private
  12. 1 UNFURLED_TWITTER_AVATAR_CSS_CLASS = "cf-twitter-avatar"
  13. 1 TWITTER_AVATAR_URL_PREFIX = "https://pbs.twimg.com/profile_images"
  14. 1 def unfurled_twitter_avatars
  15. 43 fragment.find_all("#{opengraph_css_selector}[url*='#{TWITTER_AVATAR_URL_PREFIX}']")
  16. end
  17. 1 def opengraph_css_selector
  18. 43 "action-text-attachment[@content-type='#{ActionText::Attachment::OpengraphEmbed::OPENGRAPH_EMBED_CONTENT_TYPE}']"
  19. end
  20. end

app/helpers/drop_target_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module DropTargetHelper
  2. 1 def drop_target_actions
  3. 4 "dragenter->drop-target#dragenter dragover->drop-target#dragover drop->drop-target#drop"
  4. end
  5. end

app/helpers/emoji_helper.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 module EmojiHelper
  2. REACTIONS = {
  3. 1 "👍" => "Thumbs up",
  4. "👏" => "Clapping",
  5. "👋" => "Waving hand",
  6. "💪" => "Muscle",
  7. "❤️" => "Red heart",
  8. "😂" => "Face with tears of joy",
  9. "🎉" => "Party popper",
  10. "🔥" => "Fire"
  11. }
  12. end

app/helpers/forms_helper.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 module FormsHelper
  2. 1 def auto_submit_form_with(**attributes, &)
  3. 1 data = attributes.delete(:data) || {}
  4. 1 data[:controller] = "auto-submit #{data[:controller]}".strip
  5. 1 form_with **attributes, data: data, &
  6. end
  7. end

app/helpers/messages/attachment_presentation.rb

72.22% lines covered

54 relevant lines. 39 lines covered and 15 lines missed.
    
  1. 1 class Messages::AttachmentPresentation
  2. 1 def initialize(message, context:)
  3. 2 @message, @context = message, context
  4. end
  5. 1 def render
  6. 2 if message.attachment.attached?
  7. 2 if message.attachment.previewable? || message.attachment.variable?
  8. 2 render_preview
  9. else
  10. render_link
  11. end
  12. end
  13. end
  14. 1 private
  15. 1 attr_reader :message, :context
  16. 1 delegate :tag, :link_to, :broadcast_image_tag, :rails_blob_path, :url_for, to: :context
  17. 1 def render_preview
  18. 2 if message.attachment.video?
  19. video_preview_tag
  20. else
  21. 2 lightboxed_image_preview_tag
  22. end
  23. end
  24. 1 def video_preview_tag
  25. width, height = preview_dimensions
  26. inline_media_dimension_constraints(width, height) do
  27. tag.video \
  28. src: rails_blob_path(message.attachment), poster: url_for(message.attachment.preview(format: :webp, resize_to_limit: [ Message::THUMBNAIL_MAX_WIDTH, Message::THUMBNAIL_MAX_HEIGHT ])),
  29. controls: true, preload: :none, width: "100%", height: "100%", class: "message__attachment"
  30. end
  31. end
  32. 1 def lightboxed_image_preview_tag
  33. 2 width, height = preview_dimensions
  34. 2 inline_media_dimension_constraints(width, height) do
  35. 2 lightbox_link do
  36. 2 broadcast_image_tag message.attachment.representation(:thumb), width: width, height: height, class: "message__attachment", loading: "lazy"
  37. end
  38. end
  39. end
  40. 1 def inline_media_dimension_constraints(width, height, &)
  41. 2 if width && height
  42. 2 aspect_ratio = (width / height.to_f)
  43. 2 tag.div class: "max-inline-size center flex overflow-clip", style: "width: #{width / 2}px; aspect-ratio: #{aspect_ratio};", &
  44. else
  45. tag.div class: "max-inline-size center overflow-clip", &
  46. end
  47. end
  48. 1 def preview_dimensions
  49. 2 width = message.attachment.metadata[:width]
  50. 2 height = message.attachment.metadata[:height]
  51. case
  52. 2 when width.nil? || height.nil?
  53. [ nil, nil ]
  54. when width <= Message::THUMBNAIL_MAX_WIDTH && height <= Message::THUMBNAIL_MAX_HEIGHT
  55. [ width, height ]
  56. else
  57. 2 width_factor = Message::THUMBNAIL_MAX_WIDTH.to_f / width
  58. 2 height_factor = Message::THUMBNAIL_MAX_HEIGHT.to_f / height
  59. 2 scale_factor = [ width_factor, height_factor ].min
  60. 2 [ width * scale_factor, height * scale_factor ]
  61. end
  62. end
  63. 1 def render_link
  64. tag.div class: "flex-inline align-center gap-half" do
  65. broadcast_image_tag("common-file-text.svg", size: 22, class: "colorize--black", aria: { hidden: "true" }) +
  66. tag.span(filename) + download_link + share_button
  67. end
  68. end
  69. 1 def lightbox_link(&)
  70. 2 link_to rails_blob_path(message.attachment), class: "flex", data: {
  71. lightbox_target: "image", action: "lightbox#open", lightbox_url_value: download_url }, &
  72. end
  73. 1 def download_link
  74. link_to download_url, class: "btn message__action-btn hide-in-ios-pwa", style: "--width: auto;" do
  75. broadcast_image_tag("download.svg", aria: { hidden: "true" }, size: 20) + tag.span("Download #{ filename }", class: "for-screen-reader")
  76. end
  77. end
  78. 1 def share_button
  79. tag.button class: "btn message__action-btn", style: "--width: auto;", data: { controller: "web-share", action: "web-share#share", web_share_files_value: download_url } do
  80. broadcast_image_tag("share.svg", aria: { hidden: "true" }, size: 20) + tag.span("Share #{ filename }", class: "for-screen-reader")
  81. end
  82. end
  83. 1 def filename
  84. message.attachment.filename.to_s
  85. end
  86. 1 def download_url
  87. 2 rails_blob_path message.attachment, disposition: "attachment", only_path: true
  88. end
  89. end

app/helpers/messages_helper.rb

69.05% lines covered

42 relevant lines. 29 lines covered and 13 lines missed.
    
  1. 1 module MessagesHelper
  2. 1 def message_area_tag(room, &)
  3. 2 tag.div id: "message-area", class: "message-area", contents: true, data: {
  4. controller: "messages presence drop-target",
  5. action: [ messages_actions, drop_target_actions, presence_actions ].join(" "),
  6. messages_first_of_day_class: "message--first-of-day",
  7. messages_formatted_class: "message--formatted",
  8. messages_me_class: "message--me",
  9. messages_mentioned_class: "message--mentioned",
  10. messages_threaded_class: "message--threaded",
  11. messages_page_url_value: room_messages_url(room)
  12. }, &
  13. end
  14. 1 def messages_tag(room, &)
  15. 2 tag.div id: dom_id(room, :messages), class: "messages", data: {
  16. controller: "maintain-scroll refresh-room",
  17. action: [ maintain_scroll_actions, refresh_room_actions ].join(" "),
  18. messages_target: "messages",
  19. refresh_room_loaded_at_value: room.updated_at.to_fs(:epoch),
  20. refresh_room_url_value: room_refresh_url(room)
  21. }, &
  22. end
  23. 1 def message_tag(message, &)
  24. 37 message_timestamp_milliseconds = message.created_at.to_fs(:epoch)
  25. 37 tag.div id: dom_id(message),
  26. class: "message #{"message--emoji" if message.plain_text_body.all_emoji?}",
  27. data: {
  28. controller: "reply",
  29. user_id: message.creator_id,
  30. message_id: message.id,
  31. message_timestamp: message_timestamp_milliseconds,
  32. message_updated_at: message.updated_at.to_fs(:epoch),
  33. sort_value: message_timestamp_milliseconds,
  34. messages_target: "message",
  35. search_results_target: "message",
  36. refresh_room_target: "message",
  37. reply_composer_outlet: "#composer"
  38. }, &
  39. rescue Exception => e
  40. Sentry.capture_exception(e, extra: { message: message })
  41. Rails.logger.error "Exception while rendering message #{message.class.name}##{message.id}, failed with: #{e.class} `#{e.message}`"
  42. render "messages/unrenderable"
  43. end
  44. 1 def message_timestamp(message, **attributes)
  45. 37 local_datetime_tag message.created_at, **attributes
  46. end
  47. 1 def message_presentation(message)
  48. 37 case message.content_type
  49. when "attachment"
  50. 2 message_attachment_presentation(message)
  51. when "sound"
  52. message_sound_presentation(message)
  53. else
  54. 35 auto_link h(ContentFilters::TextMessagePresentationFilters.apply(message.body.body)), html: { target: "_blank" }
  55. end
  56. rescue Exception => e
  57. Sentry.capture_exception(e, extra: { message: message })
  58. Rails.logger.error "Exception while generating message representation for #{message.class.name}##{message.id}, failed with: #{e.class} `#{e.message}`"
  59. ""
  60. end
  61. 1 private
  62. 1 def messages_actions
  63. 2 "turbo:before-stream-render@document->messages#beforeStreamRender keydown.up@document->messages#editMyLastMessage"
  64. end
  65. 1 def maintain_scroll_actions
  66. 2 "turbo:before-stream-render@document->maintain-scroll#beforeStreamRender"
  67. end
  68. 1 def refresh_room_actions
  69. 2 "visibilitychange@document->refresh-room#visibilityChanged online@window->refresh-room#online"
  70. end
  71. 1 def presence_actions
  72. 2 "visibilitychange@document->presence#visibilityChanged"
  73. end
  74. 1 def message_attachment_presentation(message)
  75. 2 Messages::AttachmentPresentation.new(message, context: self).render
  76. end
  77. 1 def message_sound_presentation(message)
  78. sound = message.sound
  79. tag.div class: "sound", data: { controller: "sound", action: "messages:play->sound#play", sound_url_value: asset_path(sound.asset_path) } do
  80. play_button + (sound.image ? sound_image_tag(sound.image) : sound.text)
  81. end
  82. end
  83. 1 def play_button
  84. tag.button "🔊", class: "btn btn--plain", data: { action: "sound#play" }
  85. end
  86. 1 def sound_image_tag(image)
  87. image_tag image.asset_path, width: image.width, height: image.height, class: "align--middle"
  88. end
  89. 1 def message_author_title(author)
  90. [ author.name, author.bio ].compact_blank.join(" – ")
  91. end
  92. end

app/helpers/qr_code_helper.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 module QrCodeHelper
  2. 1 def link_to_zoom_qr_code(url, &)
  3. 3 id = Base64.urlsafe_encode64(url)
  4. 3 link_to qr_code_path(id), class: "btn", data: {
  5. lightbox_target: "image", action: "lightbox#open", lightbox_url_value: qr_code_path(id) }, &
  6. end
  7. end

app/helpers/rich_text_helper.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 module RichTextHelper
  2. 1 def rich_text_data_actions
  3. default_actions =
  4. 2 "trix-change->typing-notifications#start keydown->composer#submitByKeyboard"
  5. autocomplete_actions =
  6. 2 "trix-focus->rich-autocomplete#focus trix-change->rich-autocomplete#search trix-blur->rich-autocomplete#blur"
  7. 2 [ default_actions, autocomplete_actions ].join(" ")
  8. end
  9. end

app/helpers/rooms/involvements_helper.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 module Rooms::InvolvementsHelper
  2. 1 def turbo_frame_for_involvement_tag(room, &)
  3. 1 turbo_frame_tag dom_id(room, :involvement), data: {
  4. controller: "turbo-frame", action: "notifications:ready@window->turbo-frame#load", turbo_frame_url_param: room_involvement_path(room)
  5. }, &
  6. end
  7. 1 def button_to_change_involvement(room, involvement)
  8. 7 button_to room_involvement_path(room, involvement: next_involvement_for(room, involvement: involvement)),
  9. method: :put,
  10. role: "checkbox", aria: { checked: true, labelledby: dom_id(room, :involvement_label) }, tabindex: 0,
  11. class: "btn #{involvement}" do
  12. 7 image_tag("notification-bell-#{involvement}.svg", aria: { hidden: "true" }, size: 20) +
  13. tag.span(HUMANIZE_INVOLVEMENT[involvement], class: "for-screen-reader", id: dom_id(room, :involvement_label))
  14. end
  15. end
  16. 1 private
  17. HUMANIZE_INVOLVEMENT = {
  18. 1 "mentions" => "Notifying about @ mentions",
  19. "everything" => "Notifying about all messages",
  20. "nothing" => "Notifications are off",
  21. "invisible" => "Notifications are off and room invisible in sidebar"
  22. }
  23. 1 SHARED_INVOLVEMENT_ORDER = %w[ mentions everything nothing invisible ]
  24. 1 DIRECT_INVOLVEMENT_ORDER = %w[ everything nothing ]
  25. 1 def next_involvement_for(room, involvement:)
  26. 7 if room.direct?
  27. 2 DIRECT_INVOLVEMENT_ORDER[DIRECT_INVOLVEMENT_ORDER.index(involvement) + 1] || DIRECT_INVOLVEMENT_ORDER.first
  28. else
  29. 5 SHARED_INVOLVEMENT_ORDER[SHARED_INVOLVEMENT_ORDER.index(involvement) + 1] || SHARED_INVOLVEMENT_ORDER.first
  30. end
  31. end
  32. end

app/helpers/rooms_helper.rb

90.63% lines covered

32 relevant lines. 29 lines covered and 3 lines missed.
    
  1. 1 module RoomsHelper
  2. 1 def link_to_room(room, **attributes, &)
  3. 32 link_to room_path(room), **attributes, data: {
  4. rooms_list_target: "room", room_id: room.id, badge_dot_target: "unread", sorted_list_target: "item"
  5. }.merge(attributes.delete(:data) || {}), &
  6. end
  7. 1 def link_to_edit_room(room, &)
  8. 2 link_to \
  9. [ :edit, @room ],
  10. class: "btn",
  11. style: "view-transition-name: edit-room-#{@room.id}",
  12. data: { room_id: @room.id },
  13. &
  14. end
  15. 1 def link_back_to_last_room_visited
  16. 3 if last_room = last_room_visited
  17. 3 link_back_to room_path(last_room)
  18. else
  19. link_back_to root_path
  20. end
  21. end
  22. 1 def button_to_delete_room(room, url: nil)
  23. button_to url || room_url(room), method: :delete, class: "btn btn--negative max-width", aria: { label: "Delete #{room.name}" },
  24. data: { turbo_confirm: "Are you sure you want to delete this room and all messages in it? This can’t be undone." } do
  25. image_tag("trash.svg", aria: { hidden: "true" }, size: 20) +
  26. tag.span(room_display_name(room), class: "overflow-ellipsis")
  27. end
  28. end
  29. 1 def button_to_jump_to_newest_message
  30. 2 tag.button \
  31. class: "message-area__return-to-latest btn",
  32. data: { action: "messages#returnToLatest", messages_target: "latest" },
  33. hidden: true do
  34. 2 image_tag("arrow-down.svg", aria: { hidden: "true" }, size: 20) +
  35. tag.span("Jump to newest message", class: "for-screen-reader")
  36. end
  37. end
  38. 1 def submit_room_button_tag
  39. 2 button_tag class: "btn btn--reversed txt-large center", type: "submit" do
  40. 2 image_tag("check.svg", aria: { hidden: "true" }, size: 20) +
  41. tag.span("Save", class: "for-screen-reader")
  42. end
  43. end
  44. 1 def composer_form_tag(room, &)
  45. 2 form_with model: Message.new, url: room_messages_path(room),
  46. id: "composer", class: "margin-block flex-item-grow contain", data: composer_data_options(room), &
  47. end
  48. 1 def room_display_name(room, for_user: Current.user)
  49. 48 if room.direct?
  50. 7 room.users.without(for_user).pluck(:name).to_sentence.presence || for_user&.name
  51. else
  52. 41 room.name
  53. end
  54. end
  55. 1 private
  56. 1 def composer_data_options(room)
  57. {
  58. 2 controller: "composer drop-target",
  59. action: composer_data_actions,
  60. composer_messages_outlet: "#message-area",
  61. composer_toolbar_class: "composer--rich-text", composer_room_id_value: room.id
  62. }
  63. end
  64. 1 def composer_data_actions
  65. 2 drag_and_drop_actions = "drop-target:drop@window->composer#dropFiles"
  66. trix_attachment_actions =
  67. 2 "trix-file-accept->composer#preventAttachment refresh-room:online@window->composer#online"
  68. remaining_actions =
  69. 2 "typing-notifications#stop paste->composer#pasteFiles turbo:submit-end->composer#submitEnd refresh-room:offline@window->composer#offline"
  70. 2 [ drop_target_actions, drag_and_drop_actions, trix_attachment_actions, remaining_actions ].join(" ")
  71. end
  72. end

app/helpers/searches_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module SearchesHelper
  2. 1 def search_results_tag(&)
  3. 3 tag.div id: "search-results", class: "messages searches__results", data: {
  4. controller: "search-results",
  5. search_results_target: "messages",
  6. search_results_me_class: "message--me",
  7. search_results_threaded_class: "message--threaded",
  8. search_results_mentioned_class: "message--mentioned",
  9. search_results_formatted_class: "message--formatted"
  10. }, &
  11. end
  12. end

app/helpers/time_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module TimeHelper
  2. 1 def local_datetime_tag(datetime, style: :time, **attributes)
  3. 74 tag.time **attributes, datetime: datetime.iso8601, data: { local_time_target: style }
  4. end
  5. end

app/helpers/translations_helper.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 1 module TranslationsHelper
  2. TRANSLATIONS = {
  3. 1 email_address: { "🇺🇸": "Enter your email address", "🇪🇸": "Introduce tu correo electrónico", "🇫🇷": "Entrez votre adresse courriel", "🇮🇳": "अपना ईमेल पता दर्ज करें", "🇩🇪": "Geben Sie Ihre E-Mail-Adresse ein", "🇧🇷": "Insira seu endereço de email" },
  4. password: { "🇺🇸": "Enter your password", "🇪🇸": "Introduce tu contraseña", "🇫🇷": "Saisissez votre mot de passe", "🇮🇳": "अपना पासवर्ड दर्ज करें", "🇩🇪": "Geben Sie Ihr Passwort ein", "🇧🇷": "Insira sua senha" },
  5. update_password: { "🇺🇸": "Change password", "🇪🇸": "Cambiar contraseña", "🇫🇷": "Changer le mot de passe", "🇮🇳": "पासवर्ड बदलें", "🇩🇪": "Passwort ändern", "🇧🇷": "Alterar senha" },
  6. user_name: { "🇺🇸": "Enter your name", "🇪🇸": "Introduce tu nombre", "🇫🇷": "Entrez votre nom", "🇮🇳": "अपना नाम दर्ज करें", "🇩🇪": "Geben Sie Ihren Namen ein", "🇧🇷": "Insira seu nome" },
  7. account_name: { "🇺🇸": "Name this account", "🇪🇸": "Nombre de esta cuenta", "🇫🇷": "Nommez ce compte", "🇮🇳": "इस खाते का नाम दें", "🇩🇪": "Benennen Sie dieses Konto", "🇧🇷": "Dê um nome a essa conta" },
  8. room_name: { "🇺🇸": "Name the room", "🇪🇸": "Nombrar la sala", "🇫🇷": "Nommez la salle", "🇮🇳": "कमरे का नाम दें", "🇩🇪": "Geben Sie dem Raum einen Namen", "🇧🇷": "Dê um nome a essa sala" },
  9. invite_message: { "🇺🇸": "Welcome to Campfire. To invite some people to chat with you, share the join link below.", "🇪🇸": "Bienvenido a Campfire. Para invitar a algunas personas a chatear contigo, comparte el enlace de unión que se encuentra a continuación.", "🇫🇷": "Bienvenue sur Campfire. Pour inviter des personnes à discuter avec vous, partagez le lien pour rejoindre ci-dessous.", "🇮🇳": "Campfire में आपका स्वागत है। अधिक लोगों को चैट के लिए आमंत्रित करने के लिए, नीचे जुड़ने का लिंक साझा करें।", "🇩🇪": "Willkommen bei Campfire. Um einige Personen zum Chatten einzuladen, teilen Sie den unten stehenden Beitrittslink.", "🇧🇷": "Boas vindas ao Campfire. Para convidar pessoas para conversarem com você, compartilhe o link de convite abaixo." },
  10. incompatible_browser_messsage: { "🇺🇸": "Upgrade to a supported web browser. Campfire requires a modern web browser. Please use one of the browsers listed below and make sure auto-updates are enabled.", "🇪🇸": "Actualiza a un navegador web compatible. Campfire requiere un navegador web moderno. Utiliza uno de los navegadores listados a continuación y asegúrate de que las actualizaciones automáticas estén habilitadas.", "🇫🇷": "Mettez à jour vers un navigateur web pris en charge. Campfire nécessite un navigateur web moderne. Veuillez utiliser l'un des navigateurs répertoriés ci-dessous et assurez-vous que les mises à jour automatiques sont activées.", "🇮🇳": "समर्थित वेब ब्राउज़र में अपग्रेड करें। Campfire को एक आधुनिक वेब ब्राउज़र की आवश्यकता है। कृपया नीचे सूचीबद्ध ब्राउज़रों में से कोई एक का उपयोग करें और सुनिश्चित करें कि स्वचालित अपडेट्स सक्षम हैं।", "🇩🇪": "Aktualisieren Sie auf einen unterstützten Webbrowser. Campfire erfordert einen modernen Webbrowser. Verwenden Sie bitte einen der unten aufgeführten Browser und stellen Sie sicher, dass automatische Updates aktiviert sind.", "🇧🇷": "Atualize para um navegador compatível. O Campfire requer um navegador moderno. Por favor, use um dos navegadores listados abaixo e certifique-se de que as atualizações automáticas estão ativadas." },
  11. bio: { "🇺🇸": "Enter a few words about yourself.", "🇪🇸": "Ingresa algunas palabras sobre ti mismo.", "🇫🇷": "Saisissez quelques mots à propos de vous-même.", "🇮🇳": "अपने बारे में कुछ शब्द लिखें.", "🇩🇪": "Geben Sie ein paar Worte über sich selbst ein.", "🇧🇷": "Insira alguma palavras sobre você." },
  12. webhook_url: { "🇺🇸": "Webhook URL", "🇪🇸": "URL del Webhook", "🇫🇷": "URL du webhook", "🇮🇳": "वेबहुक URL", "🇩🇪": "Webhook-URL", "🇧🇷": "URL do Webhook" },
  13. chat_bots: { "🇺🇸": "Chat bots. With Chat bots, other sites and services can post updates directly to Campfire.", "🇪🇸": "Bots de chat. Con los bots de chat, otros sitios y servicios pueden publicar actualizaciones directamente en Campfire.", "🇫🇷": "Bots de discussion. Avec les bots de discussion, d'autres sites et services peuvent publier des mises à jour directement sur Campfire.", "🇮🇳": "चैट बॉट। चैट बॉट के साथ, अन्य साइटों और सेवाएं सीधे कैम्पफायर में अपडेट पोस्ट कर सकती हैं।", "🇩🇪": "Chat-Bots. Mit Chat-Bots können andere Websites und Dienste Updates direkt in Campfire veröffentlichen.", "🇧🇷": "Chat bots. Com Chat bots, outros sites e serviços podem postar atualizações diretamente no Campfire." },
  14. bot_name: { "🇺🇸": "Name the bot", "🇪🇸": "Nombrar al bot", "🇫🇷": "Nommer le bot", "🇮🇳": "बॉट का नाम दें", "🇩🇪": "Benenne den Bot", "🇧🇷": "Dê um nome ao bot" },
  15. custom_styles: { "🇺🇸": "Add custom CSS styles. Use Caution: you could break things.", "🇪🇸": "Agrega estilos CSS personalizados. Usa precaución: podrías romper cosas.", "🇫🇷": "Ajoutez des styles CSS personnalisés. Utilisez avec précaution : vous pourriez casser des choses.", "🇮🇳": "कस्टम CSS स्टाइल जोड़ें। सावधानी बरतें: आप चीज़ों को तोड़ सकते हैं।", "🇩🇪": "Fügen Sie benutzerdefinierte CSS-Stile hinzu. Vorsicht: Sie könnten Dinge kaputt machen.", "🇧🇷": "Adicione estilos CSS personalizados. Use com cuidado: você pode quebrar coisas." }
  16. }
  17. 1 def translations_for(translation_key)
  18. 26 tag.dl(class: "language-list") do
  19. 26 TRANSLATIONS[translation_key].map do |language, translation|
  20. 156 concat tag.dt(language)
  21. 156 concat tag.dd(translation, class: "margin-none")
  22. end
  23. end
  24. end
  25. 1 def translation_button(translation_key)
  26. 26 tag.details(class: "position-relative", data: { controller: "popup", action: "keydown.esc->popup#close toggle->popup#toggle click@document->popup#closeOnClickOutside", popup_orientation_top_class: "popup-orientation-top" }) do
  27. 26 tag.summary(class: "btn", tabindex: -1) do
  28. 26 concat image_tag("globe.svg", size: 20, aria: { hidden: "true" }, class: "color-icon")
  29. 26 concat tag.span("Translate", class: "for-screen-reader")
  30. end +
  31. tag.div(class: "lanuage-list-menu shadow", data: { popup_target: "menu" }) do
  32. 26 translations_for(translation_key)
  33. end
  34. end
  35. end
  36. end

app/helpers/users/avatars_helper.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 require "zlib"
  2. 1 module Users::AvatarsHelper
  3. AVATAR_COLORS = %w[
  4. 1 #AF2E1B #CC6324 #3B4B59 #BFA07A #ED8008 #ED3F1C #BF1B1B #736B1E #D07B53
  5. #736356 #AD1D1D #BF7C2A #C09C6F #698F9C #7C956B #5D618F #3B3633 #67695E
  6. ]
  7. 1 def avatar_background_color(user)
  8. 1 AVATAR_COLORS[Zlib.crc32(user.to_param) % AVATAR_COLORS.size]
  9. end
  10. 1 def avatar_tag(user, **options)
  11. 69 link_to user_path(user), title: user.title, class: "btn avatar", data: { turbo_frame: "_top" } do
  12. 69 image_tag fresh_user_avatar_path(user), aria: { hidden: "true" }, size: 48, **options
  13. end
  14. end
  15. end

app/helpers/users/filter_helper.rb

80.0% lines covered

5 relevant lines. 4 lines covered and 1 lines missed.
    
  1. 1 module Users::FilterHelper
  2. 1 def user_filter_menu_tag(&)
  3. 2 tag.menu class: "flex flex-column gap margin-none pad overflow-y constrain-height",
  4. data: { controller: "filter", filter_active_class: "filter--active", filter_selected_class: "selected" }, &
  5. end
  6. 1 def user_filter_search_tag
  7. tag.input type: "search", id: "search", autocorrect: "off", autocomplete: "off", "data-1p-ignore": "true", class: "input input--transparent full-width", placeholder: "Filter…", data: { action: "input->filter#filter" }
  8. end
  9. end

app/helpers/users/profiles_helper.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 module Users::ProfilesHelper
  2. 1 def profile_form_with(model, **params, &)
  3. 3 form_with \
  4. model: @user, url: user_profile_path, method: :patch,
  5. data: { controller: "form" },
  6. **params,
  7. &
  8. end
  9. 1 def profile_form_submit_button
  10. 3 tag.button class: "btn btn--reversed center txt-large", type: "submit" do
  11. 3 image_tag("check.svg", aria: { hidden: "true" }, size: 20) +
  12. tag.span("Save changes", class: "for-screen-reader")
  13. end
  14. end
  15. 1 def web_share_session_button(url, title, text, &)
  16. 3 tag.button class: "btn", hidden: true, data: {
  17. controller: "web-share", action: "web-share#share",
  18. web_share_url_value: url,
  19. web_share_text_value: text,
  20. web_share_title_value: title
  21. }, &
  22. end
  23. end

app/helpers/users/sidebar_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module Users::SidebarHelper
  2. 1 def sidebar_turbo_frame_tag(src: nil, &)
  3. 5 turbo_frame_tag :user_sidebar, src: src, target: "_top", data: {
  4. turbo_permanent: true,
  5. controller: "rooms-list read-rooms turbo-frame",
  6. rooms_list_unread_class: "unread",
  7. action: "presence:present@window->rooms-list#read read-rooms:read->rooms-list#read turbo:frame-load->rooms-list#loaded refresh-room:visible@window->turbo-frame#reload".html_safe # otherwise -> is escaped
  8. }, &
  9. end
  10. end

app/helpers/users_helper.rb

50.0% lines covered

4 relevant lines. 2 lines covered and 2 lines missed.
    
  1. 1 module UsersHelper
  2. 1 def button_to_direct_room_with(user)
  3. button_to rooms_directs_path(user_ids: [ user.id ]), class: "btn btn--primary full-width txt--large" do
  4. image_tag("messages.svg")
  5. end
  6. end
  7. end

app/helpers/version_helper.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module VersionHelper
  2. 1 def version_badge
  3. 5 tag.span(Rails.application.config.app_version, class: "version-badge")
  4. end
  5. end

app/jobs/application_job.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. 1 class ApplicationJob < ActiveJob::Base
  2. # Automatically retry jobs that encountered a deadlock
  3. # retry_on ActiveRecord::Deadlocked
  4. # Most jobs are safe to ignore if the underlying records are no longer available
  5. # discard_on ActiveJob::DeserializationError
  6. end

app/jobs/bot/webhook_job.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class Bot::WebhookJob < ApplicationJob
  2. 1 def perform(bot, message)
  3. 1 bot.deliver_webhook(message)
  4. end
  5. end

app/jobs/room/push_message_job.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class Room::PushMessageJob < ApplicationJob
  2. 1 def perform(room, message)
  3. 6 Room::MessagePusher.new(room:, message:).push
  4. end
  5. end

app/models/account.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class Account < ApplicationRecord
  2. 1 include Joinable
  3. 1 has_one_attached :logo
  4. end

app/models/account/joinable.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 module Account::Joinable
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 7 before_create { self.join_code = generate_join_code }
  5. end
  6. 1 def reset_join_code
  7. 2 update! join_code: generate_join_code
  8. end
  9. 1 private
  10. 1 def generate_join_code
  11. 8 SecureRandom.alphanumeric(12).scan(/.{4}/).join("-")
  12. end
  13. end

app/models/application_platform.rb

62.5% lines covered

32 relevant lines. 20 lines covered and 12 lines missed.
    
  1. 1 class ApplicationPlatform < PlatformAgent
  2. 1 def ios?
  3. match? /iPhone|iPad/
  4. end
  5. 1 def android?
  6. match? /Android/
  7. end
  8. 1 def mac?
  9. match? /Macintosh/
  10. end
  11. 1 def chrome?
  12. 21 user_agent.browser.match? /Chrome/
  13. end
  14. 1 def firefox?
  15. 14 user_agent.browser.match? /Firefox|FxiOS/
  16. end
  17. 1 def safari?
  18. 14 user_agent.browser.match? /Safari/
  19. end
  20. 1 def edge?
  21. 7 user_agent.browser.match? /Edg/
  22. end
  23. 1 def apple_messages?
  24. # Apple Messages pretends to be Facebook and Twitter bots via spoofed user agent.
  25. # We want to avoid showing "Unsupported browser" message when a Campfire link
  26. # is shared via Messages.
  27. 1 match?(/facebookexternalhit/i) && match?(/Twitterbot/i)
  28. end
  29. 1 def mobile?
  30. ios? || android?
  31. end
  32. 1 def desktop?
  33. !mobile?
  34. end
  35. 1 def windows?
  36. operating_system == "Windows"
  37. end
  38. 1 def operating_system
  39. 2 case user_agent.platform
  40. when /Android/ then "Android"
  41. when /iPad/ then "iPad"
  42. when /iPhone/ then "iPhone"
  43. when /Macintosh/ then "macOS"
  44. when /Windows/ then "Windows"
  45. when /CrOS/ then "ChromeOS"
  46. else
  47. 2 os =~ /Linux/ ? "Linux" : os
  48. end
  49. end
  50. end

app/models/application_record.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 class ApplicationRecord < ActiveRecord::Base
  2. 1 primary_abstract_class
  3. end

app/models/boost.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class Boost < ApplicationRecord
  2. 1 belongs_to :message, touch: true
  3. 2 belongs_to :booster, class_name: "User", default: -> { Current.user }
  4. 38 scope :ordered, -> { order(:created_at) }
  5. end

app/models/current.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 class Current < ActiveSupport::CurrentAttributes
  2. 1 attribute :user, :request
  3. 1 delegate :host, :protocol, to: :request, prefix: true, allow_nil: true
  4. 1 def account
  5. 136 Account.first
  6. end
  7. end

app/models/first_run.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 class FirstRun
  2. 1 ACCOUNT_NAME = "Campfire"
  3. 1 FIRST_ROOM_NAME = "All Talk"
  4. 1 def self.create!(user_params)
  5. 4 account = Account.create!(name: ACCOUNT_NAME)
  6. 4 room = Rooms::Open.new(name: FIRST_ROOM_NAME)
  7. 4 administrator = room.creator = User.new(user_params.merge(role: :administrator))
  8. 4 room.save!
  9. 4 room.memberships.grant_to administrator
  10. 4 administrator
  11. end
  12. end

app/models/membership.rb

92.86% lines covered

14 relevant lines. 13 lines covered and 1 lines missed.
    
  1. 1 class Membership < ApplicationRecord
  2. 1 include Connectable
  3. 1 belongs_to :room
  4. 1 belongs_to :user
  5. 8 after_destroy_commit { user.reset_remote_connections }
  6. 1 enum involvement: %w[ invisible nothing mentions everything ].index_by(&:itself), _prefix: :involved_in
  7. 5 scope :with_ordered_room, -> { includes(:room).joins(:room).order("LOWER(rooms.name)") }
  8. 6 scope :without_direct_rooms, -> { joins(:room).where.not(room: { type: "Rooms::Direct" }) }
  9. 60 scope :visible, -> { where.not(involvement: :invisible) }
  10. 15 scope :unread, -> { where.not(unread_at: nil) }
  11. 1 def read
  12. update!(unread_at: nil)
  13. end
  14. 1 def unread?
  15. 30 unread_at.present?
  16. end
  17. end

app/models/membership/connectable.rb

96.43% lines covered

28 relevant lines. 27 lines covered and 1 lines missed.
    
  1. 1 module Membership::Connectable
  2. 1 extend ActiveSupport::Concern
  3. 1 CONNECTION_TTL = 60.seconds
  4. 1 included do
  5. 4 scope :connected, -> { where(connected_at: CONNECTION_TTL.ago..) }
  6. 60 scope :disconnected, -> { where(connected_at: [ nil, ...CONNECTION_TTL.ago ]) }
  7. end
  8. 1 class_methods do
  9. 1 def disconnect_all
  10. connected.update_all connected_at: nil, connections: 0, updated_at: Time.current
  11. end
  12. 1 def connect(membership, connections)
  13. 3 where(id: membership.id).update_all(connections: connections, connected_at: Time.current, unread_at: nil)
  14. end
  15. end
  16. 1 def connected?
  17. 35 connected_at? && connected_at >= CONNECTION_TTL.ago
  18. end
  19. 1 def present
  20. 3 self.class.connect(self, connected? ? connections + 1 : 1)
  21. end
  22. 1 def connected
  23. 14 increment_connections
  24. 14 touch :connected_at
  25. end
  26. 1 def disconnected
  27. 6 decrement_connections
  28. 6 update! connected_at: nil if connections < 1
  29. end
  30. 1 def refresh_connection
  31. 1 increment_connections unless connected?
  32. 1 touch :connected_at
  33. end
  34. 1 def increment_connections
  35. 15 connected? ? increment!(:connections, touch: true) : update!(connections: 1)
  36. end
  37. 1 def decrement_connections
  38. 6 connected? ? decrement!(:connections, touch: true) : update!(connections: 0)
  39. end
  40. end

app/models/message.rb

90.48% lines covered

21 relevant lines. 19 lines covered and 2 lines missed.
    
  1. 1 class Message < ApplicationRecord
  2. 1 include Attachment, Broadcasts, Mentionee, Pagination, Searchable
  3. 1 belongs_to :room, touch: true
  4. 9 belongs_to :creator, class_name: "User", default: -> { Current.user }
  5. 1 has_many :boosts, dependent: :destroy
  6. 1 has_rich_text :body
  7. 45 before_create -> { self.client_message_id ||= Random.uuid } # Bots don't care
  8. 45 after_create_commit -> { room.receive(self) }
  9. 32 scope :ordered, -> { order(:created_at) }
  10. 9 scope :with_creator, -> { includes(:creator) }
  11. 1 def plain_text_body
  12. 193 body.to_plain_text.presence || attachment&.filename&.to_s || ""
  13. end
  14. 1 def to_key
  15. 610 [ client_message_id ]
  16. end
  17. 1 def content_type
  18. case
  19. 78 when attachment? then "attachment"
  20. when sound.present? then "sound"
  21. 70 else "text"
  22. end.inquiry
  23. end
  24. 1 def sound
  25. 70 plain_text_body.match(/\A\/play (?<name>\w+)\z/) do |match|
  26. Sound.find_by_name match[:name]
  27. end
  28. end
  29. end

app/models/message/attachment.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. 1 module Message::Attachment
  2. 1 extend ActiveSupport::Concern
  3. 1 THUMBNAIL_MAX_WIDTH = 1200
  4. 1 THUMBNAIL_MAX_HEIGHT = 800
  5. 1 included do
  6. 1 has_one_attached :attachment do |attachable|
  7. 1 attachable.variant :thumb, resize_to_limit: [ THUMBNAIL_MAX_WIDTH, THUMBNAIL_MAX_HEIGHT ]
  8. end
  9. end
  10. 1 module ClassMethods
  11. 1 def create_with_attachment!(attributes)
  12. 12 create!(attributes).tap(&:process_attachment)
  13. end
  14. end
  15. 1 def attachment?
  16. 74 attachment.attached?
  17. end
  18. 1 def process_attachment
  19. 12 ensure_attachment_analyzed
  20. 12 process_attachment_thumbnail
  21. end
  22. 1 private
  23. 1 def ensure_attachment_analyzed
  24. 12 attachment&.analyze
  25. end
  26. 1 def process_attachment_thumbnail
  27. case
  28. 12 when attachment.video?
  29. 1 attachment.preview(format: :webp).processed
  30. when attachment.representable?
  31. 4 attachment.representation(:thumb).processed
  32. end
  33. end
  34. end

app/models/message/broadcasts.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 module Message::Broadcasts
  2. 1 def broadcast_create
  3. 11 broadcast_append_to room, :messages, target: [ room, :messages ]
  4. 11 ActionCable.server.broadcast("unread_rooms", { roomId: room.id })
  5. end
  6. end

app/models/message/mentionee.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 module Message::Mentionee
  2. 1 extend ActiveSupport::Concern
  3. 1 def mentionees
  4. 16 room.users.where(id: mentioned_users.map(&:id))
  5. end
  6. 1 private
  7. 1 def mentioned_users
  8. 16 if body.body
  9. 15 body.body.attachables.grep(User).uniq
  10. else
  11. 1 []
  12. end
  13. end
  14. end

app/models/message/pagination.rb

88.24% lines covered

17 relevant lines. 15 lines covered and 2 lines missed.
    
  1. 1 module Message::Pagination
  2. 1 extend ActiveSupport::Concern
  3. 1 PAGE_SIZE = 40
  4. 1 included do
  5. 7 scope :last_page, -> { ordered.last(PAGE_SIZE) }
  6. 3 scope :first_page, -> { ordered.first(PAGE_SIZE) }
  7. 2 scope :before, ->(message) { where("created_at < ?", message.created_at) }
  8. 2 scope :after, ->(message) { where("created_at > ?", message.created_at) }
  9. 2 scope :page_before, ->(message) { before(message).last_page }
  10. 2 scope :page_after, ->(message) { after(message).first_page }
  11. 2 scope :page_created_since, ->(time) { where("created_at > ?", time).first_page }
  12. 2 scope :page_updated_since, ->(time) { where("updated_at > ?", time).last_page }
  13. end
  14. 1 class_methods do
  15. 1 def page_around(message)
  16. page_before(message) + [ message ] + page_after(message)
  17. end
  18. 1 def paged?
  19. count > PAGE_SIZE
  20. end
  21. end
  22. end

app/models/message/searchable.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 1 module Message::Searchable
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 after_create_commit :create_in_index
  5. 1 after_update_commit :update_in_index
  6. 1 after_destroy_commit :remove_from_index
  7. 10 scope :search, ->(query) { joins("join message_search_index idx on messages.id = idx.rowid").where("idx.body match ?", query).ordered }
  8. end
  9. 1 private
  10. 1 def create_in_index
  11. 44 execute_sql_with_binds "insert into message_search_index(rowid, body) values (?, ?)", id, plain_text_body
  12. end
  13. 1 def update_in_index
  14. 20 execute_sql_with_binds "update message_search_index set body = ? where rowid = ?", plain_text_body, id
  15. end
  16. 1 def remove_from_index
  17. 110 execute_sql_with_binds "delete from message_search_index where rowid = ?", id
  18. end
  19. 1 def execute_sql_with_binds(*statement)
  20. 174 self.class.connection.execute self.class.sanitize_sql(statement)
  21. end
  22. end

app/models/opengraph/document.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 1 require "nokogiri"
  2. 1 class Opengraph::Document
  3. 1 attr_accessor :html
  4. 1 def initialize(html)
  5. 20 @html = Nokogiri::HTML(html)
  6. end
  7. 1 def opengraph_attributes
  8. 20 @opengraph_attributes ||= extract_opengraph_attributes
  9. end
  10. 1 private
  11. 1 def extract_opengraph_attributes
  12. 20 opengraph_tags = html.xpath("//*/meta[starts-with(@property, \"og:\") or starts-with(@name, \"og:\")]").map do |tag|
  13. 61 key = tag.key?("property") ? "property" : "name"
  14. 61 [ tag[key].gsub("og:", "").to_sym, sanitize_content(tag["content"]) ] if tag["content"].present?
  15. end
  16. 20 Hash[opengraph_tags.compact].slice(*Opengraph::Metadata::ATTRIBUTES)
  17. end
  18. 1 def sanitize_content(content)
  19. 61 html.meta_encoding ? content : content.encode("UTF-8", "binary", invalid: :replace, undef: :replace, replace: "")
  20. end
  21. end

app/models/opengraph/fetch.rb

100.0% lines covered

42 relevant lines. 42 lines covered and 0 lines missed.
    
  1. 1 require "net/http"
  2. 1 require "restricted_http/private_network_guard"
  3. 1 class Opengraph::Fetch
  4. 1 ALLOWED_DOCUMENT_CONTENT_TYPE = "text/html"
  5. 1 MAX_BODY_SIZE = 5.megabytes
  6. 1 MAX_REDIRECTS = 10
  7. 1 class TooManyRedirectsError < StandardError; end
  8. 1 class RedirectDeniedError < StandardError; end
  9. 1 def fetch_document(url, ip: RestrictedHTTP::PrivateNetworkGuard.resolve(url.host))
  10. 29 request(url, Net::HTTP::Get, ip: ip) do |response|
  11. 25 return body_if_acceptable(response)
  12. end
  13. end
  14. 1 def fetch_content_type(url, ip: RestrictedHTTP::PrivateNetworkGuard.resolve(url.host))
  15. 10 request(url, Net::HTTP::Head, ip: ip) do |response|
  16. 10 return response["Content-Type"]
  17. end
  18. end
  19. 1 private
  20. 1 def request(url, request_class, ip:)
  21. 39 MAX_REDIRECTS.times do
  22. 50 Net::HTTP.start(url.host, url.port, ipaddr: ip, use_ssl: url.scheme == "https") do |http|
  23. 50 http.request request_class.new(url) do |response|
  24. 48 if response.is_a?(Net::HTTPRedirection)
  25. 13 url, ip = resolve_redirect(response["location"])
  26. else
  27. 35 yield response
  28. end
  29. end
  30. end
  31. end
  32. 1 raise TooManyRedirectsError
  33. end
  34. 1 def resolve_redirect(location)
  35. 13 url = URI.parse(location)
  36. 13 raise RedirectDeniedError unless url.is_a?(URI::HTTP)
  37. 13 [ url, RestrictedHTTP::PrivateNetworkGuard.resolve(url.host) ]
  38. end
  39. 1 def body_if_acceptable(response)
  40. 25 size_restricted_body(response) if response_valid?(response)
  41. end
  42. 1 def size_restricted_body(response)
  43. # We've already checked the Content-Length header, to try to avoid reading
  44. # the body of any large responses. But that header could be wrong or
  45. # missing. To be on the safe side, we'll read the body in chunks, and bail
  46. # if it runs over our size limit.
  47. 19 "".tap do |body|
  48. 19 response.read_body do |chunk|
  49. 19 return nil if body.bytesize + chunk.bytesize > MAX_BODY_SIZE
  50. 17 body << chunk
  51. end
  52. end
  53. end
  54. 1 def response_valid?(response)
  55. 25 status_valid?(response) && content_type_valid?(response) && content_length_valid?(response)
  56. end
  57. 1 def status_valid?(response)
  58. 25 response.is_a?(Net::HTTPOK)
  59. end
  60. 1 def content_type_valid?(response)
  61. 24 response.content_type == ALLOWED_DOCUMENT_CONTENT_TYPE
  62. end
  63. 1 def content_length_valid?(response)
  64. 21 response.content_length.to_i <= MAX_BODY_SIZE
  65. end
  66. end

app/models/opengraph/location.rb

86.21% lines covered

29 relevant lines. 25 lines covered and 4 lines missed.
    
  1. 1 require "restricted_http/private_network_guard"
  2. 1 class Opengraph::Location
  3. 1 include ActiveModel::Validations
  4. 1 attr_accessor :url, :parsed_url
  5. 1 validate :validate_url, :validate_url_is_public
  6. 1 def initialize(url)
  7. 72 @url = url
  8. end
  9. 1 def read_html
  10. 25 fetch_html if valid? && !url.match(FILES_AND_MEDIA_URL_REGEX)
  11. end
  12. 1 def fetch_content_type
  13. 13 Opengraph::Fetch.new.fetch_content_type(parsed_url, ip: resolved_ip) if valid?
  14. rescue => e
  15. Rails.logger.warn "Failed to fetch #{parsed_url} at #{resolved_ip} (#{e})"
  16. nil
  17. end
  18. 1 def resolved_ip
  19. 99 return @resolved_ip if defined? @resolved_ip
  20. 71 @resolved_ip = RestrictedHTTP::PrivateNetworkGuard.resolve(parsed_url.host) rescue nil
  21. end
  22. 1 private
  23. 1 FILES_AND_MEDIA_URL_REGEX = /\bhttps?:\/\/\S+\.(?:zip|tar|tar\.gz|tar\.bz2|tar\.xz|gz|bz2|rar|7z|dmg|exe|msi|pkg|deb|iso|jpg|jpeg|png|gif|bmp|mp4|mov|avi|mkv|wmv|flv|heic|heif|mp3|wav|ogg|aac|wma|webm|ogv|mpg|mpeg)\b/
  24. 1 def validate_url
  25. 71 errors.add :url, "is invalid" unless parsed_url.is_a?(URI::HTTP)
  26. end
  27. 1 def validate_url_is_public
  28. 71 errors.add :url, "is not public" unless resolved_ip
  29. end
  30. 1 def parsed_url
  31. 170 return @parsed_url if defined? @parsed_url
  32. 71 @parsed_url = URI.parse(url) rescue nil
  33. end
  34. 1 def fetch_html
  35. 19 Opengraph::Fetch.new.fetch_document(parsed_url, ip: resolved_ip)
  36. rescue => e
  37. Rails.logger.warn "Failed to fetch #{parsed_url} at #{resolved_ip} (#{e})"
  38. nil
  39. end
  40. end

app/models/opengraph/metadata.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. 1 class Opengraph::Metadata
  2. 1 include ActiveModel::Model
  3. 1 include ActiveModel::Validations::Callbacks
  4. 1 include ActionView::Helpers::SanitizeHelper
  5. 1 include Fetching
  6. 1 ATTRIBUTES = %i[ title url image description ]
  7. 1 attr_accessor *ATTRIBUTES
  8. 1 before_validation :sanitize_fields
  9. 1 validates_presence_of :title, :url, :description
  10. 1 validate :ensure_valid_image_url
  11. 1 private
  12. 1 def sanitize_fields
  13. 17 self.title = sanitize(strip_tags(title))
  14. 17 self.description = sanitize(strip_tags(description))
  15. end
  16. 1 def ensure_valid_image_url
  17. 17 if image.present?
  18. 8 errors.add :image, "url is invalid" unless Opengraph::Location.new(image).valid?
  19. end
  20. end
  21. end

app/models/opengraph/metadata/fetching.rb

89.19% lines covered

37 relevant lines. 33 lines covered and 4 lines missed.
    
  1. 1 module Opengraph::Metadata::Fetching
  2. 1 extend ActiveSupport::Concern
  3. 1 module ClassMethods
  4. 1 def from_url(url)
  5. 17 document = fetch_document(url)
  6. 17 attributes = document.opengraph_attributes
  7. 17 new attributes.merge(url: valid_canonical_url(attributes[:url], url), image: valid_image_content_type(attributes[:image]))
  8. end
  9. 1 private
  10. 1 TWITTER_HOSTS = %w[ twitter.com www.twitter.com x.com www.x.com ]
  11. 1 FX_TWITTER_HOST = "fxtwitter.com"
  12. 1 ALLOWED_IMAGE_CONTENT_TYPES = %w[ image/jpeg image/png image/gif image/webp ]
  13. 1 def fetch_document(untrusted_url)
  14. 17 tweet_url?(untrusted_url) ? fetch_fxtwitter_document(untrusted_url) : fetch_non_fxtwitter_document(untrusted_url)
  15. end
  16. 1 def fetch_fxtwitter_document(untrusted_url)
  17. 2 fxtwitter_url = replace_twitter_domain_for_opengraph_support(untrusted_url)
  18. 2 Opengraph::Location.new(fxtwitter_url).then do |location|
  19. # fxtwitter.com HTML response does not include character encoding, resulting in emojis and quotes not
  20. # being encoded properly.
  21. 2 Opengraph::Document.new(location.read_html.force_encoding("UTF-8"))
  22. end
  23. end
  24. 1 def fetch_non_fxtwitter_document(untrusted_url)
  25. 15 Opengraph::Location.new(untrusted_url).then do |location|
  26. 15 Opengraph::Document.new(location.read_html)
  27. end
  28. end
  29. 1 def valid_canonical_url(url, fallback)
  30. 17 Opengraph::Location.new(url).valid? ? url : fallback
  31. end
  32. 1 def valid_image_content_type(image)
  33. 17 return unless image.present?
  34. 13 content_type = Opengraph::Location.new(URI.parse(image)).fetch_content_type&.downcase
  35. 13 content_type.in?(ALLOWED_IMAGE_CONTENT_TYPES) ? image : nil
  36. rescue => e
  37. Rails.logger.warn "Failed to fetch image content tpye: #{image} (#{e})"
  38. nil
  39. end
  40. # Twitter.com and X.com do not support Opengraph at the moment.
  41. # Piggybacking on fxtwitter.com allows us to have twitter unfurling
  42. # without relying on fxtwitter.com's future availability.
  43. 1 def replace_twitter_domain_for_opengraph_support(url)
  44. 2 uri = URI.parse(url)
  45. 2 uri.host = FX_TWITTER_HOST if uri.host.in?(TWITTER_HOSTS)
  46. 2 uri.to_s
  47. rescue URI::InvalidURIError
  48. nil
  49. end
  50. 1 def tweet_url?(url)
  51. 17 uri = URI.parse(url)
  52. 17 uri.host.in?(TWITTER_HOSTS) && uri.path.present? && uri.path != "/"
  53. rescue URI::InvalidURIError
  54. nil
  55. end
  56. end
  57. end

app/models/push.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module Push
  2. 1 def self.table_name_prefix
  3. 1 "push_"
  4. end
  5. end

app/models/push/subscription.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 class Push::Subscription < ApplicationRecord
  2. 1 belongs_to :user
  3. 1 def notification(**params)
  4. 14 WebPush::Notification.new(**params, badge: user.memberships.unread.count, endpoint: endpoint, p256dh_key: p256dh_key, auth_key: auth_key)
  5. end
  6. end

app/models/room.rb

100.0% lines covered

42 relevant lines. 42 lines covered and 0 lines missed.
    
  1. 1 class Room < ApplicationRecord
  2. 1 has_many :memberships, dependent: :delete_all do
  3. 1 def grant_to(users)
  4. 23 room = proxy_association.owner
  5. 72 Membership.insert_all(Array(users).collect { |user| { room_id: room.id, user_id: user.id, involvement: room.default_involvement } })
  6. end
  7. 1 def revoke_from(users)
  8. 4 destroy_by user: users
  9. end
  10. 1 def revise(granted: [], revoked: [])
  11. 4 transaction do
  12. 4 grant_to(granted) if granted.present?
  13. 4 revoke_from(revoked) if revoked.present?
  14. end
  15. end
  16. end
  17. 1 has_many :users, through: :memberships
  18. 1 has_many :messages, dependent: :destroy
  19. 5 belongs_to :creator, class_name: "User", default: -> { Current.user }
  20. 4 scope :opens, -> { where(type: "Rooms::Open") }
  21. 3 scope :closeds, -> { where(type: "Rooms::Closed") }
  22. 4 scope :directs, -> { where(type: "Rooms::Direct") }
  23. 2 scope :without_directs, -> { where.not(type: "Rooms::Direct") }
  24. 2 scope :ordered, -> { order("LOWER(name)") }
  25. 1 class << self
  26. 1 def create_for(attributes, users:)
  27. 6 transaction do
  28. 6 create!(attributes).tap do |room|
  29. 6 room.memberships.grant_to users
  30. end
  31. end
  32. end
  33. 1 def original
  34. 10 order(:created_at).first
  35. end
  36. end
  37. 1 def receive(message)
  38. 44 unread_memberships(message)
  39. 44 push_later(message)
  40. end
  41. 1 def open?
  42. 2 is_a?(Rooms::Open)
  43. end
  44. 1 def closed?
  45. 1 is_a?(Rooms::Closed)
  46. end
  47. 1 def direct?
  48. 117 is_a?(Rooms::Direct)
  49. end
  50. 1 def default_involvement
  51. 45 "mentions"
  52. end
  53. 1 private
  54. 1 def unread_memberships(message)
  55. 44 memberships.visible.disconnected.where.not(user: message.creator).update_all(unread_at: message.created_at, updated_at: Time.current)
  56. end
  57. 1 def push_later(message)
  58. 44 Room::PushMessageJob.perform_later(self, message)
  59. end
  60. end

app/models/room/message_pusher.rb

93.1% lines covered

29 relevant lines. 27 lines covered and 2 lines missed.
    
  1. 1 class Room::MessagePusher
  2. 1 attr_reader :room, :message
  3. 1 def initialize(room:, message:)
  4. 6 @room, @message = room, message
  5. end
  6. 1 def push
  7. 6 build_payload.tap do |payload|
  8. 6 push_to_users_involved_in_everything(payload)
  9. 6 push_to_users_involved_in_mentions(payload)
  10. end
  11. end
  12. 1 private
  13. 1 def build_payload
  14. 6 if room.direct?
  15. build_direct_payload
  16. else
  17. 6 build_shared_payload
  18. end
  19. end
  20. 1 def build_direct_payload
  21. {
  22. title: message.creator.name,
  23. body: message.plain_text_body,
  24. path: Rails.application.routes.url_helpers.room_path(room)
  25. }
  26. end
  27. 1 def build_shared_payload
  28. {
  29. 6 title: room.name,
  30. body: "#{message.creator.name}: #{message.plain_text_body}",
  31. path: Rails.application.routes.url_helpers.room_path(room)
  32. }
  33. end
  34. 1 def push_to_users_involved_in_everything(payload)
  35. 6 enqueue_payload_for_delivery payload, push_subscriptions_for_users_involved_in_everything
  36. end
  37. 1 def push_to_users_involved_in_mentions(payload)
  38. 6 enqueue_payload_for_delivery payload, push_subscriptions_for_mentionable_users(message.mentionees)
  39. end
  40. 1 def push_subscriptions_for_users_involved_in_everything
  41. 6 relevant_subscriptions.merge(Membership.involved_in_everything)
  42. end
  43. 1 def push_subscriptions_for_mentionable_users(mentionees)
  44. 6 relevant_subscriptions.merge(Membership.involved_in_mentions).where(user_id: mentionees.ids)
  45. end
  46. 1 def relevant_subscriptions
  47. 12 Push::Subscription
  48. .joins(user: :memberships)
  49. .merge(Membership.visible.disconnected.where(room: room).where.not(user: message.creator))
  50. end
  51. 1 def enqueue_payload_for_delivery(payload, subscriptions)
  52. 12 Rails.configuration.x.web_push_pool.queue(payload, subscriptions)
  53. end
  54. end

app/models/rooms/closed.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # Rooms where only a subset of all users on the account have been explicited granted membership.
  2. 1 class Rooms::Closed < Room
  3. end

app/models/rooms/direct.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. # Rooms for direct message chats between users. These act as a singleton, so a single set of users will
  2. # always refer to the same direct room.
  3. 1 class Rooms::Direct < Room
  4. 1 class << self
  5. 1 def find_or_create_for(users)
  6. 7 find_for(users) || create_for({}, users: users)
  7. end
  8. 1 private
  9. # FIXME: Find a more performant algorithm that won't be a problem on accounts with 10K+ direct rooms,
  10. # which could be to store the membership id list as a hash on the room, and use that for lookup.
  11. 1 def find_for(users)
  12. 7 all.joins(:users).detect do |room|
  13. 39 Set.new(room.user_ids) == Set.new(users.pluck(:id))
  14. end
  15. end
  16. end
  17. 1 def default_involvement
  18. 4 "everything"
  19. end
  20. end

app/models/rooms/open.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # Rooms open to all users on the account. When a new user is added to the account, they're automatically granted membership.
  2. 1 class Rooms::Open < Room
  3. 1 after_save_commit :grant_access_to_all_users
  4. 1 private
  5. 1 def grant_access_to_all_users
  6. 28 memberships.grant_to(User.active) if type_previously_changed?(to: "Rooms::Open")
  7. end
  8. end

app/models/search.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 class Search < ApplicationRecord
  2. 1 belongs_to :user
  3. 1 after_create :trim_recent_searches
  4. 5 scope :ordered, -> { order(updated_at: :desc) }
  5. 1 class << self
  6. 1 def record(query)
  7. 1 find_or_create_by(query: query).touch
  8. end
  9. end
  10. 1 private
  11. 1 def trim_recent_searches
  12. 1 user.searches.excluding(user.searches.ordered.limit(10)).destroy_all
  13. end
  14. end

app/models/session.rb

90.0% lines covered

10 relevant lines. 9 lines covered and 1 lines missed.
    
  1. 1 class Session < ApplicationRecord
  2. 1 ACTIVITY_REFRESH_RATE = 1.hour
  3. 1 has_secure_token
  4. 1 belongs_to :user
  5. 112 before_create { self.last_active_at ||= Time.now }
  6. 1 def self.start!(user_agent:, ip_address:)
  7. 111 create! user_agent: user_agent, ip_address: ip_address
  8. end
  9. 1 def resume(user_agent:, ip_address:)
  10. 99 if last_active_at.before?(ACTIVITY_REFRESH_RATE.ago)
  11. update! user_agent: user_agent, ip_address: ip_address, last_active_at: Time.now
  12. end
  13. end
  14. end

app/models/user.rb

100.0% lines covered

38 relevant lines. 38 lines covered and 0 lines missed.
    
  1. 1 class User < ApplicationRecord
  2. 1 include Avatar, Bot, Mentionable, Role, Transferable
  3. 1 has_many :memberships, dependent: :delete_all
  4. 1 has_many :rooms, through: :memberships
  5. 1 has_many :reachable_messages, through: :rooms, source: :messages
  6. 1 has_many :messages, dependent: :destroy, foreign_key: :creator_id
  7. 1 has_many :push_subscriptions, class_name: "Push::Subscription", dependent: :delete_all
  8. 1 has_many :boosts, dependent: :destroy, foreign_key: :booster_id
  9. 1 has_many :searches, dependent: :delete_all
  10. 1 has_many :sessions, dependent: :destroy
  11. 157 scope :active, -> { where(active: true) }
  12. 1 has_secure_password validations: false
  13. 1 after_create_commit :grant_membership_to_open_rooms
  14. 8 scope :ordered, -> { order("LOWER(name)") }
  15. 4 scope :filtered_by, ->(query) { where("name like ?", "%#{query}%") }
  16. 1 def initials
  17. 2 name.scan(/\b\w/).join
  18. end
  19. 1 def title
  20. 106 [ name, bio ].compact_blank.join(" – ")
  21. end
  22. 1 def deactivate
  23. 4 transaction do
  24. 4 close_remote_connections
  25. 4 memberships.without_direct_rooms.delete_all
  26. 4 push_subscriptions.delete_all
  27. 4 searches.delete_all
  28. 4 sessions.delete_all
  29. 4 update! active: false, email_address: deactived_email_address
  30. end
  31. end
  32. 1 def deactivated?
  33. 1 !active?
  34. end
  35. 1 def reset_remote_connections
  36. 6 close_remote_connections reconnect: true
  37. end
  38. 1 private
  39. 1 def grant_membership_to_open_rooms
  40. 29 Membership.insert_all(Rooms::Open.pluck(:id).collect { |room_id| { room_id: room_id, user_id: id } })
  41. end
  42. 1 def deactived_email_address
  43. 4 email_address&.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@")
  44. end
  45. 1 def close_remote_connections(reconnect: false)
  46. 10 ActionCable.server.remote_connections.where(current_user: self).disconnect reconnect: reconnect
  47. end
  48. end

app/models/user/avatar.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 module User::Avatar
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 has_one_attached :avatar
  5. end
  6. 1 class_methods do
  7. 1 def from_avatar_token(sid)
  8. 3 find_signed!(sid, purpose: :avatar)
  9. end
  10. end
  11. 1 def avatar_token
  12. 97 signed_id(purpose: :avatar)
  13. end
  14. end

app/models/user/bot.rb

97.22% lines covered

36 relevant lines. 35 lines covered and 1 lines missed.
    
  1. 1 module User::Bot
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 18 scope :active_bots, -> { active.where(role: :bot) }
  5. 1 scope :without_bots, -> { where.not(role: :bot) }
  6. 1 has_one :webhook, dependent: :delete
  7. end
  8. 1 module ClassMethods
  9. 1 def create_bot!(attributes)
  10. 4 bot_token = generate_bot_token
  11. 4 webhook_url = attributes.delete(:webhook_url)
  12. 4 User.create!(**attributes, bot_token: bot_token, role: :bot).tap do |user|
  13. 4 user.create_webhook!(url: webhook_url) if webhook_url
  14. end
  15. end
  16. 1 def authenticate_bot(bot_key)
  17. 7 bot_id, bot_token = bot_key.split("-")
  18. 7 active.find_by(id: bot_id, bot_token: bot_token)
  19. end
  20. 1 def generate_bot_token
  21. 8 SecureRandom.alphanumeric(12)
  22. end
  23. end
  24. 1 def update_bot!(attributes)
  25. 2 transaction do
  26. 2 update_webhook_url!(attributes.delete(:webhook_url))
  27. 2 update!(attributes)
  28. end
  29. end
  30. 1 def bot_key
  31. 20 "#{id}-#{bot_token}"
  32. end
  33. 1 def reset_bot_key
  34. 2 update! bot_token: self.class.generate_bot_token
  35. end
  36. 1 def webhook_url
  37. 2 webhook&.url
  38. end
  39. 1 def deliver_webhook_later(message)
  40. 2 Bot::WebhookJob.perform_later(self, message) if webhook
  41. end
  42. 1 def deliver_webhook(message)
  43. 1 webhook.deliver(message)
  44. end
  45. 1 private
  46. 1 def update_webhook_url!(url)
  47. 2 if url.present?
  48. webhook&.update!(url: url) || create_webhook!(url: url)
  49. else
  50. 2 webhook&.destroy
  51. end
  52. end
  53. end

app/models/user/mentionable.rb

87.5% lines covered

8 relevant lines. 7 lines covered and 1 lines missed.
    
  1. 1 module User::Mentionable
  2. 1 include ActionText::Attachable
  3. 1 def to_attachable_partial_path
  4. 3 "users/mention"
  5. end
  6. 1 def to_trix_content_attachment_partial_path
  7. "users/mention"
  8. end
  9. 1 def attachable_plain_text_representation(caption)
  10. 25 "@#{name}"
  11. end
  12. end

app/models/user/role.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. 1 module User::Role
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 1 enum role: %i[ member administrator bot ]
  5. end
  6. 1 def can_administer?(record = nil)
  7. 86 administrator? || self == record&.creator || record&.new_record?
  8. end
  9. end

app/models/user/transferable.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 module User::Transferable
  2. 1 extend ActiveSupport::Concern
  3. 1 TRANSFER_LINK_EXPIRY_DURATION = 4.hours
  4. 1 class_methods do
  5. 1 def find_by_transfer_id(id)
  6. 1 find_signed(id, purpose: :transfer)
  7. end
  8. end
  9. 1 def transfer_id
  10. 3 signed_id(purpose: :transfer, expires_in: TRANSFER_LINK_EXPIRY_DURATION)
  11. end
  12. end

app/models/webhook.rb

100.0% lines covered

40 relevant lines. 40 lines covered and 0 lines missed.
    
  1. 1 require "net/http"
  2. 1 require "uri"
  3. 1 class Webhook < ApplicationRecord
  4. 1 ENDPOINT_TIMEOUT = 7.seconds
  5. 1 belongs_to :user
  6. 1 def deliver(message)
  7. 7 post(payload(message)).tap do |response|
  8. 6 if text = extract_text_from(response)
  9. 1 receive_text_reply_to(message.room, text: text)
  10. 5 elsif attachment = extract_attachment_from(response)
  11. 1 receive_attachment_reply_to(message.room, attachment: attachment)
  12. end
  13. end
  14. rescue Net::OpenTimeout, Net::ReadTimeout
  15. 1 receive_text_reply_to message.room, text: "Failed to respond within #{ENDPOINT_TIMEOUT} seconds"
  16. end
  17. 1 private
  18. 1 def post(payload)
  19. 6 http.request \
  20. 6 Net::HTTP::Post.new(uri, "Content-Type" => "application/json").tap { |request| request.body = payload }
  21. end
  22. 1 def http
  23. 6 Net::HTTP.new(uri.host, uri.port).tap do |http|
  24. 6 http.use_ssl = (uri.scheme == "https")
  25. 6 http.open_timeout = ENDPOINT_TIMEOUT
  26. 6 http.read_timeout = ENDPOINT_TIMEOUT
  27. end
  28. end
  29. 1 def uri
  30. 24 @uri ||= URI(url)
  31. end
  32. 1 def payload(message)
  33. {
  34. 7 user: { id: message.creator.id, name: message.creator.name },
  35. room: { id: message.room.id, name: message.room.name, path: room_bot_messages_path(message) },
  36. message: { id: message.id, body: { html: message.body.body, plain: without_recipient_mentions(message.plain_text_body) }, path: message_path(message) }
  37. }.to_json
  38. end
  39. 1 def message_path(message)
  40. 7 Rails.application.routes.url_helpers.room_at_message_path(message.room, message)
  41. end
  42. 1 def room_bot_messages_path(message)
  43. 7 Rails.application.routes.url_helpers.room_bot_messages_path(message.room, user.bot_key)
  44. end
  45. 1 def extract_text_from(response)
  46. 6 response.body.force_encoding("UTF-8") if response.code == "200" && response.content_type.in?(%w[ text/html text/plain ])
  47. end
  48. 1 def receive_text_reply_to(room, text:)
  49. 2 room.messages.create!(body: text, creator: user).broadcast_create
  50. end
  51. 1 def extract_attachment_from(response)
  52. 5 if response.content_type && mime_type = Mime::Type.lookup(response.content_type)
  53. 1 ActiveStorage::Blob.create_and_upload! \
  54. io: StringIO.new(response.body), filename: "attachment.#{mime_type.symbol}", content_type: mime_type.to_s
  55. end
  56. end
  57. 1 def receive_attachment_reply_to(room, attachment:)
  58. 1 room.messages.create_with_attachment!(attachment: attachment, creator: user).broadcast_create
  59. end
  60. 1 def without_recipient_mentions(body)
  61. 7 body \
  62. .gsub(user.attachable_plain_text_representation(nil), "") # Remove mentions of the recipient user
  63. .gsub(/\A\p{Space}+|\p{Space}+\z/, "") # Remove leading and trailing whitespace uncluding unicode spaces
  64. end
  65. end

config/environment.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # Load the Rails application.
  2. 1 require_relative "application"
  3. # Initialize the Rails application.
  4. 1 Rails.application.initialize!

config/environments/test.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 1 require "active_support/core_ext/integer/time"
  2. # The test environment is used exclusively to run your application's
  3. # test suite. You never need to work with it otherwise. Remember that
  4. # your test database is "scratch space" for the test suite and is wiped
  5. # and recreated between test runs. Don't rely on the data there!
  6. 1 Rails.application.configure do
  7. # Settings specified here will take precedence over those in config/application.rb.
  8. # Turn false under Spring and add config.action_view.cache_template_loading = true.
  9. 1 config.cache_classes = true
  10. # Eager loading loads your whole application. When running a single test locally,
  11. # this probably isn't necessary. It's a good idea to do in a continuous integration
  12. # system, or in some way before deploying your code.
  13. 1 config.eager_load = ENV["CI"].present?
  14. # Configure public file server for tests with Cache-Control for performance.
  15. 1 config.public_file_server.enabled = true
  16. 1 config.public_file_server.headers = {
  17. "Cache-Control" => "public, max-age=#{1.hour.to_i}"
  18. }
  19. # Show full error reports and disable caching.
  20. 1 config.consider_all_requests_local = true
  21. 1 config.action_controller.perform_caching = false
  22. 1 config.cache_store = :null_store
  23. # Raise exceptions instead of rendering exception templates.
  24. 1 config.action_dispatch.show_exceptions = :none
  25. # Disable request forgery protection in test environment.
  26. 1 config.action_controller.allow_forgery_protection = false
  27. # Store uploaded files on the local file system in a temporary directory.
  28. 1 config.active_storage.service = :test
  29. # Print deprecation notices to the stderr.
  30. 1 config.active_support.deprecation = :stderr
  31. # Raise exceptions for disallowed deprecations.
  32. 1 config.active_support.disallowed_deprecation = :raise
  33. # Tell Active Support which deprecation messages to disallow.
  34. 1 config.active_support.disallowed_deprecation_warnings = []
  35. # Raises error for missing translations.
  36. # config.i18n.raise_on_missing_translations = true
  37. # Annotate rendered view with file names.
  38. # config.action_view.annotate_rendered_view_with_filenames = true
  39. # Load test helpers
  40. 1 config.autoload_paths += %w[ test/test_helpers ]
  41. end

config/initializers/active_storage.rb

66.67% lines covered

3 relevant lines. 2 lines covered and 1 lines missed.
    
  1. 1 ActiveSupport.on_load(:active_storage_blob) do
  2. 1 ActiveStorage::DiskController.after_action only: :show do
  3. response.set_header("Cache-Control", "max-age=3600, public")
  4. end
  5. end

config/initializers/assets.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # Be sure to restart your server when you modify this file.
  2. # Version of your assets, change this if you want to expire all your assets.
  3. 1 Rails.application.config.assets.version = "1.0"

config/initializers/content_security_policy.rb

100.0% lines covered

0 relevant lines. 0 lines covered and 0 lines missed.
    
  1. # Be sure to restart your server when you modify this file.
  2. # Define an application-wide content security policy.
  3. # See the Securing Rails Applications Guide for more information:
  4. # https://guides.rubyonrails.org/security.html#content-security-policy-header
  5. # Rails.application.configure do
  6. # config.content_security_policy do |policy|
  7. # policy.default_src :self, :https
  8. # policy.font_src :self, :https, :data
  9. # policy.img_src :self, :https, :data
  10. # policy.object_src :none
  11. # policy.script_src :self, :https
  12. # policy.style_src :self, :https
  13. # # Specify URI for violation reports
  14. # # policy.report_uri "/csp-violation-report-endpoint"
  15. # end
  16. #
  17. # # Generate session nonces for permitted importmap and inline scripts
  18. # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
  19. # config.content_security_policy_nonce_directives = %w(script-src)
  20. #
  21. # # Report violations without enforcing the policy.
  22. # # config.content_security_policy_report_only = true
  23. # end

config/initializers/extensions.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 %w[ rails_ext ].each do |extensions_dir|
  2. 6 Dir["#{Rails.root}/lib/#{extensions_dir}/*"].each { |path| require "#{extensions_dir}/#{File.basename(path)}" }
  3. end

config/initializers/filter_parameter_logging.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # Be sure to restart your server when you modify this file.
  2. # Configure parameters to be filtered from the log file. Use this to limit dissemination of
  3. # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
  4. # notations and behaviors.
  5. 1 Rails.application.config.filter_parameters += [
  6. :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :endpoint, "message.body"
  7. ]

config/initializers/inflections.rb

100.0% lines covered

0 relevant lines. 0 lines covered and 0 lines missed.
    
  1. # Be sure to restart your server when you modify this file.
  2. # Add new inflection rules using the following format. Inflections
  3. # are locale specific, and you may define rules for as many different
  4. # locales as you wish. All of these examples are active by default:
  5. # ActiveSupport::Inflector.inflections(:en) do |inflect|
  6. # inflect.plural /^(ox)$/i, "\\1en"
  7. # inflect.singular /^(ox)en/i, "\\1"
  8. # inflect.irregular "person", "people"
  9. # inflect.uncountable %w( fish sheep )
  10. # end
  11. # These inflection rules are supported but not enabled by default:
  12. # ActiveSupport::Inflector.inflections(:en) do |inflect|
  13. # inflect.acronym "RESTful"
  14. # end

config/initializers/permissions_policy.rb

100.0% lines covered

0 relevant lines. 0 lines covered and 0 lines missed.
    
  1. # Define an application-wide HTTP permissions policy. For further
  2. # information see https://developers.google.com/web/updates/2018/06/feature-policy
  3. #
  4. # Rails.application.config.permissions_policy do |f|
  5. # f.camera :none
  6. # f.gyroscope :none
  7. # f.microphone :none
  8. # f.usb :none
  9. # f.fullscreen :self
  10. # f.payment :self, "https://secure.example.com"
  11. # end

config/initializers/sentry.rb

16.67% lines covered

6 relevant lines. 1 lines covered and 5 lines missed.
    
  1. 1 if Rails.env.production? && ENV["SKIP_TELEMETRY"].blank?
  2. Sentry.init do |config|
  3. config.dsn = "https://975a8bf631edee43b6a8cf4823998d92@o33603.ingest.sentry.io/4506587182530560"
  4. config.breadcrumbs_logger = [ :active_support_logger, :http_logger ]
  5. config.send_default_pii = false
  6. config.release = ENV["GIT_REVISION"]
  7. end
  8. end

config/initializers/session_store.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. 1 Rails.application.config.session_store :cookie_store,
  2. key: "_campfire_session",
  3. # Persist session cookie as permament so re-opened browser windows maintain a CSRF token
  4. expire_after: 20.years

config/initializers/sqlite3.rb

52.17% lines covered

23 relevant lines. 12 lines covered and 11 lines missed.
    
  1. 1 module SQLite3Configuration
  2. 1 private
  3. 1 def configure_connection
  4. 2 super
  5. 2 if @config[:retries]
  6. 2 retries = self.class.type_cast_config_to_integer(@config[:retries])
  7. 2 raw_connection.busy_handler do |count|
  8. (count <= retries).tap { |result| sleep count * 0.001 if result }
  9. end
  10. end
  11. end
  12. end
  13. 1 module SQLite3DumpConfiguration
  14. 1 def structure_dump(filename, extra_flags)
  15. args = []
  16. args.concat(Array(extra_flags)) if extra_flags
  17. args << db_config.database
  18. ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
  19. if ignore_tables.any?
  20. ignore_tables = connection.data_sources.select { |table| ignore_tables.any? { |pattern| pattern === table } }
  21. condition = ignore_tables.map { |table| connection.quote(table) }.join(", ")
  22. args << "SELECT sql || ';' FROM sqlite_master WHERE tbl_name NOT IN (#{condition}) ORDER BY tbl_name, type DESC, name"
  23. else
  24. args << ".schema --nosys"
  25. end
  26. run_cmd("sqlite3", args, filename)
  27. end
  28. end
  29. 1 ActiveSupport.on_load :active_record do
  30. 1 ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend SQLite3Configuration
  31. 1 ActiveRecord::Tasks::SQLiteDatabaseTasks.prepend SQLite3DumpConfiguration
  32. end

config/initializers/storage_paths.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 Rails.application.config.after_initialize do
  2. 1 %w[ db files ].each do |dir|
  3. 2 Rails.root.join("storage", dir).mkpath
  4. end
  5. end

config/initializers/time_formats.rb

100.0% lines covered

1 relevant lines. 1 lines covered and 0 lines missed.
    
  1. # Used to match JavaScripts (new Date).getTime() for sorting
  2. 90 Time::DATE_FORMATS[:epoch] = ->(time) { (time.to_f * 1000).to_i }

config/initializers/vapid.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 Rails.application.configure do
  2. 1 config.x.vapid.private_key = ENV.fetch("VAPID_PRIVATE_KEY", Rails.application.credentials.dig(:vapid, :private_key))
  3. 1 config.x.vapid.public_key = ENV.fetch("VAPID_PUBLIC_KEY", Rails.application.credentials.dig(:vapid, :public_key))
  4. end

config/initializers/version.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 Rails.application.config.app_version = ENV.fetch("APP_VERSION", "0")
  2. 1 Rails.application.config.git_revision = ENV["GIT_REVISION"]

config/initializers/web_push.rb

46.15% lines covered

26 relevant lines. 12 lines covered and 14 lines missed.
    
  1. 1 require "web-push"
  2. 1 require "web_push/pool"
  3. 1 require "web_push/notification"
  4. 1 Rails.application.configure do
  5. 1 config.x.web_push_pool = WebPush::Pool.new(
  6. invalid_subscription_handler: ->(subscription_id) do
  7. 2 Rails.application.executor.wrap do
  8. 2 Rails.logger.info "Destroying push subscription: #{subscription_id}"
  9. 2 Push::Subscription.find_by(id: subscription_id)&.destroy
  10. end
  11. end
  12. )
  13. 1 at_exit { config.x.web_push_pool.shutdown }
  14. end
  15. 1 module WebPush::PersistentRequest
  16. 1 def perform
  17. if @options[:connection]
  18. http = @options[:connection]
  19. else
  20. http = Net::HTTP.new(uri.host, uri.port, *proxy_options)
  21. http.use_ssl = true
  22. http.ssl_timeout = @options[:ssl_timeout] unless @options[:ssl_timeout].nil?
  23. http.open_timeout = @options[:open_timeout] unless @options[:open_timeout].nil?
  24. http.read_timeout = @options[:read_timeout] unless @options[:read_timeout].nil?
  25. end
  26. req = Net::HTTP::Post.new(uri.request_uri, headers)
  27. req.body = body
  28. if http.is_a?(Net::HTTP::Persistent)
  29. response = http.request uri, req
  30. else
  31. resp = http.request(req)
  32. verify_response(resp)
  33. end
  34. resp
  35. end
  36. end
  37. 1 WebPush::Request.prepend WebPush::PersistentRequest

config/routes.rb

100.0% lines covered

54 relevant lines. 54 lines covered and 0 lines missed.
    
  1. 1 Rails.application.routes.draw do
  2. 1 root "welcome#show"
  3. 1 resource :first_run
  4. 1 resource :session do
  5. 1 scope module: "sessions" do
  6. 1 resources :transfers, only: %i[ show update ]
  7. end
  8. end
  9. 1 resource :account do
  10. 1 scope module: "accounts" do
  11. 1 resources :users
  12. 1 resources :bots do
  13. 1 scope module: "bots" do
  14. 1 resource :key, only: :update
  15. end
  16. end
  17. 1 resource :join_code, only: :create
  18. 1 resource :logo, only: %i[ show destroy ]
  19. 1 resource :custom_styles, only: %i[ edit update ]
  20. end
  21. end
  22. 1 direct :fresh_account_logo do |options|
  23. 57 route_for :account_logo, v: Current.account&.updated_at&.to_fs(:number), size: options[:size]
  24. end
  25. 1 get "join/:join_code", to: "users#new", as: :join
  26. 1 post "join/:join_code", to: "users#create"
  27. 1 resources :qr_code, only: :show
  28. 1 resources :users, only: :show do
  29. 1 scope module: "users" do
  30. 1 resource :avatar, only: %i[ show destroy ]
  31. 1 scope defaults: { user_id: "me" } do
  32. 1 resource :sidebar, only: :show
  33. 1 resource :profile
  34. 1 resources :push_subscriptions do
  35. 1 scope module: "push_subscriptions" do
  36. 1 resources :test_notifications, only: :create
  37. end
  38. end
  39. end
  40. end
  41. end
  42. 1 namespace :autocompletable do
  43. 1 resources :users, only: :index
  44. end
  45. 1 direct :fresh_user_avatar do |user, options|
  46. 95 route_for :user_avatar, user.avatar_token, v: user.updated_at.to_fs(:number)
  47. end
  48. 1 resources :rooms do
  49. 1 resources :messages
  50. 1 post ":bot_key/messages", to: "messages/by_bots#create", as: :bot_messages
  51. 1 scope module: "rooms" do
  52. 1 resource :refresh, only: :show
  53. 1 resource :settings, only: :show
  54. 1 resource :involvement, only: %i[ show update ]
  55. end
  56. 1 get "@:message_id", to: "rooms#show", as: :at_message
  57. end
  58. 1 namespace :rooms do
  59. 1 resources :opens
  60. 1 resources :closeds
  61. 1 resources :directs
  62. end
  63. 1 resources :messages do
  64. 1 scope module: "messages" do
  65. 1 resources :boosts
  66. end
  67. end
  68. 1 resources :searches, only: %i[ index create ] do
  69. 1 delete :clear, on: :collection
  70. end
  71. 1 resource :unfurl_link, only: :create
  72. 1 get "webmanifest" => "pwa#manifest"
  73. 1 get "service-worker" => "pwa#service_worker"
  74. 1 get "up" => "rails/health#show", as: :rails_health_check
  75. end

lib/rails_ext/action_text_attachables.rb

92.86% lines covered

14 relevant lines. 13 lines covered and 1 lines missed.
    
  1. 1 ActiveSupport.on_load(:action_text_content) do
  2. 1 class ActionText::Attachment
  3. 1 class << self
  4. 1 def from_node(node, attachable = nil)
  5. 39 new(node, attachable || ActionText::Attachment::OpengraphEmbed.from_node(node) || attachable_from_possibly_expired_sgid(node["sgid"]) || ActionText::Attachable.from_node(node))
  6. end
  7. 1 private
  8. # Our @mentions use ActionText attachments, which are signed. If someone rotates SECRET_KEY_BASE, the existing attachments become invalid.
  9. # This allows ignoring invalid signatures for User attachments in ActionText.
  10. 1 ATTACHABLES_PERMITTED_WITH_INVALID_SIGNATURES = %w[ User ]
  11. 1 def attachable_from_possibly_expired_sgid(sgid)
  12. 24 if message = sgid&.split("--")&.first
  13. 23 encoded_message = JSON.parse Base64.strict_decode64(message)
  14. 23 decoded_gid = Marshal.load Base64.urlsafe_decode64(encoded_message.dig("_rails", "message"))
  15. 23 model = GlobalID.find(decoded_gid)
  16. 23 model.model_name.to_s.in?(ATTACHABLES_PERMITTED_WITH_INVALID_SIGNATURES) ? model : nil
  17. end
  18. rescue ActiveRecord::RecordNotFound
  19. nil
  20. end
  21. end
  22. end
  23. end

lib/rails_ext/actiontext_opengraph_embeds.rb

85.71% lines covered

21 relevant lines. 18 lines covered and 3 lines missed.
    
  1. 1 class ActionText::Attachment::OpengraphEmbed
  2. 1 include ActiveModel::Model
  3. 1 OPENGRAPH_EMBED_CONTENT_TYPE = "application/vnd.actiontext.opengraph-embed"
  4. 1 class << self
  5. 1 def from_node(node)
  6. 36 if node["content-type"]
  7. 33 if matches = node["content-type"].match(OPENGRAPH_EMBED_CONTENT_TYPE)
  8. 12 attachment = new(attributes_from_node(node))
  9. 12 attachment if attachment.valid?
  10. end
  11. end
  12. end
  13. 1 private
  14. 1 def attributes_from_node(node)
  15. {
  16. 12 href: node["href"],
  17. url: node["url"],
  18. filename: node["filename"],
  19. description: node["caption"]
  20. }
  21. end
  22. end
  23. 1 attr_accessor :href, :url, :filename, :description
  24. 1 def attachable_content_type
  25. OPENGRAPH_EMBED_CONTENT_TYPE
  26. end
  27. 1 def attachable_plain_text_representation(caption)
  28. 12 ""
  29. end
  30. 1 def to_partial_path
  31. "action_text/attachables/opengraph_embed"
  32. end
  33. 1 def to_trix_content_attachment_partial_path
  34. "action_text/attachables/opengraph_embed"
  35. end
  36. end

lib/rails_ext/filter.rb

85.71% lines covered

14 relevant lines. 12 lines covered and 2 lines missed.
    
  1. 1 class ActionText::Content::Filter
  2. 1 class << self
  3. 1 def apply(content)
  4. 125 filter = new(content)
  5. 125 filter.applicable? ? ActionText::Content.new(filter.apply, canonicalize: false) : content
  6. end
  7. end
  8. 1 def initialize(content)
  9. 125 @content = content
  10. end
  11. 1 def applicable?
  12. raise NotImplementedError
  13. end
  14. 1 def apply
  15. raise NotImplementedError
  16. end
  17. 1 private
  18. 1 attr_reader :content
  19. 1 delegate :fragment, to: :content
  20. end

lib/rails_ext/filters.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 class ActionText::Content::Filters
  2. 1 def initialize(*filters)
  3. 1 @filters = filters
  4. end
  5. 1 def apply(content)
  6. 164 filters.reduce(content) { |content, filter| filter.apply(content) }
  7. end
  8. 1 private
  9. 1 attr_reader :filters
  10. end

lib/rails_ext/string.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 class String
  2. 1 def all_emoji?
  3. 44 self.match? /\A(\p{Emoji_Presentation}|\p{Extended_Pictographic}|\uFE0F)+\z/u
  4. end
  5. end

lib/restricted_http/private_network_guard.rb

92.31% lines covered

13 relevant lines. 12 lines covered and 1 lines missed.
    
  1. 1 require "resolv"
  2. 1 module RestrictedHTTP
  3. 1 class Violation < StandardError; end
  4. 1 module PrivateNetworkGuard
  5. 1 extend self
  6. 1 LOCAL_IP = IPAddr.new("0.0.0.0/8") # "This" network
  7. 1 def resolve(hostname)
  8. 85 Resolv.getaddress(hostname).tap do |ip|
  9. 77 raise Violation.new("Attempt to access private IP via #{hostname}") if ip && private_ip?(ip)
  10. end
  11. end
  12. 1 def private_ip?(ip)
  13. 77 IPAddr.new(ip).then do |ipaddr|
  14. 77 ipaddr.private? || ipaddr.loopback? || LOCAL_IP.include?(ipaddr)
  15. end
  16. rescue IPAddr::InvalidAddressError
  17. true
  18. end
  19. end
  20. end

lib/web_push/notification.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. 1 class WebPush::Notification
  2. 1 def initialize(title:, body:, path:, badge:, endpoint:, p256dh_key:, auth_key:)
  3. 14 @title, @body, @path, @badge = title, body, path, badge
  4. 14 @endpoint, @p256dh_key, @auth_key = endpoint, p256dh_key, auth_key
  5. end
  6. 1 def deliver(connection: nil)
  7. 14 WebPush.payload_send \
  8. message: encoded_message,
  9. endpoint: @endpoint, p256dh: @p256dh_key, auth: @auth_key,
  10. vapid: vapid_identification,
  11. connection: connection,
  12. urgency: "high"
  13. end
  14. 1 private
  15. 1 def vapid_identification
  16. 14 { subject: "mailto:support@37signals.com" }.merge \
  17. Rails.configuration.x.vapid.symbolize_keys
  18. end
  19. 1 def encoded_message
  20. 14 JSON.generate title: @title, options: { body: @body, icon: icon_path, data: { path: @path, badge: @badge } }
  21. end
  22. 1 def icon_path
  23. 14 Rails.application.routes.url_helpers.account_logo_path
  24. end
  25. end

lib/web_push/pool.rb

93.55% lines covered

31 relevant lines. 29 lines covered and 2 lines missed.
    
  1. # This is in lib so we can use it in a thread pool without the Rails executor
  2. 1 class WebPush::Pool
  3. 1 attr_reader :delivery_pool, :invalidation_pool, :connection, :invalid_subscription_handler
  4. 1 def initialize(invalid_subscription_handler:)
  5. 224 @delivery_pool = Concurrent::ThreadPoolExecutor.new(max_threads: 50, queue_size: 10000)
  6. 224 @invalidation_pool = Concurrent::FixedThreadPool.new(1)
  7. 224 @connection = Net::HTTP::Persistent.new(name: "web_push", pool_size: 150)
  8. 224 @invalid_subscription_handler = invalid_subscription_handler
  9. end
  10. 1 def queue(payload, subscriptions)
  11. 12 subscriptions.find_each do |subscription|
  12. 14 deliver_later(payload, subscription)
  13. end
  14. end
  15. 1 def shutdown
  16. 223 connection.shutdown
  17. 223 shutdown_pool(delivery_pool)
  18. 223 shutdown_pool(invalidation_pool)
  19. end
  20. 1 private
  21. 1 def deliver_later(payload, subscription)
  22. # Ensure any AR operations happen before we post to the thread pool
  23. 14 notification = subscription.notification(**payload)
  24. 14 subscription_id = subscription.id
  25. 14 delivery_pool.post do
  26. 14 deliver(notification, subscription_id)
  27. rescue Exception => e
  28. Rails.logger.error "Error in WebPush::Pool.deliver: #{e.class} #{e.message}"
  29. end
  30. rescue Concurrent::RejectedExecutionError
  31. end
  32. 1 def deliver(notification, id)
  33. 14 notification.deliver(connection: connection)
  34. rescue WebPush::ExpiredSubscription, OpenSSL::OpenSSLError => ex
  35. 2 invalidate_subscription_later(id) if invalid_subscription_handler
  36. end
  37. 1 def invalidate_subscription_later(id)
  38. 2 invalidation_pool.post do
  39. 2 invalid_subscription_handler.call(id)
  40. rescue Exception => e
  41. Rails.logger.error "Error in WebPush::Pool.invalid_subscription_handler: #{e.class} #{e.message}"
  42. end
  43. end
  44. 1 def shutdown_pool(pool)
  45. 446 pool.shutdown
  46. 446 pool.kill unless pool.wait_for_termination(1)
  47. end
  48. end

test/channels/presence_channel_test.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class PresenceChannelTest < ActionCable::Channel::TestCase
  3. 1 setup do
  4. 6 stub_connection(current_user: users(:david))
  5. end
  6. 1 test "subscribes" do
  7. 1 room = users(:david).rooms.first
  8. 1 subscribe room_id: room.id
  9. 1 assert subscription.confirmed?
  10. 1 assert_has_stream_for room
  11. end
  12. 1 test "rejects subscription to a room that the user is not a member of" do
  13. 1 subscribe room_id: Rooms::Closed.create!(name: "New Room", creator: users(:david)).id
  14. 1 assert subscription.rejected?
  15. end
  16. 1 test "rejects subscription to non-existent room" do
  17. 1 subscribe room_id: -1
  18. 1 assert subscription.rejected?
  19. end
  20. 1 test "rejects subscription without a room" do
  21. 1 subscribe room_id: -1
  22. 1 assert subscription.rejected?
  23. end
  24. 1 test "subscribing marks the membership as connected" do
  25. 1 membership = users(:david).memberships.first
  26. 3 assert_changes -> { membership.reload.connected? }, from: false, to: true do
  27. 1 subscribe room_id: membership.room_id
  28. end
  29. end
  30. 1 test "unsubscribing marks the membership as disconnected" do
  31. 1 membership = users(:david).memberships.first
  32. 1 subscribe room_id: membership.room_id
  33. 3 assert_changes -> { membership.reload.connected? }, from: true, to: false do
  34. 1 unsubscribe
  35. end
  36. end
  37. end

test/controllers/accounts/bots/keys_controller_test.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Accounts::Bots::KeysControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 1 sign_in :david
  5. end
  6. 1 test "update" do
  7. 3 assert_changes -> { users(:bender).reload.bot_token } do
  8. 1 put account_bot_key_url(users(:bender))
  9. 1 assert_redirected_to account_bots_url
  10. end
  11. end
  12. end

test/controllers/accounts/bots_controller_test.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Accounts::BotsControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 5 sign_in :david
  5. end
  6. 1 test "index" do
  7. 1 get account_bots_url
  8. 1 assert_response :ok
  9. end
  10. 1 test "create" do
  11. 1 get new_account_bot_url
  12. 1 assert_response :ok
  13. 1 post account_bots_url, params: { user: { name: "Bender's Friend" } }
  14. 1 assert_redirected_to account_bots_url
  15. 1 assert_equal "Bender's Friend", User.bot.last.name
  16. end
  17. 1 test "update" do
  18. 1 get edit_account_bot_url(users(:bender))
  19. 1 assert_response :ok
  20. 1 put account_bot_url(users(:bender)), params: { user: { name: "Bender's New Friend" } }
  21. 1 assert_redirected_to account_bots_url
  22. 1 assert_equal "Bender's New Friend", users(:bender).reload.name
  23. end
  24. 1 test "destroy" do
  25. 3 assert_difference -> { User.active_bots.count }, -1 do
  26. 1 delete account_bot_url(users(:bender))
  27. end
  28. 1 assert users(:bender).reload.deactivated?
  29. end
  30. 1 test "remove webhook" do
  31. 3 assert_difference -> { Webhook.count }, -1 do
  32. 1 put account_bot_url(users(:bender)), params: { user: { name: "Bender's New Friend", webook_url: "" } }
  33. 1 assert_redirected_to account_bots_url
  34. end
  35. end
  36. end

test/controllers/accounts/custom_styles_controller_test.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Accounts::CustomStylesControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 3 sign_in :david
  5. end
  6. 1 test "edit" do
  7. 1 get edit_account_custom_styles_url
  8. 1 assert_response :ok
  9. end
  10. 1 test "update" do
  11. 1 assert users(:david).administrator?
  12. 1 put account_custom_styles_url, params: { account: { custom_styles: ":root { --color-text: red; }" } }
  13. 1 assert_redirected_to edit_account_custom_styles_url
  14. 1 assert_equal accounts(:signal).custom_styles, ":root { --color-text: red; }"
  15. end
  16. 1 test "non-admins cannot update" do
  17. 1 sign_in :kevin
  18. 1 assert users(:kevin).member?
  19. 1 put account_custom_styles_url, params: { account: { custom_styles: ":root { --color-text: red; }" } }
  20. 1 assert_response :forbidden
  21. end
  22. end

test/controllers/accounts/join_codes_controller_test.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Accounts::JoinCodesControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 2 sign_in :david
  5. end
  6. 1 test "create new join code" do
  7. 3 assert_changes -> { accounts(:signal).reload.join_code } do
  8. 1 post account_join_code_url
  9. 1 assert_redirected_to edit_account_url
  10. end
  11. end
  12. 1 test "only administrators can create new join codes" do
  13. 1 sign_in :jz
  14. 1 post account_join_code_url
  15. 1 assert_response :forbidden
  16. end
  17. end

test/controllers/accounts/logos_controller_test.rb

100.0% lines covered

30 relevant lines. 30 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 require "vips"
  3. 1 class Accounts::LogosControllerTest < ActionDispatch::IntegrationTest
  4. 1 setup do
  5. 5 sign_in :david
  6. end
  7. 1 test "show stock" do
  8. 1 get account_logo_url
  9. 1 assert_valid_png_response size: 512
  10. end
  11. 1 test "show stock small size" do
  12. 1 get account_logo_url(size: :small)
  13. 1 assert_valid_png_response size: 192
  14. end
  15. 1 test "show custom" do
  16. 1 accounts(:signal).update! logo: fixture_file_upload("moon.jpg", "image/jpeg")
  17. 1 get account_logo_url
  18. 1 assert_valid_png_response size: 512
  19. end
  20. 1 test "show custom small size" do
  21. 1 accounts(:signal).update! logo: fixture_file_upload("moon.jpg", "image/jpeg")
  22. 1 get account_logo_url(size: :small)
  23. 1 assert_valid_png_response size: 192
  24. end
  25. 1 test "destroy" do
  26. 1 accounts(:signal).update! logo: fixture_file_upload("moon.jpg", "image/jpeg")
  27. 1 delete account_logo_url
  28. 1 assert_redirected_to edit_account_url
  29. 1 assert_not accounts(:signal).reload.logo.attached?
  30. end
  31. 1 private
  32. 1 def assert_valid_png_response(size:)
  33. 4 assert_equal @response.headers["content-type"], "image/png"
  34. 4 image = ::Vips::Image.new_from_buffer(@response.body, "")
  35. 4 assert_equal size, image.width
  36. 4 assert_equal size, image.height
  37. end
  38. end

test/controllers/accounts/users_controller_test.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Accounts::UsersControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 3 sign_in :david
  5. end
  6. 1 test "update" do
  7. 1 assert users(:david).administrator?
  8. 1 put account_user_url(users(:david)), params: { user: { role: "administrator" } }
  9. 1 assert_redirected_to edit_account_url
  10. 1 assert users(:david).reload.administrator?
  11. end
  12. 1 test "destroy" do
  13. 3 assert_difference -> { User.active.count }, -1 do
  14. 1 delete account_user_url(users(:david))
  15. end
  16. 1 assert_redirected_to edit_account_url
  17. 1 assert_nil User.active.find_by(id: users(:david).id)
  18. end
  19. 1 test "non-admins cannot perform actions" do
  20. 1 sign_in :kevin
  21. 1 put account_user_url(users(:david)), params: { user: { role: "administrator" } }
  22. 1 assert_response :forbidden
  23. 1 delete account_user_url(users(:david))
  24. 1 assert_response :forbidden
  25. end
  26. end

test/controllers/accounts_controller_test.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class AccountsControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 3 sign_in :david
  5. end
  6. 1 test "edit" do
  7. 1 get edit_account_url
  8. 1 assert_response :ok
  9. end
  10. 1 test "update" do
  11. 1 assert users(:david).administrator?
  12. 1 put account_url, params: { account: { name: "Different" } }
  13. 1 assert_redirected_to edit_account_url
  14. 1 assert_equal accounts(:signal).name, "Different"
  15. end
  16. 1 test "non-admins cannot update" do
  17. 1 sign_in :kevin
  18. 1 assert users(:kevin).member?
  19. 1 put account_url, params: { account: { name: "Different" } }
  20. 1 assert_response :forbidden
  21. end
  22. end

test/controllers/autocompletable/users_controller_test.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Autocompletable::UsersControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 4 sign_in :david
  5. end
  6. 1 test "search returns matching users" do
  7. 1 get autocompletable_users_url(format: :json), params: { query: "da" }
  8. 1 assert_response :success
  9. 1 assert_equal "David", response.parsed_body.first["name"]
  10. end
  11. 1 test "search results escape HTML in names" do
  12. 1 users(:david).update!(name: "David <script>alert(123)</script>")
  13. 1 get autocompletable_users_url(format: :json), params: { query: "da" }
  14. 1 assert_response :success
  15. 1 assert_equal "David &lt;script&gt;alert(123)&lt;/script&gt;", response.parsed_body.first["name"]
  16. end
  17. 1 test "room search returns matching users" do
  18. 1 get autocompletable_users_url(room_id: rooms(:hq).id, format: :json), params: { query: "da" }
  19. 1 assert_response :success
  20. 1 assert_equal "David", response.parsed_body.first["name"]
  21. end
  22. 1 test "room search is scoped by membership" do
  23. 1 sign_in :kevin
  24. 1 assert_not_includes users(:kevin).rooms, rooms(:watercooler)
  25. 1 assert_raises ActiveRecord::RecordNotFound do
  26. 1 get autocompletable_users_url(room_id: rooms(:watercooler).id, format: :json), params: { query: "da" }
  27. end
  28. end
  29. end

test/controllers/first_runs_controller_test.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class FirstRunsControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 3 Account.destroy_all
  5. 3 User.destroy_all
  6. 3 Room.destroy_all
  7. end
  8. 1 test "new is permitted when no other users exit" do
  9. 1 get first_run_url
  10. 1 assert_response :success
  11. end
  12. 1 test "new is not permitted when account exist" do
  13. 1 Account.create!(name: "Chat")
  14. 1 get first_run_url
  15. 1 assert_redirected_to root_url
  16. end
  17. 1 test "create" do
  18. 3 assert_difference -> { Room.count }, 1 do
  19. 3 assert_difference -> { User.count }, 1 do
  20. 1 post first_run_url, params: { account: { name: "37signals" }, user: { name: "New Person", email_address: "new@37signals.com", password: "secret123456" } }
  21. end
  22. end
  23. 1 assert_redirected_to root_url
  24. 1 assert parsed_cookies.signed[:session_token]
  25. end
  26. end

test/controllers/messages/boosts_controller_test.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Messages::BoostsControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 2 sign_in :david
  5. 2 @message = messages(:first)
  6. end
  7. 1 test "create" do
  8. 1 assert_turbo_stream_broadcasts [ @message.room, :messages ], count: 1 do
  9. 3 assert_difference -> { @message.boosts.count }, 1 do
  10. 1 post message_boosts_url(@message, format: :turbo_stream), params: { boost: { content: "Morning!" } }
  11. 1 assert_redirected_to message_boosts_url(@message)
  12. end
  13. end
  14. end
  15. 1 test "destroy" do
  16. 1 assert_turbo_stream_broadcasts [ @message.room, :messages ], count: 1 do
  17. 3 assert_difference -> { @message.boosts.count }, -1 do
  18. 1 delete message_boost_url(@message, boosts(:first), format: :turbo_stream)
  19. 1 assert_response :success
  20. end
  21. end
  22. end
  23. end

test/controllers/messages/by_bots_controller_test.rb

100.0% lines covered

26 relevant lines. 26 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Messages::ByBotsControlleTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 6 @room = rooms(:watercooler)
  5. end
  6. 1 test "create" do
  7. 3 assert_difference -> { Message.count }, +1 do
  8. 1 post room_bot_messages_url(@room, users(:bender).bot_key), params: "Hello Bot World!"
  9. 1 assert_equal "Hello Bot World!", Message.last.plain_text_body
  10. end
  11. end
  12. 1 test "create with UTF-8 content" do
  13. 3 assert_difference -> { Message.count }, +1 do
  14. 1 post room_bot_messages_url(@room, users(:bender).bot_key), params: "Hello 👋!"
  15. 1 assert_equal "Hello 👋!", Message.last.plain_text_body
  16. end
  17. end
  18. 1 test "create file" do
  19. 3 assert_difference -> { Message.count }, +1 do
  20. 1 post room_bot_messages_url(@room, users(:bender).bot_key), params: { attachment: fixture_file_upload("moon.jpg", "image/jpeg") }
  21. 1 assert Message.last.attachment.present?
  22. end
  23. end
  24. 1 test "create does not trigger a webhook to the sending bot if it mentions itself" do
  25. 1 body = "<div>Hey #{mention_attachment_for(:bender)}</div>"
  26. 1 assert_no_enqueued_jobs only: Bot::WebhookJob do
  27. 1 post room_bot_messages_url(@room, users(:bender).bot_key), params: body
  28. end
  29. end
  30. 1 test "create does not trigger a webhook to the sending bot in a direct room" do
  31. 1 assert_no_enqueued_jobs only: Bot::WebhookJob do
  32. 1 post room_bot_messages_url(rooms(:bender_and_kevin), users(:bender).bot_key), params: "Talking to myself again!"
  33. end
  34. end
  35. 1 test "denied index" do
  36. 1 get room_messages_url(@room, bot_key: users(:bender).bot_key, format: :json)
  37. 1 assert_response :forbidden
  38. end
  39. end

test/controllers/messages_controller_test.rb

100.0% lines covered

88 relevant lines. 88 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class MessagesControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 14 host! "once.campfire.test"
  5. 14 sign_in :david
  6. 14 @room = rooms(:watercooler)
  7. 14 @messages = @room.messages.ordered.to_a
  8. end
  9. 1 test "index returns the last page by default" do
  10. 1 get room_messages_url(@room)
  11. 1 assert_response :success
  12. 1 ensure_messages_present @messages.last
  13. end
  14. 1 test "index returns a page before the specified message" do
  15. 1 get room_messages_url(@room, before: @messages.third)
  16. 1 assert_response :success
  17. 1 ensure_messages_present @messages.first, @messages.second
  18. 1 ensure_messages_not_present @messages.third, @messages.fourth, @messages.fifth
  19. end
  20. 1 test "index returns a page after the specified message" do
  21. 1 get room_messages_url(@room, after: @messages.third)
  22. 1 assert_response :success
  23. 1 ensure_messages_present @messages.fourth, @messages.fifth
  24. 1 ensure_messages_not_present @messages.first, @messages.second, @messages.third
  25. end
  26. 1 test "index returns no_content when there are no messages" do
  27. 1 @room.messages.destroy_all
  28. 1 get room_messages_url(@room)
  29. 1 assert_response :no_content
  30. end
  31. 1 test "get renders a single message belonging to the user" do
  32. 1 message = @room.messages.where(creator: users(:david)).first
  33. 1 get room_message_url(@room, message)
  34. 1 assert_response :success
  35. end
  36. 1 test "creating a message broadcasts the message to the room" do
  37. 1 post room_messages_url(@room, format: :turbo_stream), params: { message: { body: "New one", client_message_id: 999 } }
  38. 1 assert_rendered_turbo_stream_broadcast @room, :messages, action: "append", target: [ @room, :messages ] do
  39. 1 assert_select ".message__body", text: /New one/
  40. 1 assert_copy_link_button room_at_message_url(@room, Message.last, host: "once.campfire.test")
  41. end
  42. end
  43. 1 test "creating a message broadcasts unread room" do
  44. 1 assert_broadcasts "unread_rooms", 1 do
  45. 1 post room_messages_url(@room, format: :turbo_stream), params: { message: { body: "New one", client_message_id: 999 } }
  46. end
  47. end
  48. 1 test "update updates a message belonging to the user" do
  49. 1 message = @room.messages.where(creator: users(:david)).first
  50. 1 Turbo::StreamsChannel.expects(:broadcast_replace_to).once
  51. 1 put room_message_url(@room, message), params: { message: { body: "Updated body" } }
  52. 1 assert_redirected_to room_message_url(@room, message)
  53. 1 assert_equal "Updated body", message.reload.plain_text_body
  54. end
  55. 1 test "admin updates a message belonging to another user" do
  56. 1 message = @room.messages.where(creator: users(:jason)).first
  57. 1 Turbo::StreamsChannel.expects(:broadcast_replace_to).once
  58. 1 put room_message_url(@room, message), params: { message: { body: "Updated body" } }
  59. 1 assert_redirected_to room_message_url(@room, message)
  60. 1 assert_equal "Updated body", message.reload.plain_text_body
  61. end
  62. 1 test "destroy destroys a message belonging to the user" do
  63. 1 message = @room.messages.where(creator: users(:david)).first
  64. 3 assert_difference -> { Message.count }, -1 do
  65. 1 Turbo::StreamsChannel.expects(:broadcast_remove_to).once
  66. 1 delete room_message_url(@room, message, format: :turbo_stream)
  67. 1 assert_response :success
  68. end
  69. end
  70. 1 test "admin destroy destroys a message belonging to another user" do
  71. 1 assert users(:david).administrator?
  72. 1 message = @room.messages.where(creator: users(:jason)).first
  73. 3 assert_difference -> { Message.count }, -1 do
  74. 1 Turbo::StreamsChannel.expects(:broadcast_remove_to).once
  75. 1 delete room_message_url(@room, message, format: :turbo_stream)
  76. 1 assert_response :success
  77. end
  78. end
  79. 1 test "ensure non-admin can't update a message belonging to another user" do
  80. 1 sign_in :jz
  81. 1 assert_not users(:jz).administrator?
  82. 1 room = rooms(:designers)
  83. 1 message = room.messages.where(creator: users(:jason)).first
  84. 1 put room_message_url(room, message), params: { message: { body: "Updated body" } }
  85. 1 assert_response :forbidden
  86. end
  87. 1 test "ensure non-admin can't destroy a message belonging to another user" do
  88. 1 sign_in :jz
  89. 1 assert_not users(:jz).administrator?
  90. 1 room = rooms(:designers)
  91. 1 message = room.messages.where(creator: users(:jason)).first
  92. 1 delete room_message_url(room, message, format: :turbo_stream)
  93. 1 assert_response :forbidden
  94. end
  95. 1 test "mentioning a bot triggers a webhook" do
  96. 1 WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200)
  97. 1 assert_enqueued_jobs 1, only: Bot::WebhookJob do
  98. 1 post room_messages_url(@room, format: :turbo_stream), params: { message: {
  99. body: "<div>Hey #{mention_attachment_for(:bender)}</div>", client_message_id: 999 } }
  100. end
  101. end
  102. 1 private
  103. 1 def ensure_messages_present(*messages, count: 1)
  104. 5 messages.each do |message|
  105. 11 assert_select "#" + dom_id(message), count:
  106. end
  107. end
  108. 1 def ensure_messages_not_present(*messages)
  109. 2 ensure_messages_present *messages, count: 0
  110. end
  111. 1 def assert_copy_link_button(url)
  112. 1 assert_select ".btn[title='Copy link'][data-copy-to-clipboard-content-value='#{url}']"
  113. end
  114. end

test/controllers/qr_code_controller_test.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class QrCodeControllerTest < ActionDispatch::IntegrationTest
  3. 1 test "show renders a QR code as a cacheable SVG image" do
  4. 1 id = Base64.urlsafe_encode64("http://example.com")
  5. 1 get qr_code_path(id)
  6. 1 assert_response :success
  7. 1 assert_includes response.content_type, "image/svg+xml"
  8. 1 assert_equal 1.year, response.cache_control[:max_age].to_i
  9. 1 assert response.cache_control[:public]
  10. end
  11. end

test/controllers/rooms/closeds_controller_test.rb

100.0% lines covered

38 relevant lines. 38 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Rooms::ClosedsControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 7 sign_in :david
  5. end
  6. 1 test "show redirects to get general show" do
  7. 1 get rooms_open_url(users(:david).rooms.closeds.last)
  8. 1 assert_redirected_to room_url(users(:david).rooms.closeds.last)
  9. end
  10. 1 test "new" do
  11. 1 get new_rooms_closed_url
  12. 1 assert_response :success
  13. end
  14. 1 test "create" do
  15. 1 assert_turbo_stream_broadcasts [ users(:david), :rooms ], count: 1 do
  16. 1 assert_turbo_stream_broadcasts [ users(:kevin), :rooms ], count: 1 do
  17. 1 assert_turbo_stream_broadcasts [ users(:jason), :rooms ], count: 1 do
  18. 1 post rooms_closeds_url, params: { room: { name: "My New Room" }, user_ids: [ users(:david).id, users(:kevin).id, users(:jason).id ] }
  19. end
  20. end
  21. end
  22. 1 new_room = Room.last
  23. 1 assert_equal new_room.memberships.count, 3
  24. 1 assert_redirected_to room_url(Room.last)
  25. end
  26. 1 test "update with membership revisions" do
  27. 3 assert_difference -> { rooms(:designers).reload.users.count }, -1 do
  28. 1 put rooms_closed_url(rooms(:designers)), params: {
  29. room: { name: "New Name" }, user_ids: rooms(:designers).users.without(users(:jason)).collect(&:id)
  30. }
  31. end
  32. 1 assert_redirected_to room_url(rooms(:designers))
  33. 1 assert rooms(:designers).reload.name, "New Name"
  34. end
  35. 1 test "update an open room to be closed" do
  36. 1 put rooms_closed_url(rooms(:pets)), params: { room: { name: "Doesn't matter" }, user_ids: [ users(:david).id, users(:jason).id ] }
  37. 1 assert_equal rooms(:pets).memberships.count, 2
  38. end
  39. 1 test "only admins or creators can update" do
  40. 1 sign_in :jz
  41. 1 assert_turbo_stream_broadcasts :rooms, count: 0 do
  42. 1 put rooms_closed_url(rooms(:designers)), params: { room: { name: "New Name" } }
  43. end
  44. 1 assert_response :forbidden
  45. 1 assert rooms(:designers).reload.name, "Designers"
  46. end
  47. 1 test "remove yourself" do
  48. 3 assert_difference -> { users(:david).rooms.count }, -1 do
  49. 1 put rooms_closed_url(rooms(:designers), params: { room: { name: "Designers" }, user_ids: [ users(:jason).id, users(:jz).id ] })
  50. 1 assert_redirected_to room_url(rooms(:designers))
  51. 1 follow_redirect!
  52. 1 assert_redirected_to root_url
  53. end
  54. end
  55. end

test/controllers/rooms/directs_controller_test.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Rooms::DirectsControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 3 sign_in :david
  5. end
  6. 1 test "create" do
  7. 1 post rooms_directs_url, params: { user_ids: [ users(:jz).id ] }
  8. 1 room = Room.last
  9. 1 assert_redirected_to room_url(room)
  10. 1 assert room.users.include?(users(:david))
  11. 1 assert room.users.include?(users(:jz))
  12. end
  13. 1 test "create only once per user set" do
  14. 3 assert_difference -> { Room.all.count }, +1 do
  15. 1 post rooms_directs_url, params: { user_ids: [ users(:jz).id ] }
  16. 1 post rooms_directs_url, params: { user_ids: [ users(:jz).id ] }
  17. end
  18. end
  19. 1 test "destroy only allowed for all room users" do
  20. 1 sign_in :kevin
  21. 3 assert_difference -> { Room.count }, -1 do
  22. 1 delete rooms_direct_url(rooms(:david_and_kevin))
  23. 1 assert_redirected_to root_url
  24. end
  25. end
  26. end

test/controllers/rooms/involvements_controller_test.rb

100.0% lines covered

26 relevant lines. 26 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Rooms::InvolvementsControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 4 sign_in :david
  5. end
  6. 1 test "show" do
  7. 1 get room_involvement_url(rooms(:designers))
  8. 1 assert_response :success
  9. end
  10. 1 test "update involvement sends turbo update when becoming visible and when going invisible" do
  11. 1 assert_turbo_stream_broadcasts [ users(:david), :rooms ], count: 1 do
  12. 3 assert_changes -> { memberships(:david_watercooler).reload.involvement }, from: "everything", to: "invisible" do
  13. 1 put room_involvement_url(rooms(:watercooler)), params: { involvement: "invisible" }
  14. 1 assert_redirected_to room_involvement_url(rooms(:watercooler))
  15. end
  16. end
  17. 1 assert_turbo_stream_broadcasts [ users(:david), :rooms ], count: 2 do
  18. 3 assert_changes -> { memberships(:david_watercooler).reload.involvement }, from: "invisible", to: "everything" do
  19. 1 put room_involvement_url(rooms(:watercooler)), params: { involvement: "everything" }
  20. 1 assert_redirected_to room_involvement_url(rooms(:watercooler))
  21. end
  22. end
  23. end
  24. 1 test "updating involvement does not send turbo update changing visible states" do
  25. 1 assert_no_turbo_stream_broadcasts [ users(:david), :rooms ] do
  26. 3 assert_changes -> { memberships(:david_watercooler).reload.involvement }, from: "everything", to: "mentions" do
  27. 1 put room_involvement_url(rooms(:watercooler)), params: { involvement: "mentions" }
  28. 1 assert_redirected_to room_involvement_url(rooms(:watercooler))
  29. end
  30. end
  31. end
  32. 1 test "updating involvement does not send turbo update for direct rooms" do
  33. 1 assert_no_turbo_stream_broadcasts [ users(:david), :rooms ] do
  34. 3 assert_changes -> { memberships(:david_david_and_jason).reload.involvement }, from: "everything", to: "nothing" do
  35. 1 put room_involvement_url(rooms(:david_and_jason)), params: { involvement: "nothing" }
  36. 1 assert_redirected_to room_involvement_url(rooms(:david_and_jason))
  37. end
  38. end
  39. end
  40. end

test/controllers/rooms/opens_controller_test.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Rooms::OpensControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 6 sign_in :david
  5. end
  6. 1 test "show redirects to get general show" do
  7. 1 get rooms_open_url(users(:david).rooms.opens.last)
  8. 1 assert_redirected_to room_url(users(:david).rooms.opens.last)
  9. end
  10. 1 test "new" do
  11. 1 get new_rooms_open_url
  12. 1 assert_response :success
  13. end
  14. 1 test "create" do
  15. 1 assert_turbo_stream_broadcasts :rooms, count: 1 do
  16. 1 post rooms_opens_url, params: { room: { name: "My New Room" } }
  17. end
  18. 1 assert_equal Room.last.memberships.count, User.count
  19. 1 assert_redirected_to room_url(Room.last)
  20. end
  21. 1 test "only admins or creators can update" do
  22. 1 sign_in :jz
  23. 1 assert_turbo_stream_broadcasts :rooms, count: 0 do
  24. 1 put rooms_open_url(rooms(:hq)), params: { room: { name: "New Name" } }
  25. end
  26. 1 assert_response :forbidden
  27. 1 assert rooms(:hq).reload.name, "HQ"
  28. end
  29. 1 test "update" do
  30. 1 assert_turbo_stream_broadcasts :rooms, count: 1 do
  31. 1 put rooms_open_url(rooms(:pets)), params: { room: { name: "New Name" } }
  32. end
  33. 1 assert_redirected_to room_url(rooms(:pets))
  34. 1 assert rooms(:pets).reload.name, "New Name"
  35. end
  36. 1 test "update a closed room to be open" do
  37. 1 put rooms_open_url(rooms(:designers)), params: { room: { name: "Doesn't matter" } }
  38. 1 assert_equal rooms(:designers).memberships.count, User.count
  39. end
  40. end

test/controllers/rooms/refreshes_controller_test.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Rooms::RefreshesControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 1 sign_in :david
  5. end
  6. 1 test "refresh includes new messages since the last known" do
  7. 1 travel_to 1.day.ago do
  8. 1 @old_message = rooms(:hq).messages.create!(creator: users(:jason), body: "Old message", client_message_id: "old")
  9. end
  10. 1 travel_to 1.minute.ago do
  11. 1 @new_message = rooms(:hq).messages.create!(creator: users(:jason), body: "New message", client_message_id: "new")
  12. 1 @old_message.touch
  13. end
  14. 1 get room_refresh_url(rooms(:hq), format: :turbo_stream), params: { since: 10.minutes.ago.to_fs(:epoch) }
  15. 1 assert_response :success
  16. 1 assert_select "turbo-stream[action='append']" do
  17. 1 assert_select "#" + dom_id(@new_message)
  18. 1 assert_select "template", count: 1
  19. end
  20. 1 assert_select "turbo-stream[action='replace']" do
  21. 1 assert_select "#" + dom_id(@old_message)
  22. 1 assert_select "template", count: 1
  23. end
  24. end
  25. end

test/controllers/rooms_controller_test.rb

100.0% lines covered

25 relevant lines. 25 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class RoomsControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 5 sign_in :david
  5. end
  6. 1 test "index redirects to the user's last room" do
  7. 1 get rooms_url
  8. 1 assert_redirected_to room_url(users(:david).rooms.last)
  9. end
  10. 1 test "show" do
  11. 1 get room_url(users(:david).rooms.last)
  12. 1 assert_response :success
  13. end
  14. 1 test "shows records the last room visited in a cookie" do
  15. 1 get room_url(users(:david).rooms.last)
  16. 1 assert response.cookies[:last_room] = users(:david).rooms.last.id
  17. end
  18. 1 test "destroy" do
  19. 1 assert_turbo_stream_broadcasts :rooms, count: 1 do
  20. 3 assert_difference -> { Room.count }, -1 do
  21. 1 delete room_url(rooms(:designers))
  22. end
  23. end
  24. end
  25. 1 test "destroy only allowed for creators or those who can administer" do
  26. 1 sign_in :jz
  27. 3 assert_no_difference -> { Room.count } do
  28. 1 delete room_url(rooms(:designers))
  29. 1 assert_response :forbidden
  30. end
  31. 1 rooms(:designers).update! creator: users(:jz)
  32. 3 assert_difference -> { Room.count }, -1 do
  33. 1 delete room_url(rooms(:designers))
  34. end
  35. end
  36. end

test/controllers/searches_controller_test.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class SearchesControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 5 sign_in :david
  5. 5 @message = rooms(:designers).messages.create! body: "Hello world!", client_message_id: "search", creator: users(:david)
  6. end
  7. 1 test "index initial view" do
  8. 1 get searches_url
  9. 1 assert_response :success
  10. 1 assert_select ".message", count: 0
  11. end
  12. 1 test "finding reachable messages" do
  13. 1 get searches_url, params: { q: "hello" }
  14. 1 assert_response :success
  15. 1 assert_select ".message", text: /Hello world!/
  16. end
  17. 1 test "unreachable messages are not found" do
  18. 1 memberships(:david_designers).destroy!
  19. 1 get searches_url, params: { q: "hello" }
  20. 1 assert_response :success
  21. 1 assert_select ".message", count: 0
  22. end
  23. 1 test "create saves the search term" do
  24. 3 assert_difference -> { users(:david).searches.count }, +1 do
  25. 1 post searches_url, params: { q: "hello" }
  26. end
  27. 1 assert_redirected_to searches_url(q: "hello")
  28. 1 assert users(:david).searches.exists?(query: "hello")
  29. end
  30. 1 test "clear search history" do
  31. 1 assert users(:david).searches.any?
  32. 1 delete clear_searches_url
  33. 1 assert users(:david).searches.none?
  34. end
  35. end

test/controllers/sessions/transfers_controller_test.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Sessions::TransfersControllerTest < ActionDispatch::IntegrationTest
  3. 1 test "show renders when not signed in" do
  4. 1 get session_transfer_url("some-token")
  5. 1 assert_response :success
  6. end
  7. 1 test "update establishes a session when the code is valid" do
  8. 1 user = users(:david)
  9. 1 put session_transfer_url(user.transfer_id)
  10. 1 assert_redirected_to root_url
  11. 1 assert parsed_cookies.signed[:session_token]
  12. end
  13. end

test/controllers/sessions_controller_test.rb

100.0% lines covered

36 relevant lines. 36 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class SessionsControllerTest < ActionDispatch::IntegrationTest
  3. 1 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"
  4. 1 DISALLOWED_BROWSER = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0"
  5. 1 test "new" do
  6. 1 get new_session_url
  7. 1 assert_response :success
  8. end
  9. 1 test "new redirects to first run when no users exist" do
  10. 1 User.destroy_all
  11. 1 get new_session_url
  12. 1 assert_redirected_to first_run_url
  13. end
  14. 1 test "new denied with incompatible browser" do
  15. 1 get new_session_url, env: { "HTTP_USER_AGENT" => DISALLOWED_BROWSER }
  16. 1 assert_select "h1", /Upgrade to a supported web browser/
  17. end
  18. 1 test "new allowed with compatible browser" do
  19. 1 get new_session_url, env: { "HTTP_USER_AGENT" => ALLOWED_BROWSER }
  20. 1 assert_select "h1", text: /Upgrade to a supported web browser/, count: 0
  21. end
  22. 1 test "create with valid credentials" do
  23. 1 post session_url, params: { email_address: "david@37signals.com", password: "secret123456" }
  24. 1 assert_redirected_to root_url
  25. 1 assert parsed_cookies.signed[:session_token]
  26. end
  27. 1 test "create with invalid credentials" do
  28. 1 post session_url, params: { email_address: "david@37signals.com", password: "wrong" }
  29. 1 assert_response :unauthorized
  30. 1 assert_nil parsed_cookies.signed[:session_token]
  31. end
  32. 1 test "destroy" do
  33. 1 sign_in :david
  34. 1 delete session_url
  35. 1 assert_redirected_to root_url
  36. 1 assert_not cookies[:session_token].present?
  37. end
  38. 1 test "destroy removes the push subscription for the device" do
  39. 1 sign_in :david
  40. 3 assert_difference -> { users(:david).push_subscriptions.count }, -1 do
  41. 1 delete session_url, params: { push_subscription_endpoint: push_subscriptions(:david_chrome).endpoint }
  42. end
  43. 1 assert_redirected_to root_url
  44. 1 assert_not cookies[:session_token].present?
  45. end
  46. end

test/controllers/unfurl_links_controller_test.rb

97.14% lines covered

35 relevant lines. 34 lines covered and 1 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class UnfurlLinksControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 5 sign_in :david
  5. end
  6. 1 test "create" do
  7. 1 stub_successful_request
  8. 1 post unfurl_link_url, params: { url: "https://www.example.com" }
  9. 1 assert_response :success
  10. 1 json_response = JSON.parse(response.body)
  11. 1 assert_equal "Hey!", json_response["title"]
  12. 1 assert_equal "https://example.com", json_response["url"]
  13. 1 assert_equal "https://example.com/image.png", json_response["image"]
  14. 1 assert_equal "desc..", json_response["description"]
  15. end
  16. 1 test "create with missing opengraph meta tags" do
  17. 1 WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: "<html><head></head></html>", headers: {})
  18. 1 post unfurl_link_url, params: { url: "https://www.example.com" }
  19. 1 assert_response :no_content
  20. end
  21. 1 test "create with a missing URL" do
  22. 1 assert_raise ActionController::ParameterMissing do
  23. 1 post unfurl_link_url, params: { url: "" }
  24. assert_response :bad_request
  25. end
  26. end
  27. 1 test "create for twitter.com" do
  28. 1 stub_successful_request url: "https://fxtwitter.com/dhh/status/834146806594433025"
  29. 1 post unfurl_link_url, params: { url: "https://twitter.com/dhh/status/834146806594433025" }
  30. 1 assert_response :success
  31. 1 assert_equal "Hey!", JSON.parse(response.body)["title"]
  32. end
  33. 1 test "create for x.com" do
  34. 1 stub_successful_request url: "https://fxtwitter.com/dhh/status/834146806594433025"
  35. 1 post unfurl_link_url, params: { url: "https://x.com/dhh/status/834146806594433025" }
  36. 1 assert_response :success
  37. 1 assert_equal "Hey!", JSON.parse(response.body)["title"]
  38. end
  39. 1 private
  40. 1 def stub_successful_request(url: "https://www.example.com/")
  41. 3 WebMock.stub_request(:get, url).to_return(
  42. status: 200,
  43. 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>",
  44. headers: { content_type: "text/html" }
  45. )
  46. 3 WebMock.stub_request(:head, "https://example.com/image.png").to_return(
  47. status: 200,
  48. headers: { content_type: "image/png" }
  49. )
  50. end
  51. end

test/controllers/users/avatars_controller_test.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Users::AvatarsControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 3 sign_in :david
  5. end
  6. 1 test "show initials" do
  7. 1 get user_avatar_url(users(:kevin).avatar_token)
  8. 1 assert_select "text", text: "K"
  9. end
  10. 1 test "show image" do
  11. 1 users(:kevin).update! avatar: fixture_file_upload("moon.jpg", "image/jpeg")
  12. 1 get user_avatar_url(users(:kevin).avatar_token)
  13. 1 assert_response :success
  14. 1 assert_equal "image/webp", @response.content_type
  15. end
  16. 1 test "show image with invalid token responds 404" do
  17. 1 get user_avatar_url("not-a-valid-token")
  18. 1 assert_response :not_found
  19. end
  20. end

test/controllers/users/profiles_controller_test.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Users::ProfilesControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 3 sign_in :david
  5. end
  6. 1 test "show" do
  7. 1 get user_profile_url
  8. 1 assert_response :success
  9. end
  10. 1 test "update" do
  11. 1 put user_profile_url, params: { user: { name: "John Doe", bio: "Acrobat" } }
  12. 1 assert_redirected_to user_profile_url
  13. 1 assert_equal "John Doe", users(:david).reload.name
  14. 1 assert_equal "Acrobat", users(:david).bio
  15. 1 assert_equal "david@37signals.com", users(:david).email_address
  16. end
  17. 1 test "updates are limited to the current user" do
  18. 1 put user_profile_url(users(:jason)), params: { user: { name: "John Doe" } }
  19. 1 assert_equal "Jason", users(:jason).reload.name
  20. end
  21. end

test/controllers/users/push_subscriptions_controller_test.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Users::PushSubscriptionsControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 3 sign_in :david
  5. end
  6. 1 test "create new push subscription" do
  7. 1 subscription_params = { "endpoint" => "https://apple", "p256dh_key" => "123", "auth_key" => "456" }
  8. 1 post user_push_subscriptions_url,
  9. params: { push_subscription: subscription_params }, headers: { "HTTP_USER_AGENT" => "Mozilla/5.0" }
  10. 1 assert_response :ok
  11. 1 assert_equal subscription_params, users(:david).push_subscriptions.last.attributes.slice("endpoint", "p256dh_key", "auth_key")
  12. 1 assert_equal "Mozilla/5.0", users(:david).push_subscriptions.last.user_agent
  13. end
  14. 1 test "touch existing subscription" do
  15. 3 assert_no_difference -> { users(:david).push_subscriptions.count } do
  16. 3 assert_changes -> { push_subscriptions(:david_chrome).reload.updated_at } do
  17. 1 post user_push_subscriptions_url(params: {
  18. push_subscription: push_subscriptions(:david_chrome).attributes.slice("endpoint", "p256dh_key", "auth_key")
  19. })
  20. end
  21. end
  22. 1 assert_response :ok
  23. end
  24. 1 test "destroy a push subscription via dev mode" do
  25. 3 assert_difference -> { Push::Subscription.count }, -1 do
  26. 1 delete user_push_subscription_url(push_subscriptions(:david_chrome))
  27. 1 assert_redirected_to user_push_subscriptions_url
  28. end
  29. end
  30. end

test/controllers/users/sidebars_controller_test.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Users::SidebarsControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 3 sign_in :david
  5. end
  6. 1 test "show" do
  7. 1 get user_sidebar_url
  8. 1 users(:david).rooms.opens.each do |room|
  9. 2 assert_match /#{room.name}/, @response.body
  10. end
  11. end
  12. 1 test "unread directs" do
  13. 1 rooms(:david_and_jason).messages.create! client_message_id: 999, body: "Hello", creator: users(:jason)
  14. 1 get user_sidebar_url
  15. 7 assert_select ".unread", count: users(:david).memberships.select { |m| m.room.direct? && m.unread? }.count
  16. end
  17. 1 test "unread other" do
  18. 1 rooms(:watercooler).messages.create! client_message_id: 999, body: "Hello", creator: users(:jason)
  19. 1 get user_sidebar_url
  20. 7 assert_select ".unread", count: users(:david).memberships.reject { |m| m.room.direct? || !m.unread? }.count
  21. end
  22. end

test/controllers/users_controller_test.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class UsersControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 6 @join_code = accounts(:signal).join_code
  5. end
  6. 1 test "show" do
  7. 1 sign_in :david
  8. 1 get user_url(users(:david))
  9. 1 assert_response :ok
  10. end
  11. 1 test "new" do
  12. 1 get join_url(@join_code)
  13. 1 assert_response :success
  14. end
  15. 1 test "new does not allow a signed in user" do
  16. 1 sign_in :david
  17. 1 get join_url(@join_code)
  18. 1 assert_redirected_to root_url
  19. end
  20. 1 test "new requires a join code" do
  21. 1 get join_url("not")
  22. 1 assert_response :not_found
  23. end
  24. 1 test "create" do
  25. 3 assert_difference -> { User.count }, 1 do
  26. 1 post join_url(@join_code), params: { user: { name: "New Person", email_address: "new@37signals.com", password: "secret123456" } }
  27. end
  28. 1 assert_redirected_to root_url
  29. 1 user = User.last
  30. 1 assert_equal user.id, Session.find_by(token: parsed_cookies.signed[:session_token]).user.id
  31. 1 assert_equal Rooms::Open.all, user.rooms
  32. end
  33. 1 test "creating a new user with an existing email address will redirect to login screen" do
  34. 3 assert_no_difference -> { User.count } do
  35. 1 post join_url(@join_code), params: { user: { name: "Another David", email_address: users(:david).email_address, password: "secret123456" } }
  36. end
  37. 1 assert_redirected_to new_session_url(email_address: users(:david).email_address)
  38. end
  39. end

test/controllers/welcome_controller_test.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class WelcomeControllerTest < ActionDispatch::IntegrationTest
  3. 1 setup do
  4. 2 sign_in :david
  5. end
  6. 1 test "redirects to the first created visible room the user has access to" do
  7. 1 get root_url
  8. 1 assert_redirected_to room_url(users(:david).rooms.original)
  9. end
  10. 1 test "redirects to the last room visited, if we have one" do
  11. 1 cookies[:last_room] = rooms(:watercooler).id
  12. 1 get root_url
  13. 1 assert_redirected_to room_url(rooms(:watercooler))
  14. end
  15. end

test/helpers/content_filters_test.rb

100.0% lines covered

55 relevant lines. 55 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class ContentFiltersTest < ActionView::TestCase
  3. 1 test "entire message contains an unfurled URL" do
  4. 1 text = "https://basecamp.com/"
  5. 1 message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_basecamp(text), client_message_id: "0015", creator: users(:jason)
  6. 1 filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
  7. 1 assert_not_equal message.body.body.to_html, filtered.to_html
  8. 1 assert_match /<div><action-text-attachment/, filtered.to_html
  9. end
  10. 1 test "message includes additional text besides an unfurled URL" do
  11. 1 text = "Hello https://basecamp.com/"
  12. 1 message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_basecamp(text), client_message_id: "0015", creator: users(:jason)
  13. 1 filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
  14. 1 assert_equal message.body.body.to_html, filtered.to_html
  15. 1 assert_match %r{<div>Hello https://basecamp\.com/<action-text-attachment}, filtered.to_html
  16. end
  17. 1 test "unfurled tweet without any image" do
  18. 1 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>"
  19. 1 message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_basecamp(text), client_message_id: "0015", creator: users(:jason)
  20. 1 filtered = ContentFilters::StyleUnfurledTwitterAvatars.apply(message.body.body)
  21. 1 assert_match %r{<div class="cf-twitter-avatar">}, filtered.to_html
  22. end
  23. 1 test "unfurled tweet containing an image" do
  24. 1 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>"
  25. 1 message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_basecamp(text), client_message_id: "0015", creator: users(:jason)
  26. 1 filtered = ContentFilters::StyleUnfurledTwitterAvatars.apply(message.body.body)
  27. 1 assert_no_match %r{<div class="cf-twitter-avatar">}, filtered.to_html
  28. end
  29. 1 test "entire message contains an unfurled URL from x.com but unfurls to twitter.com" do
  30. 1 text = "https://x.com/dhh/status/1752476663303323939"
  31. 1 message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_twitter(text), client_message_id: "0015", creator: users(:jason)
  32. 1 filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
  33. 1 assert_not_equal message.body.body.to_html, filtered.to_html
  34. 1 assert_match /<div><action-text-attachment/, filtered.to_html
  35. end
  36. 1 test "entire message contains an unfurled URL from x.com with query params" do
  37. 1 text = "https://x.com/dhh/status/1752476663303323939?s=20"
  38. 1 message = Message.create! room: rooms(:pets), body: unfurled_message_body_for_twitter(text), client_message_id: "0015", creator: users(:jason)
  39. 1 filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
  40. 1 assert_not_equal message.body.body.to_html, filtered.to_html
  41. 1 assert_match /<div><action-text-attachment/, filtered.to_html
  42. end
  43. 1 test "message contains a forbidden tag" do
  44. 1 exploit_image_tag = 'Hello <img src="https://ssecurityrise.com/tests/billionlaughs-cache.svg">World'
  45. 1 message = Message.create! room: rooms(:pets), body: exploit_image_tag, client_message_id: "0015", creator: users(:jason)
  46. 1 filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
  47. 1 assert_equal "Hello World", filtered.to_html
  48. end
  49. 1 test "message with a mention attachment" do
  50. 1 message = Message.create! room: rooms(:pets), body: "<div>Hey #{mention_attachment_for(:david)}</div>", creator: users(:jason)
  51. 1 filtered = ContentFilters::TextMessagePresentationFilters.apply(message.body.body)
  52. 1 expected = /<action-text-attachment sgid="#{users(:david).attachable_sgid}" content-type="application\/vnd\.campfire\.mention" content="(.*?)"><\/action-text-attachment>/m
  53. 1 assert_match expected, filtered.to_html
  54. end
  55. 1 private
  56. 1 def unfurled_message_body_for_basecamp(text)
  57. 4 "<div>#{text}#{unfurled_link_trix_attachment_for_basecamp}</div>"
  58. end
  59. 1 def unfurled_link_trix_attachment_for_basecamp
  60. 4 <<~BASECAMP
  61. <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>
  62. BASECAMP
  63. end
  64. 1 def unfurled_message_body_for_twitter(text)
  65. 2 "<div>#{text}#{unfurled_link_trix_attachment_for_twitter}</div>"
  66. end
  67. 1 def unfurled_link_trix_attachment_for_twitter
  68. 2 <<~TWEET
  69. <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>
  70. TWEET
  71. end
  72. end

test/models/account/joinable_test.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Account::JoinableTest < ActiveSupport::TestCase
  3. 1 test "new accounts get a joinable code" do
  4. 1 Account.destroy_all
  5. 1 account = Account.create!(name: "Chat")
  6. 1 assert_match /\w{4}-\w{4}-\w{4}/, account.join_code
  7. end
  8. 1 test "accounts can reset join code" do
  9. 3 assert_changes -> { accounts(:signal).reload.join_code } do
  10. 1 accounts(:signal).reset_join_code
  11. end
  12. end
  13. end

test/models/account_test.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class AccountTest < ActiveSupport::TestCase
  3. end

test/models/action_text_attachment_test.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class ActionTextAttachmentTest < ActiveSupport::TestCase
  3. 1 setup do
  4. 3 @user = users(:david)
  5. end
  6. 1 test "lookup user attachable with invalid sgid" do
  7. 1 message, signature = @user.attachable_sgid.split("--")
  8. 1 html = %Q(<action-text-attachment sgid="#{message}--invalid"></action-text-attachment>)
  9. 1 node = ActionText::Fragment.wrap(html).find_all(ActionText::Attachment.tag_name).first
  10. 1 attachment = ActionText::Attachment.from_node(node)
  11. 1 assert_equal @user, attachment.attachable
  12. end
  13. 1 test "lookup attachable with nil sgid" do
  14. 1 html = %Q(<action-text-attachment></action-text-attachment>)
  15. 1 node = ActionText::Fragment.wrap(html).find_all(ActionText::Attachment.tag_name).first
  16. 1 attachment = ActionText::Attachment.from_node(node)
  17. 1 assert_kind_of ActionText::Attachables::MissingAttachable, attachment.attachable
  18. end
  19. 1 test "lookup invalid sgid for an attachable requiring a valid sgid" do
  20. # Make room instance attachable for testing purposes
  21. 2 room = rooms(:pets).tap { |r| r.extend ActionText::Attachable }
  22. 1 message, signature = rooms(:pets).attachable_sgid.split("--")
  23. 1 html = %Q(<action-text-attachment sgid="#{message}--invalid"></action-text-attachment>)
  24. 1 node = ActionText::Fragment.wrap(html).find_all(ActionText::Attachment.tag_name).first
  25. 1 attachment = ActionText::Attachment.from_node(node)
  26. 1 assert_kind_of ActionText::Attachables::MissingAttachable, attachment.attachable
  27. end
  28. end

test/models/first_run_test.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class FirstRunTest < ActiveSupport::TestCase
  3. 1 setup do
  4. 3 Account.destroy_all
  5. 3 Room.destroy_all
  6. 3 User.destroy_all
  7. end
  8. 1 test "creating makes first user an administrator" do
  9. 1 user = create_first_run_user
  10. 1 assert user.administrator?
  11. end
  12. 1 test "first user has access to first room" do
  13. 1 user = create_first_run_user
  14. 1 assert user.rooms.one?
  15. end
  16. 1 test "first room is an open room" do
  17. 1 create_first_run_user
  18. 1 assert Room.first.open?
  19. end
  20. 1 private
  21. 1 def create_first_run_user
  22. 3 FirstRun.create!({ name: "User", email_address: "user@example.com", password: "secret123456" })
  23. end
  24. end

test/models/membership_test.rb

100.0% lines covered

57 relevant lines. 57 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class MembershipTest < ActiveSupport::TestCase
  3. 1 setup do
  4. 9 @membership = memberships(:david_watercooler)
  5. end
  6. 1 test "connected scope" do
  7. 1 @membership.connected
  8. 1 assert Membership.connected.exists?(@membership.id)
  9. 1 @membership.disconnected
  10. 1 assert_not Membership.connected.exists?(@membership.id)
  11. 1 travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
  12. 1 assert_not Membership.connected.exists?(@membership.id)
  13. end
  14. 1 test "disconnected scope" do
  15. 1 @membership.disconnected
  16. 1 assert Membership.disconnected.exists?(@membership.id)
  17. 1 @membership.connected
  18. 1 assert_not Membership.disconnected.exists?(@membership.id)
  19. 1 travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
  20. 1 assert Membership.disconnected.exists?(@membership.id)
  21. end
  22. 1 test "connected? is false when connection is stale" do
  23. 1 @membership.connected
  24. 1 travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
  25. 1 assert_not @membership.connected?
  26. end
  27. 1 test "connecting" do
  28. 1 @membership.connected
  29. 1 assert @membership.connected?
  30. 1 assert_equal 1, @membership.connections
  31. 1 @membership.connected
  32. 1 assert_equal 2, @membership.connections
  33. end
  34. 1 test "connecting resets stale connection count" do
  35. 3 2.times { @membership.connected }
  36. 1 assert_equal 2, @membership.connections
  37. 1 travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
  38. 1 @membership.connected
  39. 1 assert_equal 1, @membership.connections
  40. end
  41. 1 test "disconnecting" do
  42. 3 2.times { @membership.connected }
  43. 1 @membership.disconnected
  44. 1 assert @membership.connected?
  45. 1 assert_equal 1, @membership.connections
  46. 1 @membership.disconnected
  47. 1 assert_not @membership.connected?
  48. 1 assert_equal 0, @membership.connections
  49. end
  50. 1 test "disconnecting resets stale connection count" do
  51. 3 2.times { @membership.connected }
  52. 1 assert_equal 2, @membership.connections
  53. 1 travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
  54. 1 @membership.disconnected
  55. 1 assert_equal 0, @membership.connections
  56. end
  57. 1 test "refreshing the connection" do
  58. 1 @membership.connected
  59. 1 travel_to Membership::Connectable::CONNECTION_TTL.from_now + 1
  60. 1 assert_not @membership.connected?
  61. 1 @membership.refresh_connection
  62. 1 assert @membership.connected?
  63. end
  64. 1 test "removing a membership resets the user's connections" do
  65. 1 @membership.user.expects :reset_remote_connections
  66. 1 @membership.destroy
  67. end
  68. end

test/models/message/attachment_test.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Message::AttachmentTest < ActiveSupport::TestCase
  3. 1 include ActiveJob::TestHelper
  4. 1 include ActionDispatch::TestProcess
  5. 1 test "creating a message creates image thumbnail" do
  6. 1 message = create_attachment_message("moon.jpg", "image/jpeg")
  7. 1 assert message.attachment.representation(:thumb).image.present?
  8. end
  9. 1 test "creating a message creates video preview" do
  10. 1 message = create_attachment_message("alpha-centuri.mov", "video/quicktime")
  11. 1 assert message.reload.attachment.preview(format: :webp).image.attached?
  12. end
  13. 1 test "creating a blank message with attachment will use filename as plain text body" do
  14. 1 message = create_attachment_message("moon.jpg", "image/jpeg")
  15. 1 assert_equal message.plain_text_body, "moon.jpg"
  16. end
  17. 1 private
  18. 1 def create_attachment_message(file, content_type)
  19. 3 rooms(:hq).messages.create_with_attachment! \
  20. creator: users(:david),
  21. client_message_id: "message",
  22. attachment: fixture_file_upload(file, content_type)
  23. end
  24. end

test/models/message/searchable_test.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Message::SearchableTest < ActiveSupport::TestCase
  3. 1 test "message body is indexed and searchable" do
  4. 1 message = rooms(:designers).messages.create! body: "My hovercraft is full of eels", client_message_id: "earth", creator: users(:david)
  5. 1 assert_equal [ message ], rooms(:designers).messages.search("eel")
  6. 1 message.update! body: "My hovercraft is full of sharks"
  7. 1 assert_equal [ message ], rooms(:designers).messages.search("sharks")
  8. 1 message.destroy!
  9. 1 assert_equal [], rooms(:designers).messages.search("sharks")
  10. end
  11. 1 test "search results are returned in message order" do
  12. 1 messages = [ "first cat", "second cat", "third cat", "cat cat cat" ].map do |body|
  13. 4 rooms(:designers).messages.create! body: body, client_message_id: body, creator: users(:david)
  14. end
  15. 1 assert_equal messages, rooms(:designers).messages.search("cat")
  16. end
  17. 1 test "rich text body is converted to plain text for indexing" do
  18. 1 message = rooms(:designers).messages.create! body: "<span>My hovercraft is full of eels</span>", client_message_id: "earth", creator: users(:david)
  19. 1 assert_equal [], rooms(:designers).messages.search("span")
  20. 1 assert_equal [ message ], rooms(:designers).messages.search("eel")
  21. end
  22. end

test/models/message_test.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class MessageTest < ActiveSupport::TestCase
  3. 1 include ActionCable::TestHelper, ActiveJob::TestHelper
  4. 1 test "creating a message enqueues to push later" do
  5. 1 assert_enqueued_jobs 1, only: [ Room::PushMessageJob ] do
  6. 1 create_new_message_in rooms(:designers)
  7. end
  8. end
  9. 1 test "all emoji" do
  10. 1 assert Message.new(body: "😄🤘").plain_text_body.all_emoji?
  11. 1 assert_not Message.new(body: "Haha! 😄🤘").plain_text_body.all_emoji?
  12. 1 assert_not Message.new(body: "🔥\nmultiple lines\n💯").plain_text_body.all_emoji?
  13. 1 assert_not Message.new(body: "🔥 💯").plain_text_body.all_emoji?
  14. end
  15. 1 test "mentionees" do
  16. 1 message = Message.new room: rooms(:pets), body: "<div>Hey #{mention_attachment_for(:david)}</div>", creator: users(:jason), client_message_id: "earth"
  17. 1 assert_equal [ users(:david) ], message.mentionees
  18. 1 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"
  19. 1 assert_equal [ users(:david) ], message.mentionees
  20. 1 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"
  21. 1 assert_equal [], message_mentioning_a_non_member.mentionees
  22. end
  23. 1 private
  24. 1 def create_new_message_in(room)
  25. 1 room.messages.create!(creator: users(:jason), body: "Hello", client_message_id: "123")
  26. end
  27. end

test/models/opengraph/document_test.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Opengraph::DocumentTest < ActiveSupport::TestCase
  3. 1 test "extract opengraph tags using property attribute" do
  4. 1 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>")
  5. 1 attributes = document.opengraph_attributes
  6. 1 assert_equal "https://example.com", attributes[:url]
  7. 1 assert_equal "Hey!", attributes[:title]
  8. 1 assert_equal "desc..", attributes[:description]
  9. 1 assert_equal "https://example.com/image.png", attributes[:image]
  10. end
  11. 1 test "extract opengraph tags using name attribute" do
  12. 1 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>")
  13. 1 attributes = document.opengraph_attributes
  14. 1 assert_equal "https://example.com", attributes[:url]
  15. 1 assert_equal "Hey!", attributes[:title]
  16. 1 assert_equal "desc..", attributes[:description]
  17. 1 assert_equal "https://example.com/image.png", attributes[:image]
  18. end
  19. 1 test "document containing missing meta encoding tag and non-UTF8 characters" do
  20. 1 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>")
  21. 1 attributes = document.opengraph_attributes
  22. 1 assert_equal "https://example.com", attributes[:url]
  23. 1 assert_equal "Hey!", attributes[:title]
  24. 1 assert_equal "Hello World", attributes[:description]
  25. 1 assert_equal "https://example.com/image.png", attributes[:image]
  26. end
  27. end

test/models/opengraph/fetch_test.rb

100.0% lines covered

57 relevant lines. 57 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 require "restricted_http/private_network_guard"
  3. 1 class Opengraph::FetchTest < ActiveSupport::TestCase
  4. 1 setup do
  5. 11 @fetch = Opengraph::Fetch.new
  6. 11 @url = URI.parse("https://www.example.com")
  7. end
  8. 1 test "#fetch_document fetches valid HTML" do
  9. 1 WebMock.stub_request(:get, "https://www.example.com/")
  10. .to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
  11. 1 assert_equal "<body>ok<body>", @fetch.fetch_document(@url)
  12. end
  13. 1 test "#fetch_document discards other content types" do
  14. 1 WebMock.stub_request(:get, "https://www.example.com/")
  15. .to_return(status: 200, body: "I'm not HTML!", headers: { content_type: "text/plain" })
  16. 1 assert_nil @fetch.fetch_document(@url)
  17. end
  18. 1 test "#fetch_document follows redirects" do
  19. 1 WebMock.stub_request(:get, "https://www.example.com/")
  20. .to_return(status: 302, headers: { location: "https://www.other.com/" })
  21. 1 WebMock.stub_request(:get, "https://www.other.com/")
  22. .to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
  23. 1 assert_equal "<body>ok<body>", @fetch.fetch_document(@url)
  24. end
  25. 1 test "#fetch_document does not follow redirects to private networks" do
  26. 1 WebMock.stub_request(:get, "https://www.example.com/")
  27. .to_return(status: 302, headers: { location: "https://www.other.com/" })
  28. 1 WebMock.stub_request(:get, "https://www.other.com/")
  29. .to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
  30. 1 Resolv.stubs(:getaddress).with("www.other.com").returns("127.0.0.1")
  31. 1 assert_raises RestrictedHTTP::Violation do
  32. 1 @fetch.fetch_document(@url, ip: "1.2.3.4")
  33. end
  34. end
  35. 1 test "#fetch_document resolves hostnames once to avoid DNS rebinding" do
  36. # Allow but interrupt a real connection to demonstrate that we connect
  37. # to a resolved IP, not a hostname to re-resolve.
  38. 1 WebMock.disable_net_connect! allow: [ @url.host ]
  39. 1 Resolv.stubs(:getaddress).with(@url.host).returns("1.2.3.4", "127.0.0.1")
  40. 1 TCPSocket.expects(:open).with(@url.host, 443, nil, nil).never
  41. 1 TCPSocket.expects(:open).with("1.2.3.4", 443, nil, nil).throws(:dns_not_rebound)
  42. 1 assert_throws :dns_not_rebound do
  43. 1 @fetch.fetch_document(@url)
  44. end
  45. end
  46. 1 test "#fetch_document resolves redirect location hostnames once to avoid DNS rebinding" do
  47. # Stub the initial URL to redirect to a DNS-rebound location
  48. 1 WebMock.stub_request(:get, "https://www.other.com/")
  49. .to_return(status: 302, headers: { location: @url.to_s })
  50. # Allow but interrupt a real connection to demonstrate that we connect
  51. # to a resolved IP, not a hostname to re-resolve.
  52. 1 WebMock.disable_net_connect! allow: [ @url.host ]
  53. 1 Resolv.stubs(:getaddress).with(@url.host).returns("1.2.3.4", "127.0.0.1")
  54. 1 TCPSocket.expects(:open).with(@url.host, 443, nil, nil).never
  55. 1 TCPSocket.expects(:open).with("1.2.3.4", 443, nil, nil).throws(:dns_not_rebound)
  56. 1 assert_throws :dns_not_rebound do
  57. 1 @fetch.fetch_document(URI.parse("https://www.other.com/"), ip: "1.2.3.4")
  58. end
  59. end
  60. 1 test "#fetch_document is empty following redirects that never finish" do
  61. 1 WebMock.stub_request(:get, "https://www.example.com/")
  62. .to_return(status: 302, headers: { location: "https://www.example.com/" })
  63. 1 assert_raises Opengraph::Fetch::TooManyRedirectsError do
  64. 1 @fetch.fetch_document(@url)
  65. end
  66. end
  67. 1 test "#fetch_document ignores large responses" do
  68. 1 WebMock.stub_request(:get, "https://www.example.com/")
  69. .to_return(status: 200, body: "too large", headers: { content_length: 1.gigabyte, content_type: "text/html" })
  70. 1 assert_nil @fetch.fetch_document(@url)
  71. end
  72. 1 test "#fetch_document ignores large responses that were missing their content length" do
  73. 1 WebMock.stub_request(:get, "https://www.example.com/")
  74. .to_return(status: 200, body: large_body_content, headers: { content_type: "text/html" })
  75. 1 assert_nil @fetch.fetch_document(@url)
  76. end
  77. 1 test "#fetch_document ignores large responses that were lying about their content length" do
  78. 1 WebMock.stub_request(:get, "https://www.example.com/")
  79. .to_return(status: 200, body: large_body_content, headers: { content_length: 1.megabyte, content_type: "text/html" })
  80. 1 assert_nil @fetch.fetch_document(@url)
  81. end
  82. 1 test "fetch content type" do
  83. 1 WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
  84. 1 url = URI.parse("https://example.com/image.png")
  85. 1 assert_equal "image/png", @fetch.fetch_content_type(url)
  86. end
  87. 1 private
  88. 1 def large_body_content
  89. 2 "x" * (Opengraph::Fetch::MAX_BODY_SIZE + 1)
  90. end
  91. end

test/models/opengraph/location_test.rb

100.0% lines covered

32 relevant lines. 32 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 require "restricted_http/private_network_guard"
  3. 1 class Opengraph::LocationTest < ActiveSupport::TestCase
  4. 1 test "url validations" do
  5. 1 assert Opengraph::Location.new("https://www.example.com").valid?
  6. 1 assert Opengraph::Location.new("http://www.example.com").valid?
  7. 1 assert_not Opengraph::Location.new("~/etc/password").valid?
  8. 1 assert_not Opengraph::Location.new("ftp://speedtest.tele2.net").valid?
  9. 1 assert_not Opengraph::Location.new("httpfake").valid?
  10. 1 assert_not Opengraph::Location.new(" foo").valid?
  11. 1 assert_not Opengraph::Location.new("https/incorrect").valid?
  12. end
  13. 1 test "private network urls" do
  14. 1 Resolv.stubs(:getaddress).with("www.example.com").returns("172.16.0.0")
  15. 1 location = Opengraph::Location.new("https://www.example.com")
  16. 1 assert_not location.valid?
  17. 1 assert_equal [ "is not public" ], location.errors[:url]
  18. end
  19. 1 test "avoid reading file urls when expecting HTML" do
  20. 1 large_file = Opengraph::Location.new("https://www.example.com/100gb.zip")
  21. 1 assert_nil Opengraph::Location.new("http://www.example.com/video.mp4").read_html
  22. 1 assert_nil Opengraph::Location.new("http://www.example.com/archive.tar").read_html
  23. 1 assert_nil Opengraph::Location.new("https://www.example.com/large.heic").read_html
  24. 1 assert_nil Opengraph::Location.new("https://www.example.com/image.jpeg").read_html
  25. 1 assert_nil Opengraph::Location.new("https://www.example.com/malware.exe").read_html
  26. 1 assert_nil Opengraph::Location.new("https://www.example.com/massiveOS.iso").read_html
  27. end
  28. 1 test "read valid HTML" do
  29. 1 WebMock.stub_request(:get, "https://www.example.com/")
  30. .to_return(status: 200, body: "<body>ok<body>", headers: { content_type: "text/html" })
  31. 1 location = Opengraph::Location.new("https://www.example.com")
  32. 1 assert_equal "<body>ok<body>", location.read_html
  33. end
  34. 1 test "read ignores invalid responses" do
  35. 1 WebMock.stub_request(:get, "https://www.example.com/")
  36. .to_return(status: 200, body: "too large", headers: { content_length: 1.gigabyte, content_type: "text/html" })
  37. 1 location = Opengraph::Location.new("https://www.example.com")
  38. 1 assert_nil location.read_html
  39. end
  40. end

test/models/opengraph/metadata_test.rb

100.0% lines covered

67 relevant lines. 67 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Opengraph::MetadataTest < ActiveSupport::TestCase
  3. 1 test "successful fetch" do
  4. 1 body = <<~HTML
  5. <html>
  6. <head>
  7. <meta property="og:url" content="https://example.com">
  8. <meta property="og:title" content="Hey!">
  9. <meta property="og:description" content="Hello">
  10. <meta property="og:image" content="https://example.com/image.png">
  11. </head>
  12. </html>
  13. HTML
  14. 1 WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
  15. 1 WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
  16. 1 metadata = Opengraph::Metadata.from_url("https://www.example.com")
  17. 1 assert metadata.valid?
  18. 1 assert_equal "https://example.com", metadata.url
  19. 1 assert_equal "Hey!", metadata.title
  20. 1 assert_equal "Hello", metadata.description
  21. 1 assert_equal "https://example.com/image.png", metadata.image
  22. end
  23. 1 test "missing opengraph meta tags" do
  24. 1 WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: "<html><head></head></html>", headers: { content_type: "text/html" })
  25. 1 opengraph = Opengraph::Metadata.from_url("https://www.example.com")
  26. 1 assert_not opengraph.valid?
  27. 1 assert_equal [ "Title can't be blank", "Description can't be blank" ], opengraph.errors.full_messages
  28. end
  29. 1 test "URL uses the provided value if the returned value is missing" do
  30. 1 body = <<~HTML
  31. <html>
  32. <head>
  33. <meta property="og:title" content="Hey!">
  34. <meta property="og:description" content="Hello">
  35. <meta property="og:image" content="https://example.com/image.png">
  36. </head>
  37. </html>
  38. HTML
  39. 1 WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
  40. 1 WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
  41. 1 metadata = Opengraph::Metadata.from_url("https://www.example.com")
  42. 1 assert metadata.valid?
  43. 1 assert_equal "https://www.example.com", metadata.url
  44. end
  45. 1 test "URL uses the provided value if the returned value is invalid" do
  46. 1 body = <<~HTML
  47. <html>
  48. <head>
  49. <meta property="og:url" content="/foo">
  50. <meta property="og:title" content="Hey!">
  51. <meta property="og:description" content="Hello">
  52. <meta property="og:image" content="https://example.com/image.png">
  53. </head>
  54. </html>
  55. HTML
  56. 1 WebMock.stub_request(:get, "https://www.example.com/foo").to_return(status: 200, body: body, headers: { content_type: "text/html" })
  57. 1 WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
  58. 1 metadata = Opengraph::Metadata.from_url("https://www.example.com/foo")
  59. 1 assert metadata.valid?
  60. 1 assert_equal "https://www.example.com/foo", metadata.url
  61. end
  62. 1 test "missing response body" do
  63. 1 WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 403, body: "", headers: { content_type: "text/html" })
  64. 1 assert_not Opengraph::Metadata.from_url("https://www.example.com").valid?
  65. end
  66. 1 test "non html response" do
  67. 1 WebMock.stub_request(:get, "https://www.example.com/image").to_return(status: 200, body: "[blob]", headers: { content_type: "image/jpeg" })
  68. 1 assert_not Opengraph::Metadata.from_url("https://www.example.com/image").valid?
  69. end
  70. 1 test "relative and invalid image URLs are ignored" do
  71. 1 body = <<~HTML
  72. <html>
  73. <head>
  74. <meta property="og:url" content="https://example.com">
  75. <meta property="og:title" content="Hey!">
  76. <meta property="og:description" content="Hello">
  77. <meta property="og:image" content="%s">
  78. </head>
  79. </html>
  80. HTML
  81. 1 [ "/image.png", "foo", "https/incorrect", "~/etc/password" ].each do |invalid_image_url|
  82. 4 WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body % invalid_image_url, headers: { content_type: "text/html" })
  83. 4 opengraph = Opengraph::Metadata.from_url("https://www.example.com")
  84. 4 assert opengraph.valid?
  85. 4 assert_nil opengraph.image
  86. end
  87. end
  88. 1 test "sanitize title and description" do
  89. 1 body = <<~HTML
  90. <html>
  91. <head>
  92. <meta property="og:title" content="Hey!<script>alert('hi')</script>">
  93. <meta property="og:description" content="Hello<script>alert('hi')</script>">
  94. <meta property="og:image" content="https://example.com/image.png">
  95. </head>
  96. </html>
  97. HTML
  98. 1 WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
  99. 1 WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
  100. 1 metadata = Opengraph::Metadata.from_url("https://www.example.com")
  101. 1 assert metadata.valid?
  102. 1 assert_equal "Hey!", metadata.title
  103. 1 assert_equal "Hello", metadata.description
  104. end
  105. 1 test "remove encoded tags from title and description" do
  106. 1 body = <<~HTML
  107. <html>
  108. <head>
  109. <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;">
  110. <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>">
  111. <meta property="og:image" content="https://example.com/image.png">
  112. </head>
  113. </html>
  114. HTML
  115. 1 WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
  116. 1 WebMock.stub_request(:head, "https://example.com/image.png").to_return(status: 200, headers: { content_type: "image/png" })
  117. 1 metadata = Opengraph::Metadata.from_url("https://www.example.com")
  118. 1 assert metadata.valid?
  119. 1 assert_equal "Hey!", metadata.title
  120. 1 assert_equal "Hello", metadata.description
  121. end
  122. 1 test "does not allow SVG content type for preview image" do
  123. 1 body = <<~HTML
  124. <html>
  125. <head>
  126. <meta property="og:url" content="https://example.com">
  127. <meta property="og:title" content="Hey!">
  128. <meta property="og:description" content="Hello">
  129. <meta property="og:image" content="https://example.com/image.svg">
  130. </head>
  131. </html>
  132. HTML
  133. 1 WebMock.stub_request(:get, "https://www.example.com/").to_return(status: 200, body: body, headers: { content_type: "text/html" })
  134. 1 WebMock.stub_request(:head, "https://example.com/image.svg").to_return(status: 200, headers: { content_type: "image/svg+xml" })
  135. 1 metadata = Opengraph::Metadata.from_url("https://www.example.com")
  136. 1 assert metadata.valid?
  137. 1 assert_nil metadata.image
  138. end
  139. end

test/models/room/push_test.rb

100.0% lines covered

49 relevant lines. 49 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Room::PushTest < ActiveSupport::TestCase
  3. 1 include ActiveJob::TestHelper
  4. 1 test "deliver new message to other room users with push subscriptions" do
  5. 1 task_count = Push::Subscription.count - users(:david).push_subscriptions.count
  6. 1 perform_enqueued_jobs only: Room::PushMessageJob do
  7. 1 WebPush.expects(:payload_send).times(task_count)
  8. 1 rooms(:hq).messages.create! body: "This is from earth", client_message_id: "earth", creator: users(:david)
  9. end
  10. 1 wait_for_web_push_delivery_pool_tasks(task_count)
  11. end
  12. 1 test "notifies subscribed users" do
  13. 1 perform_enqueued_jobs only: Room::PushMessageJob do
  14. 1 WebPush.expects(:payload_send).times(2)
  15. 1 rooms(:designers).messages.create! body: "This is from earth", client_message_id: "earth", creator: users(:david)
  16. end
  17. 1 wait_for_web_push_delivery_pool_tasks(2)
  18. 1 perform_enqueued_jobs only: Room::PushMessageJob do
  19. 1 WebPush.expects(:payload_send).times(3)
  20. 1 rooms(:designers).messages.create! body: "Hey #{mention_attachment_for(:kevin)}", client_message_id: "earth", creator: users(:david)
  21. end
  22. 1 wait_for_web_push_delivery_pool_tasks(5)
  23. end
  24. 1 test "does not notify for connected rooms" do
  25. 1 memberships(:kevin_designers).connected
  26. 1 perform_enqueued_jobs only: Room::PushMessageJob do
  27. 1 WebPush.expects(:payload_send).times(2)
  28. 1 rooms(:designers).messages.create! body: "Hey @kevin", client_message_id: "earth", creator: users(:david)
  29. end
  30. 1 wait_for_web_push_delivery_pool_tasks(2)
  31. end
  32. 1 test "does not notify for invisible rooms" do
  33. 1 memberships(:kevin_designers).update! involvement: "invisible"
  34. 1 perform_enqueued_jobs only: Room::PushMessageJob do
  35. 1 WebPush.expects(:payload_send).times(2)
  36. 1 rooms(:designers).messages.create! body: "Hey @kevin", client_message_id: "earth", creator: users(:david)
  37. end
  38. 1 wait_for_web_push_delivery_pool_tasks(2)
  39. end
  40. 1 test "destroys invalid subscriptions" do
  41. 1 memberships(:kevin_designers).update! involvement: "invisible"
  42. 3 assert_difference -> { Push::Subscription.count }, -2 do
  43. 1 perform_enqueued_jobs only: Room::PushMessageJob do
  44. 1 WebPush.expects(:payload_send).times(2).raises(WebPush::ExpiredSubscription.new(Struct.new(:body).new, "example.com"))
  45. 1 rooms(:designers).messages.create! body: "Hey @kevin", client_message_id: "earth", creator: users(:david)
  46. end
  47. 1 wait_for_web_push_delivery_pool_tasks(2)
  48. 1 wait_for_invalidation_pool_tasks(2)
  49. end
  50. end
  51. 1 private
  52. 1 def wait_for_web_push_delivery_pool_tasks(count)
  53. 6 wait_for_pool_tasks(Rails.configuration.x.web_push_pool.delivery_pool, count)
  54. end
  55. 1 def wait_for_invalidation_pool_tasks(count)
  56. 1 wait_for_pool_tasks(Rails.configuration.x.web_push_pool.invalidation_pool, count)
  57. end
  58. 1 def wait_for_pool_tasks(pool, count)
  59. 7 start = Time.now
  60. 7 timeout = 0.2
  61. 7 while pool.completed_task_count < count
  62. 1 raise "Timeout waiting for pool tasks to complete" if Time.now - start > timeout
  63. 1 sleep timeout / 10.0
  64. end
  65. end
  66. end

test/models/room_test.rb

100.0% lines covered

24 relevant lines. 24 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class RoomTest < ActiveSupport::TestCase
  3. 1 test "grant membership to user" do
  4. 1 rooms(:watercooler).memberships.grant_to(users(:kevin))
  5. 1 assert rooms(:watercooler).users.include?(users(:kevin))
  6. end
  7. 1 test "revoke membership from user" do
  8. 1 rooms(:watercooler).memberships.revoke_from(users(:david))
  9. 1 assert_not rooms(:watercooler).users.include?(users(:david))
  10. end
  11. 1 test "revise memberships" do
  12. 1 rooms(:watercooler).memberships.revise(granted: users(:kevin), revoked: users(:david))
  13. 1 assert rooms(:watercooler).users.include?(users(:kevin))
  14. 1 assert_not rooms(:watercooler).users.include?(users(:david))
  15. end
  16. 1 test "create for users by giving them immediate membership" do
  17. 1 room = Rooms::Closed.create_for({ name: "Hello!", creator: users(:david) }, users: [ users(:kevin), users(:david) ])
  18. 1 assert room.users.include?(users(:kevin))
  19. 1 assert room.users.include?(users(:david))
  20. end
  21. 1 test "type" do
  22. 1 assert Rooms::Open.new.open?
  23. 1 assert_not Rooms::Open.new.direct?
  24. 1 assert Rooms::Direct.new.direct?
  25. 1 assert Rooms::Closed.new.closed?
  26. end
  27. 1 test "default involvement for new users" do
  28. 1 room = Rooms::Closed.create_for({ name: "Hello!", creator: users(:david) }, users: [ users(:kevin), users(:david) ])
  29. 3 assert room.memberships.all? { |m| m.involved_in_mentions? }
  30. end
  31. end

test/models/rooms/direct_test.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Rooms::DirectTest < ActiveSupport::TestCase
  3. 1 test "create room for same users" do
  4. 1 room = Rooms::Direct.find_or_create_for([ users(:david), users(:kevin) ])
  5. 1 assert room.users.include?(users(:david))
  6. 1 assert room.users.include?(users(:kevin))
  7. 1 assert_not room.users.include?(users(:jason))
  8. end
  9. 1 test "only one room will exist for the same users" do
  10. 1 room1 = Rooms::Direct.find_or_create_for([ users(:david), users(:kevin) ])
  11. 1 room2 = Rooms::Direct.find_or_create_for([ users(:kevin), users(:david) ])
  12. 1 assert_equal room1, room2
  13. end
  14. 1 test "default involvement for new users" do
  15. 1 room = Rooms::Direct.find_or_create_for([ users(:david), users(:kevin) ])
  16. 3 assert room.memberships.all? { |m| m.involved_in_everything? }
  17. end
  18. end

test/models/rooms/open_test.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class Rooms::OpenTest < ActiveSupport::TestCase
  3. 1 test "grants access to all users after creation" do
  4. 1 room = Rooms::Open.create!(name: "My open room with everyone!", creator: users(:david))
  5. 1 assert_equal User.count, room.users.count
  6. end
  7. 1 test "grants access to all users after becoming open" do
  8. 1 room = rooms(:watercooler).becomes!(Rooms::Open)
  9. 1 room.save!
  10. 1 assert_equal User.count, room.users.count
  11. end
  12. end

test/models/user/bot_test.rb

100.0% lines covered

25 relevant lines. 25 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class User::BotTest < ActiveSupport::TestCase
  3. 1 test "create bot" do
  4. 1 token = "5M0aLYwQyBXOXa5Wsz6NZb11EE4tW2"
  5. 1 SecureRandom.stubs(:alphanumeric).returns(token)
  6. 1 uuid = "3574925f-479d-44f8-82b7-fc039af5367c"
  7. 1 Random.stubs(:uuid).returns(uuid)
  8. 1 bot = User.create_bot!(name: "Bender")
  9. 1 assert_equal "#{bot.id}-#{token}", bot.bot_key
  10. end
  11. 1 test "reset bot key" do
  12. 1 first_token = "5M0aLYwQyBXOXa5Wsz6NZb11EE4tW2"
  13. 1 SecureRandom.stubs(:alphanumeric).returns(first_token)
  14. 1 bot = User.create_bot!(name: "Bender")
  15. 1 assert_equal "#{bot.id}-#{first_token}", bot.bot_key
  16. 1 second_token = "R4kme9anwWRuz3sSoBXiB8Li8ioZPP"
  17. 1 SecureRandom.stubs(:alphanumeric).returns(second_token)
  18. 1 bot.reset_bot_key
  19. 1 assert_equal "#{bot.id}-#{second_token}", bot.bot_key
  20. end
  21. 1 test "authenticate" do
  22. 1 bot = User.create_bot!(name: "Bender")
  23. 1 assert User.authenticate_bot(bot.bot_key)
  24. end
  25. 1 test "deliver message by webhook" do
  26. 1 WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200)
  27. 1 perform_enqueued_jobs only: Bot::WebhookJob do
  28. 1 users(:bender).deliver_webhook_later(messages(:first))
  29. end
  30. end
  31. end

test/models/user/role_test.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class User::RoleTest < ActiveSupport::TestCase
  3. 1 test "creating subsequent users makes them members" do
  4. 1 assert User.create!(name: "User", email_address: "user@example.com", password: "secret123456").member?
  5. end
  6. 1 test "can_administer?" do
  7. 1 assert User.new(role: :administrator).can_administer?
  8. 1 assert_not User.new(role: :member).can_administer?
  9. 1 assert_not User.new.can_administer?
  10. end
  11. 1 test "can administer a record" do
  12. 1 member = User.new(role: :member)
  13. 1 assert member.can_administer?(Room.new(creator: member))
  14. 1 another_member = User.new(role: :member)
  15. 1 assert another_member.can_administer?(Room.new(creator: member))
  16. 1 assert_not another_member.can_administer?(rooms(:designers))
  17. end
  18. end

test/models/user_test.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class UserTest < ActiveSupport::TestCase
  3. 1 test "user does not prevent very long passwords" do
  4. 1 users(:david).update(password: "secret" * 50)
  5. 1 assert users(:david).valid?
  6. end
  7. 1 test "creating users grants membership to the open rooms" do
  8. 3 assert_difference -> { Membership.count }, +Rooms::Open.count do
  9. 1 create_new_user
  10. end
  11. end
  12. 1 test "deactivating a user deletes push subscriptions, searches, memberships for non-direct rooms, and changes their email address" do
  13. 3 assert_difference -> { Membership.count }, -users(:david).memberships.without_direct_rooms.count do
  14. 3 assert_difference -> { Push::Subscription.count }, -users(:david).push_subscriptions.count do
  15. 3 assert_difference -> { Search.count }, -users(:david).searches.count do
  16. 1 SecureRandom.stubs(:uuid).returns("2e7de450-cf04-4fa8-9b02-ff5ab2d733e7")
  17. 1 users(:david).deactivate
  18. 1 assert_equal "david-deactivated-2e7de450-cf04-4fa8-9b02-ff5ab2d733e7@37signals.com", users(:david).reload.email_address
  19. end
  20. end
  21. end
  22. end
  23. 1 test "deactivating a user deletes their sessions" do
  24. 3 assert_changes -> { users(:david).sessions.count }, from: 1, to: 0 do
  25. 1 users(:david).deactivate
  26. end
  27. end
  28. 1 private
  29. 1 def create_new_user
  30. 1 User.create!(name: "User", email_address: "user@example.com", password: "secret123456")
  31. end
  32. end

test/models/webhook_test.rb

100.0% lines covered

32 relevant lines. 32 lines covered and 0 lines missed.
    
  1. 1 require "test_helper"
  2. 1 class WebhookTest < ActiveSupport::TestCase
  3. 1 test "payload" do
  4. 1 message = messages(:first)
  5. 1 message_path = Rails.application.routes.url_helpers.room_at_message_path(message.room, message)
  6. 1 bot_messages_path = Rails.application.routes.url_helpers.room_bot_messages_path(message.room, users(:bender).bot_key)
  7. 1 WebMock.stub_request(:post, webhooks(:bender).url).
  8. with(body: hash_including(
  9. user: { id: message.creator.id, name: message.creator.name },
  10. room: { id: message.room.id, name: message.room.name, path: bot_messages_path },
  11. message: { id: message.id, body: { html: "First post!", plain: "First post!" }, path: message_path },
  12. ))
  13. 1 response = webhooks(:bender).deliver(messages(:first))
  14. 1 assert_equal 200, response.code.to_i
  15. end
  16. 1 test "delivery" do
  17. 1 WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200, body: "", headers: {})
  18. 1 response = webhooks(:bender).deliver(messages(:first))
  19. 1 assert_equal 200, response.code.to_i
  20. end
  21. 1 test "delivery with OK text reply" do
  22. 1 WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200, body: "Hello back!", headers: { "Content-Type" => "text/plain" })
  23. 1 response = webhooks(:bender).deliver(messages(:first))
  24. 1 reply_message = Message.last
  25. 1 assert_equal "Hello back!", reply_message.body.to_plain_text
  26. end
  27. 1 test "delivery with OK attachment reply" do
  28. 1 WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 200, body: file_fixture("moon.jpg"), headers: { "Content-Type" => "image/jpeg" })
  29. 1 response = webhooks(:bender).deliver(messages(:first))
  30. 1 reply_message = Message.last
  31. 1 assert reply_message.attachment.present?
  32. end
  33. 1 test "delivery with error reply" do
  34. 3 assert_no_difference -> { Message.count } do
  35. 1 WebMock.stub_request(:post, webhooks(:bender).url).to_return(status: 500, body: "Internal Error!", headers: {})
  36. 1 response = webhooks(:bender).deliver(messages(:first))
  37. end
  38. end
  39. 1 test "delivery that times out" do
  40. 1 Webhook.any_instance.stubs(:post).raises(Net::OpenTimeout)
  41. 1 response = webhooks(:bender).deliver(messages(:first))
  42. 1 reply_message = Message.last
  43. 1 assert_equal "Failed to respond within 7 seconds", reply_message.body.to_plain_text
  44. end
  45. end

test/test_helpers/mention_test_helper.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 module MentionTestHelper
  2. 1 def mention_attachment_for(name)
  3. 8 user = users(name)
  4. 8 attachment_body = ApplicationController.render partial: "users/mention", locals: { user: user }
  5. 8 "<action-text-attachment sgid=\"#{user.attachable_sgid}\" content-type=\"application/vnd.campfire.mention\" content=\"#{attachment_body.gsub('"', '&quot;')}\"></action-text-attachment>"
  6. end
  7. end

test/test_helpers/session_test_helper.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. 1 module SessionTestHelper
  2. 1 def parsed_cookies
  3. 5 ActionDispatch::Cookies::CookieJar.build(request, cookies.to_hash)
  4. end
  5. 1 def sign_in(user)
  6. 107 user = users(user) unless user.is_a? User
  7. 107 post session_url, params: { email_address: user.email_address, password: "secret123456" }
  8. 107 assert cookies[:session_token].present?
  9. end
  10. end

test/test_helpers/turbo_test_helper.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. 1 module TurboTestHelper
  2. 1 def assert_rendered_turbo_stream_broadcast(*streambles, action:, target:, &block)
  3. 1 streams = find_broadcasts_for(*streambles)
  4. 1 target = ActionView::RecordIdentifier.dom_id(*target)
  5. 1 assert_select Nokogiri::HTML.fragment(streams), %(turbo-stream[action="#{action}"][target="#{target}"]), &block
  6. end
  7. 1 private
  8. 1 def find_broadcasts_for(*streambles)
  9. 1 broadcasting = streambles.collect do |streamble|
  10. 2 streamble.try(:to_gid_param) || streamble
  11. end.join(":")
  12. 1 broadcasts = ActionCable.server.pubsub.broadcasts(broadcasting)
  13. 2 broadcasts.collect { |b| JSON.parse(b) }.join("\n\n")
  14. end
  15. end