hensei-api/spec/requests/api/v1/phantom_players_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

223 lines
7.7 KiB
Ruby

require 'rails_helper'
RSpec.describe 'Api::V1::PhantomPlayers', type: :request do
let(:user) { create(:user) }
let(:access_token) do
Doorkeeper::AccessToken.create!(resource_owner_id: user.id, expires_in: 30.days, scopes: 'public')
end
let(:auth_headers) { { 'Authorization' => "Bearer #{access_token.token}" } }
let(:crew) { create(:crew) }
let!(:captain_membership) { create(:crew_membership, crew: crew, user: user, role: :captain) }
describe 'GET /api/v1/crews/:crew_id/phantom_players' do
let!(:phantom1) { create(:phantom_player, crew: crew, name: 'Phantom A') }
let!(:phantom2) { create(:phantom_player, crew: crew, name: 'Phantom B') }
context 'as crew member' do
it 'returns all phantom players' do
get "/api/v1/crews/#{crew.id}/phantom_players", headers: auth_headers
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['phantom_players'].length).to eq(2)
end
end
context 'as non-member' do
let(:other_user) { create(:user) }
let(:other_token) do
Doorkeeper::AccessToken.create!(resource_owner_id: other_user.id, expires_in: 30.days, scopes: 'public')
end
let(:other_headers) { { 'Authorization' => "Bearer #{other_token.token}" } }
it 'returns unauthorized' do
get "/api/v1/crews/#{crew.id}/phantom_players", headers: other_headers
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'GET /api/v1/crews/:crew_id/phantom_players/:id' do
let!(:phantom) { create(:phantom_player, crew: crew) }
it 'returns the phantom player with scores' do
get "/api/v1/crews/#{crew.id}/phantom_players/#{phantom.id}", headers: auth_headers
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['phantom_player']['name']).to eq(phantom.name)
expect(json['phantom_player']).to have_key('total_score')
end
end
describe 'POST /api/v1/crews/:crew_id/phantom_players' do
let(:valid_params) do
{
phantom_player: {
name: 'New Phantom',
granblue_id: '12345678',
notes: 'Former member'
}
}
end
context 'as officer' do
it 'creates a phantom player' do
expect {
post "/api/v1/crews/#{crew.id}/phantom_players",
params: valid_params,
headers: auth_headers
}.to change(PhantomPlayer, :count).by(1)
expect(response).to have_http_status(:created)
json = JSON.parse(response.body)
expect(json['phantom_player']['name']).to eq('New Phantom')
expect(json['phantom_player']['granblue_id']).to eq('12345678')
end
it 'returns validation error for missing name' do
post "/api/v1/crews/#{crew.id}/phantom_players",
params: { phantom_player: { name: '' } },
headers: auth_headers
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'as regular member' do
let!(:captain_membership) { create(:crew_membership, crew: crew, user: user, role: :member) }
it 'returns unauthorized' do
post "/api/v1/crews/#{crew.id}/phantom_players",
params: valid_params,
headers: auth_headers
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'PUT /api/v1/crews/:crew_id/phantom_players/:id' do
let!(:phantom) { create(:phantom_player, crew: crew, name: 'Old Name') }
context 'as officer' do
it 'updates the phantom player' do
put "/api/v1/crews/#{crew.id}/phantom_players/#{phantom.id}",
params: { phantom_player: { name: 'New Name' } },
headers: auth_headers
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['phantom_player']['name']).to eq('New Name')
end
end
context 'as regular member' do
let!(:captain_membership) { create(:crew_membership, crew: crew, user: user, role: :member) }
it 'returns unauthorized' do
put "/api/v1/crews/#{crew.id}/phantom_players/#{phantom.id}",
params: { phantom_player: { name: 'New Name' } },
headers: auth_headers
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'DELETE /api/v1/crews/:crew_id/phantom_players/:id' do
let!(:phantom) { create(:phantom_player, crew: crew) }
context 'as officer' do
it 'deletes the phantom player' do
expect {
delete "/api/v1/crews/#{crew.id}/phantom_players/#{phantom.id}",
headers: auth_headers
}.to change(PhantomPlayer, :count).by(-1)
expect(response).to have_http_status(:no_content)
end
end
context 'as regular member' do
let!(:captain_membership) { create(:crew_membership, crew: crew, user: user, role: :member) }
it 'returns unauthorized' do
delete "/api/v1/crews/#{crew.id}/phantom_players/#{phantom.id}",
headers: auth_headers
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'POST /api/v1/crews/:crew_id/phantom_players/:id/assign' do
let!(:phantom) { create(:phantom_player, crew: crew) }
let(:target_user) { create(:user) }
let!(:target_membership) { create(:crew_membership, crew: crew, user: target_user, role: :member) }
context 'as officer' do
it 'assigns the phantom to a user' do
post "/api/v1/crews/#{crew.id}/phantom_players/#{phantom.id}/assign",
params: { user_id: target_user.id },
headers: auth_headers
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['phantom_player']['claimed']).to be true
expect(json['phantom_player']['claimed_by']['id']).to eq(target_user.id)
end
it 'returns error for non-crew member' do
non_member = create(:user)
post "/api/v1/crews/#{crew.id}/phantom_players/#{phantom.id}/assign",
params: { user_id: non_member.id },
headers: auth_headers
expect(response).to have_http_status(:not_found)
end
end
context 'as regular member' do
let!(:captain_membership) { create(:crew_membership, crew: crew, user: user, role: :member) }
it 'returns unauthorized' do
post "/api/v1/crews/#{crew.id}/phantom_players/#{phantom.id}/assign",
params: { user_id: target_user.id },
headers: auth_headers
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'POST /api/v1/crews/:crew_id/phantom_players/:id/confirm_claim' do
let(:claimer) { create(:user) }
let!(:claimer_membership) { create(:crew_membership, crew: crew, user: claimer, role: :member) }
let!(:phantom) { create(:phantom_player, crew: crew, claimed_by: claimer) }
let(:claimer_token) do
Doorkeeper::AccessToken.create!(resource_owner_id: claimer.id, expires_in: 30.days, scopes: 'public')
end
let(:claimer_headers) { { 'Authorization' => "Bearer #{claimer_token.token}" } }
it 'confirms the claim' do
post "/api/v1/crews/#{crew.id}/phantom_players/#{phantom.id}/confirm_claim",
headers: claimer_headers
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['phantom_player']['claim_confirmed']).to be true
end
it 'returns error for wrong user' do
post "/api/v1/crews/#{crew.id}/phantom_players/#{phantom.id}/confirm_claim",
headers: auth_headers
expect(response).to have_http_status(:forbidden)
json = JSON.parse(response.body)
expect(json['code']).to eq('not_claimed_by_user')
end
end
end