parses game JSON inventory data and creates collection records 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
528 lines
14 KiB
Ruby
528 lines
14 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
RSpec.describe CharacterImportService, type: :service do
|
|
let(:user) { create(:user) }
|
|
|
|
# Create character awakening first (required by model's before_save callback)
|
|
let!(:awakening_balanced) do
|
|
Awakening.find_by(slug: 'character-balanced', object_type: 'Character') ||
|
|
create(:awakening, :for_character, slug: 'character-balanced', name_en: 'Balanced')
|
|
end
|
|
|
|
# Create characters with specific granblue_ids matching the game data
|
|
# Use unique IDs that won't conflict with seeded data
|
|
let(:standard_character) do
|
|
Character.find_by(granblue_id: '3040171000') ||
|
|
create(:character, granblue_id: '3040171000', name_en: 'Hallessena')
|
|
end
|
|
|
|
let(:flb_character) do
|
|
Character.find_by(granblue_id: '3040167000') ||
|
|
create(:character, granblue_id: '3040167000', name_en: 'Zeta')
|
|
end
|
|
|
|
let(:transcendable_character) do
|
|
Character.find_by(granblue_id: '3040036000') ||
|
|
create(:character, :transcendable, granblue_id: '3040036000', name_en: 'Siegfried')
|
|
end
|
|
|
|
let(:another_character) do
|
|
Character.find_by(granblue_id: '3040212000') ||
|
|
create(:character, granblue_id: '3040212000', name_en: 'Narmaya (Summer)')
|
|
end
|
|
|
|
before do
|
|
standard_character
|
|
flb_character
|
|
transcendable_character
|
|
another_character
|
|
end
|
|
|
|
describe '#import' do
|
|
context 'with valid game data' do
|
|
let(:game_data) do
|
|
{
|
|
'list' => [
|
|
{
|
|
'master' => {
|
|
'id' => '3040171000',
|
|
'max_evolution_level' => 4
|
|
},
|
|
'param' => {
|
|
'id' => 129_355_003,
|
|
'evolution' => '4',
|
|
'phase' => '0',
|
|
'arousal_level' => 1
|
|
}
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
it 'creates a collection character' 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 uncap_level from evolution' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
character = result.created.first
|
|
expect(character.uncap_level).to eq(4)
|
|
end
|
|
|
|
it 'associates the correct character via granblue_id' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
character = result.created.first
|
|
expect(character.character.granblue_id).to eq('3040171000')
|
|
end
|
|
|
|
it 'sets awakening_level from arousal_level' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
character = result.created.first
|
|
expect(character.awakening_level).to eq(1)
|
|
end
|
|
end
|
|
|
|
context 'with FLB character (evolution 5)' do
|
|
let(:game_data) do
|
|
{
|
|
'list' => [
|
|
{
|
|
'master' => {
|
|
'id' => '3040167000',
|
|
'max_evolution_level' => 5
|
|
},
|
|
'param' => {
|
|
'id' => 128_935_603,
|
|
'evolution' => '5',
|
|
'phase' => '0',
|
|
'arousal_level' => 9
|
|
}
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
it 'sets uncap_level to 5' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
character = result.created.first
|
|
expect(character.uncap_level).to eq(5)
|
|
end
|
|
|
|
it 'sets awakening_level from arousal_level' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
character = result.created.first
|
|
expect(character.awakening_level).to eq(9)
|
|
end
|
|
end
|
|
|
|
context 'with transcendence data (evolution 6, phase 5)' do
|
|
let(:game_data) do
|
|
{
|
|
'list' => [
|
|
{
|
|
'master' => {
|
|
'id' => '3040036000',
|
|
'max_evolution_level' => 6
|
|
},
|
|
'param' => {
|
|
'id' => 128_343_789,
|
|
'evolution' => '6',
|
|
'phase' => '5',
|
|
'arousal_level' => 10
|
|
}
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
it 'clamps uncap_level to 5 even when evolution is 6' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
character = result.created.first
|
|
expect(character.uncap_level).to eq(5)
|
|
end
|
|
|
|
it 'sets transcendence_step from phase' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
character = result.created.first
|
|
expect(character.transcendence_step).to eq(5)
|
|
end
|
|
|
|
it 'sets max awakening_level' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
character = result.created.first
|
|
expect(character.awakening_level).to eq(10)
|
|
end
|
|
end
|
|
|
|
context 'with duplicate character (unique per user)' do
|
|
let(:game_data) do
|
|
{
|
|
'list' => [
|
|
{
|
|
'master' => {
|
|
'id' => '3040171000'
|
|
},
|
|
'param' => {
|
|
'id' => 999_999_999,
|
|
'evolution' => '4',
|
|
'phase' => '0',
|
|
'arousal_level' => 5
|
|
}
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
before do
|
|
create(:collection_character, user: user, character: standard_character)
|
|
end
|
|
|
|
it 'skips the duplicate based on character_id (not game_id)' 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
|
|
let(:game_data_updated) do
|
|
{
|
|
'list' => [
|
|
{
|
|
'master' => {
|
|
'id' => '3040171000'
|
|
},
|
|
'param' => {
|
|
'id' => 999_999_999,
|
|
'evolution' => '5',
|
|
'phase' => '0',
|
|
'arousal_level' => 10
|
|
}
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
it 'updates the existing character' do
|
|
service = described_class.new(user, game_data_updated, 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)
|
|
expect(result.updated.first.uncap_level).to eq(5)
|
|
expect(result.updated.first.awakening_level).to eq(10)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with unknown character' do
|
|
let(:game_data) do
|
|
{
|
|
'list' => [
|
|
{
|
|
'master' => {
|
|
'id' => '9999999999'
|
|
},
|
|
'param' => {
|
|
'id' => 12_345,
|
|
'evolution' => '4',
|
|
'phase' => '0',
|
|
'arousal_level' => 1
|
|
}
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
it 'records an error for the unknown character' 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('Character not found')
|
|
end
|
|
end
|
|
|
|
context 'with multiple characters' do
|
|
let(:game_data) do
|
|
{
|
|
'list' => [
|
|
{
|
|
'master' => { 'id' => '3040171000' },
|
|
'param' => {
|
|
'id' => 111_111_111,
|
|
'evolution' => '4',
|
|
'phase' => '0',
|
|
'arousal_level' => 1
|
|
}
|
|
},
|
|
{
|
|
'master' => { 'id' => '3040212000' },
|
|
'param' => {
|
|
'id' => 222_222_222,
|
|
'evolution' => '5',
|
|
'phase' => '0',
|
|
'arousal_level' => 7
|
|
}
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
it 'imports all characters' 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 characters' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
granblue_ids = result.created.map { |c| c.character.granblue_id }.sort
|
|
expect(granblue_ids).to eq(%w[3040171000 3040212000])
|
|
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 character items found in data')
|
|
end
|
|
end
|
|
|
|
context 'with array format data' do
|
|
let(:game_data) do
|
|
[
|
|
{
|
|
'master' => { 'id' => '3040171000' },
|
|
'param' => {
|
|
'id' => 777_777_777,
|
|
'evolution' => '4',
|
|
'phase' => '0',
|
|
'arousal_level' => 5
|
|
}
|
|
}
|
|
]
|
|
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)
|
|
expect(result.created.first.awakening_level).to eq(5)
|
|
end
|
|
end
|
|
|
|
context 'with max values' do
|
|
let(:game_data) do
|
|
{
|
|
'list' => [
|
|
{
|
|
'master' => { 'id' => '3040036000' },
|
|
'param' => {
|
|
'id' => 888_888_888,
|
|
'evolution' => '10',
|
|
'phase' => '15',
|
|
'arousal_level' => 99
|
|
}
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
it 'clamps uncap_level to max 5' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
character = result.created.first
|
|
expect(character.uncap_level).to eq(5)
|
|
end
|
|
|
|
it 'clamps transcendence_step to max 10' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
character = result.created.first
|
|
expect(character.transcendence_step).to eq(10)
|
|
end
|
|
|
|
it 'clamps awakening_level to max 10' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
character = result.created.first
|
|
expect(character.awakening_level).to eq(10)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'edge cases' do
|
|
context 'with nil arousal_level' do
|
|
let(:game_data) do
|
|
{
|
|
'list' => [
|
|
{
|
|
'master' => { 'id' => '3040171000' },
|
|
'param' => {
|
|
'id' => 555_555_555,
|
|
'evolution' => '4',
|
|
'phase' => '0',
|
|
'arousal_level' => nil
|
|
}
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
it 'defaults awakening_level to 1' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
expect(result.success?).to be true
|
|
expect(result.created.first.awakening_level).to eq(1)
|
|
end
|
|
end
|
|
|
|
context 'with arousal_level 0' do
|
|
let(:game_data) do
|
|
{
|
|
'list' => [
|
|
{
|
|
'master' => { 'id' => '3040171000' },
|
|
'param' => {
|
|
'id' => 444_444_444,
|
|
'evolution' => '4',
|
|
'phase' => '0',
|
|
'arousal_level' => 0
|
|
}
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
it 'defaults awakening_level to 1' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
expect(result.success?).to be true
|
|
expect(result.created.first.awakening_level).to eq(1)
|
|
end
|
|
end
|
|
|
|
context 'with string evolution values' do
|
|
let(:game_data) do
|
|
{
|
|
'list' => [
|
|
{
|
|
'master' => { 'id' => '3040171000' },
|
|
'param' => {
|
|
'id' => 333_333_333,
|
|
'evolution' => '5',
|
|
'phase' => '0',
|
|
'arousal_level' => '7'
|
|
}
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
it 'handles string values correctly' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
expect(result.success?).to be true
|
|
expect(result.created.first.uncap_level).to eq(5)
|
|
expect(result.created.first.awakening_level).to eq(7)
|
|
end
|
|
end
|
|
|
|
context 'with missing param fields' do
|
|
let(:game_data) do
|
|
{
|
|
'list' => [
|
|
{
|
|
'master' => { 'id' => '3040171000' },
|
|
'param' => {
|
|
'id' => 222_222_222
|
|
}
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
it 'handles missing fields gracefully (defaults to 0/1)' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
expect(result.success?).to be true
|
|
character = result.created.first
|
|
expect(character.uncap_level).to eq(0)
|
|
expect(character.transcendence_step).to eq(0)
|
|
expect(character.awakening_level).to eq(1)
|
|
end
|
|
end
|
|
|
|
context 'assigns default awakening via model callback' do
|
|
let(:game_data) do
|
|
{
|
|
'list' => [
|
|
{
|
|
'master' => { 'id' => '3040171000' },
|
|
'param' => {
|
|
'id' => 111_111_111,
|
|
'evolution' => '4',
|
|
'phase' => '0',
|
|
'arousal_level' => 5
|
|
}
|
|
}
|
|
]
|
|
}
|
|
end
|
|
|
|
it 'has default awakening set by model' do
|
|
service = described_class.new(user, game_data)
|
|
result = service.import
|
|
|
|
expect(result.success?).to be true
|
|
expect(result.created.first.awakening).to eq(awakening_balanced)
|
|
end
|
|
end
|
|
end
|
|
end
|