issues_controller_test.rb 172 KB
Newer Older
1
# Redmine - project management software
jplang's avatar
jplang committed
2
# Copyright (C) 2006-2016  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
jplang's avatar
jplang committed
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 80 81

  def test_index_should_list_visible_issues_only
    get :index, :per_page => 100
    assert_response :success
82 83 84 85

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

88
  def test_index_with_project
89
    Setting.display_subprojects_issues = 0
90 91
    get :index, :project_id => 1
    assert_response :success
92

93
    assert_select 'a[href="/issues/1"]', :text => /Cannot print recipes/
94
    assert_select 'a[href="/issues/5"]', 0
95
  end
96

97 98 99 100
  def test_index_with_project_and_subprojects
    Setting.display_subprojects_issues = 1
    get :index, :project_id => 1
    assert_response :success
101

102
    assert_select 'a[href="/issues/1"]', :text => /Cannot print recipes/
103 104
    assert_select 'a[href="/issues/5"]', :text => /Subproject issue/
    assert_select 'a[href="/issues/6"]', 0
105
  end
106

107
  def test_index_with_project_and_subprojects_should_show_private_subprojects_with_permission
108 109 110 111
    @request.session[:user_id] = 2
    Setting.display_subprojects_issues = 1
    get :index, :project_id => 1
    assert_response :success
112

113
    assert_select 'a[href="/issues/1"]', :text => /Cannot print recipes/
114 115
    assert_select 'a[href="/issues/5"]', :text => /Subproject issue/
    assert_select 'a[href="/issues/6"]', :text => /Issue of a private subproject/
116
  end
117

118
  def test_index_with_project_and_default_filter
119 120
    get :index, :project_id => 1, :set_filter => 1
    assert_response :success
121

122
    # default filter
123
    assert_query_filters [['status_id', 'o', '']]
124
  end
125

126
  def test_index_with_project_and_filter
127
    get :index, :project_id => 1, :set_filter => 1,
jplang's avatar
jplang committed
128 129
      :f => ['tracker_id'],
      :op => {'tracker_id' => '='},
130
      :v => {'tracker_id' => ['1']}
131
    assert_response :success
132

133
    assert_query_filters [['tracker_id', '=', '1']]
134
  end
135

136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
  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'] }},
151 152
      'tracker_id' => {
        '3' => { :op => '=', :values => ['3'] },
emassip's avatar
emassip committed
153
        '=3' => { :op => '=', :values => ['3'] }},
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
      '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'] },
170 171 172
        '<t-2' => { :op => '<t-', :values => ['2'] },
        '>t-2' => { :op => '>t-', :values => ['2'] },
        't-2' => { :op => 't-', :values => ['2'] }},
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
      '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
191

192 193 194
        get :index, :set_filter => 1, field => filter_expression
        assert_response :success

195 196
        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]]}
197 198 199 200
      end
    end
  end

201 202 203
  def test_index_with_project_and_empty_filters
    get :index, :project_id => 1, :set_filter => 1, :fields => ['']
    assert_response :success
204

205
    # no filter
206
    assert_query_filters []
207
  end
208

209 210 211 212 213 214 215 216 217 218
  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

    get :index, :set_filter => 1,
      :f => [filter_name],
      :op => {filter_name => '='},
219 220
      :v => {filter_name => ['Foo']},
      :c => ['project']
221
    assert_response :success
222 223

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

jplang's avatar
jplang committed
226 227 228 229
  def test_index_with_query
    get :index, :project_id => 1, :query_id => 5
    assert_response :success
  end
230

231
  def test_index_with_query_grouped_by_tracker
jplang's avatar
jplang committed
232 233
    get :index, :project_id => 1, :query_id => 6
    assert_response :success
234
    assert_select 'tr.group span.count'
235
  end
236

237 238 239
  def test_index_with_query_grouped_and_sorted_by_category
    get :index, :project_id => 1, :set_filter => 1, :group_by => "category", :sort => "category"
    assert_response :success
240
    assert_select 'tr.group span.count'
241 242
  end

243 244 245
  def test_index_with_query_grouped_and_sorted_by_fixed_version
    get :index, :project_id => 1, :set_filter => 1, :group_by => "fixed_version", :sort => "fixed_version"
    assert_response :success
246
    assert_select 'tr.group span.count'
247 248 249 250 251
  end

  def test_index_with_query_grouped_and_sorted_by_fixed_version_in_reverse_order
    get :index, :project_id => 1, :set_filter => 1, :group_by => "fixed_version", :sort => "fixed_version:desc"
    assert_response :success
252
    assert_select 'tr.group span.count'
253 254
  end

255 256 257
  def test_index_with_query_grouped_by_list_custom_field
    get :index, :project_id => 1, :query_id => 9
    assert_response :success
258
    assert_select 'tr.group span.count'
jplang's avatar
jplang committed
259
  end
260

261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283
  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 => '')

    get :index, :project_id => 1, :set_filter => 1, :group_by => "cf_#{cf.id}"
    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

284 285 286 287 288 289 290 291 292 293 294 295 296
  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 => '')

    get :index, :project_id => 1, :set_filter => 1, :group_by => "cf_#{cf.id}"
    assert_response :success

    assert_select 'tr.group', 3
    assert_select 'tr.group' do
      assert_select 'a', :text => 'John Smith'
jplang's avatar
jplang committed
297
      assert_select 'span.count', :text => '1'
298 299 300
    end
    assert_select 'tr.group' do
      assert_select 'a', :text => 'Dave Lopper'
jplang's avatar
jplang committed
301
      assert_select 'span.count', :text => '2'
302 303 304
    end
  end

305 306 307 308 309 310 311 312 313 314 315
  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
      get :index, :project_id => 1, :set_filter => 1, :group_by => "cf_#{cf.id}"
      assert_response :success
    end

316
    assert_select 'tr.group', 3
317 318
    assert_select 'tr.group', :text => /Yes/
    assert_select 'tr.group', :text => /No/
jplang's avatar
jplang committed
319
    assert_select 'tr.group', :text => /blank/
320 321
  end

322 323 324 325 326 327 328 329 330 331
  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
      get :index, :project_id => 1, :set_filter => 1, "cf_#{cf.id}" => "*", :group_by => "cf_#{cf.id}"
      assert_response :success
    end

332
    assert_equal [1, 2], issues_in_list.map(&:id).sort
333 334 335 336
    assert_select 'tr.group', 1
    assert_select 'tr.group', :text => /No/
  end

337
  def test_index_with_query_grouped_by_tracker_in_normal_order
338 339 340 341 342
    3.times {|i| Issue.generate!(:tracker_id => (i + 1))}

    get :index, :set_filter => 1, :group_by => 'tracker', :sort => 'id:desc'
    assert_response :success

343 344
    assert_equal ["Bug", "Feature request", "Support request"],
      css_select("tr.issue td.tracker").map(&:text).uniq
345 346 347 348 349 350 351 352
  end

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

    get :index, :set_filter => 1, :group_by => 'tracker', :sort => 'id:desc,tracker:desc'
    assert_response :success

353 354
    assert_equal ["Bug", "Feature request", "Support request"].reverse,
      css_select("tr.issue td.tracker").map(&:text).uniq
355 356
  end

357 358 359
  def test_index_with_query_id_and_project_id_should_set_session_query
    get :index, :project_id => 1, :query_id => 4
    assert_response :success
360 361 362
    assert_kind_of Hash, session[:issue_query]
    assert_equal 4, session[:issue_query][:id]
    assert_equal 1, session[:issue_query][:project_id]
363 364
  end

365 366 367 368 369
  def test_index_with_invalid_query_id_should_respond_404
    get :index, :project_id => 1, :query_id => 999
    assert_response 404
  end

370
  def test_index_with_cross_project_query_in_session_should_show_project_issues
371
    q = IssueQuery.create!(:name => "cross_project_query", :user_id => 2, :project => nil, :column_names => ['project'])
372
    @request.session[:issue_query] = {:id => q.id, :project_id => 1}
373 374 375 376 377

    with_settings :display_subprojects_issues => '0' do
      get :index, :project_id => 1
    end
    assert_response :success
378 379 380

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

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

387 388 389
    get :index, :query_id => q.id
    assert_response 403
  end
390

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

395 396 397
    get :index, :query_id => q.id
    assert_response :success
  end
398

399
  def test_public_query_should_be_available_to_other_users
400
    q = IssueQuery.create!(:name => "public", :user => User.find(2), :visibility => IssueQuery::VISIBILITY_PUBLIC, :project => nil)
401
    @request.session[:user_id] = 3
402

403 404 405
    get :index, :query_id => q.id
    assert_response :success
  end
406

407 408 409
  def test_index_should_omit_page_param_in_export_links
    get :index, :page => 2
    assert_response :success
410 411 412
    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
413
    assert_select 'form#csv-export-form[action="/issues.csv"]'
414 415
  end

416 417 418 419 420 421 422 423 424 425 426 427 428 429
  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

430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
  def test_index_should_include_query_params_as_hidden_fields_in_csv_export_form
    get :index, :project_id => 1, :set_filter => "1", :tracker_id => "2", :sort => 'status', :c => ["status", "priority"]

    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
448 449 450

    get :index, :project_id => 1, :set_filter => "1", :f => []
    assert_select '#csv-export-form input[name=?][value=?]', 'f[]', ''
451 452
  end

453
  def test_index_csv
454 455
    get :index, :format => 'csv'
    assert_response :success
456

jplang's avatar
jplang committed
457
    assert_equal 'text/csv; header=present', @response.content_type
458 459 460 461
    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
462
  end
463

464
  def test_index_csv_with_project
465 466
    get :index, :project_id => 1, :format => 'csv'
    assert_response :success
467

jplang's avatar
jplang committed
468
    assert_equal 'text/csv; header=present', @response.content_type
469
  end
470

471 472 473 474 475
  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)
    get :index, :set_filter => 1, :f => [], :format => 'csv'
    assert_response :success
476 477
    # -1 for headers
    assert_equal Issue.count, response.body.chomp.split("\n").size - 1
478 479
  end

480
  def test_index_csv_with_description
481 482 483
    Issue.generate!(:description => 'test_index_csv_with_description')

    with_settings :default_language => 'en' do
484
      get :index, :format => 'csv', :csv => {:description => '1'}
485 486 487 488 489 490 491
      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
492 493
  end

494
  def test_index_csv_with_spent_time_column
jplang's avatar
jplang committed
495 496
    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)
497 498 499

    get :index, :format => 'csv', :set_filter => '1', :c => %w(subject spent_hours)
    assert_response :success
jplang's avatar
jplang committed
500
    assert_equal 'text/csv; header=present', @response.content_type
501 502 503 504
    lines = @response.body.chomp.split("\n")
    assert_include "#{issue.id},#{issue.subject},7.33", lines
  end

505
  def test_index_csv_with_all_columns
506
    get :index, :format => 'csv', :csv => {:columns => 'all'}
507
    assert_response :success
508

jplang's avatar
jplang committed
509
    assert_equal 'text/csv; header=present', @response.content_type
510 511
    assert_match /\A#,/, response.body
    lines = response.body.chomp.split("\n")
512
    assert_equal IssueQuery.new.available_inline_columns.size, lines[0].split(',').size
513 514
  end

jplang's avatar
jplang committed
515 516 517 518 519 520
  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!

521
    get :index, :format => 'csv', :csv => {:columns => 'all'}
jplang's avatar
jplang committed
522 523 524 525 526
    assert_response :success
    lines = @response.body.chomp.split("\n")
    assert lines.detect {|line| line.include?('"MySQL, Oracle"')}
  end

527 528 529 530 531
  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
532
      get :index, :format => 'csv', :csv => {:columns => 'all'}
533 534 535 536 537 538
      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
539
      get :index, :format => 'csv', :csv => {:columns => 'all'}
540 541 542 543 544
      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
545 546 547 548 549 550 551 552 553

  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
      get :index, :format => 'csv', :c => %w(parent)
    end
jplang's avatar
Typo.  
jplang committed
554
    lines = response.body.split("\n")
555 556
    assert_include "#{child.id},#{parent.id}", lines
  end
557

558 559
  def test_index_csv_big_5
    with_settings :default_language => "zh-TW" do
jplang's avatar
jplang committed
560 561
      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
562
      issue = Issue.generate!(:subject => str_utf8)
563 564 565 566 567

      get :index, :project_id => 1, 
                  :f => ['subject'], 
                  :op => '=', :values => [str_utf8],
                  :format => 'csv'
jplang's avatar
jplang committed
568
      assert_equal 'text/csv; header=present', @response.content_type
569
      lines = @response.body.chomp.split("\n")
570
      header = lines[0]
571
      status = "\xaa\xac\xbaA".force_encoding('Big5')
572
      assert_include status, header
573
      issue_line = lines.find {|l| l =~ /^#{issue.id},/}
574
      assert_include str_big5, issue_line
575 576 577
    end
  end

578 579
  def test_index_csv_cannot_convert_should_be_replaced_big_5
    with_settings :default_language => "zh-TW" do
jplang's avatar
jplang committed
580
      str_utf8  = "\xe4\xbb\xa5\xe5\x86\x85".force_encoding('UTF-8')
jplang's avatar
jplang committed
581
      issue = Issue.generate!(:subject => str_utf8)
582 583 584 585

      get :index, :project_id => 1, 
                  :f => ['subject'], 
                  :op => '=', :values => [str_utf8],
586 587 588
                  :c => ['status', 'subject'],
                  :format => 'csv',
                  :set_filter => 1
jplang's avatar
jplang committed
589
      assert_equal 'text/csv; header=present', @response.content_type
590
      lines = @response.body.chomp.split("\n")
591 592
      header = lines[0]
      issue_line = lines.find {|l| l =~ /^#{issue.id},/}
jplang's avatar
jplang committed
593
      s1 = "\xaa\xac\xbaA".force_encoding('Big5') # status
594 595
      assert header.include?(s1)
      s2 = issue_line.split(",")[2]
jplang's avatar
jplang committed
596 597
      s3 = "\xa5H?".force_encoding('Big5') # subject
      assert_equal s3, s2
598 599 600
    end
  end

601 602 603
  def test_index_csv_tw
    with_settings :default_language => "zh-TW" do
      str1  = "test_index_csv_tw"
jplang's avatar
jplang committed
604
      issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5')
605 606 607 608 609 610 611

      get :index, :project_id => 1, 
                  :f => ['subject'], 
                  :op => '=', :values => [str1],
                  :c => ['estimated_hours', 'subject'],
                  :format => 'csv',
                  :set_filter => 1
jplang's avatar
jplang committed
612
      assert_equal 'text/csv; header=present', @response.content_type
613
      lines = @response.body.chomp.split("\n")
614
      assert_include "#{issue.id},1234.50,#{str1}", lines
615 616 617 618 619 620
    end
  end

  def test_index_csv_fr
    with_settings :default_language => "fr" do
      str1  = "test_index_csv_fr"
jplang's avatar
jplang committed
621
      issue = Issue.generate!(:subject => str1, :estimated_hours => '1234.5')
622 623 624 625 626 627 628

      get :index, :project_id => 1, 
                  :f => ['subject'], 
                  :op => '=', :values => [str1],
                  :c => ['estimated_hours', 'subject'],
                  :format => 'csv',
                  :set_filter => 1
jplang's avatar
jplang committed
629
      assert_equal 'text/csv; header=present', @response.content_type
630
      lines = @response.body.chomp.split("\n")
631
      assert_include "#{issue.id};1234,50;#{str1}", lines
632 633 634
    end
  end

635
  def test_index_pdf
636 637
    ["en", "zh", "zh-TW", "ja", "ko"].each do |lang|
      with_settings :default_language => lang do
638

639 640
        get :index
        assert_response :success
641

642 643 644 645 646 647 648 649 650 651 652 653 654
        get :index, :format => 'pdf'
        assert_response :success
        assert_equal 'application/pdf', @response.content_type

        get :index, :project_id => 1, :format => 'pdf'
        assert_response :success
        assert_equal 'application/pdf', @response.content_type

        get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
        assert_response :success
        assert_equal 'application/pdf', @response.content_type
      end
    end
655
  end
656

657 658 659 660 661
  def test_index_pdf_with_query_grouped_by_list_custom_field
    get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
    assert_response :success
    assert_equal 'application/pdf', @response.content_type
  end
662

663 664 665
  def test_index_atom
    get :index, :project_id => 'ecookbook', :format => 'atom'
    assert_response :success
666
    assert_equal 'application/atom+xml', response.content_type
667

668 669 670 671 672
    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
673 674
  end

jplang's avatar
jplang committed
675 676 677 678 679 680
  def test_index_should_include_back_url_input
    get :index, :project_id => 'ecookbook', :foo => 'bar'
    assert_response :success
    assert_select 'input[name=back_url][value=?]', '/projects/ecookbook/issues?foo=bar'
  end

681
  def test_index_sort
jplang's avatar
jplang committed
682
    get :index, :sort => 'tracker,id:desc'
683
    assert_response :success
684

jplang's avatar
jplang committed
685 686 687
    sort_params = @request.session['issues_index_sort']
    assert sort_params.is_a?(String)
    assert_equal 'tracker,id:desc', sort_params
688

689
    assert_equal issues_in_list.sort_by {|issue| [issue.tracker.position, -issue.id]}, issues_in_list
690
    assert_select 'table.issues.sort-by-tracker.sort-asc'
691
  end
692 693 694 695 696 697 698 699 700

  def test_index_sort_by_field_not_included_in_columns
    Setting.issue_list_default_columns = %w(subject author)
    get :index, :sort => 'tracker'
  end
  
  def test_index_sort_by_assigned_to
    get :index, :sort => 'assigned_to'
    assert_response :success
701 702
    
    assignees = issues_in_list.map(&:assigned_to).compact
703
    assert_equal assignees.sort, assignees
704
    assert_select 'table.issues.sort-by-assigned-to.sort-asc'
705 706 707 708 709
  end
  
  def test_index_sort_by_assigned_to_desc
    get :index, :sort => 'assigned_to:desc'
    assert_response :success
710 711
    
    assignees = issues_in_list.map(&:assigned_to).compact
712
    assert_equal assignees.sort.reverse, assignees
713
    assert_select 'table.issues.sort-by-assigned-to.sort-desc'
714 715 716 717 718 719
  end
  
  def test_index_group_by_assigned_to
    get :index, :group_by => 'assigned_to', :sort => 'priority'
    assert_response :success
  end
720 721
  
  def test_index_sort_by_author
722
    get :index, :sort => 'author', :c => ['author']
723
    assert_response :success
724 725
    
    authors = issues_in_list.map(&:author)
726 727
    assert_equal authors.sort, authors
  end
728

729 730 731
  def test_index_sort_by_author_desc
    get :index, :sort => 'author:desc'
    assert_response :success
732 733
    
    authors = issues_in_list.map(&:author)
734
    assert_equal authors.sort.reverse, authors
735 736 737 738 739 740
  end
  
  def test_index_group_by_author
    get :index, :group_by => 'author', :sort => 'priority'
    assert_response :success
  end
741
  
jplang's avatar
jplang committed
742 743
  def test_index_sort_by_spent_hours
    get :index, :sort => 'spent_hours:desc'
744
    assert_response :success
745
    hours = issues_in_list.map(&:spent_hours)
746 747
    assert_equal hours.sort.reverse, hours
  end
748 749 750 751
  
  def test_index_sort_by_total_spent_hours
    get :index, :sort => 'total_spent_hours:desc'
    assert_response :success
752
    hours = issues_in_list.map(&:total_spent_hours)
753 754
    assert_equal hours.sort.reverse, hours
  end
755 756 757 758
  
  def test_index_sort_by_total_estimated_hours
    get :index, :sort => 'total_estimated_hours:desc'
    assert_response :success
759
    hours = issues_in_list.map(&:total_estimated_hours)
760 761
    assert_equal hours.sort.reverse, hours
  end
762

763 764 765 766 767 768 769 770 771 772
  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 => '')

    get :index, :project_id => 1, :set_filter => 1, :sort => "cf_#{cf.id},id"
    assert_response :success

773
    assert_equal [2, 3, 1], issues_in_list.select {|issue| issue.custom_field_value(cf).present?}.map(&:id)
774 775
  end

776 777
  def test_index_with_columns
    columns = ['tracker', 'subject', 'assigned_to']
778
    get :index, :set_filter => 1, :c => columns
779
    assert_response :success
780

781 782
    # query should use specified columns + id and checkbox
    assert_select 'table.issues thead th', columns.size + 2
783

784
    # columns should be stored in session
785 786 787
    assert_kind_of Hash, session[:issue_query]
    assert_kind_of Array, session[:issue_query][:column_names]
    assert_equal columns, session[:issue_query][:column_names].map(&:to_s)
788 789

    # ensure only these columns are kept in the selected columns list
790 791 792 793 794
    assert_select 'select#selected_columns option' do
      assert_select 'option', 3
      assert_select 'option[value=tracker]'
      assert_select 'option[value=project]', 0
    end
795
  end
796

797 798 799 800 801
  def test_index_without_project_should_implicitly_add_project_column_to_default_columns
    Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
    get :index, :set_filter => 1

    # query should use specified columns
802
    assert_equal ["#", "Project", "Tracker", "Subject", "Assignee"], columns_in_issues_list
803 804 805 806
  end

  def test_index_without_project_and_explicit_default_columns_should_not_add_project_column
    Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
807
    columns = ['id', 'tracker', 'subject', 'assigned_to']
808 809 810
    get :index, :set_filter => 1, :c => columns

    # query should use specified columns
811
    assert_equal ["#", "Tracker", "Subject", "Assignee"], columns_in_issues_list
812 813
  end

814 815 816 817 818
  def test_index_with_default_columns_should_respect_default_columns_order
    columns = ['assigned_to', 'subject', 'status', 'tracker']
    with_settings :issue_list_default_columns => columns do
      get :index, :project_id => 1, :set_filter => 1

819
      assert_equal ["#", "Assignee", "Subject", "Status", "Tracker"], columns_in_issues_list
820 821 822
    end
  end

823 824 825 826
  def test_index_with_custom_field_column
    columns = %w(tracker subject cf_2)
    get :index, :set_filter => 1, :c => columns
    assert_response :success
827

828
    # query should use specified columns
829
    assert_equal ["#", "Tracker", "Subject", "Searchable field"], columns_in_issues_list
830
    assert_select 'table.issues td.cf_2.string'
831
  end
832

833 834 835 836 837 838 839 840 841 842
  def test_index_with_multi_custom_field_column
    field = CustomField.find(1)
    field.update_attribute :multiple, true
    issue = Issue.find(1)
    issue.custom_field_values = {1 => ['MySQL', 'Oracle']}
    issue.save!

    get :index, :set_filter => 1, :c => %w(tracker subject cf_1)
    assert_response :success

843
    assert_select 'table.issues td.cf_1', :text => 'MySQL, Oracle'
844 845 846 847 848 849 850 851 852 853 854 855
  end

  def test_index_with_multi_user_custom_field_column
    field = IssueCustomField.create!(:name => 'Multi user', :field_format => 'user', :multiple => true,
      :tracker_ids => [1], :is_for_all => true)
    issue = Issue.find(1)
    issue.custom_field_values = {field.id => ['2', '3']}
    issue.save!

    get :index, :set_filter => 1, :c => ['tracker', 'subject', "cf_#{field.id}"]
    assert_response :success

856 857 858 859 860
    assert_select "table.issues td.cf_#{field.id}" do
      assert_select 'a', 2
      assert_select 'a[href=?]', '/users/2', :text => 'John Smith'
      assert_select 'a[href=?]', '/users/3', :text => 'Dave Lopper'
    end
861 862
  end

863 864
  def test_index_with_date_column
    with_settings :date_format => '%d/%m/%Y' do
865
      Issue.find(1).update_attribute :start_date, '1987-08-24'
866
      get :index, :set_filter => 1, :c => %w(start_date)
867
      assert_select "table.issues td.start_date", :text => '24/08/1987'
868 869 870
    end
  end

871
  def test_index_with_done_ratio_column
872 873
    Issue.find(1).update_attribute :done_ratio, 40
    get :index, :set_filter => 1, :c => %w(done_ratio)
874 875 876 877 878
    assert_select 'table.issues td.done_ratio' do
      assert_select 'table.progress' do
        assert_select 'td.closed[style=?]', 'width: 40%;'
      end
    end
879 880
  end

881
  def test_index_with_spent_hours_column
jplang's avatar
jplang committed
882
    Issue.expects(:load_visible_spent_hours).once
883
    get :index, :set_filter => 1, :c => %w(subject spent_hours)
884
    assert_select 'table.issues tr#issue-3 td.spent_hours', :text => '1.00'
885 886
  end

887
  def test_index_with_total_spent_hours_column
jplang's avatar
jplang committed
888
    Issue.expects(:load_visible_total_spent_hours).once
889 890 891 892
    get :index, :set_filter => 1, :c => %w(subject total_spent_hours)
    assert_select 'table.issues tr#issue-3 td.total_spent_hours', :text => '1.00'
  end

893 894 895 896 897
  def test_index_with_total_estimated_hours_column
    get :index, :set_filter => 1, :c => %w(subject total_estimated_hours)
    assert_select 'table.issues td.total_estimated_hours'
  end