Fix some tests

This commit is contained in:
Eugene Burmakin 2025-09-27 14:26:08 +02:00
parent f0f0f20200
commit f817e3513c
4 changed files with 110 additions and 33 deletions

View file

@ -49,6 +49,7 @@ class FamilyInvitationsController < ApplicationController
) )
if service.call if service.call
current_user.reload
redirect_to family_path(current_user.family), notice: 'Welcome to the family!' redirect_to family_path(current_user.family), notice: 'Welcome to the family!'
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'

View file

@ -4,7 +4,7 @@ module Families
class Invite class Invite
include ActiveModel::Validations include ActiveModel::Validations
attr_reader :family, :email, :invited_by, :invitation attr_reader :family, :email, :invited_by, :invitation, :errors
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP } validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
@ -12,6 +12,7 @@ module Families
@family = family @family = family
@email = email.downcase.strip @email = email.downcase.strip
@invited_by = invited_by @invited_by = invited_by
@errors = {}
end end
def call def call
@ -25,21 +26,38 @@ module Families
end end
true true
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid => e
@errors[:base] = e.message
false false
end end
def error_message
return errors.values.first if errors.any?
return validation_error_message unless valid?
'Failed to send invitation'
end
private private
def validation_error_message
errors.full_messages.first || 'Invalid invitation data'
end
def invite_sendable? def invite_sendable?
return false unless invited_by.family_owner? return add_error_and_false(:invited_by, 'You must be a family owner to send invitations') unless invited_by.family_owner?
return false if family.members.count >= Family::MAX_MEMBERS return add_error_and_false(:family, 'Family is full') if family.members.count >= Family::MAX_MEMBERS
return false if user_already_in_family? return add_error_and_false(:email, 'User is already in a family') if user_already_in_family?
return false if pending_invitation_exists? return add_error_and_false(:email, 'Invitation already sent to this email') if pending_invitation_exists?
true true
end end
def add_error_and_false(attribute, message)
@errors[attribute] = message
false
end
def user_already_in_family? def user_already_in_family?
User.joins(:family_membership) User.joins(:family_membership)
.where(email: email) .where(email: email)
@ -71,4 +89,4 @@ module Families
) )
end end
end end
end end

View file

@ -0,0 +1,57 @@
<div class="container mx-auto px-4 py-8">
<div class="max-w-4xl mx-auto">
<div class="bg-white shadow rounded-lg p-6">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-gray-900">
<%= t('family_invitations.index.title', default: 'Family Invitations') %>
</h1>
<%= link_to family_path(@family),
class: "bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md font-medium transition-colors duration-200" do %>
<%= t('family_invitations.index.back_to_family', default: 'Back to Family') %>
<% end %>
</div>
<% if @pending_invitations.any? %>
<div class="space-y-4">
<% @pending_invitations.each do |invitation| %>
<div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div>
<div class="font-medium text-gray-900"><%= invitation.email %></div>
<div class="text-sm text-gray-500">
<%= t('family_invitations.index.invited_on', default: 'Invited') %>
<%= invitation.created_at.strftime('%B %d, %Y') %>
</div>
<div class="text-xs text-gray-400">
<%= t('family_invitations.index.expires_on', default: 'Expires') %>
<%= invitation.expires_at.strftime('%B %d, %Y at %I:%M %p') %>
</div>
</div>
<div class="flex space-x-2">
<%= link_to public_invitation_path(invitation.token),
class: "text-blue-600 hover:text-blue-800 text-sm font-medium" do %>
<%= t('family_invitations.index.view_invitation', default: 'View') %>
<% end %>
<% if policy(@family).manage_invitations? %>
<%= link_to family_invitation_path(@family, invitation),
method: :delete,
confirm: t('family_invitations.index.cancel_confirm', default: 'Are you sure you want to cancel this invitation?'),
class: "text-red-600 hover:text-red-800 text-sm font-medium" do %>
<%= t('family_invitations.index.cancel', default: 'Cancel') %>
<% end %>
<% end %>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="text-center py-8">
<p class="text-gray-500 text-lg">
<%= t('family_invitations.index.no_invitations', default: 'No pending invitations') %>
</p>
</div>
<% end %>
</div>
</div>
</div>

