mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-09 08:47:11 -05:00
Refactor family invitations and memberships into separate models and controllers
This commit is contained in:
parent
6fb5d98b19
commit
e711ff25fe
37 changed files with 336 additions and 245 deletions
1
Procfile
1
Procfile
|
|
@ -1,2 +1,3 @@
|
|||
release: bundle exec rails db:migrate
|
||||
web: bundle exec puma -C config/puma.rb
|
||||
worker: bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
|
|
|||
5
app.json
5
app.json
|
|
@ -5,11 +5,6 @@
|
|||
{ "url": "https://github.com/heroku/heroku-buildpack-nodejs.git" },
|
||||
{ "url": "https://github.com/heroku/heroku-buildpack-ruby.git" }
|
||||
],
|
||||
"scripts": {
|
||||
"dokku": {
|
||||
"predeploy": "bundle exec rails db:migrate"
|
||||
}
|
||||
},
|
||||
"healthchecks": {
|
||||
"web": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@
|
|||
class Family::InvitationsController < ApplicationController
|
||||
before_action :authenticate_user!, except: %i[show]
|
||||
before_action :ensure_family_feature_enabled!, except: %i[show]
|
||||
before_action :set_family, except: %i[show accept]
|
||||
before_action :set_family, except: %i[show]
|
||||
before_action :set_invitation_by_id_and_family, only: %i[destroy]
|
||||
before_action :set_invitation_by_id, only: %i[accept]
|
||||
|
||||
def index
|
||||
authorize @family, :show?
|
||||
|
|
@ -14,7 +13,7 @@ class Family::InvitationsController < ApplicationController
|
|||
end
|
||||
|
||||
def show
|
||||
@invitation = FamilyInvitation.find_by!(token: params[:token])
|
||||
@invitation = Family::Invitation.find_by!(token: params[:token])
|
||||
|
||||
if @invitation.expired?
|
||||
redirect_to root_path, alert: 'This invitation has expired.' and return
|
||||
|
|
@ -41,34 +40,6 @@ class Family::InvitationsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def accept
|
||||
unless @invitation.pending?
|
||||
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(
|
||||
invitation: @invitation,
|
||||
user: current_user
|
||||
)
|
||||
|
||||
if service.call
|
||||
redirect_to family_path, notice: 'Welcome to the family!'
|
||||
else
|
||||
redirect_to root_path, alert: service.error_message || 'Unable to accept invitation'
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error accepting family invitation: #{e.message}"
|
||||
redirect_to root_path, alert: 'An unexpected error occurred. Please try again later'
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @family, :manage_invitations?
|
||||
|
||||
|
|
@ -92,10 +63,6 @@ class Family::InvitationsController < ApplicationController
|
|||
redirect_to new_family_path, alert: 'You are not in a family' and return unless @family
|
||||
end
|
||||
|
||||
def set_invitation_by_id
|
||||
@invitation = FamilyInvitation.find_by!(token: params[:id])
|
||||
end
|
||||
|
||||
def set_invitation_by_id_and_family
|
||||
# For authenticated nested routes: /families/:family_id/invitations/:id
|
||||
# The :id param contains the token value
|
||||
|
|
|
|||
|
|
@ -3,8 +3,37 @@
|
|||
class Family::MembershipsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :ensure_family_feature_enabled!
|
||||
before_action :set_family
|
||||
before_action :set_family, except: %i[create]
|
||||
before_action :set_membership, only: %i[destroy]
|
||||
before_action :set_invitation, only: %i[create]
|
||||
|
||||
def create
|
||||
unless @invitation.pending?
|
||||
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(
|
||||
invitation: @invitation,
|
||||
user: current_user
|
||||
)
|
||||
|
||||
if service.call
|
||||
redirect_to family_path, notice: 'Welcome to the family!'
|
||||
else
|
||||
redirect_to root_path, alert: service.error_message || 'Unable to accept invitation'
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error accepting family invitation: #{e.message}"
|
||||
redirect_to root_path, alert: 'An unexpected error occurred. Please try again later'
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @membership
|
||||
|
|
@ -34,4 +63,8 @@ class Family::MembershipsController < ApplicationController
|
|||
def set_membership
|
||||
@membership = @family.family_memberships.find(params[:id])
|
||||
end
|
||||
|
||||
def set_invitation
|
||||
@invitation = Family::Invitation.find_by!(token: params[:token])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class Users::RegistrationsController < Devise::RegistrationsController
|
|||
def set_invitation
|
||||
return unless invitation_token.present?
|
||||
|
||||
@invitation = FamilyInvitation.find_by(token: invitation_token)
|
||||
@invitation = Family::Invitation.find_by(token: invitation_token)
|
||||
end
|
||||
|
||||
def self_hosted_mode?
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class Users::SessionsController < Devise::SessionsController
|
|||
|
||||
def after_sign_in_path_for(resource)
|
||||
if invitation_token.present?
|
||||
invitation = FamilyInvitation.find_by(token: invitation_token)
|
||||
invitation = Family::Invitation.find_by(token: invitation_token)
|
||||
|
||||
if invitation&.can_be_accepted?
|
||||
return family_invitation_path(invitation.token)
|
||||
|
|
@ -26,7 +26,7 @@ class Users::SessionsController < Devise::SessionsController
|
|||
def load_invitation_context
|
||||
return unless invitation_token.present?
|
||||
|
||||
@invitation = FamilyInvitation.find_by(token: invitation_token)
|
||||
@invitation = Family::Invitation.find_by(token: invitation_token)
|
||||
end
|
||||
|
||||
def invitation_token
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@ class FamilyInvitationsCleanupJob < ApplicationJob
|
|||
def perform
|
||||
Rails.logger.info 'Starting family invitations cleanup'
|
||||
|
||||
expired_count = FamilyInvitation.where(status: :pending)
|
||||
.where('expires_at < ?', Time.current)
|
||||
.update_all(status: :expired)
|
||||
expired_count = Family::Invitation.where(status: :pending)
|
||||
.where('expires_at < ?', Time.current)
|
||||
.update_all(status: :expired)
|
||||
|
||||
Rails.logger.info "Updated #{expired_count} expired family invitations"
|
||||
|
||||
cleanup_threshold = 30.days.ago
|
||||
deleted_count = FamilyInvitation.where(status: [:expired, :cancelled])
|
||||
.where('updated_at < ?', cleanup_threshold)
|
||||
.delete_all
|
||||
deleted_count = Family::Invitation.where(status: [:expired, :cancelled])
|
||||
.where('updated_at < ?', cleanup_threshold)
|
||||
.delete_all
|
||||
|
||||
Rails.logger.info "Deleted #{deleted_count} old family invitations"
|
||||
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ class FamilyMailer < ApplicationMailer
|
|||
@family = invitation.family
|
||||
@invited_by = invitation.invited_by
|
||||
@accept_url = family_invitation_url(@invitation.token)
|
||||
pp @accept_url
|
||||
|
||||
mail(
|
||||
to: @invitation.email,
|
||||
subject: "You've been invited to join #{@family.name} on Dawarich"
|
||||
subject: "🎉 You've been invited to join #{@family.name} on Dawarich!"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@ module UserFamily
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# Family associations
|
||||
has_one :family_membership, dependent: :destroy
|
||||
has_one :family_membership, dependent: :destroy, class_name: 'Family::Membership'
|
||||
has_one :family, through: :family_membership
|
||||
has_one :created_family, class_name: 'Family', foreign_key: 'creator_id', inverse_of: :creator, dependent: :destroy
|
||||
has_many :sent_family_invitations, class_name: 'FamilyInvitation', foreign_key: 'invited_by_id',
|
||||
has_many :sent_family_invitations, class_name: 'Family::Invitation', foreign_key: 'invited_by_id',
|
||||
inverse_of: :invited_by, dependent: :destroy
|
||||
|
||||
before_destroy :check_family_ownership
|
||||
|
|
@ -30,25 +29,14 @@ module UserFamily
|
|||
end
|
||||
|
||||
def family_sharing_enabled?
|
||||
# User must be in a family and have explicitly enabled location sharing
|
||||
return false unless in_family?
|
||||
|
||||
sharing_settings = settings.dig('family', 'location_sharing')
|
||||
return false if sharing_settings.blank?
|
||||
return false unless sharing_settings.is_a?(Hash)
|
||||
return false unless sharing_settings['enabled'] == true
|
||||
|
||||
# If it's a boolean (legacy support), return it
|
||||
return sharing_settings if [true, false].include?(sharing_settings)
|
||||
|
||||
# If it's time-limited sharing, check if it's still active
|
||||
if sharing_settings.is_a?(Hash)
|
||||
return false unless sharing_settings['enabled'] == true
|
||||
|
||||
# Check if sharing has an expiration
|
||||
expires_at = sharing_settings['expires_at']
|
||||
return expires_at.blank? || Time.parse(expires_at) > Time.current
|
||||
end
|
||||
|
||||
false
|
||||
expires_at = sharing_settings['expires_at']
|
||||
expires_at.blank? || Time.parse(expires_at).future?
|
||||
end
|
||||
|
||||
def update_family_location_sharing!(enabled, duration: nil)
|
||||
|
|
@ -60,21 +48,14 @@ module UserFamily
|
|||
if enabled
|
||||
sharing_config = { 'enabled' => true }
|
||||
|
||||
# Add expiration if duration is specified
|
||||
if duration.present?
|
||||
expiration_time = case duration
|
||||
when '1h'
|
||||
1.hour.from_now
|
||||
when '6h'
|
||||
6.hours.from_now
|
||||
when '12h'
|
||||
12.hours.from_now
|
||||
when '24h'
|
||||
24.hours.from_now
|
||||
when 'permanent'
|
||||
nil # No expiration
|
||||
else
|
||||
duration.to_i.hours.from_now if duration.to_i > 0
|
||||
when '1h' then 1.hour.from_now
|
||||
when '6h' then 6.hours.from_now
|
||||
when '12h' then 12.hours.from_now
|
||||
when '24h' then 24.hours.from_now
|
||||
when 'permanent' then nil
|
||||
else duration.to_i.hours.from_now if duration.to_i > 0
|
||||
end
|
||||
|
||||
sharing_config['expires_at'] = expiration_time.iso8601 if expiration_time
|
||||
|
|
@ -106,21 +87,21 @@ module UserFamily
|
|||
def latest_location_for_family
|
||||
return nil unless family_sharing_enabled?
|
||||
|
||||
# Use select to only fetch needed columns and limit to 1 for efficiency
|
||||
latest_point = points.select(:latitude, :longitude, :timestamp)
|
||||
.order(timestamp: :desc)
|
||||
.limit(1)
|
||||
.first
|
||||
latest_point =
|
||||
points.select(:lonlat, :timestamp)
|
||||
.order(timestamp: :desc)
|
||||
.limit(1)
|
||||
.first
|
||||
|
||||
return nil unless latest_point
|
||||
|
||||
{
|
||||
user_id: id,
|
||||
email: email,
|
||||
latitude: latest_point.latitude,
|
||||
longitude: latest_point.longitude,
|
||||
latitude: latest_point.lat,
|
||||
longitude: latest_point.lon,
|
||||
timestamp: latest_point.timestamp,
|
||||
updated_at: Time.at(latest_point.timestamp)
|
||||
updated_at: Time.zone.at(latest_point.timestamp)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Family < ApplicationRecord
|
||||
has_many :family_memberships, dependent: :destroy
|
||||
has_many :family_memberships, dependent: :destroy, class_name: 'Family::Membership'
|
||||
has_many :members, through: :family_memberships, source: :user
|
||||
has_many :family_invitations, dependent: :destroy
|
||||
has_many :family_invitations, dependent: :destroy, class_name: 'Family::Invitation'
|
||||
belongs_to :creator, class_name: 'User'
|
||||
|
||||
validates :name, presence: true, length: { maximum: 50 }
|
||||
|
|
|
|||
46
app/models/family/invitation.rb
Normal file
46
app/models/family/invitation.rb
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Family::Invitation < ApplicationRecord
|
||||
self.table_name = 'family_invitations'
|
||||
|
||||
EXPIRY_DAYS = 7
|
||||
|
||||
belongs_to :family
|
||||
belongs_to :invited_by, class_name: 'User'
|
||||
|
||||
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
||||
validates :token, presence: true, uniqueness: true
|
||||
validates :expires_at, :status, presence: true
|
||||
|
||||
enum :status, { pending: 0, accepted: 1, expired: 2, cancelled: 3 }
|
||||
|
||||
scope :active, -> { where(status: :pending).where('expires_at > ?', Time.current) }
|
||||
|
||||
before_validation :generate_token, :set_expiry, on: :create
|
||||
|
||||
after_create :clear_family_cache
|
||||
after_update :clear_family_cache, if: :saved_change_to_status?
|
||||
after_destroy :clear_family_cache
|
||||
|
||||
def expired?
|
||||
expires_at.past?
|
||||
end
|
||||
|
||||
def can_be_accepted?
|
||||
pending? && !expired?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_token
|
||||
self.token = SecureRandom.urlsafe_base64(32) if token.blank?
|
||||
end
|
||||
|
||||
def set_expiry
|
||||
self.expires_at = EXPIRY_DAYS.days.from_now if expires_at.blank?
|
||||
end
|
||||
|
||||
def clear_family_cache
|
||||
family&.clear_member_cache!
|
||||
end
|
||||
end
|
||||
23
app/models/family/membership.rb
Normal file
23
app/models/family/membership.rb
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Family::Membership < ApplicationRecord
|
||||
self.table_name = 'family_memberships'
|
||||
|
||||
belongs_to :family
|
||||
belongs_to :user
|
||||
|
||||
validates :user_id, presence: true, uniqueness: true
|
||||
validates :role, presence: true
|
||||
|
||||
enum :role, { owner: 0, member: 1 }
|
||||
|
||||
after_create :clear_family_cache
|
||||
after_update :clear_family_cache
|
||||
after_destroy :clear_family_cache
|
||||
|
||||
private
|
||||
|
||||
def clear_family_cache
|
||||
family&.clear_member_cache!
|
||||
end
|
||||
end
|
||||
22
app/policies/family/invitation_policy.rb
Normal file
22
app/policies/family/invitation_policy.rb
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Family::InvitationPolicy < ApplicationPolicy
|
||||
def show?
|
||||
# Public endpoint for invitation acceptance - no authentication required
|
||||
true
|
||||
end
|
||||
|
||||
def create?
|
||||
user.family == record.family && user.family_owner?
|
||||
end
|
||||
|
||||
def accept?
|
||||
# Users can accept invitations sent to their email
|
||||
user.email == record.email
|
||||
end
|
||||
|
||||
def destroy?
|
||||
# Only family owners can cancel invitations
|
||||
user.family == record.family && user.family_owner?
|
||||
end
|
||||
end
|
||||
23
app/policies/family/membership_policy.rb
Normal file
23
app/policies/family/membership_policy.rb
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Family::MembershipPolicy < ApplicationPolicy
|
||||
def show?
|
||||
user.family == record.family
|
||||
end
|
||||
|
||||
def update?
|
||||
# Users can update their own settings
|
||||
return true if user == record.user
|
||||
|
||||
# Family owners can update any member's settings
|
||||
user.family == record.family && user.family_owner?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
# Users can remove themselves (handled by family leave logic)
|
||||
return true if user == record.user
|
||||
|
||||
# Family owners can remove other members
|
||||
user.family == record.family && user.family_owner?
|
||||
end
|
||||
end
|
||||
|
|
@ -65,7 +65,7 @@ module Families
|
|||
end
|
||||
|
||||
def create_membership
|
||||
FamilyMembership.create!(
|
||||
Family::Membership.create!(
|
||||
family: invitation.family,
|
||||
user: user,
|
||||
role: :member
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ module Families
|
|||
end
|
||||
|
||||
def create_owner_membership
|
||||
FamilyMembership.create!(
|
||||
Family::Membership.create!(
|
||||
family: family,
|
||||
user: user,
|
||||
role: :owner
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ module Families
|
|||
end
|
||||
|
||||
def create_invitation
|
||||
@invitation = FamilyInvitation.create!(
|
||||
@invitation = Family::Invitation.create!(
|
||||
family: family,
|
||||
email: email,
|
||||
invited_by: invited_by
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@
|
|||
<div class="space-y-4">
|
||||
<% if user_signed_in? %>
|
||||
<!-- User is logged in, show accept button -->
|
||||
<%= link_to accept_family_invitation_path(@invitation.family, @invitation),
|
||||
<%= link_to accept_family_invitation_path(token: @invitation.token),
|
||||
method: :post,
|
||||
class: "btn btn-success btn-lg w-full text-lg shadow-lg" do %>
|
||||
✓ Accept Invitation & Join Family
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
|
||||
<p style="color: #6b7280; font-size: 14px; line-height: 1.6; text-align: center;">
|
||||
Best regards,<br>
|
||||
The Dawarich Team
|
||||
Evgenii from Dawarich
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,4 +19,4 @@ If you don't have a Dawarich account yet, you'll be able to create one when you
|
|||
If you didn't expect this invitation, you can safely ignore this email.
|
||||
|
||||
Best regards,
|
||||
The Dawarich Team
|
||||
Evgenii from Dawarich
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= link_to 'Family', new_family_path, class: "#{active_class?(new_family_path)}" %>
|
||||
<%= link_to 'Family<sup>α</sup>'.html_safe, new_family_path, class: "#{active_class?(new_family_path)}" %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
|
|
@ -79,14 +79,14 @@
|
|||
<div data-controller="family-navbar-indicator"
|
||||
data-family-navbar-indicator-enabled-value="<%= current_user.family_sharing_enabled? %>">
|
||||
<%= link_to family_path, class: "mx-1 #{active_class?(family_path)} flex items-center space-x-2" do %>
|
||||
<span>Family</span>
|
||||
<span>Family<sup>α</sup></span>
|
||||
<div data-family-navbar-indicator-target="indicator"
|
||||
class="w-2 h-2 <%= current_user.family_sharing_enabled? ? 'bg-green-500 animate-pulse' : 'bg-gray-400' %> rounded-full"
|
||||
title="<%= current_user.family_sharing_enabled? ? 'Location sharing enabled' : 'Location sharing disabled' %>"></div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= link_to 'Family', new_family_path, class: "mx-1 #{active_class?(new_family_path)}" %>
|
||||
<%= link_to 'Family<sup>α</sup>'.html_safe, new_family_path, class: "mx-1 #{active_class?(new_family_path)}" %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -38,17 +38,18 @@
|
|||
<%= current_user.total_cities %>
|
||||
</div>
|
||||
<div class="stat-title">Cities visited</div>
|
||||
<dialog id="cities_visited" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg">Cities visited</h3>
|
||||
<p class="py-4">
|
||||
<% current_user.cities_visited.each do |city| %>
|
||||
<p><%= city %></p>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="cities_visited" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg">Cities visited</h3>
|
||||
<p class="py-4">
|
||||
<% current_user.cities_visited.each do |city| %>
|
||||
<p><%= city %></p>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -62,15 +62,12 @@ Rails.application.routes.draw do
|
|||
resource :family, only: %i[show new create edit update destroy] do
|
||||
patch :update_location_sharing, on: :member
|
||||
|
||||
resources :invitations, except: %i[edit update], controller: 'family/invitations' do
|
||||
member do
|
||||
post :accept
|
||||
end
|
||||
end
|
||||
resources :invitations, except: %i[edit update], controller: 'family/invitations'
|
||||
resources :members, only: %i[destroy], controller: 'family/memberships'
|
||||
end
|
||||
|
||||
get 'invitations/:token', to: 'family/invitations#show', as: :public_invitation
|
||||
post 'family/memberships', to: 'family/memberships#create', as: :accept_family_invitation
|
||||
end
|
||||
|
||||
resources :points, only: %i[index] do
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :family_invitation do
|
||||
factory :family_invitation, class: 'Family::Invitation' do
|
||||
association :family
|
||||
association :invited_by, factory: :user
|
||||
sequence(:email) { |n| "invite#{n}@example.com" }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :family_membership do
|
||||
factory :family_membership, class: 'Family::Membership' do
|
||||
association :family
|
||||
association :user
|
||||
role { :member }
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe FamilyInvitation, type: :model do
|
||||
RSpec.describe Family::Invitation, type: :model do
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:family) }
|
||||
it { is_expected.to belong_to(:invited_by).class_name('User') }
|
||||
|
|
@ -44,9 +44,9 @@ RSpec.describe FamilyInvitation, type: :model do
|
|||
|
||||
describe '.active' do
|
||||
it 'returns only pending and non-expired invitations' do
|
||||
expect(FamilyInvitation.active).to include(pending_invitation)
|
||||
expect(FamilyInvitation.active).not_to include(expired_invitation)
|
||||
expect(FamilyInvitation.active).not_to include(accepted_invitation)
|
||||
expect(Family::Invitation.active).to include(pending_invitation)
|
||||
expect(Family::Invitation.active).not_to include(expired_invitation)
|
||||
expect(Family::Invitation.active).not_to include(accepted_invitation)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -63,7 +63,7 @@ RSpec.describe FamilyInvitation, type: :model do
|
|||
|
||||
it 'sets expiry date' do
|
||||
invitation.save
|
||||
expect(invitation.expires_at).to be_within(1.minute).of(FamilyInvitation::EXPIRY_DAYS.days.from_now)
|
||||
expect(invitation.expires_at).to be_within(1.minute).of(Family::Invitation::EXPIRY_DAYS.days.from_now)
|
||||
end
|
||||
|
||||
it 'does not override existing token' do
|
||||
|
|
@ -136,7 +136,7 @@ RSpec.describe FamilyInvitation, type: :model do
|
|||
|
||||
describe 'constants' do
|
||||
it 'defines EXPIRY_DAYS' do
|
||||
expect(FamilyInvitation::EXPIRY_DAYS).to eq(7)
|
||||
expect(Family::Invitation::EXPIRY_DAYS).to eq(7)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe FamilyMembership, type: :model do
|
||||
RSpec.describe Family::Membership, type: :model do
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:family) }
|
||||
it { is_expected.to belong_to(:user) }
|
||||
|
|
@ -107,8 +107,8 @@ RSpec.describe Family, type: :model do
|
|||
it 'destroys associated invitations when family is destroyed' do
|
||||
invitation = create(:family_invitation, family: family, invited_by: user)
|
||||
|
||||
expect { family.destroy }.to change(FamilyInvitation, :count).by(-1)
|
||||
expect(FamilyInvitation.find_by(id: invitation.id)).to be_nil
|
||||
expect { family.destroy }.to change(Family::Invitation, :count).by(-1)
|
||||
expect(Family::Invitation.find_by(id: invitation.id)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -118,8 +118,8 @@ RSpec.describe Family, type: :model do
|
|||
it 'destroys associated memberships when family is destroyed' do
|
||||
membership = create(:family_membership, family: family, user: user, role: :owner)
|
||||
|
||||
expect { family.destroy }.to change(FamilyMembership, :count).by(-1)
|
||||
expect(FamilyMembership.find_by(id: membership.id)).to be_nil
|
||||
expect { family.destroy }.to change(Family::Membership, :count).by(-1)
|
||||
expect(Family::Membership.find_by(id: membership.id)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ RSpec.describe User, 'family methods', type: :model do
|
|||
is_expected.to have_one(:created_family).class_name('Family').with_foreign_key('creator_id').dependent(:destroy)
|
||||
}
|
||||
it {
|
||||
is_expected.to have_many(:sent_family_invitations).class_name('FamilyInvitation').with_foreign_key('invited_by_id').dependent(:destroy)
|
||||
is_expected.to have_many(:sent_family_invitations).class_name('Family::Invitation').with_foreign_key('invited_by_id').dependent(:destroy)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -119,7 +119,7 @@ RSpec.describe User, 'family methods', type: :model do
|
|||
end
|
||||
|
||||
it 'destroys associated invitations when user is destroyed' do
|
||||
expect { user.destroy }.to change(FamilyInvitation, :count).by(-1)
|
||||
expect { user.destroy }.to change(Family::Invitation, :count).by(-1)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ RSpec.describe User, 'family methods', type: :model do
|
|||
end
|
||||
|
||||
it 'destroys associated membership when user is destroyed' do
|
||||
expect { user.destroy }.to change(FamilyMembership, :count).by(-1)
|
||||
expect { user.destroy }.to change(Family::Membership, :count).by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ RSpec.describe 'Family', type: :request do
|
|||
it 'creates a family membership for the user' do
|
||||
expect do
|
||||
post '/family', params: valid_attributes
|
||||
end.to change(FamilyMembership, :count).by(1)
|
||||
end.to change(Family::Membership, :count).by(1)
|
||||
end
|
||||
|
||||
it 'redirects to the new family with success message' do
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ RSpec.describe 'Family::Invitations', type: :request do
|
|||
it 'creates a new invitation' do
|
||||
expect do
|
||||
post "/family/invitations", params: valid_params
|
||||
end.to change(FamilyInvitation, :count).by(1)
|
||||
end.to change(Family::Invitation, :count).by(1)
|
||||
end
|
||||
|
||||
it 'redirects with success message' do
|
||||
|
|
@ -112,7 +112,7 @@ RSpec.describe 'Family::Invitations', type: :request do
|
|||
invitation # create the existing invitation
|
||||
expect do
|
||||
post "/family/invitations", params: duplicate_params
|
||||
end.not_to change(FamilyInvitation, :count)
|
||||
end.not_to change(Family::Invitation, :count)
|
||||
end
|
||||
|
||||
it 'redirects with error message' do
|
||||
|
|
@ -161,81 +161,6 @@ RSpec.describe 'Family::Invitations', type: :request do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'POST /family/invitations/:id/accept' do
|
||||
let(:invitee) { create(:user) }
|
||||
let(:invitee_invitation) { create(:family_invitation, family: family, invited_by: user, email: invitee.email) }
|
||||
|
||||
context 'with valid invitation and user' do
|
||||
before { sign_in invitee }
|
||||
|
||||
it 'accepts the invitation' do
|
||||
expect do
|
||||
post "/family/invitations/#{invitee_invitation.token}/accept"
|
||||
end.to change { invitee.reload.family }.from(nil).to(family)
|
||||
end
|
||||
|
||||
it 'redirects with success message' do
|
||||
post "/family/invitations/#{invitee_invitation.token}/accept"
|
||||
expect(response).to redirect_to(family_path)
|
||||
follow_redirect!
|
||||
expect(response.body).to include('Welcome to the family!')
|
||||
end
|
||||
|
||||
it 'marks invitation as accepted' do
|
||||
post "/family/invitations/#{invitee_invitation.token}/accept"
|
||||
invitee_invitation.reload
|
||||
expect(invitee_invitation.status).to eq('accepted')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is already in a family' do
|
||||
let(:other_family) { create(:family) }
|
||||
|
||||
before do
|
||||
create(:family_membership, user: invitee, family: other_family, role: :member)
|
||||
sign_in invitee
|
||||
end
|
||||
|
||||
it 'does not accept the invitation' do
|
||||
expect do
|
||||
post "/family/invitations/#{invitee_invitation.token}/accept"
|
||||
end.not_to(change { invitee.reload.family })
|
||||
end
|
||||
|
||||
it 'redirects with error message' do
|
||||
post "/family/invitations/#{invitee_invitation.token}/accept"
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:alert]).to include('You must leave your current family before joining a new one')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invitation is expired' do
|
||||
before do
|
||||
invitee_invitation.update!(expires_at: 1.day.ago)
|
||||
sign_in invitee
|
||||
end
|
||||
|
||||
it 'does not accept the invitation' do
|
||||
expect do
|
||||
post "/family/invitations/#{invitee_invitation.token}/accept"
|
||||
end.not_to(change { invitee.reload.family })
|
||||
end
|
||||
|
||||
it 'redirects with error message' do
|
||||
post "/family/invitations/#{invitee_invitation.token}/accept"
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:alert]).to include('This invitation is no longer valid or has expired')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not authenticated' do
|
||||
it 'redirects to login' do
|
||||
post "/family/invitations/#{invitee_invitation.token}/accept"
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /family/invitations/:id' do
|
||||
before { sign_in user }
|
||||
|
||||
|
|
@ -294,7 +219,7 @@ RSpec.describe 'Family::Invitations', type: :request do
|
|||
}
|
||||
expect(response).to redirect_to(family_path)
|
||||
|
||||
created_invitation = FamilyInvitation.last
|
||||
created_invitation = Family::Invitation.last
|
||||
expect(created_invitation.email).to eq(invitee.email)
|
||||
|
||||
# 2. Invitee views public invitation page
|
||||
|
|
@ -304,7 +229,7 @@ RSpec.describe 'Family::Invitations', type: :request do
|
|||
|
||||
# 3. Invitee accepts invitation
|
||||
sign_in invitee
|
||||
post "/family/invitations/#{created_invitation.token}/accept"
|
||||
post accept_family_invitation_path(token: created_invitation.token)
|
||||
expect(response).to redirect_to(family_path)
|
||||
|
||||
# 4. Verify invitee is now in family
|
||||
|
|
|
|||
|
|
@ -15,12 +15,89 @@ RSpec.describe 'Family::Memberships', type: :request do
|
|||
sign_in user
|
||||
end
|
||||
|
||||
describe 'POST /family/memberships' do
|
||||
let(:invitee) { create(:user) }
|
||||
let(:invitee_invitation) { create(:family_invitation, family: family, invited_by: user, email: invitee.email) }
|
||||
|
||||
context 'with valid invitation and user' do
|
||||
before { sign_in invitee }
|
||||
|
||||
it 'accepts the invitation' do
|
||||
expect do
|
||||
post accept_family_invitation_path(token: invitee_invitation.token)
|
||||
end.to change { invitee.reload.family }.from(nil).to(family)
|
||||
end
|
||||
|
||||
it 'redirects with success message' do
|
||||
post accept_family_invitation_path(token: invitee_invitation.token)
|
||||
expect(response).to redirect_to(family_path)
|
||||
follow_redirect!
|
||||
expect(response.body).to include('Welcome to the family!')
|
||||
end
|
||||
|
||||
it 'marks invitation as accepted' do
|
||||
post accept_family_invitation_path(token: invitee_invitation.token)
|
||||
invitee_invitation.reload
|
||||
expect(invitee_invitation.status).to eq('accepted')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is already in a family' do
|
||||
let(:other_family) { create(:family) }
|
||||
|
||||
before do
|
||||
create(:family_membership, user: invitee, family: other_family, role: :member)
|
||||
sign_in invitee
|
||||
end
|
||||
|
||||
it 'does not accept the invitation' do
|
||||
expect do
|
||||
post accept_family_invitation_path(token: invitee_invitation.token)
|
||||
end.not_to(change { invitee.reload.family })
|
||||
end
|
||||
|
||||
it 'redirects with error message' do
|
||||
post accept_family_invitation_path(token: invitee_invitation.token)
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:alert]).to include('You must leave your current family before joining a new one')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invitation is expired' do
|
||||
before do
|
||||
invitee_invitation.update!(expires_at: 1.day.ago)
|
||||
sign_in invitee
|
||||
end
|
||||
|
||||
it 'does not accept the invitation' do
|
||||
expect do
|
||||
post accept_family_invitation_path(token: invitee_invitation.token)
|
||||
end.not_to(change { invitee.reload.family })
|
||||
end
|
||||
|
||||
it 'redirects with error message' do
|
||||
post accept_family_invitation_path(token: invitee_invitation.token)
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:alert]).to include('This invitation is no longer valid or has expired')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not authenticated' do
|
||||
before { sign_out user }
|
||||
|
||||
it 'redirects to login' do
|
||||
post accept_family_invitation_path(token: invitee_invitation.token)
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /family/members/:id' do
|
||||
context 'when removing a regular member' do
|
||||
it 'removes the member from the family' do
|
||||
expect do
|
||||
delete "/family/members/#{member_membership.id}"
|
||||
end.to change(FamilyMembership, :count).by(-1)
|
||||
end.to change(Family::Membership, :count).by(-1)
|
||||
end
|
||||
|
||||
it 'redirects with success message' do
|
||||
|
|
@ -41,7 +118,7 @@ RSpec.describe 'Family::Memberships', type: :request do
|
|||
it 'does not remove the owner' do
|
||||
expect do
|
||||
delete "/family/members/#{owner_membership.id}"
|
||||
end.not_to change(FamilyMembership, :count)
|
||||
end.not_to change(Family::Membership, :count)
|
||||
end
|
||||
|
||||
it 'redirects with error message explaining owners must delete family' do
|
||||
|
|
@ -56,7 +133,7 @@ RSpec.describe 'Family::Memberships', type: :request do
|
|||
|
||||
expect do
|
||||
delete "/family/members/#{owner_membership.id}"
|
||||
end.not_to change(FamilyMembership, :count)
|
||||
end.not_to change(Family::Membership, :count)
|
||||
|
||||
expect(response).to redirect_to(family_path)
|
||||
follow_redirect!
|
||||
|
|
@ -149,7 +226,7 @@ RSpec.describe 'Family::Memberships', type: :request do
|
|||
# Try to remove owner - should be prevented
|
||||
expect do
|
||||
delete "/family/members/#{owner_membership.id}"
|
||||
end.not_to change(FamilyMembership, :count)
|
||||
end.not_to change(Family::Membership, :count)
|
||||
|
||||
expect(response).to redirect_to(family_path)
|
||||
expect(user.reload.family).to eq(family)
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ RSpec.describe 'Family Workflows', type: :request do
|
|||
|
||||
# User2 accepts invitation
|
||||
sign_in user2
|
||||
post "/family/invitations/#{invitation.token}/accept"
|
||||
post accept_family_invitation_path(token: invitation.token)
|
||||
expect(response).to redirect_to(family_path)
|
||||
|
||||
expect(user2.reload.family).to eq(family)
|
||||
|
|
@ -71,7 +71,7 @@ RSpec.describe 'Family Workflows', type: :request do
|
|||
|
||||
# Step 5: User3 accepts invitation
|
||||
sign_in user3
|
||||
post "/family/invitations/#{invitation2.token}/accept"
|
||||
post accept_family_invitation_path(token: invitation2.token)
|
||||
|
||||
expect(user3.reload.family).to eq(family)
|
||||
expect(family.reload.members.count).to eq(3)
|
||||
|
|
@ -108,7 +108,7 @@ RSpec.describe 'Family Workflows', type: :request do
|
|||
|
||||
# User2 tries to accept expired invitation
|
||||
sign_in user2
|
||||
post "/family/invitations/#{invitation.token}/accept"
|
||||
post accept_family_invitation_path(token: invitation.token)
|
||||
expect(response).to redirect_to(root_path)
|
||||
|
||||
expect(user2.reload.family).to be_nil
|
||||
|
|
@ -127,12 +127,12 @@ RSpec.describe 'Family Workflows', type: :request do
|
|||
it 'prevents users from joining multiple families' do
|
||||
# User3 accepts invitation to Family 1
|
||||
sign_in user3
|
||||
post "/family/invitations/#{invitation1.token}/accept"
|
||||
post accept_family_invitation_path(token: invitation1.token)
|
||||
expect(response).to redirect_to(family_path)
|
||||
expect(user3.family).to eq(family1)
|
||||
|
||||
# User3 tries to accept invitation to Family 2
|
||||
post "/family/invitations/#{invitation2.token}/accept"
|
||||
post accept_family_invitation_path(token: invitation2.token)
|
||||
expect(response).to redirect_to(root_path)
|
||||
expect(flash[:alert]).to include('You must leave your current family')
|
||||
|
||||
|
|
@ -268,7 +268,7 @@ RSpec.describe 'Family Workflows', type: :request do
|
|||
post "/family/invitations", params: {
|
||||
family_invitation: { email: 'newuser@example.com' }
|
||||
}
|
||||
end.to change(FamilyInvitation, :count).by(1)
|
||||
end.to change(Family::Invitation, :count).by(1)
|
||||
|
||||
invitation = family.family_invitations.find_by(email: 'newuser@example.com')
|
||||
expect(invitation.email).to eq('newuser@example.com')
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ RSpec.describe Families::AcceptInvitation do
|
|||
describe '#call' do
|
||||
context 'when invitation can be accepted' do
|
||||
it 'creates membership for user' do
|
||||
expect { service.call }.to change(FamilyMembership, :count).by(1)
|
||||
membership = invitee.family_membership
|
||||
expect { service.call }.to change(Family::Membership, :count).by(1)
|
||||
membership = invitee.reload.family_membership
|
||||
expect(membership.family).to eq(family)
|
||||
expect(membership.role).to eq('member')
|
||||
end
|
||||
|
|
@ -47,7 +47,7 @@ RSpec.describe Families::AcceptInvitation do
|
|||
end
|
||||
|
||||
it 'does not create membership' do
|
||||
expect { service.call }.not_to change(FamilyMembership, :count)
|
||||
expect { service.call }.not_to change(Family::Membership, :count)
|
||||
end
|
||||
|
||||
it 'sets appropriate error message' do
|
||||
|
|
@ -68,7 +68,7 @@ RSpec.describe Families::AcceptInvitation do
|
|||
end
|
||||
|
||||
it 'does not create membership' do
|
||||
expect { service.call }.not_to change(FamilyMembership, :count)
|
||||
expect { service.call }.not_to change(Family::Membership, :count)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ RSpec.describe Families::AcceptInvitation do
|
|||
end
|
||||
|
||||
it 'does not create membership' do
|
||||
expect { service.call }.not_to change(FamilyMembership, :count)
|
||||
expect { service.call }.not_to change(Family::Membership, :count)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ RSpec.describe Families::AcceptInvitation do
|
|||
end
|
||||
|
||||
it 'does not create membership' do
|
||||
expect { service.call }.not_to change(FamilyMembership, :count)
|
||||
expect { service.call }.not_to change(Family::Membership, :count)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -108,7 +108,7 @@ RSpec.describe Families::AcceptInvitation do
|
|||
end
|
||||
|
||||
it 'does not create membership' do
|
||||
expect { service.call }.not_to change(FamilyMembership, :count)
|
||||
expect { service.call }.not_to change(Family::Membership, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ RSpec.describe Families::Create do
|
|||
|
||||
it 'creates owner membership' do
|
||||
service.call
|
||||
membership = user.family_membership
|
||||
membership = user.reload.family_membership
|
||||
expect(membership.role).to eq('owner')
|
||||
expect(membership.family).to eq(service.family)
|
||||
end
|
||||
|
|
@ -38,7 +38,7 @@ RSpec.describe Families::Create do
|
|||
end
|
||||
|
||||
it 'does not create a membership' do
|
||||
expect { service.call }.not_to change(FamilyMembership, :count)
|
||||
expect { service.call }.not_to change(Family::Membership, :count)
|
||||
end
|
||||
|
||||
it 'sets appropriate error message' do
|
||||
|
|
@ -65,7 +65,7 @@ RSpec.describe Families::Create do
|
|||
end
|
||||
|
||||
it 'does not create a membership' do
|
||||
expect { service.call }.not_to change(FamilyMembership, :count)
|
||||
expect { service.call }.not_to change(Family::Membership, :count)
|
||||
end
|
||||
|
||||
it 'sets appropriate error message' do
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ RSpec.describe Families::Invite do
|
|||
describe '#call' do
|
||||
context 'when invitation is valid' do
|
||||
it 'creates an invitation' do
|
||||
expect { service.call }.to change(FamilyInvitation, :count).by(1)
|
||||
expect { service.call }.to change(Family::Invitation, :count).by(1)
|
||||
|
||||
invitation = owner.sent_family_invitations.last
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ RSpec.describe Families::Invite do
|
|||
end
|
||||
|
||||
it 'does not create invitation' do
|
||||
expect { service.call }.not_to change(FamilyInvitation, :count)
|
||||
expect { service.call }.not_to change(Family::Invitation, :count)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ RSpec.describe Families::Invite do
|
|||
end
|
||||
|
||||
it 'does not create invitation' do
|
||||
expect { service.call }.not_to change(FamilyInvitation, :count)
|
||||
expect { service.call }.not_to change(Family::Invitation, :count)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ RSpec.describe Families::Invite do
|
|||
end
|
||||
|
||||
it 'does not create invitation' do
|
||||
expect { service.call }.not_to change(FamilyInvitation, :count)
|
||||
expect { service.call }.not_to change(Family::Invitation, :count)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ RSpec.describe Families::Invite do
|
|||
end
|
||||
|
||||
it 'does not create another invitation' do
|
||||
expect { service.call }.not_to change(FamilyInvitation, :count)
|
||||
expect { service.call }.not_to change(Family::Invitation, :count)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ RSpec.describe Families::Memberships::Destroy do
|
|||
it 'removes the membership' do
|
||||
result = service.call
|
||||
expect(result).to be_truthy, "Expected service to succeed but got error: #{service.error_message}"
|
||||
expect(FamilyMembership.count).to eq(1) # Only owner should remain
|
||||
expect(Family::Membership.count).to eq(1) # Only owner should remain
|
||||
expect(member.reload.family_membership).to be_nil
|
||||
end
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ RSpec.describe Families::Memberships::Destroy do
|
|||
let!(:membership) { create(:family_membership, user: user, family: family, role: :owner) }
|
||||
|
||||
it 'prevents owner from leaving' do
|
||||
expect { service.call }.not_to change(FamilyMembership, :count)
|
||||
expect { service.call }.not_to change(Family::Membership, :count)
|
||||
expect(user.reload.family_membership).to be_present
|
||||
end
|
||||
|
||||
|
|
@ -75,7 +75,7 @@ RSpec.describe Families::Memberships::Destroy do
|
|||
end
|
||||
|
||||
it 'does not remove membership' do
|
||||
expect { service.call }.not_to change(FamilyMembership, :count)
|
||||
expect { service.call }.not_to change(Family::Membership, :count)
|
||||
expect(user.reload.family_membership).to be_present
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue