Refactor GridCharactersController
- Refactors controller - Adds YARD documentation - Adds Rspec tests
This commit is contained in:
parent
3b560d07cc
commit
7b3d353e19
2 changed files with 614 additions and 259 deletions
|
|
@ -2,73 +2,117 @@
|
||||||
|
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
|
##
|
||||||
|
# Controller handling API requests related to grid characters within a party.
|
||||||
|
#
|
||||||
|
# This controller provides endpoints for creating, updating, resolving conflicts,
|
||||||
|
# updating uncap levels, and deleting grid characters. It follows the structure of
|
||||||
|
# GridSummonsController and GridWeaponsController by using the new authorization method
|
||||||
|
# `authorize_party_edit!` and deprecating legacy methods such as `set` in favor of
|
||||||
|
# `find_party`, `find_grid_character`, and `find_incoming_character`.
|
||||||
|
#
|
||||||
|
# @see Api::V1::ApiController for shared API behavior.
|
||||||
class GridCharactersController < Api::V1::ApiController
|
class GridCharactersController < Api::V1::ApiController
|
||||||
attr_reader :party, :incoming_character, :current_characters
|
before_action :find_grid_character, only: %i[update update_uncap_level destroy resolve]
|
||||||
|
before_action :find_party, only: %i[create resolve update update_uncap_level destroy]
|
||||||
before_action :find_party, only: :create
|
|
||||||
before_action :set, only: %i[update destroy]
|
|
||||||
before_action :authorize, only: %i[create update destroy]
|
|
||||||
before_action :find_incoming_character, only: :create
|
before_action :find_incoming_character, only: :create
|
||||||
before_action :find_current_characters, only: :create
|
before_action :authorize_party_edit!, only: %i[create resolve update update_uncap_level destroy]
|
||||||
|
|
||||||
|
##
|
||||||
|
# Creates a new grid character.
|
||||||
|
#
|
||||||
|
# If a conflicting grid character is found (i.e. one with the same character_id already exists
|
||||||
|
# in the party), a conflict view is rendered so the user can decide on removal. Otherwise,
|
||||||
|
# any grid character occupying the desired position is removed and a new one is created.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
def create
|
def create
|
||||||
if !conflict_characters.nil? && conflict_characters.length.positive?
|
processed_params = transform_character_params(character_params)
|
||||||
# Render a template with the conflicting and incoming characters,
|
|
||||||
# as well as the selected position, so the user can be presented with
|
|
||||||
# a decision.
|
|
||||||
|
|
||||||
# Up to 3 characters can be removed at the same time
|
if conflict_characters.present?
|
||||||
conflict_view = render_conflict_view(conflict_characters, incoming_character, character_params[:position])
|
render json: render_conflict_view(conflict_characters, @incoming_character, character_params[:position])
|
||||||
render json: conflict_view
|
|
||||||
else
|
else
|
||||||
# Destroy the grid character in the position if it is already filled
|
# Remove any existing grid character occupying the specified position.
|
||||||
if GridCharacter.where(party_id: party.id, position: character_params[:position]).exists?
|
if (existing = GridCharacter.find_by(party_id: @party.id, position: character_params[:position]))
|
||||||
character = GridCharacter.where(party_id: party.id, position: character_params[:position]).limit(1)[0]
|
existing.destroy
|
||||||
character.destroy
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Then, create a new grid character
|
# Build the new grid character
|
||||||
character = GridCharacter.create!(character_params.merge(party_id: party.id,
|
grid_character = build_new_grid_character(processed_params)
|
||||||
character_id: incoming_character.id))
|
|
||||||
|
|
||||||
if character.save!
|
if grid_character.save
|
||||||
grid_character_view = render_grid_character_view(character)
|
render json: GridCharacterBlueprint.render(grid_character,
|
||||||
render json: grid_character_view, status: :created
|
root: :grid_character,
|
||||||
|
view: :nested), status: :created
|
||||||
|
else
|
||||||
|
render_validation_error_response(grid_character)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Updates an existing grid character.
|
||||||
|
#
|
||||||
|
# Assigns new rings and awakening data to their respective virtual attributes and updates other
|
||||||
|
# permitted attributes. On success, the updated grid character view is rendered.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
def update
|
def update
|
||||||
permitted = character_params.to_h.deep_symbolize_keys
|
processed_params = transform_character_params(character_params)
|
||||||
puts "Permitted:"
|
assign_raw_attributes(@grid_character)
|
||||||
ap permitted
|
assign_transformed_attributes(@grid_character, processed_params)
|
||||||
|
|
||||||
# For the new nested structure, assign them to the virtual attributes:
|
if @grid_character.save
|
||||||
@character.new_rings = permitted[:rings] if permitted[:rings].present?
|
render json: GridCharacterBlueprint.render(@grid_character,
|
||||||
@character.new_awakening = permitted[:awakening] if permitted[:awakening].present?
|
root: :grid_character,
|
||||||
|
view: :nested)
|
||||||
# For the rest of the attributes, you can assign them normally.
|
|
||||||
@character.assign_attributes(permitted.except(:rings, :awakening))
|
|
||||||
|
|
||||||
if @character.save
|
|
||||||
render json: GridCharacterBlueprint.render(@character, view: :nested)
|
|
||||||
else
|
else
|
||||||
render_validation_error_response(@character)
|
ap "you are Here"
|
||||||
|
ap @grid_character.errors
|
||||||
|
render_validation_error_response(@grid_character)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Updates the uncap level and transcendence step of a grid character.
|
||||||
|
#
|
||||||
|
# The grid character's uncap level and transcendence step are updated based on the provided parameters.
|
||||||
|
# This action requires that the current user is authorized to modify the party.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def update_uncap_level
|
||||||
|
@grid_character.uncap_level = character_params[:uncap_level]
|
||||||
|
@grid_character.transcendence_step = character_params[:transcendence_step]
|
||||||
|
|
||||||
|
if @grid_character.save
|
||||||
|
render json: GridCharacterBlueprint.render(@grid_character,
|
||||||
|
root: :grid_character,
|
||||||
|
view: :nested)
|
||||||
|
else
|
||||||
|
render_validation_error_response(@grid_character)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Resolves conflicts for grid characters.
|
||||||
|
#
|
||||||
|
# This action destroys any conflicting grid characters as well as any grid character occupying
|
||||||
|
# the target position, then creates a new grid character using a computed default uncap level.
|
||||||
|
# The default uncap level is determined by the incoming character's attributes.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
def resolve
|
def resolve
|
||||||
incoming = Character.find(resolve_params[:incoming])
|
incoming = Character.find_by(id: resolve_params[:incoming])
|
||||||
conflicting = resolve_params[:conflicting].map { |id| GridCharacter.find(id) }
|
render_not_found_response('character') and return unless incoming
|
||||||
party = conflicting.first.party
|
|
||||||
|
|
||||||
# Destroy each conflicting character
|
conflicting = resolve_params[:conflicting].map { |id| GridCharacter.find_by(id: id) }.compact
|
||||||
conflicting.each { |character| GridCharacter.destroy(character.id) }
|
conflicting.each(&:destroy)
|
||||||
|
|
||||||
# Destroy the character at the desired position if it exists
|
if (existing = GridCharacter.find_by(party_id: @party.id, position: resolve_params[:position]))
|
||||||
existing_character = GridCharacter.where(party: party.id, position: resolve_params[:position]).first
|
existing.destroy
|
||||||
GridCharacter.destroy(existing_character.id) if existing_character
|
end
|
||||||
|
|
||||||
|
# Compute the default uncap level based on the incoming character's flags.
|
||||||
if incoming.special
|
if incoming.special
|
||||||
uncap_level = 3
|
uncap_level = 3
|
||||||
uncap_level = 5 if incoming.ulb
|
uncap_level = 5 if incoming.ulb
|
||||||
|
|
@ -79,33 +123,146 @@ module Api
|
||||||
uncap_level = 5 if incoming.flb
|
uncap_level = 5 if incoming.flb
|
||||||
end
|
end
|
||||||
|
|
||||||
character = GridCharacter.create!(party_id: party.id, character_id: incoming.id,
|
grid_character = GridCharacter.create!(
|
||||||
position: resolve_params[:position], uncap_level: uncap_level)
|
party_id: @party.id,
|
||||||
render json: GridCharacterBlueprint.render(character, view: :nested), status: :created if character.save!
|
character_id: incoming.id,
|
||||||
|
position: resolve_params[:position],
|
||||||
|
uncap_level: uncap_level
|
||||||
|
)
|
||||||
|
render json: GridCharacterBlueprint.render(grid_character,
|
||||||
|
root: :grid_character,
|
||||||
|
view: :nested), status: :created
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_uncap_level
|
##
|
||||||
character = GridCharacter.find(character_params[:id])
|
# Destroys a grid character.
|
||||||
|
#
|
||||||
render_unauthorized_response if current_user && (character.party.user != current_user)
|
# If the current user is not the owner of the party, an unauthorized response is rendered.
|
||||||
|
# On successful destruction, the destroyed grid character view is rendered.
|
||||||
character.uncap_level = character_params[:uncap_level]
|
#
|
||||||
character.transcendence_step = character_params[:transcendence_step]
|
# @return [void]
|
||||||
return unless character.save!
|
|
||||||
|
|
||||||
render json: GridCharacterBlueprint.render(character, view: :nested, root: :grid_character)
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: Implement removing characters
|
|
||||||
def destroy
|
def destroy
|
||||||
render_unauthorized_response if @character.party.user != current_user
|
grid_character = GridCharacter.find_by('id = ?', params[:id])
|
||||||
return render json: GridCharacterBlueprint.render(@character, view: :destroyed) if @character.destroy
|
|
||||||
|
return render_not_found_response('grid_character') if grid_character.nil?
|
||||||
|
|
||||||
|
render json: GridCharacterBlueprint.render(grid_character, view: :destroyed) if grid_character.destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
##
|
||||||
|
# Builds a new grid character using the transformed parameters.
|
||||||
|
#
|
||||||
|
# @param processed_params [Hash] the transformed parameters.
|
||||||
|
# @return [GridCharacter] the newly built grid character.
|
||||||
|
def build_new_grid_character(processed_params)
|
||||||
|
grid_character = GridCharacter.new(
|
||||||
|
character_params.except(:rings, :awakening).merge(
|
||||||
|
party_id: @party.id,
|
||||||
|
character_id: @incoming_character.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assign_transformed_attributes(grid_character, processed_params)
|
||||||
|
assign_raw_attributes(grid_character)
|
||||||
|
grid_character
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Assigns raw attributes from the original parameters to the grid character.
|
||||||
|
#
|
||||||
|
# These attributes (like new_rings and new_awakening) are used by model callbacks.
|
||||||
|
#
|
||||||
|
# @param grid_character [GridCharacter] the grid character instance.
|
||||||
|
# @return [void]
|
||||||
|
def assign_raw_attributes(grid_character)
|
||||||
|
grid_character.new_rings = character_params[:rings] if character_params[:rings].present?
|
||||||
|
grid_character.new_awakening = character_params[:awakening] if character_params[:awakening].present?
|
||||||
|
grid_character.assign_attributes(character_params.except(:rings, :awakening))
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Assigns transformed attributes (such as uncap_level, transcendence_step, etc.) to the grid character.
|
||||||
|
#
|
||||||
|
# @param grid_character [GridCharacter] the grid character instance.
|
||||||
|
# @param processed_params [Hash] the transformed parameters.
|
||||||
|
# @return [void]
|
||||||
|
def assign_transformed_attributes(grid_character, processed_params)
|
||||||
|
grid_character.uncap_level = processed_params[:uncap_level] if processed_params[:uncap_level]
|
||||||
|
grid_character.transcendence_step = processed_params[:transcendence_step] if processed_params[:transcendence_step]
|
||||||
|
grid_character.perpetuity = processed_params[:perpetuity] if processed_params.key?(:perpetuity)
|
||||||
|
grid_character.earring = processed_params[:earring] if processed_params[:earring]
|
||||||
|
|
||||||
|
return unless processed_params[:awakening_id]
|
||||||
|
|
||||||
|
grid_character.awakening_id = processed_params[:awakening_id]
|
||||||
|
grid_character.awakening_level = processed_params[:awakening_level]
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Transforms the incoming character parameters to the required format.
|
||||||
|
#
|
||||||
|
# The frontend sends parameters in a raw format that need to be processed (e.g., converting string
|
||||||
|
# values to integers, handling nested attributes for rings and awakening). This method extracts and
|
||||||
|
# converts only the keys that were provided.
|
||||||
|
#
|
||||||
|
# @param raw_params [ActionController::Parameters] the raw permitted parameters.
|
||||||
|
# @return [Hash] the transformed parameters.
|
||||||
|
def transform_character_params(raw_params)
|
||||||
|
# Convert to a symbolized hash for convenience.
|
||||||
|
raw = raw_params.to_h.deep_symbolize_keys
|
||||||
|
|
||||||
|
# Only update keys that were provided.
|
||||||
|
transformed = raw.slice(:uncap_level, :transcendence_step, :perpetuity)
|
||||||
|
transformed[:uncap_level] = raw[:uncap_level] if raw[:uncap_level].present?
|
||||||
|
transformed[:transcendence_step] = raw[:transcendence_step] if raw[:transcendence_step].present?
|
||||||
|
|
||||||
|
# Process rings if provided.
|
||||||
|
transformed.merge!(transform_rings(raw[:rings])) if raw[:rings].present?
|
||||||
|
|
||||||
|
# Process earring if provided.
|
||||||
|
transformed[:earring] = raw[:earring] if raw[:earring].present?
|
||||||
|
|
||||||
|
# Process awakening if provided.
|
||||||
|
if raw[:awakening].present?
|
||||||
|
transformed[:awakening_id] = raw[:awakening][:id]
|
||||||
|
# Default to 1 if level is missing (to satisfy validations)
|
||||||
|
transformed[:awakening_level] = raw[:awakening][:level].present? ? raw[:awakening][:level] : 1
|
||||||
|
end
|
||||||
|
|
||||||
|
transformed
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Transforms the rings data to ensure exactly four rings are present.
|
||||||
|
#
|
||||||
|
# Pads the array with a default ring hash if necessary.
|
||||||
|
#
|
||||||
|
# @param rings [Array, Hash] the rings data from the frontend.
|
||||||
|
# @return [Hash] a hash with keys :ring1, :ring2, :ring3, :ring4.
|
||||||
|
def transform_rings(rings)
|
||||||
|
default_ring = { modifier: nil, strength: nil }
|
||||||
|
# Ensure rings is an array of hashes.
|
||||||
|
rings_array = Array(rings).map(&:to_h)
|
||||||
|
# Pad the array to exactly four rings if needed.
|
||||||
|
rings_array.fill(default_ring, rings_array.size...4)
|
||||||
|
{
|
||||||
|
ring1: rings_array[0],
|
||||||
|
ring2: rings_array[1],
|
||||||
|
ring3: rings_array[2],
|
||||||
|
ring4: rings_array[3]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Returns any grid characters in the party that conflict with the incoming character.
|
||||||
|
#
|
||||||
|
# Conflict is defined as any grid character already in the party with the same character_id as the
|
||||||
|
# incoming character. This method is used to prompt the user for conflict resolution.
|
||||||
|
#
|
||||||
|
# @return [Array<GridCharacter>]
|
||||||
def conflict_characters
|
def conflict_characters
|
||||||
@conflict_characters ||= find_conflict_characters(incoming_character)
|
@party.characters.where(character_id: @incoming_character.id).to_a
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_conflict_characters(incoming_character)
|
def find_conflict_characters(incoming_character)
|
||||||
|
|
@ -127,67 +284,102 @@ module Api
|
||||||
end.flatten
|
end.flatten
|
||||||
end
|
end
|
||||||
|
|
||||||
def set
|
##
|
||||||
@character = GridCharacter.includes(:awakening).find(params[:id])
|
# Finds and sets the party based on parameters.
|
||||||
end
|
#
|
||||||
|
# Checks for the party id in params[:character][:party_id], params[:party_id], or falls back to the party
|
||||||
def find_incoming_character
|
# associated with the current grid character. Renders a not found response if the party is missing.
|
||||||
@incoming_character = Character.find(character_params[:character_id])
|
#
|
||||||
end
|
# @return [void]
|
||||||
|
|
||||||
def find_party
|
def find_party
|
||||||
@party = Party.find(character_params[:party_id])
|
@party = Party.find_by(id: params.dig(:character, :party_id)) ||
|
||||||
render_unauthorized_response if current_user && (party.user != current_user)
|
Party.find_by(id: params[:party_id]) ||
|
||||||
|
@grid_character&.party
|
||||||
|
render_not_found_response('party') unless @party
|
||||||
end
|
end
|
||||||
|
|
||||||
def authorize
|
##
|
||||||
# Create
|
# Finds and sets the grid character based on the provided parameters.
|
||||||
unauthorized_create = @party && (@party.user != current_user || @party.edit_key != edit_key)
|
#
|
||||||
unauthorized_update = @character && @character.party && (@character.party.user != current_user || @character.party.edit_key != edit_key)
|
# Searches for a grid character by its ID and renders a not found response if it is absent.
|
||||||
|
#
|
||||||
render_unauthorized_response if unauthorized_create || unauthorized_update
|
# @return [void]
|
||||||
|
def find_grid_character
|
||||||
|
grid_character_id = params[:id] || params.dig(:character, :id) || params.dig(:resolve, :conflicting)
|
||||||
|
@grid_character = GridCharacter.includes(:awakening).find_by(id: grid_character_id)
|
||||||
|
render_not_found_response('grid_character') unless @grid_character
|
||||||
end
|
end
|
||||||
|
|
||||||
def transform_character_params(raw_params)
|
##
|
||||||
# Convert to a symbolized hash for convenience.
|
# Finds and sets the incoming character based on the provided parameters.
|
||||||
raw = raw_params.deep_symbolize_keys
|
#
|
||||||
|
# Searches for a character using the :character_id parameter and renders a not found response if it is absent.
|
||||||
# Only update keys that were provided.
|
#
|
||||||
transformed = raw.slice(:uncap_level, :transcendence_step, :perpetuity)
|
# @return [void]
|
||||||
transformed[:uncap_level] = raw[:uncap_level].to_i if raw[:uncap_level].present?
|
def find_incoming_character
|
||||||
transformed[:transcendence_step] = raw[:transcendence_step].to_i if raw[:transcendence_step].present?
|
@incoming_character = Character.find_by(id: character_params[:character_id])
|
||||||
|
render_unprocessable_entity_response(Api::V1::NoCharacterProvidedError.new) unless @incoming_character
|
||||||
# Process rings if provided.
|
|
||||||
transformed.merge!(transform_rings(raw[:rings])) if raw[:rings].present?
|
|
||||||
|
|
||||||
# Process earring if provided.
|
|
||||||
transformed[:earring] = raw[:earring] if raw[:earring].present?
|
|
||||||
|
|
||||||
# Process awakening if provided.
|
|
||||||
if raw[:awakening].present?
|
|
||||||
transformed[:awakening_id] = raw[:awakening][:id]
|
|
||||||
# Default to 1 if level is missing (to satisfy validations)
|
|
||||||
transformed[:awakening_level] = raw[:awakening][:level].present? ? raw[:awakening][:level].to_i : 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
transformed
|
##
|
||||||
|
# Authorizes the current action by ensuring that the current user or provided edit key
|
||||||
|
# matches the party's owner.
|
||||||
|
#
|
||||||
|
# For parties associated with a user, it verifies that the current user is the owner.
|
||||||
|
# For anonymous parties, it compares the provided edit key with the party's edit key.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def authorize_party_edit!
|
||||||
|
if @party.user.present?
|
||||||
|
authorize_user_party
|
||||||
|
else
|
||||||
|
authorize_anonymous_party
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def transform_rings(rings)
|
##
|
||||||
default_ring = { modifier: nil, strength: nil }
|
# Authorizes an action for a party that belongs to a user.
|
||||||
# Ensure rings is an array of hashes.
|
#
|
||||||
rings_array = Array(rings).map(&:to_h)
|
# Renders an unauthorized response unless the current user is present and matches the party's user.
|
||||||
# Pad the array to exactly four rings if needed.
|
#
|
||||||
rings_array.fill(default_ring, rings_array.size...4)
|
# @return [void]
|
||||||
{
|
def authorize_user_party
|
||||||
ring1: rings_array[0],
|
return if current_user.present? && @party.user == current_user
|
||||||
ring2: rings_array[1],
|
|
||||||
ring3: rings_array[2],
|
render_unauthorized_response
|
||||||
ring4: rings_array[3]
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Specify whitelisted properties that can be modified.
|
##
|
||||||
|
# Authorizes an action for an anonymous party using an edit key.
|
||||||
|
#
|
||||||
|
# Compares the provided edit key with the party's edit key and renders an unauthorized response
|
||||||
|
# if they do not match.
|
||||||
|
#
|
||||||
|
# @return [void]
|
||||||
|
def authorize_anonymous_party
|
||||||
|
provided_edit_key = edit_key.to_s.strip.force_encoding('UTF-8')
|
||||||
|
party_edit_key = @party.edit_key.to_s.strip.force_encoding('UTF-8')
|
||||||
|
return if valid_edit_key?(provided_edit_key, party_edit_key)
|
||||||
|
|
||||||
|
render_unauthorized_response
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Validates that the provided edit key matches the party's edit key.
|
||||||
|
#
|
||||||
|
# @param provided_edit_key [String] the edit key provided in the request.
|
||||||
|
# @param party_edit_key [String] the edit key associated with the party.
|
||||||
|
# @return [Boolean] true if the keys match; false otherwise.
|
||||||
|
def valid_edit_key?(provided_edit_key, party_edit_key)
|
||||||
|
provided_edit_key.present? &&
|
||||||
|
provided_edit_key.bytesize == party_edit_key.bytesize &&
|
||||||
|
ActiveSupport::SecurityUtils.secure_compare(provided_edit_key, party_edit_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Specifies and permits the allowed character parameters.
|
||||||
|
#
|
||||||
|
# @return [ActionController::Parameters] the permitted parameters.
|
||||||
def character_params
|
def character_params
|
||||||
params.require(:character).permit(
|
params.require(:character).permit(
|
||||||
:id,
|
:id,
|
||||||
|
|
@ -203,21 +395,13 @@ module Api
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Specifies and permits the allowed resolve parameters.
|
||||||
|
#
|
||||||
|
# @return [ActionController::Parameters] the permitted parameters.
|
||||||
def resolve_params
|
def resolve_params
|
||||||
params.require(:resolve).permit(:position, :incoming, conflicting: [])
|
params.require(:resolve).permit(:position, :incoming, conflicting: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_conflict_view(conflict_characters, incoming_character, incoming_position)
|
|
||||||
ConflictBlueprint.render(nil,
|
|
||||||
view: :characters,
|
|
||||||
conflict_characters: conflict_characters,
|
|
||||||
incoming_character: incoming_character,
|
|
||||||
incoming_position: incoming_position)
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_grid_character_view(grid_character)
|
|
||||||
GridCharacterBlueprint.render(grid_character, view: :nested)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'GridCharacters API', type: :request do
|
RSpec.describe 'GridCharacters API', type: :request do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:party) { create(:party, user: user, edit_key: 'secret') }
|
let(:party) { create(:party, user: user, edit_key: 'secret') }
|
||||||
# Use canonical records seeded into your DB.
|
|
||||||
# For example, assume Rosamia (granblue_id "3040087000") and Seofon (granblue_id "3040036000")
|
|
||||||
let(:rosamia) { Character.find_by(granblue_id: '3040087000') }
|
|
||||||
let(:seofon) { Character.find_by(granblue_id: '3040036000') }
|
|
||||||
let(:access_token) do
|
let(:access_token) do
|
||||||
Doorkeeper::AccessToken.create!(
|
Doorkeeper::AccessToken.create!(
|
||||||
resource_owner_id: user.id,
|
resource_owner_id: user.id,
|
||||||
|
|
@ -18,196 +15,370 @@ RSpec.describe 'GridCharacters API', type: :request do
|
||||||
let(:headers) do
|
let(:headers) do
|
||||||
{
|
{
|
||||||
'Authorization' => "Bearer #{access_token.token}",
|
'Authorization' => "Bearer #{access_token.token}",
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json'
|
||||||
'X-Edit-Key' => 'secret'
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST /api/v1/characters (create)' do
|
# Using canonical data from CSV for non-user-generated models.
|
||||||
context 'when creating a grid character with a unique canonical character (e.g. Seofon)' do
|
let(:incoming_character) { Character.find_by(granblue_id: '3040036000') }
|
||||||
|
|
||||||
|
describe 'Authorization for editing grid characters' do
|
||||||
|
context 'when the party is owned by a logged in user' do
|
||||||
let(:valid_params) do
|
let(:valid_params) do
|
||||||
{
|
{
|
||||||
character: {
|
character: {
|
||||||
party_id: party.id,
|
party_id: party.id,
|
||||||
character_id: seofon.id,
|
character_id: incoming_character.id,
|
||||||
position: 0,
|
position: 1,
|
||||||
uncap_level: 3,
|
uncap_level: 3,
|
||||||
transcendence_step: 0,
|
transcendence_step: 0,
|
||||||
rings: [
|
perpetuity: false,
|
||||||
{ modifier: 'A', strength: 1 },
|
rings: [{ modifier: '1', strength: '1500' }],
|
||||||
{ modifier: 'B', strength: 2 }
|
awakening: { id: 'character-balanced', level: 1 }
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates the grid character and returns status created' do
|
it 'allows the owner to create a grid character' do
|
||||||
expect do
|
expect do
|
||||||
post '/api/v1/characters', params: valid_params.to_json, headers: headers
|
post '/api/v1/characters', params: valid_params.to_json, headers: headers
|
||||||
end.to change(GridCharacter, :count).by(1)
|
end.to change(GridCharacter, :count).by(1)
|
||||||
expect(response).to have_http_status(:created)
|
expect(response).to have_http_status(:created)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows the owner to update a grid character' do
|
||||||
|
grid_character = create(:grid_character,
|
||||||
|
party: party,
|
||||||
|
character: incoming_character,
|
||||||
|
position: 2,
|
||||||
|
uncap_level: 3,
|
||||||
|
transcendence_step: 0)
|
||||||
|
update_params = {
|
||||||
|
character: {
|
||||||
|
id: grid_character.id,
|
||||||
|
party_id: party.id,
|
||||||
|
character_id: incoming_character.id,
|
||||||
|
position: 2,
|
||||||
|
uncap_level: 4,
|
||||||
|
transcendence_step: 1,
|
||||||
|
rings: [{ modifier: '1', strength: '1500' }, { modifier: '2', strength: '750' }],
|
||||||
|
awakening: { id: 'character-attack', level: 2 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use the resource route for update (as defined by resources :grid_characters)
|
||||||
|
put "/api/v1/grid_characters/#{grid_character.id}", params: update_params.to_json, headers: headers
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
json_response = JSON.parse(response.body)
|
json_response = JSON.parse(response.body)
|
||||||
expect(json_response).to include('position' => 0)
|
expect(json_response['grid_character']).to include('uncap_level' => 4, 'transcendence_step' => 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows the owner to update the uncap level and transcendence step' do
|
||||||
|
grid_character = create(:grid_character,
|
||||||
|
party: party,
|
||||||
|
character: incoming_character,
|
||||||
|
position: 3,
|
||||||
|
uncap_level: 3,
|
||||||
|
transcendence_step: 0)
|
||||||
|
update_uncap_params = {
|
||||||
|
character: {
|
||||||
|
id: grid_character.id,
|
||||||
|
party_id: party.id,
|
||||||
|
character_id: incoming_character.id,
|
||||||
|
uncap_level: 5,
|
||||||
|
transcendence_step: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post '/api/v1/characters/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_character']).to include('uncap_level' => 5, 'transcendence_step' => 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows the owner to resolve conflicts by replacing an existing grid character' do
|
||||||
|
# Create a conflicting grid character (same character_id) at the target position.
|
||||||
|
conflicting_character = create(:grid_character,
|
||||||
|
party: party,
|
||||||
|
character: incoming_character,
|
||||||
|
position: 4,
|
||||||
|
uncap_level: 3)
|
||||||
|
resolve_params = {
|
||||||
|
resolve: {
|
||||||
|
position: 4,
|
||||||
|
incoming: incoming_character.id,
|
||||||
|
conflicting: [conflicting_character.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect do
|
||||||
|
post '/api/v1/characters/resolve', params: resolve_params.to_json, headers: headers
|
||||||
|
end.to change(GridCharacter, :count).by(0) # one record is destroyed and one is created
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['grid_character']).to include('position' => 4)
|
||||||
|
expect { conflicting_character.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows the owner to destroy a grid character' do
|
||||||
|
grid_character = create(:grid_character,
|
||||||
|
party: party,
|
||||||
|
character: incoming_character,
|
||||||
|
position: 5,
|
||||||
|
uncap_level: 3)
|
||||||
|
# Using the custom route for destroy: DELETE '/api/v1/characters'
|
||||||
|
expect do
|
||||||
|
delete '/api/v1/characters', params: { id: grid_character.id }.to_json, headers: headers
|
||||||
|
end.to change(GridCharacter, :count).by(-1)
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when attempting to add a duplicate canonical character (e.g. Rosamia)' do
|
context 'when the party is anonymous (no user)' do
|
||||||
before do
|
let(:anon_party) { create(:party, user: nil, edit_key: 'anonsecret') }
|
||||||
# Create an initial grid character for Rosamia.
|
let(:headers) { { 'Content-Type' => 'application/json', 'X-Edit-Key' => 'anonsecret' } }
|
||||||
GridCharacter.create!(
|
let(:valid_params) do
|
||||||
party_id: party.id,
|
{
|
||||||
character_id: rosamia.id,
|
character: {
|
||||||
|
party_id: anon_party.id,
|
||||||
|
character_id: incoming_character.id,
|
||||||
position: 1,
|
position: 1,
|
||||||
uncap_level: 3,
|
uncap_level: 3,
|
||||||
transcendence_step: 0
|
transcendence_step: 0,
|
||||||
)
|
perpetuity: false,
|
||||||
|
rings: [{ modifier: '1', strength: '1500' }],
|
||||||
|
awakening: { id: 'character-balanced', level: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:duplicate_params) do
|
it 'allows anonymous creation with correct edit_key' do
|
||||||
|
expect do
|
||||||
|
post '/api/v1/characters', params: valid_params.to_json, headers: headers
|
||||||
|
end.to change(GridCharacter, :count).by(1)
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when an incorrect edit_key is provided' do
|
||||||
|
let(:headers) { super().merge('X-Edit-Key' => 'wrong') }
|
||||||
|
|
||||||
|
it 'returns an unauthorized response' do
|
||||||
|
post '/api/v1/characters', params: valid_params.to_json, headers: headers
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/v1/characters (create action) with invalid parameters' do
|
||||||
|
context 'with missing or invalid required fields' do
|
||||||
|
let(:invalid_params) do
|
||||||
{
|
{
|
||||||
character: {
|
character: {
|
||||||
party_id: party.id,
|
party_id: party.id,
|
||||||
character_id: rosamia.id, # same canonical character
|
# Missing character_id
|
||||||
|
position: 1,
|
||||||
|
uncap_level: 2,
|
||||||
|
transcendence_step: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns unprocessable entity status with error messages' do
|
||||||
|
post '/api/v1/characters', 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')
|
||||||
|
# Verify that the error message on uncap_level includes a specific phrase.
|
||||||
|
expect(json_response['errors']['code'].to_s).to eq('no_character_provided')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'PUT /api/v1/grid_characters/:id (update action)' do
|
||||||
|
let!(:grid_character) do
|
||||||
|
create(:grid_character,
|
||||||
|
party: party,
|
||||||
|
character: incoming_character,
|
||||||
position: 2,
|
position: 2,
|
||||||
uncap_level: 3,
|
uncap_level: 3,
|
||||||
transcendence_step: 0
|
transcendence_step: 0)
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'detects the conflict and returns a conflict resolution view without adding a duplicate' do
|
|
||||||
# Here we simulate conflict resolution via the resolve endpoint.
|
|
||||||
expect do
|
|
||||||
post '/api/v1/characters/resolve',
|
|
||||||
params: { resolve: { position: 2, incoming: rosamia.id, conflicting: [GridCharacter.last.id] } }.to_json,
|
|
||||||
headers: headers
|
|
||||||
end.to change(GridCharacter, :count).by(0)
|
|
||||||
expect(response).to have_http_status(:created)
|
|
||||||
json_response = JSON.parse(response.body)
|
|
||||||
expect(json_response).to include('position' => 2)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'PUT /api/v1/characters/:id (update)' do
|
|
||||||
before do
|
|
||||||
@grid_character = GridCharacter.create!(
|
|
||||||
party_id: party.id,
|
|
||||||
character_id: rosamia.id,
|
|
||||||
position: 1,
|
|
||||||
uncap_level: 3,
|
|
||||||
transcendence_step: 0
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with valid parameters' do
|
||||||
let(:update_params) do
|
let(:update_params) do
|
||||||
{
|
{
|
||||||
character: {
|
character: {
|
||||||
id: @grid_character.id,
|
id: grid_character.id,
|
||||||
party_id: party.id,
|
party_id: party.id,
|
||||||
character_id: rosamia.id,
|
character_id: incoming_character.id,
|
||||||
position: 1,
|
position: 2,
|
||||||
uncap_level: 4,
|
uncap_level: 4,
|
||||||
transcendence_step: 0,
|
transcendence_step: 1,
|
||||||
rings: [
|
rings: [{ modifier: '1', strength: '1500' }, { modifier: '2', strength: '750' }],
|
||||||
{ modifier: 'C', strength: 3 }
|
awakening: { id: 'character-balanced', level: 2 }
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates the grid character and returns the updated record' do
|
it 'updates the grid character and returns the updated record' do
|
||||||
put "/api/v1/grid_characters/#{@grid_character.id}", params: update_params.to_json, headers: headers
|
put "/api/v1/grid_characters/#{grid_character.id}", params: update_params.to_json, headers: headers
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
json_response = JSON.parse(response.body)
|
json_response = JSON.parse(response.body)
|
||||||
expect(json_response).to include('uncap_level' => 4)
|
expect(json_response['grid_character']).to include('uncap_level' => 4, 'transcendence_step' => 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST /api/v1/characters/update_uncap (update uncap level)' do
|
context 'with invalid parameters' do
|
||||||
context 'for a character that does NOT allow transcendence (e.g. Rosamia)' do
|
let(:invalid_update_params) do
|
||||||
before do
|
|
||||||
@grid_character = GridCharacter.create!(
|
|
||||||
party_id: party.id,
|
|
||||||
character_id: rosamia.id,
|
|
||||||
position: 2,
|
|
||||||
uncap_level: 2,
|
|
||||||
transcendence_step: 0
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:update_uncap_params) do
|
|
||||||
{
|
{
|
||||||
character: {
|
character: {
|
||||||
id: @grid_character.id,
|
id: grid_character.id,
|
||||||
party_id: party.id,
|
party_id: party.id,
|
||||||
character_id: rosamia.id,
|
character_id: incoming_character.id,
|
||||||
|
position: 2,
|
||||||
|
uncap_level: 'invalid',
|
||||||
|
transcendence_step: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns unprocessable entity status with error details' do
|
||||||
|
put "/api/v1/grid_characters/#{grid_character.id}", params: invalid_update_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')
|
||||||
|
expect(json_response['errors']['uncap_level'].to_s).to include('is not a number')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST /api/v1/characters/update_uncap (update uncap level action)' do
|
||||||
|
let!(:grid_character) do
|
||||||
|
create(:grid_character,
|
||||||
|
party: party,
|
||||||
|
character: incoming_character,
|
||||||
|
position: 3,
|
||||||
uncap_level: 3,
|
uncap_level: 3,
|
||||||
transcendence_step: 0
|
transcendence_step: 0)
|
||||||
}
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'caps the uncap level at 4 for a character with flb true' do
|
|
||||||
post '/api/v1/characters/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_character']).to include('uncap_level' => 3)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'for a character that allows transcendence (e.g. Seofon)' do
|
|
||||||
before do
|
|
||||||
# For Seofon, the "transcendence" behavior is enabled by its ulb flag.
|
|
||||||
@grid_character = GridCharacter.create!(
|
|
||||||
party_id: party.id,
|
|
||||||
character_id: seofon.id,
|
|
||||||
position: 2,
|
|
||||||
uncap_level: 5,
|
|
||||||
transcendence_step: 0
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with valid uncap level parameters' do
|
||||||
let(:update_uncap_params) do
|
let(:update_uncap_params) do
|
||||||
{
|
{
|
||||||
character: {
|
character: {
|
||||||
id: @grid_character.id,
|
id: grid_character.id,
|
||||||
party_id: party.id,
|
party_id: party.id,
|
||||||
character_id: seofon.id,
|
character_id: incoming_character.id,
|
||||||
uncap_level: 5,
|
uncap_level: 5,
|
||||||
transcendence_step: 1
|
transcendence_step: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates the uncap level to 6 when the character supports transcendence via ulb' do
|
it 'updates the uncap level and transcendence step' do
|
||||||
post '/api/v1/characters/update_uncap', params: update_uncap_params.to_json, headers: headers
|
post '/api/v1/characters/update_uncap', params: update_uncap_params.to_json, headers: headers
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
|
|
||||||
json_response = JSON.parse(response.body)
|
json_response = JSON.parse(response.body)
|
||||||
expect(json_response['grid_character']).to include('uncap_level' => 5, 'transcendence_step' => 1)
|
expect(json_response['grid_character']).to include('uncap_level' => 5, 'transcendence_step' => 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'DELETE /api/v1/characters (destroy)' do
|
describe 'POST /api/v1/characters/resolve (conflict resolution action)' do
|
||||||
before do
|
let!(:conflicting_character) do
|
||||||
@grid_character = GridCharacter.create!(
|
create(:grid_character,
|
||||||
party_id: party.id,
|
party: party,
|
||||||
character_id: rosamia.id,
|
character: incoming_character,
|
||||||
position: 4,
|
position: 4,
|
||||||
uncap_level: 3,
|
uncap_level: 3)
|
||||||
transcendence_step: 0
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'destroys the grid character and returns a destroyed view' do
|
let(:resolve_params) do
|
||||||
|
{
|
||||||
|
resolve: {
|
||||||
|
position: 4,
|
||||||
|
incoming: incoming_character.id,
|
||||||
|
conflicting: [conflicting_character.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'resolves conflicts by replacing the existing grid character' do
|
||||||
|
expect(GridCharacter.exists?(conflicting_character.id)).to be true
|
||||||
expect do
|
expect do
|
||||||
delete '/api/v1/characters', params: { id: @grid_character.id }.to_json, headers: headers
|
post '/api/v1/characters/resolve', params: resolve_params.to_json, headers: headers
|
||||||
|
end.to change(GridCharacter, :count).by(0) # one record deleted, one created
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['grid_character']).to include('position' => 4)
|
||||||
|
expect { conflicting_character.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'DELETE /api/v1/characters (destroy action)' do
|
||||||
|
context 'when the party is owned by a logged in user' do
|
||||||
|
let!(:grid_character) do
|
||||||
|
create(:grid_character,
|
||||||
|
party: party,
|
||||||
|
character: incoming_character,
|
||||||
|
position: 6,
|
||||||
|
uncap_level: 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'destroys the grid character and returns a success response' do
|
||||||
|
expect do
|
||||||
|
delete '/api/v1/characters', params: { id: grid_character.id }.to_json, headers: headers
|
||||||
end.to change(GridCharacter, :count).by(-1)
|
end.to change(GridCharacter, :count).by(-1)
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns not found when trying to delete a non-existent grid character' do
|
||||||
|
delete '/api/v1/characters', params: { id: '00000000-0000-0000-0000-000000000000' }.to_json, headers: headers
|
||||||
|
expect(response).to have_http_status(:not_found)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the party is anonymous' do
|
||||||
|
let(:anon_party) { create(:party, user: nil, edit_key: 'anonsecret') }
|
||||||
|
let(:headers) { { 'Content-Type' => 'application/json', 'X-Edit-Key' => 'anonsecret' } }
|
||||||
|
let!(:grid_character) do
|
||||||
|
create(:grid_character,
|
||||||
|
party: anon_party,
|
||||||
|
character: incoming_character,
|
||||||
|
position: 6,
|
||||||
|
uncap_level: 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows anonymous user to destroy the grid character' do
|
||||||
|
expect do
|
||||||
|
delete '/api/v1/characters', params: { id: grid_character.id }.to_json, headers: headers
|
||||||
|
end.to change(GridCharacter, :count).by(-1)
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'prevents deletion when a logged in user attempts to delete an anonymous grid character' do
|
||||||
|
auth_headers = headers.except('X-Edit-Key')
|
||||||
|
expect do
|
||||||
|
delete '/api/v1/characters', params: { id: grid_character.id }.to_json, headers: auth_headers
|
||||||
|
end.not_to change(GridCharacter, :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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue