issues_controller_test.rb 205 KB
Newer Older
1
# Redmine - project management software
jplang's avatar
jplang committed
2
# Copyright (C) 2006-2017  Jean-Philippe Lang
3 4 5 6 7
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
8
#
9 10 11 12
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
13
#
14 15 16 17
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

18
require File.expand_path('../../test_helper', __FILE__)
19

20
class IssuesControllerTest < Redmine::ControllerTest
21
  fixtures :projects,
22
           :users, :email_addresses, :user_preferences,
23 24
           :roles,
           :members,
25
           :member_roles,
26 27
           :issues,
           :issue_statuses,
28
           :issue_relations,
29
           :versions,
30
           :trackers,
31
           :projects_trackers,
32 33 34
           :issue_categories,
           :enabled_modules,
           :enumerations,
35
           :attachments,
36 37 38
           :workflows,
           :custom_fields,
           :custom_values,
jplang's avatar
jplang committed
39
           :custom_fields_projects,
40
           :custom_fields_trackers,
41 42
           :time_entries,
           :journals,
edavis10's avatar
edavis10 committed
43
           :journal_details,
44 45 46
           :queries,
           :repositories,
           :changesets
47

48 49
  include Redmine::I18n

50 51 52
  def setup
    User.current = nil
  end
53

54
  def test_index
55 56 57
    with_settings :default_language => "en" do
      get :index
      assert_response :success
58 59

      # links to visible issues
60
      assert_select 'a[href="/issues/1"]', :text => /Cannot print recipes/
61
      assert_select 'a[href="/issues/5"]', :text => /Subproject issue/
62
      # private projects hidden
63 64
      assert_select 'a[href="/issues/6"]', 0
      assert_select 'a[href="/issues/4"]', 0
65
      # project column
66
      assert_select 'th', :text => /Project/
67
    end
68
  end
69

70
  def test_index_should_not_list_issues_when_module_disabled
71
    EnabledModule.where("name = 'issue_tracking' AND project_id = 1").delete_all
72 73
    get :index
    assert_response :success
74

75 76
    assert_select 'a[href="/issues/1"]', 0
    assert_select 'a[href="/issues/5"]', :text => /Subproject issue/
77
  end
jplang's avatar
jplang committed
78 79

  def test_index_should_list_visible_issues_only
80 81 82
    get :index, :params => {
        :per_page => 100
      }
jplang's avatar
jplang committed
83
    assert_response :success
84 85 86 87

    Issue.open.each do |issue|
      assert_select "tr#issue-#{issue.id}", issue.visible? ? 1 : 0
    end
jplang's avatar
jplang committed
88
  end
89

90
  def test_index_with_project
91
    Setting.display_subprojects_issues = 0
92 93 94
    get :index, :params => {
        :project_id => 1
      }
95
    assert_response :success
96

97 98 99 100 101 102 103 104 105 106 107
    # query form
    assert_select 'form#query_form' do
      assert_select 'div#query_form_with_buttons.hide-when-print' do
        assert_select 'div#query_form_content' do
          assert_select 'fieldset#filters.collapsible'
          assert_select 'fieldset#options'
        end
        assert_select 'p.buttons'
      end
    end

108
    assert_select 'a[href="/issues/1"]', :text => /Cannot print recipes/
109
    assert_select 'a[href="/issues/5"]', 0
110
  end
111

112 113
  def test_index_with_project_and_subprojects
    Setting.display_subprojects_issues = 1
114 115 116
    get :index, :params => {
        :project_id => 1
      }
117
    assert_response :success
118

119
    assert_select 'a[href="/issues/1"]', :text => /Cannot print recipes/
120 121
    assert_select 'a[href="/issues/5"]', :text => /Subproject issue/
    assert_select 'a[href="/issues/6"]', 0
122
  end
123

124
  def test_index_with_project_and_subprojects_should_show_private_subprojects_with_permission
125 126
    @request.session[:user_id] = 2
    Setting.display_subprojects_issues = 1
127 128 129
    get :index, :params => {
        :project_id => 1
      }
130
    assert_response :success
131

132
    assert_select 'a[href="/issues/1"]', :text => /Cannot print recipes/
133 134
    assert_select 'a[href="/issues/5"]', :text => /Subproject issue/
    assert_select 'a[href="/issues/6"]', :text => /Issue of a private subproject/
135
  end
136

137
  def test_index_with_project_and_default_filter
138 139 140 141
    get :index, :params => {
        :project_id => 1,
        :set_filter => 1
      }
142
    assert_response :success
143

144
    # default filter
