From b8947dbaf3aaca745872183e9c1cfdb2ce52e433 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 13 Dec 2025 20:54:42 -0800 Subject: [PATCH] add artifact import service spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tests parsing game artifact data with skill decoding 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- spec/services/artifact_import_service_spec.rb | 441 ++++++++++++++++++ 1 file changed, 441 insertions(+) create mode 100644 spec/services/artifact_import_service_spec.rb diff --git a/spec/services/artifact_import_service_spec.rb b/spec/services/artifact_import_service_spec.rb new file mode 100644 index 0000000..7596a7c --- /dev/null +++ b/spec/services/artifact_import_service_spec.rb @@ -0,0 +1,441 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ArtifactImportService, type: :service do + let(:user) { create(:user) } + + # Create artifacts with specific granblue_ids matching the game data + let(:standard_artifact) do + Artifact.find_by(granblue_id: '301070101') || + create(:artifact, granblue_id: '301070101', name_en: 'Ominous Bangle', proficiency: :melee) + end + let(:standard_artifact_2) do + Artifact.find_by(granblue_id: '301090101') || + create(:artifact, granblue_id: '301090101', name_en: 'Ominous Whistle', proficiency: :gun) + end + let(:quirk_artifact) do + Artifact.find_by(granblue_id: '401110401') || + create(:artifact, :quirk, granblue_id: '401110401', name_en: 'Fantosmik Fengtooth') + end + + # Create artifact skills for the skill lookups + before do + # Create artifacts + standard_artifact + standard_artifact_2 + quirk_artifact + + # Group I skills + ArtifactSkill.find_by(skill_group: 1, modifier: 2) || + create(:artifact_skill, :group_i, modifier: 2, name_en: 'HP', base_values: [660, 720, 780, 840, 900]) + ArtifactSkill.find_by(skill_group: 1, modifier: 5) || + create(:artifact_skill, :group_i, modifier: 5, name_en: 'Elemental ATK', base_values: [8.8, 9.6, 10.4, 11.2, 12.0]) + ArtifactSkill.find_by(skill_group: 1, modifier: 11) || + create(:artifact_skill, :group_i, modifier: 11, name_en: 'Dodge Rate', base_values: [4.4, 4.8, 5.2, 5.6, 6.0]) + + # Group II skills + ArtifactSkill.find_by(skill_group: 2, modifier: 2) || + create(:artifact_skill, :group_ii, modifier: 2, name_en: 'Skill DMG Cap', base_values: [8.8, 9.6, 10.4, 11.2, 12.0]) + ArtifactSkill.find_by(skill_group: 2, modifier: 8) || + create(:artifact_skill, :group_ii, modifier: 8, name_en: 'C.A. DMG cap boost tradeoff', base_values: [13.2, 14.4, 15.6, 16.8, 18.0]) + + # Group III skills + ArtifactSkill.find_by(skill_group: 3, modifier: 5) || + create(:artifact_skill, :group_iii, modifier: 5, name_en: 'Switch amplified', base_values: [3, 6, 9, 12, 15]) + ArtifactSkill.find_by(skill_group: 3, modifier: 25) || + create(:artifact_skill, :group_iii, modifier: 25, name_en: 'Armored', base_values: [5, 10, 15, 20, 25]) + + # Clear the cache so new skills are picked up + ArtifactSkill.clear_cache! + end + + after do + ArtifactSkill.clear_cache! + end + + describe '#import' do + context 'with valid game data' do + let(:game_data) do + { + 'list' => [ + { + 'artifact_id' => 301_070_101, + 'id' => 8_138_020, + 'level' => '1', + 'kind' => '7', + 'attribute' => '5', + 'skill1_info' => { 'skill_id' => 30_091, 'skill_quality' => 1, 'level' => 1 }, + 'skill2_info' => { 'skill_id' => 20_015, 'skill_quality' => 5, 'level' => 1 }, + 'skill3_info' => { 'skill_id' => 30_301, 'skill_quality' => 1, 'level' => 1 }, + 'skill4_info' => { 'skill_id' => 50_201, 'skill_quality' => 1, 'level' => 1 } + } + ] + } + end + + it 'creates a collection artifact' do + service = described_class.new(user, game_data) + result = service.import + + expect(result.success?).to be true + expect(result.created.size).to eq(1) + expect(result.errors).to be_empty + end + + it 'sets the correct game_id' do + service = described_class.new(user, game_data) + result = service.import + + artifact = result.created.first + expect(artifact.game_id).to eq('8138020') + end + + it 'maps element correctly' do + # attribute 5 = Light in game, which maps to our light (6) + service = described_class.new(user, game_data) + result = service.import + + artifact = result.created.first + expect(artifact.element).to eq('light') + end + + it 'decodes skill1 correctly' do + # skill_id 30091 = Dodge Rate (group 1, modifier 11), quality 1 = strength 4.4 + service = described_class.new(user, game_data) + result = service.import + + artifact = result.created.first + expect(artifact.skill1['modifier']).to eq(11) + expect(artifact.skill1['strength']).to eq(4.4) + expect(artifact.skill1['level']).to eq(1) + end + + it 'decodes skill2 with max quality correctly' do + # skill_id 20015 = HP (group 1, modifier 2), quality 5 = strength 900 + service = described_class.new(user, game_data) + result = service.import + + artifact = result.created.first + expect(artifact.skill2['modifier']).to eq(2) + expect(artifact.skill2['strength']).to eq(900) + end + + it 'decodes skill3 (group II) correctly' do + # skill_id 30301 = C.A. DMG cap boost tradeoff (group 2, modifier 8), quality 1 = strength 13.2 + service = described_class.new(user, game_data) + result = service.import + + artifact = result.created.first + expect(artifact.skill3['modifier']).to eq(8) + expect(artifact.skill3['strength']).to eq(13.2) + end + + it 'decodes skill4 (group III) correctly' do + # skill_id 50201 = Switch amplified (group 3, modifier 5), quality 1 = strength 3 + service = described_class.new(user, game_data) + result = service.import + + artifact = result.created.first + expect(artifact.skill4['modifier']).to eq(5) + expect(artifact.skill4['strength']).to eq(3) + end + end + + context 'with duplicate game_id' do + let(:game_data) do + { + 'list' => [ + { + 'artifact_id' => 301_070_101, + 'id' => 8_138_020, + 'level' => '1', + 'kind' => '7', + 'attribute' => '5', + 'skill1_info' => { 'skill_id' => 30_091, 'skill_quality' => 1, 'level' => 1 }, + 'skill2_info' => { 'skill_id' => 20_015, 'skill_quality' => 5, 'level' => 1 }, + 'skill3_info' => { 'skill_id' => 30_301, 'skill_quality' => 1, 'level' => 1 }, + 'skill4_info' => { 'skill_id' => 50_201, 'skill_quality' => 1, 'level' => 1 } + } + ] + } + end + + before do + create(:collection_artifact, user: user, artifact: standard_artifact, game_id: '8138020') + end + + it 'skips the duplicate' do + service = described_class.new(user, game_data) + result = service.import + + expect(result.success?).to be true + expect(result.created.size).to eq(0) + expect(result.skipped.size).to eq(1) + expect(result.skipped.first[:reason]).to eq('Already exists') + end + + context 'with update_existing: true' do + it 'updates the existing artifact' do + service = described_class.new(user, game_data, update_existing: true) + result = service.import + + expect(result.success?).to be true + expect(result.created.size).to eq(0) + expect(result.updated.size).to eq(1) + end + end + end + + context 'with quirk artifact' do + let(:game_data) do + { + 'list' => [ + { + 'artifact_id' => 401_110_401, + 'rarity' => '4', + 'id' => 7_977_596, + 'level' => '1', + 'kind' => '8', + 'attribute' => '6', + 'skill1_info' => { 'skill_id' => 50_321, 'skill_quality' => 1, 'level' => 1 }, + 'skill2_info' => { 'skill_id' => 50_331, 'skill_quality' => 1, 'level' => 1 }, + 'skill3_info' => { 'skill_id' => 50_341, 'skill_quality' => 1, 'level' => 1 }, + 'skill4_info' => { 'skill_id' => 50_351, 'skill_quality' => 1, 'level' => 1 } + } + ] + } + end + + it 'creates the quirk artifact with proficiency' do + service = described_class.new(user, game_data) + result = service.import + + expect(result.success?).to be true + artifact = result.created.first + expect(artifact.artifact.quirk?).to be true + expect(artifact.proficiency).to eq('harp') # kind 8 = harp + end + + it 'stores empty skills for unknown skill_ids' do + service = described_class.new(user, game_data) + result = service.import + + artifact = result.created.first + expect(artifact.skill1).to eq({}) + expect(artifact.skill2).to eq({}) + expect(artifact.skill3).to eq({}) + expect(artifact.skill4).to eq({}) + end + + it 'maps element correctly for quirk artifacts' do + # attribute 6 = Dark in game, which maps to our dark (5) + service = described_class.new(user, game_data) + result = service.import + + artifact = result.created.first + expect(artifact.element).to eq('dark') + end + end + + context 'with unknown artifact_id' do + let(:game_data) do + { + 'list' => [ + { + 'artifact_id' => 999_999_999, + 'id' => 1234, + 'level' => '1', + 'kind' => '1', + 'attribute' => '1', + 'skill1_info' => {}, + 'skill2_info' => {}, + 'skill3_info' => {}, + 'skill4_info' => {} + } + ] + } + end + + it 'records an error for the unknown artifact' do + service = described_class.new(user, game_data) + result = service.import + + expect(result.errors.size).to eq(1) + expect(result.errors.first[:error]).to eq('Artifact not found') + end + end + + context 'with multiple artifacts' do + let(:game_data) do + { + 'list' => [ + { + 'artifact_id' => 301_070_101, + 'id' => 8_138_020, + 'level' => '1', + 'kind' => '7', + 'attribute' => '5', + 'skill1_info' => { 'skill_id' => 30_091, 'skill_quality' => 1, 'level' => 1 }, + 'skill2_info' => { 'skill_id' => 20_015, 'skill_quality' => 5, 'level' => 1 }, + 'skill3_info' => { 'skill_id' => 30_301, 'skill_quality' => 1, 'level' => 1 }, + 'skill4_info' => { 'skill_id' => 50_201, 'skill_quality' => 1, 'level' => 1 } + }, + { + 'artifact_id' => 301_090_101, + 'id' => 8_061_615, + 'level' => '1', + 'kind' => '9', + 'attribute' => '6', + 'skill1_info' => { 'skill_id' => 30_112, 'skill_quality' => 2, 'level' => 1 }, + 'skill2_info' => { 'skill_id' => 20_011, 'skill_quality' => 1, 'level' => 1 }, + 'skill3_info' => { 'skill_id' => 30_141, 'skill_quality' => 1, 'level' => 1 }, + 'skill4_info' => { 'skill_id' => 50_121, 'skill_quality' => 1, 'level' => 1 } + } + ] + } + end + + it 'imports all artifacts' do + service = described_class.new(user, game_data) + result = service.import + + expect(result.success?).to be true + expect(result.created.size).to eq(2) + end + + it 'associates correct artifacts' do + service = described_class.new(user, game_data) + result = service.import + + artifacts = result.created.sort_by(&:game_id) + expect(artifacts[0].artifact.name_en).to eq('Ominous Whistle') + expect(artifacts[1].artifact.name_en).to eq('Ominous Bangle') + end + end + + context 'with empty data' do + let(:game_data) { { 'list' => [] } } + + it 'returns an error' do + service = described_class.new(user, game_data) + result = service.import + + expect(result.success?).to be false + expect(result.errors).to include('No artifact items found in data') + end + end + + context 'with array format data' do + let(:game_data) do + [ + { + 'artifact_id' => 301_070_101, + 'id' => 8_138_020, + 'level' => '1', + 'kind' => '7', + 'attribute' => '5', + 'skill1_info' => { 'skill_id' => 30_091, 'skill_quality' => 1, 'level' => 1 }, + 'skill2_info' => { 'skill_id' => 20_015, 'skill_quality' => 5, 'level' => 1 }, + 'skill3_info' => { 'skill_id' => 30_301, 'skill_quality' => 1, 'level' => 1 }, + 'skill4_info' => { 'skill_id' => 50_201, 'skill_quality' => 1, 'level' => 1 } + } + ] + end + + it 'handles array format correctly' do + service = described_class.new(user, game_data) + result = service.import + + expect(result.success?).to be true + expect(result.created.size).to eq(1) + end + end + end + + describe 'element mapping' do + # Game: 1=Fire, 2=Water, 3=Earth, 4=Wind, 5=Light, 6=Dark + # Ours: wind=1, fire=2, water=3, earth=4, dark=5, light=6 + { + '1' => 'fire', + '2' => 'water', + '3' => 'earth', + '4' => 'wind', + '5' => 'light', + '6' => 'dark' + }.each do |game_attr, expected_element| + it "maps game attribute #{game_attr} to #{expected_element}" do + game_data = { + 'list' => [ + { + 'artifact_id' => 301_070_101, + 'id' => 1234, + 'level' => '1', + 'kind' => '7', + 'attribute' => game_attr, + 'skill1_info' => {}, + 'skill2_info' => {}, + 'skill3_info' => {}, + 'skill4_info' => {} + } + ] + } + + service = described_class.new(user, game_data) + result = service.import + + expect(result.created.first.element).to eq(expected_element) + end + end + end + + describe 'skill quality to strength mapping' do + it 'maps quality 1 to base_values[0]' do + game_data = { + 'list' => [ + { + 'artifact_id' => 301_070_101, + 'id' => 1234, + 'level' => '1', + 'kind' => '7', + 'attribute' => '1', + 'skill1_info' => { 'skill_id' => 30_091, 'skill_quality' => 1, 'level' => 1 }, + 'skill2_info' => {}, + 'skill3_info' => {}, + 'skill4_info' => {} + } + ] + } + + service = described_class.new(user, game_data) + result = service.import + + # Dodge Rate base_values[0] = 4.4 + expect(result.created.first.skill1['strength']).to eq(4.4) + end + + it 'maps quality 5 to base_values[4]' do + game_data = { + 'list' => [ + { + 'artifact_id' => 301_070_101, + 'id' => 1234, + 'level' => '1', + 'kind' => '7', + 'attribute' => '1', + 'skill1_info' => { 'skill_id' => 30_095, 'skill_quality' => 5, 'level' => 1 }, + 'skill2_info' => {}, + 'skill3_info' => {}, + 'skill4_info' => {} + } + ] + } + + service = described_class.new(user, game_data) + result = service.import + + # Dodge Rate base_values[4] = 6.0 + expect(result.created.first.skill1['strength']).to eq(6.0) + end + end +end