mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-09 00:00:12 +09:00
462eff10df
Deleting the memberships with delete_all skips Membership's after_destroy_commit, which resets a member's connections when one membership is revoked. Until the job ran, a member who had the room open kept its streams, and a message that still landed in the room (a request already past the membership check, a bot's reply) reached them. The request now reads the members' ids in the transaction that deletes their memberships, and the job resets their connections before it destroys the messages. It costs a Redis round trip per member, about 1.5 s for 10,000, so it's done in the job rather than in the request. User#grant_membership_to_open_rooms read the open rooms and inserted in a separate statement, so a user created while a room was being closed could read it as open and be granted it after the close. It's now a single insert ... select, which SQLite runs under the write lock, skipping duplicates as insert_all did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bj8KnxpTf9sj2Ysa8aLAVa
99 lines
3.7 KiB
Ruby
99 lines
3.7 KiB
Ruby
require "test_helper"
|
|
|
|
class RoomTest < ActiveSupport::TestCase
|
|
include ActionCable::TestHelper
|
|
|
|
test "grant membership to user" do
|
|
rooms(:watercooler).memberships.grant_to(users(:kevin))
|
|
assert rooms(:watercooler).users.include?(users(:kevin))
|
|
end
|
|
|
|
test "revoke membership from user" do
|
|
rooms(:watercooler).memberships.revoke_from(users(:david))
|
|
assert_not rooms(:watercooler).users.include?(users(:david))
|
|
end
|
|
|
|
test "revise memberships" do
|
|
rooms(:watercooler).memberships.revise(granted: users(:kevin), revoked: users(:david))
|
|
assert rooms(:watercooler).users.include?(users(:kevin))
|
|
assert_not rooms(:watercooler).users.include?(users(:david))
|
|
end
|
|
|
|
test "create for users by giving them immediate membership" do
|
|
room = Rooms::Closed.create_for({ name: "Hello!", creator: users(:david) }, users: [ users(:kevin), users(:david) ])
|
|
assert room.users.include?(users(:kevin))
|
|
assert room.users.include?(users(:david))
|
|
end
|
|
|
|
test "type" do
|
|
assert Rooms::Open.new.open?
|
|
assert_not Rooms::Open.new.direct?
|
|
assert Rooms::Direct.new.direct?
|
|
assert Rooms::Closed.new.closed?
|
|
end
|
|
|
|
test "an open room destroyed later lets no one in who joins the account before the job runs" do
|
|
room = rooms(:pets)
|
|
|
|
room.destroy_later
|
|
newcomer = User.create!(name: "Newcomer", email_address: "newcomer@example.com", password: "secret123456")
|
|
|
|
assert_not newcomer.memberships.exists?(room_id: room.id)
|
|
perform_enqueued_jobs only: Room::DestroyJob
|
|
assert_not Room.exists?(room.id)
|
|
end
|
|
|
|
test "a room destroyed later resets the connections of its former members, as revoking their memberships would" do
|
|
room = rooms(:designers)
|
|
former_members = room.users.to_a
|
|
assert former_members.many?
|
|
|
|
room.destroy_later
|
|
perform_enqueued_jobs only: Room::DestroyJob
|
|
|
|
former_members.each do |member|
|
|
assert_broadcast_on "action_cable/#{member.to_gid_param}", { type: "disconnect", reconnect: true }
|
|
end
|
|
end
|
|
|
|
test "destroying one message at a time leaves nothing of the room behind" do
|
|
room = rooms(:designers)
|
|
searchable = room.messages.create!(body: "Kept in the search index", creator: users(:david))
|
|
searchable.attachment.attach io: StringIO.new("hello"), filename: "hello.txt", content_type: "text/plain"
|
|
message_ids = room.messages.ids
|
|
assert Boost.where(message_id: message_ids).exists?
|
|
assert_equal 1, search_index_rows(searchable)
|
|
|
|
room.destroy_one_message_at_a_time
|
|
|
|
assert_not Room.exists?(room.id)
|
|
assert_empty Message.where(id: message_ids)
|
|
assert_empty Boost.where(message_id: message_ids)
|
|
assert_empty ActionText::RichText.where(record_type: "Message", record_id: message_ids)
|
|
assert_empty ActiveStorage::Attachment.where(record_type: "Message", record_id: message_ids)
|
|
assert_enqueued_jobs 1, only: ActiveStorage::PurgeJob
|
|
assert_equal 0, search_index_rows(searchable)
|
|
end
|
|
|
|
test "each message is destroyed in its own transaction, so other writes get through in between" do
|
|
room = rooms(:designers)
|
|
messages = room.messages.count
|
|
transactions = 0
|
|
count_transactions = ->(*, payload) { transactions += 1 if payload[:sql].start_with?("RELEASE SAVEPOINT") }
|
|
|
|
ActiveSupport::Notifications.subscribed(count_transactions, "sql.active_record") { room.destroy_one_message_at_a_time }
|
|
|
|
assert_equal messages + 1, transactions
|
|
end
|
|
|
|
test "default involvement for new users" do
|
|
room = Rooms::Closed.create_for({ name: "Hello!", creator: users(:david) }, users: [ users(:kevin), users(:david) ])
|
|
assert room.memberships.all? { |m| m.involved_in_mentions? }
|
|
end
|
|
|
|
private
|
|
def search_index_rows(message)
|
|
Message.connection.select_value("select count(*) from message_search_index where rowid = #{message.id}")
|
|
end
|
|
end
|