145
    assert_query_filters [['status_id', 'o', '']]
146
  end
147

148
  def test_index_with_project_and_filter
149 150 151 152 153 154 155 156 157 158 159
    get :index, :params => {
        :project_id => 1,
        :set_filter => 1,
        :f => ['tracker_id'],
        :op => {
          'tracker_id' => '='
        },  
        :v => {
          'tracker_id' => ['1']
        }
      }
160
    assert_response :success
161

162
    assert_query_filters [['tracker_id', '=', '1']]
163
  end
164

165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
  def test_index_with_short_filters
    to_test = {
      'status_id' => {
        'o' => { :op => 'o', :values => [''] },
        'c' => { :op => 'c', :values => [''] },
        '7' => { :op => '=', :values => ['7'] },
        '7|3|4' => { :op => '=', :values => ['7', '3', '4'] },
        '=7' => { :op => '=', :values => ['7'] },
        '!3' => { :op => '!', :values => ['3'] },
        '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }},
      'subject' => {
        'This is a subject' => { :op => '=', :values => ['This is a subject'] },
        'o' => { :op => '=', :values => ['o'] },
        '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] },
        '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }},
180 181
      'tracker_id' => {
        '3' => { :op => '=', :values => ['3'] },
emassip's avatar
emassip committed
182
        '=3' => { :op => '=', :values => ['3'] }},
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
      'start_date' => {
        '2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
        '=2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
        '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
        '<=2011-10-12' => { :op => '<=', :values => ['2011-10-12'] },
        '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] },
        '<t+2' => { :op => '<t+', :values => ['2'] },
        '>t+2' => { :op => '>t+', :values => ['2'] },
        't+2' => { :op => 't+', :values => ['2'] },
        't' => { :op => 't', :values => [''] },
        'w' => { :op => 'w', :values => [''] },
        '>t-2' => { :op => '>t-', :values => ['2'] },
        '<t-2' => { :op => '<t-', :values => ['2'] },
        't-2' => { :op => 't-', :values => ['2'] }},
      'created_on' => {
        '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
199 200 201
        '<t-2' => { :op => '<t-', :values => ['2'] },
        '>t-2' => { :op => '>t-', :values => ['2'] },
        't-2' => { :op => 't-', :values => ['2'] }},
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
      'cf_1' => {
        'c' => { :op => '=', :values => ['c'] },
        '!c' => { :op => '!', :values => ['c'] },
        '!*' => { :op => '!*', :values => [''] },
        '*' => { :op => '*', :values => [''] }},
      'estimated_hours' => {
        '=13.4' => { :op => '=', :values => ['13.4'] },
        '>=45' => { :op => '>=', :values => ['45'] },
        '<=125' => { :op => '<=', :values => ['125'] },
        '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] },
        '!*' => { :op => '!*', :values => [''] },
        '*' => { :op => '*', :values => [''] }}
    }

    default_filter = { 'status_id' => {:operator => 'o', :values => [''] }}

    to_test.each do |field, expression_and_expected|
      expression_and_expected.each do |filter_expression, expected|
emassip's avatar
emassip committed
220

221 222 223
        get :index, :params => {
            :set_filter => 1, field => filter_expression
          }
224 225
        assert_response :success

226 227
        expected_with_default = default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}})
        assert_query_filters expected_with_default.map {|f, v| [f, v[:operator], v[:values]]}
228 229 230 231
      end
    end
  end

232
  def test_index_with_project_and_empty_filters
233 234 235 236 237
    get :index, :params => {
        :project_id => 1,
        :set_filter => 1,
        :fields => ['']
      }
238
    assert_response :success
239

240
    # no filter
241
    assert_query_filters []
242
  end
243

244 245 246 247 248 249 250
  def test_index_with_project_custom_field_filter
    field = ProjectCustomField.create!(:name => 'Client', :is_filter => true, :field_format => 'string')
    CustomValue.create!(:custom_field => field, :customized => Project.find(3), :value => 'Foo')
    CustomValue.create!(:custom_field => field, :customized => Project.find(5), :value => 'Foo')
    filter_name = "project.cf_#{field.id}"
    @request.session[:user_id] = 1

251 252 253 254 255 256 257 258 259 260 261
    get :index, :params => {
        :set_filter => 1,
        :f => [filter_name],
        :op => {
          filter_name => '='
        },  
        :v => {
          filter_name => ['Foo']
        },  
        :c => ['project']
      }
262
    assert_response :success
263 264

    assert_equal [3, 5], issues_in_list.map(&:project_id).uniq.sort
265 266
  end

jplang's avatar
jplang committed
267
  def test_index_with_query
268 269 270 271
    get :index, :params => {
        :project_id => 1,
        :query_id => 5
      }
jplang's avatar
jplang committed
272 273
    assert_response :success
  end
274

275
  def test_index_with_query_grouped_by_tracker
276 277 278 279
    get :index, :params => {
        :project_id => 1,
        :query_id => 6
      }
jplang's avatar
jplang committed
280
    assert_response :success
281
    assert_select 'tr.group span.count'
282
  end
283

284
  def test_index_with_query_grouped_and_sorted_by_category
285 286 287 288 289 290
    get :index, :params => {
        :project_id => 1,
        :set_filter => 1,
        :group_by => "category",
        :sort => "category"
      }
291
    assert_response :success
292
    assert_select 'tr.group span.count'
293 294
  end

295
  def test_index_with_query_grouped_and_sorted_by_fixed_version
296 297 298 299 300 301
    get :index, :params => {
        :project_id => 1,
        :set_filter => 1,
        :group_by => "fixed_version",
        :sort => "fixed_version"
      }
302
    assert_response :success
303
    assert_select 'tr.group span.count'
304 305 306
  end

  def test_index_with_query_grouped_and_sorted_by_fixed_version_in_reverse_order
307 308 309 310 311 312
    get :index, :params => {
        :project_id => 1,
        :set_filter => 1,
        :group_by => "fixed_version",
        :sort => "fixed_version:desc"
      }
313
    assert_response :success
314
    assert_select 'tr.group span.count'
315 316
  end

317
  def test_index_with_query_grouped_by_list_custom_field
318 319 320 321
    get :index, :params => {
        :project_id => 1,
        :query_id => 9
      }
322
    assert_response :success
323
    assert_select 'tr.group span.count'
jplang's avatar
jplang committed
324
  end
325

326 327 328 329 330 331 332 333 334
  def test_index_with_query_grouped_by_key_value_custom_field
    cf = IssueCustomField.create!(:name => 'Key', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'enumeration')
    cf.enumerations << valueb = CustomFieldEnumeration.new(:name => 'Value B', :position => 1)
    cf.enumerations << valuea = CustomFieldEnumeration.new(:name => 'Value A', :position => 2)
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => valueb.id)
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => valueb.id)
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => valuea.id)
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')

335 336 337 338 339
    get :index, :params => {
        :project_id => 1,
        :set_filter => 1,
      :group_by => "cf_#{cf.id}"
      }
340 341 342 343 344 345 346 347 348 349 350 351 352
    assert_response :success

    assert_select 'tr.group', 3
    assert_select 'tr.group' do
      assert_select 'span.name', :text => 'Value B'
      assert_select 'span.count', :text => '2'
    end
    assert_select 'tr.group' do
      assert_select 'span.name', :text => 'Value A'
      assert_select 'span.count', :text => '1'
    end
  end

353 354 355 356 357 358 359
  def test_index_with_query_grouped_by_user_custom_field
    cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3')
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3')
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')

360 361 362 363 364
    get :index, :params => {
        :project_id => 1,
        :set_filter => 1,
      :group_by => "cf_#{cf.id}"
      }
365 366 367 368 369
    assert_response :success

    assert_select 'tr.group', 3
    assert_select 'tr.group' do
      assert_select 'a', :text => 'John Smith'
jplang's avatar
jplang committed
370
      assert_select 'span.count', :text => '1'
371 372 373
    end
    assert_select 'tr.group' do
      assert_select 'a', :text => 'Dave Lopper'
jplang's avatar
jplang committed
374
      assert_select 'span.count', :text => '2'
375 376 377
    end
  end

378 379 380 381 382 383 384
  def test_index_grouped_by_boolean_custom_field_should_distinguish_blank_and_false_values
    cf = IssueCustomField.create!(:name => 'Bool', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'bool')
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '1')
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '0')
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '')

    with_settings :default_language => 'en' do
385 386 387 388 389
      get :index, :params => {
          :project_id => 1,
          :set_filter => 1,
        :group_by => "cf_#{cf.id}"
        }
390 391 392
      assert_response :success
    end

393
    assert_select 'tr.group', 3
394 395
    assert_select 'tr.group', :text => /Yes/
    assert_select 'tr.group', :text => /No/
jplang's avatar
jplang committed
396
    assert_select 'tr.group', :text => /blank/
397 398
  end

399 400 401 402 403 404
  def test_index_grouped_by_boolean_custom_field_with_false_group_in_first_position_should_show_the_group
    cf = IssueCustomField.create!(:name => 'Bool', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'bool', :is_filter => true)
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '0')
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '0')

    with_settings :default_language => 'en' do
