add artifact specs and factories

This commit is contained in:
Justin Edmund 2025-12-03 12:58:49 -08:00
parent cc7ac1956b
commit 233b3430ef
11 changed files with 1212 additions and 0 deletions

View file

@ -0,0 +1,63 @@
# frozen_string_literal: true
FactoryBot.define do
factory :artifact_skill do
skill_group { :group_i }
# Use high sequence numbers to avoid conflicts with seeded data (modifiers 1-29 used)
sequence(:modifier) { |n| 1000 + n }
sequence(:name_en) { |n| "Test Skill #{n}" }
name_jp { 'テストスキル' }
base_values { [1320, 1440, 1560, 1680, 1800] }
growth { 300.0 }
suffix_en { '' }
suffix_jp { '' }
polarity { :positive }
trait :group_i do
skill_group { :group_i }
end
trait :group_ii do
skill_group { :group_ii }
end
trait :group_iii do
skill_group { :group_iii }
end
trait :atk do
modifier { 1 }
name_en { 'ATK' }
name_jp { '攻撃力' }
base_values { [1320, 1440, 1560, 1680, 1800] }
growth { 300.0 }
end
trait :hp do
modifier { 2 }
name_en { 'HP' }
name_jp { 'HP' }
base_values { [660, 720, 780, 840, 900] }
growth { 150.0 }
end
trait :ca_dmg do
modifier { 3 }
name_en { 'C.A. DMG' }
name_jp { '奥義ダメ' }
base_values { [13.2, 14.4, 15.6, 16.8, 18.0] }
growth { 3.0 }
suffix_en { '%' }
suffix_jp { '%' }
end
trait :negative do
polarity { :negative }
growth { -6.0 }
end
trait :no_growth do
growth { nil }
end
end
end

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
FactoryBot.define do
factory :artifact do
# Use high sequence numbers to avoid conflicts with seeded data
sequence(:granblue_id) { |n| "39999#{n.to_s.rjust(4, '0')}" }
sequence(:name_en) { |n| "Test Artifact #{n}" }
name_jp { 'テストアーティファクト' }
proficiency { :sabre }
rarity { :standard }
release_date { Date.new(2025, 3, 10) }
trait :quirk do
rarity { :quirk }
proficiency { nil }
sequence(:granblue_id) { |n| "38888#{n.to_s.rjust(4, '0')}" }
sequence(:name_en) { |n| "Quirk Artifact #{n}" }
name_jp { 'クィルクアーティファクト' }
end
trait :dagger do
proficiency { :dagger }
end
trait :spear do
proficiency { :spear }
end
trait :staff do
proficiency { :staff }
end
end
end

View file

@ -0,0 +1,58 @@
# frozen_string_literal: true
FactoryBot.define do
factory :collection_artifact do
association :user
association :artifact
element { 'fire' }
level { 1 }
proficiency { nil }
nickname { nil }
skill1 { { 'modifier' => 1, 'strength' => 1800, 'level' => 1 } }
skill2 { { 'modifier' => 2, 'strength' => 900, 'level' => 1 } }
skill3 { { 'modifier' => 1, 'strength' => 18.0, 'level' => 1 } }
skill4 { { 'modifier' => 1, 'strength' => 10, 'level' => 1 } }
trait :max_level do
level { 5 }
skill1 { { 'modifier' => 1, 'strength' => 1800, 'level' => 2 } }
skill2 { { 'modifier' => 2, 'strength' => 900, 'level' => 2 } }
skill3 { { 'modifier' => 1, 'strength' => 18.0, 'level' => 2 } }
skill4 { { 'modifier' => 1, 'strength' => 10, 'level' => 2 } }
end
trait :quirk do
association :artifact, factory: [:artifact, :quirk]
proficiency { :sabre }
level { 1 }
skill1 { {} }
skill2 { {} }
skill3 { {} }
skill4 { {} }
end
trait :with_nickname do
nickname { 'My Favorite Artifact' }
end
trait :water do
element { 'water' }
end
trait :earth do
element { 'earth' }
end
trait :wind do
element { 'wind' }
end
trait :light do
element { 'light' }
end
trait :dark do
element { 'dark' }
end
end
end

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
FactoryBot.define do
factory :grid_artifact do
association :grid_character
association :artifact
element { 'fire' }
level { 1 }
proficiency { nil }
skill1 { { 'modifier' => 1, 'strength' => 1800, 'level' => 1 } }
skill2 { { 'modifier' => 2, 'strength' => 900, 'level' => 1 } }
skill3 { { 'modifier' => 1, 'strength' => 18.0, 'level' => 1 } }
skill4 { { 'modifier' => 1, 'strength' => 10, 'level' => 1 } }
trait :max_level do
level { 5 }
skill1 { { 'modifier' => 1, 'strength' => 1800, 'level' => 2 } }
skill2 { { 'modifier' => 2, 'strength' => 900, 'level' => 2 } }
skill3 { { 'modifier' => 1, 'strength' => 18.0, 'level' => 2 } }
skill4 { { 'modifier' => 1, 'strength' => 10, 'level' => 2 } }
end
trait :quirk do
association :artifact, factory: [:artifact, :quirk]
proficiency { :sabre }
level { 1 }
skill1 { {} }
skill2 { {} }
skill3 { {} }
skill4 { {} }
end
end
end

