##// END OF EJS Templates
Sort the issue list by author/assignee according to user display format (#9669)....
Jean-Philippe Lang -
r7818:5a1fcf826ffe
parent child
Show More
@@ -1,789 +1,793
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class QueryColumn
19 19 attr_accessor :name, :sortable, :groupable, :default_order
20 20 include Redmine::I18n
21 21
22 22 def initialize(name, options={})
23 23 self.name = name
24 24 self.sortable = options[:sortable]
25 25 self.groupable = options[:groupable] || false
26 26 if groupable == true
27 27 self.groupable = name.to_s
28 28 end
29 29 self.default_order = options[:default_order]
30 30 @caption_key = options[:caption] || "field_#{name}"
31 31 end
32 32
33 33 def caption
34 34 l(@caption_key)
35 35 end
36 36
37 37 # Returns true if the column is sortable, otherwise false
38 38 def sortable?
39 !sortable.nil?
39 !@sortable.nil?
40 end
41
42 def sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
40 44 end
41 45
42 46 def value(issue)
43 47 issue.send name
44 48 end
45 49
46 50 def css_classes
47 51 name
48 52 end
49 53 end
50 54
51 55 class QueryCustomFieldColumn < QueryColumn
52 56
53 57 def initialize(custom_field)
54 58 self.name = "cf_#{custom_field.id}".to_sym
55 59 self.sortable = custom_field.order_statement || false
56 60 if %w(list date bool int).include?(custom_field.field_format)
57 61 self.groupable = custom_field.order_statement
58 62 end
59 63 self.groupable ||= false
60 64 @cf = custom_field
61 65 end
62 66
63 67 def caption
64 68 @cf.name
65 69 end
66 70
67 71 def custom_field
68 72 @cf
69 73 end
70 74
71 75 def value(issue)
72 76 cv = issue.custom_values.detect {|v| v.custom_field_id == @cf.id}
73 77 cv && @cf.cast_value(cv.value)
74 78 end
75 79
76 80 def css_classes
77 81 @css_classes ||= "#{name} #{@cf.field_format}"
78 82 end
79 83 end
80 84
81 85 class Query < ActiveRecord::Base
82 86 class StatementInvalid < ::ActiveRecord::StatementInvalid
83 87 end
84 88
85 89 belongs_to :project
86 90 belongs_to :user
87 91 serialize :filters
88 92 serialize :column_names
89 93 serialize :sort_criteria, Array
90 94
91 95 attr_protected :project_id, :user_id
92 96
93 97 validates_presence_of :name, :on => :save
94 98 validates_length_of :name, :maximum => 255
95 99 validate :validate_query_filters
96 100
97 101 @@operators = { "=" => :label_equals,
98 102 "!" => :label_not_equals,
99 103 "o" => :label_open_issues,
100 104 "c" => :label_closed_issues,
101 105 "!*" => :label_none,
102 106 "*" => :label_all,
103 107 ">=" => :label_greater_or_equal,
104 108 "<=" => :label_less_or_equal,
105 109 "><" => :label_between,
106 110 "<t+" => :label_in_less_than,
107 111 ">t+" => :label_in_more_than,
108 112 "t+" => :label_in,
109 113 "t" => :label_today,
110 114 "w" => :label_this_week,
111 115 ">t-" => :label_less_than_ago,
112 116 "<t-" => :label_more_than_ago,
113 117 "t-" => :label_ago,
114 118 "~" => :label_contains,
115 119 "!~" => :label_not_contains }
116 120
117 121 cattr_reader :operators
118 122
119 123 @@operators_by_filter_type = { :list => [ "=", "!" ],
120 124 :list_status => [ "o", "=", "!", "c", "*" ],
121 125 :list_optional => [ "=", "!", "!*", "*" ],
122 126 :list_subprojects => [ "*", "!*", "=" ],
123 127 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
124 128 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
125 129 :string => [ "=", "~", "!", "!~" ],
126 130 :text => [ "~", "!~" ],
127 131 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
128 132 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
129 133
130 134 cattr_reader :operators_by_filter_type
131 135
132 136 @@available_columns = [
133 137 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
134 138 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
135 139 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
136 140 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
137 141 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
138 142 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
139 QueryColumn.new(:author, :sortable => ["authors.lastname", "authors.firstname", "authors.id"], :groupable => true),
140 QueryColumn.new(:assigned_to, :sortable => ["#{User.table_name}.lastname", "#{User.table_name}.firstname", "#{User.table_name}.id"], :groupable => true),
143 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
144 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
141 145 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
142 146 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
143 147 QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
144 148 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
145 149 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
146 150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
147 151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
148 152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
149 153 ]
150 154 cattr_reader :available_columns
151 155
152 156 named_scope :visible, lambda {|*args|
153 157 user = args.shift || User.current
154 158 base = Project.allowed_to_condition(user, :view_issues, *args)
155 159 user_id = user.logged? ? user.id : 0
156 160 {
157 161 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
158 162 :include => :project
159 163 }
160 164 }
161 165
162 166 def initialize(attributes = nil)
163 167 super attributes
164 168 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
165 169 end
166 170
167 171 def after_initialize
168 172 # Store the fact that project is nil (used in #editable_by?)
169 173 @is_for_all = project.nil?
170 174 end
171 175
172 176 def validate_query_filters
173 177 filters.each_key do |field|
174 178 if values_for(field)
175 179 case type_for(field)
176 180 when :integer
177 181 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
178 182 when :float
179 183 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
180 184 when :date, :date_past
181 185 case operator_for(field)
182 186 when "=", ">=", "<=", "><"
183 187 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
184 188 when ">t-", "<t-", "t-"
185 189 errors.add(label_for(field), :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
186 190 end
187 191 end
188 192 end
189 193
190 194 errors.add label_for(field), :blank unless
191 195 # filter requires one or more values
192 196 (values_for(field) and !values_for(field).first.blank?) or
193 197 # filter doesn't require any value
194 198 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
195 199 end if filters
196 200 end
197 201
198 202 # Returns true if the query is visible to +user+ or the current user.
199 203 def visible?(user=User.current)
200 204 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
201 205 end
202 206
203 207 def editable_by?(user)
204 208 return false unless user
205 209 # Admin can edit them all and regular users can edit their private queries
206 210 return true if user.admin? || (!is_public && self.user_id == user.id)
207 211 # Members can not edit public queries that are for all project (only admin is allowed to)
208 212 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
209 213 end
210 214
211 215 def available_filters
212 216 return @available_filters if @available_filters
213 217
214 218 trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
215 219
216 220 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
217 221 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
218 222 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
219 223 "subject" => { :type => :text, :order => 8 },
220 224 "created_on" => { :type => :date_past, :order => 9 },
221 225 "updated_on" => { :type => :date_past, :order => 10 },
222 226 "start_date" => { :type => :date, :order => 11 },
223 227 "due_date" => { :type => :date, :order => 12 },
224 228 "estimated_hours" => { :type => :float, :order => 13 },
225 229 "done_ratio" => { :type => :integer, :order => 14 }}
226 230
227 231 principals = []
228 232 if project
229 233 principals += project.principals.sort
230 234 else
231 235 all_projects = Project.visible.all
232 236 if all_projects.any?
233 237 # members of visible projects
234 238 principals += Principal.active.find(:all, :conditions => ["#{User.table_name}.id IN (SELECT DISTINCT user_id FROM members WHERE project_id IN (?))", all_projects.collect(&:id)]).sort
235 239
236 240 # project filter
237 241 project_values = []
238 242 Project.project_tree(all_projects) do |p, level|
239 243 prefix = (level > 0 ? ('--' * level + ' ') : '')
240 244 project_values << ["#{prefix}#{p.name}", p.id.to_s]
241 245 end
242 246 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
243 247 end
244 248 end
245 249 users = principals.select {|p| p.is_a?(User)}
246 250
247 251 assigned_to_values = []
248 252 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
249 253 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
250 254 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
251 255
252 256 author_values = []
253 257 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
254 258 author_values += users.collect{|s| [s.name, s.id.to_s] }
255 259 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
256 260
257 261 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
258 262 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
259 263
260 264 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
261 265 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
262 266
263 267 if User.current.logged?
264 268 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
265 269 end
266 270
267 271 if project
268 272 # project specific filters
269 273 categories = project.issue_categories.all
270 274 unless categories.empty?
271 275 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
272 276 end
273 277 versions = project.shared_versions.all
274 278 unless versions.empty?
275 279 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
276 280 end
277 281 unless project.leaf?
278 282 subprojects = project.descendants.visible.all
279 283 unless subprojects.empty?
280 284 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
281 285 end
282 286 end
283 287 add_custom_fields_filters(project.all_issue_custom_fields)
284 288 else
285 289 # global filters for cross project issue list
286 290 system_shared_versions = Version.visible.find_all_by_sharing('system')
287 291 unless system_shared_versions.empty?
288 292 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
289 293 end
290 294 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
291 295 end
292 296 @available_filters
293 297 end
294 298
295 299 def add_filter(field, operator, values)
296 300 # values must be an array
297 301 return unless values.nil? || values.is_a?(Array)
298 302 # check if field is defined as an available filter
299 303 if available_filters.has_key? field
300 304 filter_options = available_filters[field]
301 305 # check if operator is allowed for that filter
302 306 #if @@operators_by_filter_type[filter_options[:type]].include? operator
303 307 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
304 308 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
305 309 #end
306 310 filters[field] = {:operator => operator, :values => (values || [''])}
307 311 end
308 312 end
309 313
310 314 def add_short_filter(field, expression)
311 315 return unless expression && available_filters.has_key?(field)
312 316 field_type = available_filters[field][:type]
313 317 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
314 318 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
315 319 add_filter field, operator, $1.present? ? $1.split('|') : ['']
316 320 end || add_filter(field, '=', expression.split('|'))
317 321 end
318 322
319 323 # Add multiple filters using +add_filter+
320 324 def add_filters(fields, operators, values)
321 325 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
322 326 fields.each do |field|
323 327 add_filter(field, operators[field], values && values[field])
324 328 end
325 329 end
326 330 end
327 331
328 332 def has_filter?(field)
329 333 filters and filters[field]
330 334 end
331 335
332 336 def type_for(field)
333 337 available_filters[field][:type] if available_filters.has_key?(field)
334 338 end
335 339
336 340 def operator_for(field)
337 341 has_filter?(field) ? filters[field][:operator] : nil
338 342 end
339 343
340 344 def values_for(field)
341 345 has_filter?(field) ? filters[field][:values] : nil
342 346 end
343 347
344 348 def value_for(field, index=0)
345 349 (values_for(field) || [])[index]
346 350 end
347 351
348 352 def label_for(field)
349 353 label = available_filters[field][:name] if available_filters.has_key?(field)
350 354 label ||= field.gsub(/\_id$/, "")
351 355 end
352 356
353 357 def available_columns
354 358 return @available_columns if @available_columns
355 359 @available_columns = ::Query.available_columns
356 360 @available_columns += (project ?
357 361 project.all_issue_custom_fields :
358 362 IssueCustomField.find(:all)
359 363 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
360 364 end
361 365
362 366 def self.available_columns=(v)
363 367 self.available_columns = (v)
364 368 end
365 369
366 370 def self.add_available_column(column)
367 371 self.available_columns << (column) if column.is_a?(QueryColumn)
368 372 end
369 373
370 374 # Returns an array of columns that can be used to group the results
371 375 def groupable_columns
372 376 available_columns.select {|c| c.groupable}
373 377 end
374 378
375 379 # Returns a Hash of columns and the key for sorting
376 380 def sortable_columns
377 381 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
378 382 h[column.name.to_s] = column.sortable
379 383 h
380 384 })
381 385 end
382 386
383 387 def columns
384 388 # preserve the column_names order
385 389 (has_default_columns? ? default_columns_names : column_names).collect do |name|
386 390 available_columns.find { |col| col.name == name }
387 391 end.compact
388 392 end
389 393
390 394 def default_columns_names
391 395 @default_columns_names ||= begin
392 396 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
393 397
394 398 project.present? ? default_columns : [:project] | default_columns
395 399 end
396 400 end
397 401
398 402 def column_names=(names)
399 403 if names
400 404 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
401 405 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
402 406 # Set column_names to nil if default columns
403 407 if names == default_columns_names
404 408 names = nil
405 409 end
406 410 end
407 411 write_attribute(:column_names, names)
408 412 end
409 413
410 414 def has_column?(column)
411 415 column_names && column_names.include?(column.name)
412 416 end
413 417
414 418 def has_default_columns?
415 419 column_names.nil? || column_names.empty?
416 420 end
417 421
418 422 def sort_criteria=(arg)
419 423 c = []
420 424 if arg.is_a?(Hash)
421 425 arg = arg.keys.sort.collect {|k| arg[k]}
422 426 end
423 427 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
424 428 write_attribute(:sort_criteria, c)
425 429 end
426 430
427 431 def sort_criteria
428 432 read_attribute(:sort_criteria) || []
429 433 end
430 434
431 435 def sort_criteria_key(arg)
432 436 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
433 437 end
434 438
435 439 def sort_criteria_order(arg)
436 440 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
437 441 end
438 442
439 443 # Returns the SQL sort order that should be prepended for grouping
440 444 def group_by_sort_order
441 445 if grouped? && (column = group_by_column)
442 446 column.sortable.is_a?(Array) ?
443 447 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
444 448 "#{column.sortable} #{column.default_order}"
445 449 end
446 450 end
447 451
448 452 # Returns true if the query is a grouped query
449 453 def grouped?
450 454 !group_by_column.nil?
451 455 end
452 456
453 457 def group_by_column
454 458 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
455 459 end
456 460
457 461 def group_by_statement
458 462 group_by_column.try(:groupable)
459 463 end
460 464
461 465 def project_statement
462 466 project_clauses = []
463 467 if project && !project.descendants.active.empty?
464 468 ids = [project.id]
465 469 if has_filter?("subproject_id")
466 470 case operator_for("subproject_id")
467 471 when '='
468 472 # include the selected subprojects
469 473 ids += values_for("subproject_id").each(&:to_i)
470 474 when '!*'
471 475 # main project only
472 476 else
473 477 # all subprojects
474 478 ids += project.descendants.collect(&:id)
475 479 end
476 480 elsif Setting.display_subprojects_issues?
477 481 ids += project.descendants.collect(&:id)
478 482 end
479 483 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
480 484 elsif project
481 485 project_clauses << "#{Project.table_name}.id = %d" % project.id
482 486 end
483 487 project_clauses.any? ? project_clauses.join(' AND ') : nil
484 488 end
485 489
486 490 def statement
487 491 # filters clauses
488 492 filters_clauses = []
489 493 filters.each_key do |field|
490 494 next if field == "subproject_id"
491 495 v = values_for(field).clone
492 496 next unless v and !v.empty?
493 497 operator = operator_for(field)
494 498
495 499 # "me" value subsitution
496 500 if %w(assigned_to_id author_id watcher_id).include?(field)
497 501 if v.delete("me")
498 502 if User.current.logged?
499 503 v.push(User.current.id.to_s)
500 504 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
501 505 else
502 506 v.push("0")
503 507 end
504 508 end
505 509 end
506 510
507 511 if field =~ /^cf_(\d+)$/
508 512 # custom field
509 513 filters_clauses << sql_for_custom_field(field, operator, v, $1)
510 514 elsif respond_to?("sql_for_#{field}_field")
511 515 # specific statement
512 516 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
513 517 else
514 518 # regular field
515 519 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
516 520 end
517 521 end if filters and valid?
518 522
519 523 filters_clauses << project_statement
520 524 filters_clauses.reject!(&:blank?)
521 525
522 526 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
523 527 end
524 528
525 529 # Returns the issue count
526 530 def issue_count
527 531 Issue.visible.count(:include => [:status, :project], :conditions => statement)
528 532 rescue ::ActiveRecord::StatementInvalid => e
529 533 raise StatementInvalid.new(e.message)
530 534 end
531 535
532 536 # Returns the issue count by group or nil if query is not grouped
533 537 def issue_count_by_group
534 538 r = nil
535 539 if grouped?
536 540 begin
537 541 # Rails will raise an (unexpected) RecordNotFound if there's only a nil group value
538 542 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
539 543 rescue ActiveRecord::RecordNotFound
540 544 r = {nil => issue_count}
541 545 end
542 546 c = group_by_column
543 547 if c.is_a?(QueryCustomFieldColumn)
544 548 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
545 549 end
546 550 end
547 551 r
548 552 rescue ::ActiveRecord::StatementInvalid => e
549 553 raise StatementInvalid.new(e.message)
550 554 end
551 555
552 556 # Returns the issues
553 557 # Valid options are :order, :offset, :limit, :include, :conditions
554 558 def issues(options={})
555 559 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
556 560 order_option = nil if order_option.blank?
557 561
558 562 joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
559 563
560 564 Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
561 565 :conditions => statement,
562 566 :order => order_option,
563 567 :joins => joins,
564 568 :limit => options[:limit],
565 569 :offset => options[:offset]
566 570 rescue ::ActiveRecord::StatementInvalid => e
567 571 raise StatementInvalid.new(e.message)
568 572 end
569 573
570 574 # Returns the journals
571 575 # Valid options are :order, :offset, :limit
572 576 def journals(options={})
573 577 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
574 578 :conditions => statement,
575 579 :order => options[:order],
576 580 :limit => options[:limit],
577 581 :offset => options[:offset]
578 582 rescue ::ActiveRecord::StatementInvalid => e
579 583 raise StatementInvalid.new(e.message)
580 584 end
581 585
582 586 # Returns the versions
583 587 # Valid options are :conditions
584 588 def versions(options={})
585 589 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
586 590 rescue ::ActiveRecord::StatementInvalid => e
587 591 raise StatementInvalid.new(e.message)
588 592 end
589 593
590 594 def sql_for_watcher_id_field(field, operator, value)
591 595 db_table = Watcher.table_name
592 596 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
593 597 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
594 598 end
595 599
596 600 def sql_for_member_of_group_field(field, operator, value)
597 601 if operator == '*' # Any group
598 602 groups = Group.all
599 603 operator = '=' # Override the operator since we want to find by assigned_to
600 604 elsif operator == "!*"
601 605 groups = Group.all
602 606 operator = '!' # Override the operator since we want to find by assigned_to
603 607 else
604 608 groups = Group.find_all_by_id(value)
605 609 end
606 610 groups ||= []
607 611
608 612 members_of_groups = groups.inject([]) {|user_ids, group|
609 613 if group && group.user_ids.present?
610 614 user_ids << group.user_ids
611 615 end
612 616 user_ids.flatten.uniq.compact
613 617 }.sort.collect(&:to_s)
614 618
615 619 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
616 620 end
617 621
618 622 def sql_for_assigned_to_role_field(field, operator, value)
619 623 case operator
620 624 when "*", "!*" # Member / Not member
621 625 sw = operator == "!*" ? 'NOT' : ''
622 626 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
623 627 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
624 628 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
625 629 when "=", "!"
626 630 role_cond = value.any? ?
627 631 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
628 632 "1=0"
629 633
630 634 sw = operator == "!" ? 'NOT' : ''
631 635 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
632 636 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
633 637 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
634 638 end
635 639 end
636 640
637 641 private
638 642
639 643 def sql_for_custom_field(field, operator, value, custom_field_id)
640 644 db_table = CustomValue.table_name
641 645 db_field = 'value'
642 646 "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
643 647 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
644 648 end
645 649
646 650 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
647 651 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
648 652 sql = ''
649 653 case operator
650 654 when "="
651 655 if value.any?
652 656 case type_for(field)
653 657 when :date, :date_past
654 658 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
655 659 when :integer
656 660 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
657 661 when :float
658 662 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
659 663 else
660 664 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
661 665 end
662 666 else
663 667 # IN an empty set
664 668 sql = "1=0"
665 669 end
666 670 when "!"
667 671 if value.any?
668 672 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
669 673 else
670 674 # NOT IN an empty set
671 675 sql = "1=1"
672 676 end
673 677 when "!*"
674 678 sql = "#{db_table}.#{db_field} IS NULL"
675 679 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
676 680 when "*"
677 681 sql = "#{db_table}.#{db_field} IS NOT NULL"
678 682 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
679 683 when ">="
680 684 if [:date, :date_past].include?(type_for(field))
681 685 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
682 686 else
683 687 if is_custom_filter
684 688 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f}"
685 689 else
686 690 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
687 691 end
688 692 end
689 693 when "<="
690 694 if [:date, :date_past].include?(type_for(field))
691 695 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
692 696 else
693 697 if is_custom_filter
694 698 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f}"
695 699 else
696 700 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
697 701 end
698 702 end
699 703 when "><"
700 704 if [:date, :date_past].include?(type_for(field))
701 705 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
702 706 else
703 707 if is_custom_filter
704 708 sql = "CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
705 709 else
706 710 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
707 711 end
708 712 end
709 713 when "o"
710 714 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
711 715 when "c"
712 716 sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
713 717 when ">t-"
714 718 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
715 719 when "<t-"
716 720 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
717 721 when "t-"
718 722 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
719 723 when ">t+"
720 724 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
721 725 when "<t+"
722 726 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
723 727 when "t+"
724 728 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
725 729 when "t"
726 730 sql = relative_date_clause(db_table, db_field, 0, 0)
727 731 when "w"
728 732 first_day_of_week = l(:general_first_day_of_week).to_i
729 733 day_of_week = Date.today.cwday
730 734 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
731 735 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
732 736 when "~"
733 737 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
734 738 when "!~"
735 739 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
736 740 else
737 741 raise "Unknown query operator #{operator}"
738 742 end
739 743
740 744 return sql
741 745 end
742 746
743 747 def add_custom_fields_filters(custom_fields)
744 748 @available_filters ||= {}
745 749
746 750 custom_fields.select(&:is_filter?).each do |field|
747 751 case field.field_format
748 752 when "text"
749 753 options = { :type => :text, :order => 20 }
750 754 when "list"
751 755 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
752 756 when "date"
753 757 options = { :type => :date, :order => 20 }
754 758 when "bool"
755 759 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
756 760 when "int"
757 761 options = { :type => :integer, :order => 20 }
758 762 when "float"
759 763 options = { :type => :float, :order => 20 }
760 764 when "user", "version"
761 765 next unless project
762 766 options = { :type => :list_optional, :values => field.possible_values_options(project), :order => 20}
763 767 else
764 768 options = { :type => :string, :order => 20 }
765 769 end
766 770 @available_filters["cf_#{field.id}"] = options.merge({ :name => field.name })
767 771 end
768 772 end
769 773
770 774 # Returns a SQL clause for a date or datetime field.
771 775 def date_clause(table, field, from, to)
772 776 s = []
773 777 if from
774 778 from_yesterday = from - 1
775 779 from_yesterday_utc = Time.gm(from_yesterday.year, from_yesterday.month, from_yesterday.day)
776 780 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_utc.end_of_day)])
777 781 end
778 782 if to
779 783 to_utc = Time.gm(to.year, to.month, to.day)
780 784 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_utc.end_of_day)])
781 785 end
782 786 s.join(' AND ')
783 787 end
784 788
785 789 # Returns a SQL clause for a date or datetime field using relative dates.
786 790 def relative_date_clause(table, field, days_from, days_to)
787 791 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
788 792 end
789 793 end
@@ -1,619 +1,636
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/sha1"
19 19
20 20 class User < Principal
21 21 include Redmine::SafeAttributes
22 22
23 23 # Account statuses
24 24 STATUS_ANONYMOUS = 0
25 25 STATUS_ACTIVE = 1
26 26 STATUS_REGISTERED = 2
27 27 STATUS_LOCKED = 3
28 28
29 # Different ways of displaying/sorting users
29 30 USER_FORMATS = {
30 :firstname_lastname => '#{firstname} #{lastname}',
31 :firstname => '#{firstname}',
32 :lastname_firstname => '#{lastname} #{firstname}',
33 :lastname_coma_firstname => '#{lastname}, #{firstname}',
34 :username => '#{login}'
31 :firstname_lastname => {:string => '#{firstname} #{lastname}', :order => %w(firstname lastname id)},
32 :firstname => {:string => '#{firstname}', :order => %w(firstname id)},
33 :lastname_firstname => {:string => '#{lastname} #{firstname}', :order => %w(lastname firstname id)},
34 :lastname_coma_firstname => {:string => '#{lastname}, #{firstname}', :order => %w(lastname firstname id)},
35 :username => {:string => '#{login}', :order => %w(login id)},
35 36 }
36 37
37 38 MAIL_NOTIFICATION_OPTIONS = [
38 39 ['all', :label_user_mail_option_all],
39 40 ['selected', :label_user_mail_option_selected],
40 41 ['only_my_events', :label_user_mail_option_only_my_events],
41 42 ['only_assigned', :label_user_mail_option_only_assigned],
42 43 ['only_owner', :label_user_mail_option_only_owner],
43 44 ['none', :label_user_mail_option_none]
44 45 ]
45 46
46 47 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
47 48 :after_remove => Proc.new {|user, group| group.user_removed(user)}
48 49 has_many :changesets, :dependent => :nullify
49 50 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
50 51 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
51 52 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
52 53 belongs_to :auth_source
53 54
54 55 # Active non-anonymous users scope
55 56 named_scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
56 57
57 58 acts_as_customizable
58 59
59 60 attr_accessor :password, :password_confirmation
60 61 attr_accessor :last_before_login_on
61 62 # Prevents unauthorized assignments
62 63 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
63 64
64 65 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
65 66 validates_uniqueness_of :login, :if => Proc.new { |user| !user.login.blank? }, :case_sensitive => false
66 67 validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
67 68 # Login must contain lettres, numbers, underscores only
68 69 validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
69 70 validates_length_of :login, :maximum => 30
70 71 validates_length_of :firstname, :lastname, :maximum => 30
71 72 validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true
72 73 validates_length_of :mail, :maximum => 60, :allow_nil => true
73 74 validates_confirmation_of :password, :allow_nil => true
74 75 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
75 76 validate :validate_password_length
76 77
77 78 before_create :set_mail_notification
78 79 before_save :update_hashed_password
79 80 before_destroy :remove_references_before_destroy
80 81
81 82 named_scope :in_group, lambda {|group|
82 83 group_id = group.is_a?(Group) ? group.id : group.to_i
83 84 { :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
84 85 }
85 86 named_scope :not_in_group, lambda {|group|
86 87 group_id = group.is_a?(Group) ? group.id : group.to_i
87 88 { :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
88 89 }
89 90
90 91 def set_mail_notification
91 92 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
92 93 true
93 94 end
94 95
95 96 def update_hashed_password
96 97 # update hashed_password if password was set
97 98 if self.password && self.auth_source_id.blank?
98 99 salt_password(password)
99 100 end
100 101 end
101 102
102 103 def reload(*args)
103 104 @name = nil
104 105 @projects_by_role = nil
105 106 super
106 107 end
107 108
108 109 def mail=(arg)
109 110 write_attribute(:mail, arg.to_s.strip)
110 111 end
111 112
112 113 def identity_url=(url)
113 114 if url.blank?
114 115 write_attribute(:identity_url, '')
115 116 else
116 117 begin
117 118 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
118 119 rescue OpenIdAuthentication::InvalidOpenId
119 120 # Invlaid url, don't save
120 121 end
121 122 end
122 123 self.read_attribute(:identity_url)
123 124 end
124 125
125 126 # Returns the user that matches provided login and password, or nil
126 127 def self.try_to_login(login, password)
127 128 # Make sure no one can sign in with an empty password
128 129 return nil if password.to_s.empty?
129 130 user = find_by_login(login)
130 131 if user
131 132 # user is already in local database
132 133 return nil if !user.active?
133 134 if user.auth_source
134 135 # user has an external authentication method
135 136 return nil unless user.auth_source.authenticate(login, password)
136 137 else
137 138 # authentication with local password
138 139 return nil unless user.check_password?(password)
139 140 end
140 141 else
141 142 # user is not yet registered, try to authenticate with available sources
142 143 attrs = AuthSource.authenticate(login, password)
143 144 if attrs
144 145 user = new(attrs)
145 146 user.login = login
146 147 user.language = Setting.default_language
147 148 if user.save
148 149 user.reload
149 150 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
150 151 end
151 152 end
152 153 end
153 154 user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
154 155 user
155 156 rescue => text
156 157 raise text
157 158 end
158 159
159 160 # Returns the user who matches the given autologin +key+ or nil
160 161 def self.try_to_autologin(key)
161 162 tokens = Token.find_all_by_action_and_value('autologin', key)
162 163 # Make sure there's only 1 token that matches the key
163 164 if tokens.size == 1
164 165 token = tokens.first
165 166 if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
166 167 token.user.update_attribute(:last_login_on, Time.now)
167 168 token.user
168 169 end
169 170 end
170 171 end
171
172
173 def self.name_formatter(formatter = nil)
174 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
175 end
176
177 # Returns an array of fields names than can be used to make an order statement for users
178 # according to how user names are displayed
179 # Examples:
180 #
181 # User.fields_for_order_statement => ['users.login', 'users.id']
182 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
183 def self.fields_for_order_statement(table=nil)
184 table ||= table_name
185 name_formatter[:order].map {|field| "#{table}.#{field}"}
186 end
187
172 188 # Return user's full name for display
173 189 def name(formatter = nil)
190 f = self.class.name_formatter(formatter)
174 191 if formatter
175 eval('"' + (USER_FORMATS[formatter] || USER_FORMATS[:firstname_lastname]) + '"')
192 eval('"' + f[:string] + '"')
176 193 else
177 @name ||= eval('"' + (USER_FORMATS[Setting.user_format] || USER_FORMATS[:firstname_lastname]) + '"')
194 @name ||= eval('"' + f[:string] + '"')
178 195 end
179 196 end
180 197
181 198 def active?
182 199 self.status == STATUS_ACTIVE
183 200 end
184 201
185 202 def registered?
186 203 self.status == STATUS_REGISTERED
187 204 end
188 205
189 206 def locked?
190 207 self.status == STATUS_LOCKED
191 208 end
192 209
193 210 def activate
194 211 self.status = STATUS_ACTIVE
195 212 end
196 213
197 214 def register
198 215 self.status = STATUS_REGISTERED
199 216 end
200 217
201 218 def lock
202 219 self.status = STATUS_LOCKED
203 220 end
204 221
205 222 def activate!
206 223 update_attribute(:status, STATUS_ACTIVE)
207 224 end
208 225
209 226 def register!
210 227 update_attribute(:status, STATUS_REGISTERED)
211 228 end
212 229
213 230 def lock!
214 231 update_attribute(:status, STATUS_LOCKED)
215 232 end
216 233
217 234 # Returns true if +clear_password+ is the correct user's password, otherwise false
218 235 def check_password?(clear_password)
219 236 if auth_source_id.present?
220 237 auth_source.authenticate(self.login, clear_password)
221 238 else
222 239 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
223 240 end
224 241 end
225 242
226 243 # Generates a random salt and computes hashed_password for +clear_password+
227 244 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
228 245 def salt_password(clear_password)
229 246 self.salt = User.generate_salt
230 247 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
231 248 end
232 249
233 250 # Does the backend storage allow this user to change their password?
234 251 def change_password_allowed?
235 252 return true if auth_source_id.blank?
236 253 return auth_source.allow_password_changes?
237 254 end
238 255
239 256 # Generate and set a random password. Useful for automated user creation
240 257 # Based on Token#generate_token_value
241 258 #
242 259 def random_password
243 260 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
244 261 password = ''
245 262 40.times { |i| password << chars[rand(chars.size-1)] }
246 263 self.password = password
247 264 self.password_confirmation = password
248 265 self
249 266 end
250 267
251 268 def pref
252 269 self.preference ||= UserPreference.new(:user => self)
253 270 end
254 271
255 272 def time_zone
256 273 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
257 274 end
258 275
259 276 def wants_comments_in_reverse_order?
260 277 self.pref[:comments_sorting] == 'desc'
261 278 end
262 279
263 280 # Return user's RSS key (a 40 chars long string), used to access feeds
264 281 def rss_key
265 282 token = self.rss_token || Token.create(:user => self, :action => 'feeds')
266 283 token.value
267 284 end
268 285
269 286 # Return user's API key (a 40 chars long string), used to access the API
270 287 def api_key
271 288 token = self.api_token || self.create_api_token(:action => 'api')
272 289 token.value
273 290 end
274 291
275 292 # Return an array of project ids for which the user has explicitly turned mail notifications on
276 293 def notified_projects_ids
277 294 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
278 295 end
279 296
280 297 def notified_project_ids=(ids)
281 298 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
282 299 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
283 300 @notified_projects_ids = nil
284 301 notified_projects_ids
285 302 end
286 303
287 304 def valid_notification_options
288 305 self.class.valid_notification_options(self)
289 306 end
290 307
291 308 # Only users that belong to more than 1 project can select projects for which they are notified
292 309 def self.valid_notification_options(user=nil)
293 310 # Note that @user.membership.size would fail since AR ignores
294 311 # :include association option when doing a count
295 312 if user.nil? || user.memberships.length < 1
296 313 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
297 314 else
298 315 MAIL_NOTIFICATION_OPTIONS
299 316 end
300 317 end
301 318
302 319 # Find a user account by matching the exact login and then a case-insensitive
303 320 # version. Exact matches will be given priority.
304 321 def self.find_by_login(login)
305 322 # force string comparison to be case sensitive on MySQL
306 323 type_cast = (ActiveRecord::Base.connection.adapter_name == 'MySQL') ? 'BINARY' : ''
307 324
308 325 # First look for an exact match
309 326 user = first(:conditions => ["#{type_cast} login = ?", login])
310 327 # Fail over to case-insensitive if none was found
311 328 user ||= first(:conditions => ["#{type_cast} LOWER(login) = ?", login.to_s.downcase])
312 329 end
313 330
314 331 def self.find_by_rss_key(key)
315 332 token = Token.find_by_value(key)
316 333 token && token.user.active? ? token.user : nil
317 334 end
318 335
319 336 def self.find_by_api_key(key)
320 337 token = Token.find_by_action_and_value('api', key)
321 338 token && token.user.active? ? token.user : nil
322 339 end
323 340
324 341 # Makes find_by_mail case-insensitive
325 342 def self.find_by_mail(mail)
326 343 find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
327 344 end
328 345
329 346 def to_s
330 347 name
331 348 end
332 349
333 350 # Returns the current day according to user's time zone
334 351 def today
335 352 if time_zone.nil?
336 353 Date.today
337 354 else
338 355 Time.now.in_time_zone(time_zone).to_date
339 356 end
340 357 end
341 358
342 359 def logged?
343 360 true
344 361 end
345 362
346 363 def anonymous?
347 364 !logged?
348 365 end
349 366
350 367 # Return user's roles for project
351 368 def roles_for_project(project)
352 369 roles = []
353 370 # No role on archived projects
354 371 return roles unless project && project.active?
355 372 if logged?
356 373 # Find project membership
357 374 membership = memberships.detect {|m| m.project_id == project.id}
358 375 if membership
359 376 roles = membership.roles
360 377 else
361 378 @role_non_member ||= Role.non_member
362 379 roles << @role_non_member
363 380 end
364 381 else
365 382 @role_anonymous ||= Role.anonymous
366 383 roles << @role_anonymous
367 384 end
368 385 roles
369 386 end
370 387
371 388 # Return true if the user is a member of project
372 389 def member_of?(project)
373 390 !roles_for_project(project).detect {|role| role.member?}.nil?
374 391 end
375 392
376 393 # Returns a hash of user's projects grouped by roles
377 394 def projects_by_role
378 395 return @projects_by_role if @projects_by_role
379 396
380 397 @projects_by_role = Hash.new {|h,k| h[k]=[]}
381 398 memberships.each do |membership|
382 399 membership.roles.each do |role|
383 400 @projects_by_role[role] << membership.project if membership.project
384 401 end
385 402 end
386 403 @projects_by_role.each do |role, projects|
387 404 projects.uniq!
388 405 end
389 406
390 407 @projects_by_role
391 408 end
392 409
393 410 # Returns true if user is arg or belongs to arg
394 411 def is_or_belongs_to?(arg)
395 412 if arg.is_a?(User)
396 413 self == arg
397 414 elsif arg.is_a?(Group)
398 415 arg.users.include?(self)
399 416 else
400 417 false
401 418 end
402 419 end
403 420
404 421 # Return true if the user is allowed to do the specified action on a specific context
405 422 # Action can be:
406 423 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
407 424 # * a permission Symbol (eg. :edit_project)
408 425 # Context can be:
409 426 # * a project : returns true if user is allowed to do the specified action on this project
410 427 # * an array of projects : returns true if user is allowed on every project
411 428 # * nil with options[:global] set : check if user has at least one role allowed for this action,
412 429 # or falls back to Non Member / Anonymous permissions depending if the user is logged
413 430 def allowed_to?(action, context, options={}, &block)
414 431 if context && context.is_a?(Project)
415 432 # No action allowed on archived projects
416 433 return false unless context.active?
417 434 # No action allowed on disabled modules
418 435 return false unless context.allows_to?(action)
419 436 # Admin users are authorized for anything else
420 437 return true if admin?
421 438
422 439 roles = roles_for_project(context)
423 440 return false unless roles
424 441 roles.detect {|role|
425 442 (context.is_public? || role.member?) &&
426 443 role.allowed_to?(action) &&
427 444 (block_given? ? yield(role, self) : true)
428 445 }
429 446 elsif context && context.is_a?(Array)
430 447 # Authorize if user is authorized on every element of the array
431 448 context.map do |project|
432 449 allowed_to?(action, project, options, &block)
433 450 end.inject do |memo,allowed|
434 451 memo && allowed
435 452 end
436 453 elsif options[:global]
437 454 # Admin users are always authorized
438 455 return true if admin?
439 456
440 457 # authorize if user has at least one role that has this permission
441 458 roles = memberships.collect {|m| m.roles}.flatten.uniq
442 459 roles << (self.logged? ? Role.non_member : Role.anonymous)
443 460 roles.detect {|role|
444 461 role.allowed_to?(action) &&
445 462 (block_given? ? yield(role, self) : true)
446 463 }
447 464 else
448 465 false
449 466 end
450 467 end
451 468
452 469 # Is the user allowed to do the specified action on any project?
453 470 # See allowed_to? for the actions and valid options.
454 471 def allowed_to_globally?(action, options, &block)
455 472 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
456 473 end
457 474
458 475 safe_attributes 'login',
459 476 'firstname',
460 477 'lastname',
461 478 'mail',
462 479 'mail_notification',
463 480 'language',
464 481 'custom_field_values',
465 482 'custom_fields',
466 483 'identity_url'
467 484
468 485 safe_attributes 'status',
469 486 'auth_source_id',
470 487 :if => lambda {|user, current_user| current_user.admin?}
471 488
472 489 safe_attributes 'group_ids',
473 490 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
474 491
475 492 # Utility method to help check if a user should be notified about an
476 493 # event.
477 494 #
478 495 # TODO: only supports Issue events currently
479 496 def notify_about?(object)
480 497 case mail_notification
481 498 when 'all'
482 499 true
483 500 when 'selected'
484 501 # user receives notifications for created/assigned issues on unselected projects
485 502 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to))
486 503 true
487 504 else
488 505 false
489 506 end
490 507 when 'none'
491 508 false
492 509 when 'only_my_events'
493 510 if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to))
494 511 true
495 512 else
496 513 false
497 514 end
498 515 when 'only_assigned'
499 516 if object.is_a?(Issue) && is_or_belongs_to?(object.assigned_to)
500 517 true
501 518 else
502 519 false
503 520 end
504 521 when 'only_owner'
505 522 if object.is_a?(Issue) && object.author == self
506 523 true
507 524 else
508 525 false
509 526 end
510 527 else
511 528 false
512 529 end
513 530 end
514 531
515 532 def self.current=(user)
516 533 @current_user = user
517 534 end
518 535
519 536 def self.current
520 537 @current_user ||= User.anonymous
521 538 end
522 539
523 540 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
524 541 # one anonymous user per database.
525 542 def self.anonymous
526 543 anonymous_user = AnonymousUser.find(:first)
527 544 if anonymous_user.nil?
528 545 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
529 546 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
530 547 end
531 548 anonymous_user
532 549 end
533 550
534 551 # Salts all existing unsalted passwords
535 552 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
536 553 # This method is used in the SaltPasswords migration and is to be kept as is
537 554 def self.salt_unsalted_passwords!
538 555 transaction do
539 556 User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
540 557 next if user.hashed_password.blank?
541 558 salt = User.generate_salt
542 559 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
543 560 User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
544 561 end
545 562 end
546 563 end
547 564
548 565 protected
549 566
550 567 def validate_password_length
551 568 # Password length validation based on setting
552 569 if !password.nil? && password.size < Setting.password_min_length.to_i
553 570 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
554 571 end
555 572 end
556 573
557 574 private
558 575
559 576 # Removes references that are not handled by associations
560 577 # Things that are not deleted are reassociated with the anonymous user
561 578 def remove_references_before_destroy
562 579 return if self.id.nil?
563 580
564 581 substitute = User.anonymous
565 582 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
566 583 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
567 584 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
568 585 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
569 586 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
570 587 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
571 588 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
572 589 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
573 590 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
574 591 # Remove private queries and keep public ones
575 592 Query.delete_all ['user_id = ? AND is_public = ?', id, false]
576 593 Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
577 594 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
578 595 Token.delete_all ['user_id = ?', id]
579 596 Watcher.delete_all ['user_id = ?', id]
580 597 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
581 598 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
582 599 end
583 600
584 601 # Return password digest
585 602 def self.hash_password(clear_password)
586 603 Digest::SHA1.hexdigest(clear_password || "")
587 604 end
588 605
589 606 # Returns a 128bits random salt as a hex string (32 chars long)
590 607 def self.generate_salt
591 608 ActiveSupport::SecureRandom.hex(16)
592 609 end
593 610
594 611 end
595 612
596 613 class AnonymousUser < User
597 614
598 615 def validate_on_create
599 616 # There should be only one AnonymousUser in the database
600 617 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first)
601 618 end
602 619
603 620 def available_custom_fields
604 621 []
605 622 end
606 623
607 624 # Overrides a few properties
608 625 def logged?; false end
609 626 def admin; false end
610 627 def name(*args); I18n.t(:label_user_anonymous) end
611 628 def mail; nil end
612 629 def time_zone; nil end
613 630 def rss_key; nil end
614 631
615 632 # Anonymous user can not be destroyed
616 633 def destroy
617 634 false
618 635 end
619 636 end
@@ -1,1868 +1,1896
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19 require 'issues_controller'
20 20
21 21 class IssuesControllerTest < ActionController::TestCase
22 22 fixtures :projects,
23 23 :users,
24 24 :roles,
25 25 :members,
26 26 :member_roles,
27 27 :issues,
28 28 :issue_statuses,
29 29 :versions,
30 30 :trackers,
31 31 :projects_trackers,
32 32 :issue_categories,
33 33 :enabled_modules,
34 34 :enumerations,
35 35 :attachments,
36 36 :workflows,
37 37 :custom_fields,
38 38 :custom_values,
39 39 :custom_fields_projects,
40 40 :custom_fields_trackers,
41 41 :time_entries,
42 42 :journals,
43 43 :journal_details,
44 44 :queries
45 45
46 46 def setup
47 47 @controller = IssuesController.new
48 48 @request = ActionController::TestRequest.new
49 49 @response = ActionController::TestResponse.new
50 50 User.current = nil
51 51 end
52 52
53 53 def test_index
54 54 Setting.default_language = 'en'
55 55
56 56 get :index
57 57 assert_response :success
58 58 assert_template 'index'
59 59 assert_not_nil assigns(:issues)
60 60 assert_nil assigns(:project)
61 61 assert_tag :tag => 'a', :content => /Can't print recipes/
62 62 assert_tag :tag => 'a', :content => /Subproject issue/
63 63 # private projects hidden
64 64 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
65 65 assert_no_tag :tag => 'a', :content => /Issue on project 2/
66 66 # project column
67 67 assert_tag :tag => 'th', :content => /Project/
68 68 end
69 69
70 70 def test_index_should_not_list_issues_when_module_disabled
71 71 EnabledModule.delete_all("name = 'issue_tracking' AND project_id = 1")
72 72 get :index
73 73 assert_response :success
74 74 assert_template 'index'
75 75 assert_not_nil assigns(:issues)
76 76 assert_nil assigns(:project)
77 77 assert_no_tag :tag => 'a', :content => /Can't print recipes/
78 78 assert_tag :tag => 'a', :content => /Subproject issue/
79 79 end
80 80
81 81 def test_index_should_list_visible_issues_only
82 82 get :index, :per_page => 100
83 83 assert_response :success
84 84 assert_not_nil assigns(:issues)
85 85 assert_nil assigns(:issues).detect {|issue| !issue.visible?}
86 86 end
87 87
88 88 def test_index_with_project
89 89 Setting.display_subprojects_issues = 0
90 90 get :index, :project_id => 1
91 91 assert_response :success
92 92 assert_template 'index'
93 93 assert_not_nil assigns(:issues)
94 94 assert_tag :tag => 'a', :content => /Can't print recipes/
95 95 assert_no_tag :tag => 'a', :content => /Subproject issue/
96 96 end
97 97
98 98 def test_index_with_project_and_subprojects
99 99 Setting.display_subprojects_issues = 1
100 100 get :index, :project_id => 1
101 101 assert_response :success
102 102 assert_template 'index'
103 103 assert_not_nil assigns(:issues)
104 104 assert_tag :tag => 'a', :content => /Can't print recipes/
105 105 assert_tag :tag => 'a', :content => /Subproject issue/
106 106 assert_no_tag :tag => 'a', :content => /Issue of a private subproject/
107 107 end
108 108
109 109 def test_index_with_project_and_subprojects_should_show_private_subprojects
110 110 @request.session[:user_id] = 2
111 111 Setting.display_subprojects_issues = 1
112 112 get :index, :project_id => 1
113 113 assert_response :success
114 114 assert_template 'index'
115 115 assert_not_nil assigns(:issues)
116 116 assert_tag :tag => 'a', :content => /Can't print recipes/
117 117 assert_tag :tag => 'a', :content => /Subproject issue/
118 118 assert_tag :tag => 'a', :content => /Issue of a private subproject/
119 119 end
120 120
121 121 def test_index_with_project_and_default_filter
122 122 get :index, :project_id => 1, :set_filter => 1
123 123 assert_response :success
124 124 assert_template 'index'
125 125 assert_not_nil assigns(:issues)
126 126
127 127 query = assigns(:query)
128 128 assert_not_nil query
129 129 # default filter
130 130 assert_equal({'status_id' => {:operator => 'o', :values => ['']}}, query.filters)
131 131 end
132 132
133 133 def test_index_with_project_and_filter
134 134 get :index, :project_id => 1, :set_filter => 1,
135 135 :f => ['tracker_id'],
136 136 :op => {'tracker_id' => '='},
137 137 :v => {'tracker_id' => ['1']}
138 138 assert_response :success
139 139 assert_template 'index'
140 140 assert_not_nil assigns(:issues)
141 141
142 142 query = assigns(:query)
143 143 assert_not_nil query
144 144 assert_equal({'tracker_id' => {:operator => '=', :values => ['1']}}, query.filters)
145 145 end
146 146
147 147 def test_index_with_short_filters
148 148
149 149 to_test = {
150 150 'status_id' => {
151 151 'o' => { :op => 'o', :values => [''] },
152 152 'c' => { :op => 'c', :values => [''] },
153 153 '7' => { :op => '=', :values => ['7'] },
154 154 '7|3|4' => { :op => '=', :values => ['7', '3', '4'] },
155 155 '=7' => { :op => '=', :values => ['7'] },
156 156 '!3' => { :op => '!', :values => ['3'] },
157 157 '!7|3|4' => { :op => '!', :values => ['7', '3', '4'] }},
158 158 'subject' => {
159 159 'This is a subject' => { :op => '=', :values => ['This is a subject'] },
160 160 'o' => { :op => '=', :values => ['o'] },
161 161 '~This is part of a subject' => { :op => '~', :values => ['This is part of a subject'] },
162 162 '!~This is part of a subject' => { :op => '!~', :values => ['This is part of a subject'] }},
163 163 'tracker_id' => {
164 164 '3' => { :op => '=', :values => ['3'] },
165 165 '=3' => { :op => '=', :values => ['3'] }},
166 166 'start_date' => {
167 167 '2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
168 168 '=2011-10-12' => { :op => '=', :values => ['2011-10-12'] },
169 169 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
170 170 '<=2011-10-12' => { :op => '<=', :values => ['2011-10-12'] },
171 171 '><2011-10-01|2011-10-30' => { :op => '><', :values => ['2011-10-01', '2011-10-30'] },
172 172 '<t+2' => { :op => '<t+', :values => ['2'] },
173 173 '>t+2' => { :op => '>t+', :values => ['2'] },
174 174 't+2' => { :op => 't+', :values => ['2'] },
175 175 't' => { :op => 't', :values => [''] },
176 176 'w' => { :op => 'w', :values => [''] },
177 177 '>t-2' => { :op => '>t-', :values => ['2'] },
178 178 '<t-2' => { :op => '<t-', :values => ['2'] },
179 179 't-2' => { :op => 't-', :values => ['2'] }},
180 180 'created_on' => {
181 181 '>=2011-10-12' => { :op => '>=', :values => ['2011-10-12'] },
182 182 '<t+2' => { :op => '=', :values => ['<t+2'] },
183 183 '>t+2' => { :op => '=', :values => ['>t+2'] },
184 184 't+2' => { :op => 't', :values => ['+2'] }},
185 185 'cf_1' => {
186 186 'c' => { :op => '=', :values => ['c'] },
187 187 '!c' => { :op => '!', :values => ['c'] },
188 188 '!*' => { :op => '!*', :values => [''] },
189 189 '*' => { :op => '*', :values => [''] }},
190 190 'estimated_hours' => {
191 191 '=13.4' => { :op => '=', :values => ['13.4'] },
192 192 '>=45' => { :op => '>=', :values => ['45'] },
193 193 '<=125' => { :op => '<=', :values => ['125'] },
194 194 '><10.5|20.5' => { :op => '><', :values => ['10.5', '20.5'] },
195 195 '!*' => { :op => '!*', :values => [''] },
196 196 '*' => { :op => '*', :values => [''] }}
197 197 }
198 198
199 199 default_filter = { 'status_id' => {:operator => 'o', :values => [''] }}
200 200
201 201 to_test.each do |field, expression_and_expected|
202 202 expression_and_expected.each do |filter_expression, expected|
203 203
204 204 get :index, :set_filter => 1, field => filter_expression
205 205
206 206 assert_response :success
207 207 assert_template 'index'
208 208 assert_not_nil assigns(:issues)
209 209
210 210 query = assigns(:query)
211 211 assert_not_nil query
212 212 assert query.has_filter?(field)
213 213 assert_equal(default_filter.merge({field => {:operator => expected[:op], :values => expected[:values]}}), query.filters)
214 214 end
215 215 end
216 216
217 217 end
218 218
219 219 def test_index_with_project_and_empty_filters
220 220 get :index, :project_id => 1, :set_filter => 1, :fields => ['']
221 221 assert_response :success
222 222 assert_template 'index'
223 223 assert_not_nil assigns(:issues)
224 224
225 225 query = assigns(:query)
226 226 assert_not_nil query
227 227 # no filter
228 228 assert_equal({}, query.filters)
229 229 end
230 230
231 231 def test_index_with_query
232 232 get :index, :project_id => 1, :query_id => 5
233 233 assert_response :success
234 234 assert_template 'index'
235 235 assert_not_nil assigns(:issues)
236 236 assert_nil assigns(:issue_count_by_group)
237 237 end
238 238
239 239 def test_index_with_query_grouped_by_tracker
240 240 get :index, :project_id => 1, :query_id => 6
241 241 assert_response :success
242 242 assert_template 'index'
243 243 assert_not_nil assigns(:issues)
244 244 assert_not_nil assigns(:issue_count_by_group)
245 245 end
246 246
247 247 def test_index_with_query_grouped_by_list_custom_field
248 248 get :index, :project_id => 1, :query_id => 9
249 249 assert_response :success
250 250 assert_template 'index'
251 251 assert_not_nil assigns(:issues)
252 252 assert_not_nil assigns(:issue_count_by_group)
253 253 end
254 254
255 255 def test_private_query_should_not_be_available_to_other_users
256 256 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
257 257 @request.session[:user_id] = 3
258 258
259 259 get :index, :query_id => q.id
260 260 assert_response 403
261 261 end
262 262
263 263 def test_private_query_should_be_available_to_its_user
264 264 q = Query.create!(:name => "private", :user => User.find(2), :is_public => false, :project => nil)
265 265 @request.session[:user_id] = 2
266 266
267 267 get :index, :query_id => q.id
268 268 assert_response :success
269 269 end
270 270
271 271 def test_public_query_should_be_available_to_other_users
272 272 q = Query.create!(:name => "private", :user => User.find(2), :is_public => true, :project => nil)
273 273 @request.session[:user_id] = 3
274 274
275 275 get :index, :query_id => q.id
276 276 assert_response :success
277 277 end
278 278
279 def test_index_sort_by_field_not_included_in_columns
280 Setting.issue_list_default_columns = %w(subject author)
281 get :index, :sort => 'tracker'
282 end
283
284 279 def test_index_csv
285 280 get :index, :format => 'csv'
286 281 assert_response :success
287 282 assert_not_nil assigns(:issues)
288 283 assert_equal 'text/csv', @response.content_type
289 284 assert @response.body.starts_with?("#,")
290 285 lines = @response.body.chomp.split("\n")
291 286 assert_equal assigns(:query).columns.size + 1, lines[0].split(',').size
292 287 end
293 288
294 289 def test_index_csv_with_project
295 290 get :index, :project_id => 1, :format => 'csv'
296 291 assert_response :success
297 292 assert_not_nil assigns(:issues)
298 293 assert_equal 'text/csv', @response.content_type
299 294 end
300 295
301 296 def test_index_csv_with_description
302 297 get :index, :format => 'csv', :description => '1'
303 298 assert_response :success
304 299 assert_not_nil assigns(:issues)
305 300 assert_equal 'text/csv', @response.content_type
306 301 assert @response.body.starts_with?("#,")
307 302 lines = @response.body.chomp.split("\n")
308 303 assert_equal assigns(:query).columns.size + 2, lines[0].split(',').size
309 304 end
310 305
311 306 def test_index_csv_with_all_columns
312 307 get :index, :format => 'csv', :columns => 'all'
313 308 assert_response :success
314 309 assert_not_nil assigns(:issues)
315 310 assert_equal 'text/csv', @response.content_type
316 311 assert @response.body.starts_with?("#,")
317 312 lines = @response.body.chomp.split("\n")
318 313 assert_equal assigns(:query).available_columns.size + 1, lines[0].split(',').size
319 314 end
320 315
321 316 def test_index_csv_big_5
322 317 with_settings :default_language => "zh-TW" do
323 318 str_utf8 = "\xe4\xb8\x80\xe6\x9c\x88"
324 319 str_big5 = "\xa4@\xa4\xeb"
325 320 if str_utf8.respond_to?(:force_encoding)
326 321 str_utf8.force_encoding('UTF-8')
327 322 str_big5.force_encoding('Big5')
328 323 end
329 324 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
330 325 :status_id => 1, :priority => IssuePriority.all.first,
331 326 :subject => str_utf8)
332 327 assert issue.save
333 328
334 329 get :index, :project_id => 1,
335 330 :f => ['subject'],
336 331 :op => '=', :values => [str_utf8],
337 332 :format => 'csv'
338 333 assert_equal 'text/csv', @response.content_type
339 334 lines = @response.body.chomp.split("\n")
340 335 s1 = "\xaa\xac\xbaA"
341 336 if str_utf8.respond_to?(:force_encoding)
342 337 s1.force_encoding('Big5')
343 338 end
344 339 assert lines[0].include?(s1)
345 340 assert lines[1].include?(str_big5)
346 341 end
347 342 end
348 343
349 344 def test_index_csv_cannot_convert_should_be_replaced_big_5
350 345 with_settings :default_language => "zh-TW" do
351 346 str_utf8 = "\xe4\xbb\xa5\xe5\x86\x85"
352 347 if str_utf8.respond_to?(:force_encoding)
353 348 str_utf8.force_encoding('UTF-8')
354 349 end
355 350 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
356 351 :status_id => 1, :priority => IssuePriority.all.first,
357 352 :subject => str_utf8)
358 353 assert issue.save
359 354
360 355 get :index, :project_id => 1,
361 356 :f => ['subject'],
362 357 :op => '=', :values => [str_utf8],
363 358 :c => ['status', 'subject'],
364 359 :format => 'csv',
365 360 :set_filter => 1
366 361 assert_equal 'text/csv', @response.content_type
367 362 lines = @response.body.chomp.split("\n")
368 363 s1 = "\xaa\xac\xbaA" # status
369 364 if str_utf8.respond_to?(:force_encoding)
370 365 s1.force_encoding('Big5')
371 366 end
372 367 assert lines[0].include?(s1)
373 368 s2 = lines[1].split(",")[2]
374 369 if s1.respond_to?(:force_encoding)
375 370 s3 = "\xa5H?" # subject
376 371 s3.force_encoding('Big5')
377 372 assert_equal s3, s2
378 373 elsif RUBY_PLATFORM == 'java'
379 374 assert_equal "??", s2
380 375 else
381 376 assert_equal "\xa5H???", s2
382 377 end
383 378 end
384 379 end
385 380
386 381 def test_index_pdf
387 382 get :index, :format => 'pdf'
388 383 assert_response :success
389 384 assert_not_nil assigns(:issues)
390 385 assert_equal 'application/pdf', @response.content_type
391 386
392 387 get :index, :project_id => 1, :format => 'pdf'
393 388 assert_response :success
394 389 assert_not_nil assigns(:issues)
395 390 assert_equal 'application/pdf', @response.content_type
396 391
397 392 get :index, :project_id => 1, :query_id => 6, :format => 'pdf'
398 393 assert_response :success
399 394 assert_not_nil assigns(:issues)
400 395 assert_equal 'application/pdf', @response.content_type
401 396 end
402 397
403 398 def test_index_pdf_with_query_grouped_by_list_custom_field
404 399 get :index, :project_id => 1, :query_id => 9, :format => 'pdf'
405 400 assert_response :success
406 401 assert_not_nil assigns(:issues)
407 402 assert_not_nil assigns(:issue_count_by_group)
408 403 assert_equal 'application/pdf', @response.content_type
409 404 end
410 405
411 406 def test_index_sort
412 407 get :index, :sort => 'tracker,id:desc'
413 408 assert_response :success
414 409
415 410 sort_params = @request.session['issues_index_sort']
416 411 assert sort_params.is_a?(String)
417 412 assert_equal 'tracker,id:desc', sort_params
418 413
419 414 issues = assigns(:issues)
420 415 assert_not_nil issues
421 416 assert !issues.empty?
422 417 assert_equal issues.sort {|a,b| a.tracker == b.tracker ? b.id <=> a.id : a.tracker <=> b.tracker }.collect(&:id), issues.collect(&:id)
423 418 end
419
420 def test_index_sort_by_field_not_included_in_columns
421 Setting.issue_list_default_columns = %w(subject author)
422 get :index, :sort => 'tracker'
423 end
424
425 def test_index_sort_by_assigned_to
426 get :index, :sort => 'assigned_to'
427 assert_response :success
428 assignees = assigns(:issues).collect(&:assigned_to).compact
429 assert_equal assignees.sort, assignees
430 end
431
432 def test_index_sort_by_assigned_to_desc
433 get :index, :sort => 'assigned_to:desc'
434 assert_response :success
435 assignees = assigns(:issues).collect(&:assigned_to).compact
436 assert_equal assignees.sort.reverse, assignees
437 end
438
439 def test_index_group_by_assigned_to
440 get :index, :group_by => 'assigned_to', :sort => 'priority'
441 assert_response :success
442 end
424 443
425 444 def test_index_sort_by_author
426 445 get :index, :sort => 'author'
427 446 assert_response :success
447 authors = assigns(:issues).collect(&:author)
448 assert_equal authors.sort, authors
449 end
450
451 def test_index_sort_by_author_desc
452 get :index, :sort => 'author:desc'
453 assert_response :success
454 authors = assigns(:issues).collect(&:author)
455 assert_equal authors.sort.reverse, authors
428 456 end
429 457
430 458 def test_index_group_by_author
431 459 get :index, :group_by => 'author', :sort => 'priority'
432 460 assert_response :success
433 461 end
434 462
435 463 def test_index_with_columns
436 464 columns = ['tracker', 'subject', 'assigned_to']
437 465 get :index, :set_filter => 1, :c => columns
438 466 assert_response :success
439 467
440 468 # query should use specified columns
441 469 query = assigns(:query)
442 470 assert_kind_of Query, query
443 471 assert_equal columns, query.column_names.map(&:to_s)
444 472
445 473 # columns should be stored in session
446 474 assert_kind_of Hash, session[:query]
447 475 assert_kind_of Array, session[:query][:column_names]
448 476 assert_equal columns, session[:query][:column_names].map(&:to_s)
449 477
450 478 # ensure only these columns are kept in the selected columns list
451 479 assert_tag :tag => 'select', :attributes => { :id => 'selected_columns' },
452 480 :children => { :count => 3 }
453 481 assert_no_tag :tag => 'option', :attributes => { :value => 'project' },
454 482 :parent => { :tag => 'select', :attributes => { :id => "selected_columns" } }
455 483 end
456 484
457 485 def test_index_without_project_should_implicitly_add_project_column_to_default_columns
458 486 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
459 487 get :index, :set_filter => 1
460 488
461 489 # query should use specified columns
462 490 query = assigns(:query)
463 491 assert_kind_of Query, query
464 492 assert_equal [:project, :tracker, :subject, :assigned_to], query.columns.map(&:name)
465 493 end
466 494
467 495 def test_index_without_project_and_explicit_default_columns_should_not_add_project_column
468 496 Setting.issue_list_default_columns = ['tracker', 'subject', 'assigned_to']
469 497 columns = ['tracker', 'subject', 'assigned_to']
470 498 get :index, :set_filter => 1, :c => columns
471 499
472 500 # query should use specified columns
473 501 query = assigns(:query)
474 502 assert_kind_of Query, query
475 503 assert_equal columns.map(&:to_sym), query.columns.map(&:name)
476 504 end
477 505
478 506 def test_index_with_custom_field_column
479 507 columns = %w(tracker subject cf_2)
480 508 get :index, :set_filter => 1, :c => columns
481 509 assert_response :success
482 510
483 511 # query should use specified columns
484 512 query = assigns(:query)
485 513 assert_kind_of Query, query
486 514 assert_equal columns, query.column_names.map(&:to_s)
487 515
488 516 assert_tag :td,
489 517 :attributes => {:class => 'cf_2 string'},
490 518 :ancestor => {:tag => 'table', :attributes => {:class => /issues/}}
491 519 end
492 520
493 521 def test_index_send_html_if_query_is_invalid
494 522 get :index, :f => ['start_date'], :op => {:start_date => '='}
495 523 assert_equal 'text/html', @response.content_type
496 524 assert_template 'index'
497 525 end
498 526
499 527 def test_index_send_nothing_if_query_is_invalid
500 528 get :index, :f => ['start_date'], :op => {:start_date => '='}, :format => 'csv'
501 529 assert_equal 'text/csv', @response.content_type
502 530 assert @response.body.blank?
503 531 end
504 532
505 533 def test_show_by_anonymous
506 534 get :show, :id => 1
507 535 assert_response :success
508 536 assert_template 'show'
509 537 assert_not_nil assigns(:issue)
510 538 assert_equal Issue.find(1), assigns(:issue)
511 539
512 540 # anonymous role is allowed to add a note
513 541 assert_tag :tag => 'form',
514 542 :descendant => { :tag => 'fieldset',
515 543 :child => { :tag => 'legend',
516 544 :content => /Notes/ } }
517 545 assert_tag :tag => 'title',
518 546 :content => "Bug #1: Can't print recipes - eCookbook - Redmine"
519 547 end
520 548
521 549 def test_show_by_manager
522 550 @request.session[:user_id] = 2
523 551 get :show, :id => 1
524 552 assert_response :success
525 553
526 554 assert_tag :tag => 'a',
527 555 :content => /Quote/
528 556
529 557 assert_tag :tag => 'form',
530 558 :descendant => { :tag => 'fieldset',
531 559 :child => { :tag => 'legend',
532 560 :content => /Change properties/ } },
533 561 :descendant => { :tag => 'fieldset',
534 562 :child => { :tag => 'legend',
535 563 :content => /Log time/ } },
536 564 :descendant => { :tag => 'fieldset',
537 565 :child => { :tag => 'legend',
538 566 :content => /Notes/ } }
539 567 end
540 568
541 569 def test_update_form_should_not_display_inactive_enumerations
542 570 @request.session[:user_id] = 2
543 571 get :show, :id => 1
544 572 assert_response :success
545 573
546 574 assert ! IssuePriority.find(15).active?
547 575 assert_no_tag :option, :attributes => {:value => '15'},
548 576 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
549 577 end
550 578
551 579 def test_update_form_should_allow_attachment_upload
552 580 @request.session[:user_id] = 2
553 581 get :show, :id => 1
554 582
555 583 assert_tag :tag => 'form',
556 584 :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'},
557 585 :descendant => {
558 586 :tag => 'input',
559 587 :attributes => {:type => 'file', :name => 'attachments[1][file]'}
560 588 }
561 589 end
562 590
563 591 def test_show_should_deny_anonymous_access_without_permission
564 592 Role.anonymous.remove_permission!(:view_issues)
565 593 get :show, :id => 1
566 594 assert_response :redirect
567 595 end
568 596
569 597 def test_show_should_deny_anonymous_access_to_private_issue
570 598 Issue.update_all(["is_private = ?", true], "id = 1")
571 599 get :show, :id => 1
572 600 assert_response :redirect
573 601 end
574 602
575 603 def test_show_should_deny_non_member_access_without_permission
576 604 Role.non_member.remove_permission!(:view_issues)
577 605 @request.session[:user_id] = 9
578 606 get :show, :id => 1
579 607 assert_response 403
580 608 end
581 609
582 610 def test_show_should_deny_non_member_access_to_private_issue
583 611 Issue.update_all(["is_private = ?", true], "id = 1")
584 612 @request.session[:user_id] = 9
585 613 get :show, :id => 1
586 614 assert_response 403
587 615 end
588 616
589 617 def test_show_should_deny_member_access_without_permission
590 618 Role.find(1).remove_permission!(:view_issues)
591 619 @request.session[:user_id] = 2
592 620 get :show, :id => 1
593 621 assert_response 403
594 622 end
595 623
596 624 def test_show_should_deny_member_access_to_private_issue_without_permission
597 625 Issue.update_all(["is_private = ?", true], "id = 1")
598 626 @request.session[:user_id] = 3
599 627 get :show, :id => 1
600 628 assert_response 403
601 629 end
602 630
603 631 def test_show_should_allow_author_access_to_private_issue
604 632 Issue.update_all(["is_private = ?, author_id = 3", true], "id = 1")
605 633 @request.session[:user_id] = 3
606 634 get :show, :id => 1
607 635 assert_response :success
608 636 end
609 637
610 638 def test_show_should_allow_assignee_access_to_private_issue
611 639 Issue.update_all(["is_private = ?, assigned_to_id = 3", true], "id = 1")
612 640 @request.session[:user_id] = 3
613 641 get :show, :id => 1
614 642 assert_response :success
615 643 end
616 644
617 645 def test_show_should_allow_member_access_to_private_issue_with_permission
618 646 Issue.update_all(["is_private = ?", true], "id = 1")
619 647 User.find(3).roles_for_project(Project.find(1)).first.update_attribute :issues_visibility, 'all'
620 648 @request.session[:user_id] = 3
621 649 get :show, :id => 1
622 650 assert_response :success
623 651 end
624 652
625 653 def test_show_should_not_disclose_relations_to_invisible_issues
626 654 Setting.cross_project_issue_relations = '1'
627 655 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(2), :relation_type => 'relates')
628 656 # Relation to a private project issue
629 657 IssueRelation.create!(:issue_from => Issue.find(1), :issue_to => Issue.find(4), :relation_type => 'relates')
630 658
631 659 get :show, :id => 1
632 660 assert_response :success
633 661
634 662 assert_tag :div, :attributes => { :id => 'relations' },
635 663 :descendant => { :tag => 'a', :content => /#2$/ }
636 664 assert_no_tag :div, :attributes => { :id => 'relations' },
637 665 :descendant => { :tag => 'a', :content => /#4$/ }
638 666 end
639 667
640 668 def test_show_atom
641 669 get :show, :id => 2, :format => 'atom'
642 670 assert_response :success
643 671 assert_template 'journals/index'
644 672 # Inline image
645 673 assert_select 'content', :text => Regexp.new(Regexp.quote('http://test.host/attachments/download/10'))
646 674 end
647 675
648 676 def test_show_export_to_pdf
649 677 get :show, :id => 3, :format => 'pdf'
650 678 assert_response :success
651 679 assert_equal 'application/pdf', @response.content_type
652 680 assert @response.body.starts_with?('%PDF')
653 681 assert_not_nil assigns(:issue)
654 682 end
655 683
656 684 def test_get_new
657 685 @request.session[:user_id] = 2
658 686 get :new, :project_id => 1, :tracker_id => 1
659 687 assert_response :success
660 688 assert_template 'new'
661 689
662 690 assert_tag :tag => 'input', :attributes => { :name => 'issue[custom_field_values][2]',
663 691 :value => 'Default string' }
664 692
665 693 # Be sure we don't display inactive IssuePriorities
666 694 assert ! IssuePriority.find(15).active?
667 695 assert_no_tag :option, :attributes => {:value => '15'},
668 696 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
669 697 end
670 698
671 699 def test_get_new_without_default_start_date_is_creation_date
672 700 Setting.default_issue_start_date_to_creation_date = 0
673 701
674 702 @request.session[:user_id] = 2
675 703 get :new, :project_id => 1, :tracker_id => 1
676 704 assert_response :success
677 705 assert_template 'new'
678 706
679 707 assert_tag :tag => 'input', :attributes => { :name => 'issue[start_date]',
680 708 :value => nil }
681 709 end
682 710
683 711 def test_get_new_with_default_start_date_is_creation_date
684 712 Setting.default_issue_start_date_to_creation_date = 1
685 713
686 714 @request.session[:user_id] = 2
687 715 get :new, :project_id => 1, :tracker_id => 1
688 716 assert_response :success
689 717 assert_template 'new'
690 718
691 719 assert_tag :tag => 'input', :attributes => { :name => 'issue[start_date]',
692 720 :value => Date.today.to_s }
693 721 end
694 722
695 723 def test_get_new_form_should_allow_attachment_upload
696 724 @request.session[:user_id] = 2
697 725 get :new, :project_id => 1, :tracker_id => 1
698 726
699 727 assert_tag :tag => 'form',
700 728 :attributes => {:id => 'issue-form', :method => 'post', :enctype => 'multipart/form-data'},
701 729 :descendant => {
702 730 :tag => 'input',
703 731 :attributes => {:type => 'file', :name => 'attachments[1][file]'}
704 732 }
705 733 end
706 734
707 735 def test_get_new_without_tracker_id
708 736 @request.session[:user_id] = 2
709 737 get :new, :project_id => 1
710 738 assert_response :success
711 739 assert_template 'new'
712 740
713 741 issue = assigns(:issue)
714 742 assert_not_nil issue
715 743 assert_equal Project.find(1).trackers.first, issue.tracker
716 744 end
717 745
718 746 def test_get_new_with_no_default_status_should_display_an_error
719 747 @request.session[:user_id] = 2
720 748 IssueStatus.delete_all
721 749
722 750 get :new, :project_id => 1
723 751 assert_response 500
724 752 assert_error_tag :content => /No default issue/
725 753 end
726 754
727 755 def test_get_new_with_no_tracker_should_display_an_error
728 756 @request.session[:user_id] = 2
729 757 Tracker.delete_all
730 758
731 759 get :new, :project_id => 1
732 760 assert_response 500
733 761 assert_error_tag :content => /No tracker/
734 762 end
735 763
736 764 def test_update_new_form
737 765 @request.session[:user_id] = 2
738 766 xhr :post, :new, :project_id => 1,
739 767 :issue => {:tracker_id => 2,
740 768 :subject => 'This is the test_new issue',
741 769 :description => 'This is the description',
742 770 :priority_id => 5}
743 771 assert_response :success
744 772 assert_template 'attributes'
745 773
746 774 issue = assigns(:issue)
747 775 assert_kind_of Issue, issue
748 776 assert_equal 1, issue.project_id
749 777 assert_equal 2, issue.tracker_id
750 778 assert_equal 'This is the test_new issue', issue.subject
751 779 end
752 780
753 781 def test_post_create
754 782 @request.session[:user_id] = 2
755 783 assert_difference 'Issue.count' do
756 784 post :create, :project_id => 1,
757 785 :issue => {:tracker_id => 3,
758 786 :status_id => 2,
759 787 :subject => 'This is the test_new issue',
760 788 :description => 'This is the description',
761 789 :priority_id => 5,
762 790 :start_date => '2010-11-07',
763 791 :estimated_hours => '',
764 792 :custom_field_values => {'2' => 'Value for field 2'}}
765 793 end
766 794 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
767 795
768 796 issue = Issue.find_by_subject('This is the test_new issue')
769 797 assert_not_nil issue
770 798 assert_equal 2, issue.author_id
771 799 assert_equal 3, issue.tracker_id
772 800 assert_equal 2, issue.status_id
773 801 assert_equal Date.parse('2010-11-07'), issue.start_date
774 802 assert_nil issue.estimated_hours
775 803 v = issue.custom_values.find(:first, :conditions => {:custom_field_id => 2})
776 804 assert_not_nil v
777 805 assert_equal 'Value for field 2', v.value
778 806 end
779 807
780 808 def test_post_new_with_group_assignment
781 809 group = Group.find(11)
782 810 project = Project.find(1)
783 811 project.members << Member.new(:principal => group, :roles => [Role.first])
784 812
785 813 with_settings :issue_group_assignment => '1' do
786 814 @request.session[:user_id] = 2
787 815 assert_difference 'Issue.count' do
788 816 post :create, :project_id => project.id,
789 817 :issue => {:tracker_id => 3,
790 818 :status_id => 1,
791 819 :subject => 'This is the test_new_with_group_assignment issue',
792 820 :assigned_to_id => group.id}
793 821 end
794 822 end
795 823 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
796 824
797 825 issue = Issue.find_by_subject('This is the test_new_with_group_assignment issue')
798 826 assert_not_nil issue
799 827 assert_equal group, issue.assigned_to
800 828 end
801 829
802 830 def test_post_create_without_start_date_and_default_start_date_is_not_creation_date
803 831 Setting.default_issue_start_date_to_creation_date = 0
804 832
805 833 @request.session[:user_id] = 2
806 834 assert_difference 'Issue.count' do
807 835 post :create, :project_id => 1,
808 836 :issue => {:tracker_id => 3,
809 837 :status_id => 2,
810 838 :subject => 'This is the test_new issue',
811 839 :description => 'This is the description',
812 840 :priority_id => 5,
813 841 :estimated_hours => '',
814 842 :custom_field_values => {'2' => 'Value for field 2'}}
815 843 end
816 844 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
817 845
818 846 issue = Issue.find_by_subject('This is the test_new issue')
819 847 assert_not_nil issue
820 848 assert_nil issue.start_date
821 849 end
822 850
823 851 def test_post_create_without_start_date_and_default_start_date_is_creation_date
824 852 Setting.default_issue_start_date_to_creation_date = 1
825 853
826 854 @request.session[:user_id] = 2
827 855 assert_difference 'Issue.count' do
828 856 post :create, :project_id => 1,
829 857 :issue => {:tracker_id => 3,
830 858 :status_id => 2,
831 859 :subject => 'This is the test_new issue',
832 860 :description => 'This is the description',
833 861 :priority_id => 5,
834 862 :estimated_hours => '',
835 863 :custom_field_values => {'2' => 'Value for field 2'}}
836 864 end
837 865 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
838 866
839 867 issue = Issue.find_by_subject('This is the test_new issue')
840 868 assert_not_nil issue
841 869 assert_equal Date.today, issue.start_date
842 870 end
843 871
844 872 def test_post_create_and_continue
845 873 @request.session[:user_id] = 2
846 874 assert_difference 'Issue.count' do
847 875 post :create, :project_id => 1,
848 876 :issue => {:tracker_id => 3, :subject => 'This is first issue', :priority_id => 5},
849 877 :continue => ''
850 878 end
851 879
852 880 issue = Issue.first(:order => 'id DESC')
853 881 assert_redirected_to :controller => 'issues', :action => 'new', :project_id => 'ecookbook', :issue => {:tracker_id => 3}
854 882 assert_not_nil flash[:notice], "flash was not set"
855 883 assert flash[:notice].include?("<a href='/issues/#{issue.id}'>##{issue.id}</a>"), "issue link not found in flash: #{flash[:notice]}"
856 884 end
857 885
858 886 def test_post_create_without_custom_fields_param
859 887 @request.session[:user_id] = 2
860 888 assert_difference 'Issue.count' do
861 889 post :create, :project_id => 1,
862 890 :issue => {:tracker_id => 1,
863 891 :subject => 'This is the test_new issue',
864 892 :description => 'This is the description',
865 893 :priority_id => 5}
866 894 end
867 895 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
868 896 end
869 897
870 898 def test_post_create_with_required_custom_field_and_without_custom_fields_param
871 899 field = IssueCustomField.find_by_name('Database')
872 900 field.update_attribute(:is_required, true)
873 901
874 902 @request.session[:user_id] = 2
875 903 post :create, :project_id => 1,
876 904 :issue => {:tracker_id => 1,
877 905 :subject => 'This is the test_new issue',
878 906 :description => 'This is the description',
879 907 :priority_id => 5}
880 908 assert_response :success
881 909 assert_template 'new'
882 910 issue = assigns(:issue)
883 911 assert_not_nil issue
884 912 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
885 913 end
886 914
887 915 def test_post_create_with_watchers
888 916 @request.session[:user_id] = 2
889 917 ActionMailer::Base.deliveries.clear
890 918
891 919 assert_difference 'Watcher.count', 2 do
892 920 post :create, :project_id => 1,
893 921 :issue => {:tracker_id => 1,
894 922 :subject => 'This is a new issue with watchers',
895 923 :description => 'This is the description',
896 924 :priority_id => 5,
897 925 :watcher_user_ids => ['2', '3']}
898 926 end
899 927 issue = Issue.find_by_subject('This is a new issue with watchers')
900 928 assert_not_nil issue
901 929 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue
902 930
903 931 # Watchers added
904 932 assert_equal [2, 3], issue.watcher_user_ids.sort
905 933 assert issue.watched_by?(User.find(3))
906 934 # Watchers notified
907 935 mail = ActionMailer::Base.deliveries.last
908 936 assert_kind_of TMail::Mail, mail
909 937 assert [mail.bcc, mail.cc].flatten.include?(User.find(3).mail)
910 938 end
911 939
912 940 def test_post_create_subissue
913 941 @request.session[:user_id] = 2
914 942
915 943 assert_difference 'Issue.count' do
916 944 post :create, :project_id => 1,
917 945 :issue => {:tracker_id => 1,
918 946 :subject => 'This is a child issue',
919 947 :parent_issue_id => 2}
920 948 end
921 949 issue = Issue.find_by_subject('This is a child issue')
922 950 assert_not_nil issue
923 951 assert_equal Issue.find(2), issue.parent
924 952 end
925 953
926 954 def test_post_create_subissue_with_non_numeric_parent_id
927 955 @request.session[:user_id] = 2
928 956
929 957 assert_difference 'Issue.count' do
930 958 post :create, :project_id => 1,
931 959 :issue => {:tracker_id => 1,
932 960 :subject => 'This is a child issue',
933 961 :parent_issue_id => 'ABC'}
934 962 end
935 963 issue = Issue.find_by_subject('This is a child issue')
936 964 assert_not_nil issue
937 965 assert_nil issue.parent
938 966 end
939 967
940 968 def test_post_create_private
941 969 @request.session[:user_id] = 2
942 970
943 971 assert_difference 'Issue.count' do
944 972 post :create, :project_id => 1,
945 973 :issue => {:tracker_id => 1,
946 974 :subject => 'This is a private issue',
947 975 :is_private => '1'}
948 976 end
949 977 issue = Issue.first(:order => 'id DESC')
950 978 assert issue.is_private?
951 979 end
952 980
953 981 def test_post_create_private_with_set_own_issues_private_permission
954 982 role = Role.find(1)
955 983 role.remove_permission! :set_issues_private
956 984 role.add_permission! :set_own_issues_private
957 985
958 986 @request.session[:user_id] = 2
959 987
960 988 assert_difference 'Issue.count' do
961 989 post :create, :project_id => 1,
962 990 :issue => {:tracker_id => 1,
963 991 :subject => 'This is a private issue',
964 992 :is_private => '1'}
965 993 end
966 994 issue = Issue.first(:order => 'id DESC')
967 995 assert issue.is_private?
968 996 end
969 997
970 998 def test_post_create_should_send_a_notification
971 999 ActionMailer::Base.deliveries.clear
972 1000 @request.session[:user_id] = 2
973 1001 assert_difference 'Issue.count' do
974 1002 post :create, :project_id => 1,
975 1003 :issue => {:tracker_id => 3,
976 1004 :subject => 'This is the test_new issue',
977 1005 :description => 'This is the description',
978 1006 :priority_id => 5,
979 1007 :estimated_hours => '',
980 1008 :custom_field_values => {'2' => 'Value for field 2'}}
981 1009 end
982 1010 assert_redirected_to :controller => 'issues', :action => 'show', :id => Issue.last.id
983 1011
984 1012 assert_equal 1, ActionMailer::Base.deliveries.size
985 1013 end
986 1014
987 1015 def test_post_create_should_preserve_fields_values_on_validation_failure
988 1016 @request.session[:user_id] = 2
989 1017 post :create, :project_id => 1,
990 1018 :issue => {:tracker_id => 1,
991 1019 # empty subject
992 1020 :subject => '',
993 1021 :description => 'This is a description',
994 1022 :priority_id => 6,
995 1023 :custom_field_values => {'1' => 'Oracle', '2' => 'Value for field 2'}}
996 1024 assert_response :success
997 1025 assert_template 'new'
998 1026
999 1027 assert_tag :textarea, :attributes => { :name => 'issue[description]' },
1000 1028 :content => 'This is a description'
1001 1029 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
1002 1030 :child => { :tag => 'option', :attributes => { :selected => 'selected',
1003 1031 :value => '6' },
1004 1032 :content => 'High' }
1005 1033 # Custom fields
1006 1034 assert_tag :select, :attributes => { :name => 'issue[custom_field_values][1]' },
1007 1035 :child => { :tag => 'option', :attributes => { :selected => 'selected',
1008 1036 :value => 'Oracle' },
1009 1037 :content => 'Oracle' }
1010 1038 assert_tag :input, :attributes => { :name => 'issue[custom_field_values][2]',
1011 1039 :value => 'Value for field 2'}
1012 1040 end
1013 1041
1014 1042 def test_post_create_should_ignore_non_safe_attributes
1015 1043 @request.session[:user_id] = 2
1016 1044 assert_nothing_raised do
1017 1045 post :create, :project_id => 1, :issue => { :tracker => "A param can not be a Tracker" }
1018 1046 end
1019 1047 end
1020 1048
1021 1049 def test_post_create_with_attachment
1022 1050 set_tmp_attachments_directory
1023 1051 @request.session[:user_id] = 2
1024 1052
1025 1053 assert_difference 'Issue.count' do
1026 1054 assert_difference 'Attachment.count' do
1027 1055 post :create, :project_id => 1,
1028 1056 :issue => { :tracker_id => '1', :subject => 'With attachment' },
1029 1057 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
1030 1058 end
1031 1059 end
1032 1060
1033 1061 issue = Issue.first(:order => 'id DESC')
1034 1062 attachment = Attachment.first(:order => 'id DESC')
1035 1063
1036 1064 assert_equal issue, attachment.container
1037 1065 assert_equal 2, attachment.author_id
1038 1066 assert_equal 'testfile.txt', attachment.filename
1039 1067 assert_equal 'text/plain', attachment.content_type
1040 1068 assert_equal 'test file', attachment.description
1041 1069 assert_equal 59, attachment.filesize
1042 1070 assert File.exists?(attachment.diskfile)
1043 1071 assert_equal 59, File.size(attachment.diskfile)
1044 1072 end
1045 1073
1046 1074 context "without workflow privilege" do
1047 1075 setup do
1048 1076 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
1049 1077 Role.anonymous.add_permission! :add_issues, :add_issue_notes
1050 1078 end
1051 1079
1052 1080 context "#new" do
1053 1081 should "propose default status only" do
1054 1082 get :new, :project_id => 1
1055 1083 assert_response :success
1056 1084 assert_template 'new'
1057 1085 assert_tag :tag => 'select',
1058 1086 :attributes => {:name => 'issue[status_id]'},
1059 1087 :children => {:count => 1},
1060 1088 :child => {:tag => 'option', :attributes => {:value => IssueStatus.default.id.to_s}}
1061 1089 end
1062 1090
1063 1091 should "accept default status" do
1064 1092 assert_difference 'Issue.count' do
1065 1093 post :create, :project_id => 1,
1066 1094 :issue => {:tracker_id => 1,
1067 1095 :subject => 'This is an issue',
1068 1096 :status_id => 1}
1069 1097 end
1070 1098 issue = Issue.last(:order => 'id')
1071 1099 assert_equal IssueStatus.default, issue.status
1072 1100 end
1073 1101
1074 1102 should "ignore unauthorized status" do
1075 1103 assert_difference 'Issue.count' do
1076 1104 post :create, :project_id => 1,
1077 1105 :issue => {:tracker_id => 1,
1078 1106 :subject => 'This is an issue',
1079 1107 :status_id => 3}
1080 1108 end
1081 1109 issue = Issue.last(:order => 'id')
1082 1110 assert_equal IssueStatus.default, issue.status
1083 1111 end
1084 1112 end
1085 1113
1086 1114 context "#update" do
1087 1115 should "ignore status change" do
1088 1116 assert_difference 'Journal.count' do
1089 1117 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1090 1118 end
1091 1119 assert_equal 1, Issue.find(1).status_id
1092 1120 end
1093 1121
1094 1122 should "ignore attributes changes" do
1095 1123 assert_difference 'Journal.count' do
1096 1124 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
1097 1125 end
1098 1126 issue = Issue.find(1)
1099 1127 assert_equal "Can't print recipes", issue.subject
1100 1128 assert_nil issue.assigned_to
1101 1129 end
1102 1130 end
1103 1131 end
1104 1132
1105 1133 context "with workflow privilege" do
1106 1134 setup do
1107 1135 Workflow.delete_all(["role_id = ?", Role.anonymous.id])
1108 1136 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3)
1109 1137 Workflow.create!(:role => Role.anonymous, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4)
1110 1138 Role.anonymous.add_permission! :add_issues, :add_issue_notes
1111 1139 end
1112 1140
1113 1141 context "#update" do
1114 1142 should "accept authorized status" do
1115 1143 assert_difference 'Journal.count' do
1116 1144 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1117 1145 end
1118 1146 assert_equal 3, Issue.find(1).status_id
1119 1147 end
1120 1148
1121 1149 should "ignore unauthorized status" do
1122 1150 assert_difference 'Journal.count' do
1123 1151 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
1124 1152 end
1125 1153 assert_equal 1, Issue.find(1).status_id
1126 1154 end
1127 1155
1128 1156 should "accept authorized attributes changes" do
1129 1157 assert_difference 'Journal.count' do
1130 1158 put :update, :id => 1, :notes => 'just trying', :issue => {:assigned_to_id => 2}
1131 1159 end
1132 1160 issue = Issue.find(1)
1133 1161 assert_equal 2, issue.assigned_to_id
1134 1162 end
1135 1163
1136 1164 should "ignore unauthorized attributes changes" do
1137 1165 assert_difference 'Journal.count' do
1138 1166 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed'}
1139 1167 end
1140 1168 issue = Issue.find(1)
1141 1169 assert_equal "Can't print recipes", issue.subject
1142 1170 end
1143 1171 end
1144 1172
1145 1173 context "and :edit_issues permission" do
1146 1174 setup do
1147 1175 Role.anonymous.add_permission! :add_issues, :edit_issues
1148 1176 end
1149 1177
1150 1178 should "accept authorized status" do
1151 1179 assert_difference 'Journal.count' do
1152 1180 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 3}
1153 1181 end
1154 1182 assert_equal 3, Issue.find(1).status_id
1155 1183 end
1156 1184
1157 1185 should "ignore unauthorized status" do
1158 1186 assert_difference 'Journal.count' do
1159 1187 put :update, :id => 1, :notes => 'just trying', :issue => {:status_id => 2}
1160 1188 end
1161 1189 assert_equal 1, Issue.find(1).status_id
1162 1190 end
1163 1191
1164 1192 should "accept authorized attributes changes" do
1165 1193 assert_difference 'Journal.count' do
1166 1194 put :update, :id => 1, :notes => 'just trying', :issue => {:subject => 'changed', :assigned_to_id => 2}
1167 1195 end
1168 1196 issue = Issue.find(1)
1169 1197 assert_equal "changed", issue.subject
1170 1198 assert_equal 2, issue.assigned_to_id
1171 1199 end
1172 1200 end
1173 1201 end
1174 1202
1175 1203 def test_copy_issue
1176 1204 @request.session[:user_id] = 2
1177 1205 get :new, :project_id => 1, :copy_from => 1
1178 1206 assert_template 'new'
1179 1207 assert_not_nil assigns(:issue)
1180 1208 orig = Issue.find(1)
1181 1209 assert_equal orig.subject, assigns(:issue).subject
1182 1210 end
1183 1211
1184 1212 def test_get_edit
1185 1213 @request.session[:user_id] = 2
1186 1214 get :edit, :id => 1
1187 1215 assert_response :success
1188 1216 assert_template 'edit'
1189 1217 assert_not_nil assigns(:issue)
1190 1218 assert_equal Issue.find(1), assigns(:issue)
1191 1219
1192 1220 # Be sure we don't display inactive IssuePriorities
1193 1221 assert ! IssuePriority.find(15).active?
1194 1222 assert_no_tag :option, :attributes => {:value => '15'},
1195 1223 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1196 1224 end
1197 1225
1198 1226 def test_get_edit_should_display_the_time_entry_form_with_log_time_permission
1199 1227 @request.session[:user_id] = 2
1200 1228 Role.find_by_name('Manager').update_attribute :permissions, [:view_issues, :edit_issues, :log_time]
1201 1229
1202 1230 get :edit, :id => 1
1203 1231 assert_tag 'input', :attributes => {:name => 'time_entry[hours]'}
1204 1232 end
1205 1233
1206 1234 def test_get_edit_should_not_display_the_time_entry_form_without_log_time_permission
1207 1235 @request.session[:user_id] = 2
1208 1236 Role.find_by_name('Manager').remove_permission! :log_time
1209 1237
1210 1238 get :edit, :id => 1
1211 1239 assert_no_tag 'input', :attributes => {:name => 'time_entry[hours]'}
1212 1240 end
1213 1241
1214 1242 def test_get_edit_with_params
1215 1243 @request.session[:user_id] = 2
1216 1244 get :edit, :id => 1, :issue => { :status_id => 5, :priority_id => 7 },
1217 1245 :time_entry => { :hours => '2.5', :comments => 'test_get_edit_with_params', :activity_id => TimeEntryActivity.first.id }
1218 1246 assert_response :success
1219 1247 assert_template 'edit'
1220 1248
1221 1249 issue = assigns(:issue)
1222 1250 assert_not_nil issue
1223 1251
1224 1252 assert_equal 5, issue.status_id
1225 1253 assert_tag :select, :attributes => { :name => 'issue[status_id]' },
1226 1254 :child => { :tag => 'option',
1227 1255 :content => 'Closed',
1228 1256 :attributes => { :selected => 'selected' } }
1229 1257
1230 1258 assert_equal 7, issue.priority_id
1231 1259 assert_tag :select, :attributes => { :name => 'issue[priority_id]' },
1232 1260 :child => { :tag => 'option',
1233 1261 :content => 'Urgent',
1234 1262 :attributes => { :selected => 'selected' } }
1235 1263
1236 1264 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => '2.5' }
1237 1265 assert_tag :select, :attributes => { :name => 'time_entry[activity_id]' },
1238 1266 :child => { :tag => 'option',
1239 1267 :attributes => { :selected => 'selected', :value => TimeEntryActivity.first.id } }
1240 1268 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => 'test_get_edit_with_params' }
1241 1269 end
1242 1270
1243 1271 def test_update_edit_form
1244 1272 @request.session[:user_id] = 2
1245 1273 xhr :post, :new, :project_id => 1,
1246 1274 :id => 1,
1247 1275 :issue => {:tracker_id => 2,
1248 1276 :subject => 'This is the test_new issue',
1249 1277 :description => 'This is the description',
1250 1278 :priority_id => 5}
1251 1279 assert_response :success
1252 1280 assert_template 'attributes'
1253 1281
1254 1282 issue = assigns(:issue)
1255 1283 assert_kind_of Issue, issue
1256 1284 assert_equal 1, issue.id
1257 1285 assert_equal 1, issue.project_id
1258 1286 assert_equal 2, issue.tracker_id
1259 1287 assert_equal 'This is the test_new issue', issue.subject
1260 1288 end
1261 1289
1262 1290 def test_update_using_invalid_http_verbs
1263 1291 @request.session[:user_id] = 2
1264 1292 subject = 'Updated by an invalid http verb'
1265 1293
1266 1294 get :update, :id => 1, :issue => {:subject => subject}
1267 1295 assert_not_equal subject, Issue.find(1).subject
1268 1296
1269 1297 post :update, :id => 1, :issue => {:subject => subject}
1270 1298 assert_not_equal subject, Issue.find(1).subject
1271 1299
1272 1300 delete :update, :id => 1, :issue => {:subject => subject}
1273 1301 assert_not_equal subject, Issue.find(1).subject
1274 1302 end
1275 1303
1276 1304 def test_put_update_without_custom_fields_param
1277 1305 @request.session[:user_id] = 2
1278 1306 ActionMailer::Base.deliveries.clear
1279 1307
1280 1308 issue = Issue.find(1)
1281 1309 assert_equal '125', issue.custom_value_for(2).value
1282 1310 old_subject = issue.subject
1283 1311 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
1284 1312
1285 1313 assert_difference('Journal.count') do
1286 1314 assert_difference('JournalDetail.count', 2) do
1287 1315 put :update, :id => 1, :issue => {:subject => new_subject,
1288 1316 :priority_id => '6',
1289 1317 :category_id => '1' # no change
1290 1318 }
1291 1319 end
1292 1320 end
1293 1321 assert_redirected_to :action => 'show', :id => '1'
1294 1322 issue.reload
1295 1323 assert_equal new_subject, issue.subject
1296 1324 # Make sure custom fields were not cleared
1297 1325 assert_equal '125', issue.custom_value_for(2).value
1298 1326
1299 1327 mail = ActionMailer::Base.deliveries.last
1300 1328 assert_kind_of TMail::Mail, mail
1301 1329 assert mail.subject.starts_with?("[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}]")
1302 1330 assert mail.body.include?("Subject changed from #{old_subject} to #{new_subject}")
1303 1331 end
1304 1332
1305 1333 def test_put_update_with_custom_field_change
1306 1334 @request.session[:user_id] = 2
1307 1335 issue = Issue.find(1)
1308 1336 assert_equal '125', issue.custom_value_for(2).value
1309 1337
1310 1338 assert_difference('Journal.count') do
1311 1339 assert_difference('JournalDetail.count', 3) do
1312 1340 put :update, :id => 1, :issue => {:subject => 'Custom field change',
1313 1341 :priority_id => '6',
1314 1342 :category_id => '1', # no change
1315 1343 :custom_field_values => { '2' => 'New custom value' }
1316 1344 }
1317 1345 end
1318 1346 end
1319 1347 assert_redirected_to :action => 'show', :id => '1'
1320 1348 issue.reload
1321 1349 assert_equal 'New custom value', issue.custom_value_for(2).value
1322 1350
1323 1351 mail = ActionMailer::Base.deliveries.last
1324 1352 assert_kind_of TMail::Mail, mail
1325 1353 assert mail.body.include?("Searchable field changed from 125 to New custom value")
1326 1354 end
1327 1355
1328 1356 def test_put_update_with_status_and_assignee_change
1329 1357 issue = Issue.find(1)
1330 1358 assert_equal 1, issue.status_id
1331 1359 @request.session[:user_id] = 2
1332 1360 assert_difference('TimeEntry.count', 0) do
1333 1361 put :update,
1334 1362 :id => 1,
1335 1363 :issue => { :status_id => 2, :assigned_to_id => 3 },
1336 1364 :notes => 'Assigned to dlopper',
1337 1365 :time_entry => { :hours => '', :comments => '', :activity_id => TimeEntryActivity.first }
1338 1366 end
1339 1367 assert_redirected_to :action => 'show', :id => '1'
1340 1368 issue.reload
1341 1369 assert_equal 2, issue.status_id
1342 1370 j = Journal.find(:first, :order => 'id DESC')
1343 1371 assert_equal 'Assigned to dlopper', j.notes
1344 1372 assert_equal 2, j.details.size
1345 1373
1346 1374 mail = ActionMailer::Base.deliveries.last
1347 1375 assert mail.body.include?("Status changed from New to Assigned")
1348 1376 # subject should contain the new status
1349 1377 assert mail.subject.include?("(#{ IssueStatus.find(2).name })")
1350 1378 end
1351 1379
1352 1380 def test_put_update_with_note_only
1353 1381 notes = 'Note added by IssuesControllerTest#test_update_with_note_only'
1354 1382 # anonymous user
1355 1383 put :update,
1356 1384 :id => 1,
1357 1385 :notes => notes
1358 1386 assert_redirected_to :action => 'show', :id => '1'
1359 1387 j = Journal.find(:first, :order => 'id DESC')
1360 1388 assert_equal notes, j.notes
1361 1389 assert_equal 0, j.details.size
1362 1390 assert_equal User.anonymous, j.user
1363 1391
1364 1392 mail = ActionMailer::Base.deliveries.last
1365 1393 assert mail.body.include?(notes)
1366 1394 end
1367 1395
1368 1396 def test_put_update_with_note_and_spent_time
1369 1397 @request.session[:user_id] = 2
1370 1398 spent_hours_before = Issue.find(1).spent_hours
1371 1399 assert_difference('TimeEntry.count') do
1372 1400 put :update,
1373 1401 :id => 1,
1374 1402 :notes => '2.5 hours added',
1375 1403 :time_entry => { :hours => '2.5', :comments => 'test_put_update_with_note_and_spent_time', :activity_id => TimeEntryActivity.first.id }
1376 1404 end
1377 1405 assert_redirected_to :action => 'show', :id => '1'
1378 1406
1379 1407 issue = Issue.find(1)
1380 1408
1381 1409 j = Journal.find(:first, :order => 'id DESC')
1382 1410 assert_equal '2.5 hours added', j.notes
1383 1411 assert_equal 0, j.details.size
1384 1412
1385 1413 t = issue.time_entries.find_by_comments('test_put_update_with_note_and_spent_time')
1386 1414 assert_not_nil t
1387 1415 assert_equal 2.5, t.hours
1388 1416 assert_equal spent_hours_before + 2.5, issue.spent_hours
1389 1417 end
1390 1418
1391 1419 def test_put_update_with_attachment_only
1392 1420 set_tmp_attachments_directory
1393 1421
1394 1422 # Delete all fixtured journals, a race condition can occur causing the wrong
1395 1423 # journal to get fetched in the next find.
1396 1424 Journal.delete_all
1397 1425
1398 1426 # anonymous user
1399 1427 assert_difference 'Attachment.count' do
1400 1428 put :update, :id => 1,
1401 1429 :notes => '',
1402 1430 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
1403 1431 end
1404 1432
1405 1433 assert_redirected_to :action => 'show', :id => '1'
1406 1434 j = Issue.find(1).journals.find(:first, :order => 'id DESC')
1407 1435 assert j.notes.blank?
1408 1436 assert_equal 1, j.details.size
1409 1437 assert_equal 'testfile.txt', j.details.first.value
1410 1438 assert_equal User.anonymous, j.user
1411 1439
1412 1440 attachment = Attachment.first(:order => 'id DESC')
1413 1441 assert_equal Issue.find(1), attachment.container
1414 1442 assert_equal User.anonymous, attachment.author
1415 1443 assert_equal 'testfile.txt', attachment.filename
1416 1444 assert_equal 'text/plain', attachment.content_type
1417 1445 assert_equal 'test file', attachment.description
1418 1446 assert_equal 59, attachment.filesize
1419 1447 assert File.exists?(attachment.diskfile)
1420 1448 assert_equal 59, File.size(attachment.diskfile)
1421 1449
1422 1450 mail = ActionMailer::Base.deliveries.last
1423 1451 assert mail.body.include?('testfile.txt')
1424 1452 end
1425 1453
1426 1454 def test_put_update_with_attachment_that_fails_to_save
1427 1455 set_tmp_attachments_directory
1428 1456
1429 1457 # Delete all fixtured journals, a race condition can occur causing the wrong
1430 1458 # journal to get fetched in the next find.
1431 1459 Journal.delete_all
1432 1460
1433 1461 # Mock out the unsaved attachment
1434 1462 Attachment.any_instance.stubs(:create).returns(Attachment.new)
1435 1463
1436 1464 # anonymous user
1437 1465 put :update,
1438 1466 :id => 1,
1439 1467 :notes => '',
1440 1468 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
1441 1469 assert_redirected_to :action => 'show', :id => '1'
1442 1470 assert_equal '1 file(s) could not be saved.', flash[:warning]
1443 1471
1444 1472 end if Object.const_defined?(:Mocha)
1445 1473
1446 1474 def test_put_update_with_no_change
1447 1475 issue = Issue.find(1)
1448 1476 issue.journals.clear
1449 1477 ActionMailer::Base.deliveries.clear
1450 1478
1451 1479 put :update,
1452 1480 :id => 1,
1453 1481 :notes => ''
1454 1482 assert_redirected_to :action => 'show', :id => '1'
1455 1483
1456 1484 issue.reload
1457 1485 assert issue.journals.empty?
1458 1486 # No email should be sent
1459 1487 assert ActionMailer::Base.deliveries.empty?
1460 1488 end
1461 1489
1462 1490 def test_put_update_should_send_a_notification
1463 1491 @request.session[:user_id] = 2
1464 1492 ActionMailer::Base.deliveries.clear
1465 1493 issue = Issue.find(1)
1466 1494 old_subject = issue.subject
1467 1495 new_subject = 'Subject modified by IssuesControllerTest#test_post_edit'
1468 1496
1469 1497 put :update, :id => 1, :issue => {:subject => new_subject,
1470 1498 :priority_id => '6',
1471 1499 :category_id => '1' # no change
1472 1500 }
1473 1501 assert_equal 1, ActionMailer::Base.deliveries.size
1474 1502 end
1475 1503
1476 1504 def test_put_update_with_invalid_spent_time_hours_only
1477 1505 @request.session[:user_id] = 2
1478 1506 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
1479 1507
1480 1508 assert_no_difference('Journal.count') do
1481 1509 put :update,
1482 1510 :id => 1,
1483 1511 :notes => notes,
1484 1512 :time_entry => {"comments"=>"", "activity_id"=>"", "hours"=>"2z"}
1485 1513 end
1486 1514 assert_response :success
1487 1515 assert_template 'edit'
1488 1516
1489 1517 assert_error_tag :descendant => {:content => /Activity can't be blank/}
1490 1518 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
1491 1519 assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
1492 1520 end
1493 1521
1494 1522 def test_put_update_with_invalid_spent_time_comments_only
1495 1523 @request.session[:user_id] = 2
1496 1524 notes = 'Note added by IssuesControllerTest#test_post_edit_with_invalid_spent_time'
1497 1525
1498 1526 assert_no_difference('Journal.count') do
1499 1527 put :update,
1500 1528 :id => 1,
1501 1529 :notes => notes,
1502 1530 :time_entry => {"comments"=>"this is my comment", "activity_id"=>"", "hours"=>""}
1503 1531 end
1504 1532 assert_response :success
1505 1533 assert_template 'edit'
1506 1534
1507 1535 assert_error_tag :descendant => {:content => /Activity can't be blank/}
1508 1536 assert_error_tag :descendant => {:content => /Hours can't be blank/}
1509 1537 assert_tag :textarea, :attributes => { :name => 'notes' }, :content => notes
1510 1538 assert_tag :input, :attributes => { :name => 'time_entry[comments]', :value => "this is my comment" }
1511 1539 end
1512 1540
1513 1541 def test_put_update_should_allow_fixed_version_to_be_set_to_a_subproject
1514 1542 issue = Issue.find(2)
1515 1543 @request.session[:user_id] = 2
1516 1544
1517 1545 put :update,
1518 1546 :id => issue.id,
1519 1547 :issue => {
1520 1548 :fixed_version_id => 4
1521 1549 }
1522 1550
1523 1551 assert_response :redirect
1524 1552 issue.reload
1525 1553 assert_equal 4, issue.fixed_version_id
1526 1554 assert_not_equal issue.project_id, issue.fixed_version.project_id
1527 1555 end
1528 1556
1529 1557 def test_put_update_should_redirect_back_using_the_back_url_parameter
1530 1558 issue = Issue.find(2)
1531 1559 @request.session[:user_id] = 2
1532 1560
1533 1561 put :update,
1534 1562 :id => issue.id,
1535 1563 :issue => {
1536 1564 :fixed_version_id => 4
1537 1565 },
1538 1566 :back_url => '/issues'
1539 1567
1540 1568 assert_response :redirect
1541 1569 assert_redirected_to '/issues'
1542 1570 end
1543 1571
1544 1572 def test_put_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1545 1573 issue = Issue.find(2)
1546 1574 @request.session[:user_id] = 2
1547 1575
1548 1576 put :update,
1549 1577 :id => issue.id,
1550 1578 :issue => {
1551 1579 :fixed_version_id => 4
1552 1580 },
1553 1581 :back_url => 'http://google.com'
1554 1582
1555 1583 assert_response :redirect
1556 1584 assert_redirected_to :controller => 'issues', :action => 'show', :id => issue.id
1557 1585 end
1558 1586
1559 1587 def test_get_bulk_edit
1560 1588 @request.session[:user_id] = 2
1561 1589 get :bulk_edit, :ids => [1, 2]
1562 1590 assert_response :success
1563 1591 assert_template 'bulk_edit'
1564 1592
1565 1593 assert_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
1566 1594
1567 1595 # Project specific custom field, date type
1568 1596 field = CustomField.find(9)
1569 1597 assert !field.is_for_all?
1570 1598 assert_equal 'date', field.field_format
1571 1599 assert_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1572 1600
1573 1601 # System wide custom field
1574 1602 assert CustomField.find(1).is_for_all?
1575 1603 assert_tag :select, :attributes => {:name => 'issue[custom_field_values][1]'}
1576 1604
1577 1605 # Be sure we don't display inactive IssuePriorities
1578 1606 assert ! IssuePriority.find(15).active?
1579 1607 assert_no_tag :option, :attributes => {:value => '15'},
1580 1608 :parent => {:tag => 'select', :attributes => {:id => 'issue_priority_id'} }
1581 1609 end
1582 1610
1583 1611 def test_get_bulk_edit_on_different_projects
1584 1612 @request.session[:user_id] = 2
1585 1613 get :bulk_edit, :ids => [1, 2, 6]
1586 1614 assert_response :success
1587 1615 assert_template 'bulk_edit'
1588 1616
1589 1617 # Can not set issues from different projects as children of an issue
1590 1618 assert_no_tag :input, :attributes => {:name => 'issue[parent_issue_id]'}
1591 1619
1592 1620 # Project specific custom field, date type
1593 1621 field = CustomField.find(9)
1594 1622 assert !field.is_for_all?
1595 1623 assert !field.project_ids.include?(Issue.find(6).project_id)
1596 1624 assert_no_tag :input, :attributes => {:name => 'issue[custom_field_values][9]'}
1597 1625 end
1598 1626
1599 1627 def test_get_bulk_edit_with_user_custom_field
1600 1628 field = IssueCustomField.create!(:name => 'Tester', :field_format => 'user', :is_for_all => true)
1601 1629
1602 1630 @request.session[:user_id] = 2
1603 1631 get :bulk_edit, :ids => [1, 2]
1604 1632 assert_response :success
1605 1633 assert_template 'bulk_edit'
1606 1634
1607 1635 assert_tag :select,
1608 1636 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
1609 1637 :children => {
1610 1638 :only => {:tag => 'option'},
1611 1639 :count => Project.find(1).users.count + 1
1612 1640 }
1613 1641 end
1614 1642
1615 1643 def test_get_bulk_edit_with_version_custom_field
1616 1644 field = IssueCustomField.create!(:name => 'Affected version', :field_format => 'version', :is_for_all => true)
1617 1645
1618 1646 @request.session[:user_id] = 2
1619 1647 get :bulk_edit, :ids => [1, 2]
1620 1648 assert_response :success
1621 1649 assert_template 'bulk_edit'
1622 1650
1623 1651 assert_tag :select,
1624 1652 :attributes => {:name => "issue[custom_field_values][#{field.id}]"},
1625 1653 :children => {
1626 1654 :only => {:tag => 'option'},
1627 1655 :count => Project.find(1).shared_versions.count + 1
1628 1656 }
1629 1657 end
1630 1658
1631 1659 def test_bulk_update
1632 1660 @request.session[:user_id] = 2
1633 1661 # update issues priority
1634 1662 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
1635 1663 :issue => {:priority_id => 7,
1636 1664 :assigned_to_id => '',
1637 1665 :custom_field_values => {'2' => ''}}
1638 1666
1639 1667 assert_response 302
1640 1668 # check that the issues were updated
1641 1669 assert_equal [7, 7], Issue.find_all_by_id([1, 2]).collect {|i| i.priority.id}
1642 1670
1643 1671 issue = Issue.find(1)
1644 1672 journal = issue.journals.find(:first, :order => 'created_on DESC')
1645 1673 assert_equal '125', issue.custom_value_for(2).value
1646 1674 assert_equal 'Bulk editing', journal.notes
1647 1675 assert_equal 1, journal.details.size
1648 1676 end
1649 1677
1650 1678 def test_bulk_update_with_group_assignee
1651 1679 group = Group.find(11)
1652 1680 project = Project.find(1)
1653 1681 project.members << Member.new(:principal => group, :roles => [Role.first])
1654 1682
1655 1683 @request.session[:user_id] = 2
1656 1684 # update issues assignee
1657 1685 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing',
1658 1686 :issue => {:priority_id => '',
1659 1687 :assigned_to_id => group.id,
1660 1688 :custom_field_values => {'2' => ''}}
1661 1689
1662 1690 assert_response 302
1663 1691 assert_equal [group, group], Issue.find_all_by_id([1, 2]).collect {|i| i.assigned_to}
1664 1692 end
1665 1693
1666 1694 def test_bulk_update_on_different_projects
1667 1695 @request.session[:user_id] = 2
1668 1696 # update issues priority
1669 1697 post :bulk_update, :ids => [1, 2, 6], :notes => 'Bulk editing',
1670 1698 :issue => {:priority_id => 7,
1671 1699 :assigned_to_id => '',
1672 1700 :custom_field_values => {'2' => ''}}
1673 1701
1674 1702 assert_response 302
1675 1703 # check that the issues were updated
1676 1704 assert_equal [7, 7, 7], Issue.find([1,2,6]).map(&:priority_id)
1677 1705
1678 1706 issue = Issue.find(1)
1679 1707 journal = issue.journals.find(:first, :order => 'created_on DESC')
1680 1708 assert_equal '125', issue.custom_value_for(2).value
1681 1709 assert_equal 'Bulk editing', journal.notes
1682 1710 assert_equal 1, journal.details.size
1683 1711 end
1684 1712
1685 1713 def test_bulk_update_on_different_projects_without_rights
1686 1714 @request.session[:user_id] = 3
1687 1715 user = User.find(3)
1688 1716 action = { :controller => "issues", :action => "bulk_update" }
1689 1717 assert user.allowed_to?(action, Issue.find(1).project)
1690 1718 assert ! user.allowed_to?(action, Issue.find(6).project)
1691 1719 post :bulk_update, :ids => [1, 6], :notes => 'Bulk should fail',
1692 1720 :issue => {:priority_id => 7,
1693 1721 :assigned_to_id => '',
1694 1722 :custom_field_values => {'2' => ''}}
1695 1723 assert_response 403
1696 1724 assert_not_equal "Bulk should fail", Journal.last.notes
1697 1725 end
1698 1726
1699 1727 def test_bullk_update_should_send_a_notification
1700 1728 @request.session[:user_id] = 2
1701 1729 ActionMailer::Base.deliveries.clear
1702 1730 post(:bulk_update,
1703 1731 {
1704 1732 :ids => [1, 2],
1705 1733 :notes => 'Bulk editing',
1706 1734 :issue => {
1707 1735 :priority_id => 7,
1708 1736 :assigned_to_id => '',
1709 1737 :custom_field_values => {'2' => ''}
1710 1738 }
1711 1739 })
1712 1740
1713 1741 assert_response 302
1714 1742 assert_equal 2, ActionMailer::Base.deliveries.size
1715 1743 end
1716 1744
1717 1745 def test_bulk_update_status
1718 1746 @request.session[:user_id] = 2
1719 1747 # update issues priority
1720 1748 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing status',
1721 1749 :issue => {:priority_id => '',
1722 1750 :assigned_to_id => '',
1723 1751 :status_id => '5'}
1724 1752
1725 1753 assert_response 302
1726 1754 issue = Issue.find(1)
1727 1755 assert issue.closed?
1728 1756 end
1729 1757
1730 1758 def test_bulk_update_parent_id
1731 1759 @request.session[:user_id] = 2
1732 1760 post :bulk_update, :ids => [1, 3],
1733 1761 :notes => 'Bulk editing parent',
1734 1762 :issue => {:priority_id => '', :assigned_to_id => '', :status_id => '', :parent_issue_id => '2'}
1735 1763
1736 1764 assert_response 302
1737 1765 parent = Issue.find(2)
1738 1766 assert_equal parent.id, Issue.find(1).parent_id
1739 1767 assert_equal parent.id, Issue.find(3).parent_id
1740 1768 assert_equal [1, 3], parent.children.collect(&:id).sort
1741 1769 end
1742 1770
1743 1771 def test_bulk_update_custom_field
1744 1772 @request.session[:user_id] = 2
1745 1773 # update issues priority
1746 1774 post :bulk_update, :ids => [1, 2], :notes => 'Bulk editing custom field',
1747 1775 :issue => {:priority_id => '',
1748 1776 :assigned_to_id => '',
1749 1777 :custom_field_values => {'2' => '777'}}
1750 1778
1751 1779 assert_response 302
1752 1780
1753 1781 issue = Issue.find(1)
1754 1782 journal = issue.journals.find(:first, :order => 'created_on DESC')
1755 1783 assert_equal '777', issue.custom_value_for(2).value
1756 1784 assert_equal 1, journal.details.size
1757 1785 assert_equal '125', journal.details.first.old_value
1758 1786 assert_equal '777', journal.details.first.value
1759 1787 end
1760 1788
1761 1789 def test_bulk_update_unassign
1762 1790 assert_not_nil Issue.find(2).assigned_to
1763 1791 @request.session[:user_id] = 2
1764 1792 # unassign issues
1765 1793 post :bulk_update, :ids => [1, 2], :notes => 'Bulk unassigning', :issue => {:assigned_to_id => 'none'}
1766 1794 assert_response 302
1767 1795 # check that the issues were updated
1768 1796 assert_nil Issue.find(2).assigned_to
1769 1797 end
1770 1798
1771 1799 def test_post_bulk_update_should_allow_fixed_version_to_be_set_to_a_subproject
1772 1800 @request.session[:user_id] = 2
1773 1801
1774 1802 post :bulk_update, :ids => [1,2], :issue => {:fixed_version_id => 4}
1775 1803
1776 1804 assert_response :redirect
1777 1805 issues = Issue.find([1,2])
1778 1806 issues.each do |issue|
1779 1807 assert_equal 4, issue.fixed_version_id
1780 1808 assert_not_equal issue.project_id, issue.fixed_version.project_id
1781 1809 end
1782 1810 end
1783 1811
1784 1812 def test_post_bulk_update_should_redirect_back_using_the_back_url_parameter
1785 1813 @request.session[:user_id] = 2
1786 1814 post :bulk_update, :ids => [1,2], :back_url => '/issues'
1787 1815
1788 1816 assert_response :redirect
1789 1817 assert_redirected_to '/issues'
1790 1818 end
1791 1819
1792 1820 def test_post_bulk_update_should_not_redirect_back_using_the_back_url_parameter_off_the_host
1793 1821 @request.session[:user_id] = 2
1794 1822 post :bulk_update, :ids => [1,2], :back_url => 'http://google.com'
1795 1823
1796 1824 assert_response :redirect
1797 1825 assert_redirected_to :controller => 'issues', :action => 'index', :project_id => Project.find(1).identifier
1798 1826 end
1799 1827
1800 1828 def test_destroy_issue_with_no_time_entries
1801 1829 assert_nil TimeEntry.find_by_issue_id(2)
1802 1830 @request.session[:user_id] = 2
1803 1831 post :destroy, :id => 2
1804 1832 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1805 1833 assert_nil Issue.find_by_id(2)
1806 1834 end
1807 1835
1808 1836 def test_destroy_issues_with_time_entries
1809 1837 @request.session[:user_id] = 2
1810 1838 post :destroy, :ids => [1, 3]
1811 1839 assert_response :success
1812 1840 assert_template 'destroy'
1813 1841 assert_not_nil assigns(:hours)
1814 1842 assert Issue.find_by_id(1) && Issue.find_by_id(3)
1815 1843 end
1816 1844
1817 1845 def test_destroy_issues_and_destroy_time_entries
1818 1846 @request.session[:user_id] = 2
1819 1847 post :destroy, :ids => [1, 3], :todo => 'destroy'
1820 1848 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1821 1849 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1822 1850 assert_nil TimeEntry.find_by_id([1, 2])
1823 1851 end
1824 1852
1825 1853 def test_destroy_issues_and_assign_time_entries_to_project
1826 1854 @request.session[:user_id] = 2
1827 1855 post :destroy, :ids => [1, 3], :todo => 'nullify'
1828 1856 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1829 1857 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1830 1858 assert_nil TimeEntry.find(1).issue_id
1831 1859 assert_nil TimeEntry.find(2).issue_id
1832 1860 end
1833 1861
1834 1862 def test_destroy_issues_and_reassign_time_entries_to_another_issue
1835 1863 @request.session[:user_id] = 2
1836 1864 post :destroy, :ids => [1, 3], :todo => 'reassign', :reassign_to_id => 2
1837 1865 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
1838 1866 assert !(Issue.find_by_id(1) || Issue.find_by_id(3))
1839 1867 assert_equal 2, TimeEntry.find(1).issue_id
1840 1868 assert_equal 2, TimeEntry.find(2).issue_id
1841 1869 end
1842 1870
1843 1871 def test_destroy_issues_from_different_projects
1844 1872 @request.session[:user_id] = 2
1845 1873 post :destroy, :ids => [1, 2, 6], :todo => 'destroy'
1846 1874 assert_redirected_to :controller => 'issues', :action => 'index'
1847 1875 assert !(Issue.find_by_id(1) || Issue.find_by_id(2) || Issue.find_by_id(6))
1848 1876 end
1849 1877
1850 1878 def test_destroy_parent_and_child_issues
1851 1879 parent = Issue.generate!(:project_id => 1, :tracker_id => 1)
1852 1880 child = Issue.generate!(:project_id => 1, :tracker_id => 1, :parent_issue_id => parent.id)
1853 1881 assert child.is_descendant_of?(parent.reload)
1854 1882
1855 1883 @request.session[:user_id] = 2
1856 1884 assert_difference 'Issue.count', -2 do
1857 1885 post :destroy, :ids => [parent.id, child.id], :todo => 'destroy'
1858 1886 end
1859 1887 assert_response 302
1860 1888 end
1861 1889
1862 1890 def test_default_search_scope
1863 1891 get :index
1864 1892 assert_tag :div, :attributes => {:id => 'quick-search'},
1865 1893 :child => {:tag => 'form',
1866 1894 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
1867 1895 end
1868 1896 end
@@ -1,460 +1,460
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 ENV["RAILS_ENV"] = "test"
19 19 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
20 20 require 'test_help'
21 21 require File.expand_path(File.dirname(__FILE__) + '/helper_testcase')
22 22 require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s
23 23
24 24 require File.expand_path(File.dirname(__FILE__) + '/object_daddy_helpers')
25 25 include ObjectDaddyHelpers
26 26
27 27 class ActiveSupport::TestCase
28 28 # Transactional fixtures accelerate your tests by wrapping each test method
29 29 # in a transaction that's rolled back on completion. This ensures that the
30 30 # test database remains unchanged so your fixtures don't have to be reloaded
31 31 # between every test method. Fewer database queries means faster tests.
32 32 #
33 33 # Read Mike Clark's excellent walkthrough at
34 34 # http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
35 35 #
36 36 # Every Active Record database supports transactions except MyISAM tables
37 37 # in MySQL. Turn off transactional fixtures in this case; however, if you
38 38 # don't care one way or the other, switching from MyISAM to InnoDB tables
39 39 # is recommended.
40 40 self.use_transactional_fixtures = true
41 41
42 42 # Instantiated fixtures are slow, but give you @david where otherwise you
43 43 # would need people(:david). If you don't want to migrate your existing
44 44 # test cases which use the @david style and don't mind the speed hit (each
45 45 # instantiated fixtures translates to a database query per test method),
46 46 # then set this back to true.
47 47 self.use_instantiated_fixtures = false
48 48
49 49 # Add more helper methods to be used by all tests here...
50 50
51 51 def log_user(login, password)
52 52 User.anonymous
53 53 get "/login"
54 54 assert_equal nil, session[:user_id]
55 55 assert_response :success
56 56 assert_template "account/login"
57 57 post "/login", :username => login, :password => password
58 58 assert_equal login, User.find(session[:user_id]).login
59 59 end
60 60
61 61 def uploaded_test_file(name, mime)
62 62 ActionController::TestUploadedFile.new(
63 63 ActiveSupport::TestCase.fixture_path + "/files/#{name}", mime, true)
64 64 end
65 65
66 66 # Mock out a file
67 67 def self.mock_file
68 68 file = 'a_file.png'
69 69 file.stubs(:size).returns(32)
70 70 file.stubs(:original_filename).returns('a_file.png')
71 71 file.stubs(:content_type).returns('image/png')
72 72 file.stubs(:read).returns(false)
73 73 file
74 74 end
75 75
76 76 def mock_file
77 77 self.class.mock_file
78 78 end
79 79
80 80 def mock_file_with_options(options={})
81 81 file = ''
82 82 file.stubs(:size).returns(32)
83 83 original_filename = options[:original_filename] || nil
84 84 file.stubs(:original_filename).returns(original_filename)
85 85 content_type = options[:content_type] || nil
86 86 file.stubs(:content_type).returns(content_type)
87 87 file.stubs(:read).returns(false)
88 88 file
89 89 end
90 90
91 91 # Use a temporary directory for attachment related tests
92 92 def set_tmp_attachments_directory
93 93 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
94 94 unless File.directory?("#{Rails.root}/tmp/test/attachments")
95 95 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
96 96 end
97 97 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
98 98 end
99 99
100 100 def with_settings(options, &block)
101 saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].dup; h}
101 saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].is_a?(Symbol) ? Setting[k] : Setting[k].dup; h}
102 102 options.each {|k, v| Setting[k] = v}
103 103 yield
104 104 ensure
105 saved_settings.each {|k, v| Setting[k] = v}
105 saved_settings.each {|k, v| Setting[k] = v} if saved_settings
106 106 end
107 107
108 108 def change_user_password(login, new_password)
109 109 user = User.first(:conditions => {:login => login})
110 110 user.password, user.password_confirmation = new_password, new_password
111 111 user.save!
112 112 end
113 113
114 114 def self.ldap_configured?
115 115 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
116 116 return @test_ldap.bind
117 117 rescue Exception => e
118 118 # LDAP is not listening
119 119 return nil
120 120 end
121 121
122 122 # Returns the path to the test +vendor+ repository
123 123 def self.repository_path(vendor)
124 124 Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
125 125 end
126 126
127 127 # Returns the url of the subversion test repository
128 128 def self.subversion_repository_url
129 129 path = repository_path('subversion')
130 130 path = '/' + path unless path.starts_with?('/')
131 131 "file://#{path}"
132 132 end
133 133
134 134 # Returns true if the +vendor+ test repository is configured
135 135 def self.repository_configured?(vendor)
136 136 File.directory?(repository_path(vendor))
137 137 end
138 138
139 139 def assert_error_tag(options={})
140 140 assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options))
141 141 end
142 142
143 143 def assert_include(expected, s)
144 144 assert s.include?(expected), "\"#{expected}\" not found in \"#{s}\""
145 145 end
146 146
147 147 # Shoulda macros
148 148 def self.should_render_404
149 149 should_respond_with :not_found
150 150 should_render_template 'common/error'
151 151 end
152 152
153 153 def self.should_have_before_filter(expected_method, options = {})
154 154 should_have_filter('before', expected_method, options)
155 155 end
156 156
157 157 def self.should_have_after_filter(expected_method, options = {})
158 158 should_have_filter('after', expected_method, options)
159 159 end
160 160
161 161 def self.should_have_filter(filter_type, expected_method, options)
162 162 description = "have #{filter_type}_filter :#{expected_method}"
163 163 description << " with #{options.inspect}" unless options.empty?
164 164
165 165 should description do
166 166 klass = "action_controller/filters/#{filter_type}_filter".classify.constantize
167 167 expected = klass.new(:filter, expected_method.to_sym, options)
168 168 assert_equal 1, @controller.class.filter_chain.select { |filter|
169 169 filter.method == expected.method && filter.kind == expected.kind &&
170 170 filter.options == expected.options && filter.class == expected.class
171 171 }.size
172 172 end
173 173 end
174 174
175 175 def self.should_show_the_old_and_new_values_for(prop_key, model, &block)
176 176 context "" do
177 177 setup do
178 178 if block_given?
179 179 instance_eval &block
180 180 else
181 181 @old_value = model.generate!
182 182 @new_value = model.generate!
183 183 end
184 184 end
185 185
186 186 should "use the new value's name" do
187 187 @detail = JournalDetail.generate!(:property => 'attr',
188 188 :old_value => @old_value.id,
189 189 :value => @new_value.id,
190 190 :prop_key => prop_key)
191 191
192 192 assert_match @new_value.name, show_detail(@detail, true)
193 193 end
194 194
195 195 should "use the old value's name" do
196 196 @detail = JournalDetail.generate!(:property => 'attr',
197 197 :old_value => @old_value.id,
198 198 :value => @new_value.id,
199 199 :prop_key => prop_key)
200 200
201 201 assert_match @old_value.name, show_detail(@detail, true)
202 202 end
203 203 end
204 204 end
205 205
206 206 def self.should_create_a_new_user(&block)
207 207 should "create a new user" do
208 208 user = instance_eval &block
209 209 assert user
210 210 assert_kind_of User, user
211 211 assert !user.new_record?
212 212 end
213 213 end
214 214
215 215 # Test that a request allows the three types of API authentication
216 216 #
217 217 # * HTTP Basic with username and password
218 218 # * HTTP Basic with an api key for the username
219 219 # * Key based with the key=X parameter
220 220 #
221 221 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
222 222 # @param [String] url the request url
223 223 # @param [optional, Hash] parameters additional request parameters
224 224 # @param [optional, Hash] options additional options
225 225 # @option options [Symbol] :success_code Successful response code (:success)
226 226 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
227 227 def self.should_allow_api_authentication(http_method, url, parameters={}, options={})
228 228 should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options)
229 229 should_allow_http_basic_auth_with_key(http_method, url, parameters, options)
230 230 should_allow_key_based_auth(http_method, url, parameters, options)
231 231 end
232 232
233 233 # Test that a request allows the username and password for HTTP BASIC
234 234 #
235 235 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
236 236 # @param [String] url the request url
237 237 # @param [optional, Hash] parameters additional request parameters
238 238 # @param [optional, Hash] options additional options
239 239 # @option options [Symbol] :success_code Successful response code (:success)
240 240 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
241 241 def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={})
242 242 success_code = options[:success_code] || :success
243 243 failure_code = options[:failure_code] || :unauthorized
244 244
245 245 context "should allow http basic auth using a username and password for #{http_method} #{url}" do
246 246 context "with a valid HTTP authentication" do
247 247 setup do
248 248 @user = User.generate_with_protected!(:password => 'my_password', :password_confirmation => 'my_password', :admin => true) # Admin so they can access the project
249 249 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@user.login, 'my_password')
250 250 send(http_method, url, parameters, {:authorization => @authorization})
251 251 end
252 252
253 253 should_respond_with success_code
254 254 should_respond_with_content_type_based_on_url(url)
255 255 should "login as the user" do
256 256 assert_equal @user, User.current
257 257 end
258 258 end
259 259
260 260 context "with an invalid HTTP authentication" do
261 261 setup do
262 262 @user = User.generate_with_protected!
263 263 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@user.login, 'wrong_password')
264 264 send(http_method, url, parameters, {:authorization => @authorization})
265 265 end
266 266
267 267 should_respond_with failure_code
268 268 should_respond_with_content_type_based_on_url(url)
269 269 should "not login as the user" do
270 270 assert_equal User.anonymous, User.current
271 271 end
272 272 end
273 273
274 274 context "without credentials" do
275 275 setup do
276 276 send(http_method, url, parameters, {:authorization => ''})
277 277 end
278 278
279 279 should_respond_with failure_code
280 280 should_respond_with_content_type_based_on_url(url)
281 281 should "include_www_authenticate_header" do
282 282 assert @controller.response.headers.has_key?('WWW-Authenticate')
283 283 end
284 284 end
285 285 end
286 286
287 287 end
288 288
289 289 # Test that a request allows the API key with HTTP BASIC
290 290 #
291 291 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
292 292 # @param [String] url the request url
293 293 # @param [optional, Hash] parameters additional request parameters
294 294 # @param [optional, Hash] options additional options
295 295 # @option options [Symbol] :success_code Successful response code (:success)
296 296 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
297 297 def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={})
298 298 success_code = options[:success_code] || :success
299 299 failure_code = options[:failure_code] || :unauthorized
300 300
301 301 context "should allow http basic auth with a key for #{http_method} #{url}" do
302 302 context "with a valid HTTP authentication using the API token" do
303 303 setup do
304 304 @user = User.generate_with_protected!(:admin => true)
305 305 @token = Token.generate!(:user => @user, :action => 'api')
306 306 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@token.value, 'X')
307 307 send(http_method, url, parameters, {:authorization => @authorization})
308 308 end
309 309
310 310 should_respond_with success_code
311 311 should_respond_with_content_type_based_on_url(url)
312 312 should_be_a_valid_response_string_based_on_url(url)
313 313 should "login as the user" do
314 314 assert_equal @user, User.current
315 315 end
316 316 end
317 317
318 318 context "with an invalid HTTP authentication" do
319 319 setup do
320 320 @user = User.generate_with_protected!
321 321 @token = Token.generate!(:user => @user, :action => 'feeds')
322 322 @authorization = ActionController::HttpAuthentication::Basic.encode_credentials(@token.value, 'X')
323 323 send(http_method, url, parameters, {:authorization => @authorization})
324 324 end
325 325
326 326 should_respond_with failure_code
327 327 should_respond_with_content_type_based_on_url(url)
328 328 should "not login as the user" do
329 329 assert_equal User.anonymous, User.current
330 330 end
331 331 end
332 332 end
333 333 end
334 334
335 335 # Test that a request allows full key authentication
336 336 #
337 337 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
338 338 # @param [String] url the request url, without the key=ZXY parameter
339 339 # @param [optional, Hash] parameters additional request parameters
340 340 # @param [optional, Hash] options additional options
341 341 # @option options [Symbol] :success_code Successful response code (:success)
342 342 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
343 343 def self.should_allow_key_based_auth(http_method, url, parameters={}, options={})
344 344 success_code = options[:success_code] || :success
345 345 failure_code = options[:failure_code] || :unauthorized
346 346
347 347 context "should allow key based auth using key=X for #{http_method} #{url}" do
348 348 context "with a valid api token" do
349 349 setup do
350 350 @user = User.generate_with_protected!(:admin => true)
351 351 @token = Token.generate!(:user => @user, :action => 'api')
352 352 # Simple url parse to add on ?key= or &key=
353 353 request_url = if url.match(/\?/)
354 354 url + "&key=#{@token.value}"
355 355 else
356 356 url + "?key=#{@token.value}"
357 357 end
358 358 send(http_method, request_url, parameters)
359 359 end
360 360
361 361 should_respond_with success_code
362 362 should_respond_with_content_type_based_on_url(url)
363 363 should_be_a_valid_response_string_based_on_url(url)
364 364 should "login as the user" do
365 365 assert_equal @user, User.current
366 366 end
367 367 end
368 368
369 369 context "with an invalid api token" do
370 370 setup do
371 371 @user = User.generate_with_protected!
372 372 @token = Token.generate!(:user => @user, :action => 'feeds')
373 373 # Simple url parse to add on ?key= or &key=
374 374 request_url = if url.match(/\?/)
375 375 url + "&key=#{@token.value}"
376 376 else
377 377 url + "?key=#{@token.value}"
378 378 end
379 379 send(http_method, request_url, parameters)
380 380 end
381 381
382 382 should_respond_with failure_code
383 383 should_respond_with_content_type_based_on_url(url)
384 384 should "not login as the user" do
385 385 assert_equal User.anonymous, User.current
386 386 end
387 387 end
388 388 end
389 389
390 390 context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do
391 391 setup do
392 392 @user = User.generate_with_protected!(:admin => true)
393 393 @token = Token.generate!(:user => @user, :action => 'api')
394 394 send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s})
395 395 end
396 396
397 397 should_respond_with success_code
398 398 should_respond_with_content_type_based_on_url(url)
399 399 should_be_a_valid_response_string_based_on_url(url)
400 400 should "login as the user" do
401 401 assert_equal @user, User.current
402 402 end
403 403 end
404 404 end
405 405
406 406 # Uses should_respond_with_content_type based on what's in the url:
407 407 #
408 408 # '/project/issues.xml' => should_respond_with_content_type :xml
409 409 # '/project/issues.json' => should_respond_with_content_type :json
410 410 #
411 411 # @param [String] url Request
412 412 def self.should_respond_with_content_type_based_on_url(url)
413 413 case
414 414 when url.match(/xml/i)
415 415 should_respond_with_content_type :xml
416 416 when url.match(/json/i)
417 417 should_respond_with_content_type :json
418 418 else
419 419 raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}"
420 420 end
421 421
422 422 end
423 423
424 424 # Uses the url to assert which format the response should be in
425 425 #
426 426 # '/project/issues.xml' => should_be_a_valid_xml_string
427 427 # '/project/issues.json' => should_be_a_valid_json_string
428 428 #
429 429 # @param [String] url Request
430 430 def self.should_be_a_valid_response_string_based_on_url(url)
431 431 case
432 432 when url.match(/xml/i)
433 433 should_be_a_valid_xml_string
434 434 when url.match(/json/i)
435 435 should_be_a_valid_json_string
436 436 else
437 437 raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}"
438 438 end
439 439
440 440 end
441 441
442 442 # Checks that the response is a valid JSON string
443 443 def self.should_be_a_valid_json_string
444 444 should "be a valid JSON string (or empty)" do
445 445 assert(response.body.blank? || ActiveSupport::JSON.decode(response.body))
446 446 end
447 447 end
448 448
449 449 # Checks that the response is a valid XML string
450 450 def self.should_be_a_valid_xml_string
451 451 should "be a valid XML string" do
452 452 assert REXML::Document.new(response.body)
453 453 end
454 454 end
455 455
456 456 end
457 457
458 458 # Simple module to "namespace" all of the API tests
459 459 module ApiTest
460 460 end
@@ -1,808 +1,824
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class QueryTest < ActiveSupport::TestCase
21 21 fixtures :projects, :enabled_modules, :users, :members,
22 22 :member_roles, :roles, :trackers, :issue_statuses,
23 23 :issue_categories, :enumerations, :issues,
24 24 :watchers, :custom_fields, :custom_values, :versions,
25 25 :queries,
26 26 :projects_trackers
27 27
28 28 def test_custom_fields_for_all_projects_should_be_available_in_global_queries
29 29 query = Query.new(:project => nil, :name => '_')
30 30 assert query.available_filters.has_key?('cf_1')
31 31 assert !query.available_filters.has_key?('cf_3')
32 32 end
33 33
34 34 def test_system_shared_versions_should_be_available_in_global_queries
35 35 Version.find(2).update_attribute :sharing, 'system'
36 36 query = Query.new(:project => nil, :name => '_')
37 37 assert query.available_filters.has_key?('fixed_version_id')
38 38 assert query.available_filters['fixed_version_id'][:values].detect {|v| v.last == '2'}
39 39 end
40 40
41 41 def test_project_filter_in_global_queries
42 42 query = Query.new(:project => nil, :name => '_')
43 43 project_filter = query.available_filters["project_id"]
44 44 assert_not_nil project_filter
45 45 project_ids = project_filter[:values].map{|p| p[1]}
46 46 assert project_ids.include?("1") #public project
47 47 assert !project_ids.include?("2") #private project user cannot see
48 48 end
49 49
50 50 def find_issues_with_query(query)
51 51 Issue.find :all,
52 52 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
53 53 :conditions => query.statement
54 54 end
55 55
56 56 def assert_find_issues_with_query_is_successful(query)
57 57 assert_nothing_raised do
58 58 find_issues_with_query(query)
59 59 end
60 60 end
61 61
62 62 def assert_query_statement_includes(query, condition)
63 63 assert query.statement.include?(condition), "Query statement condition not found in: #{query.statement}"
64 64 end
65 65
66 66 def assert_query_result(expected, query)
67 67 assert_nothing_raised do
68 68 assert_equal expected.map(&:id).sort, query.issues.map(&:id).sort
69 69 assert_equal expected.size, query.issue_count
70 70 end
71 71 end
72 72
73 73 def test_query_should_allow_shared_versions_for_a_project_query
74 74 subproject_version = Version.find(4)
75 75 query = Query.new(:project => Project.find(1), :name => '_')
76 76 query.add_filter('fixed_version_id', '=', [subproject_version.id.to_s])
77 77
78 78 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IN ('4')")
79 79 end
80 80
81 81 def test_query_with_multiple_custom_fields
82 82 query = Query.find(1)
83 83 assert query.valid?
84 84 assert query.statement.include?("#{CustomValue.table_name}.value IN ('MySQL')")
85 85 issues = find_issues_with_query(query)
86 86 assert_equal 1, issues.length
87 87 assert_equal Issue.find(3), issues.first
88 88 end
89 89
90 90 def test_operator_none
91 91 query = Query.new(:project => Project.find(1), :name => '_')
92 92 query.add_filter('fixed_version_id', '!*', [''])
93 93 query.add_filter('cf_1', '!*', [''])
94 94 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NULL")
95 95 assert query.statement.include?("#{CustomValue.table_name}.value IS NULL OR #{CustomValue.table_name}.value = ''")
96 96 find_issues_with_query(query)
97 97 end
98 98
99 99 def test_operator_none_for_integer
100 100 query = Query.new(:project => Project.find(1), :name => '_')
101 101 query.add_filter('estimated_hours', '!*', [''])
102 102 issues = find_issues_with_query(query)
103 103 assert !issues.empty?
104 104 assert issues.all? {|i| !i.estimated_hours}
105 105 end
106 106
107 107 def test_operator_none_for_date
108 108 query = Query.new(:project => Project.find(1), :name => '_')
109 109 query.add_filter('start_date', '!*', [''])
110 110 issues = find_issues_with_query(query)
111 111 assert !issues.empty?
112 112 assert issues.all? {|i| i.start_date.nil?}
113 113 end
114 114
115 115 def test_operator_all
116 116 query = Query.new(:project => Project.find(1), :name => '_')
117 117 query.add_filter('fixed_version_id', '*', [''])
118 118 query.add_filter('cf_1', '*', [''])
119 119 assert query.statement.include?("#{Issue.table_name}.fixed_version_id IS NOT NULL")
120 120 assert query.statement.include?("#{CustomValue.table_name}.value IS NOT NULL AND #{CustomValue.table_name}.value <> ''")
121 121 find_issues_with_query(query)
122 122 end
123 123
124 124 def test_operator_all_for_date
125 125 query = Query.new(:project => Project.find(1), :name => '_')
126 126 query.add_filter('start_date', '*', [''])
127 127 issues = find_issues_with_query(query)
128 128 assert !issues.empty?
129 129 assert issues.all? {|i| i.start_date.present?}
130 130 end
131 131
132 132 def test_numeric_filter_should_not_accept_non_numeric_values
133 133 query = Query.new(:name => '_')
134 134 query.add_filter('estimated_hours', '=', ['a'])
135 135
136 136 assert query.has_filter?('estimated_hours')
137 137 assert !query.valid?
138 138 end
139 139
140 140 def test_operator_is_on_float
141 141 Issue.update_all("estimated_hours = 171.2", "id=2")
142 142
143 143 query = Query.new(:name => '_')
144 144 query.add_filter('estimated_hours', '=', ['171.20'])
145 145 issues = find_issues_with_query(query)
146 146 assert_equal 1, issues.size
147 147 assert_equal 2, issues.first.id
148 148 end
149 149
150 150 def test_operator_greater_than
151 151 query = Query.new(:project => Project.find(1), :name => '_')
152 152 query.add_filter('done_ratio', '>=', ['40'])
153 153 assert query.statement.include?("#{Issue.table_name}.done_ratio >= 40.0")
154 154 find_issues_with_query(query)
155 155 end
156 156
157 157 def test_operator_greater_than_a_float
158 158 query = Query.new(:project => Project.find(1), :name => '_')
159 159 query.add_filter('estimated_hours', '>=', ['40.5'])
160 160 assert query.statement.include?("#{Issue.table_name}.estimated_hours >= 40.5")
161 161 find_issues_with_query(query)
162 162 end
163 163
164 164 def test_operator_greater_than_on_custom_field
165 165 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
166 166 query = Query.new(:project => Project.find(1), :name => '_')
167 167 query.add_filter("cf_#{f.id}", '>=', ['40'])
168 168 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) >= 40.0")
169 169 find_issues_with_query(query)
170 170 end
171 171
172 172 def test_operator_lesser_than
173 173 query = Query.new(:project => Project.find(1), :name => '_')
174 174 query.add_filter('done_ratio', '<=', ['30'])
175 175 assert query.statement.include?("#{Issue.table_name}.done_ratio <= 30.0")
176 176 find_issues_with_query(query)
177 177 end
178 178
179 179 def test_operator_lesser_than_on_custom_field
180 180 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
181 181 query = Query.new(:project => Project.find(1), :name => '_')
182 182 query.add_filter("cf_#{f.id}", '<=', ['30'])
183 183 assert query.statement.include?("CAST(custom_values.value AS decimal(60,3)) <= 30.0")
184 184 find_issues_with_query(query)
185 185 end
186 186
187 187 def test_operator_between
188 188 query = Query.new(:project => Project.find(1), :name => '_')
189 189 query.add_filter('done_ratio', '><', ['30', '40'])
190 190 assert_include "#{Issue.table_name}.done_ratio BETWEEN 30.0 AND 40.0", query.statement
191 191 find_issues_with_query(query)
192 192 end
193 193
194 194 def test_operator_between_on_custom_field
195 195 f = IssueCustomField.create!(:name => 'filter', :field_format => 'int', :is_filter => true, :is_for_all => true)
196 196 query = Query.new(:project => Project.find(1), :name => '_')
197 197 query.add_filter("cf_#{f.id}", '><', ['30', '40'])
198 198 assert_include "CAST(custom_values.value AS decimal(60,3)) BETWEEN 30.0 AND 40.0", query.statement
199 199 find_issues_with_query(query)
200 200 end
201 201
202 202 def test_date_filter_should_not_accept_non_date_values
203 203 query = Query.new(:name => '_')
204 204 query.add_filter('created_on', '=', ['a'])
205 205
206 206 assert query.has_filter?('created_on')
207 207 assert !query.valid?
208 208 end
209 209
210 210 def test_date_filter_should_not_accept_invalid_date_values
211 211 query = Query.new(:name => '_')
212 212 query.add_filter('created_on', '=', ['2011-01-34'])
213 213
214 214 assert query.has_filter?('created_on')
215 215 assert !query.valid?
216 216 end
217 217
218 218 def test_relative_date_filter_should_not_accept_non_integer_values
219 219 query = Query.new(:name => '_')
220 220 query.add_filter('created_on', '>t-', ['a'])
221 221
222 222 assert query.has_filter?('created_on')
223 223 assert !query.valid?
224 224 end
225 225
226 226 def test_operator_date_equals
227 227 query = Query.new(:name => '_')
228 228 query.add_filter('due_date', '=', ['2011-07-10'])
229 229 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
230 230 find_issues_with_query(query)
231 231 end
232 232
233 233 def test_operator_date_lesser_than
234 234 query = Query.new(:name => '_')
235 235 query.add_filter('due_date', '<=', ['2011-07-10'])
236 236 assert_match /issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
237 237 find_issues_with_query(query)
238 238 end
239 239
240 240 def test_operator_date_greater_than
241 241 query = Query.new(:name => '_')
242 242 query.add_filter('due_date', '>=', ['2011-07-10'])
243 243 assert_match /issues\.due_date > '2011-07-09 23:59:59(\.9+)?'/, query.statement
244 244 find_issues_with_query(query)
245 245 end
246 246
247 247 def test_operator_date_between
248 248 query = Query.new(:name => '_')
249 249 query.add_filter('due_date', '><', ['2011-06-23', '2011-07-10'])
250 250 assert_match /issues\.due_date > '2011-06-22 23:59:59(\.9+)?' AND issues\.due_date <= '2011-07-10 23:59:59(\.9+)?/, query.statement
251 251 find_issues_with_query(query)
252 252 end
253 253
254 254 def test_operator_in_more_than
255 255 Issue.find(7).update_attribute(:due_date, (Date.today + 15))
256 256 query = Query.new(:project => Project.find(1), :name => '_')
257 257 query.add_filter('due_date', '>t+', ['15'])
258 258 issues = find_issues_with_query(query)
259 259 assert !issues.empty?
260 260 issues.each {|issue| assert(issue.due_date >= (Date.today + 15))}
261 261 end
262 262
263 263 def test_operator_in_less_than
264 264 query = Query.new(:project => Project.find(1), :name => '_')
265 265 query.add_filter('due_date', '<t+', ['15'])
266 266 issues = find_issues_with_query(query)
267 267 assert !issues.empty?
268 268 issues.each {|issue| assert(issue.due_date >= Date.today && issue.due_date <= (Date.today + 15))}
269 269 end
270 270
271 271 def test_operator_less_than_ago
272 272 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
273 273 query = Query.new(:project => Project.find(1), :name => '_')
274 274 query.add_filter('due_date', '>t-', ['3'])
275 275 issues = find_issues_with_query(query)
276 276 assert !issues.empty?
277 277 issues.each {|issue| assert(issue.due_date >= (Date.today - 3) && issue.due_date <= Date.today)}
278 278 end
279 279
280 280 def test_operator_more_than_ago
281 281 Issue.find(7).update_attribute(:due_date, (Date.today - 10))
282 282 query = Query.new(:project => Project.find(1), :name => '_')
283 283 query.add_filter('due_date', '<t-', ['10'])
284 284 assert query.statement.include?("#{Issue.table_name}.due_date <=")
285 285 issues = find_issues_with_query(query)
286 286 assert !issues.empty?
287 287 issues.each {|issue| assert(issue.due_date <= (Date.today - 10))}
288 288 end
289 289
290 290 def test_operator_in
291 291 Issue.find(7).update_attribute(:due_date, (Date.today + 2))
292 292 query = Query.new(:project => Project.find(1), :name => '_')
293 293 query.add_filter('due_date', 't+', ['2'])
294 294 issues = find_issues_with_query(query)
295 295 assert !issues.empty?
296 296 issues.each {|issue| assert_equal((Date.today + 2), issue.due_date)}
297 297 end
298 298
299 299 def test_operator_ago
300 300 Issue.find(7).update_attribute(:due_date, (Date.today - 3))
301 301 query = Query.new(:project => Project.find(1), :name => '_')
302 302 query.add_filter('due_date', 't-', ['3'])
303 303 issues = find_issues_with_query(query)
304 304 assert !issues.empty?
305 305 issues.each {|issue| assert_equal((Date.today - 3), issue.due_date)}
306 306 end
307 307
308 308 def test_operator_today
309 309 query = Query.new(:project => Project.find(1), :name => '_')
310 310 query.add_filter('due_date', 't', [''])
311 311 issues = find_issues_with_query(query)
312 312 assert !issues.empty?
313 313 issues.each {|issue| assert_equal Date.today, issue.due_date}
314 314 end
315 315
316 316 def test_operator_this_week_on_date
317 317 query = Query.new(:project => Project.find(1), :name => '_')
318 318 query.add_filter('due_date', 'w', [''])
319 319 find_issues_with_query(query)
320 320 end
321 321
322 322 def test_operator_this_week_on_datetime
323 323 query = Query.new(:project => Project.find(1), :name => '_')
324 324 query.add_filter('created_on', 'w', [''])
325 325 find_issues_with_query(query)
326 326 end
327 327
328 328 def test_operator_contains
329 329 query = Query.new(:project => Project.find(1), :name => '_')
330 330 query.add_filter('subject', '~', ['uNable'])
331 331 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) LIKE '%unable%'")
332 332 result = find_issues_with_query(query)
333 333 assert result.empty?
334 334 result.each {|issue| assert issue.subject.downcase.include?('unable') }
335 335 end
336 336
337 337 def test_range_for_this_week_with_week_starting_on_monday
338 338 I18n.locale = :fr
339 339 assert_equal '1', I18n.t(:general_first_day_of_week)
340 340
341 341 Date.stubs(:today).returns(Date.parse('2011-04-29'))
342 342
343 343 query = Query.new(:project => Project.find(1), :name => '_')
344 344 query.add_filter('due_date', 'w', [''])
345 345 assert query.statement.match(/issues\.due_date > '2011-04-24 23:59:59(\.9+)?' AND issues\.due_date <= '2011-05-01 23:59:59(\.9+)?/), "range not found in #{query.statement}"
346 346 I18n.locale = :en
347 347 end
348 348
349 349 def test_range_for_this_week_with_week_starting_on_sunday
350 350 I18n.locale = :en
351 351 assert_equal '7', I18n.t(:general_first_day_of_week)
352 352
353 353 Date.stubs(:today).returns(Date.parse('2011-04-29'))
354 354
355 355 query = Query.new(:project => Project.find(1), :name => '_')
356 356 query.add_filter('due_date', 'w', [''])
357 357 assert query.statement.match(/issues\.due_date > '2011-04-23 23:59:59(\.9+)?' AND issues\.due_date <= '2011-04-30 23:59:59(\.9+)?/), "range not found in #{query.statement}"
358 358 end
359 359
360 360 def test_operator_does_not_contains
361 361 query = Query.new(:project => Project.find(1), :name => '_')
362 362 query.add_filter('subject', '!~', ['uNable'])
363 363 assert query.statement.include?("LOWER(#{Issue.table_name}.subject) NOT LIKE '%unable%'")
364 364 find_issues_with_query(query)
365 365 end
366 366
367 367 def test_filter_assigned_to_me
368 368 user = User.find(2)
369 369 group = Group.find(10)
370 370 User.current = user
371 371 i1 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => user)
372 372 i2 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => group)
373 373 i3 = Issue.generate!(:project_id => 1, :tracker_id => 1, :assigned_to => Group.find(11))
374 374 group.users << user
375 375
376 376 query = Query.new(:name => '_', :filters => { 'assigned_to_id' => {:operator => '=', :values => ['me']}})
377 377 result = query.issues
378 378 assert_equal Issue.visible.all(:conditions => {:assigned_to_id => ([2] + user.reload.group_ids)}).sort_by(&:id), result.sort_by(&:id)
379 379
380 380 assert result.include?(i1)
381 381 assert result.include?(i2)
382 382 assert !result.include?(i3)
383 383 end
384 384
385 385 def test_filter_watched_issues
386 386 User.current = User.find(1)
387 387 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '=', :values => ['me']}})
388 388 result = find_issues_with_query(query)
389 389 assert_not_nil result
390 390 assert !result.empty?
391 391 assert_equal Issue.visible.watched_by(User.current).sort_by(&:id), result.sort_by(&:id)
392 392 User.current = nil
393 393 end
394 394
395 395 def test_filter_unwatched_issues
396 396 User.current = User.find(1)
397 397 query = Query.new(:name => '_', :filters => { 'watcher_id' => {:operator => '!', :values => ['me']}})
398 398 result = find_issues_with_query(query)
399 399 assert_not_nil result
400 400 assert !result.empty?
401 401 assert_equal((Issue.visible - Issue.watched_by(User.current)).sort_by(&:id).size, result.sort_by(&:id).size)
402 402 User.current = nil
403 403 end
404 404
405 405 def test_statement_should_be_nil_with_no_filters
406 406 q = Query.new(:name => '_')
407 407 q.filters = {}
408 408
409 409 assert q.valid?
410 410 assert_nil q.statement
411 411 end
412 412
413 413 def test_default_columns
414 414 q = Query.new
415 415 assert !q.columns.empty?
416 416 end
417 417
418 418 def test_set_column_names
419 419 q = Query.new
420 420 q.column_names = ['tracker', :subject, '', 'unknonw_column']
421 421 assert_equal [:tracker, :subject], q.columns.collect {|c| c.name}
422 422 c = q.columns.first
423 423 assert q.has_column?(c)
424 424 end
425 425
426 426 def test_groupable_columns_should_include_custom_fields
427 427 q = Query.new
428 428 assert q.groupable_columns.detect {|c| c.is_a? QueryCustomFieldColumn}
429 429 end
430 430
431 431 def test_grouped_with_valid_column
432 432 q = Query.new(:group_by => 'status')
433 433 assert q.grouped?
434 434 assert_not_nil q.group_by_column
435 435 assert_equal :status, q.group_by_column.name
436 436 assert_not_nil q.group_by_statement
437 437 assert_equal 'status', q.group_by_statement
438 438 end
439 439
440 440 def test_grouped_with_invalid_column
441 441 q = Query.new(:group_by => 'foo')
442 442 assert !q.grouped?
443 443 assert_nil q.group_by_column
444 444 assert_nil q.group_by_statement
445 445 end
446
447 def test_sortable_columns_should_sort_assignees_according_to_user_format_setting
448 with_settings :user_format => 'lastname_coma_firstname' do
449 q = Query.new
450 assert q.sortable_columns.has_key?('assigned_to')
451 assert_equal %w(users.lastname users.firstname users.id), q.sortable_columns['assigned_to']
452 end
453 end
454
455 def test_sortable_columns_should_sort_authors_according_to_user_format_setting
456 with_settings :user_format => 'lastname_coma_firstname' do
457 q = Query.new
458 assert q.sortable_columns.has_key?('author')
459 assert_equal %w(authors.lastname authors.firstname authors.id), q.sortable_columns['author']
460 end
461 end
446 462
447 463 def test_default_sort
448 464 q = Query.new
449 465 assert_equal [], q.sort_criteria
450 466 end
451 467
452 468 def test_set_sort_criteria_with_hash
453 469 q = Query.new
454 470 q.sort_criteria = {'0' => ['priority', 'desc'], '2' => ['tracker']}
455 471 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
456 472 end
457 473
458 474 def test_set_sort_criteria_with_array
459 475 q = Query.new
460 476 q.sort_criteria = [['priority', 'desc'], 'tracker']
461 477 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
462 478 end
463 479
464 480 def test_create_query_with_sort
465 481 q = Query.new(:name => 'Sorted')
466 482 q.sort_criteria = [['priority', 'desc'], 'tracker']
467 483 assert q.save
468 484 q.reload
469 485 assert_equal [['priority', 'desc'], ['tracker', 'asc']], q.sort_criteria
470 486 end
471 487
472 488 def test_sort_by_string_custom_field_asc
473 489 q = Query.new
474 490 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
475 491 assert c
476 492 assert c.sortable
477 493 issues = Issue.find :all,
478 494 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
479 495 :conditions => q.statement,
480 496 :order => "#{c.sortable} ASC"
481 497 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
482 498 assert !values.empty?
483 499 assert_equal values.sort, values
484 500 end
485 501
486 502 def test_sort_by_string_custom_field_desc
487 503 q = Query.new
488 504 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'string' }
489 505 assert c
490 506 assert c.sortable
491 507 issues = Issue.find :all,
492 508 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
493 509 :conditions => q.statement,
494 510 :order => "#{c.sortable} DESC"
495 511 values = issues.collect {|i| i.custom_value_for(c.custom_field).to_s}
496 512 assert !values.empty?
497 513 assert_equal values.sort.reverse, values
498 514 end
499 515
500 516 def test_sort_by_float_custom_field_asc
501 517 q = Query.new
502 518 c = q.available_columns.find {|col| col.is_a?(QueryCustomFieldColumn) && col.custom_field.field_format == 'float' }
503 519 assert c
504 520 assert c.sortable
505 521 issues = Issue.find :all,
506 522 :include => [ :assigned_to, :status, :tracker, :project, :priority ],
507 523 :conditions => q.statement,
508 524 :order => "#{c.sortable} ASC"
509 525 values = issues.collect {|i| begin; Kernel.Float(i.custom_value_for(c.custom_field).to_s); rescue; nil; end}.compact
510 526 assert !values.empty?
511 527 assert_equal values.sort, values
512 528 end
513 529
514 530 def test_invalid_query_should_raise_query_statement_invalid_error
515 531 q = Query.new
516 532 assert_raise Query::StatementInvalid do
517 533 q.issues(:conditions => "foo = 1")
518 534 end
519 535 end
520 536
521 537 def test_issue_count
522 538 q = Query.new(:name => '_')
523 539 issue_count = q.issue_count
524 540 assert_equal q.issues.size, issue_count
525 541 end
526 542
527 543 def test_issue_count_with_archived_issues
528 544 p = Project.generate!( :status => Project::STATUS_ARCHIVED )
529 545 i = Issue.generate!( :project => p, :tracker => p.trackers.first )
530 546 assert !i.visible?
531 547
532 548 test_issue_count
533 549 end
534 550
535 551 def test_issue_count_by_association_group
536 552 q = Query.new(:name => '_', :group_by => 'assigned_to')
537 553 count_by_group = q.issue_count_by_group
538 554 assert_kind_of Hash, count_by_group
539 555 assert_equal %w(NilClass User), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
540 556 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
541 557 assert count_by_group.has_key?(User.find(3))
542 558 end
543 559
544 560 def test_issue_count_by_list_custom_field_group
545 561 q = Query.new(:name => '_', :group_by => 'cf_1')
546 562 count_by_group = q.issue_count_by_group
547 563 assert_kind_of Hash, count_by_group
548 564 assert_equal %w(NilClass String), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
549 565 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
550 566 assert count_by_group.has_key?('MySQL')
551 567 end
552 568
553 569 def test_issue_count_by_date_custom_field_group
554 570 q = Query.new(:name => '_', :group_by => 'cf_8')
555 571 count_by_group = q.issue_count_by_group
556 572 assert_kind_of Hash, count_by_group
557 573 assert_equal %w(Date NilClass), count_by_group.keys.collect {|k| k.class.name}.uniq.sort
558 574 assert_equal %w(Fixnum), count_by_group.values.collect {|k| k.class.name}.uniq
559 575 end
560 576
561 577 def test_label_for
562 578 q = Query.new
563 579 assert_equal 'assigned_to', q.label_for('assigned_to_id')
564 580 end
565 581
566 582 def test_editable_by
567 583 admin = User.find(1)
568 584 manager = User.find(2)
569 585 developer = User.find(3)
570 586
571 587 # Public query on project 1
572 588 q = Query.find(1)
573 589 assert q.editable_by?(admin)
574 590 assert q.editable_by?(manager)
575 591 assert !q.editable_by?(developer)
576 592
577 593 # Private query on project 1
578 594 q = Query.find(2)
579 595 assert q.editable_by?(admin)
580 596 assert !q.editable_by?(manager)
581 597 assert q.editable_by?(developer)
582 598
583 599 # Private query for all projects
584 600 q = Query.find(3)
585 601 assert q.editable_by?(admin)
586 602 assert !q.editable_by?(manager)
587 603 assert q.editable_by?(developer)
588 604
589 605 # Public query for all projects
590 606 q = Query.find(4)
591 607 assert q.editable_by?(admin)
592 608 assert !q.editable_by?(manager)
593 609 assert !q.editable_by?(developer)
594 610 end
595 611
596 612 def test_visible_scope
597 613 query_ids = Query.visible(User.anonymous).map(&:id)
598 614
599 615 assert query_ids.include?(1), 'public query on public project was not visible'
600 616 assert query_ids.include?(4), 'public query for all projects was not visible'
601 617 assert !query_ids.include?(2), 'private query on public project was visible'
602 618 assert !query_ids.include?(3), 'private query for all projects was visible'
603 619 assert !query_ids.include?(7), 'public query on private project was visible'
604 620 end
605 621
606 622 context "#available_filters" do
607 623 setup do
608 624 @query = Query.new(:name => "_")
609 625 end
610 626
611 627 should "include users of visible projects in cross-project view" do
612 628 users = @query.available_filters["assigned_to_id"]
613 629 assert_not_nil users
614 630 assert users[:values].map{|u|u[1]}.include?("3")
615 631 end
616 632
617 633 should "include visible projects in cross-project view" do
618 634 projects = @query.available_filters["project_id"]
619 635 assert_not_nil projects
620 636 assert projects[:values].map{|u|u[1]}.include?("1")
621 637 end
622 638
623 639 context "'member_of_group' filter" do
624 640 should "be present" do
625 641 assert @query.available_filters.keys.include?("member_of_group")
626 642 end
627 643
628 644 should "be an optional list" do
629 645 assert_equal :list_optional, @query.available_filters["member_of_group"][:type]
630 646 end
631 647
632 648 should "have a list of the groups as values" do
633 649 Group.destroy_all # No fixtures
634 650 group1 = Group.generate!.reload
635 651 group2 = Group.generate!.reload
636 652
637 653 expected_group_list = [
638 654 [group1.name, group1.id.to_s],
639 655 [group2.name, group2.id.to_s]
640 656 ]
641 657 assert_equal expected_group_list.sort, @query.available_filters["member_of_group"][:values].sort
642 658 end
643 659
644 660 end
645 661
646 662 context "'assigned_to_role' filter" do
647 663 should "be present" do
648 664 assert @query.available_filters.keys.include?("assigned_to_role")
649 665 end
650 666
651 667 should "be an optional list" do
652 668 assert_equal :list_optional, @query.available_filters["assigned_to_role"][:type]
653 669 end
654 670
655 671 should "have a list of the Roles as values" do
656 672 assert @query.available_filters["assigned_to_role"][:values].include?(['Manager','1'])
657 673 assert @query.available_filters["assigned_to_role"][:values].include?(['Developer','2'])
658 674 assert @query.available_filters["assigned_to_role"][:values].include?(['Reporter','3'])
659 675 end
660 676
661 677 should "not include the built in Roles as values" do
662 678 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Non member','4'])
663 679 assert ! @query.available_filters["assigned_to_role"][:values].include?(['Anonymous','5'])
664 680 end
665 681
666 682 end
667 683
668 684 end
669 685
670 686 context "#statement" do
671 687 context "with 'member_of_group' filter" do
672 688 setup do
673 689 Group.destroy_all # No fixtures
674 690 @user_in_group = User.generate!
675 691 @second_user_in_group = User.generate!
676 692 @user_in_group2 = User.generate!
677 693 @user_not_in_group = User.generate!
678 694
679 695 @group = Group.generate!.reload
680 696 @group.users << @user_in_group
681 697 @group.users << @second_user_in_group
682 698
683 699 @group2 = Group.generate!.reload
684 700 @group2.users << @user_in_group2
685 701
686 702 end
687 703
688 704 should "search assigned to for users in the group" do
689 705 @query = Query.new(:name => '_')
690 706 @query.add_filter('member_of_group', '=', [@group.id.to_s])
691 707
692 708 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}')"
693 709 assert_find_issues_with_query_is_successful @query
694 710 end
695 711
696 712 should "search not assigned to any group member (none)" do
697 713 @query = Query.new(:name => '_')
698 714 @query.add_filter('member_of_group', '!*', [''])
699 715
700 716 # Users not in a group
701 717 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IS NULL OR #{Issue.table_name}.assigned_to_id NOT IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
702 718 assert_find_issues_with_query_is_successful @query
703 719 end
704 720
705 721 should "search assigned to any group member (all)" do
706 722 @query = Query.new(:name => '_')
707 723 @query.add_filter('member_of_group', '*', [''])
708 724
709 725 # Only users in a group
710 726 assert_query_statement_includes @query, "#{Issue.table_name}.assigned_to_id IN ('#{@user_in_group.id}','#{@second_user_in_group.id}','#{@user_in_group2.id}')"
711 727 assert_find_issues_with_query_is_successful @query
712 728 end
713 729
714 730 should "return an empty set with = empty group" do
715 731 @empty_group = Group.generate!
716 732 @query = Query.new(:name => '_')
717 733 @query.add_filter('member_of_group', '=', [@empty_group.id.to_s])
718 734
719 735 assert_equal [], find_issues_with_query(@query)
720 736 end
721 737
722 738 should "return issues with ! empty group" do
723 739 @empty_group = Group.generate!
724 740 @query = Query.new(:name => '_')
725 741 @query.add_filter('member_of_group', '!', [@empty_group.id.to_s])
726 742
727 743 assert_find_issues_with_query_is_successful @query
728 744 end
729 745 end
730 746
731 747 context "with 'assigned_to_role' filter" do
732 748 setup do
733 749 @manager_role = Role.find_by_name('Manager')
734 750 @developer_role = Role.find_by_name('Developer')
735 751
736 752 @project = Project.generate!
737 753 @manager = User.generate!
738 754 @developer = User.generate!
739 755 @boss = User.generate!
740 756 @guest = User.generate!
741 757 User.add_to_project(@manager, @project, @manager_role)
742 758 User.add_to_project(@developer, @project, @developer_role)
743 759 User.add_to_project(@boss, @project, [@manager_role, @developer_role])
744 760
745 761 @issue1 = Issue.generate_for_project!(@project, :assigned_to_id => @manager.id)
746 762 @issue2 = Issue.generate_for_project!(@project, :assigned_to_id => @developer.id)
747 763 @issue3 = Issue.generate_for_project!(@project, :assigned_to_id => @boss.id)
748 764 @issue4 = Issue.generate_for_project!(@project, :assigned_to_id => @guest.id)
749 765 @issue5 = Issue.generate_for_project!(@project)
750 766 end
751 767
752 768 should "search assigned to for users with the Role" do
753 769 @query = Query.new(:name => '_', :project => @project)
754 770 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
755 771
756 772 assert_query_result [@issue1, @issue3], @query
757 773 end
758 774
759 775 should "search assigned to for users with the Role on the issue project" do
760 776 other_project = Project.generate!
761 777 User.add_to_project(@developer, other_project, @manager_role)
762 778
763 779 @query = Query.new(:name => '_', :project => @project)
764 780 @query.add_filter('assigned_to_role', '=', [@manager_role.id.to_s])
765 781
766 782 assert_query_result [@issue1, @issue3], @query
767 783 end
768 784
769 785 should "return an empty set with empty role" do
770 786 @empty_role = Role.generate!
771 787 @query = Query.new(:name => '_', :project => @project)
772 788 @query.add_filter('assigned_to_role', '=', [@empty_role.id.to_s])
773 789
774 790 assert_query_result [], @query
775 791 end
776 792
777 793 should "search assigned to for users without the Role" do
778 794 @query = Query.new(:name => '_', :project => @project)
779 795 @query.add_filter('assigned_to_role', '!', [@manager_role.id.to_s])
780 796
781 797 assert_query_result [@issue2, @issue4, @issue5], @query
782 798 end
783 799
784 800 should "search assigned to for users not assigned to any Role (none)" do
785 801 @query = Query.new(:name => '_', :project => @project)
786 802 @query.add_filter('assigned_to_role', '!*', [''])
787 803
788 804 assert_query_result [@issue4, @issue5], @query
789 805 end
790 806
791 807 should "search assigned to for users assigned to any Role (all)" do
792 808 @query = Query.new(:name => '_', :project => @project)
793 809 @query.add_filter('assigned_to_role', '*', [''])
794 810
795 811 assert_query_result [@issue1, @issue2, @issue3], @query
796 812 end
797 813
798 814 should "return issues with ! empty role" do
799 815 @empty_role = Role.generate!
800 816 @query = Query.new(:name => '_', :project => @project)
801 817 @query.add_filter('assigned_to_role', '!', [@empty_role.id.to_s])
802 818
803 819 assert_query_result [@issue1, @issue2, @issue3, @issue4, @issue5], @query
804 820 end
805 821 end
806 822 end
807 823
808 824 end
@@ -1,853 +1,877
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class UserTest < ActiveSupport::TestCase
21 21 fixtures :users, :members, :projects, :roles, :member_roles, :auth_sources,
22 22 :trackers, :issue_statuses,
23 23 :projects_trackers,
24 24 :watchers,
25 25 :issue_categories, :enumerations, :issues,
26 26 :journals, :journal_details,
27 27 :groups_users,
28 28 :enabled_modules,
29 29 :workflows
30 30
31 31 def setup
32 32 @admin = User.find(1)
33 33 @jsmith = User.find(2)
34 34 @dlopper = User.find(3)
35 35 end
36 36
37 37 test 'object_daddy creation' do
38 38 User.generate_with_protected!(:firstname => 'Testing connection')
39 39 User.generate_with_protected!(:firstname => 'Testing connection')
40 40 assert_equal 2, User.count(:all, :conditions => {:firstname => 'Testing connection'})
41 41 end
42 42
43 43 def test_truth
44 44 assert_kind_of User, @jsmith
45 45 end
46 46
47 47 def test_mail_should_be_stripped
48 48 u = User.new
49 49 u.mail = " foo@bar.com "
50 50 assert_equal "foo@bar.com", u.mail
51 51 end
52 52
53 53 def test_mail_validation
54 54 u = User.new
55 55 u.mail = ''
56 56 assert !u.valid?
57 57 assert_equal I18n.translate('activerecord.errors.messages.blank'), u.errors.on(:mail)
58 58 end
59 59
60 60 def test_create
61 61 user = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
62 62
63 63 user.login = "jsmith"
64 64 user.password, user.password_confirmation = "password", "password"
65 65 # login uniqueness
66 66 assert !user.save
67 67 assert_equal 1, user.errors.count
68 68
69 69 user.login = "newuser"
70 70 user.password, user.password_confirmation = "passwd", "password"
71 71 # password confirmation
72 72 assert !user.save
73 73 assert_equal 1, user.errors.count
74 74
75 75 user.password, user.password_confirmation = "password", "password"
76 76 assert user.save
77 77 end
78 78
79 79 context "User#before_create" do
80 80 should "set the mail_notification to the default Setting" do
81 81 @user1 = User.generate_with_protected!
82 82 assert_equal 'only_my_events', @user1.mail_notification
83 83
84 84 with_settings :default_notification_option => 'all' do
85 85 @user2 = User.generate_with_protected!
86 86 assert_equal 'all', @user2.mail_notification
87 87 end
88 88 end
89 89 end
90 90
91 91 context "User.login" do
92 92 should "be case-insensitive." do
93 93 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
94 94 u.login = 'newuser'
95 95 u.password, u.password_confirmation = "password", "password"
96 96 assert u.save
97 97
98 98 u = User.new(:firstname => "Similar", :lastname => "User", :mail => "similaruser@somenet.foo")
99 99 u.login = 'NewUser'
100 100 u.password, u.password_confirmation = "password", "password"
101 101 assert !u.save
102 102 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:login)
103 103 end
104 104 end
105 105
106 106 def test_mail_uniqueness_should_not_be_case_sensitive
107 107 u = User.new(:firstname => "new", :lastname => "user", :mail => "newuser@somenet.foo")
108 108 u.login = 'newuser1'
109 109 u.password, u.password_confirmation = "password", "password"
110 110 assert u.save
111 111
112 112 u = User.new(:firstname => "new", :lastname => "user", :mail => "newUser@Somenet.foo")
113 113 u.login = 'newuser2'
114 114 u.password, u.password_confirmation = "password", "password"
115 115 assert !u.save
116 116 assert_equal I18n.translate('activerecord.errors.messages.taken'), u.errors.on(:mail)
117 117 end
118 118
119 119 def test_update
120 120 assert_equal "admin", @admin.login
121 121 @admin.login = "john"
122 122 assert @admin.save, @admin.errors.full_messages.join("; ")
123 123 @admin.reload
124 124 assert_equal "john", @admin.login
125 125 end
126 126
127 127 def test_destroy_should_delete_members_and_roles
128 128 members = Member.find_all_by_user_id(2)
129 129 ms = members.size
130 130 rs = members.collect(&:roles).flatten.size
131 131
132 132 assert_difference 'Member.count', - ms do
133 133 assert_difference 'MemberRole.count', - rs do
134 134 User.find(2).destroy
135 135 end
136 136 end
137 137
138 138 assert_nil User.find_by_id(2)
139 139 assert Member.find_all_by_user_id(2).empty?
140 140 end
141 141
142 142 def test_destroy_should_update_attachments
143 143 attachment = Attachment.create!(:container => Project.find(1),
144 144 :file => uploaded_test_file("testfile.txt", "text/plain"),
145 145 :author_id => 2)
146 146
147 147 User.find(2).destroy
148 148 assert_nil User.find_by_id(2)
149 149 assert_equal User.anonymous, attachment.reload.author
150 150 end
151 151
152 152 def test_destroy_should_update_comments
153 153 comment = Comment.create!(
154 154 :commented => News.create!(:project_id => 1, :author_id => 1, :title => 'foo', :description => 'foo'),
155 155 :author => User.find(2),
156 156 :comments => 'foo'
157 157 )
158 158
159 159 User.find(2).destroy
160 160 assert_nil User.find_by_id(2)
161 161 assert_equal User.anonymous, comment.reload.author
162 162 end
163 163
164 164 def test_destroy_should_update_issues
165 165 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
166 166
167 167 User.find(2).destroy
168 168 assert_nil User.find_by_id(2)
169 169 assert_equal User.anonymous, issue.reload.author
170 170 end
171 171
172 172 def test_destroy_should_unassign_issues
173 173 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
174 174
175 175 User.find(2).destroy
176 176 assert_nil User.find_by_id(2)
177 177 assert_nil issue.reload.assigned_to
178 178 end
179 179
180 180 def test_destroy_should_update_journals
181 181 issue = Issue.create!(:project_id => 1, :author_id => 2, :tracker_id => 1, :subject => 'foo')
182 182 issue.init_journal(User.find(2), "update")
183 183 issue.save!
184 184
185 185 User.find(2).destroy
186 186 assert_nil User.find_by_id(2)
187 187 assert_equal User.anonymous, issue.journals.first.reload.user
188 188 end
189 189
190 190 def test_destroy_should_update_journal_details_old_value
191 191 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo', :assigned_to_id => 2)
192 192 issue.init_journal(User.find(1), "update")
193 193 issue.assigned_to_id = nil
194 194 assert_difference 'JournalDetail.count' do
195 195 issue.save!
196 196 end
197 197 journal_detail = JournalDetail.first(:order => 'id DESC')
198 198 assert_equal '2', journal_detail.old_value
199 199
200 200 User.find(2).destroy
201 201 assert_nil User.find_by_id(2)
202 202 assert_equal User.anonymous.id.to_s, journal_detail.reload.old_value
203 203 end
204 204
205 205 def test_destroy_should_update_journal_details_value
206 206 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
207 207 issue.init_journal(User.find(1), "update")
208 208 issue.assigned_to_id = 2
209 209 assert_difference 'JournalDetail.count' do
210 210 issue.save!
211 211 end
212 212 journal_detail = JournalDetail.first(:order => 'id DESC')
213 213 assert_equal '2', journal_detail.value
214 214
215 215 User.find(2).destroy
216 216 assert_nil User.find_by_id(2)
217 217 assert_equal User.anonymous.id.to_s, journal_detail.reload.value
218 218 end
219 219
220 220 def test_destroy_should_update_messages
221 221 board = Board.create!(:project_id => 1, :name => 'Board', :description => 'Board')
222 222 message = Message.create!(:board_id => board.id, :author_id => 2, :subject => 'foo', :content => 'foo')
223 223
224 224 User.find(2).destroy
225 225 assert_nil User.find_by_id(2)
226 226 assert_equal User.anonymous, message.reload.author
227 227 end
228 228
229 229 def test_destroy_should_update_news
230 230 news = News.create!(:project_id => 1, :author_id => 2, :title => 'foo', :description => 'foo')
231 231
232 232 User.find(2).destroy
233 233 assert_nil User.find_by_id(2)
234 234 assert_equal User.anonymous, news.reload.author
235 235 end
236 236
237 237 def test_destroy_should_delete_private_queries
238 238 query = Query.new(:name => 'foo', :is_public => false)
239 239 query.project_id = 1
240 240 query.user_id = 2
241 241 query.save!
242 242
243 243 User.find(2).destroy
244 244 assert_nil User.find_by_id(2)
245 245 assert_nil Query.find_by_id(query.id)
246 246 end
247 247
248 248 def test_destroy_should_update_public_queries
249 249 query = Query.new(:name => 'foo', :is_public => true)
250 250 query.project_id = 1
251 251 query.user_id = 2
252 252 query.save!
253 253
254 254 User.find(2).destroy
255 255 assert_nil User.find_by_id(2)
256 256 assert_equal User.anonymous, query.reload.user
257 257 end
258 258
259 259 def test_destroy_should_update_time_entries
260 260 entry = TimeEntry.new(:hours => '2', :spent_on => Date.today, :activity => TimeEntryActivity.create!(:name => 'foo'))
261 261 entry.project_id = 1
262 262 entry.user_id = 2
263 263 entry.save!
264 264
265 265 User.find(2).destroy
266 266 assert_nil User.find_by_id(2)
267 267 assert_equal User.anonymous, entry.reload.user
268 268 end
269 269
270 270 def test_destroy_should_delete_tokens
271 271 token = Token.create!(:user_id => 2, :value => 'foo')
272 272
273 273 User.find(2).destroy
274 274 assert_nil User.find_by_id(2)
275 275 assert_nil Token.find_by_id(token.id)
276 276 end
277 277
278 278 def test_destroy_should_delete_watchers
279 279 issue = Issue.create!(:project_id => 1, :author_id => 1, :tracker_id => 1, :subject => 'foo')
280 280 watcher = Watcher.create!(:user_id => 2, :watchable => issue)
281 281
282 282 User.find(2).destroy
283 283 assert_nil User.find_by_id(2)
284 284 assert_nil Watcher.find_by_id(watcher.id)
285 285 end
286 286
287 287 def test_destroy_should_update_wiki_contents
288 288 wiki_content = WikiContent.create!(
289 289 :text => 'foo',
290 290 :author_id => 2,
291 291 :page => WikiPage.create!(:title => 'Foo', :wiki => Wiki.create!(:project_id => 1, :start_page => 'Start'))
292 292 )
293 293 wiki_content.text = 'bar'
294 294 assert_difference 'WikiContent::Version.count' do
295 295 wiki_content.save!
296 296 end
297 297
298 298 User.find(2).destroy
299 299 assert_nil User.find_by_id(2)
300 300 assert_equal User.anonymous, wiki_content.reload.author
301 301 wiki_content.versions.each do |version|
302 302 assert_equal User.anonymous, version.reload.author
303 303 end
304 304 end
305 305
306 306 def test_destroy_should_nullify_issue_categories
307 307 category = IssueCategory.create!(:project_id => 1, :assigned_to_id => 2, :name => 'foo')
308 308
309 309 User.find(2).destroy
310 310 assert_nil User.find_by_id(2)
311 311 assert_nil category.reload.assigned_to_id
312 312 end
313 313
314 314 def test_destroy_should_nullify_changesets
315 315 changeset = Changeset.create!(
316 316 :repository => Repository::Subversion.create!(
317 317 :project_id => 1,
318 318 :url => 'file:///var/svn'
319 319 ),
320 320 :revision => '12',
321 321 :committed_on => Time.now,
322 322 :committer => 'jsmith'
323 323 )
324 324 assert_equal 2, changeset.user_id
325 325
326 326 User.find(2).destroy
327 327 assert_nil User.find_by_id(2)
328 328 assert_nil changeset.reload.user_id
329 329 end
330 330
331 331 def test_anonymous_user_should_not_be_destroyable
332 332 assert_no_difference 'User.count' do
333 333 assert_equal false, User.anonymous.destroy
334 334 end
335 335 end
336 336
337 337 def test_validate_login_presence
338 338 @admin.login = ""
339 339 assert !@admin.save
340 340 assert_equal 1, @admin.errors.count
341 341 end
342 342
343 343 def test_validate_mail_notification_inclusion
344 344 u = User.new
345 345 u.mail_notification = 'foo'
346 346 u.save
347 347 assert_not_nil u.errors[:mail_notification]
348 348 end
349 349
350 350 context "User#try_to_login" do
351 351 should "fall-back to case-insensitive if user login is not found as-typed." do
352 352 user = User.try_to_login("AdMin", "admin")
353 353 assert_kind_of User, user
354 354 assert_equal "admin", user.login
355 355 end
356 356
357 357 should "select the exact matching user first" do
358 358 case_sensitive_user = User.generate_with_protected!(
359 359 :login => 'changed', :password => 'admin',
360 360 :password_confirmation => 'admin')
361 361 # bypass validations to make it appear like existing data
362 362 case_sensitive_user.update_attribute(:login, 'ADMIN')
363 363
364 364 user = User.try_to_login("ADMIN", "admin")
365 365 assert_kind_of User, user
366 366 assert_equal "ADMIN", user.login
367 367
368 368 end
369 369 end
370 370
371 371 def test_password
372 372 user = User.try_to_login("admin", "admin")
373 373 assert_kind_of User, user
374 374 assert_equal "admin", user.login
375 375 user.password = "hello"
376 376 assert user.save
377 377
378 378 user = User.try_to_login("admin", "hello")
379 379 assert_kind_of User, user
380 380 assert_equal "admin", user.login
381 381 end
382 382
383 383 def test_validate_password_length
384 384 with_settings :password_min_length => '100' do
385 385 user = User.new(:firstname => "new100", :lastname => "user100", :mail => "newuser100@somenet.foo")
386 386 user.login = "newuser100"
387 387 user.password, user.password_confirmation = "password100", "password100"
388 388 assert !user.save
389 389 assert_equal 1, user.errors.count
390 390 end
391 391 end
392 392
393 393 def test_name_format
394 394 assert_equal 'Smith, John', @jsmith.name(:lastname_coma_firstname)
395 395 Setting.user_format = :firstname_lastname
396 396 assert_equal 'John Smith', @jsmith.reload.name
397 397 Setting.user_format = :username
398 398 assert_equal 'jsmith', @jsmith.reload.name
399 399 end
400
401 def test_fields_for_order_statement_should_return_fields_according_user_format_setting
402 with_settings :user_format => 'lastname_coma_firstname' do
403 assert_equal ['users.lastname', 'users.firstname', 'users.id'], User.fields_for_order_statement
404 end
405 end
406
407 def test_fields_for_order_statement_width_table_name_should_prepend_table_name
408 with_settings :user_format => 'lastname_firstname' do
409 assert_equal ['authors.lastname', 'authors.firstname', 'authors.id'], User.fields_for_order_statement('authors')
410 end
411 end
412
413 def test_fields_for_order_statement_with_blank_format_should_return_default
414 with_settings :user_format => '' do
415 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
416 end
417 end
418
419 def test_fields_for_order_statement_with_invalid_format_should_return_default
420 with_settings :user_format => 'foo' do
421 assert_equal ['users.firstname', 'users.lastname', 'users.id'], User.fields_for_order_statement
422 end
423 end
400 424
401 425 def test_lock
402 426 user = User.try_to_login("jsmith", "jsmith")
403 427 assert_equal @jsmith, user
404 428
405 429 @jsmith.status = User::STATUS_LOCKED
406 430 assert @jsmith.save
407 431
408 432 user = User.try_to_login("jsmith", "jsmith")
409 433 assert_equal nil, user
410 434 end
411 435
412 436 context ".try_to_login" do
413 437 context "with good credentials" do
414 438 should "return the user" do
415 439 user = User.try_to_login("admin", "admin")
416 440 assert_kind_of User, user
417 441 assert_equal "admin", user.login
418 442 end
419 443 end
420 444
421 445 context "with wrong credentials" do
422 446 should "return nil" do
423 447 assert_nil User.try_to_login("admin", "foo")
424 448 end
425 449 end
426 450 end
427 451
428 452 if ldap_configured?
429 453 context "#try_to_login using LDAP" do
430 454 context "with failed connection to the LDAP server" do
431 455 should "return nil" do
432 456 @auth_source = AuthSourceLdap.find(1)
433 457 AuthSource.any_instance.stubs(:initialize_ldap_con).raises(Net::LDAP::LdapError, 'Cannot connect')
434 458
435 459 assert_equal nil, User.try_to_login('edavis', 'wrong')
436 460 end
437 461 end
438 462
439 463 context "with an unsuccessful authentication" do
440 464 should "return nil" do
441 465 assert_equal nil, User.try_to_login('edavis', 'wrong')
442 466 end
443 467 end
444 468
445 469 context "on the fly registration" do
446 470 setup do
447 471 @auth_source = AuthSourceLdap.find(1)
448 472 end
449 473
450 474 context "with a successful authentication" do
451 475 should "create a new user account if it doesn't exist" do
452 476 assert_difference('User.count') do
453 477 user = User.try_to_login('edavis', '123456')
454 478 assert !user.admin?
455 479 end
456 480 end
457 481
458 482 should "retrieve existing user" do
459 483 user = User.try_to_login('edavis', '123456')
460 484 user.admin = true
461 485 user.save!
462 486
463 487 assert_no_difference('User.count') do
464 488 user = User.try_to_login('edavis', '123456')
465 489 assert user.admin?
466 490 end
467 491 end
468 492 end
469 493 end
470 494 end
471 495
472 496 else
473 497 puts "Skipping LDAP tests."
474 498 end
475 499
476 500 def test_create_anonymous
477 501 AnonymousUser.delete_all
478 502 anon = User.anonymous
479 503 assert !anon.new_record?
480 504 assert_kind_of AnonymousUser, anon
481 505 end
482 506
483 507 def test_ensure_single_anonymous_user
484 508 AnonymousUser.delete_all
485 509 anon1 = User.anonymous
486 510 assert !anon1.new_record?
487 511 assert_kind_of AnonymousUser, anon1
488 512 anon2 = AnonymousUser.create(
489 513 :lastname => 'Anonymous', :firstname => '',
490 514 :mail => '', :login => '', :status => 0)
491 515 assert_equal 1, anon2.errors.count
492 516 end
493 517
494 518 should_have_one :rss_token
495 519
496 520 def test_rss_key
497 521 assert_nil @jsmith.rss_token
498 522 key = @jsmith.rss_key
499 523 assert_equal 40, key.length
500 524
501 525 @jsmith.reload
502 526 assert_equal key, @jsmith.rss_key
503 527 end
504 528
505 529
506 530 should_have_one :api_token
507 531
508 532 context "User#api_key" do
509 533 should "generate a new one if the user doesn't have one" do
510 534 user = User.generate_with_protected!(:api_token => nil)
511 535 assert_nil user.api_token
512 536
513 537 key = user.api_key
514 538 assert_equal 40, key.length
515 539 user.reload
516 540 assert_equal key, user.api_key
517 541 end
518 542
519 543 should "return the existing api token value" do
520 544 user = User.generate_with_protected!
521 545 token = Token.generate!(:action => 'api')
522 546 user.api_token = token
523 547 assert user.save
524 548
525 549 assert_equal token.value, user.api_key
526 550 end
527 551 end
528 552
529 553 context "User#find_by_api_key" do
530 554 should "return nil if no matching key is found" do
531 555 assert_nil User.find_by_api_key('zzzzzzzzz')
532 556 end
533 557
534 558 should "return nil if the key is found for an inactive user" do
535 559 user = User.generate_with_protected!(:status => User::STATUS_LOCKED)
536 560 token = Token.generate!(:action => 'api')
537 561 user.api_token = token
538 562 user.save
539 563
540 564 assert_nil User.find_by_api_key(token.value)
541 565 end
542 566
543 567 should "return the user if the key is found for an active user" do
544 568 user = User.generate_with_protected!(:status => User::STATUS_ACTIVE)
545 569 token = Token.generate!(:action => 'api')
546 570 user.api_token = token
547 571 user.save
548 572
549 573 assert_equal user, User.find_by_api_key(token.value)
550 574 end
551 575 end
552 576
553 577 def test_roles_for_project
554 578 # user with a role
555 579 roles = @jsmith.roles_for_project(Project.find(1))
556 580 assert_kind_of Role, roles.first
557 581 assert_equal "Manager", roles.first.name
558 582
559 583 # user with no role
560 584 assert_nil @dlopper.roles_for_project(Project.find(2)).detect {|role| role.member?}
561 585 end
562 586
563 587 def test_projects_by_role_for_user_with_role
564 588 user = User.find(2)
565 589 assert_kind_of Hash, user.projects_by_role
566 590 assert_equal 2, user.projects_by_role.size
567 591 assert_equal [1,5], user.projects_by_role[Role.find(1)].collect(&:id).sort
568 592 assert_equal [2], user.projects_by_role[Role.find(2)].collect(&:id).sort
569 593 end
570 594
571 595 def test_projects_by_role_for_user_with_no_role
572 596 user = User.generate!
573 597 assert_equal({}, user.projects_by_role)
574 598 end
575 599
576 600 def test_projects_by_role_for_anonymous
577 601 assert_equal({}, User.anonymous.projects_by_role)
578 602 end
579 603
580 604 def test_valid_notification_options
581 605 # without memberships
582 606 assert_equal 5, User.find(7).valid_notification_options.size
583 607 # with memberships
584 608 assert_equal 6, User.find(2).valid_notification_options.size
585 609 end
586 610
587 611 def test_valid_notification_options_class_method
588 612 assert_equal 5, User.valid_notification_options.size
589 613 assert_equal 5, User.valid_notification_options(User.find(7)).size
590 614 assert_equal 6, User.valid_notification_options(User.find(2)).size
591 615 end
592 616
593 617 def test_mail_notification_all
594 618 @jsmith.mail_notification = 'all'
595 619 @jsmith.notified_project_ids = []
596 620 @jsmith.save
597 621 @jsmith.reload
598 622 assert @jsmith.projects.first.recipients.include?(@jsmith.mail)
599 623 end
600 624
601 625 def test_mail_notification_selected
602 626 @jsmith.mail_notification = 'selected'
603 627 @jsmith.notified_project_ids = [1]
604 628 @jsmith.save
605 629 @jsmith.reload
606 630 assert Project.find(1).recipients.include?(@jsmith.mail)
607 631 end
608 632
609 633 def test_mail_notification_only_my_events
610 634 @jsmith.mail_notification = 'only_my_events'
611 635 @jsmith.notified_project_ids = []
612 636 @jsmith.save
613 637 @jsmith.reload
614 638 assert !@jsmith.projects.first.recipients.include?(@jsmith.mail)
615 639 end
616 640
617 641 def test_comments_sorting_preference
618 642 assert !@jsmith.wants_comments_in_reverse_order?
619 643 @jsmith.pref.comments_sorting = 'asc'
620 644 assert !@jsmith.wants_comments_in_reverse_order?
621 645 @jsmith.pref.comments_sorting = 'desc'
622 646 assert @jsmith.wants_comments_in_reverse_order?
623 647 end
624 648
625 649 def test_find_by_mail_should_be_case_insensitive
626 650 u = User.find_by_mail('JSmith@somenet.foo')
627 651 assert_not_nil u
628 652 assert_equal 'jsmith@somenet.foo', u.mail
629 653 end
630 654
631 655 def test_random_password
632 656 u = User.new
633 657 u.random_password
634 658 assert !u.password.blank?
635 659 assert !u.password_confirmation.blank?
636 660 end
637 661
638 662 context "#change_password_allowed?" do
639 663 should "be allowed if no auth source is set" do
640 664 user = User.generate_with_protected!
641 665 assert user.change_password_allowed?
642 666 end
643 667
644 668 should "delegate to the auth source" do
645 669 user = User.generate_with_protected!
646 670
647 671 allowed_auth_source = AuthSource.generate!
648 672 def allowed_auth_source.allow_password_changes?; true; end
649 673
650 674 denied_auth_source = AuthSource.generate!
651 675 def denied_auth_source.allow_password_changes?; false; end
652 676
653 677 assert user.change_password_allowed?
654 678
655 679 user.auth_source = allowed_auth_source
656 680 assert user.change_password_allowed?, "User not allowed to change password, though auth source does"
657 681
658 682 user.auth_source = denied_auth_source
659 683 assert !user.change_password_allowed?, "User allowed to change password, though auth source does not"
660 684 end
661 685
662 686 end
663 687
664 688 context "#allowed_to?" do
665 689 context "with a unique project" do
666 690 should "return false if project is archived" do
667 691 project = Project.find(1)
668 692 Project.any_instance.stubs(:status).returns(Project::STATUS_ARCHIVED)
669 693 assert ! @admin.allowed_to?(:view_issues, Project.find(1))
670 694 end
671 695
672 696 should "return false if related module is disabled" do
673 697 project = Project.find(1)
674 698 project.enabled_module_names = ["issue_tracking"]
675 699 assert @admin.allowed_to?(:add_issues, project)
676 700 assert ! @admin.allowed_to?(:view_wiki_pages, project)
677 701 end
678 702
679 703 should "authorize nearly everything for admin users" do
680 704 project = Project.find(1)
681 705 assert ! @admin.member_of?(project)
682 706 %w(edit_issues delete_issues manage_news manage_documents manage_wiki).each do |p|
683 707 assert @admin.allowed_to?(p.to_sym, project)
684 708 end
685 709 end
686 710
687 711 should "authorize normal users depending on their roles" do
688 712 project = Project.find(1)
689 713 assert @jsmith.allowed_to?(:delete_messages, project) #Manager
690 714 assert ! @dlopper.allowed_to?(:delete_messages, project) #Developper
691 715 end
692 716 end
693 717
694 718 context "with multiple projects" do
695 719 should "return false if array is empty" do
696 720 assert ! @admin.allowed_to?(:view_project, [])
697 721 end
698 722
699 723 should "return true only if user has permission on all these projects" do
700 724 assert @admin.allowed_to?(:view_project, Project.all)
701 725 assert ! @dlopper.allowed_to?(:view_project, Project.all) #cannot see Project(2)
702 726 assert @jsmith.allowed_to?(:edit_issues, @jsmith.projects) #Manager or Developer everywhere
703 727 assert ! @jsmith.allowed_to?(:delete_issue_watchers, @jsmith.projects) #Dev cannot delete_issue_watchers
704 728 end
705 729
706 730 should "behave correctly with arrays of 1 project" do
707 731 assert ! User.anonymous.allowed_to?(:delete_issues, [Project.first])
708 732 end
709 733 end
710 734
711 735 context "with options[:global]" do
712 736 should "authorize if user has at least one role that has this permission" do
713 737 @dlopper2 = User.find(5) #only Developper on a project, not Manager anywhere
714 738 @anonymous = User.find(6)
715 739 assert @jsmith.allowed_to?(:delete_issue_watchers, nil, :global => true)
716 740 assert ! @dlopper2.allowed_to?(:delete_issue_watchers, nil, :global => true)
717 741 assert @dlopper2.allowed_to?(:add_issues, nil, :global => true)
718 742 assert ! @anonymous.allowed_to?(:add_issues, nil, :global => true)
719 743 assert @anonymous.allowed_to?(:view_issues, nil, :global => true)
720 744 end
721 745 end
722 746 end
723 747
724 748 context "User#notify_about?" do
725 749 context "Issues" do
726 750 setup do
727 751 @project = Project.find(1)
728 752 @author = User.generate_with_protected!
729 753 @assignee = User.generate_with_protected!
730 754 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
731 755 end
732 756
733 757 should "be true for a user with :all" do
734 758 @author.update_attribute(:mail_notification, 'all')
735 759 assert @author.notify_about?(@issue)
736 760 end
737 761
738 762 should "be false for a user with :none" do
739 763 @author.update_attribute(:mail_notification, 'none')
740 764 assert ! @author.notify_about?(@issue)
741 765 end
742 766
743 767 should "be false for a user with :only_my_events and isn't an author, creator, or assignee" do
744 768 @user = User.generate_with_protected!(:mail_notification => 'only_my_events')
745 769 Member.create!(:user => @user, :project => @project, :role_ids => [1])
746 770 assert ! @user.notify_about?(@issue)
747 771 end
748 772
749 773 should "be true for a user with :only_my_events and is the author" do
750 774 @author.update_attribute(:mail_notification, 'only_my_events')
751 775 assert @author.notify_about?(@issue)
752 776 end
753 777
754 778 should "be true for a user with :only_my_events and is the assignee" do
755 779 @assignee.update_attribute(:mail_notification, 'only_my_events')
756 780 assert @assignee.notify_about?(@issue)
757 781 end
758 782
759 783 should "be true for a user with :only_assigned and is the assignee" do
760 784 @assignee.update_attribute(:mail_notification, 'only_assigned')
761 785 assert @assignee.notify_about?(@issue)
762 786 end
763 787
764 788 should "be false for a user with :only_assigned and is not the assignee" do
765 789 @author.update_attribute(:mail_notification, 'only_assigned')
766 790 assert ! @author.notify_about?(@issue)
767 791 end
768 792
769 793 should "be true for a user with :only_owner and is the author" do
770 794 @author.update_attribute(:mail_notification, 'only_owner')
771 795 assert @author.notify_about?(@issue)
772 796 end
773 797
774 798 should "be false for a user with :only_owner and is not the author" do
775 799 @assignee.update_attribute(:mail_notification, 'only_owner')
776 800 assert ! @assignee.notify_about?(@issue)
777 801 end
778 802
779 803 should "be true for a user with :selected and is the author" do
780 804 @author.update_attribute(:mail_notification, 'selected')
781 805 assert @author.notify_about?(@issue)
782 806 end
783 807
784 808 should "be true for a user with :selected and is the assignee" do
785 809 @assignee.update_attribute(:mail_notification, 'selected')
786 810 assert @assignee.notify_about?(@issue)
787 811 end
788 812
789 813 should "be false for a user with :selected and is not the author or assignee" do
790 814 @user = User.generate_with_protected!(:mail_notification => 'selected')
791 815 Member.create!(:user => @user, :project => @project, :role_ids => [1])
792 816 assert ! @user.notify_about?(@issue)
793 817 end
794 818 end
795 819
796 820 context "other events" do
797 821 should 'be added and tested'
798 822 end
799 823 end
800 824
801 825 def test_salt_unsalted_passwords
802 826 # Restore a user with an unsalted password
803 827 user = User.find(1)
804 828 user.salt = nil
805 829 user.hashed_password = User.hash_password("unsalted")
806 830 user.save!
807 831
808 832 User.salt_unsalted_passwords!
809 833
810 834 user.reload
811 835 # Salt added
812 836 assert !user.salt.blank?
813 837 # Password still valid
814 838 assert user.check_password?("unsalted")
815 839 assert_equal user, User.try_to_login(user.login, "unsalted")
816 840 end
817 841
818 842 if Object.const_defined?(:OpenID)
819 843
820 844 def test_setting_identity_url
821 845 normalized_open_id_url = 'http://example.com/'
822 846 u = User.new( :identity_url => 'http://example.com/' )
823 847 assert_equal normalized_open_id_url, u.identity_url
824 848 end
825 849
826 850 def test_setting_identity_url_without_trailing_slash
827 851 normalized_open_id_url = 'http://example.com/'
828 852 u = User.new( :identity_url => 'http://example.com' )
829 853 assert_equal normalized_open_id_url, u.identity_url
830 854 end
831 855
832 856 def test_setting_identity_url_without_protocol
833 857 normalized_open_id_url = 'http://example.com/'
834 858 u = User.new( :identity_url => 'example.com' )
835 859 assert_equal normalized_open_id_url, u.identity_url
836 860 end
837 861
838 862 def test_setting_blank_identity_url
839 863 u = User.new( :identity_url => 'example.com' )
840 864 u.identity_url = ''
841 865 assert u.identity_url.blank?
842 866 end
843 867
844 868 def test_setting_invalid_identity_url
845 869 u = User.new( :identity_url => 'this is not an openid url' )
846 870 assert u.identity_url.blank?
847 871 end
848 872
849 873 else
850 874 puts "Skipping openid tests."
851 875 end
852 876
853 877 end
General Comments 0
You need to be logged in to leave comments. Login now