405 406 407 408 409
      get :index, :params => {
          :project_id => 1,
        :set_filter => 1, "cf_#{cf.id}" => "*",
        :group_by => "cf_#{cf.id}"
        }
410 411 412
      assert_response :success
    end

413
    assert_equal [1, 2], issues_in_list.map(&:id).sort
414 415 416 417
    assert_select 'tr.group', 1
    assert_select 'tr.group', :text => /No/
  end

418
  def test_index_with_query_grouped_by_tracker_in_normal_order
419 420
    3.times {|i| Issue.generate!(:tracker_id => (i + 1))}

421 422 423 424 425
    get :index, :params => {
        :set_filter => 1,
        :group_by => 'tracker',
        :sort => 'id:desc'
      }
426 427
    assert_response :success

428 429
    assert_equal ["Bug", "Feature request", "Support request"],
      css_select("tr.issue td.tracker").map(&:text).uniq
430 431 432 433 434
  end

  def test_index_with_query_grouped_by_tracker_in_reverse_order
    3.times {|i| Issue.generate!(:tracker_id => (i + 1))}

435 436 437 438 439 440
    get :index, :params => {
        :set_filter => 1,
        :group_by => 'tracker',
        :c => ['tracker', 'subject'],
        :sort => 'id:desc,tracker:desc'
      }
441 442
    assert_response :success

443 444
    assert_equal ["Bug", "Feature request", "Support request"].reverse,
      css_select("tr.issue td.tracker").map(&:text).uniq
445 446
  end

447
  def test_index_with_query_id_and_project_id_should_set_session_query
448 449 450 451
    get :index, :params => {
        :project_id => 1,
        :query_id => 4
      }
452
    assert_response :success
453 454 455
    assert_kind_of Hash, session[:issue_query]
    assert_equal 4, session[:issue_query][:id]
    assert_equal 1, session[:issue_query][:project_id]
456 457
  end

458
  def test_index_with_invalid_query_id_should_respond_404
459 460 461 462
    get :index, :params => {
        :project_id => 1,
        :query_id => 999
      }
463 464 465
    assert_response 404
  end

466
  def test_index_with_cross_project_query_in_session_should_show_project_issues
467
    q = IssueQuery.create!(:name => "cross_project_query", :user_id => 2, :project => nil, :column_names => ['project'])
468
    @request.session[:issue_query] = {:id => q.id, :project_id => 1}
469 470

    with_settings :display_subprojects_issues => '0' do
471 472 473
      get :index, :params => {
          :project_id => 1
        }
474 475
    end
    assert_response :success
476 477 478

    assert_select 'h2', :text => q.name
    assert_equal ["eCookbook"], css_select("tr.issue td.project").map(&:text).uniq
479 480
  end

481
  def test_private_query_should_not_be_available_to_other_users
jplang's avatar
jplang committed
482
    q = IssueQuery.create!(:name => "private", :user => User.find(2), :visibility => IssueQuery::VISIBILITY_PRIVATE, :project => nil)
483
    @request.session[:user_id] = 3
484

485 486 487
    get :index, :params => {
        :query_id => q.id
      }
488 489
    assert_response 403
  end
490

491
  def test_private_query_should_be_available_to_its_user
jplang's avatar
jplang committed
492
    q = IssueQuery.create!(:name => "private", :user => User.find(2), :visibility => IssueQuery::VISIBILITY_PRIVATE, :project => nil)
493
    @request.session[:user_id] = 2
494

495 496 497
    get :index, :params => {
        :query_id => q.id
      }
498 499
    assert_response :success
  end
500

501
  def test_public_query_should_be_available_to_other_users
502
    q = IssueQuery.create!(:name => "public", :user => User.find(2), :visibility => IssueQuery::VISIBILITY_PUBLIC, :project => nil)
503
    @request.session[:user_id] = 3
504

505 506 507
    get :index, :params => {
        :query_id => q.id
      }
508 509
    assert_response :success
  end
510

511
  def test_index_should_omit_page_param_in_export_links
512 513 514
    get :index, :params => {
        :page => 2
      }
515
    assert_response :success
516 517 518
    assert_select 'a.atom[href="/issues.atom"]'
    assert_select 'a.csv[href="/issues.csv"]'
    assert_select 'a.pdf[href="/issues.pdf"]'
jplang's avatar
jplang committed
519
    assert_select 'form#csv-export-form[action="/issues.csv"]'
520 521
  end