View file

@ -0,0 +1,181 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ArtifactSkill, type: :model do
describe 'validations' do
subject { build(:artifact_skill) }
it { is_expected.to validate_presence_of(:skill_group) }
it { is_expected.to validate_presence_of(:modifier) }
it { is_expected.to validate_presence_of(:name_en) }
it { is_expected.to validate_presence_of(:name_jp) }
it { is_expected.to validate_presence_of(:base_values) }
it { is_expected.to validate_presence_of(:polarity) }
it 'validates uniqueness of modifier within skill_group' do
# Create with unique modifier, then try to create duplicate
existing = create(:artifact_skill, skill_group: :group_i, modifier: 5000)
duplicate = build(:artifact_skill, skill_group: :group_i, modifier: 5000)
expect(duplicate).not_to be_valid
expect(duplicate.errors[:modifier]).to include('has already been taken')
end
it 'allows same modifier in different skill groups' do
create(:artifact_skill, skill_group: :group_i, modifier: 5001)
different_group = build(:artifact_skill, skill_group: :group_ii, modifier: 5001)
expect(different_group).to be_valid
end
end
describe 'enums' do
it 'defines skill_group enum' do
expect(ArtifactSkill.skill_groups).to eq(
'group_i' => 1,
'group_ii' => 2,
'group_iii' => 3
)
end
it 'defines polarity enum' do
expect(ArtifactSkill.polarities).to eq(
'positive' => 'positive',
'negative' => 'negative'
)
end
end
describe '.for_slot' do
before do
# Use unique modifiers that won't conflict with seeded data
@group_i_skill = create(:artifact_skill, :group_i, modifier: 6000)
@group_ii_skill = create(:artifact_skill, :group_ii, modifier: 6001)
@group_iii_skill = create(:artifact_skill, :group_iii, modifier: 6002)
end
it 'returns Group I skills for slot 1' do
expect(ArtifactSkill.for_slot(1)).to include(@group_i_skill)
expect(ArtifactSkill.for_slot(1)).not_to include(@group_ii_skill)
end
it 'returns Group I skills for slot 2' do
expect(ArtifactSkill.for_slot(2)).to include(@group_i_skill)
expect(ArtifactSkill.for_slot(2)).not_to include(@group_ii_skill)
end
it 'returns Group II skills for slot 3' do
expect(ArtifactSkill.for_slot(3)).to include(@group_ii_skill)
expect(ArtifactSkill.for_slot(3)).not_to include(@group_i_skill)
end
it 'returns Group III skills for slot 4' do
expect(ArtifactSkill.for_slot(4)).to include(@group_iii_skill)
expect(ArtifactSkill.for_slot(4)).not_to include(@group_i_skill)
end
end
describe '.find_skill' do
before do
ArtifactSkill.clear_cache!
@test_skill = create(:artifact_skill, skill_group: :group_i, modifier: 7000)
end
after do
ArtifactSkill.clear_cache!
end
it 'finds skill by group number and modifier' do
ArtifactSkill.clear_cache!
found = ArtifactSkill.find_skill(1, 7000)
expect(found).to eq(@test_skill)
end
it 'returns nil for non-existent skill' do
ArtifactSkill.clear_cache!
expect(ArtifactSkill.find_skill(1, 99999)).to be_nil
end
it 'caches skills for performance' do
ArtifactSkill.clear_cache!
ArtifactSkill.find_skill(1, 7000)
expect(ArtifactSkill.instance_variable_get(:@cached_skills)).not_to be_nil
end
end
describe '#calculate_value' do
let(:skill) { build(:artifact_skill, growth: 300.0) }
it 'returns base strength at level 1' do
expect(skill.calculate_value(1800, 1)).to eq(1800)
end
it 'adds growth for each level above 1' do
expect(skill.calculate_value(1800, 3)).to eq(2400) # 1800 + (300 * 2)
end
it 'handles level 5' do
expect(skill.calculate_value(1800, 5)).to eq(3000) # 1800 + (300 * 4)
end
context 'with nil growth' do
let(:skill) { build(:artifact_skill, :no_growth) }
it 'returns base strength regardless of level' do
expect(skill.calculate_value(10, 1)).to eq(10)
expect(skill.calculate_value(10, 5)).to eq(10)
end
end
context 'with negative growth' do
let(:skill) { build(:artifact_skill, :negative, growth: -6.0) }
it 'subtracts growth for each level' do
expect(skill.calculate_value(30, 3)).to eq(18) # 30 + (-6 * 2)
end
end
end
describe '#format_value' do
context 'with percentage suffix' do
let(:skill) { build(:artifact_skill, suffix_en: '%', suffix_jp: '%') }
it 'formats with English suffix' do
expect(skill.format_value(18.0, :en)).to eq('18.0%')
end
it 'formats with Japanese suffix' do
expect(skill.format_value(18.0, :jp)).to eq('18.0%')
end
end
context 'with no suffix' do
let(:skill) { build(:artifact_skill, suffix_en: '', suffix_jp: '') }
it 'returns value without suffix' do
expect(skill.format_value(1800, :en)).to eq('1800')
end
end
end
describe '#valid_strength?' do
let(:skill) { build(:artifact_skill, base_values: [1320, 1440, 1560, 1680, 1800]) }
it 'returns true for valid base values' do
expect(skill.valid_strength?(1800)).to be true
expect(skill.valid_strength?(1320)).to be true
end
it 'returns false for invalid values' do
expect(skill.valid_strength?(1500)).to be false
expect(skill.valid_strength?(9999)).to be false
end
context 'with nil in base_values (unknown values)' do
let(:skill) { build(:artifact_skill, base_values: [nil]) }
it 'returns true for any value' do
expect(skill.valid_strength?(9999)).to be true
end
end
end
end

View file

@ -0,0 +1,82 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Artifact, type: :model do
describe 'validations' do
subject { build(:artifact) }
it { is_expected.to validate_presence_of(:granblue_id) }
it 'validates uniqueness of granblue_id' do
create(:artifact)
duplicate = build(:artifact, granblue_id: Artifact.first.granblue_id)
expect(duplicate).not_to be_valid
expect(duplicate.errors[:granblue_id]).to include('has already been taken')
end
it { is_expected.to validate_presence_of(:name_en) }
it { is_expected.to validate_presence_of(:rarity) }
context 'when standard artifact' do
subject { build(:artifact, rarity: :standard) }
it { is_expected.to validate_presence_of(:proficiency) }
end
context 'when quirk artifact' do
subject { build(:artifact, :quirk) }
it 'requires proficiency to be nil' do
subject.proficiency = :sabre
expect(subject).not_to be_valid
expect(subject.errors[:proficiency]).to include('must be blank')
end
it 'is valid without proficiency' do
expect(subject).to be_valid
end
end
end
describe 'associations' do
it { is_expected.to have_many(:collection_artifacts).dependent(:restrict_with_error) }
it { is_expected.to have_many(:grid_artifacts).dependent(:restrict_with_error) }
end
describe 'enums' do
it 'defines proficiency enum' do
expect(Artifact.proficiencies).to include(
'sabre' => 1,
'dagger' => 2,
'spear' => 4
)
end
it 'defines rarity enum' do
expect(Artifact.rarities).to eq('standard' => 0, 'quirk' => 1)
end
end
describe 'scopes' do
let!(:standard_artifact) { create(:artifact, rarity: :standard) }
let!(:quirk_artifact) { create(:artifact, :quirk) }
it 'filters by rarity' do
expect(Artifact.standard).to include(standard_artifact)
expect(Artifact.standard).not_to include(quirk_artifact)
expect(Artifact.quirk).to include(quirk_artifact)
expect(Artifact.quirk).not_to include(standard_artifact)
end
end
describe '#quirk?' do
it 'returns true for quirk artifacts' do
artifact = build(:artifact, :quirk)
expect(artifact.quirk?).to be true
end
it 'returns false for standard artifacts' do
artifact = build(:artifact, rarity: :standard)
expect(artifact.quirk?).to be false
end
end
end

View file

@ -0,0 +1,202 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe CollectionArtifact, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:artifact) }
end
describe 'validations' do
subject { build(:collection_artifact) }
it { is_expected.to validate_presence_of(:element) }
it 'validates presence of level' do
subject.level = nil
expect(subject).not_to be_valid
end
it 'validates level is between 1 and 5' do
subject.level = 0
expect(subject).not_to be_valid
subject.level = 6
expect(subject).not_to be_valid
subject.level = 3
expect(subject).to be_valid
end
end
describe 'enums' do
it 'defines element enum' do
expect(CollectionArtifact.elements).to include(
'wind' => 1,
'fire' => 2,
'water' => 3,
'earth' => 4,
'dark' => 5,
'light' => 6
)
end
it 'defines proficiency enum' do
expect(CollectionArtifact.proficiencies).to include(
'sabre' => 1,
'dagger' => 2
)
end
end
describe '#effective_proficiency' do
context 'for standard artifact' do
let(:artifact) { create(:artifact, proficiency: :dagger) }
it 'returns proficiency from base artifact' do
collection_artifact = build(:collection_artifact,
artifact: artifact,
proficiency: nil,
skill1: {}, skill2: {}, skill3: {}, skill4: {}
)
expect(collection_artifact.effective_proficiency).to eq('dagger')
end
end
context 'for quirk artifact' do
let(:artifact) { create(:artifact, :quirk) }
it 'returns proficiency from instance' do
collection_artifact = build(:collection_artifact,
artifact: artifact,
proficiency: :staff,
level: 1,
skill1: {}, skill2: {}, skill3: {}, skill4: {}
)
expect(collection_artifact.effective_proficiency).to eq('staff')
end
end
end
describe 'skill validations' do
let(:artifact) { create(:artifact) }
before do
# Seed the required artifact skills for validation
ArtifactSkill.find_or_create_by!(skill_group: :group_i, modifier: 1) do |s|
s.name_en = 'ATK'
s.name_jp = '攻撃力'
s.base_values = [1320, 1440, 1560, 1680, 1800]
s.growth = 300.0
s.polarity = :positive
end
ArtifactSkill.find_or_create_by!(skill_group: :group_i, modifier: 2) do |s|
s.name_en = 'HP'
s.name_jp = 'HP'
s.base_values = [660, 720, 780, 840, 900]
s.growth = 150.0
s.polarity = :positive
end
ArtifactSkill.find_or_create_by!(skill_group: :group_ii, modifier: 1) do |s|
s.name_en = 'C.A. DMG'
s.name_jp = '奥義ダメ'
s.base_values = [13.2, 14.4, 15.6, 16.8, 18.0]
s.growth = 3.0
s.polarity = :positive
end
ArtifactSkill.find_or_create_by!(skill_group: :group_iii, modifier: 1) do |s|
s.name_en = 'Chain Burst DMG'
s.name_jp = 'チェインダメ'
s.base_values = [6, 7, 8, 9, 10]
s.growth = 2.5
s.polarity = :positive
end
ArtifactSkill.clear_cache!
end
it 'is valid with correct skills' do
collection_artifact = build(:collection_artifact,
artifact: artifact,
level: 1,
skill1: { 'modifier' => 1, 'strength' => 1800, 'level' => 1 },
skill2: { 'modifier' => 2, 'strength' => 900, 'level' => 1 },
skill3: { 'modifier' => 1, 'strength' => 18.0, 'level' => 1 },
skill4: { 'modifier' => 1, 'strength' => 10, 'level' => 1 }
)
expect(collection_artifact).to be_valid
end
it 'is invalid when skill1 and skill2 have the same modifier' do
collection_artifact = build(:collection_artifact,
artifact: artifact,
level: 1,
skill1: { 'modifier' => 1, 'strength' => 1800, 'level' => 1 },
skill2: { 'modifier' => 1, 'strength' => 1800, 'level' => 1 }, # Same modifier
skill3: { 'modifier' => 1, 'strength' => 18.0, 'level' => 1 },
skill4: { 'modifier' => 1, 'strength' => 10, 'level' => 1 }
)
expect(collection_artifact).not_to be_valid
expect(collection_artifact.errors[:base]).to include('Skill 1 and Skill 2 cannot have the same modifier')
end
it 'validates skill levels sum correctly' do
# At level 1, skill levels must sum to 4 (1 + 3)
collection_artifact = build(:collection_artifact,
artifact: artifact,
level: 1,
skill1: { 'modifier' => 1, 'strength' => 1800, 'level' => 2 },
skill2: { 'modifier' => 2, 'strength' => 900, 'level' => 2 },
skill3: { 'modifier' => 1, 'strength' => 18.0, 'level' => 2 },
skill4: { 'modifier' => 1, 'strength' => 10, 'level' => 2 }
)
expect(collection_artifact).not_to be_valid
expect(collection_artifact.errors[:base].first).to include('Skill levels must sum to')
end
end
describe 'quirk artifact constraints' do
let(:quirk_artifact) { create(:artifact, :quirk) }
it 'requires level 1 for quirk artifacts' do
collection_artifact = build(:collection_artifact,
artifact: quirk_artifact,
proficiency: :sabre,
level: 3,
skill1: {},
skill2: {},
skill3: {},
skill4: {}
)
expect(collection_artifact).not_to be_valid
expect(collection_artifact.errors[:level]).to include('must be 1 for quirk artifacts')
end
it 'requires empty skills for quirk artifacts' do
collection_artifact = build(:collection_artifact,
artifact: quirk_artifact,
proficiency: :sabre,
level: 1,
skill1: { 'modifier' => 1, 'strength' => 1800, 'level' => 1 },
skill2: {},
skill3: {},
skill4: {}
)
expect(collection_artifact).not_to be_valid
expect(collection_artifact.errors[:skill1]).to include('must be empty for quirk artifacts')
end
it 'is valid with empty skills and level 1' do
collection_artifact = build(:collection_artifact,
artifact: quirk_artifact,
proficiency: :sabre,
level: 1,
skill1: {},
skill2: {},
skill3: {},
skill4: {}
)
expect(collection_artifact).to be_valid
end
end
end

View file

@ -0,0 +1,96 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe GridArtifact, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:grid_character) }
it { is_expected.to belong_to(:artifact) }
end
describe 'validations' do
subject { build(:grid_artifact) }
it { is_expected.to validate_presence_of(:element) }
it 'validates presence of level' do
subject.level = nil
expect(subject).not_to be_valid
end
it 'validates level is between 1 and 5' do
subject.level = 0
expect(subject).not_to be_valid
subject.level = 6
expect(subject).not_to be_valid
subject.level = 3
expect(subject).to be_valid
end
end
describe 'enums' do
it 'defines element enum' do
expect(GridArtifact.elements).to include(
'wind' => 1,
'fire' => 2,
'water' => 3,
'earth' => 4
)
end
end
describe '#effective_proficiency' do
context 'for standard artifact' do
let(:artifact) { create(:artifact, proficiency: :spear) }
it 'returns proficiency from base artifact' do
grid_artifact = build(:grid_artifact,
artifact: artifact,
proficiency: nil,
skill1: {}, skill2: {}, skill3: {}, skill4: {}
)
expect(grid_artifact.effective_proficiency).to eq('spear')
end
end
context 'for quirk artifact' do
let(:artifact) { create(:artifact, :quirk) }
it 'returns proficiency from instance' do
grid_artifact = build(:grid_artifact,
artifact: artifact,
proficiency: :melee,
level: 1,
skill1: {}, skill2: {}, skill3: {}, skill4: {}
)
expect(grid_artifact.effective_proficiency).to eq('melee')
end
end
end
describe 'unique constraint on grid_character' do
it 'does not allow duplicate grid_artifacts for same grid_character' do
grid_character = create(:grid_character)
create(:grid_artifact,
grid_character: grid_character,
skill1: {}, skill2: {}, skill3: {}, skill4: {}
)
duplicate = build(:grid_artifact,
grid_character: grid_character,
skill1: {}, skill2: {}, skill3: {}, skill4: {}
)
expect(duplicate).not_to be_valid
end
end
describe 'amoeba duplication' do
let(:grid_character) { create(:grid_character) }
let(:grid_artifact) { create(:grid_artifact, grid_character: grid_character) }
it 'can be duplicated via amoeba' do
expect(GridArtifact).to respond_to(:amoeba_block)
end
end
end

View file

