hensei-api/spec/models/phantom_player_spec.rb
Justin Edmund 4c8f4ffcf3 add phantom players for non-registered crew members
- phantom_players table for tracking scores of non-user members
- claim flow: officer assigns phantom to user, user confirms, scores transfer
- CRUD endpoints plus /assign and /confirm_claim actions
- model/request specs for all functionality (37 examples)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 23:55:15 -08:00

162 lines
5.6 KiB
Ruby

require 'rails_helper'
RSpec.describe PhantomPlayer, type: :model do
describe 'associations' do
it { should belong_to(:crew) }
it { should belong_to(:claimed_by).class_name('User').optional }
it { should belong_to(:claimed_from_membership).class_name('CrewMembership').optional }
it { should have_many(:gw_individual_scores) }
end
describe 'validations' do
it { should validate_presence_of(:name) }
it { should validate_length_of(:name).is_at_most(100) }
it { should validate_length_of(:granblue_id).is_at_most(20) }
describe 'granblue_id uniqueness within crew' do
let(:crew) { create(:crew) }
let!(:existing) { create(:phantom_player, crew: crew, granblue_id: '12345678') }
it 'rejects duplicate granblue_id in same crew' do
phantom = build(:phantom_player, crew: crew, granblue_id: '12345678')
expect(phantom).not_to be_valid
expect(phantom.errors[:granblue_id]).to be_present
end
it 'allows same granblue_id in different crew' do
other_crew = create(:crew)
phantom = build(:phantom_player, crew: other_crew, granblue_id: '12345678')
expect(phantom).to be_valid
end
it 'allows multiple nil granblue_ids in same crew' do
phantom = build(:phantom_player, crew: crew, granblue_id: nil)
expect(phantom).to be_valid
end
end
describe 'claimed_by must be crew member' do
let(:crew) { create(:crew) }
let(:phantom) { build(:phantom_player, crew: crew) }
it 'accepts claimed_by who is a crew member' do
member = create(:user)
create(:crew_membership, crew: crew, user: member, role: :member)
phantom.claimed_by = member
expect(phantom).to be_valid
end
it 'rejects claimed_by who is not a crew member' do
non_member = create(:user)
phantom.claimed_by = non_member
expect(phantom).not_to be_valid
expect(phantom.errors[:claimed_by]).to include('must be a member of this crew')
end
end
describe 'claim_confirmed requires claimed_by' do
let(:phantom) { build(:phantom_player, claim_confirmed: true, claimed_by: nil) }
it 'is invalid' do
expect(phantom).not_to be_valid
expect(phantom.errors[:claim_confirmed]).to include('requires a claimed_by user')
end
end
end
describe 'scopes' do
let(:crew) { create(:crew) }
let(:member) { create(:user) }
let!(:membership) { create(:crew_membership, crew: crew, user: member, role: :member) }
let!(:unclaimed) { create(:phantom_player, crew: crew) }
let!(:claimed) { create(:phantom_player, crew: crew, claimed_by: member) }
let!(:confirmed) { create(:phantom_player, crew: crew, claimed_by: member, claim_confirmed: true) }
describe '.unclaimed' do
it 'returns only unclaimed phantoms' do
expect(PhantomPlayer.unclaimed).to contain_exactly(unclaimed)
end
end
describe '.claimed' do
it 'returns claimed phantoms (confirmed or not)' do
expect(PhantomPlayer.claimed).to contain_exactly(claimed, confirmed)
end
end
describe '.pending_confirmation' do
it 'returns claimed but unconfirmed phantoms' do
expect(PhantomPlayer.pending_confirmation).to contain_exactly(claimed)
end
end
end
describe '#assign_to' do
let(:crew) { create(:crew) }
let(:phantom) { create(:phantom_player, crew: crew) }
let(:member) { create(:user) }
let!(:membership) { create(:crew_membership, crew: crew, user: member, role: :member) }
it 'assigns the phantom to the user' do
phantom.assign_to(member)
expect(phantom.claimed_by).to eq(member)
expect(phantom.claim_confirmed).to be false
end
it 'raises error for non-crew member' do
non_member = create(:user)
expect { phantom.assign_to(non_member) }.to raise_error(CrewErrors::MemberNotFoundError)
end
end
describe '#confirm_claim!' do
let(:crew) { create(:crew) }
let(:member) { create(:user) }
let!(:membership) { create(:crew_membership, crew: crew, user: member, role: :member) }
let(:phantom) { create(:phantom_player, crew: crew, claimed_by: member) }
it 'confirms the claim' do
phantom.confirm_claim!(member)
expect(phantom.claim_confirmed).to be true
end
it 'raises error for wrong user' do
other_user = create(:user)
create(:crew_membership, crew: crew, user: other_user, role: :member)
expect { phantom.confirm_claim!(other_user) }.to raise_error(CrewErrors::NotClaimedByUserError)
end
context 'with individual scores' do
let(:gw_event) { create(:gw_event) }
let(:participation) { create(:crew_gw_participation, crew: crew, gw_event: gw_event) }
let!(:phantom_score) do
create(:gw_individual_score,
crew_gw_participation: participation,
phantom_player: phantom,
crew_membership: nil,
score: 1_000_000)
end
it 'transfers scores to the membership' do
phantom.confirm_claim!(member)
phantom_score.reload
expect(phantom_score.crew_membership).to eq(membership)
expect(phantom_score.phantom_player).to be_nil
end
end
end
describe '#unassign!' do
let(:crew) { create(:crew) }
let(:member) { create(:user) }
let!(:membership) { create(:crew_membership, crew: crew, user: member, role: :member) }
let(:phantom) { create(:phantom_player, crew: crew, claimed_by: member) }
it 'removes the assignment' do
phantom.unassign!
expect(phantom.claimed_by).to be_nil
expect(phantom.claim_confirmed).to be false
end
end
end