require 'rails_helper' RSpec.describe 'Api::V1::CrewMemberships', 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(:target_user) { create(:user) } let!(:target_membership) { create(:crew_membership, crew: crew, user: target_user, role: :member) } describe 'PUT /api/v1/crews/:crew_id/memberships/:id' do context 'as captain' do let!(:captain_membership) { create(:crew_membership, crew: crew, user: user, role: :captain) } it 'updates member role' do put "/api/v1/crews/#{crew.id}/memberships/#{target_membership.id}", params: { membership: { role: 'vice_captain' } }, headers: auth_headers expect(response).to have_http_status(:ok) json = JSON.parse(response.body) expect(json['membership']['role']).to eq('vice_captain') end end context 'as vice captain' do let!(:vc_membership) { create(:crew_membership, crew: crew, user: user, role: :vice_captain) } it 'cannot change role but request succeeds' do put "/api/v1/crews/#{crew.id}/memberships/#{target_membership.id}", params: { membership: { role: 'vice_captain' } }, headers: auth_headers # Vice captains can access update endpoint (for joined_at) but role param is ignored expect(response).to have_http_status(:ok) expect(target_membership.reload.role).to eq('member') # Role not changed end it 'can update joined_at' do put "/api/v1/crews/#{crew.id}/memberships/#{target_membership.id}", params: { membership: { joined_at: '2024-01-15' } }, headers: auth_headers expect(response).to have_http_status(:ok) json = JSON.parse(response.body) expect(json['membership']['joined_at']).to include('2024-01-15') end end end describe 'DELETE /api/v1/crews/:crew_id/memberships/:id' do context 'as captain' do let!(:captain_membership) { create(:crew_membership, crew: crew, user: user, role: :captain) } it 'removes a member' do delete "/api/v1/crews/#{crew.id}/memberships/#{target_membership.id}", headers: auth_headers expect(response).to have_http_status(:no_content) expect(target_membership.reload.retired).to be true end it 'cannot remove self (captain)' do delete "/api/v1/crews/#{crew.id}/memberships/#{captain_membership.id}", headers: auth_headers expect(response).to have_http_status(:unprocessable_entity) json = JSON.parse(response.body) expect(json['code']).to eq('cannot_remove_captain') end end context 'as vice captain' do let!(:vc_membership) { create(:crew_membership, crew: crew, user: user, role: :vice_captain) } it 'can remove regular members' do delete "/api/v1/crews/#{crew.id}/memberships/#{target_membership.id}", headers: auth_headers expect(response).to have_http_status(:no_content) end end context 'as regular member' do let!(:membership) { create(:crew_membership, crew: crew, user: user, role: :member) } it 'returns unauthorized' do delete "/api/v1/crews/#{crew.id}/memberships/#{target_membership.id}", headers: auth_headers expect(response).to have_http_status(:unauthorized) end end end describe 'POST /api/v1/crews/:crew_id/memberships/:id/promote' do context 'as captain' do let!(:captain_membership) { create(:crew_membership, crew: crew, user: user, role: :captain) } it 'promotes member to vice captain' do post "/api/v1/crews/#{crew.id}/memberships/#{target_membership.id}/promote", headers: auth_headers expect(response).to have_http_status(:ok) json = JSON.parse(response.body) expect(json['membership']['role']).to eq('vice_captain') end it 'returns error when VC limit reached' do 3.times { create(:crew_membership, crew: crew, role: :vice_captain) } post "/api/v1/crews/#{crew.id}/memberships/#{target_membership.id}/promote", headers: auth_headers expect(response).to have_http_status(:unprocessable_entity) json = JSON.parse(response.body) expect(json['code']).to eq('vice_captain_limit') end it 'cannot promote captain' do post "/api/v1/crews/#{crew.id}/memberships/#{captain_membership.id}/promote", headers: auth_headers expect(response).to have_http_status(:unprocessable_entity) end end context 'as vice captain' do let!(:vc_membership) { create(:crew_membership, crew: crew, user: user, role: :vice_captain) } it 'returns unauthorized' do post "/api/v1/crews/#{crew.id}/memberships/#{target_membership.id}/promote", headers: auth_headers expect(response).to have_http_status(:unauthorized) end end end describe 'POST /api/v1/crews/:crew_id/memberships/:id/demote' do let!(:vc_target) { create(:crew_membership, crew: crew, role: :vice_captain) } context 'as captain' do let!(:captain_membership) { create(:crew_membership, crew: crew, user: user, role: :captain) } it 'demotes vice captain to member' do post "/api/v1/crews/#{crew.id}/memberships/#{vc_target.id}/demote", headers: auth_headers expect(response).to have_http_status(:ok) json = JSON.parse(response.body) expect(json['membership']['role']).to eq('member') end it 'cannot demote captain' do post "/api/v1/crews/#{crew.id}/memberships/#{captain_membership.id}/demote", headers: auth_headers expect(response).to have_http_status(:unprocessable_entity) json = JSON.parse(response.body) expect(json['code']).to eq('cannot_demote_captain') end end context 'as vice captain' do let!(:vc_membership) { create(:crew_membership, crew: crew, user: user, role: :vice_captain) } it 'returns unauthorized' do post "/api/v1/crews/#{crew.id}/memberships/#{vc_target.id}/demote", headers: auth_headers expect(response).to have_http_status(:unauthorized) end end end end