@ -0,0 +1,90 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Artifact Skills API', type: :request do
before do
# Create test skills in different groups
create(:artifact_skill, :group_i, :atk, modifier: 1)
create(:artifact_skill, :group_i, :hp, modifier: 2)
create(:artifact_skill, :group_ii, modifier: 1, name_en: 'C.A. DMG', name_jp: '奥義ダメ')
create(:artifact_skill, :group_iii, modifier: 1, name_en: 'Chain Burst DMG', name_jp: 'チェインダメ')
end
describe 'GET /api/v1/artifact_skills' do
it 'returns all artifact skills' do
get '/api/v1/artifact_skills'
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['artifact_skills'].length).to eq(4)
end
it 'filters by skill group' do
get '/api/v1/artifact_skills', params: { group: 'group_i' }
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['artifact_skills'].length).to eq(2)
expect(json['artifact_skills'].all? { |s| s['skill_group'] == 'group_i' }).to be true
end
it 'filters by polarity' do
create(:artifact_skill, :group_i, :negative, modifier: 99)
get '/api/v1/artifact_skills', params: { polarity: 'negative' }
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['artifact_skills'].all? { |s| s['polarity'] == 'negative' }).to be true
end
end
describe 'GET /api/v1/artifact_skills/for_slot/:slot' do
it 'returns Group I skills for slot 1' do
get '/api/v1/artifact_skills/for_slot/1'
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['artifact_skills'].all? { |s| s['skill_group'] == 'group_i' }).to be true
end
it 'returns Group I skills for slot 2' do
get '/api/v1/artifact_skills/for_slot/2'
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['artifact_skills'].all? { |s| s['skill_group'] == 'group_i' }).to be true
end
it 'returns Group II skills for slot 3' do
get '/api/v1/artifact_skills/for_slot/3'
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['artifact_skills'].all? { |s| s['skill_group'] == 'group_ii' }).to be true
end
it 'returns Group III skills for slot 4' do
get '/api/v1/artifact_skills/for_slot/4'
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['artifact_skills'].all? { |s| s['skill_group'] == 'group_iii' }).to be true
end
it 'returns error for invalid slot' do
get '/api/v1/artifact_skills/for_slot/5'
expect(response).to have_http_status(:unprocessable_entity)
json = JSON.parse(response.body)
expect(json['error']).to include('Slot must be between 1 and 4')
end
it 'returns error for slot 0' do
get '/api/v1/artifact_skills/for_slot/0'
expect(response).to have_http_status(:unprocessable_entity)
end
end
end

View file

@ -0,0 +1,56 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Artifacts API', type: :request do
describe 'GET /api/v1/artifacts' do
let!(:standard_artifact) { create(:artifact, proficiency: :sabre) }
let!(:quirk_artifact) { create(:artifact, :quirk) }
it 'returns all artifacts' do
get '/api/v1/artifacts'
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['artifacts'].length).to eq(2)
end
it 'filters by rarity' do
get '/api/v1/artifacts', params: { rarity: 'standard' }
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['artifacts'].length).to eq(1)
expect(json['artifacts'].first['rarity']).to eq('standard')
end
it 'filters by proficiency' do
create(:artifact, proficiency: :dagger)
get '/api/v1/artifacts', params: { proficiency: 'sabre' }
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['artifacts'].all? { |a| a['proficiency'] == 'sabre' }).to be true
end
end
describe 'GET /api/v1/artifacts/:id' do
let!(:artifact) { create(:artifact) }
it 'returns the artifact' do
get "/api/v1/artifacts/#{artifact.id}"
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['id']).to eq(artifact.id)
expect(json['name']['en']).to eq(artifact.name_en)
end
it 'returns not found for non-existent artifact' do
get "/api/v1/artifacts/#{SecureRandom.uuid}"
expect(response).to have_http_status(:not_found)
end
end
end

View file

