mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 01:31:39 -05:00
Refactor family membership and invitation policies for clarity and security
This commit is contained in:
parent
923ea113c8
commit
29ae5c04f1
9 changed files with 98 additions and 266 deletions
|
|
@ -8,17 +8,7 @@ class Family::MembershipsController < ApplicationController
|
||||||
before_action :set_invitation, only: %i[create]
|
before_action :set_invitation, only: %i[create]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
unless @invitation.pending?
|
authorize @invitation, policy_class: Family::MembershipPolicy
|
||||||
redirect_to root_path, alert: 'This invitation has already been processed' and return
|
|
||||||
end
|
|
||||||
|
|
||||||
if @invitation.expired?
|
|
||||||
redirect_to root_path, alert: 'This invitation is no longer valid or has expired' and return
|
|
||||||
end
|
|
||||||
|
|
||||||
if @invitation.email != current_user.email
|
|
||||||
redirect_to root_path, alert: 'This invitation is not for your email address' and return
|
|
||||||
end
|
|
||||||
|
|
||||||
service = Families::AcceptInvitation.new(
|
service = Families::AcceptInvitation.new(
|
||||||
invitation: @invitation,
|
invitation: @invitation,
|
||||||
|
|
@ -30,6 +20,16 @@ class Family::MembershipsController < ApplicationController
|
||||||
else
|
else
|
||||||
redirect_to root_path, alert: service.error_message || 'Unable to accept invitation'
|
redirect_to root_path, alert: service.error_message || 'Unable to accept invitation'
|
||||||
end
|
end
|
||||||
|
rescue Pundit::NotAuthorizedError
|
||||||
|
if @invitation.expired?
|
||||||
|
redirect_to root_path, alert: 'This invitation is no longer valid or has expired'
|
||||||
|
elsif !@invitation.pending?
|
||||||
|
redirect_to root_path, alert: 'This invitation has already been processed'
|
||||||
|
elsif @invitation.email != current_user.email
|
||||||
|
redirect_to root_path, alert: 'This invitation is not for your email address'
|
||||||
|
else
|
||||||
|
redirect_to root_path, alert: 'You are not authorized to accept this invitation'
|
||||||
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error "Error accepting family invitation: #{e.message}"
|
Rails.logger.error "Error accepting family invitation: #{e.message}"
|
||||||
redirect_to root_path, alert: 'An unexpected error occurred. Please try again later'
|
redirect_to root_path, alert: 'An unexpected error occurred. Please try again later'
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Family::Invitations::CleanupJob < ApplicationJob
|
class Family::Invitations::CleanupJob < ApplicationJob
|
||||||
queue_as :family
|
queue_as :families
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
Rails.logger.info 'Starting family invitations cleanup'
|
Rails.logger.info 'Starting family invitations cleanup'
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Family::InvitationPolicy < ApplicationPolicy
|
class Family::InvitationPolicy < ApplicationPolicy
|
||||||
def show?
|
|
||||||
# Public endpoint for invitation acceptance - no authentication required
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
def create?
|
def create?
|
||||||
return false unless user
|
return false unless user
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,22 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Family::MembershipPolicy < ApplicationPolicy
|
class Family::MembershipPolicy < ApplicationPolicy
|
||||||
def show?
|
def create?
|
||||||
return false unless user
|
return false unless user
|
||||||
|
return false unless record.is_a?(Family::Invitation)
|
||||||
|
|
||||||
user.family == record.family
|
# User can only accept invitations that:
|
||||||
end
|
# 1. Are for their email address
|
||||||
|
# 2. Are still pending
|
||||||
def update?
|
# 3. Haven't expired
|
||||||
return false unless user
|
record.email == user.email && record.pending? && !record.expired?
|
||||||
|
|
||||||
# Users can update their own settings
|
|
||||||
return true if user == record.user
|
|
||||||
|
|
||||||
# Family owners can update any member's settings
|
|
||||||
show? && user.family_owner?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy?
|
def destroy?
|
||||||
return false unless user
|
return false unless user
|
||||||
|
|
||||||
# Users can remove themselves (handled by family leave logic)
|
|
||||||
return true if user == record.user
|
return true if user == record.user
|
||||||
|
|
||||||
# Family owners can remove other members
|
# Family owners can remove other members
|
||||||
update?
|
user.family == record.family && user.family_owner?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -34,17 +34,15 @@ class Families::Locations
|
||||||
sharing_members.map { _1.points.last }.compact
|
sharing_members.map { _1.points.last }.compact
|
||||||
|
|
||||||
latest_points.map do |point|
|
latest_points.map do |point|
|
||||||
next unless point
|
|
||||||
|
|
||||||
{
|
{
|
||||||
user_id: point.user_id,
|
user_id: point.user_id,
|
||||||
email: point.user.email,
|
email: point.user.email,
|
||||||
email_initial: point.user.email.first.upcase,
|
email_initial: point.user.email.first.upcase,
|
||||||
latitude: point.lat.to_f,
|
latitude: point.lat,
|
||||||
longitude: point.lon.to_f,
|
longitude: point.lon,
|
||||||
timestamp: point.timestamp.to_i,
|
timestamp: point.timestamp.to_i,
|
||||||
updated_at: Time.zone.at(point.timestamp.to_i)
|
updated_at: Time.zone.at(point.timestamp.to_i)
|
||||||
}
|
}
|
||||||
end.compact
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -6,20 +6,18 @@ module Families
|
||||||
attr_reader :user, :member_to_remove, :error_message
|
attr_reader :user, :member_to_remove, :error_message
|
||||||
|
|
||||||
def initialize(user:, member_to_remove: nil)
|
def initialize(user:, member_to_remove: nil)
|
||||||
@user = user # The user performing the action (current_user)
|
@user = user
|
||||||
@member_to_remove = member_to_remove || user # The user being removed (defaults to self)
|
@member_to_remove = member_to_remove || user
|
||||||
@error_message = nil
|
@error_message = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
return false unless validate_can_leave
|
return false unless validate_can_leave
|
||||||
|
|
||||||
# Store family info before removing membership
|
|
||||||
@family_name = member_to_remove.family.name
|
@family_name = member_to_remove.family.name
|
||||||
@family_owner = member_to_remove.family.owner
|
@family_owner = member_to_remove.family.owner
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
handle_ownership_transfer if member_to_remove.family_owner?
|
|
||||||
remove_membership
|
remove_membership
|
||||||
send_notifications
|
send_notifications
|
||||||
end
|
end
|
||||||
|
|
@ -52,10 +50,8 @@ module Families
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_removal_allowed
|
def validate_removal_allowed
|
||||||
# If removing self (user == member_to_remove)
|
|
||||||
return validate_owner_can_leave if removing_self?
|
return validate_owner_can_leave if removing_self?
|
||||||
|
|
||||||
# If removing another member, user must be owner and member must be in same family
|
|
||||||
return false unless validate_remover_is_owner
|
return false unless validate_remover_is_owner
|
||||||
return false unless validate_same_family
|
return false unless validate_same_family
|
||||||
return false unless validate_not_removing_owner
|
return false unless validate_not_removing_owner
|
||||||
|
|
@ -95,19 +91,6 @@ module Families
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
def family_has_other_members?
|
|
||||||
member_to_remove.family.members.count > 1
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_ownership_transfer
|
|
||||||
# If this is the last member (owner), delete the family
|
|
||||||
return unless member_to_remove.family.members.count == 1
|
|
||||||
|
|
||||||
member_to_remove.family.destroy!
|
|
||||||
|
|
||||||
# If owner tries to leave with other members, it should be prevented in validation
|
|
||||||
end
|
|
||||||
|
|
||||||
def remove_membership
|
def remove_membership
|
||||||
member_to_remove.family_membership.destroy!
|
member_to_remove.family_membership.destroy!
|
||||||
end
|
end
|
||||||
|
|
@ -123,7 +106,6 @@ module Families
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_self_removal_notifications
|
def send_self_removal_notifications
|
||||||
# Notify the user who left
|
|
||||||
Notification.create!(
|
Notification.create!(
|
||||||
user: member_to_remove,
|
user: member_to_remove,
|
||||||
kind: :info,
|
kind: :info,
|
||||||
|
|
@ -131,7 +113,6 @@ module Families
|
||||||
content: "You've left the family \"#{@family_name}\""
|
content: "You've left the family \"#{@family_name}\""
|
||||||
)
|
)
|
||||||
|
|
||||||
# Notify the family owner
|
|
||||||
return unless @family_owner&.persisted?
|
return unless @family_owner&.persisted?
|
||||||
|
|
||||||
Notification.create!(
|
Notification.create!(
|
||||||
|
|
@ -143,7 +124,6 @@ module Families
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_member_removed_notifications
|
def send_member_removed_notifications
|
||||||
# Notify the member who was removed
|
|
||||||
Notification.create!(
|
Notification.create!(
|
||||||
user: member_to_remove,
|
user: member_to_remove,
|
||||||
kind: :info,
|
kind: :info,
|
||||||
|
|
@ -151,7 +131,6 @@ module Families
|
||||||
content: "You have been removed from the family \"#{@family_name}\" by #{user.email}"
|
content: "You have been removed from the family \"#{@family_name}\" by #{user.email}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Notify the owner who removed the member (if different from the member)
|
|
||||||
return unless user != member_to_remove
|
return unless user != member_to_remove
|
||||||
|
|
||||||
Notification.create!(
|
Notification.create!(
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
- points
|
- points
|
||||||
- default
|
- default
|
||||||
- mailers
|
- mailers
|
||||||
- family
|
- families
|
||||||
- imports
|
- imports
|
||||||
- exports
|
- exports
|
||||||
- stats
|
- stats
|
||||||
|
|
|
||||||
|
|
@ -10,36 +10,10 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
let(:invitation) { create(:family_invitation, family: family, invited_by: owner) }
|
let(:invitation) { create(:family_invitation, family: family, invited_by: owner) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
# Set up family membership for owner
|
|
||||||
create(:family_membership, family: family, user: owner, role: :owner)
|
create(:family_membership, family: family, user: owner, role: :owner)
|
||||||
# Set up family membership for regular member
|
|
||||||
create(:family_membership, family: family, user: member, role: :member)
|
create(:family_membership, family: family, user: member, role: :member)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#show?' do
|
|
||||||
context 'with authenticated user' do
|
|
||||||
it 'allows any authenticated user to view invitation' do
|
|
||||||
policy = Family::InvitationPolicy.new(owner, invitation)
|
|
||||||
|
|
||||||
expect(policy).to permit(:show)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows other users to view invitation' do
|
|
||||||
policy = Family::InvitationPolicy.new(other_user, invitation)
|
|
||||||
|
|
||||||
expect(policy).to permit(:show)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with unauthenticated user' do
|
|
||||||
it 'allows unauthenticated access (public endpoint)' do
|
|
||||||
policy = Family::InvitationPolicy.new(nil, invitation)
|
|
||||||
|
|
||||||
expect(policy).to permit(:show)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#create?' do
|
describe '#create?' do
|
||||||
context 'when user is family owner' do
|
context 'when user is family owner' do
|
||||||
before do
|
before do
|
||||||
|
|
@ -48,7 +22,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows family owner to create invitations' do
|
it 'allows family owner to create invitations' do
|
||||||
policy = Family::InvitationPolicy.new(owner, invitation)
|
policy = described_class.new(owner, invitation)
|
||||||
|
|
||||||
expect(policy).to permit(:create)
|
expect(policy).to permit(:create)
|
||||||
end
|
end
|
||||||
|
|
@ -61,7 +35,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'denies regular family member from creating invitations' do
|
it 'denies regular family member from creating invitations' do
|
||||||
policy = Family::InvitationPolicy.new(member, invitation)
|
policy = described_class.new(member, invitation)
|
||||||
|
|
||||||
expect(policy).not_to permit(:create)
|
expect(policy).not_to permit(:create)
|
||||||
end
|
end
|
||||||
|
|
@ -69,7 +43,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
|
|
||||||
context 'when user is not in the family' do
|
context 'when user is not in the family' do
|
||||||
it 'denies user not in the family from creating invitations' do
|
it 'denies user not in the family from creating invitations' do
|
||||||
policy = Family::InvitationPolicy.new(other_user, invitation)
|
policy = described_class.new(other_user, invitation)
|
||||||
|
|
||||||
expect(policy).not_to permit(:create)
|
expect(policy).not_to permit(:create)
|
||||||
end
|
end
|
||||||
|
|
@ -77,7 +51,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
|
|
||||||
context 'with unauthenticated user' do
|
context 'with unauthenticated user' do
|
||||||
it 'denies unauthenticated user from creating invitations' do
|
it 'denies unauthenticated user from creating invitations' do
|
||||||
policy = Family::InvitationPolicy.new(nil, invitation)
|
policy = described_class.new(nil, invitation)
|
||||||
|
|
||||||
expect(policy).not_to permit(:create)
|
expect(policy).not_to permit(:create)
|
||||||
end
|
end
|
||||||
|
|
@ -89,7 +63,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
let(:invited_user) { create(:user, email: invitation.email) }
|
let(:invited_user) { create(:user, email: invitation.email) }
|
||||||
|
|
||||||
it 'allows user to accept invitation sent to their email' do
|
it 'allows user to accept invitation sent to their email' do
|
||||||
policy = Family::InvitationPolicy.new(invited_user, invitation)
|
policy = described_class.new(invited_user, invitation)
|
||||||
|
|
||||||
expect(policy).to permit(:accept)
|
expect(policy).to permit(:accept)
|
||||||
end
|
end
|
||||||
|
|
@ -97,7 +71,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
|
|
||||||
context 'when user email does not match invitation email' do
|
context 'when user email does not match invitation email' do
|
||||||
it 'denies user with different email from accepting invitation' do
|
it 'denies user with different email from accepting invitation' do
|
||||||
policy = Family::InvitationPolicy.new(other_user, invitation)
|
policy = described_class.new(other_user, invitation)
|
||||||
|
|
||||||
expect(policy).not_to permit(:accept)
|
expect(policy).not_to permit(:accept)
|
||||||
end
|
end
|
||||||
|
|
@ -105,7 +79,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
|
|
||||||
context 'when family owner tries to accept invitation' do
|
context 'when family owner tries to accept invitation' do
|
||||||
it 'denies family owner from accepting invitation sent to different email' do
|
it 'denies family owner from accepting invitation sent to different email' do
|
||||||
policy = Family::InvitationPolicy.new(owner, invitation)
|
policy = described_class.new(owner, invitation)
|
||||||
|
|
||||||
expect(policy).not_to permit(:accept)
|
expect(policy).not_to permit(:accept)
|
||||||
end
|
end
|
||||||
|
|
@ -113,7 +87,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
|
|
||||||
context 'with unauthenticated user' do
|
context 'with unauthenticated user' do
|
||||||
it 'denies unauthenticated user from accepting invitation' do
|
it 'denies unauthenticated user from accepting invitation' do
|
||||||
policy = Family::InvitationPolicy.new(nil, invitation)
|
policy = described_class.new(nil, invitation)
|
||||||
|
|
||||||
expect(policy).not_to permit(:accept)
|
expect(policy).not_to permit(:accept)
|
||||||
end
|
end
|
||||||
|
|
@ -128,7 +102,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows family owner to cancel invitations' do
|
it 'allows family owner to cancel invitations' do
|
||||||
policy = Family::InvitationPolicy.new(owner, invitation)
|
policy = described_class.new(owner, invitation)
|
||||||
|
|
||||||
expect(policy).to permit(:destroy)
|
expect(policy).to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
@ -141,7 +115,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'denies regular family member from cancelling invitations' do
|
it 'denies regular family member from cancelling invitations' do
|
||||||
policy = Family::InvitationPolicy.new(member, invitation)
|
policy = described_class.new(member, invitation)
|
||||||
|
|
||||||
expect(policy).not_to permit(:destroy)
|
expect(policy).not_to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
@ -149,7 +123,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
|
|
||||||
context 'when user is not in the family' do
|
context 'when user is not in the family' do
|
||||||
it 'denies user not in the family from cancelling invitations' do
|
it 'denies user not in the family from cancelling invitations' do
|
||||||
policy = Family::InvitationPolicy.new(other_user, invitation)
|
policy = described_class.new(other_user, invitation)
|
||||||
|
|
||||||
expect(policy).not_to permit(:destroy)
|
expect(policy).not_to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
@ -157,7 +131,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
|
|
||||||
context 'with unauthenticated user' do
|
context 'with unauthenticated user' do
|
||||||
it 'denies unauthenticated user from cancelling invitations' do
|
it 'denies unauthenticated user from cancelling invitations' do
|
||||||
policy = Family::InvitationPolicy.new(nil, invitation)
|
policy = described_class.new(nil, invitation)
|
||||||
|
|
||||||
expect(policy).not_to permit(:destroy)
|
expect(policy).not_to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
@ -177,13 +151,13 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'denies owner from creating invitations for different family' do
|
it 'denies owner from creating invitations for different family' do
|
||||||
policy = Family::InvitationPolicy.new(owner, other_invitation)
|
policy = described_class.new(owner, other_invitation)
|
||||||
|
|
||||||
expect(policy).not_to permit(:create)
|
expect(policy).not_to permit(:create)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'denies owner from destroying invitations for different family' do
|
it 'denies owner from destroying invitations for different family' do
|
||||||
policy = Family::InvitationPolicy.new(owner, other_invitation)
|
policy = described_class.new(owner, other_invitation)
|
||||||
|
|
||||||
expect(policy).not_to permit(:destroy)
|
expect(policy).not_to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
@ -194,7 +168,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
let(:invited_user) { create(:user, email: expired_invitation.email) }
|
let(:invited_user) { create(:user, email: expired_invitation.email) }
|
||||||
|
|
||||||
it 'still allows user to attempt to accept expired invitation (business logic handles expiry)' do
|
it 'still allows user to attempt to accept expired invitation (business logic handles expiry)' do
|
||||||
policy = Family::InvitationPolicy.new(invited_user, expired_invitation)
|
policy = described_class.new(invited_user, expired_invitation)
|
||||||
|
|
||||||
expect(policy).to permit(:accept)
|
expect(policy).to permit(:accept)
|
||||||
end
|
end
|
||||||
|
|
@ -202,7 +176,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
it 'allows owner to destroy expired invitation' do
|
it 'allows owner to destroy expired invitation' do
|
||||||
allow(owner).to receive(:family).and_return(family)
|
allow(owner).to receive(:family).and_return(family)
|
||||||
allow(owner).to receive(:family_owner?).and_return(true)
|
allow(owner).to receive(:family_owner?).and_return(true)
|
||||||
policy = Family::InvitationPolicy.new(owner, expired_invitation)
|
policy = described_class.new(owner, expired_invitation)
|
||||||
|
|
||||||
expect(policy).to permit(:destroy)
|
expect(policy).to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
@ -214,7 +188,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
it 'allows owner to destroy accepted invitation' do
|
it 'allows owner to destroy accepted invitation' do
|
||||||
allow(owner).to receive(:family).and_return(family)
|
allow(owner).to receive(:family).and_return(family)
|
||||||
allow(owner).to receive(:family_owner?).and_return(true)
|
allow(owner).to receive(:family_owner?).and_return(true)
|
||||||
policy = Family::InvitationPolicy.new(owner, accepted_invitation)
|
policy = described_class.new(owner, accepted_invitation)
|
||||||
|
|
||||||
expect(policy).to permit(:destroy)
|
expect(policy).to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
@ -226,7 +200,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
it 'allows owner to destroy cancelled invitation' do
|
it 'allows owner to destroy cancelled invitation' do
|
||||||
allow(owner).to receive(:family).and_return(family)
|
allow(owner).to receive(:family).and_return(family)
|
||||||
allow(owner).to receive(:family_owner?).and_return(true)
|
allow(owner).to receive(:family_owner?).and_return(true)
|
||||||
policy = Family::InvitationPolicy.new(owner, cancelled_invitation)
|
policy = described_class.new(owner, cancelled_invitation)
|
||||||
|
|
||||||
expect(policy).to permit(:destroy)
|
expect(policy).to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
@ -237,7 +211,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
it 'ensures owner can both create and destroy invitations' do
|
it 'ensures owner can both create and destroy invitations' do
|
||||||
allow(owner).to receive(:family).and_return(family)
|
allow(owner).to receive(:family).and_return(family)
|
||||||
allow(owner).to receive(:family_owner?).and_return(true)
|
allow(owner).to receive(:family_owner?).and_return(true)
|
||||||
policy = Family::InvitationPolicy.new(owner, invitation)
|
policy = described_class.new(owner, invitation)
|
||||||
|
|
||||||
expect(policy).to permit(:create)
|
expect(policy).to permit(:create)
|
||||||
expect(policy).to permit(:destroy)
|
expect(policy).to permit(:destroy)
|
||||||
|
|
@ -246,7 +220,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
it 'ensures regular members cannot create or destroy invitations' do
|
it 'ensures regular members cannot create or destroy invitations' do
|
||||||
allow(member).to receive(:family).and_return(family)
|
allow(member).to receive(:family).and_return(family)
|
||||||
allow(member).to receive(:family_owner?).and_return(false)
|
allow(member).to receive(:family_owner?).and_return(false)
|
||||||
policy = Family::InvitationPolicy.new(member, invitation)
|
policy = described_class.new(member, invitation)
|
||||||
|
|
||||||
expect(policy).not_to permit(:create)
|
expect(policy).not_to permit(:create)
|
||||||
expect(policy).not_to permit(:destroy)
|
expect(policy).not_to permit(:destroy)
|
||||||
|
|
@ -254,7 +228,7 @@ RSpec.describe Family::InvitationPolicy, type: :policy do
|
||||||
|
|
||||||
it 'ensures invited users can only accept their own invitations' do
|
it 'ensures invited users can only accept their own invitations' do
|
||||||
invited_user = create(:user, email: invitation.email)
|
invited_user = create(:user, email: invitation.email)
|
||||||
policy = Family::InvitationPolicy.new(invited_user, invitation)
|
policy = described_class.new(invited_user, invitation)
|
||||||
|
|
||||||
expect(policy).to permit(:accept)
|
expect(policy).to permit(:accept)
|
||||||
expect(policy).not_to permit(:create)
|
expect(policy).not_to permit(:create)
|
||||||
|
|
|
||||||
|
|
@ -13,126 +13,49 @@ RSpec.describe Family::MembershipPolicy, type: :policy do
|
||||||
let(:member_membership) { create(:family_membership, family: family, user: member) }
|
let(:member_membership) { create(:family_membership, family: family, user: member) }
|
||||||
let(:another_member_membership) { create(:family_membership, family: family, user: another_member) }
|
let(:another_member_membership) { create(:family_membership, family: family, user: another_member) }
|
||||||
|
|
||||||
describe '#show?' do
|
describe '#create?' do
|
||||||
context 'when user is in the same family' do
|
let(:valid_invitation) { create(:family_invitation, family: family, email: member.email) }
|
||||||
before do
|
let(:expired_invitation) { create(:family_invitation, family: family, email: member.email, expires_at: 1.day.ago) }
|
||||||
allow(owner).to receive(:family).and_return(family)
|
let(:accepted_invitation) { create(:family_invitation, :accepted, family: family, email: member.email) }
|
||||||
end
|
let(:wrong_email_invitation) { create(:family_invitation, family: family, email: 'wrong@example.com') }
|
||||||
|
|
||||||
it 'allows family owner to view member details' do
|
context 'when user has valid invitation' do
|
||||||
policy = Family::MembershipPolicy.new(owner, member_membership)
|
it 'allows user to create membership with valid pending invitation for their email' do
|
||||||
|
policy = described_class.new(member, valid_invitation)
|
||||||
|
|
||||||
expect(policy).to permit(:show)
|
expect(policy).to permit(:create)
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows family owner to view their own membership' do
|
|
||||||
policy = Family::MembershipPolicy.new(owner, owner_membership)
|
|
||||||
|
|
||||||
expect(policy).to permit(:show)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows regular member to view other members' do
|
|
||||||
allow(member).to receive(:family).and_return(family)
|
|
||||||
policy = Family::MembershipPolicy.new(member, another_member_membership)
|
|
||||||
|
|
||||||
expect(policy).to permit(:show)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows member to view their own membership' do
|
|
||||||
allow(member).to receive(:family).and_return(family)
|
|
||||||
policy = Family::MembershipPolicy.new(member, member_membership)
|
|
||||||
|
|
||||||
expect(policy).to permit(:show)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user is not in the same family' do
|
context 'when invitation is expired' do
|
||||||
it 'denies user from different family from viewing membership' do
|
it 'denies user from creating membership with expired invitation' do
|
||||||
policy = Family::MembershipPolicy.new(other_user, member_membership)
|
policy = described_class.new(member, expired_invitation)
|
||||||
|
|
||||||
expect(policy).not_to permit(:show)
|
expect(policy).not_to permit(:create)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when invitation is already accepted' do
|
||||||
|
it 'denies user from creating membership with already accepted invitation' do
|
||||||
|
policy = described_class.new(member, accepted_invitation)
|
||||||
|
|
||||||
|
expect(policy).not_to permit(:create)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when invitation is for different email' do
|
||||||
|
it 'denies user from creating membership with invitation for different email' do
|
||||||
|
policy = described_class.new(member, wrong_email_invitation)
|
||||||
|
|
||||||
|
expect(policy).not_to permit(:create)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with unauthenticated user' do
|
context 'with unauthenticated user' do
|
||||||
it 'denies unauthenticated user from viewing membership' do
|
it 'denies unauthenticated user from creating membership' do
|
||||||
policy = Family::MembershipPolicy.new(nil, member_membership)
|
policy = described_class.new(nil, valid_invitation)
|
||||||
|
|
||||||
expect(policy).not_to permit(:show)
|
expect(policy).not_to permit(:create)
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#update?' do
|
|
||||||
context 'when user is updating their own membership' do
|
|
||||||
it 'allows user to update their own membership settings' do
|
|
||||||
allow(member).to receive(:family).and_return(family)
|
|
||||||
policy = Family::MembershipPolicy.new(member, member_membership)
|
|
||||||
|
|
||||||
expect(policy).to permit(:update)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows owner to update their own membership' do
|
|
||||||
allow(owner).to receive(:family).and_return(family)
|
|
||||||
policy = Family::MembershipPolicy.new(owner, owner_membership)
|
|
||||||
|
|
||||||
expect(policy).to permit(:update)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is family owner' do
|
|
||||||
before do
|
|
||||||
allow(owner).to receive(:family).and_return(family)
|
|
||||||
allow(owner).to receive(:family_owner?).and_return(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows family owner to update other members settings' do
|
|
||||||
policy = Family::MembershipPolicy.new(owner, member_membership)
|
|
||||||
|
|
||||||
expect(policy).to permit(:update)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows family owner to update multiple members' do
|
|
||||||
policy1 = Family::MembershipPolicy.new(owner, member_membership)
|
|
||||||
policy2 = Family::MembershipPolicy.new(owner, another_member_membership)
|
|
||||||
|
|
||||||
expect(policy1).to permit(:update)
|
|
||||||
expect(policy2).to permit(:update)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is regular family member' do
|
|
||||||
before do
|
|
||||||
allow(member).to receive(:family).and_return(family)
|
|
||||||
allow(member).to receive(:family_owner?).and_return(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'denies regular member from updating other members settings' do
|
|
||||||
policy = Family::MembershipPolicy.new(member, another_member_membership)
|
|
||||||
|
|
||||||
expect(policy).not_to permit(:update)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'denies regular member from updating owner settings' do
|
|
||||||
policy = Family::MembershipPolicy.new(member, owner_membership)
|
|
||||||
|
|
||||||
expect(policy).not_to permit(:update)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user is not in the family' do
|
|
||||||
it 'denies user from updating membership of different family' do
|
|
||||||
policy = Family::MembershipPolicy.new(other_user, member_membership)
|
|
||||||
|
|
||||||
expect(policy).not_to permit(:update)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with unauthenticated user' do
|
|
||||||
it 'denies unauthenticated user from updating membership' do
|
|
||||||
policy = Family::MembershipPolicy.new(nil, member_membership)
|
|
||||||
|
|
||||||
expect(policy).not_to permit(:update)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -141,14 +64,14 @@ RSpec.describe Family::MembershipPolicy, type: :policy do
|
||||||
context 'when user is removing themselves' do
|
context 'when user is removing themselves' do
|
||||||
it 'allows user to remove their own membership (leave family)' do
|
it 'allows user to remove their own membership (leave family)' do
|
||||||
allow(member).to receive(:family).and_return(family)
|
allow(member).to receive(:family).and_return(family)
|
||||||
policy = Family::MembershipPolicy.new(member, member_membership)
|
policy = described_class.new(member, member_membership)
|
||||||
|
|
||||||
expect(policy).to permit(:destroy)
|
expect(policy).to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows owner to remove their own membership' do
|
it 'allows owner to remove their own membership' do
|
||||||
allow(owner).to receive(:family).and_return(family)
|
allow(owner).to receive(:family).and_return(family)
|
||||||
policy = Family::MembershipPolicy.new(owner, owner_membership)
|
policy = described_class.new(owner, owner_membership)
|
||||||
|
|
||||||
expect(policy).to permit(:destroy)
|
expect(policy).to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
@ -161,14 +84,14 @@ RSpec.describe Family::MembershipPolicy, type: :policy do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows family owner to remove other members' do
|
it 'allows family owner to remove other members' do
|
||||||
policy = Family::MembershipPolicy.new(owner, member_membership)
|
policy = described_class.new(owner, member_membership)
|
||||||
|
|
||||||
expect(policy).to permit(:destroy)
|
expect(policy).to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows family owner to remove multiple members' do
|
it 'allows family owner to remove multiple members' do
|
||||||
policy1 = Family::MembershipPolicy.new(owner, member_membership)
|
policy1 = described_class.new(owner, member_membership)
|
||||||
policy2 = Family::MembershipPolicy.new(owner, another_member_membership)
|
policy2 = described_class.new(owner, another_member_membership)
|
||||||
|
|
||||||
expect(policy1).to permit(:destroy)
|
expect(policy1).to permit(:destroy)
|
||||||
expect(policy2).to permit(:destroy)
|
expect(policy2).to permit(:destroy)
|
||||||
|
|
@ -182,13 +105,13 @@ RSpec.describe Family::MembershipPolicy, type: :policy do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'denies regular member from removing other members' do
|
it 'denies regular member from removing other members' do
|
||||||
policy = Family::MembershipPolicy.new(member, another_member_membership)
|
policy = described_class.new(member, another_member_membership)
|
||||||
|
|
||||||
expect(policy).not_to permit(:destroy)
|
expect(policy).not_to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'denies regular member from removing owner' do
|
it 'denies regular member from removing owner' do
|
||||||
policy = Family::MembershipPolicy.new(member, owner_membership)
|
policy = described_class.new(member, owner_membership)
|
||||||
|
|
||||||
expect(policy).not_to permit(:destroy)
|
expect(policy).not_to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
@ -196,7 +119,7 @@ RSpec.describe Family::MembershipPolicy, type: :policy do
|
||||||
|
|
||||||
context 'when user is not in the family' do
|
context 'when user is not in the family' do
|
||||||
it 'denies user from removing membership of different family' do
|
it 'denies user from removing membership of different family' do
|
||||||
policy = Family::MembershipPolicy.new(other_user, member_membership)
|
policy = described_class.new(other_user, member_membership)
|
||||||
|
|
||||||
expect(policy).not_to permit(:destroy)
|
expect(policy).not_to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
@ -204,7 +127,7 @@ RSpec.describe Family::MembershipPolicy, type: :policy do
|
||||||
|
|
||||||
context 'with unauthenticated user' do
|
context 'with unauthenticated user' do
|
||||||
it 'denies unauthenticated user from removing membership' do
|
it 'denies unauthenticated user from removing membership' do
|
||||||
policy = Family::MembershipPolicy.new(nil, member_membership)
|
policy = described_class.new(nil, member_membership)
|
||||||
|
|
||||||
expect(policy).not_to permit(:destroy)
|
expect(policy).not_to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
@ -224,20 +147,8 @@ RSpec.describe Family::MembershipPolicy, type: :policy do
|
||||||
allow(owner).to receive(:family_owner?).and_return(true)
|
allow(owner).to receive(:family_owner?).and_return(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'denies owner from viewing membership of different family' do
|
|
||||||
policy = Family::MembershipPolicy.new(owner, other_family_membership)
|
|
||||||
|
|
||||||
expect(policy).not_to permit(:show)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'denies owner from updating membership of different family' do
|
|
||||||
policy = Family::MembershipPolicy.new(owner, other_family_membership)
|
|
||||||
|
|
||||||
expect(policy).not_to permit(:update)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'denies owner from destroying membership of different family' do
|
it 'denies owner from destroying membership of different family' do
|
||||||
policy = Family::MembershipPolicy.new(owner, other_family_membership)
|
policy = described_class.new(owner, other_family_membership)
|
||||||
|
|
||||||
expect(policy).not_to permit(:destroy)
|
expect(policy).not_to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
@ -252,20 +163,8 @@ RSpec.describe Family::MembershipPolicy, type: :policy do
|
||||||
allow(owner).to receive(:family_owner?).and_return(true)
|
allow(owner).to receive(:family_owner?).and_return(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allows owner to view another owner' do
|
|
||||||
policy = Family::MembershipPolicy.new(owner, co_owner_membership)
|
|
||||||
|
|
||||||
expect(policy).to permit(:show)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows owner to update another owner (family owner has full control)' do
|
|
||||||
policy = Family::MembershipPolicy.new(owner, co_owner_membership)
|
|
||||||
|
|
||||||
expect(policy).to permit(:update)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'allows owner to remove another owner (family owner has full control)' do
|
it 'allows owner to remove another owner (family owner has full control)' do
|
||||||
policy = Family::MembershipPolicy.new(owner, co_owner_membership)
|
policy = described_class.new(owner, co_owner_membership)
|
||||||
|
|
||||||
expect(policy).to permit(:destroy)
|
expect(policy).to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
@ -273,38 +172,32 @@ RSpec.describe Family::MembershipPolicy, type: :policy do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'authorization consistency' do
|
describe 'authorization consistency' do
|
||||||
it 'ensures owner can view, update, and destroy all memberships in their family' do
|
it 'ensures owner can destroy all memberships in their family' do
|
||||||
allow(owner).to receive(:family).and_return(family)
|
allow(owner).to receive(:family).and_return(family)
|
||||||
allow(owner).to receive(:family_owner?).and_return(true)
|
allow(owner).to receive(:family_owner?).and_return(true)
|
||||||
|
|
||||||
policy = Family::MembershipPolicy.new(owner, member_membership)
|
policy = described_class.new(owner, member_membership)
|
||||||
|
|
||||||
expect(policy).to permit(:show)
|
|
||||||
expect(policy).to permit(:update)
|
|
||||||
expect(policy).to permit(:destroy)
|
expect(policy).to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'ensures regular members can only manage their own membership' do
|
it 'ensures regular members can only remove their own membership' do
|
||||||
allow(member).to receive(:family).and_return(family)
|
allow(member).to receive(:family).and_return(family)
|
||||||
allow(member).to receive(:family_owner?).and_return(false)
|
allow(member).to receive(:family_owner?).and_return(false)
|
||||||
|
|
||||||
own_policy = Family::MembershipPolicy.new(member, member_membership)
|
own_policy = described_class.new(member, member_membership)
|
||||||
other_policy = Family::MembershipPolicy.new(member, another_member_membership)
|
other_policy = described_class.new(member, another_member_membership)
|
||||||
|
|
||||||
# Can manage own membership
|
# Can remove own membership
|
||||||
expect(own_policy).to permit(:show)
|
|
||||||
expect(own_policy).to permit(:update)
|
|
||||||
expect(own_policy).to permit(:destroy)
|
expect(own_policy).to permit(:destroy)
|
||||||
|
|
||||||
# Can view but not manage others
|
# Cannot remove others
|
||||||
expect(other_policy).to permit(:show)
|
|
||||||
expect(other_policy).not_to permit(:update)
|
|
||||||
expect(other_policy).not_to permit(:destroy)
|
expect(other_policy).not_to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'ensures users can always leave the family (remove own membership)' do
|
it 'ensures users can always leave the family (remove own membership)' do
|
||||||
allow(member).to receive(:family).and_return(family)
|
allow(member).to receive(:family).and_return(family)
|
||||||
policy = Family::MembershipPolicy.new(member, member_membership)
|
policy = described_class.new(member, member_membership)
|
||||||
|
|
||||||
expect(policy).to permit(:destroy)
|
expect(policy).to permit(:destroy)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue