require 'rails_helper' RSpec.describe PostImportPreviewer do before do allow(Preview::UrlSafety).to receive(:validate) do |url| [URI.parse(url), ['8.8.8.8']] end end def row(source_row:, url:, attributes: { }, provenance: { }, **values) { source_row:, url:, attributes:, provenance: }.merge(values) end describe '#preview_rows' do it 'marks an existing post for skipping without fetching metadata' do existing = create(:post, url: 'https://example.com/existing') expect(PostMetadataFetcher).not_to receive(:fetch) result = described_class.new.preview_rows(rows: [ row(source_row: 1, url: 'https://EXAMPLE.com/existing/') ]).first expect(result).to include( url: 'https://example.com/existing', skip_reason: 'existing', existing_post_id: existing.id, validation_errors: {} ) expect(result.fetch(:field_warnings)).to eq({}) end it 'reports list duplicates as URL errors instead of existing skips' do create(:post, url: 'https://example.com/duplicate') expect(PostMetadataFetcher).not_to receive(:fetch) results = described_class.new.preview_rows(rows: [ row(source_row: 1, url: 'https://example.com/duplicate'), row(source_row: 2, url: 'https://EXAMPLE.com/duplicate/') ]) expect(results.map { _1[:validation_errors] }).to all( include(url: ['URL が重複しています.']) ) expect(results).to all(include(skip_reason: nil, existing_post_id: nil)) end it 'does not fetch metadata for an unsafe URL' do allow(Preview::UrlSafety).to receive(:validate) .and_raise(Preview::UrlSafety::UnsafeUrl, '安全でない接続先は使用できません.') expect(PostMetadataFetcher).not_to receive(:fetch) result = described_class.new.preview_rows(rows: [ row(source_row: 1, url: 'https://unsafe.example/post') ]).first expect(result.fetch(:validation_errors)).to include( url: ['安全でない接続先は使用できません.'] ) end it 'applies metadata to automatic fields and recognises metadata tags' do Tag.create!(name: 'known-tag', category: :general) allow(PostMetadataFetcher).to receive(:fetch).and_return( title: 'metadata title', thumbnail_base: 'https://example.com/thumb.jpg', duration: '2', tags: 'known-tag' ) result = described_class.new.preview_rows(rows: [ row(source_row: 1, url: 'https://example.com/new') ]).first expect(result.fetch(:attributes)).to include( 'title' => 'metadata title', 'thumbnail_base' => 'https://example.com/thumb.jpg', 'duration' => '2', 'tags' => 'known-tag' ) expect(result.fetch(:field_warnings)).not_to have_key('tags') expect(PostMetadataFetcher).to have_received(:fetch).once end it 'preserves manual empty values when metadata is available' do allow(PostMetadataFetcher).to receive(:fetch).and_return( title: 'metadata title', tags: 'metadata-tag' ) result = described_class.new.preview_rows(rows: [ row( source_row: 1, url: 'https://example.com/manual', attributes: { title: '', tags: '' }, provenance: { title: 'manual', tags: 'manual' }, tag_sources: { automatic: 'old-tag', manual: '' } ) ]).first expect(result.fetch(:attributes)).to include('title' => '', 'tags' => '') expect(result.fetch(:tag_sources)).to include('manual' => '') end it 'clears warnings derived from an old metadata URL' do allow(PostMetadataFetcher).to receive(:fetch).and_return( title: 'new title', thumbnail_base: 'https://example.com/new-thumb.jpg', tags: '' ) result = described_class.new.preview_rows(rows: [ row( source_row: 1, url: 'https://example.com/new-url', attributes: { title: 'old title', tags: 'old-tag' }, provenance: { title: 'automatic', tags: 'automatic' }, tag_sources: { automatic: 'old-tag', manual: '' }, metadata_url: 'https://example.com/old-url', field_warnings: { tags: ['old tag warning'], url: ['old fetch warning'] }, base_warnings: ['old base warning'] ) ]).first expect(result.fetch(:attributes)).to include('title' => 'new title', 'tags' => '') expect(result.fetch(:base_warnings)).to eq([]) expect(result.fetch(:field_warnings).values.flatten) .not_to include('old tag warning', 'old fetch warning') end it 'isolates an unexpected metadata failure to the affected URL' do allow(PostMetadataFetcher).to receive(:fetch) do |url| raise 'fetch failure' if url.include?('failure') { title: 'successful title', tags: '' } end allow(Rails.logger).to receive(:error) results = described_class.new.preview_rows(rows: [ row(source_row: 1, url: 'https://example.com/failure'), row(source_row: 2, url: 'https://example.com/success') ]) expect(results[0].fetch(:field_warnings)).to include( 'url' => [described_class::METADATA_FETCH_WARNING] ) expect(results[1].fetch(:attributes)).to include('title' => 'successful title') end it 'turns an unsafe URL detected during metadata fetching into a URL error' do allow(PostMetadataFetcher).to receive(:fetch) .and_raise(Preview::UrlSafety::UnsafeUrl, '安全でない接続先です.') allow(Rails.logger).to receive(:info) result = described_class.new.preview_rows(rows: [ row(source_row: 1, url: 'https://example.com/redirects-to-private') ]).first expect(result.fetch(:validation_errors)).to include( url: ['安全でない接続先です.'] ) end it 'validates parent IDs from the preloaded set' do parent = create(:post) results = described_class.new.preview_rows( rows: [ row(source_row: 1, url: 'https://example.com/valid-parent', attributes: { parent_post_ids: parent.id.to_s }), row(source_row: 2, url: 'https://example.com/missing-parent', attributes: { parent_post_ids: '999999' }) ], fetch_metadata: false ) expect(results[0].fetch(:validation_errors)).not_to have_key(:parent_post_ids) expect(results[1].fetch(:validation_errors)).to include( parent_post_ids: ['存在しない親投稿 Id. があります.'] ) end end end