hensei-api/spec/models/crew_invitation_spec.rb
Justin Edmund b75a905e2e add crew invitations system
- create crew_invitations table with status enum
- add CrewInvitation model with accept/reject flow
- add CrewInvitationsController for send/accept/reject
- add invitation error classes
- add invitation routes nested under crews
- add pending invitations endpoint for current user
- 38 passing specs for model and controller
2025-12-03 23:06:07 -08:00

181 lines
5.8 KiB
Ruby

require 'rails_helper'
RSpec.describe CrewInvitation, type: :model do
let(:crew) { create(:crew) }
let(:captain) { create(:user) }
let(:invitee) { create(:user) }
before do
create(:crew_membership, :captain, crew: crew, user: captain)
end
describe 'associations' do
it { is_expected.to belong_to(:crew) }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:invited_by).class_name('User') }
end
describe 'validations' do
context 'when user is already in a crew' do
let(:other_crew) { create(:crew) }
before do
create(:crew_membership, crew: other_crew, user: invitee)
end
it 'is invalid' do
invitation = build(:crew_invitation, crew: crew, user: invitee, invited_by: captain)
expect(invitation).not_to be_valid
expect(invitation.errors[:user]).to include('is already in a crew')
end
end
context 'when inviter is not an officer' do
let(:regular_member) { create(:user) }
before do
create(:crew_membership, crew: crew, user: regular_member)
end
it 'is invalid' do
invitation = build(:crew_invitation, crew: crew, user: invitee, invited_by: regular_member)
expect(invitation).not_to be_valid
expect(invitation.errors[:invited_by]).to include('must be an officer of the crew')
end
end
context 'when inviter is captain' do
it 'is valid' do
invitation = build(:crew_invitation, crew: crew, user: invitee, invited_by: captain)
expect(invitation).to be_valid
end
end
context 'when inviter is vice captain' do
let(:vice_captain) { create(:user) }
before do
create(:crew_membership, :vice_captain, crew: crew, user: vice_captain)
end
it 'is valid' do
invitation = build(:crew_invitation, crew: crew, user: invitee, invited_by: vice_captain)
expect(invitation).to be_valid
end
end
end
describe '#accept!' do
let(:invitation) { create(:crew_invitation, crew: crew, user: invitee, invited_by: captain) }
it 'creates a crew membership' do
expect { invitation.accept! }.to change(CrewMembership, :count).by(1)
end
it 'sets the invitation status to accepted' do
invitation.accept!
expect(invitation.reload.status).to eq('accepted')
end
it 'makes the user a member of the crew' do
invitation.accept!
expect(invitee.reload.crew).to eq(crew)
expect(invitee.crew_role).to eq('member')
end
context 'when invitation is expired by time' do
let(:invitation) { create(:crew_invitation, :expired_by_time, crew: crew, user: invitee, invited_by: captain) }
it 'raises InvitationExpiredError' do
expect { invitation.accept! }.to raise_error(CrewErrors::InvitationExpiredError)
end
end
context 'when invitation status is expired' do
let(:invitation) { create(:crew_invitation, :expired, crew: crew, user: invitee, invited_by: captain) }
it 'raises InvitationExpiredError' do
expect { invitation.accept! }.to raise_error(CrewErrors::InvitationExpiredError)
end
end
context 'when user joins another crew after invitation was created' do
let(:other_crew) { create(:crew) }
it 'raises AlreadyInCrewError' do
# Create invitation first while user is not in any crew
inv = invitation
# Then user joins another crew
create(:crew_membership, :captain, crew: other_crew)
create(:crew_membership, crew: other_crew, user: invitee)
# Now accepting should fail
expect { inv.accept! }.to raise_error(CrewErrors::AlreadyInCrewError)
end
end
end
describe '#reject!' do
let(:invitation) { create(:crew_invitation, crew: crew, user: invitee, invited_by: captain) }
it 'sets the invitation status to rejected' do
invitation.reject!
expect(invitation.reload.status).to eq('rejected')
end
context 'when invitation is already expired' do
let(:invitation) { create(:crew_invitation, :expired, crew: crew, user: invitee, invited_by: captain) }
it 'raises InvitationExpiredError' do
expect { invitation.reject! }.to raise_error(CrewErrors::InvitationExpiredError)
end
end
end
describe '#active?' do
context 'when pending and not expired' do
let(:invitation) { create(:crew_invitation, crew: crew, user: invitee, invited_by: captain) }
it 'returns true' do
expect(invitation.active?).to be true
end
end
context 'when pending but expired by time' do
let(:invitation) { create(:crew_invitation, :expired_by_time, crew: crew, user: invitee, invited_by: captain) }
it 'returns false' do
expect(invitation.active?).to be false
end
end
context 'when already accepted' do
let(:invitation) { create(:crew_invitation, :accepted, crew: crew, user: invitee, invited_by: captain) }
it 'returns false' do
expect(invitation.active?).to be false
end
end
end
describe 'expiration' do
it 'sets default expiration to 7 days' do
invitation = create(:crew_invitation, crew: crew, user: invitee, invited_by: captain, expires_at: nil)
expect(invitation.expires_at).to be_within(1.minute).of(7.days.from_now)
end
end
describe 'scopes' do
describe '.active' do
let!(:active_invitation) { create(:crew_invitation, crew: crew, user: invitee, invited_by: captain) }
let(:other_user) { create(:user) }
let!(:expired_invitation) { create(:crew_invitation, :expired_by_time, crew: crew, user: other_user, invited_by: captain) }
it 'returns only active invitations' do
expect(CrewInvitation.active).to include(active_invitation)
expect(CrewInvitation.active).not_to include(expired_invitation)
end
end
end
end