issues_controller_test.rb 177 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
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
  end

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

jplang's avatar
jplang committed
350
    get :index, :set_filter => 1, :group_by => 'tracker', :c => ['tracker', 'subject'], :sort => 'id:desc,tracker:desc'
351 352
    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', :c => [:tracker, :description]
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', :c => ['all_inline']
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', :c => ['tracker', "cf_1"]
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', :c => ['id', 'tracker', "cf_#{field.id}"]
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', :c => ['id', 'tracker', "cf_#{field.id}"]
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

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

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

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

765 766 767 768 769 770 771 772 773 774
  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

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

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

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

786
    # columns should be stored in session
787 788 789
    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)
790 791

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

799
  def test_index_without_project_should_implicitly_add_project_column_to_default_columns
jplang's avatar
jplang committed
800 801 802
    with_settings :issue_list_default_columns => ['tracker', 'subject', 'assigned_to'] do
      get :index, :set_filter => 1
    end
803 804

    # query should use specified columns
805
    assert_equal ["#", "Project", "Tracker", "Subject", "Assignee"], columns_in_issues_list
806 807 808
  end

  def test_index_without_project_and_explicit_default_columns_should_not_add_project_column
jplang's avatar
jplang committed
809 810 811 812
    with_settings :issue_list_default_columns => ['tracker', 'subject', 'assigned_to'] do
      columns = ['id', 'tracker', 'subject', 'assigned_to']
      get :index, :set_filter => 1, :c => columns
    end
813 814

    # query should use specified columns
815
    assert_equal ["#", "Tracker", "Subject", "Assignee"], columns_in_issues_list
816 817
  end

818 819 820 821 822
  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

823
      assert_equal ["#", "Assignee", "Subject", "Status", "Tracker"], columns_in_issues_list
824 825 826
    end
  end

827 828 829 830
  def test_index_with_custom_field_column
    columns = %w(tracker subject cf_2)
    get :index, :set_filter => 1, :c => columns
    assert_response :success
831

832
    # query should use specified columns
833
    assert_equal ["#", "Tracker", "Subject", "Searchable field"], columns_in_issues_list
834
    assert_select 'table.issues td.cf_2.string'
835
  end
836

837 838 839 840 841 842 843 844 845 846
  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

847
    assert_select 'table.issues td.cf_1', :text => 'MySQL, Oracle'
848 849 850 851 852 853 854 855 856 857 858 859
  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

860 861 862 863 864
    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
865 866
  end

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

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

885
  def test_index_with_spent_hours_column
886
    Issue.expects(:load_visible_spent_hours).once
887
    get :index, :set_filter => 1, :c => %w(subject spent_hours)
888
    assert_select 'table.issues tr#issue-3 td.spent_hours', :text => '1.00'
889 890
  end

891
  def test_index_with_total_spent_hours_column
892
    Issue.expects(:load_visible_total_spent_hours).once
893 894 895 896
    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

897 898 899 900 901
  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

902 903 904
  def test_index_should_not_show_spent_hours_column_without_permission
    Role.anonymous.remove_permission! :view_time_entries
    get :index, :set_filter => 1, :c => %w(subject spent_hours)
905
    assert_select 'td.spent_hours', 0
906 907
  end

908
  def test_index_with_fixed_version_column
909
    get :index, :set_filter => 1, :c => %w(fixed_version)
910
    assert_select 'table.issues td.fixed_version' do
911
      assert_select 'a[href=?]', '/versions/2', :text => 'eCookbook - 1.0'
912
    end
913 914
  end

915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950
  def test_index_with_relations_column
    IssueRelation.delete_all
    IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(1), :issue_to => Issue.find(7))
    IssueRelation.create!(:relation_type => "relates", :issue_from => Issue.find(8), :issue_to => Issue.find(1))
    IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(1), :issue_to => Issue.find(11))
    IssueRelation.create!(:relation_type => "blocks", :issue_from => Issue.find(12), :issue_to => Issue.find(2))

    get :index, :set_filter => 1, :c => %w(subject relations)
    assert_response :success
    assert_select "tr#issue-1 td.relations" do
      assert_select "span", 3
      assert_select "span", :text => "Related to #7"
      assert_select "span", :text => "Related to #8"
      assert_select "span", :text => "Blocks #11"
    end
    assert_select "tr#issue-2 td.relations" do
      assert_select "span", 1
      assert_select "span", :text => "Blocked by #12"
    end
    assert_select "tr#issue-3 td.relations" do
      assert_select "span", 0
    end

    get :index, :set_filter => 1, :c => %w(relations), :format => 'csv'
    assert_response :success
    assert_equal 'text/csv; header=present', response.content_type
    lines = response.body.chomp.split("\n")
    assert_include '1,"Related to #7, Related to #8, Blocks #11"', lines
    assert_include '2,Blocked by #12', lines
    assert_include '3,""', lines

    get :index, :set_filter => 1, :c => %w(subject relations), :format => 'pdf'
    assert_response :success
    assert_equal 'application/pdf', response.content_type
  end

