import_spec.rb 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. # frozen_string_literal: true
  2. require 'rails_helper'
  3. RSpec.describe Form::Import do
  4. subject { described_class.new(current_account: account, type: import_type, mode: import_mode, data: data) }
  5. let(:account) { Fabricate(:account) }
  6. let(:data) { fixture_file_upload(import_file) }
  7. let(:import_mode) { 'merge' }
  8. describe 'validations' do
  9. shared_examples 'incompatible import type' do |type, file|
  10. let(:import_file) { file }
  11. let(:import_type) { type }
  12. it 'has errors' do
  13. subject.validate
  14. expect(subject.errors[:data]).to include(I18n.t('imports.errors.incompatible_type'))
  15. end
  16. end
  17. shared_examples 'too many CSV rows' do |type, file, allowed_rows|
  18. let(:import_file) { file }
  19. let(:import_type) { type }
  20. before do
  21. stub_const 'Form::Import::ROWS_PROCESSING_LIMIT', allowed_rows
  22. end
  23. it 'has errors' do
  24. subject.validate
  25. expect(subject.errors[:data]).to include(I18n.t('imports.errors.over_rows_processing_limit', count: described_class::ROWS_PROCESSING_LIMIT))
  26. end
  27. end
  28. shared_examples 'valid import' do |type, file|
  29. let(:import_file) { file }
  30. let(:import_type) { type }
  31. it 'passes validation' do
  32. expect(subject).to be_valid
  33. end
  34. end
  35. context 'when the file too large' do
  36. let(:import_type) { 'following' }
  37. let(:import_file) { 'imports.txt' }
  38. before do
  39. stub_const 'Form::Import::FILE_SIZE_LIMIT', 5
  40. end
  41. it 'has errors' do
  42. subject.validate
  43. expect(subject.errors[:data]).to include(I18n.t('imports.errors.too_large'))
  44. end
  45. end
  46. context 'when the CSV file is malformed CSV' do
  47. let(:import_type) { 'following' }
  48. let(:import_file) { 'boop.ogg' }
  49. it 'has errors' do
  50. # NOTE: not testing more specific error because we don't know the string to match
  51. expect(subject).to model_have_error_on_field(:data)
  52. end
  53. end
  54. context 'when importing more follows than allowed' do
  55. let(:import_type) { 'following' }
  56. let(:import_file) { 'imports.txt' }
  57. before do
  58. allow(FollowLimitValidator).to receive(:limit_for_account).with(account).and_return(1)
  59. end
  60. it 'has errors' do
  61. subject.validate
  62. expect(subject.errors[:data]).to include(I18n.t('users.follow_limit_reached', limit: 1))
  63. end
  64. end
  65. it_behaves_like 'too many CSV rows', 'following', 'imports.txt', 1
  66. it_behaves_like 'too many CSV rows', 'blocking', 'imports.txt', 1
  67. it_behaves_like 'too many CSV rows', 'muting', 'imports.txt', 1
  68. it_behaves_like 'too many CSV rows', 'domain_blocking', 'domain_blocks.csv', 2
  69. it_behaves_like 'too many CSV rows', 'bookmarks', 'bookmark-imports.txt', 3
  70. it_behaves_like 'too many CSV rows', 'lists', 'lists.csv', 2
  71. # Importing list of addresses with no headers into various types
  72. it_behaves_like 'valid import', 'following', 'imports.txt'
  73. it_behaves_like 'valid import', 'blocking', 'imports.txt'
  74. it_behaves_like 'valid import', 'muting', 'imports.txt'
  75. # Importing domain blocks with headers into expected type
  76. it_behaves_like 'valid import', 'domain_blocking', 'domain_blocks.csv'
  77. # Importing bookmarks list with no headers into expected type
  78. it_behaves_like 'valid import', 'bookmarks', 'bookmark-imports.txt'
  79. # Importing lists with no headers into expected type
  80. it_behaves_like 'valid import', 'lists', 'lists.csv'
  81. # Importing followed accounts with headers into various compatible types
  82. it_behaves_like 'valid import', 'following', 'following_accounts.csv'
  83. it_behaves_like 'valid import', 'blocking', 'following_accounts.csv'
  84. it_behaves_like 'valid import', 'muting', 'following_accounts.csv'
  85. # Importing domain blocks with headers into incompatible types
  86. it_behaves_like 'incompatible import type', 'following', 'domain_blocks.csv'
  87. it_behaves_like 'incompatible import type', 'blocking', 'domain_blocks.csv'
  88. it_behaves_like 'incompatible import type', 'muting', 'domain_blocks.csv'
  89. it_behaves_like 'incompatible import type', 'bookmarks', 'domain_blocks.csv'
  90. # Importing followed accounts with headers into incompatible types
  91. it_behaves_like 'incompatible import type', 'domain_blocking', 'following_accounts.csv'
  92. it_behaves_like 'incompatible import type', 'bookmarks', 'following_accounts.csv'
  93. end
  94. describe '#guessed_type' do
  95. shared_examples 'with enough information' do |type, file, original_filename, expected_guess|
  96. let(:import_file) { file }
  97. let(:import_type) { type }
  98. before do
  99. allow(data).to receive(:original_filename).and_return(original_filename)
  100. end
  101. it 'guesses the expected type' do
  102. expect(subject.guessed_type).to eq expected_guess
  103. end
  104. end
  105. context 'when the headers are enough to disambiguate' do
  106. it_behaves_like 'with enough information', 'following', 'following_accounts.csv', 'import.csv', :following
  107. it_behaves_like 'with enough information', 'blocking', 'following_accounts.csv', 'import.csv', :following
  108. it_behaves_like 'with enough information', 'muting', 'following_accounts.csv', 'import.csv', :following
  109. it_behaves_like 'with enough information', 'following', 'muted_accounts.csv', 'imports.csv', :muting
  110. it_behaves_like 'with enough information', 'blocking', 'muted_accounts.csv', 'imports.csv', :muting
  111. it_behaves_like 'with enough information', 'muting', 'muted_accounts.csv', 'imports.csv', :muting
  112. end
  113. context 'when the file name is enough to disambiguate' do
  114. it_behaves_like 'with enough information', 'following', 'imports.txt', 'following_accounts.csv', :following
  115. it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'following_accounts.csv', :following
  116. it_behaves_like 'with enough information', 'muting', 'imports.txt', 'following_accounts.csv', :following
  117. it_behaves_like 'with enough information', 'following', 'imports.txt', 'follows.csv', :following
  118. it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'follows.csv', :following
  119. it_behaves_like 'with enough information', 'muting', 'imports.txt', 'follows.csv', :following
  120. it_behaves_like 'with enough information', 'following', 'imports.txt', 'blocked_accounts.csv', :blocking
  121. it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'blocked_accounts.csv', :blocking
  122. it_behaves_like 'with enough information', 'muting', 'imports.txt', 'blocked_accounts.csv', :blocking
  123. it_behaves_like 'with enough information', 'following', 'imports.txt', 'blocks.csv', :blocking
  124. it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'blocks.csv', :blocking
  125. it_behaves_like 'with enough information', 'muting', 'imports.txt', 'blocks.csv', :blocking
  126. it_behaves_like 'with enough information', 'following', 'imports.txt', 'muted_accounts.csv', :muting
  127. it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'muted_accounts.csv', :muting
  128. it_behaves_like 'with enough information', 'muting', 'imports.txt', 'muted_accounts.csv', :muting
  129. it_behaves_like 'with enough information', 'following', 'imports.txt', 'mutes.csv', :muting
  130. it_behaves_like 'with enough information', 'blocking', 'imports.txt', 'mutes.csv', :muting
  131. it_behaves_like 'with enough information', 'muting', 'imports.txt', 'mutes.csv', :muting
  132. end
  133. end
  134. describe '#likely_mismatched?' do
  135. shared_examples 'with matching types' do |type, file, original_filename = nil|
  136. let(:import_file) { file }
  137. let(:import_type) { type }
  138. before do
  139. allow(data).to receive(:original_filename).and_return(original_filename) if original_filename.present?
  140. end
  141. it 'returns false' do
  142. expect(subject.likely_mismatched?).to be false
  143. end
  144. end
  145. shared_examples 'with mismatching types' do |type, file, original_filename = nil|
  146. let(:import_file) { file }
  147. let(:import_type) { type }
  148. before do
  149. allow(data).to receive(:original_filename).and_return(original_filename) if original_filename.present?
  150. end
  151. it 'returns true' do
  152. expect(subject.likely_mismatched?).to be true
  153. end
  154. end
  155. it_behaves_like 'with matching types', 'following', 'following_accounts.csv'
  156. it_behaves_like 'with matching types', 'following', 'following_accounts.csv', 'imports.txt'
  157. it_behaves_like 'with matching types', 'following', 'imports.txt'
  158. it_behaves_like 'with matching types', 'blocking', 'imports.txt', 'blocks.csv'
  159. it_behaves_like 'with matching types', 'blocking', 'imports.txt'
  160. it_behaves_like 'with matching types', 'muting', 'muted_accounts.csv'
  161. it_behaves_like 'with matching types', 'muting', 'muted_accounts.csv', 'imports.txt'
  162. it_behaves_like 'with matching types', 'muting', 'imports.txt'
  163. it_behaves_like 'with matching types', 'domain_blocking', 'domain_blocks.csv'
  164. it_behaves_like 'with matching types', 'domain_blocking', 'domain_blocks.csv', 'imports.txt'
  165. it_behaves_like 'with matching types', 'bookmarks', 'bookmark-imports.txt'
  166. it_behaves_like 'with matching types', 'bookmarks', 'bookmark-imports.txt', 'imports.txt'
  167. it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'blocks.csv'
  168. it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'blocked_accounts.csv'
  169. it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'mutes.csv'
  170. it_behaves_like 'with mismatching types', 'following', 'imports.txt', 'muted_accounts.csv'
  171. it_behaves_like 'with mismatching types', 'following', 'muted_accounts.csv'
  172. it_behaves_like 'with mismatching types', 'following', 'muted_accounts.csv', 'imports.txt'
  173. it_behaves_like 'with mismatching types', 'blocking', 'following_accounts.csv'
  174. it_behaves_like 'with mismatching types', 'blocking', 'following_accounts.csv', 'imports.txt'
  175. it_behaves_like 'with mismatching types', 'blocking', 'muted_accounts.csv'
  176. it_behaves_like 'with mismatching types', 'blocking', 'muted_accounts.csv', 'imports.txt'
  177. it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'follows.csv'
  178. it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'following_accounts.csv'
  179. it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'mutes.csv'
  180. it_behaves_like 'with mismatching types', 'blocking', 'imports.txt', 'muted_accounts.csv'
  181. it_behaves_like 'with mismatching types', 'muting', 'following_accounts.csv'
  182. it_behaves_like 'with mismatching types', 'muting', 'following_accounts.csv', 'imports.txt'
  183. it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'follows.csv'
  184. it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'following_accounts.csv'
  185. it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'blocks.csv'
  186. it_behaves_like 'with mismatching types', 'muting', 'imports.txt', 'blocked_accounts.csv'
  187. end
  188. describe 'save' do
  189. shared_examples 'on successful import' do |type, mode, file, expected_rows|
  190. let(:import_type) { type }
  191. let(:import_file) { file }
  192. let(:import_mode) { mode }
  193. before { subject.save }
  194. context 'with a BulkImport' do
  195. let(:bulk_import) { account.bulk_imports.first }
  196. it 'creates a bulk import with correct values' do
  197. expect(bulk_import)
  198. .to be_present
  199. .and have_attributes(
  200. type: eq(subject.type),
  201. original_filename: eq(subject.data.original_filename),
  202. likely_mismatched?: eq(subject.likely_mismatched?),
  203. overwrite?: eq(!!subject.overwrite), # rubocop:disable Style/DoubleNegation
  204. processed_items: eq(0),
  205. imported_items: eq(0),
  206. total_items: eq(bulk_import.rows.count),
  207. state_unconfirmed?: be(true)
  208. )
  209. expect(bulk_import.rows.pluck(:data))
  210. .to match_array(expected_rows)
  211. end
  212. end
  213. end
  214. it_behaves_like 'on successful import', 'following', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
  215. it_behaves_like 'on successful import', 'following', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
  216. it_behaves_like 'on successful import', 'blocking', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
  217. it_behaves_like 'on successful import', 'blocking', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
  218. it_behaves_like 'on successful import', 'muting', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
  219. it_behaves_like 'on successful import', 'domain_blocking', 'merge', 'domain_blocks.csv', (%w(bad.domain worse.domain reject.media).map { |domain| { 'domain' => domain } })
  220. it_behaves_like 'on successful import', 'bookmarks', 'merge', 'bookmark-imports.txt', (%w(https://example.com/statuses/1312 https://local.com/users/foo/statuses/42 https://unknown-remote.com/users/bar/statuses/1 https://example.com/statuses/direct).map { |uri| { 'uri' => uri } })
  221. it_behaves_like 'on successful import', 'following', 'merge', 'following_accounts.csv', [
  222. { 'acct' => 'user@example.com', 'show_reblogs' => true, 'notify' => false, 'languages' => nil },
  223. { 'acct' => 'user@test.com', 'show_reblogs' => true, 'notify' => true, 'languages' => %w(en fr) },
  224. ]
  225. it_behaves_like 'on successful import', 'muting', 'merge', 'muted_accounts.csv', [
  226. { 'acct' => 'user@example.com', 'hide_notifications' => true },
  227. { 'acct' => 'user@test.com', 'hide_notifications' => false },
  228. ]
  229. it_behaves_like 'on successful import', 'lists', 'merge', 'lists.csv', [
  230. { 'acct' => 'gargron@example.com', 'list_name' => 'Mastodon project' },
  231. { 'acct' => 'mastodon@example.com', 'list_name' => 'Mastodon project' },
  232. { 'acct' => 'foo@example.com', 'list_name' => 'test' },
  233. ]
  234. # Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users
  235. #
  236. # https://github.com/mastodon/mastodon/issues/20571
  237. it_behaves_like 'on successful import', 'following', 'merge', 'utf8-followers.txt', [{ 'acct' => 'nare@թութ.հայ' }]
  238. end
  239. end