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
This commit is contained in:
parent
274881e894
commit
872b6fdb59
13 changed files with 963 additions and 101 deletions
|
|
@ -31,10 +31,19 @@ module Api
|
|||
render json: e.to_hash, status: e.http_status
|
||||
end
|
||||
|
||||
# Crew errors
|
||||
rescue_from CrewErrors::CrewError do |e|
|
||||
render json: e.to_hash, status: e.http_status
|
||||
end
|
||||
|
||||
rescue_from GranblueError do |e|
|
||||
render_error(e)
|
||||
end
|
||||
|
||||
rescue_from Api::V1::GranblueError do |e|
|
||||
render_error(e)
|
||||
end
|
||||
|
||||
##### Hooks
|
||||
before_action :current_user
|
||||
before_action :default_content_type
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ module Api
|
|||
|
||||
# DELETE /crews/:crew_id/memberships/:id
|
||||
def destroy
|
||||
raise CannotRemoveCaptainError if @membership.captain?
|
||||
raise CrewErrors::CannotRemoveCaptainError if @membership.captain?
|
||||
|
||||
@membership.retire!
|
||||
head :no_content
|
||||
|
|
@ -33,11 +33,11 @@ module Api
|
|||
|
||||
# POST /crews/:crew_id/memberships/:id/promote
|
||||
def promote
|
||||
raise CannotRemoveCaptainError if @membership.captain?
|
||||
raise CrewErrors::CannotRemoveCaptainError if @membership.captain?
|
||||
|
||||
# Check vice captain limit
|
||||
current_vc_count = @crew.crew_memberships.where(role: :vice_captain, retired: false).count
|
||||
raise ViceCaptainLimitError if current_vc_count >= 3 && !@membership.vice_captain?
|
||||
raise CrewErrors::ViceCaptainLimitError if current_vc_count >= 3 && !@membership.vice_captain?
|
||||
|
||||
@membership.update!(role: :vice_captain)
|
||||
render json: CrewMembershipBlueprint.render(@membership, view: :with_user, root: :membership)
|
||||
|
|
@ -45,7 +45,7 @@ module Api
|
|||
|
||||
# POST /crews/:crew_id/memberships/:id/demote
|
||||
def demote
|
||||
raise CannotRemoveCaptainError if @membership.captain?
|
||||
raise CrewErrors::CannotDemoteCaptainError if @membership.captain?
|
||||
|
||||
@membership.update!(role: :member)
|
||||
render json: CrewMembershipBlueprint.render(@membership, view: :with_user, root: :membership)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ module Api
|
|||
|
||||
before_action :restrict_access
|
||||
before_action :set_crew, only: %i[show update members leave transfer_captain]
|
||||
before_action :require_crew!, only: %i[show update members]
|
||||
before_action :authorize_crew_member!, only: %i[show members]
|
||||
before_action :authorize_crew_officer!, only: %i[update]
|
||||
before_action :authorize_crew_captain!, only: %i[transfer_captain]
|
||||
|
|
@ -18,7 +19,7 @@ module Api
|
|||
|
||||
# POST /crews
|
||||
def create
|
||||
raise AlreadyInCrewError if current_user.crew.present?
|
||||
raise CrewErrors::AlreadyInCrewError if current_user.crew.present?
|
||||
|
||||
@crew = Crew.new(crew_params)
|
||||
|
||||
|
|
@ -47,10 +48,9 @@ module Api
|
|||
|
||||
# POST /crew/leave
|
||||
def leave
|
||||
raise NotInCrewError unless @crew
|
||||
|
||||
membership = current_user.active_crew_membership
|
||||
raise CaptainCannotLeaveError if membership.captain?
|
||||
raise CrewErrors::NotInCrewError unless membership
|
||||
raise CrewErrors::CaptainCannotLeaveError if membership.captain?
|
||||
|
||||
membership.retire!
|
||||
head :no_content
|
||||
|
|
@ -61,7 +61,7 @@ module Api
|
|||
new_captain_id = params[:user_id]
|
||||
new_captain_membership = @crew.active_memberships.find_by(user_id: new_captain_id)
|
||||
|
||||
raise MemberNotFoundError unless new_captain_membership
|
||||
raise CrewErrors::MemberNotFoundError unless new_captain_membership
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
current_user.active_crew_membership.update!(role: :vice_captain)
|
||||
|
|
@ -84,6 +84,10 @@ module Api
|
|||
def crew_params
|
||||
params.require(:crew).permit(:name, :gamertag, :granblue_crew_id, :description)
|
||||
end
|
||||
|
||||
def require_crew!
|
||||
render_not_found_response('crew') unless @crew
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class AlreadyInCrewError < GranblueError
|
||||
def http_status
|
||||
422
|
||||
end
|
||||
|
||||
def code
|
||||
'already_in_crew'
|
||||
end
|
||||
|
||||
def message
|
||||
'You are already in a crew'
|
||||
end
|
||||
end
|
||||
|
||||
class CaptainCannotLeaveError < GranblueError
|
||||
def http_status
|
||||
422
|
||||
end
|
||||
|
||||
def code
|
||||
'captain_cannot_leave'
|
||||
end
|
||||
|
||||
def message
|
||||
'Captain must transfer ownership before leaving'
|
||||
end
|
||||
end
|
||||
|
||||
class CannotRemoveCaptainError < GranblueError
|
||||
def http_status
|
||||
422
|
||||
end
|
||||
|
||||
def code
|
||||
'cannot_remove_captain'
|
||||
end
|
||||
|
||||
def message
|
||||
'Cannot remove the captain from the crew'
|
||||
end
|
||||
end
|
||||
|
||||
class ViceCaptainLimitError < GranblueError
|
||||
def http_status
|
||||
422
|
||||
end
|
||||
|
||||
def code
|
||||
'vice_captain_limit'
|
||||
end
|
||||
|
||||
def message
|
||||
'Crew can only have up to 3 vice captains'
|
||||
end
|
||||
end
|
||||
|
||||
class NotInCrewError < GranblueError
|
||||
def http_status
|
||||
422
|
||||
end
|
||||
|
||||
def code
|
||||
'not_in_crew'
|
||||
end
|
||||
|
||||
def message
|
||||
'You are not in a crew'
|
||||
end
|
||||
end
|
||||
|
||||
class MemberNotFoundError < GranblueError
|
||||
def http_status
|
||||
404
|
||||
end
|
||||
|
||||
def code
|
||||
'member_not_found'
|
||||
end
|
||||
|
||||
def message
|
||||
'Member not found in this crew'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
119
app/errors/crew_errors.rb
Normal file
119
app/errors/crew_errors.rb
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module CrewErrors
|
||||
# Base class for all crew-related errors
|
||||
class CrewError < StandardError
|
||||
def http_status
|
||||
:unprocessable_entity
|
||||
end
|
||||
|
||||
def code
|
||||
self.class.name.demodulize.underscore
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
message: message,
|
||||
code: code
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
class AlreadyInCrewError < CrewError
|
||||
def http_status
|
||||
:unprocessable_entity
|
||||
end
|
||||
|
||||
def code
|
||||
'already_in_crew'
|
||||
end
|
||||
|
||||
def message
|
||||
'You are already in a crew'
|
||||
end
|
||||
end
|
||||
|
||||
class CaptainCannotLeaveError < CrewError
|
||||
def http_status
|
||||
:unprocessable_entity
|
||||
end
|
||||
|
||||
def code
|
||||
'captain_cannot_leave'
|
||||
end
|
||||
|
||||
def message
|
||||
'Captain must transfer ownership before leaving'
|
||||
end
|
||||
end
|
||||
|
||||
class CannotRemoveCaptainError < CrewError
|
||||
def http_status
|
||||
:unprocessable_entity
|
||||
end
|
||||
|
||||
def code
|
||||
'cannot_remove_captain'
|
||||
end
|
||||
|
||||
def message
|
||||
'Cannot remove the captain from the crew'
|
||||
end
|
||||
end
|
||||
|
||||
class ViceCaptainLimitError < CrewError
|
||||
def http_status
|
||||
:unprocessable_entity
|
||||
end
|
||||
|
||||
def code
|
||||
'vice_captain_limit'
|
||||
end
|
||||
|
||||
def message
|
||||
'Crew can only have up to 3 vice captains'
|
||||
end
|
||||
end
|
||||
|
||||
class NotInCrewError < CrewError
|
||||
def http_status
|
||||
:unprocessable_entity
|
||||
end
|
||||
|
||||
def code
|
||||
'not_in_crew'
|
||||
end
|
||||
|
||||
def message
|
||||
'You are not in a crew'
|
||||
end
|
||||
end
|
||||
|
||||
class MemberNotFoundError < CrewError
|
||||
def http_status
|
||||
:not_found
|
||||
end
|
||||
|
||||
def code
|
||||
'member_not_found'
|
||||
end
|
||||
|
||||
def message
|
||||
'Member not found in this crew'
|
||||
end
|
||||
end
|
||||
|
||||
class CannotDemoteCaptainError < CrewError
|
||||
def http_status
|
||||
:unprocessable_entity
|
||||
end
|
||||
|
||||
def code
|
||||
'cannot_demote_captain'
|
||||
end
|
||||
|
||||
def message
|
||||
'Cannot demote the captain'
|
||||
end
|
||||
end
|
||||
end
|
||||
21
spec/factories/crew_memberships.rb
Normal file
21
spec/factories/crew_memberships.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
FactoryBot.define do
|
||||
factory :crew_membership do
|
||||
crew
|
||||
user
|
||||
role { :member }
|
||||
retired { false }
|
||||
|
||||
trait :captain do
|
||||
role { :captain }
|
||||
end
|
||||
|
||||
trait :vice_captain do
|
||||
role { :vice_captain }
|
||||
end
|
||||
|
||||
trait :retired do
|
||||
retired { true }
|
||||
retired_at { Time.current }
|
||||
end
|
||||
end
|
||||
end
|
||||
8
spec/factories/crews.rb
Normal file
8
spec/factories/crews.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
FactoryBot.define do
|
||||
factory :crew do
|
||||
name { Faker::Team.name }
|
||||
gamertag { Faker::Alphanumeric.alpha(number: 5).upcase }
|
||||
granblue_crew_id { Faker::Number.number(digits: 8).to_s }
|
||||
description { Faker::Lorem.paragraph }
|
||||
end
|
||||
end
|
||||
137
spec/models/crew_membership_spec.rb
Normal file
137
spec/models/crew_membership_spec.rb
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
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
|
||||
134
spec/models/crew_spec.rb
Normal file
134
spec/models/crew_spec.rb
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Crew, type: :model do
|
||||
describe 'associations' do
|
||||
it { should have_many(:crew_memberships).dependent(:destroy) }
|
||||
it { should have_many(:users).through(:crew_memberships) }
|
||||
it { should have_many(:active_memberships) }
|
||||
it { should have_many(:active_members).through(:active_memberships) }
|
||||
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(:gamertag).is_at_most(50) }
|
||||
|
||||
context 'granblue_crew_id uniqueness' do
|
||||
it 'validates uniqueness' do
|
||||
crew1 = create(:crew, granblue_crew_id: 'ABC123')
|
||||
crew2 = build(:crew, granblue_crew_id: 'ABC123')
|
||||
expect(crew2).not_to be_valid
|
||||
expect(crew2.errors[:granblue_crew_id]).to include('has already been taken')
|
||||
end
|
||||
|
||||
it 'allows nil values' do
|
||||
create(:crew, granblue_crew_id: nil)
|
||||
crew2 = build(:crew, granblue_crew_id: nil)
|
||||
expect(crew2).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#captain' do
|
||||
let(:crew) { create(:crew) }
|
||||
let(:captain_user) { create(:user) }
|
||||
let(:member_user) { create(:user) }
|
||||
|
||||
before do
|
||||
create(:crew_membership, :captain, crew: crew, user: captain_user)
|
||||
create(:crew_membership, crew: crew, user: member_user)
|
||||
end
|
||||
|
||||
it 'returns the captain user' do
|
||||
expect(crew.captain).to eq(captain_user)
|
||||
end
|
||||
|
||||
it 'returns nil if no captain' do
|
||||
crew.crew_memberships.find_by(role: :captain).update!(role: :member)
|
||||
expect(crew.captain).to be_nil
|
||||
end
|
||||
|
||||
it 'does not return retired captains' do
|
||||
crew.crew_memberships.find_by(role: :captain).retire!
|
||||
expect(crew.captain).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#vice_captains' do
|
||||
let(:crew) { create(:crew) }
|
||||
let(:captain_user) { create(:user) }
|
||||
let(:vc1) { create(:user) }
|
||||
let(:vc2) { create(:user) }
|
||||
let(:member_user) { create(:user) }
|
||||
|
||||
before do
|
||||
create(:crew_membership, :captain, crew: crew, user: captain_user)
|
||||
create(:crew_membership, :vice_captain, crew: crew, user: vc1)
|
||||
create(:crew_membership, :vice_captain, crew: crew, user: vc2)
|
||||
create(:crew_membership, crew: crew, user: member_user)
|
||||
end
|
||||
|
||||
it 'returns all vice captains' do
|
||||
expect(crew.vice_captains).to contain_exactly(vc1, vc2)
|
||||
end
|
||||
|
||||
it 'does not include retired vice captains' do
|
||||
crew.crew_memberships.find_by(user: vc1).retire!
|
||||
expect(crew.vice_captains).to contain_exactly(vc2)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#officers' do
|
||||
let(:crew) { create(:crew) }
|
||||
let(:captain_user) { create(:user) }
|
||||
let(:vc1) { create(:user) }
|
||||
let(:member_user) { create(:user) }
|
||||
|
||||
before do
|
||||
create(:crew_membership, :captain, crew: crew, user: captain_user)
|
||||
create(:crew_membership, :vice_captain, crew: crew, user: vc1)
|
||||
create(:crew_membership, crew: crew, user: member_user)
|
||||
end
|
||||
|
||||
it 'returns captain and vice captains' do
|
||||
expect(crew.officers).to contain_exactly(captain_user, vc1)
|
||||
end
|
||||
|
||||
it 'does not include regular members' do
|
||||
expect(crew.officers).not_to include(member_user)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#member_count' do
|
||||
let(:crew) { create(:crew) }
|
||||
|
||||
before do
|
||||
create(:crew_membership, :captain, crew: crew)
|
||||
create(:crew_membership, crew: crew)
|
||||
create(:crew_membership, :retired, crew: crew)
|
||||
end
|
||||
|
||||
it 'returns count of active members only' do
|
||||
expect(crew.member_count).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'active_members scope' do
|
||||
let(:crew) { create(:crew) }
|
||||
let(:active_user) { create(:user) }
|
||||
let(:retired_user) { create(:user) }
|
||||
|
||||
before do
|
||||
create(:crew_membership, crew: crew, user: active_user)
|
||||
create(:crew_membership, :retired, crew: crew, user: retired_user)
|
||||
end
|
||||
|
||||
it 'returns only active members' do
|
||||
expect(crew.active_members).to contain_exactly(active_user)
|
||||
end
|
||||
|
||||
it 'does not include retired members' do
|
||||
expect(crew.active_members).not_to include(retired_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -8,6 +8,9 @@ RSpec.describe User, type: :model do
|
|||
it { should have_many(:collection_weapons).dependent(:destroy) }
|
||||
it { should have_many(:collection_summons).dependent(:destroy) }
|
||||
it { should have_many(:collection_job_accessories).dependent(:destroy) }
|
||||
it { should have_many(:crew_memberships).dependent(:destroy) }
|
||||
it { should have_one(:active_crew_membership) }
|
||||
it { should have_one(:crew).through(:active_crew_membership) }
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
|
|
@ -72,8 +75,20 @@ RSpec.describe User, type: :model do
|
|||
end
|
||||
|
||||
context 'when collection privacy is crew_only' do
|
||||
it 'returns false for non-owner (crews not yet implemented)' do
|
||||
let(:crew) { create(:crew) }
|
||||
|
||||
it 'returns true for crew members' do
|
||||
create(:crew_membership, :captain, crew: crew, user: owner)
|
||||
create(:crew_membership, crew: crew, user: viewer)
|
||||
owner.update(collection_privacy: :crew_only)
|
||||
|
||||
expect(owner.collection_viewable_by?(viewer)).to be true
|
||||
end
|
||||
|
||||
it 'returns false for non-crew members' do
|
||||
create(:crew_membership, :captain, crew: crew, user: owner)
|
||||
owner.update(collection_privacy: :crew_only)
|
||||
|
||||
expect(owner.collection_viewable_by?(viewer)).to be false
|
||||
end
|
||||
|
||||
|
|
@ -87,14 +102,112 @@ RSpec.describe User, type: :model do
|
|||
describe '#in_same_crew_as?' do
|
||||
let(:user1) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
let(:crew) { create(:crew) }
|
||||
|
||||
it 'returns false (placeholder until crews are implemented)' do
|
||||
it 'returns false when neither user is in a crew' do
|
||||
expect(user1.in_same_crew_as?(user2)).to be false
|
||||
end
|
||||
|
||||
it 'returns false when other_user is present' do
|
||||
it 'returns false when only one user is in a crew' do
|
||||
create(:crew_membership, :captain, crew: crew, user: user1)
|
||||
expect(user1.in_same_crew_as?(user2)).to be false
|
||||
end
|
||||
|
||||
it 'returns true when both users are in the same crew' do
|
||||
create(:crew_membership, :captain, crew: crew, user: user1)
|
||||
create(:crew_membership, crew: crew, user: user2)
|
||||
expect(user1.in_same_crew_as?(user2)).to be true
|
||||
end
|
||||
|
||||
it 'returns false when users are in different crews' do
|
||||
crew2 = create(:crew)
|
||||
create(:crew_membership, :captain, crew: crew, user: user1)
|
||||
create(:crew_membership, :captain, crew: crew2, user: user2)
|
||||
expect(user1.in_same_crew_as?(user2)).to be false
|
||||
end
|
||||
|
||||
it 'returns false when other_user is nil' do
|
||||
create(:crew_membership, :captain, crew: crew, user: user1)
|
||||
expect(user1.in_same_crew_as?(nil)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#crew_role' do
|
||||
let(:user) { create(:user) }
|
||||
let(:crew) { create(:crew) }
|
||||
|
||||
it 'returns nil when user is not in a crew' do
|
||||
expect(user.crew_role).to be_nil
|
||||
end
|
||||
|
||||
it 'returns captain when user is captain' do
|
||||
create(:crew_membership, :captain, crew: crew, user: user)
|
||||
expect(user.crew_role).to eq('captain')
|
||||
end
|
||||
|
||||
it 'returns vice_captain when user is vice captain' do
|
||||
create(:crew_membership, :captain, crew: crew)
|
||||
create(:crew_membership, :vice_captain, crew: crew, user: user)
|
||||
expect(user.crew_role).to eq('vice_captain')
|
||||
end
|
||||
|
||||
it 'returns member when user is regular member' do
|
||||
create(:crew_membership, :captain, crew: crew)
|
||||
create(:crew_membership, crew: crew, user: user)
|
||||
expect(user.crew_role).to eq('member')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#crew_officer?' do
|
||||
let(:user) { create(:user) }
|
||||
let(:crew) { create(:crew) }
|
||||
|
||||
it 'returns false when user is not in a crew' do
|
||||
expect(user.crew_officer?).to be false
|
||||
end
|
||||
|
||||
it 'returns true for captain' do
|
||||
create(:crew_membership, :captain, crew: crew, user: user)
|
||||
expect(user.crew_officer?).to be true
|
||||
end
|
||||
|
||||
it 'returns true for vice captain' do
|
||||
create(:crew_membership, :captain, crew: crew)
|
||||
create(:crew_membership, :vice_captain, crew: crew, user: user)
|
||||
expect(user.crew_officer?).to be true
|
||||
end
|
||||
|
||||
it 'returns false for regular member' do
|
||||
create(:crew_membership, :captain, crew: crew)
|
||||
create(:crew_membership, crew: crew, user: user)
|
||||
expect(user.crew_officer?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe '#crew_captain?' do
|
||||
let(:user) { create(:user) }
|
||||
let(:crew) { create(:crew) }
|
||||
|
||||
it 'returns false when user is not in a crew' do
|
||||
expect(user.crew_captain?).to be false
|
||||
end
|
||||
|
||||
it 'returns true for captain' do
|
||||
create(:crew_membership, :captain, crew: crew, user: user)
|
||||
expect(user.crew_captain?).to be true
|
||||
end
|
||||
|
||||
it 'returns false for vice captain' do
|
||||
create(:crew_membership, :captain, crew: crew)
|
||||
create(:crew_membership, :vice_captain, crew: crew, user: user)
|
||||
expect(user.crew_captain?).to be false
|
||||
end
|
||||
|
||||
it 'returns false for regular member' do
|
||||
create(:crew_membership, :captain, crew: crew)
|
||||
create(:crew_membership, crew: crew, user: user)
|
||||
expect(user.crew_captain?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'collection associations behavior' do
|
||||
|
|
|
|||
|
|
@ -39,6 +39,15 @@ rescue ActiveRecord::PendingMigrationError => e
|
|||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
# -----------------------------------------------------------------------------
|
||||
# Use transactional fixtures:
|
||||
#
|
||||
# Wrap each example in a database transaction that is rolled back after the
|
||||
# example completes. This ensures test isolation without needing to truncate
|
||||
# or delete data between tests.
|
||||
# -----------------------------------------------------------------------------
|
||||
config.use_transactional_fixtures = true
|
||||
|
||||
# Disable ActiveRecord logging during tests for a cleaner test output.
|
||||
ActiveRecord::Base.logger = nil if Rails.env.test?
|
||||
|
||||
|
|
|
|||
144
spec/requests/crew_memberships_controller_spec.rb
Normal file
144
spec/requests/crew_memberships_controller_spec.rb
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Crew Memberships API', type: :request do
|
||||
let(:captain) { create(:user) }
|
||||
let(:vice_captain) { create(:user) }
|
||||
let(:member) { create(:user) }
|
||||
let(:crew) { create(:crew) }
|
||||
|
||||
let(:captain_token) do
|
||||
Doorkeeper::AccessToken.create!(resource_owner_id: captain.id, expires_in: 30.days, scopes: 'public')
|
||||
end
|
||||
let(:vc_token) do
|
||||
Doorkeeper::AccessToken.create!(resource_owner_id: vice_captain.id, expires_in: 30.days, scopes: 'public')
|
||||
end
|
||||
let(:member_token) do
|
||||
Doorkeeper::AccessToken.create!(resource_owner_id: member.id, expires_in: 30.days, scopes: 'public')
|
||||
end
|
||||
|
||||
let(:captain_headers) do
|
||||
{ 'Authorization' => "Bearer #{captain_token.token}", 'Content-Type' => 'application/json' }
|
||||
end
|
||||
let(:vc_headers) do
|
||||
{ 'Authorization' => "Bearer #{vc_token.token}", 'Content-Type' => 'application/json' }
|
||||
end
|
||||
let(:member_headers) do
|
||||
{ 'Authorization' => "Bearer #{member_token.token}", 'Content-Type' => 'application/json' }
|
||||
end
|
||||
|
||||
before do
|
||||
create(:crew_membership, :captain, crew: crew, user: captain)
|
||||
create(:crew_membership, :vice_captain, crew: crew, user: vice_captain)
|
||||
create(:crew_membership, crew: crew, user: member)
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/crews/:crew_id/memberships/:id' do
|
||||
context 'as captain' do
|
||||
it 'removes a member' do
|
||||
membership = crew.crew_memberships.find_by(user: member)
|
||||
|
||||
delete "/api/v1/crews/#{crew.id}/memberships/#{membership.id}", headers: captain_headers
|
||||
|
||||
expect(response).to have_http_status(:no_content)
|
||||
membership.reload
|
||||
expect(membership.retired).to be true
|
||||
end
|
||||
|
||||
it 'cannot remove the captain' do
|
||||
captain_membership = crew.crew_memberships.find_by(user: captain)
|
||||
|
||||
delete "/api/v1/crews/#{crew.id}/memberships/#{captain_membership.id}", headers: captain_headers
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json['message']).to eq('Cannot remove the captain from the crew')
|
||||
end
|
||||
end
|
||||
|
||||
context 'as vice captain' do
|
||||
it 'removes a member' do
|
||||
membership = crew.crew_memberships.find_by(user: member)
|
||||
|
||||
delete "/api/v1/crews/#{crew.id}/memberships/#{membership.id}", headers: vc_headers
|
||||
|
||||
expect(response).to have_http_status(:no_content)
|
||||
end
|
||||
end
|
||||
|
||||
context 'as member' do
|
||||
it 'returns unauthorized' do
|
||||
other_member = create(:user)
|
||||
create(:crew_membership, crew: crew, user: other_member)
|
||||
membership = crew.crew_memberships.find_by(user: other_member)
|
||||
|
||||
delete "/api/v1/crews/#{crew.id}/memberships/#{membership.id}", headers: member_headers
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/crews/:crew_id/memberships/:id/promote' do
|
||||
it 'promotes a member to vice captain' do
|
||||
membership = crew.crew_memberships.find_by(user: member)
|
||||
|
||||
post "/api/v1/crews/#{crew.id}/memberships/#{membership.id}/promote", headers: captain_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 vice captain limit reached' do
|
||||
# Add 2 more vice captains (already have 1)
|
||||
2.times do
|
||||
vc_user = create(:user)
|
||||
create(:crew_membership, :vice_captain, crew: crew, user: vc_user)
|
||||
end
|
||||
|
||||
membership = crew.crew_memberships.find_by(user: member)
|
||||
|
||||
post "/api/v1/crews/#{crew.id}/memberships/#{membership.id}/promote", headers: captain_headers
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json['message']).to eq('Crew can only have up to 3 vice captains')
|
||||
end
|
||||
|
||||
it 'requires captain role' do
|
||||
membership = crew.crew_memberships.find_by(user: member)
|
||||
|
||||
post "/api/v1/crews/#{crew.id}/memberships/#{membership.id}/promote", headers: vc_headers
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/crews/:crew_id/memberships/:id/demote' do
|
||||
it 'demotes a vice captain to member' do
|
||||
membership = crew.crew_memberships.find_by(user: vice_captain)
|
||||
|
||||
post "/api/v1/crews/#{crew.id}/memberships/#{membership.id}/demote", headers: captain_headers
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json['membership']['role']).to eq('member')
|
||||
end
|
||||
|
||||
it 'cannot demote the captain' do
|
||||
captain_membership = crew.crew_memberships.find_by(user: captain)
|
||||
|
||||
post "/api/v1/crews/#{crew.id}/memberships/#{captain_membership.id}/demote", headers: captain_headers
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
it 'requires captain role' do
|
||||
membership = crew.crew_memberships.find_by(user: member)
|
||||
|
||||
post "/api/v1/crews/#{crew.id}/memberships/#{membership.id}/demote", headers: vc_headers
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
253
spec/requests/crews_controller_spec.rb
Normal file
253
spec/requests/crews_controller_spec.rb
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Crews API', type: :request do
|
||||
let(:user) { create(:user) }
|
||||
let(:other_user) { create(:user) }
|
||||
let(:access_token) do
|
||||
Doorkeeper::AccessToken.create!(resource_owner_id: user.id, expires_in: 30.days, scopes: 'public')
|
||||
end
|
||||
let(:other_access_token) do
|
||||
Doorkeeper::AccessToken.create!(resource_owner_id: other_user.id, expires_in: 30.days, scopes: 'public')
|
||||
end
|
||||
let(:headers) do
|
||||
{ 'Authorization' => "Bearer #{access_token.token}", 'Content-Type' => 'application/json' }
|
||||
end
|
||||
let(:other_headers) do
|
||||
{ 'Authorization' => "Bearer #{other_access_token.token}", 'Content-Type' => 'application/json' }
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/crews' do
|
||||
let(:valid_params) do
|
||||
{
|
||||
crew: {
|
||||
name: 'Test Crew',
|
||||
gamertag: 'TEST',
|
||||
granblue_crew_id: '12345678',
|
||||
description: 'A test crew'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates a crew and makes user captain' do
|
||||
post '/api/v1/crews', params: valid_params.to_json, headers: headers
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json['crew']['name']).to eq('Test Crew')
|
||||
expect(json['crew']['gamertag']).to eq('TEST')
|
||||
|
||||
user.reload
|
||||
expect(user.crew).to be_present
|
||||
expect(user.crew_captain?).to be true
|
||||
end
|
||||
|
||||
it 'returns error if user already in a crew' do
|
||||
crew = create(:crew)
|
||||
create(:crew_membership, :captain, crew: crew, user: user)
|
||||
|
||||
post '/api/v1/crews', params: valid_params.to_json, headers: headers
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json['message']).to eq('You are already in a crew')
|
||||
end
|
||||
|
||||
it 'returns unauthorized without authentication' do
|
||||
post '/api/v1/crews', params: valid_params.to_json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'validates crew name presence' do
|
||||
invalid_params = { crew: { name: '' } }
|
||||
post '/api/v1/crews', params: invalid_params.to_json, headers: headers
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/crew' do
|
||||
let(:crew) { create(:crew) }
|
||||
|
||||
before do
|
||||
create(:crew_membership, :captain, crew: crew, user: user)
|
||||
end
|
||||
|
||||
it 'returns the current user crew' do
|
||||
get '/api/v1/crew', headers: headers
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json['crew']['name']).to eq(crew.name)
|
||||
end
|
||||
|
||||
it 'returns 404 if user has no crew' do
|
||||
user.active_crew_membership.retire!
|
||||
|
||||
get '/api/v1/crew', headers: headers
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'returns unauthorized without authentication' do
|
||||
get '/api/v1/crew'
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v1/crew' do
|
||||
let(:crew) { create(:crew) }
|
||||
|
||||
context 'as captain' do
|
||||
before do
|
||||
create(:crew_membership, :captain, crew: crew, user: user)
|
||||
end
|
||||
|
||||
it 'updates the crew' do
|
||||
put '/api/v1/crew', params: { crew: { name: 'New Name' } }.to_json, headers: headers
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json['crew']['name']).to eq('New Name')
|
||||
end
|
||||
end
|
||||
|
||||
context 'as vice captain' do
|
||||
before do
|
||||
create(:crew_membership, :captain, crew: crew)
|
||||
create(:crew_membership, :vice_captain, crew: crew, user: user)
|
||||
end
|
||||
|
||||
it 'updates the crew' do
|
||||
put '/api/v1/crew', params: { crew: { name: 'New Name' } }.to_json, headers: headers
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context 'as member' do
|
||||
before do
|
||||
create(:crew_membership, :captain, crew: crew)
|
||||
create(:crew_membership, crew: crew, user: user)
|
||||
end
|
||||
|
||||
it 'returns unauthorized' do
|
||||
put '/api/v1/crew', params: { crew: { name: 'New Name' } }.to_json, headers: headers
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/crew/members' do
|
||||
let(:crew) { create(:crew) }
|
||||
let(:captain) { create(:user) }
|
||||
let(:member) { create(:user) }
|
||||
|
||||
before do
|
||||
create(:crew_membership, :captain, crew: crew, user: captain)
|
||||
create(:crew_membership, crew: crew, user: user)
|
||||
create(:crew_membership, crew: crew, user: member)
|
||||
end
|
||||
|
||||
it 'returns all active members' do
|
||||
get '/api/v1/crew/members', headers: headers
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json['members'].length).to eq(3)
|
||||
end
|
||||
|
||||
it 'does not include retired members' do
|
||||
crew.crew_memberships.find_by(user: member).retire!
|
||||
|
||||
get '/api/v1/crew/members', headers: headers
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json['members'].length).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/crew/leave' do
|
||||
let(:crew) { create(:crew) }
|
||||
|
||||
context 'as regular member' do
|
||||
before do
|
||||
create(:crew_membership, :captain, crew: crew)
|
||||
create(:crew_membership, crew: crew, user: user)
|
||||
end
|
||||
|
||||
it 'retires the membership' do
|
||||
post '/api/v1/crew/leave', headers: headers
|
||||
|
||||
expect(response).to have_http_status(:no_content)
|
||||
user.reload
|
||||
expect(user.crew).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'as captain' do
|
||||
before do
|
||||
create(:crew_membership, :captain, crew: crew, user: user)
|
||||
end
|
||||
|
||||
it 'returns error' do
|
||||
post '/api/v1/crew/leave', headers: headers
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
json = JSON.parse(response.body)
|
||||
expect(json['message']).to eq('Captain must transfer ownership before leaving')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not in crew' do
|
||||
it 'returns error' do
|
||||
post '/api/v1/crew/leave', headers: headers
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/crews/:id/transfer_captain' do
|
||||
let(:crew) { create(:crew) }
|
||||
let(:vice_captain) { create(:user) }
|
||||
|
||||
before do
|
||||
create(:crew_membership, :captain, crew: crew, user: user)
|
||||
create(:crew_membership, :vice_captain, crew: crew, user: vice_captain)
|
||||
end
|
||||
|
||||
it 'transfers captain role to another member' do
|
||||
post "/api/v1/crews/#{crew.id}/transfer_captain",
|
||||
params: { user_id: vice_captain.id }.to_json,
|
||||
headers: headers
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
user.reload
|
||||
vice_captain.reload
|
||||
expect(user.crew_role).to eq('vice_captain')
|
||||
expect(vice_captain.crew_role).to eq('captain')
|
||||
end
|
||||
|
||||
it 'returns error if target user is not in crew' do
|
||||
post "/api/v1/crews/#{crew.id}/transfer_captain",
|
||||
params: { user_id: other_user.id }.to_json,
|
||||
headers: headers
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'requires captain role' do
|
||||
create(:crew_membership, crew: crew, user: other_user)
|
||||
|
||||
post "/api/v1/crews/#{crew.id}/transfer_captain",
|
||||
params: { user_id: vice_captain.id }.to_json,
|
||||
headers: other_headers
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue