mirror of
https://github.com/we-promise/sure.git
synced 2026-04-18 19:44:09 +00:00
Family sharing (#1272)
* Initial account sharing changes * Update schema.rb * Update schema.rb * Change sharing UI to modal * UX fixes and sharing controls * Scope include in finances better * Update totals.rb * Update totals.rb * Scope reports to finance account scope * Update impersonation_sessions_controller_test.rb * Review fixes * Update schema.rb * Update show.html.erb * FIX db validation * Refine edit permissions * Review items * Review * Review * Add application level helper * Critical review * Address remaining review items * Fix modals * more scoping * linter * small UI fix * Fix: Sync broadcasts push unscoped balance sheet to all users * Update sync_complete_event.rb The fix removes the sidebar broadcasts (which rendered unscoped account groups using family.balance_sheet without user context) along with the now-unused sidebar_targets, account_group, and family_balance_sheet private methods. The sidebar will still update correctly — when the sync completes, Family::SyncCompleteEvent#broadcast fires family.broadcast_refresh, which triggers a morph-based page refresh for each user with their own authenticated session, rendering properly scoped sidebar content.
This commit is contained in:
61
test/models/account_share_test.rb
Normal file
61
test/models/account_share_test.rb
Normal file
@@ -0,0 +1,61 @@
|
||||
require "test_helper"
|
||||
|
||||
class AccountShareTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@admin = users(:family_admin)
|
||||
@member = users(:family_member)
|
||||
@account = accounts(:depository)
|
||||
end
|
||||
|
||||
test "valid share" do
|
||||
# Use an account that doesn't already have a share with member
|
||||
account = accounts(:investment)
|
||||
account.account_shares.where(user: @member).destroy_all
|
||||
share = AccountShare.new(account: account, user: @member, permission: "read_only")
|
||||
assert share.valid?
|
||||
end
|
||||
|
||||
test "invalid permission" do
|
||||
share = AccountShare.new(account: @account, user: @member, permission: "invalid")
|
||||
assert_not share.valid?
|
||||
assert_includes share.errors[:permission], "is not included in the list"
|
||||
end
|
||||
|
||||
test "cannot share with account owner" do
|
||||
share = AccountShare.new(account: @account, user: @admin, permission: "read_only")
|
||||
assert_not share.valid?
|
||||
assert_includes share.errors[:user], "is already the owner of this account"
|
||||
end
|
||||
|
||||
test "cannot duplicate share for same user and account" do
|
||||
# depository already shared with member via fixture
|
||||
duplicate = AccountShare.new(account: @account, user: @member, permission: "read_only")
|
||||
assert_not duplicate.valid?
|
||||
end
|
||||
|
||||
test "permission helper methods" do
|
||||
share = AccountShare.new(permission: "full_control")
|
||||
assert share.full_control?
|
||||
assert_not share.read_write?
|
||||
assert_not share.read_only?
|
||||
assert share.can_annotate?
|
||||
assert share.can_edit?
|
||||
|
||||
share.permission = "read_write"
|
||||
assert share.read_write?
|
||||
assert share.can_annotate?
|
||||
assert_not share.can_edit?
|
||||
|
||||
share.permission = "read_only"
|
||||
assert share.read_only?
|
||||
assert_not share.can_annotate?
|
||||
assert_not share.can_edit?
|
||||
end
|
||||
|
||||
test "cannot share with user from different family" do
|
||||
other_user = users(:empty)
|
||||
share = AccountShare.new(account: @account, user: other_user, permission: "read_only")
|
||||
assert_not share.valid?
|
||||
assert_includes share.errors[:user], "must be in the same family"
|
||||
end
|
||||
end
|
||||
@@ -6,6 +6,8 @@ class AccountTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@account = @syncable = accounts(:depository)
|
||||
@family = families(:dylan_family)
|
||||
@admin = users(:family_admin)
|
||||
@member = users(:family_member)
|
||||
end
|
||||
|
||||
test "can destroy" do
|
||||
@@ -19,6 +21,7 @@ class AccountTest < ActiveSupport::TestCase
|
||||
|
||||
account = Account.create_and_sync({
|
||||
family: @family,
|
||||
owner: @admin,
|
||||
name: "Test Account",
|
||||
balance: 100,
|
||||
currency: "USD",
|
||||
@@ -37,6 +40,7 @@ class AccountTest < ActiveSupport::TestCase
|
||||
account = Account.create_and_sync(
|
||||
{
|
||||
family: @family,
|
||||
owner: @admin,
|
||||
name: "Linked Account",
|
||||
balance: 500,
|
||||
currency: "EUR",
|
||||
@@ -57,6 +61,7 @@ class AccountTest < ActiveSupport::TestCase
|
||||
account = Account.create_and_sync(
|
||||
{
|
||||
family: @family,
|
||||
owner: @admin,
|
||||
name: "Test Account",
|
||||
balance: 1000,
|
||||
currency: "GBP",
|
||||
@@ -79,6 +84,7 @@ class AccountTest < ActiveSupport::TestCase
|
||||
account = Account.create_and_sync(
|
||||
{
|
||||
family: @family,
|
||||
owner: @admin,
|
||||
name: "Test Account",
|
||||
balance: 1000,
|
||||
currency: "USD",
|
||||
@@ -96,6 +102,7 @@ class AccountTest < ActiveSupport::TestCase
|
||||
test "gets short/long subtype label" do
|
||||
investment = Investment.new(subtype: "hsa")
|
||||
account = @family.accounts.create!(
|
||||
owner: @admin,
|
||||
name: "Test Investment",
|
||||
balance: 1000,
|
||||
currency: "USD",
|
||||
@@ -116,6 +123,7 @@ class AccountTest < ActiveSupport::TestCase
|
||||
test "tax_treatment delegates to accountable for Investment" do
|
||||
investment = Investment.new(subtype: "401k")
|
||||
account = @family.accounts.create!(
|
||||
owner: @admin,
|
||||
name: "Test 401k",
|
||||
balance: 1000,
|
||||
currency: "USD",
|
||||
@@ -129,6 +137,7 @@ class AccountTest < ActiveSupport::TestCase
|
||||
test "tax_treatment delegates to accountable for Crypto" do
|
||||
crypto = Crypto.new(tax_treatment: :taxable)
|
||||
account = @family.accounts.create!(
|
||||
owner: @admin,
|
||||
name: "Test Crypto",
|
||||
balance: 500,
|
||||
currency: "USD",
|
||||
@@ -148,6 +157,7 @@ class AccountTest < ActiveSupport::TestCase
|
||||
test "tax_advantaged? returns true for tax-advantaged accounts" do
|
||||
investment = Investment.new(subtype: "401k")
|
||||
account = @family.accounts.create!(
|
||||
owner: @admin,
|
||||
name: "Test 401k",
|
||||
balance: 1000,
|
||||
currency: "USD",
|
||||
@@ -161,6 +171,7 @@ class AccountTest < ActiveSupport::TestCase
|
||||
test "tax_advantaged? returns false for taxable accounts" do
|
||||
investment = Investment.new(subtype: "brokerage")
|
||||
account = @family.accounts.create!(
|
||||
owner: @admin,
|
||||
name: "Test Brokerage",
|
||||
balance: 1000,
|
||||
currency: "USD",
|
||||
@@ -193,4 +204,86 @@ class AccountTest < ActiveSupport::TestCase
|
||||
|
||||
assert_not ActiveStorage::Attachment.exists?(attachment_id)
|
||||
end
|
||||
|
||||
# Account sharing tests
|
||||
|
||||
test "owned_by? returns true for account owner" do
|
||||
assert @account.owned_by?(@admin)
|
||||
assert_not @account.owned_by?(@member)
|
||||
end
|
||||
|
||||
test "shared_with? returns true for owner and shared users" do
|
||||
assert @account.shared_with?(@admin) # owner
|
||||
# depository already shared with member via fixture
|
||||
assert @account.shared_with?(@member)
|
||||
end
|
||||
|
||||
test "shared? returns true when account has shares" do
|
||||
account = accounts(:investment)
|
||||
account.account_shares.destroy_all
|
||||
assert_not account.shared?
|
||||
|
||||
account.share_with!(@member, permission: "read_only")
|
||||
assert account.shared?
|
||||
end
|
||||
|
||||
test "permission_for returns correct permission level" do
|
||||
assert_equal :owner, @account.permission_for(@admin)
|
||||
|
||||
# depository already shared with member via fixture
|
||||
share = @account.account_shares.find_by(user: @member)
|
||||
share.update!(permission: "read_write")
|
||||
assert_equal :read_write, @account.permission_for(@member)
|
||||
end
|
||||
|
||||
test "accessible_by scope returns owned and shared accounts" do
|
||||
# Clear existing shares for clean test
|
||||
AccountShare.delete_all
|
||||
|
||||
admin_accessible = @family.accounts.accessible_by(@admin)
|
||||
member_accessible = @family.accounts.accessible_by(@member)
|
||||
|
||||
# Admin owns all fixture accounts
|
||||
assert_equal @family.accounts.count, admin_accessible.count
|
||||
# Member has no access (no shares, no owned accounts)
|
||||
assert_equal 0, member_accessible.count
|
||||
|
||||
# Share one account
|
||||
@account.share_with!(@member, permission: "read_only")
|
||||
member_accessible = @family.accounts.accessible_by(@member)
|
||||
assert_equal 1, member_accessible.count
|
||||
assert_includes member_accessible, @account
|
||||
end
|
||||
|
||||
test "included_in_finances_for scope respects include_in_finances flag" do
|
||||
AccountShare.delete_all
|
||||
|
||||
@account.share_with!(@member, permission: "read_only", include_in_finances: true)
|
||||
assert_includes @family.accounts.included_in_finances_for(@member), @account
|
||||
|
||||
share = @account.account_shares.find_by(user: @member)
|
||||
share.update!(include_in_finances: false)
|
||||
assert_not_includes @family.accounts.included_in_finances_for(@member), @account
|
||||
end
|
||||
|
||||
test "auto_share_with_family creates shares for all non-owner members" do
|
||||
account = Account.create_and_sync({
|
||||
family: @family,
|
||||
owner: @admin,
|
||||
name: "New Shared Account",
|
||||
balance: 100,
|
||||
currency: "USD",
|
||||
accountable_type: "Depository",
|
||||
accountable_attributes: {}
|
||||
})
|
||||
|
||||
assert_difference -> { AccountShare.count }, @family.users.where.not(id: @admin.id).count do
|
||||
account.auto_share_with_family!
|
||||
end
|
||||
|
||||
share = account.account_shares.find_by(user: @member)
|
||||
assert_not_nil share
|
||||
assert_equal "read_write", share.permission
|
||||
assert share.include_in_finances?
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user