522 523 524 525 526 527 528 529 530 531 532 533 534 535
  def test_index_should_not_warn_when_not_exceeding_export_limit
    with_settings :issues_export_limit => 200 do
      get :index
      assert_select '#csv-export-options p.icon-warning', 0
    end
  end

  def test_index_should_warn_when_exceeding_export_limit
    with_settings :issues_export_limit => 2 do
      get :index
      assert_select '#csv-export-options p.icon-warning', :text => %r{limit: 2}
    end
  end

536
  def test_index_should_include_query_params_as_hidden_fields_in_csv_export_form
537 538 539 540 541 542 543
    get :index, :params => {
        :project_id => 1,
        :set_filter => "1",
        :tracker_id => "2",
        :sort => 'status',
        :c => ["status", "priority"]
      }
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559

    assert_select '#csv-export-form[action=?]', '/projects/ecookbook/issues.csv'
    assert_select '#csv-export-form[method=?]', 'get'

    assert_select '#csv-export-form' do
      assert_select 'input[name=?][value=?]', 'set_filter', '1'

      assert_select 'input[name=?][value=?]', 'f[]', 'tracker_id'
      assert_select 'input[name=?][value=?]', 'op[tracker_id]', '='
      assert_select 'input[name=?][value=?]', 'v[tracker_id][]', '2'

      assert_select 'input[name=?][value=?]', 'c[]', 'status'
      assert_select 'input[name=?][value=?]', 'c[]', 'priority'

      assert_select 'input[name=?][value=?]', 'sort', 'status'
    end
560

561 562 563 564 565
    get :index, :params => {
        :project_id => 1,
        :set_filter => "1",
        :f => ['']
      }
566
    assert_select '#csv-export-form input[name=?][value=?]', 'f[]', ''
567 568
  end

569
  def test_index_csv
570 571 572
    get :index, :params => {
        :format => 'csv'
      }
573
    assert_response :success
574

jplang's avatar
jplang committed
575
    assert_equal 'text/csv; header=present', @response.content_type
576 577 578 579
    assert response.body.starts_with?("#,")
    lines = response.body.chomp.split("\n")
    # default columns + id and project
    assert_equal Setting.issue_list_default_columns.size + 2, lines[0].split(',').size
580
  end
581

582
  def test_index_csv_with_project
583 584 585 586
    get :index, :params => {
        :project_id => 1,
        :format => 'csv'
      }
587
    assert_response :success
588

jplang's avatar
jplang committed
589
    assert_equal 'text/csv; header=present', @response.content_type
590
  end
591

592 593 594
  def test_index_csv_without_any_filters
    @request.session[:user_id] = 1
    Issue.create!(:project_id => 1, :tracker_id => 1, :status_id => 5, :subject => 'Closed issue', :author_id => 1)
595 596 597 598 599
    get :index, :params => {
        :set_filter => 1,
        :f => [''],
        :format => 'csv'
      }
600
    assert_response :success
601 602
    # -1 for headers
    assert_equal Issue.count, response.body.chomp.split("\n").size - 1
603 604
  end

605
  def test_index_csv_with_description
606 607 608
    Issue.generate!(:description => 'test_index_csv_with_description')

    with_settings :default_language => 'en' do
609 610 611 612 613
      get :index, :params => {
          :format => 'csv',
          :c => [:tracker,
          :description]
        }
614 615 616 617 618 619 620
      assert_response :success
    end

    assert_equal 'text/csv; header=present', response.content_type
    headers = response.body.chomp.split("\n").first.split(',')
    assert_include 'Description', headers
    assert_include 'test_index_csv_with_description', response.body
621 622
  end

623
  def test_index_csv_with_spent_time_column
jplang's avatar
jplang committed
624 625
    issue = Issue.create!(:project_id => 1, :tracker_id => 1, :subject => 'test_index_csv_with_spent_time_column', :author_id => 2)
    TimeEntry.create!(:project => issue.project, :issue => issue, :hours => 7.33, :user => User.find(2), :spent_on => Date.today)
626

627 628 629 630 631
    get :index, :params => {
        :format => 'csv',
        :set_filter => '1',
        :c => %w(subject spent_hours)
      }
632
    assert_response :success
jplang's avatar
jplang committed
633
    assert_equal 'text/csv; header=present', @response.content_type
634 635 636 637
    lines = @response.body.chomp.split("\n")
    assert_include "#{issue.id},#{issue.subject},7.33", lines
  end

638
  def test_index_csv_with_all_columns
639 640 641 642
    get :index, :params => {
        :format => 'csv',
        :c => ['all_inline']
      }
