require 'rails_helper' RSpec.describe PostImportRunner do let(:actor) { create(:user, :member) } def row(source_row: 1, url: 'https://example.com/post') { sourceRow: source_row, url:, attributes: { title: 'title', tags: '' }, provenance: { url: 'manual', title: 'manual', tags: 'manual' }, tagSources: { automatic: '', manual: '' } } end def preview(source_row: 1, errors: { }, skip_reason: nil, existing_post_id: nil) { source_row:, attributes: { 'title' => 'title', 'tags' => '' }, validation_errors: errors, skip_reason:, existing_post_id: } end it 'previews the whole batch once before processing individual rows' do rows = [row, row(source_row: 2, url: 'https://example.com/two')] previewer = instance_double(PostImportPreviewer) allow(PostImportPreviewer).to receive(:new).and_return(previewer) expect(previewer).to receive(:preview_rows) .with( rows: satisfy { _1.map { |row_value| row_value['source_row'] } == [1, 2] }, fetch_metadata: false ) .and_return([preview, preview(source_row: 2)]) allow(PostCreator).to receive(:new).and_return( instance_double(PostCreator, create!: create(:post)) ) result = described_class.new(actor:, rows:).run expect(result).to include(created: 2, skipped: 0, failed: 0) end it 'treats validation errors as failures before an existing skip' do existing = create(:post) previewer = instance_double(PostImportPreviewer) allow(PostImportPreviewer).to receive(:new).and_return(previewer) allow(previewer).to receive(:preview_rows).and_return([ preview(errors: { url: ['URL が重複しています.'] }, skip_reason: 'existing', existing_post_id: existing.id) ]) expect(PostCreator).not_to receive(:new) result = described_class.new(actor:, rows: [row]).run.fetch(:rows).first expect(result).to include( status: 'failed', errors: { url: ['URL が重複しています.'] }, recoverable: true ) end it 'returns the existing post ID for skipped rows' do existing = create(:post) previewer = instance_double(PostImportPreviewer) allow(PostImportPreviewer).to receive(:new).and_return(previewer) allow(previewer).to receive(:preview_rows).and_return([ preview(skip_reason: 'existing', existing_post_id: existing.id) ]) result = described_class.new(actor:, rows: [row]).run.fetch(:rows).first expect(result).to eq( source_row: 1, status: 'skipped', existing_post_id: existing.id ) end it 'converts a URL uniqueness validation race into a skip' do existing = create(:post, url: 'https://example.com/race') invalid = Post.new(url: existing.url) invalid.errors.add(:url, :taken) previewer = instance_double(PostImportPreviewer) allow(PostImportPreviewer).to receive(:new).and_return(previewer) allow(previewer).to receive(:preview_rows).and_return([preview]) creator = instance_double(PostCreator) allow(PostCreator).to receive(:new).and_return(creator) allow(creator).to receive(:create!).and_raise(ActiveRecord::RecordInvalid.new(invalid)) result = described_class.new( actor:, rows: [row(url: 'https://EXAMPLE.com/race/')] ).run.fetch(:rows).first expect(result).to include(status: 'skipped', existing_post_id: existing.id) end it 're-raises RecordNotUnique errors unrelated to the posts URL index' do previewer = instance_double(PostImportPreviewer) allow(PostImportPreviewer).to receive(:new).and_return(previewer) allow(previewer).to receive(:preview_rows).and_return([preview]) creator = instance_double(PostCreator) allow(PostCreator).to receive(:new).and_return(creator) allow(creator).to receive(:create!) .and_raise(ActiveRecord::RecordNotUnique, 'other_unique_index') expect { described_class.new(actor:, rows: [row]).run }.to raise_error(ActiveRecord::RecordNotUnique) end it 'converts a posts URL index race into a skip' do existing = create(:post, url: 'https://example.com/index-race') previewer = instance_double(PostImportPreviewer) allow(PostImportPreviewer).to receive(:new).and_return(previewer) allow(previewer).to receive(:preview_rows).and_return([preview]) creator = instance_double(PostCreator) allow(PostCreator).to receive(:new).and_return(creator) allow(creator).to receive(:create!).and_raise( ActiveRecord::RecordNotUnique, 'duplicate key index_posts_on_url' ) result = described_class.new( actor:, rows: [row(url: 'https://EXAMPLE.com/index-race/')] ).run.fetch(:rows).first expect(result).to include(status: 'skipped', existing_post_id: existing.id) end it 'returns thumbnail warnings from PostCreator on a created row' do previewer = instance_double(PostImportPreviewer) allow(PostImportPreviewer).to receive(:new).and_return(previewer) allow(previewer).to receive(:preview_rows).and_return([preview]) created_post = create(:post) creator = instance_double( PostCreator, create!: created_post, field_warnings: { thumbnail_base: ['サムネール画像を取得できませんでした.'] }) allow(PostCreator).to receive(:new).and_return(creator) result = described_class.new(actor:, rows: [row]).run.fetch(:rows).first expect(result).to include( status: 'created', field_warnings: { thumbnail_base: ['サムネール画像を取得できませんでした.'] } ) end end