* Install Rspec * Create .aidigestignore * Update rails_helper - Added sections and comments - Add support for loading via canonical.rb - Add FactoryBot syntax methods - Disable SQL logging in test environment * Move gems around * Add canonical.rb and test env CSVs We load these CSVs via canonical.rb when we run tests as a data source for canonical objects. * Remove RBS for now This is too much and we need to find the right solution * Refactor GridSummonsController and add tests * Create GridSummon factory * Refactor GridSummon and add documentation and tests * Create have_error_on.rb * Update .aidigestignore * Fix warnings * Add GridWeapons and Parties factories * Refactor GridWeapon and add documentation and tests * Create .rubocop.yml * Create no_weapon_provided_error.rb * Refactor GridWeaponsController - Refactors controller - Adds YARD documentation - Adds Rspec tests * Refactor GridSummonsController - Refactors controller - Adds YARD documentation - Adds Rspec tests * Enable shoulda/matchers * Update User factory * Update party.rb We moved updating the party's element and extra flag to inside the party. We use an after_commit hook to minimize the amount of queries we're running to do this. * Update party.rb We change setting the edit key to use the conditional assignment operator so that it doesn't get overridden when we're running tests. This shouldn't have an effect in production. * Update api_controller.rb Change render_unprocessable_entity_response to render the errors hash instead of the exception so that we get more helpful errors. * Add new errors Added NoCharacterProvidedError and NoSummonProvidedError * Add tests and docs to GridCharacter We added a factory, spec and documentation to the GridCharacter model * Ensure numericality * Move enums into GranblueEnums We don't use these yet, but it gives us a structured place to pull them from. * Refactor GridCharactersController - Refactors controller - Adds YARD documentation - Adds Rspec tests * Add debug hook and other small changes * Update grid_characters_controller.rb Removes logs * Update .gitignore * Update .aidigestignore * Refactored PartiesController - Split PartiesController into three concerns - Implemented testing for PartiesController and two concerns - Implemented fixes across other files to ensure PartiesController tests pass - Added Favorites factory * Implement SimpleCov * Refactor Party model - Refactors Party model - Adds tests - Adds documentation * Update granblue_enums.rb Remove included block
364 lines
12 KiB
Ruby
364 lines
12 KiB
Ruby
# 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
|