Skip to content

Commit 372d46f

Browse files
authored
Merge pull request #1849 from basecamp/prevent-crash-in-join-code-race-condition
Fix crash in join code redemption race condition
2 parents b969444 + 6e9381a commit 372d46f

File tree

6 files changed

+41
-24
lines changed

6 files changed

+41
-24
lines changed

app/controllers/join_codes_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def new
1212
def create
1313
identity = Identity.find_or_create_by!(email_address: params.expect(:email_address))
1414

15-
@join_code.redeem { |account| identity.join(account) } unless identity.member_of?(@join_code.account)
15+
@join_code.redeem_if { |account| identity.join(account) }
1616
user = User.active.find_by!(account: @join_code.account, identity: identity)
1717

1818
if identity == Current.identity && user.setup?

app/models/account/join_code.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ class Account::JoinCode < ApplicationRecord
77

88
before_create :generate_code, if: -> { code.blank? }
99

10-
def redeem
10+
def redeem_if(&block)
1111
transaction do
12-
increment!(:usage_count)
13-
yield account if block_given?
12+
increment!(:usage_count) if block.call(account)
1413
end
1514
end
1615

app/models/identity/joinable.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ def join(account, **attributes)
55
attributes[:name] ||= email_address
66

77
transaction do
8-
account.users.create!(**attributes, identity: self)
8+
account.users.find_or_create_by!(identity: self) do |user|
9+
user.assign_attributes(attributes)
10+
end.previously_new_record?
911
end
1012
end
11-
12-
def member_of?(account)
13-
account.users.exists?(identity: self)
14-
end
1513
end

test/controllers/join_codes_controller_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class JoinCodesControllerTest < ActionDispatch::IntegrationTest
4343
identity = identities(:jz)
4444
sign_in_as :jz
4545

46-
assert identity.member_of?(@account), "JZ should be a member of 37s for this test"
46+
assert identity.users.exists?(account: @account), "JZ should be a member of 37s for this test"
4747
assert identity.users.find_by!(account: @account).setup?, "JZ's user should be setup for this test"
4848

4949
assert_no_difference -> { Identity.count } do
@@ -59,7 +59,7 @@ class JoinCodesControllerTest < ActionDispatch::IntegrationTest
5959
identity = identities(:mike)
6060
sign_in_as :mike
6161

62-
assert_not identity.member_of?(@account), "Mike should not be a member of 37s for this test"
62+
assert_not identity.users.exists?(account: @account), "Mike should not be a member of 37s for this test"
6363

6464
assert_no_difference -> { Identity.count } do
6565
assert_difference -> { User.count }, 1 do

test/models/account/join_code_test.rb

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,19 @@ class Account::JoinCodeTest < ActiveSupport::TestCase
1010
assert_equal 3, parts.count
1111
end
1212

13-
test "redeem" do
13+
test "redeem_if increments usage_count when block returns true" do
1414
join_code = account_join_codes(:"37s")
1515

16-
assert_difference -> { join_code.reload.usage_count }, 1 do
17-
join_code.redeem
16+
assert_difference -> { join_code.reload.usage_count }, +1 do
17+
join_code.redeem_if { true }
18+
end
19+
end
20+
21+
test "redeem_if does not increment usage_count when block returns false" do
22+
join_code = account_join_codes(:"37s")
23+
24+
assert_no_difference -> { join_code.reload.usage_count } do
25+
join_code.redeem_if { false }
1826
end
1927
end
2028

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,37 @@
11
require "test_helper"
22

33
class Identity::JoinableTest < ActiveSupport::TestCase
4-
test "join" do
4+
test "join creates a new user and returns true" do
55
identity = identities(:david)
66

7-
user = identity.join(accounts(:initech))
8-
assert_kind_of User, user
9-
assert_equal accounts(:initech), user.account
7+
assert_difference -> { User.count }, 1 do
8+
result = identity.join(accounts(:initech))
9+
assert result, "join should return true when creating a new user"
10+
end
11+
12+
user = identity.users.find_by!(account: accounts(:initech))
1013
assert_equal identity.email_address, user.name
14+
end
1115

16+
test "join with custom attributes" do
1217
identity = identities(:mike)
1318

14-
user = identity.join(accounts("37s"), name: "Mike")
15-
assert_kind_of User, user
16-
assert_equal accounts("37s"), user.account
19+
result = identity.join(accounts("37s"), name: "Mike")
20+
assert result
21+
22+
user = identity.users.find_by!(account: accounts("37s"))
1723
assert_equal "Mike", user.name
1824
end
1925

20-
test "member_of?" do
26+
test "join returns false if user already exists" do
2127
identity = identities(:david)
22-
assert identity.member_of?(accounts("37s"))
23-
assert_not identity.member_of?(accounts(:initech))
28+
account = accounts("37s")
29+
30+
assert identity.users.exists?(account: account), "David should already be a member of 37s"
31+
32+
assert_no_difference -> { User.count } do
33+
result = identity.join(account)
34+
assert_not result, "join should return false when user already exists"
35+
end
2436
end
2537
end

0 commit comments

Comments
 (0)