@ -0,0 +1,318 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Collection Artifacts 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(:headers) do
{ 'Authorization' => "Bearer #{access_token.token}", 'Content-Type' => 'application/json' }
end
let(:artifact) { create(:artifact) }
let(:quirk_artifact) { create(:artifact, :quirk) }
before do
# Seed required artifact skills for validation
ArtifactSkill.find_or_create_by!(skill_group: :group_i, modifier: 1) do |s|
s.name_en = 'ATK'
s.name_jp = '攻撃力'
s.base_values = [1320, 1440, 1560, 1680, 1800]
s.growth = 300.0
s.polarity = :positive
end
ArtifactSkill.find_or_create_by!(skill_group: :group_i, modifier: 2) do |s|
s.name_en = 'HP'
s.name_jp = 'HP'
s.base_values = [660, 720, 780, 840, 900]
s.growth = 150.0
s.polarity = :positive
end
ArtifactSkill.find_or_create_by!(skill_group: :group_ii, modifier: 1) do |s|
s.name_en = 'C.A. DMG'
s.name_jp = '奥義ダメ'
s.base_values = [13.2, 14.4, 15.6, 16.8, 18.0]
s.growth = 3.0
s.polarity = :positive
end
ArtifactSkill.find_or_create_by!(skill_group: :group_iii, modifier: 1) do |s|
s.name_en = 'Chain Burst DMG'
s.name_jp = 'チェインダメ'
s.base_values = [6, 7, 8, 9, 10]
s.growth = 2.5
s.polarity = :positive
end
ArtifactSkill.clear_cache!
end
describe 'GET /api/v1/users/:user_id/collection/artifacts' do
let!(:collection_artifact1) { create(:collection_artifact, user: user, artifact: artifact) }
let!(:collection_artifact2) { create(:collection_artifact, user: user, element: :water) }
let!(:other_user_artifact) { create(:collection_artifact, user: other_user) }
it "returns the user's collection artifacts" do
get "/api/v1/users/#{user.id}/collection/artifacts", headers: headers
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['artifacts'].length).to eq(2)
end
it 'supports pagination' do
get "/api/v1/users/#{user.id}/collection/artifacts", params: { page: 1, limit: 1 }, headers: headers
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['artifacts'].length).to eq(1)
expect(json['meta']['total_pages']).to be >= 2
end
it 'filters by artifact_id' do
get "/api/v1/users/#{user.id}/collection/artifacts", params: { artifact_id: artifact.id }, headers: headers
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['artifacts'].length).to eq(1)
end
it 'filters by element' do
get "/api/v1/users/#{user.id}/collection/artifacts", params: { element: 'water' }, headers: headers
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['artifacts'].all? { |a| a['element'] == 'water' }).to be true
end
it 'returns unauthorized without authentication' do
other_user.update!(collection_visibility: 'private')
get "/api/v1/users/#{other_user.id}/collection/artifacts"
expect(response).to have_http_status(:unauthorized)
end
end
describe 'GET /api/v1/users/:user_id/collection/artifacts/:id' do
let!(:collection_artifact) { create(:collection_artifact, user: user, artifact: artifact) }
it 'returns the collection artifact' do
get "/api/v1/users/#{user.id}/collection/artifacts/#{collection_artifact.id}", headers: headers
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['id']).to eq(collection_artifact.id)
expect(json['artifact']['id']).to eq(artifact.id)
end
end
describe 'POST /api/v1/collection/artifacts' do
let(:valid_attributes) do
{
collection_artifact: {
artifact_id: artifact.id,
element: 'fire',
level: 1,
skill1: { modifier: 1, strength: 1800, level: 1 },
skill2: { modifier: 2, strength: 900, level: 1 },
skill3: { modifier: 1, strength: 18.0, level: 1 },
skill4: { modifier: 1, strength: 10, level: 1 }
}
}
end
it 'creates a new collection artifact' do
expect do
post '/api/v1/collection/artifacts', params: valid_attributes.to_json, headers: headers
end.to change(CollectionArtifact, :count).by(1)
expect(response).to have_http_status(:created)
json = JSON.parse(response.body)
expect(json['artifact']['id']).to eq(artifact.id)
expect(json['element']).to eq('fire')
end
it 'allows multiple copies of the same artifact' do
create(:collection_artifact, user: user, artifact: artifact)
expect do
post '/api/v1/collection/artifacts', params: valid_attributes.to_json, headers: headers
end.to change(CollectionArtifact, :count).by(1)
expect(response).to have_http_status(:created)
end
it 'creates artifact with nickname' do
attributes_with_nickname = valid_attributes.deep_merge(
collection_artifact: { nickname: 'My Best Artifact' }
)
post '/api/v1/collection/artifacts', params: attributes_with_nickname.to_json, headers: headers
expect(response).to have_http_status(:created)
json = JSON.parse(response.body)
expect(json['nickname']).to eq('My Best Artifact')
end
it 'creates quirk artifact with proficiency' do
quirk_attributes = {
collection_artifact: {
artifact_id: quirk_artifact.id,
element: 'dark',
proficiency: 'staff',
level: 1,
skill1: {},
skill2: {},
skill3: {},
skill4: {}
}
}
post '/api/v1/collection/artifacts', params: quirk_attributes.to_json, headers: headers
expect(response).to have_http_status(:created)
json = JSON.parse(response.body)
expect(json['proficiency']).to eq('staff')
end
it 'returns error when skill1 and skill2 have same modifier' do
invalid_attributes = valid_attributes.deep_merge(
collection_artifact: {
skill1: { modifier: 1, strength: 1800, level: 1 },
skill2: { modifier: 1, strength: 1800, level: 1 }
}
)
post '/api/v1/collection/artifacts', params: invalid_attributes.to_json, headers: headers
expect(response).to have_http_status(:unprocessable_entity)
json = JSON.parse(response.body)
expect(json['errors'].to_s).to include('cannot have the same modifier')
end
it 'returns unauthorized without authentication' do
post '/api/v1/collection/artifacts', params: valid_attributes.to_json
expect(response).to have_http_status(:unauthorized)
end
end
describe 'PUT /api/v1/collection/artifacts/:id' do
let!(:collection_artifact) { create(:collection_artifact, user: user, artifact: artifact, level: 1) }
it 'updates the collection artifact' do
update_attributes = {
collection_artifact: {
nickname: 'Updated Name',
element: 'water'
}
}
put "/api/v1/collection/artifacts/#{collection_artifact.id}",
params: update_attributes.to_json, headers: headers
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['nickname']).to eq('Updated Name')
expect(json['element']).to eq('water')
end
it 'returns not found for other user\'s artifact' do
other_collection = create(:collection_artifact, user: other_user)
put "/api/v1/collection/artifacts/#{other_collection.id}",
params: { collection_artifact: { nickname: 'Hack' } }.to_json, headers: headers
expect(response).to have_http_status(:not_found)
end
end
describe 'DELETE /api/v1/collection/artifacts/:id' do
let!(:collection_artifact) { create(:collection_artifact, user: user, artifact: artifact) }
it 'deletes the collection artifact' do
expect do
delete "/api/v1/collection/artifacts/#{collection_artifact.id}", headers: headers
end.to change(CollectionArtifact, :count).by(-1)
expect(response).to have_http_status(:no_content)
end
it 'returns not found for other user\'s artifact' do
other_collection = create(:collection_artifact, user: other_user)
expect do
delete "/api/v1/collection/artifacts/#{other_collection.id}", headers: headers
end.not_to change(CollectionArtifact, :count)
expect(response).to have_http_status(:not_found)
end
end
describe 'POST /api/v1/collection/artifacts/batch' do
let(:artifact2) { create(:artifact, :dagger) }
it 'creates multiple collection artifacts' do
batch_attributes = {
collection_artifacts: [
{
artifact_id: artifact.id,
element: 'fire',
level: 1,
skill1: { modifier: 1, strength: 1800, level: 1 },
skill2: { modifier: 2, strength: 900, level: 1 },
skill3: { modifier: 1, strength: 18.0, level: 1 },
skill4: { modifier: 1, strength: 10, level: 1 }
},
{
artifact_id: artifact2.id,
element: 'water',
level: 1,
skill1: { modifier: 1, strength: 1800, level: 1 },
skill2: { modifier: 2, strength: 900, level: 1 },
skill3: { modifier: 1, strength: 18.0, level: 1 },
skill4: { modifier: 1, strength: 10, level: 1 }
}
]
}
expect do
post '/api/v1/collection/artifacts/batch', params: batch_attributes.to_json, headers: headers
end.to change(CollectionArtifact, :count).by(2)
expect(response).to have_http_status(:created)
json = JSON.parse(response.body)
expect(json['meta']['created']).to eq(2)
expect(json['meta']['errors']).to be_empty
end
it 'returns multi_status when some items fail' do
batch_attributes = {
collection_artifacts: [
{
artifact_id: artifact.id,
element: 'fire',
level: 1,
skill1: { modifier: 1, strength: 1800, level: 1 },
skill2: { modifier: 2, strength: 900, level: 1 },
skill3: { modifier: 1, strength: 18.0, level: 1 },
skill4: { modifier: 1, strength: 10, level: 1 }
},
{
artifact_id: artifact.id,
element: 'invalid_element', # Invalid
level: 1
}
]
}
post '/api/v1/collection/artifacts/batch', params: batch_attributes.to_json, headers: headers
expect(response).to have_http_status(:multi_status)
json = JSON.parse(response.body)
expect(json['meta']['created']).to eq(1)
expect(json['meta']['errors'].length).to eq(1)
end
end
end