hensei-api/spec/models/crew_membership_spec.rb
Justin Edmund 872b6fdb59 add crew specs and fix error handling
- add transactional fixtures to rails_helper for test isolation
- restructure crew errors to CrewErrors module for Zeitwerk
- add rescue_from for CrewErrors::CrewError in api_controller
- add model specs for Crew and CrewMembership (34 examples)
- add controller specs for crews and memberships (28 examples)
- add crew-related specs to User model (22 examples)
- add factories for crews and crew_memberships
2025-12-03 22:51:34 -08:00

137 lines
4.1 KiB
Ruby

require 'rails_helper'
RSpec.describe CrewMembership, type: :model do
describe 'associations' do
it { should belong_to(:crew) }
it { should belong_to(:user) }
end
describe 'validations' do
it 'validates uniqueness of user_id scoped to crew_id' do
crew = create(:crew)
user = create(:user)
create(:crew_membership, crew: crew, user: user)
duplicate = build(:crew_membership, crew: crew, user: user)
expect(duplicate).not_to be_valid
expect(duplicate.errors[:user_id]).to include('has already been taken')
end
end
describe 'role enum' do
it { should define_enum_for(:role).with_values(member: 0, vice_captain: 1, captain: 2) }
end
describe 'scopes' do
let(:crew) { create(:crew) }
describe '.active' do
it 'returns only non-retired memberships' do
active = create(:crew_membership, crew: crew)
retired = create(:crew_membership, :retired, crew: crew)
expect(CrewMembership.active).to include(active)
expect(CrewMembership.active).not_to include(retired)
end
end
describe '.retired' do
it 'returns only retired memberships' do
active = create(:crew_membership, crew: crew)
retired = create(:crew_membership, :retired, crew: crew)
expect(CrewMembership.retired).to include(retired)
expect(CrewMembership.retired).not_to include(active)
end
end
end
describe 'one active crew per user validation' do
let(:crew1) { create(:crew) }
let(:crew2) { create(:crew) }
let(:user) { create(:user) }
it 'prevents user from joining multiple active crews' do
create(:crew_membership, crew: crew1, user: user)
membership2 = build(:crew_membership, crew: crew2, user: user)
expect(membership2).not_to be_valid
expect(membership2.errors[:user]).to include('can only be in one active crew')
end
it 'allows user to join new crew after retiring from old one' do
membership1 = create(:crew_membership, crew: crew1, user: user)
membership1.retire!
membership2 = build(:crew_membership, crew: crew2, user: user)
expect(membership2).to be_valid
end
end
describe 'captain limit validation' do
let(:crew) { create(:crew) }
let(:captain_user) { create(:user) }
before do
create(:crew_membership, :captain, crew: crew, user: captain_user)
end
it 'prevents multiple captains' do
new_captain = build(:crew_membership, :captain, crew: crew)
expect(new_captain).not_to be_valid
expect(new_captain.errors[:role]).to include('crew can only have one captain')
end
it 'allows captain after previous captain retires' do
crew.crew_memberships.find_by(role: :captain).retire!
new_captain = build(:crew_membership, :captain, crew: crew)
expect(new_captain).to be_valid
end
end
describe 'vice captain limit validation' do
let(:crew) { create(:crew) }
before do
create(:crew_membership, :captain, crew: crew)
3.times { create(:crew_membership, :vice_captain, crew: crew) }
end
it 'prevents more than 3 vice captains' do
fourth_vc = build(:crew_membership, :vice_captain, crew: crew)
expect(fourth_vc).not_to be_valid
expect(fourth_vc.errors[:role]).to include('crew can only have up to 3 vice captains')
end
it 'allows new vice captain after one retires' do
crew.crew_memberships.where(role: :vice_captain).first.retire!
new_vc = build(:crew_membership, :vice_captain, crew: crew)
expect(new_vc).to be_valid
end
end
describe '#retire!' do
let(:crew) { create(:crew) }
let(:user) { create(:user) }
let(:membership) { create(:crew_membership, :vice_captain, crew: crew, user: user) }
it 'sets retired to true' do
membership.retire!
expect(membership.retired).to be true
end
it 'sets retired_at timestamp' do
membership.retire!
expect(membership.retired_at).to be_within(1.second).of(Time.current)
end
it 'demotes to member role' do
membership.retire!
expect(membership.role).to eq('member')
end
end
end