643
    assert_response :success
644

jplang's avatar
jplang committed
645
    assert_equal 'text/csv; header=present', @response.content_type
646 647
    assert_match /\A#,/, response.body
    lines = response.body.chomp.split("\n")
648
    assert_equal IssueQuery.new.available_inline_columns.size, lines[0].split(',').size
649 650
  end

jplang's avatar
jplang committed
651 652 653 654 655 656
  def test_index_csv_with_multi_column_field
    CustomField.find(1).update_attribute :multiple, true
    issue = Issue.find(1)
    issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
    issue.save!

657 658 659 660
    get :index, :params => {
        :format => 'csv',
        :c => ['tracker', "cf_1"]
      }
jplang's avatar
jplang committed
661 662 663 664 665
    assert_response :success
    lines = @response.body.chomp.split("\n")
    assert lines.detect {|line| line.include?('"MySQL, Oracle"')}
  end

666 667 668 669 670
  def test_index_csv_should_format_float_custom_fields_with_csv_decimal_separator
    field = IssueCustomField.create!(:name => 'Float', :is_for_all => true, :tracker_ids => [1], :field_format => 'float')
    issue = Issue.generate!(:project_id => 1, :tracker_id => 1, :custom_field_values => {field.id => '185.6'})

    with_settings :default_language => 'fr' do
671 672 673 674
      get :index, :params => {
          :format => 'csv',
        :c => ['id', 'tracker', "cf_#{field.id}"]
        }
675 676 677 678 679 680
      assert_response :success
      issue_line = response.body.chomp.split("\n").map {|line| line.split(';')}.detect {|line| line[0]==issue.id.to_s}
      assert_include '185,60', issue_line
    end

    with_settings :default_language => 'en' do
681 682 683 684
      get :index, :params => {
          :format => 'csv',
        :c => ['id', 'tracker', "cf_#{field.id}"]
        }
685 686 687 688 689
      assert_response :success
      issue_line = response.body.chomp.split("\n").map {|line| line.split(',')}.detect {|line| line[0]==issue.id.to_s}
      assert_include '185.60', issue_line
    end
  end
690 691 692 693 694 695 696

  def test_index_csv_should_fill_parent_column_with_parent_id
    Issue.delete_all
    parent = Issue.generate!
    child = Issue.generate!(:parent_issue_id => parent.id)

    with_settings :default_language => 'en' do
697 698 699 700
      get :index, :params => {
          :format => 'csv',
          :c => %w(parent)
        }
701
    end
jplang's avatar
Typo.  
jplang committed
702
    lines = response.body.split("\n")
703 704
    assert_include "#{child.id},#{parent.id}", lines
  end
705

706 707
  def test_index_csv_big_5
    with_settings :default_language => "zh-TW" do
jplang's avatar
jplang committed
708 709
      str_utf8  = "\xe4\xb8\x80\xe6\x9c\x88".force_encoding('UTF-8')
      str_big5  = "\xa4@\xa4\xeb".force_encoding('Big5')
jplang's avatar
jplang committed
710
      issue = Issue.generate!(:subject => str_utf8)
711

712 713 714 715 716
      get :index, :params => {
          :project_id => 1,
          :subject => str_utf8,
          :format => 'csv'
        }
jplang's avatar
jplang committed
717
      assert_equal 'text/csv; header=present', @response.content_type
718
      lines = @response.body.chomp.split("\n")
719
      header = lines[0]
720
      status = "\xaa\xac\xbaA".force_encoding('Big5')
721
      assert_include status, header