951 952 953 954
  def test_index_with_description_column
    get :index, :set_filter => 1, :c => %w(subject description)

    assert_select 'table.issues thead th', 3 # columns: chekbox + id + subject
jplang's avatar
jplang committed
955
    assert_select 'td.description[colspan="3"]', :text => 'Unable to print recipes'
956 957 958 959 960 961

    get :index, :set_filter => 1, :c => %w(subject description), :format => 'pdf'
    assert_response :success
    assert_equal 'application/pdf', response.content_type
  end

962 963 964 965 966 967 968 969 970 971 972
  def test_index_with_parent_column
    Issue.delete_all
    parent = Issue.generate!
    child = Issue.generate!(:parent_issue_id => parent.id)

    get :index, :c => %w(parent)

    assert_select 'td.parent', :text => "#{parent.tracker} ##{parent.id}"
    assert_select 'td.parent a[title=?]', parent.subject
  end

973 974 975 976 977 978 979 980
  def test_index_with_estimated_hours_total
    Issue.delete_all
    Issue.generate!(:estimated_hours => 5.5)
    Issue.generate!(:estimated_hours => 1.1)

    get :index, :t => %w(estimated_hours)
    assert_response :success
    assert_select '.query-totals'
981
    assert_select '.total-for-estimated-hours span.value', :text => '6.60'
982 983 984
    assert_select 'input[type=checkbox][name=?][value=estimated_hours][checked=checked]', 't[]'
  end

985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006
  def test_index_with_grouped_query_and_estimated_hours_total
    Issue.delete_all
    Issue.generate!(:estimated_hours => 5.5, :category_id => 1)
    Issue.generate!(:estimated_hours => 2.3, :category_id => 1)
    Issue.generate!(:estimated_hours => 1.1, :category_id => 2)
    Issue.generate!(:estimated_hours => 4.6)

    get :index, :t => %w(estimated_hours), :group_by => 'category'
    assert_response :success
    assert_select '.query-totals'
    assert_select '.query-totals .total-for-estimated-hours span.value', :text => '13.50'
    assert_select 'tr.group', :text => /Printing/ do
      assert_select '.total-for-estimated-hours span.value', :text => '7.80'
    end
    assert_select 'tr.group', :text => /Recipes/ do
      assert_select '.total-for-estimated-hours span.value', :text => '1.10'
    end
    assert_select 'tr.group', :text => /blank/ do
      assert_select '.total-for-estimated-hours span.value', :text => '4.60'
    end
  end

1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026
  def test_index_with_int_custom_field_total
    field = IssueCustomField.generate!(:field_format => 'int', :is_for_all => true)
    CustomValue.create!(:customized => Issue.find(1), :custom_field => field, :value => '2')
    CustomValue.create!(:customized => Issue.find(2), :custom_field => field, :value => '7')

    get :index, :t => ["cf_#{field.id}"]
    assert_response :success
    assert_select '.query-totals'
    assert_select ".total-for-cf-#{field.id} span.value", :text => '9'
  end

  def test_index_totals_should_default_to_settings
    with_settings :issue_list_default_totals => ['estimated_hours'] do
      get :index
      assert_response :success
      assert_select '.total-for-estimated-hours span.value'
      assert_select '.query-totals>span', 1
    end
  end

1027 1028 1029
  def test_index_send_html_if_query_is_invalid
    get :index, :f => ['start_date'], :op => {:start_date => '='}
    assert_equal 'text/html', @response.content_type
1030
    assert_select_error /Start date cannot be blank/i
1031 1032
  end

1033 1034 1035 1036 1037 1038
  def test_index_send_nothing_if_query_is_invalid
    get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv'
    assert_equal 'text/csv', @response.content_type
    assert @response.body.blank?
  end

1039 1040 1041
  def test_index_should_include_new_issue_link
    @request.session[:user_id] = 2
    get :index, :project_id => 1
1042
    assert_select '#content a.new-issue[href="/projects/ecookbook/issues/new"]', :text => 'New issue'
1043 1044 1045 1046 1047