# frozen_string_literal: true require 'rails_helper' RSpec.describe 'GridWeapons API', type: :request do let(:user) { create(:user) } # By default, we create a party owned by the user with edit_key 'secret' let(:party) { create(:party, user: user, edit_key: 'secret') } let(:access_token) do Doorkeeper::AccessToken.create!( resource_owner_id: user.id, expires_in: 30.days, scopes: 'public' ) end let(:headers) do { 'Authorization' => "Bearer #{access_token.token}", 'Content-Type' => 'application/json', } end let(:weapon) { Weapon.find_by!(granblue_id: '1040611300') } let(:incoming_weapon) { Weapon.find_by!(granblue_id: '1040912100') } describe 'Authorization for editing grid weapons' do context 'when the party is owned by a logged in user' do let(:weapon_params) do { weapon: { party_id: party.id, weapon_id: weapon.id, position: 0, mainhand: true, uncap_level: 3, transcendence_step: 0, element: weapon.element, } } end it 'allows the owner to create a grid weapon' do expect do post '/api/v1/weapons', params: weapon_params.to_json, headers: headers end.to change(GridWeapon, :count).by(1) expect(response).to have_http_status(:created) end it 'rejects a logged-in user that does not own the party' do # Create a party owned by a different user. other_user = create(:user) party_owned_by_other = create(:party, user: other_user, edit_key: 'secret') weapon_params[:weapon][:party_id] = party_owned_by_other.id post '/api/v1/weapons', params: weapon_params.to_json, headers: headers expect(response).to have_http_status(:unauthorized) end end context 'when the party is anonymous (no user)' do # Override party to be anonymous with its own edit_key. let(:headers) { super().merge('X-Edit-Key' => 'anonsecret') } let(:party) { create(:party, user: nil, edit_key: 'anonsecret') } let(:anon_params) do { weapon: { party_id: party.id, weapon_id: weapon.id, position: 0, mainhand: true, uncap_level: 3, transcendence_step: 0, element: weapon.element, } } end it 'allows editing with correct edit_key' do expect { post '/api/v1/weapons', params: anon_params.to_json, headers: headers } .to change(GridWeapon, :count).by(1) expect(response).to have_http_status(:created) end context 'when an incorrect edit_key is provided' do # Override the edit_key (simulate invalid key) let(:headers) { super().merge('X-Edit-Key' => 'wrong') } it 'returns an unauthorized response' do post '/api/v1/weapons', params: anon_params.to_json, headers: headers expect(response).to have_http_status(:unauthorized) end end end end describe 'POST /api/v1/weapons (create action)' do context 'with valid parameters' do let(:valid_params) do { weapon: { party_id: party.id, weapon_id: weapon.id, position: 0, mainhand: true, uncap_level: 3, transcendence_step: 0, element: weapon.element, weapon_key1_id: nil, weapon_key2_id: nil, weapon_key3_id: nil, ax_modifier1: nil, ax_modifier2: nil, ax_strength1: nil, ax_strength2: nil, awakening_id: nil, awakening_level: 1, } } end it 'creates a grid weapon and returns status created' do expect { post '/api/v1/weapons', params: valid_params.to_json, headers: headers } .to change(GridWeapon, :count).by(1) expect(response).to have_http_status(:created) json_response = JSON.parse(response.body) expect(json_response).to have_key('grid_weapon') expect(json_response['grid_weapon']).to include('position' => 0) end end context 'with invalid parameters' do let(:invalid_params) do { weapon: { party_id: party.id, weapon_id: nil, # Missing required weapon_id position: 0, mainhand: true, uncap_level: 3, transcendence_step: 0 } } end it 'returns unprocessable entity status with errors' do post '/api/v1/weapons', params: invalid_params.to_json, headers: headers expect(response).to have_http_status(:unprocessable_entity) json_response = JSON.parse(response.body) expect(json_response).to have_key('errors') end end context 'when unauthorized (invalid edit key)' do # For this test, use an anonymous party so that edit key checking is applied. let(:party) { create(:party, user: nil, edit_key: 'anonsecret') } let(:valid_params) do { weapon: { party_id: party.id, weapon_id: weapon.id, position: 0, mainhand: true, uncap_level: 3, transcendence_step: 0, element: weapon.element, } } end let(:unauthorized_headers) { headers.merge('X-Edit-Key' => 'wrong') } it 'returns an unauthorized response' do post '/api/v1/weapons', params: valid_params.to_json, headers: unauthorized_headers expect(response).to have_http_status(:unauthorized) end end end describe 'PUT /api/v1/grid_weapons/:id (update action)' do let!(:grid_weapon) do create(:grid_weapon, party: party, weapon: weapon, position: 2, uncap_level: 3, transcendence_step: 0, mainhand: false) end let(:update_params) do { weapon: { id: grid_weapon.id, party_id: party.id, weapon_id: weapon.id, position: 2, mainhand: false, uncap_level: 4, transcendence_step: 1, element: weapon.element, weapon_key1_id: nil, weapon_key2_id: nil, weapon_key3_id: nil, ax_modifier1: nil, ax_modifier2: nil, ax_strength1: nil, ax_strength2: nil, awakening_id: nil, awakening_level: 1 } } end it 'updates the grid weapon and returns the updated record' do put "/api/v1/grid_weapons/#{grid_weapon.id}", params: update_params.to_json, headers: headers expect(response).to have_http_status(:ok) json_response = JSON.parse(response.body) expect(json_response['grid_weapon']).to include('mainhand' => false, 'uncap_level' => 4) end end describe 'POST /api/v1/weapons/update_uncap (update uncap level action)' do before do # For this test, update the weapon so that its conditions dictate a maximum uncap of 5. weapon.update!(flb: false, ulb: true, transcendence: false) end let!(:grid_weapon) do create(:grid_weapon, party: party, weapon: weapon, position: 3, uncap_level: 3, transcendence_step: 0) end let(:update_uncap_params) do { weapon: { id: grid_weapon.id, # now nested inside the weapon hash party_id: party.id, weapon_id: weapon.id, uncap_level: 6, # attempt above allowed; should be capped at 5 transcendence_step: 0 } } end it 'updates the uncap level to 5 for the grid weapon' do post '/api/v1/weapons/update_uncap', params: update_uncap_params.to_json, headers: headers expect(response).to have_http_status(:ok) json_response = JSON.parse(response.body) expect(json_response['grid_weapon']).to include('uncap_level' => 5) end end describe 'POST /api/v1/weapons/resolve (conflict resolution action)' do let!(:conflicting_weapon) do create(:grid_weapon, party: party, weapon: weapon, position: 5, uncap_level: 3) end before do # Set up the incoming weapon with flags such that: default uncap is 3, # but if flb is true then uncap should become 4. incoming_weapon.update!(flb: true, ulb: false, transcendence: false) end let(:resolve_params) do { resolve: { position: 5, incoming: incoming_weapon.id, conflicting: [conflicting_weapon.id] } } end it 'resolves conflicts by destroying conflicting grid weapons and creating a new one' do expect(GridWeapon.exists?(conflicting_weapon.id)).to be true # The net change should be zero: one grid weapon is destroyed and one is created. expect { post '/api/v1/weapons/resolve', params: resolve_params.to_json, headers: headers } .to change(GridWeapon, :count).by(0) expect(response).to have_http_status(:created) json_response = JSON.parse(response.body) expect(json_response).to have_key('grid_weapon') # According to the controller logic, with incoming.flb true, the uncap level should be 4. expect(json_response['grid_weapon']).to include('uncap_level' => 4) expect { conflicting_weapon.reload }.to raise_error(ActiveRecord::RecordNotFound) end end describe 'DELETE /api/v1/grid_weapons/:id (destroy action)' do context 'when the party is owned by a logged in user' do let!(:grid_weapon) do create(:grid_weapon, party: party, weapon: weapon, position: 4, uncap_level: 3) end it 'destroys the grid weapon and returns a success response' do expect { delete "/api/v1/grid_weapons/#{grid_weapon.id}", headers: headers } .to change(GridWeapon, :count).by(-1) expect(response).to have_http_status(:ok) end it 'returns not found when trying to delete a non-existent grid weapon' do delete '/api/v1/grid_weapons/00000000-0000-0000-0000-000000000000', headers: headers expect(response).to have_http_status(:not_found) end end context 'when the party is anonymous (no user)' do # For anonymous users, we override both the party and header edit key. let(:headers) { super().merge('X-Edit-Key' => 'anonsecret') } let(:party) { create(:party, user: nil, edit_key: 'anonsecret') } let!(:grid_weapon) do create(:grid_weapon, party: party, weapon: weapon, position: 4, uncap_level: 3) end it 'allows anonymous user to destroy grid weapon with correct edit key' do expect { delete "/api/v1/grid_weapons/#{grid_weapon.id}", headers: headers } .to change(GridWeapon, :count).by(-1) expect(response).to have_http_status(:ok) end it 'prevents destruction with incorrect edit key' do wrong_headers = headers.merge('X-Edit-Key' => 'wrong') delete "/api/v1/grid_weapons/#{grid_weapon.id}", headers: wrong_headers expect(response).to have_http_status(:unauthorized) end it 'prevents deletion when a logged in user attempts to delete an anonymous grid weapon' do # When a logged in user (with an access token) tries to delete a grid weapon # that belongs to an anonymous party, authorization should fail. auth_headers = headers.except('X-Edit-Key') expect { delete "/api/v1/grid_weapons/#{grid_weapon.id}", headers: auth_headers } .not_to change(GridWeapon, :count) expect(response).to have_http_status(:unauthorized) end end end # Debug hook: if any example fails and a response exists, print the error message. after(:each) do |example| if example.exception && defined?(response) && response.present? error_message = begin JSON.parse(response.body)['exception'] rescue JSON::ParserError response.body end puts "\nDEBUG: Error Message for '#{example.full_description}': #{error_message}" # Parse once and grab the trace safely parsed_body = JSON.parse(response.body) trace = parsed_body.dig('traces', 'Application Trace') ap trace if trace # Only print if trace is not nil end end end