diff --git a/spec/factories/artifact_skills.rb b/spec/factories/artifact_skills.rb new file mode 100644 index 0000000..a2f7137 --- /dev/null +++ b/spec/factories/artifact_skills.rb @@ -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 diff --git a/spec/factories/artifacts.rb b/spec/factories/artifacts.rb new file mode 100644 index 0000000..8153c78 --- /dev/null +++ b/spec/factories/artifacts.rb @@ -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 diff --git a/spec/factories/collection_artifacts.rb b/spec/factories/collection_artifacts.rb new file mode 100644 index 0000000..0da50b4 --- /dev/null +++ b/spec/factories/collection_artifacts.rb @@ -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 diff --git a/spec/factories/grid_artifacts.rb b/spec/factories/grid_artifacts.rb new file mode 100644 index 0000000..4ec23d5 --- /dev/null +++ b/spec/factories/grid_artifacts.rb @@ -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 diff --git a/spec/models/artifact_skill_spec.rb b/spec/models/artifact_skill_spec.rb new file mode 100644 index 0000000..757c31f --- /dev/null +++ b/spec/models/artifact_skill_spec.rb @@ -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 diff --git a/spec/models/artifact_spec.rb b/spec/models/artifact_spec.rb new file mode 100644 index 0000000..c98efbf --- /dev/null +++ b/spec/models/artifact_spec.rb @@ -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 diff --git a/spec/models/collection_artifact_spec.rb b/spec/models/collection_artifact_spec.rb new file mode 100644 index 0000000..8645852 --- /dev/null +++ b/spec/models/collection_artifact_spec.rb @@ -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 diff --git a/spec/models/grid_artifact_spec.rb b/spec/models/grid_artifact_spec.rb new file mode 100644 index 0000000..40fc5f8 --- /dev/null +++ b/spec/models/grid_artifact_spec.rb @@ -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 diff --git a/spec/requests/artifact_skills_controller_spec.rb b/spec/requests/artifact_skills_controller_spec.rb new file mode 100644 index 0000000..4878943 --- /dev/null +++ b/spec/requests/artifact_skills_controller_spec.rb @@ -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 diff --git a/spec/requests/artifacts_controller_spec.rb b/spec/requests/artifacts_controller_spec.rb new file mode 100644 index 0000000..7d5f442 --- /dev/null +++ b/spec/requests/artifacts_controller_spec.rb @@ -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 diff --git a/spec/requests/collection_artifacts_controller_spec.rb b/spec/requests/collection_artifacts_controller_spec.rb new file mode 100644 index 0000000..a2e89b2 --- /dev/null +++ b/spec/requests/collection_artifacts_controller_spec.rb @@ -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