View file

@ -120,18 +120,19 @@ RSpec.describe 'Family Invitations', type: :request do
post "/families/#{family.id}/invitations", params: duplicate_params post "/families/#{family.id}/invitations", params: duplicate_params
expect(response).to redirect_to(family_path(family)) expect(response).to redirect_to(family_path(family))
follow_redirect! follow_redirect!
expect(response.body).to include('Failed to send invitation') expect(response.body).to include('Invitation already sent to this email')
end end
end end
context 'when user is not the owner' do context 'when user is not the owner' do
before { membership.update!(role: :member) } before { membership.update!(role: :member) }
it 'returns forbidden' do it 'redirects due to authorization failure' do
post "/families/#{family.id}/invitations", params: { post "/families/#{family.id}/invitations", params: {
family_invitation: { email: 'test@example.com' } family_invitation: { email: 'test@example.com' }
} }
expect(response).to have_http_status(:forbidden) expect(response).to have_http_status(:see_other)
expect(flash[:alert]).to include('not authorized')
end end
end end
@ -162,27 +163,28 @@ RSpec.describe 'Family Invitations', type: :request do
describe 'POST /families/:family_id/invitations/:id/accept' do describe 'POST /families/:family_id/invitations/:id/accept' do
let(:invitee) { create(:user) } 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 context 'with valid invitation and user' do
before { sign_in invitee } before { sign_in invitee }
it 'accepts the invitation' do it 'accepts the invitation' do
expect do expect do
post "/families/#{family.id}/invitations/#{invitation.id}/accept" post "/families/#{family.id}/invitations/#{invitee_invitation.token}/accept"
end.to change { invitee.reload.family }.from(nil).to(family) end.to change { invitee.reload.family }.from(nil).to(family)
end end
it 'redirects with success message' do it 'redirects with success message' do
post "/families/#{family.id}/invitations/#{invitation.id}/accept" post "/families/#{family.id}/invitations/#{invitee_invitation.token}/accept"
expect(response).to redirect_to(family_path(family)) expect(response).to redirect_to(family_path(family))
follow_redirect! follow_redirect!
expect(response.body).to include('Welcome to the family!') expect(response.body).to include('Welcome to the family!')
end end
it 'marks invitation as accepted' do it 'marks invitation as accepted' do
post "/families/#{family.id}/invitations/#{invitation.id}/accept" post "/families/#{family.id}/invitations/#{invitee_invitation.token}/accept"
invitation.reload invitee_invitation.reload
expect(invitation.status).to eq('accepted') expect(invitee_invitation.status).to eq('accepted')
end end
end end
@ -196,41 +198,39 @@ RSpec.describe 'Family Invitations', type: :request do
it 'does not accept the invitation' do it 'does not accept the invitation' do
expect do expect do
post "/families/#{family.id}/invitations/#{invitation.id}/accept" post "/families/#{family.id}/invitations/#{invitee_invitation.token}/accept"
end.not_to(change { invitee.reload.family }) end.not_to(change { invitee.reload.family })
end end
it 'redirects with error message' do it 'redirects with error message' do
post "/families/#{family.id}/invitations/#{invitation.id}/accept" post "/families/#{family.id}/invitations/#{invitee_invitation.token}/accept"
expect(response).to redirect_to(root_path) expect(response).to redirect_to(root_path)
follow_redirect! expect(flash[:alert]).to include('You must leave your current family before joining a new one')
expect(response.body).to include('You must leave your current family')
end end
end end
context 'when invitation is expired' do context 'when invitation is expired' do
before do before do
invitation.update!(expires_at: 1.day.ago) invitee_invitation.update!(expires_at: 1.day.ago)
sign_in invitee sign_in invitee
end end
it 'does not accept the invitation' do it 'does not accept the invitation' do
expect do expect do
post "/families/#{family.id}/invitations/#{invitation.id}/accept" post "/families/#{family.id}/invitations/#{invitee_invitation.token}/accept"
end.not_to(change { invitee.reload.family }) end.not_to(change { invitee.reload.family })
end end
it 'redirects with error message' do it 'redirects with error message' do
post "/families/#{family.id}/invitations/#{invitation.id}/accept" post "/families/#{family.id}/invitations/#{invitee_invitation.token}/accept"
expect(response).to redirect_to(root_path) expect(response).to redirect_to(root_path)
follow_redirect! expect(flash[:alert]).to include('This invitation is no longer valid or has expired')
expect(response.body).to include('expired')
end end
end end
context 'when not authenticated' do context 'when not authenticated' do
it 'redirects to login' do it 'redirects to login' do
post "/families/#{family.id}/invitations/#{invitation.id}/accept" post "/families/#{family.id}/invitations/#{invitee_invitation.token}/accept"
expect(response).to redirect_to(new_user_session_path) expect(response).to redirect_to(new_user_session_path)
end end
end end
@ -240,14 +240,14 @@ RSpec.describe 'Family Invitations', type: :request do
before { sign_in user } before { sign_in user }
it 'cancels the invitation' do it 'cancels the invitation' do
delete "/families/#{family.id}/invitations/#{invitation.id}" delete "/families/#{family.id}/invitations/#{invitation.token}"
invitation.reload invitation.reload
expect(invitation.status).to eq('cancelled') expect(invitation.status).to eq('cancelled')
expect(response).to redirect_to(family_path(family)) expect(response).to redirect_to(family_path(family))
end end
it 'redirects with success message' do it 'redirects with success message' do
delete "/families/#{family.id}/invitations/#{invitation.id}" delete "/families/#{family.id}/invitations/#{invitation.token}"
expect(response).to redirect_to(family_path(family)) expect(response).to redirect_to(family_path(family))
follow_redirect! follow_redirect!
expect(response.body).to include('Invitation cancelled') expect(response.body).to include('Invitation cancelled')
@ -256,9 +256,10 @@ RSpec.describe 'Family Invitations', type: :request do
context 'when user is not the owner' do context 'when user is not the owner' do
before { membership.update!(role: :member) } before { membership.update!(role: :member) }
it 'returns forbidden' do it 'redirects due to authorization failure' do
delete "/families/#{family.id}/invitations/#{invitation.id}" delete "/families/#{family.id}/invitations/#{invitation.token}"
expect(response).to have_http_status(:forbidden) expect(response).to have_http_status(:see_other)
expect(flash[:alert]).to include('not authorized')
end end
end end
@ -268,7 +269,7 @@ RSpec.describe 'Family Invitations', type: :request do
before { sign_in outsider } before { sign_in outsider }
it 'redirects to families index' do it 'redirects to families index' do
delete "/families/#{family.id}/invitations/#{invitation.id}" delete "/families/#{family.id}/invitations/#{invitation.token}"
expect(response).to redirect_to(families_path) expect(response).to redirect_to(families_path)
end end
end end
@ -277,7 +278,7 @@ RSpec.describe 'Family Invitations', type: :request do
before { sign_out user } before { sign_out user }
it 'redirects to login' do it 'redirects to login' do
delete "/families/#{family.id}/invitations/#{invitation.id}" delete "/families/#{family.id}/invitations/#{invitation.token}"
expect(response).to redirect_to(new_user_session_path) expect(response).to redirect_to(new_user_session_path)
end end
end end
@ -304,7 +305,7 @@ RSpec.describe 'Family Invitations', type: :request do
# 3. Invitee accepts invitation # 3. Invitee accepts invitation
sign_in invitee sign_in invitee
post "/families/#{family.id}/invitations/#{created_invitation.id}/accept" post "/families/#{family.id}/invitations/#{created_invitation.token}/accept"
expect(response).to redirect_to(family_path(family)) expect(response).to redirect_to(family_path(family))
# 4. Verify invitee is now in family # 4. Verify invitee is now in family