722
      issue_line = lines.find {|l| l =~ /^#{issue.id},/}
723
      assert_include str_big5, issue_line
724 725 726
    end
  end

727 728
  def test_index_csv_cannot_convert_should_be_replaced_big_5
    with_settings :default_language => "zh-TW" do
jplang's avatar
jplang committed
729
      str_utf8  = "\xe4\xbb\xa5\xe5\x86\x85".force_encoding('UTF-8')
jplang's avatar
jplang committed
730
      issue = Issue.generate!(:subject => str_utf8)
731

732 733 734 735 736 737 738
      get :index, :params => {
          :project_id => 1,
          :subject => str_utf8,
          :c => ['status', 'subject'],
          :format => 'csv',
          :set_filter => 1
        }
jplang's avatar
jplang committed
739
      assert_equal 'text/csv; header=present', @response.content_type
740
      lines = @response.body.chomp.split("\n")
741 742
      header = lines[0]
      issue_line = lines.find {|l| l =~ /^#{issue.id},/}
jplang's avatar
jplang committed
743
      s1 = "\xaa\xac\xbaA".force_encoding('Big5') # status
744 745
      assert header.include?(s1)
      s2 = issue_line.split(",")[2]
jplang's avatar
jplang committed
746 747
      s3 = "\xa5H?".force_encoding('Big5') # subject
      assert_equal s3, s2
748 749 750
    end
  end

751 752 753
  def test_index_csv_tw
    with_settings :default_language => "zh-TW" do
      str1  = "test_index_csv_tw"
jplang's avatar
jplang committed
754
      issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5')
755

756 757 758 759 760 761 762
      get :index, :params => {
          :project_id => 1,
          :subject => str1,
          :c => ['estimated_hours', 'subject'],
          :format => 'csv',
          :set_filter => 1
        }
jplang's avatar
jplang committed
763
      assert_equal 'text/csv; header=present', @response.content_type
764
      lines = @response.body.chomp.split("\n")
765
      assert_include "#{issue.id},1234.50,#{str1}", lines
766 767 768 769 770 771
    end
  end

  def test_index_csv_fr
    with_settings :default_language => "fr" do
      str1  = "test_index_csv_fr"
jplang's avatar
jplang committed
772
      issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5')
773

774 775 776 777 778 779 780
      get :index, :params => {
          :project_id => 1,
          :subject => str1,
          :c => ['estimated_hours', 'subject'],
          :format => 'csv',
          :set_filter => 1
        }
jplang's avatar
jplang committed
781
      assert_equal 'text/csv; header=present', @response.content_type
782
      lines = @response.body.chomp.split("\n")
783
      assert_include "#{issue.id};1234,50;#{str1}", lines
784 785 786
    end
  end

787
  def test_index_pdf
788 789
    ["en", "zh", "zh-TW", "ja", "ko"].each do |lang|
      with_settings :default_language => lang do
790

791 792
        get :index
        assert_response :success
793

794 795 796
        get :index, :params => {
            :format => 'pdf'
          }
797 798 799
        assert_response :success
        assert_equal 'application/pdf', @response.content_type

800 801 802 803
        get :index, :params => {
            :project_id => 1,
            :format => 'pdf'
          }
804 805 806
        assert_response :success
        assert_equal 'application/pdf', @response.content_type

807 808 809 810 811
        get :index, :params => {
            :project_id => 1,
            :query_id => 6,
            :format => 'pdf'
          }
812 813 814 815
        assert_response :success
        assert_equal 'application/pdf', @response.content_type
      end
    end
816
  end
817

818
  def test_index_pdf_with_query_grouped_by_list_custom_field
819 820 821 822 823
    get :index, :params => {
        :project_id => 1,
        :query_id => 9,
        :format => 'pdf'
      }
824 825 826
    assert_response :success
    assert_equal 'application/pdf', @response.content_type
  end
827

828
  def test_index_atom
829 830 831 832
    get :index, :params => {
        :project_id => 'ecookbook',
        :format => 'atom'
      }
833
    assert_response :success
834
    assert_equal 'application/atom+xml', response.content_type
835

836 837 838 839 840
    assert_select 'feed' do
      assert_select 'link[rel=self][href=?]', 'http://test.host/projects/ecookbook/issues.atom'
      assert_select 'link[rel=alternate][href=?]', 'http://test.host/projects/ecookbook/issues'
      assert_select 'entry link[href=?]', 'http://test.host/issues/1'
    end
841 842
  end

843
  def test_index_should_include_back_url_input
844 845 846 847
    get :index, :params => {
        :project_id => 'ecookbook',
        :foo => 'bar'
      }
848 849 850 851
    assert_response :success
    assert_select 'input[name=back_url][value=?]', '/projects/ecookbook/issues?foo=bar'
  end

852
  def test_index_sort
853 854 855
    get :index, :params => {
        :sort => 'tracker,id:desc'
      }
856
    assert_response :success
857

858
    assert_equal issues_in_list.sort_by {|issue| [issue.tracker.position, -issue.id]}, issues_in_list
859
    assert_select 'table.issues.sort-by-tracker.sort-asc'
860
  end
861 862

  def test_index_sort_by_field_not_included_in_columns
jplang's avatar
jplang committed
863
    with_settings :issue_list_default_columns => %w(subject author) do
864 865 866
      get :index, :params => {
          :sort => 'tracker'
        }
jplang's avatar
jplang committed
867 868
      assert_response :success
    end
869
  end
870

871
  def test_index_sort_by_assigned_to
872 873 874
    get :index, :params => {
        :sort => 'assigned_to'
      }
875
    assert_response :success
876

877
    assignees = issues_in_list.map(&:assigned_to).compact
878
    assert_equal assignees.sort, assignees
879
    assert_select 'table.issues.sort-by-assigned-to.sort-asc'
880
  end
881

882
  def test_index_sort_by_assigned_to_desc
883 884 885
    get :index, :params => {
        :sort => 'assigned_to:desc'
      }
886
    assert_response :success
887

888
    assignees = issues_in_list.map(&:assigned_to).compact
889
    assert_equal assignees.sort.reverse, assignees
890
    assert_select 'table.issues.sort-by-assigned-to.sort-desc'
891
  end
892

893
  def test_index_group_by_assigned_to
894 895 896 897
    get :index, :params => {
        :group_by => 'assigned_to',
        :sort => 'priority'
      }
898 899
    assert_response :success
  end
900

901
  def test_index_sort_by_author
902 903 904 905
    get :index, :params => {
        :sort => 'author',
        :c => ['author']
      }
906
    assert_response :success
907

908
    authors = issues_in_list.map(&:author)
909 910
    assert_equal authors.sort, authors
  end
911

912
  def test_index_sort_by_author_desc
913 914 915
    get :index, :params => {
        :sort => 'author:desc'
      }
916
    assert_response :success
917

918
    authors = issues_in_list.map(&:author)
919
    assert_equal authors.sort.reverse, authors
920
  end
921

922
  def test_index_group_by_author
923 924 925 926
    get :index, :params => {
        :group_by => 'author',
        :sort => 'priority'
      }
927 928
    assert_response :success
  end
929

930
  def test_index_sort_by_last_updated_by
931 932 933
    get :index, :params => {
        :sort => 'last_updated_by'
      }
934 935 936 937 938
    assert_response :success
    assert_select 'table.issues.sort-by-last-updated-by.sort-asc'
  end

  def test_index_sort_by_last_updated_by_desc
939 940 941
    get :index, :params => {
        :sort => 'last_updated_by:desc'
      }
942 943 944 945
    assert_response :success
    assert_select 'table.issues.sort-by-last-updated-by.sort-desc'
  end

jplang's avatar
jplang committed
946
  def test_index_sort_by_spent_hours
947 948 949
    get :index, :params => {
        :sort => 'spent_hours:desc'
      }
950
    assert_response :success
951
    hours = issues_in_list.map(&:spent_hours)
952 953
    assert_equal hours.sort.reverse, hours
  end
954

955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970
  def test_index_sort_by_spent_hours_should_sort_by_visible_spent_hours
    TimeEntry.delete_all
    TimeEntry.generate!(:issue => Issue.generate!(:project_id => 1), :hours => 3)
    TimeEntry.generate!(:issue => Issue.generate!(:project_id => 3), :hours => 4)

    get :index, :params => {:sort => "spent_hours:desc", :c => ['subject','spent_hours']}
    assert_response :success
    assert_equal [4.0, 3.0, 0.0], issues_in_list.map(&:spent_hours)[0..2]

    Project.find(3).disable_module!(:time_tracking)

    get :index, :params => {:sort => "spent_hours:desc", :c => ['subject','spent_hours']}
    assert_response :success
    assert_equal [3.0, 0.0, 0.0], issues_in_list.map(&:spent_hours)[0..2]
  end

971
  def test_index_sort_by_total_spent_hours
972 973 974
    get :index, :params => {
        :sort => 'total_spent_hours:desc'
      }
975
    assert_response :success
976
    hours = issues_in_list.map(&:total_spent_hours)
977 978
    assert_equal hours.sort.reverse, hours
  end
979

980
  def test_index_sort_by_total_estimated_hours
981 982 983
    get :index, :params => {
        :sort => 'total_estimated_hours:desc'
      }
984
    assert_response :success
985
    hours = issues_in_list.map(&:total_estimated_hours)
986 987
    assert_equal hours.sort.reverse, hours
  end
988

989 990 991 992 993 994 995
  def test_index_sort_by_user_custom_field
    cf = IssueCustomField.create!(:name => 'User', :is_for_all => true, :tracker_ids => [1,2,3], :field_format => 'user')
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(1), :value => '2')
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(2), :value => '3')
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(3), :value => '3')
    CustomValue.create!(:custom_field => cf, :customized => Issue.find(5), :value => '')

996 997 998 999 1000
    get :index, :params => {
        :project_id => 1,
        :set_filter => 1,
      :sort => "cf_#{cf.id},id"
      }
1001 1002
    assert_response :success

1003
    assert_equal [2, 3, 1], issues_in_list.select {