You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

issue_import_test.rb 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. # frozen_string_literal: true
  2. # Redmine - project management software
  3. # Copyright (C) 2006- Jean-Philippe Lang
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License
  7. # as published by the Free Software Foundation; either version 2
  8. # of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. require_relative '../test_helper'
  19. class IssueImportTest < ActiveSupport::TestCase
  20. fixtures :projects, :enabled_modules,
  21. :users, :email_addresses,
  22. :roles, :members, :member_roles,
  23. :issues, :issue_statuses,
  24. :trackers, :projects_trackers,
  25. :versions,
  26. :issue_categories,
  27. :enumerations,
  28. :workflows,
  29. :custom_fields,
  30. :custom_values,
  31. :custom_fields_projects,
  32. :custom_fields_trackers
  33. include Redmine::I18n
  34. def setup
  35. User.current = nil
  36. set_language_if_valid 'en'
  37. end
  38. def test_authorized
  39. assert IssueImport.authorized?(User.find(1)) # admins
  40. assert IssueImport.authorized?(User.find(2)) # has import_issues permission
  41. assert !IssueImport.authorized?(User.find(3)) # does not have permission
  42. end
  43. def test_create_versions_should_create_missing_versions
  44. import = generate_import_with_mapping
  45. import.mapping.merge!('fixed_version' => '9', 'create_versions' => '1')
  46. import.save!
  47. version = new_record(Version) do
  48. assert_difference 'Issue.count', 3 do
  49. import.run
  50. end
  51. end
  52. assert_equal '2.1', version.name
  53. end
  54. def test_create_categories_should_create_missing_categories
  55. import = generate_import_with_mapping
  56. import.mapping.merge!('category' => '10', 'create_categories' => '1')
  57. import.save!
  58. category = new_record(IssueCategory) do
  59. assert_difference 'Issue.count', 3 do
  60. import.run
  61. end
  62. end
  63. assert_equal 'New category', category.name
  64. end
  65. def test_mapping_with_fixed_tracker
  66. import = generate_import_with_mapping
  67. import.mapping['tracker'] = 'value:2'
  68. import.save!
  69. issues = new_records(Issue, 3) {import.run}
  70. assert_equal [2], issues.map(&:tracker_id).uniq
  71. end
  72. def test_mapping_with_mapped_tracker
  73. import = generate_import_with_mapping
  74. import.mapping['tracker'] = '13'
  75. import.save!
  76. issues = new_records(Issue, 3) {import.run}
  77. assert_equal [1, 2, 1], issues.map(&:tracker_id)
  78. end
  79. def test_should_not_import_with_default_tracker_when_tracker_is_invalid
  80. Tracker.find_by_name('Feature request').update!(:name => 'Feature')
  81. import = generate_import_with_mapping
  82. import.mapping['tracker'] = '13'
  83. import.save!
  84. import.run
  85. assert_equal 1, import.unsaved_items.count
  86. item = import.unsaved_items.first
  87. assert_include "Tracker cannot be blank", item.message
  88. end
  89. def test_status_should_be_set
  90. import = generate_import_with_mapping
  91. import.mapping['status'] = '14'
  92. import.save!
  93. issues = new_records(Issue, 3) {import.run}
  94. assert_equal ['New', 'New', 'Assigned'], issues.map {|x| x.status.name}
  95. end
  96. def test_parent_should_be_set
  97. import = generate_import_with_mapping
  98. import.mapping['parent_issue_id'] = '5'
  99. import.save!
  100. issues = new_records(Issue, 3) {import.run}
  101. assert_nil issues[0].parent
  102. assert_equal issues[0].id, issues[1].parent_id
  103. assert_equal 2, issues[2].parent_id
  104. end
  105. def test_import_utf8_with_bom
  106. import = generate_import_with_mapping('import_issues_utf8_with_bom.csv')
  107. import.settings['encoding'] = 'UTF-8'
  108. import.save
  109. issues = new_records(Issue, 3) {import.run}
  110. assert_equal 3, issues.count
  111. end
  112. def test_backward_and_forward_reference_to_parent_should_work
  113. import = generate_import('import_subtasks.csv')
  114. import.settings = {
  115. 'separator' => ";", 'wrapper' => '"', 'encoding' => "UTF-8",
  116. 'mapping' => {'project_id' => '1', 'tracker' => '1', 'subject' => '2', 'parent_issue_id' => '3'}
  117. }
  118. import.save!
  119. root, child1, grandchild, child2 = new_records(Issue, 4) {import.run}
  120. assert_equal root, child1.parent
  121. assert_equal child2, grandchild.parent
  122. end
  123. def test_references_with_unique_id
  124. import = generate_import_with_mapping('import_subtasks_with_unique_id.csv')
  125. import.settings['mapping'] = {'project_id' => '1', 'unique_id' => '0', 'tracker' => '1', 'subject' => '2', 'parent_issue_id' => '3', 'relation_follows' => '4'}
  126. import.save!
  127. red4, red3, red2, red1, blue1, blue2, blue3, blue4, green = new_records(Issue, 9) {import.run}
  128. # future references
  129. assert_equal red1, red2.parent
  130. assert_equal red3, red4.parent
  131. assert IssueRelation.where('issue_from_id' => red2.id, 'issue_to_id' => red3.id, 'delay' => 1, 'relation_type' => 'precedes').present?
  132. # past references
  133. assert_equal blue1, blue2.parent
  134. assert_equal blue3, blue4.parent
  135. assert IssueRelation.where('issue_from_id' => blue2.id, 'issue_to_id' => blue3.id, 'delay' => 1, 'relation_type' => 'precedes').present?
  136. assert_equal issues(:issues_001), green.parent
  137. assert IssueRelation.where('issue_from_id' => issues(:issues_002).id, 'issue_to_id' => green.id, 'delay' => 3, 'relation_type' => 'precedes').present?
  138. end
  139. def test_follow_relation
  140. import = generate_import_with_mapping('import_subtasks.csv')
  141. import.settings['mapping'] = {'project_id' => '1', 'tracker' => '1', 'subject' => '2', 'relation_relates' => '4'}
  142. import.save!
  143. one, one_one, one_two_one, one_two = new_records(Issue, 4) {import.run}
  144. assert_equal 2, one.relations.count
  145. assert one.relations.all? {|r| r.relation_type == 'relates'}
  146. assert one.relations.any? {|r| r.other_issue(one) == one_one}
  147. assert one.relations.any? {|r| r.other_issue(one) == one_two}
  148. assert_equal 2, one_one.relations.count
  149. assert one_one.relations.all? {|r| r.relation_type == 'relates'}
  150. assert one_one.relations.any? {|r| r.other_issue(one_one) == one}
  151. assert one_one.relations.any? {|r| r.other_issue(one_one) == one_two}
  152. assert_equal 3, one_two.relations.count
  153. assert one_two.relations.all? {|r| r.relation_type == 'relates'}
  154. assert one_two.relations.any? {|r| r.other_issue(one_two) == one}
  155. assert one_two.relations.any? {|r| r.other_issue(one_two) == one_one}
  156. assert one_two.relations.any? {|r| r.other_issue(one_two) == one_two_one}
  157. assert_equal 1, one_two_one.relations.count
  158. assert one_two_one.relations.all? {|r| r.relation_type == 'relates'}
  159. assert one_two_one.relations.any? {|r| r.other_issue(one_two_one) == one_two}
  160. end
  161. def test_delayed_relation
  162. import = generate_import_with_mapping('import_subtasks.csv')
  163. import.settings['mapping'] = {'project_id' => '1', 'tracker' => '1', 'subject' => '2', 'relation_precedes' => '5'}
  164. import.save!
  165. one, one_one, one_two_one, one_two = new_records(Issue, 4) {import.run}
  166. assert_equal 2, one.relations_to.count
  167. assert one.relations_to.all? {|r| r.relation_type == 'precedes'}
  168. assert one.relations_to.any? {|r| r.issue_from == one_one && r.delay == 2}
  169. assert one.relations_to.any? {|r| r.issue_from == one_two && r.delay == 1}
  170. assert_equal 1, one_one.relations_from.count
  171. assert one_one.relations_from.all? {|r| r.relation_type == 'precedes'}
  172. assert one_one.relations_from.any? {|r| r.issue_to == one && r.delay == 2}
  173. assert_equal 1, one_two.relations_to.count
  174. assert one_two.relations_to.all? {|r| r.relation_type == 'precedes'}
  175. assert one_two.relations_to.any? {|r| r.issue_from == one_two_one && r.delay == -1}
  176. assert_equal 1, one_two.relations_from.count
  177. assert one_two.relations_from.all? {|r| r.relation_type == 'precedes'}
  178. assert one_two.relations_from.any? {|r| r.issue_to == one && r.delay == 1}
  179. assert_equal 1, one_two_one.relations_from.count
  180. assert one_two_one.relations_from.all? {|r| r.relation_type == 'precedes'}
  181. assert one_two_one.relations_from.any? {|r| r.issue_to == one_two && r.delay == -1}
  182. end
  183. def test_parent_and_follows_relation
  184. import = generate_import_with_mapping('import_subtasks_with_relations.csv')
  185. import.settings['mapping'] = {
  186. 'project_id' => '1',
  187. 'tracker' => '1',
  188. 'subject' => '2',
  189. 'start_date' => '3',
  190. 'due_date' => '4',
  191. 'parent_issue_id' => '5',
  192. 'relation_follows' => '6'
  193. }
  194. import.save!
  195. second, first, parent, third = assert_difference('IssueRelation.count', 2) {new_records(Issue, 4) {import.run}}
  196. # Parent relations
  197. assert_equal parent, first.parent
  198. assert_equal parent, second.parent
  199. assert_equal parent, third.parent
  200. # Issue relations
  201. assert IssueRelation.where(
  202. :issue_from_id => first.id,
  203. :issue_to_id => second.id,
  204. :relation_type => 'precedes',
  205. :delay => 1).present?
  206. assert IssueRelation.where(
  207. :issue_from_id => second.id,
  208. :issue_to_id => third.id,
  209. :relation_type => 'precedes',
  210. :delay => 1).present?
  211. # Checking dates, because they might act weird, when relations are added
  212. assert_equal Date.new(2020, 1, 1), parent.start_date
  213. assert_equal Date.new(2020, 2, 3), parent.due_date
  214. assert_equal Date.new(2020, 1, 1), first.start_date
  215. assert_equal Date.new(2020, 1, 10), first.due_date
  216. assert_equal Date.new(2020, 1, 14), second.start_date
  217. assert_equal Date.new(2020, 1, 21), second.due_date
  218. assert_equal Date.new(2020, 1, 23), third.start_date
  219. assert_equal Date.new(2020, 2, 3), third.due_date
  220. end
  221. def test_import_with_relations_and_invalid_issue_should_not_fail
  222. import = generate_import_with_mapping('import_issues_with_relation_and_invalid_issues.csv')
  223. import.settings['mapping'] = {
  224. 'project_id' => '1',
  225. 'tracker' => '1',
  226. 'subject' => '2',
  227. 'status' => '3',
  228. 'relation_relates' => '4',
  229. }
  230. import.save!
  231. first, second, third, fourth = new_records(Issue, 4) {import.run}
  232. assert_equal 1, import.unsaved_items.count
  233. item = import.unsaved_items.first
  234. assert_include "Subject cannot be blank", item.message
  235. assert_equal 1, first.relations_from.count
  236. assert_equal 1, second.relations_to.count
  237. end
  238. def test_assignee_should_be_set
  239. import = generate_import_with_mapping
  240. import.mapping['assigned_to'] = '11'
  241. import.save!
  242. issues = new_records(Issue, 3) {import.run}
  243. assert_equal [User.find(3), nil, nil], issues.map(&:assigned_to)
  244. end
  245. def test_user_custom_field_should_be_set
  246. field = IssueCustomField.generate!(:field_format => 'user', :is_for_all => true, :trackers => Tracker.all)
  247. import = generate_import_with_mapping
  248. import.mapping["cf_#{field.id}"] = '11'
  249. import.save!
  250. issues = new_records(Issue, 3) {import.run}
  251. assert_equal '3', issues.first.custom_field_value(field)
  252. end
  253. def test_list_custom_field_should_be_set
  254. field = CustomField.find(1)
  255. field.tracker_ids = Tracker.ids
  256. field.save!
  257. import = generate_import_with_mapping
  258. import.mapping["cf_1"] = '8'
  259. import.save!
  260. issues = new_records(Issue, 3) {import.run}
  261. assert_equal 'PostgreSQL', issues[0].custom_field_value(1)
  262. assert_equal 'MySQL', issues[1].custom_field_value(1)
  263. assert_equal '', issues.third.custom_field_value(1)
  264. end
  265. def test_multiple_list_custom_field_should_be_set
  266. field = CustomField.find(1)
  267. field.tracker_ids = Tracker.ids
  268. field.multiple = true
  269. field.save!
  270. import = generate_import_with_mapping
  271. import.mapping["cf_1"] = '15'
  272. import.save!
  273. issues = new_records(Issue, 3) {import.run}
  274. assert_equal ['Oracle', 'PostgreSQL'], issues[0].custom_field_value(1).sort
  275. assert_equal ['MySQL'], issues[1].custom_field_value(1)
  276. assert_equal [''], issues.third.custom_field_value(1)
  277. end
  278. def test_is_private_should_be_set_based_on_user_locale
  279. import = generate_import_with_mapping
  280. import.mapping['is_private'] = '6'
  281. import.save!
  282. issues = new_records(Issue, 3) {import.run}
  283. assert_equal [false, true, false], issues.map(&:is_private)
  284. end
  285. def test_dates_should_be_parsed_using_date_format_setting
  286. field = IssueCustomField.generate!(:field_format => 'date', :is_for_all => true, :trackers => Tracker.all)
  287. import = generate_import_with_mapping('import_dates.csv')
  288. import.settings['date_format'] = Import::DATE_FORMATS[1]
  289. import.mapping.merge!('tracker' => 'value:1', 'subject' => '0', 'start_date' => '1', 'due_date' => '2', "cf_#{field.id}" => '3')
  290. import.save!
  291. issue = new_record(Issue) {import.run} # only 1 valid issue
  292. assert_equal "Valid dates", issue.subject
  293. assert_equal Date.parse('2015-07-10'), issue.start_date
  294. assert_equal Date.parse('2015-08-12'), issue.due_date
  295. assert_equal '2015-07-14', issue.custom_field_value(field)
  296. # Tests using other date formats
  297. import = generate_import_with_mapping('import_dates_ja.csv')
  298. import.settings['date_format'] = Import::DATE_FORMATS[3]
  299. import.mapping.merge!('tracker' => 'value:1', 'subject' => '0', 'start_date' => '1')
  300. import.save!
  301. issue = new_record(Issue) {import.run}
  302. assert_equal Date.parse('2019-05-28'), issue.start_date
  303. end
  304. def test_date_format_should_default_to_user_language
  305. user = User.generate!(:language => 'fr')
  306. import = Import.new
  307. import.user = user
  308. assert_nil import.settings['date_format']
  309. import.set_default_settings
  310. assert_equal '%d/%m/%Y', import.settings['date_format']
  311. end
  312. def test_run_should_remove_the_file
  313. import = generate_import_with_mapping
  314. file_path = import.filepath
  315. assert File.exist?(file_path)
  316. import.run
  317. assert !File.exist?(file_path)
  318. end
  319. def test_run_should_consider_project_shared_versions
  320. system_version = Version.generate!(:project_id => 2, :sharing => 'system', :name => '2.1')
  321. system_version.save!
  322. import = generate_import_with_mapping
  323. import.mapping['fixed_version'] = '9'
  324. import.save!
  325. issues = new_records(Issue, 3) {import.run}
  326. assert [nil, 3, system_version.id], issues.map(&:fixed_version_id)
  327. end
  328. def test_set_default_settings_with_project_id
  329. import = Import.new
  330. import.set_default_settings(:project_id => 3)
  331. assert_equal 3, import.mapping['project_id']
  332. end
  333. def test_set_default_settings_with_project_identifier
  334. import = Import.new
  335. import.set_default_settings(:project_id => 'ecookbook')
  336. assert_equal 1, import.mapping['project_id']
  337. end
  338. def test_set_default_settings_without_project_id
  339. import = Import.new
  340. import.set_default_settings
  341. assert_empty import.mapping
  342. end
  343. def test_set_default_settings_with_invalid_project_should_not_fail
  344. import = Import.new
  345. import.set_default_settings(:project_id => 'abc')
  346. assert_empty import.mapping
  347. end
  348. def test_set_default_settings_should_guess_encoding
  349. import = generate_import('import_iso8859-1.csv')
  350. user = User.generate!(:language => 'ja')
  351. import.user = user
  352. assert_equal 'CP932', lu(user, :general_csv_encoding)
  353. with_settings :repositories_encodings => 'UTF-8,ISO-8859-1' do
  354. import.set_default_settings
  355. guessed_encoding = import.settings['encoding']
  356. assert_equal 'ISO-8859-1', guessed_encoding
  357. end
  358. with_settings :repositories_encodings => 'UTF-8,iso8859-1' do
  359. import.set_default_settings
  360. guessed_encoding = import.settings['encoding']
  361. assert_equal 'ISO-8859-1', guessed_encoding
  362. assert_includes Setting::ENCODINGS, guessed_encoding
  363. end
  364. end
  365. def test_set_default_settings_should_use_general_csv_encoding_when_cannnot_guess_encoding
  366. import = generate_import('import_iso8859-1.csv')
  367. user = User.generate!(:language => 'ja')
  368. import.user = user
  369. with_settings :repositories_encodings => 'UTF-8' do
  370. import.set_default_settings
  371. guessed_encoding = import.settings['encoding']
  372. assert_equal 'CP932', lu(user, :general_csv_encoding)
  373. assert_equal 'CP932', guessed_encoding
  374. end
  375. end
  376. def test_set_default_settings_should_detect_field_wrapper
  377. to_test = {
  378. 'import_issues.csv' => '"',
  379. 'import_issues_single_quotation.csv' => "'",
  380. # Use '"' as a wrapper for CSV file with no wrappers
  381. 'import_dates.csv' => '"',
  382. }
  383. to_test.each do |file, expected|
  384. import = generate_import(file)
  385. import.set_default_settings
  386. assert_equal expected, import.settings['wrapper']
  387. end
  388. end
  389. end